注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Compose 动画边学边做 - 夏日彩虹

引言 Compose 在动画方面下足了功夫,提供了种类丰富的 API。但也正由于 API 种类繁多,如果想一气儿学下来,可能会消化不良导致似懂非懂。结合例子学习是一个不错的方法,本文就带大家边学边做,通过高仿微博长按点赞的彩虹动画,学习和实践 Compose ...
继续阅读 »

引言


Compose 在动画方面下足了功夫,提供了种类丰富的 API。但也正由于 API 种类繁多,如果想一气儿学下来,可能会消化不良导致似懂非懂。结合例子学习是一个不错的方法,本文就带大家边学边做,通过高仿微博长按点赞的彩虹动画,学习和实践 Compose 动画的相关技巧。















原版:微博长按点赞本文:掘金夏日主题
ezgif.com-gif-maker (23).gifezgif.com-gif-maker (24).gif


代码地址: github.com/vitaviva/An…



1. Compose 动画 API 概览


Compose 动画 API 在使用场景的维度上大体分为两类:高级别 API 和低级别 API。就像编程语
言分为高级语言和低级语言一样,这列高级低级指 API 的易用性:



  • 高级别 API 主打开箱即用,适用于一些 UI 元素的展现/退出/切换等常见场景,例如常见的 AnimatedVisibility 以及 AnimatedContent 等,它们被设计成 Composable 组件,可以在声明式布局中与其他组件融为一体。


//Text通过动画淡入
var editable by remember { mutableStateOf(true) }
AnimatedVisibility(visible = editable) {
Text(text = "Edit")
}


  • 低级别 API 使用成本更高但是更加灵活,可以更精准地实现 UI 元素个别属性的动画,多个低级别动画还可以组合实现更复杂的动画效果。最常见的低级别 animateFloatAsState 系列了,它们也是 Composable 函数,可以参与 Composition 的组合过程。


//动画改变 Box 透明度
val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f)
Box(
Modifier.fillMaxSize()
.graphicsLayer(alpha = alpha)
.background(Color.Red)
)


处于上层的 API 由底层 API 支撑实现,TargetBasedAnimation 是开发者可直接使用的最低级 API。Animatable 也是一个相对低级的 API,它是一个动画值的包装器,在协程中完成状态值的变化,向上提供对 animate*AsState 的支撑。它与其他 API 不同,是一个普通类而非一个 Composable 函数,所以可以在 Composable 之外使用,因此更具灵活性。本例子的动画主要也是依靠它完成的。


// Animtable 包装了一个颜色状态值
val color = remember { Animatable(Color.Gray) }
LaunchedEffect(ok) {
// animateTo 是个挂起函数,驱动状态之变化
color.animateTo(if (ok) Color.Green else Color.Gray)
}
Box(Modifier.fillMaxSize().background(color.value))

无论高级别 API 还是低级别 API ,它们都遵循状态驱动的动画方式,即目标对象通过观察状态变化实现自身的动画。


2. 长按点赞动画分解


长按点赞的动画乍看之下非常复杂,但是稍加分解后,不难发现它也是由一些常见的动画形式组合而成,因此我们可以对其拆解后逐个实现:



  • 彩虹动画:全屏范围内不断扩散的彩虹效果。可以通过半径不断扩大的圆形图案并依次叠加来实现

  • 表情动画:从按压位置不断抛出的表情。可以进一步拆解为三个动画:透明度动画,旋转动画以及抛物线轨迹动画。

  • 烟花动画:抛出的表情在消失时会有一个烟花炸裂的效果。其实就是围绕中心的八个圆点逐渐消失的过程,圆点的颜色提取自表情本身。


传统视图动画可以作用在 View 上,通过动画改变其属性;也可以在 onDraw 中通过不断重绘实现逐帧的动画效果。 Compose 也同样,我们可以在 Composable 中观察动画状态,通过重组实现动画效果(本质是改变 UI 组件的布局属性),也可以在 Canvas 中观察动画状态,只在重绘中实现动画(跳过组合)。这个例子的动画效果也需要通过 Canvas 的不断重绘来实现。
Compose 的 Canvas 也可以像 Composable 一样声明式的调用,基本写法如下:


Canvas {
...
drawRainbow(rainbowState) //绘制彩虹
...
drawEmoji(emojiState) //绘制表情
...
drawFlow(flowState) //绘制烟花
...
}

State 的变化会驱动 Canvas 会自动重绘,无需手动调用 invalidate 之类的方法。那么接下来针对彩虹、表情、烟花等各种动画的实现,我们的工作主要有两个:



  • 状态管理:定义相关 State,并在在动画中驱动其变化,如前所述这主要依靠 Animatable 实现。

  • 内容绘制:通过 Canvas API 基于当前状态绘制图案


3. 彩虹动画


3.1 状态管理


对于彩虹动画,唯一的动画状态就是圆的半径,其值从 0F 过渡到 screensize,圆形面积铺满至整个屏幕。我们使用 Animatable 包装这个状态值,调用 animateTo 方法可以驱动状态变化:


val raduis = Animatable(0f) //初始值 0f

radius.animateTo(
targetValue = screenSize, //目标值
animationSpec = tween(
durationMillis = duration, //动画时长
easing = FastOutSlowInEasing //动画衰减效果
)
)

animationSpec 用来指定动画规格,不同的动画规格决定了了状态值变化的节奏。Compose 中常用的创建动画规格的方法有以下几种,它们创建不同类型的动画规格,但都是 AnimationSpec 的子类:



  • tween:创建补间动画规格,补间动画是一个固定时长动画,比如上面例子中这样设置时长 duration,此外,tween 还能通过 easiing 指定动画衰减效果,后文详细介绍。

  • spring: 弹跳动画:spring 可以创建基于物理特性的弹簧动画,它通过设置阻尼比实现符合物理规律的动画衰减,因此不需要也不能指定动画时长

  • Keyframes:创建关键帧动画规格,关键帧动画可以逐帧设置当前动画的轨迹,后文会详细介绍。


AnimatedRainbow


ezgif.com-gif-maker (20).gif


要实现上面这样多个彩虹叠加的效果,我们还需有多个 Animtable 同时运行,在 Canvas 中依次对它们进行绘制。绘制彩虹除了依靠 Animtable 的状态值,还有 Color 等其他信息,因此我们定义一个 AnimatedRainbow 类保存包括 Animtable 在内的绘制所需的的状态


class AnimatedRainbow(
//屏幕尺寸(宽边长边大的一方)
private val screenSize: Float,
//RainbowColors是彩虹的候选颜色
private val color: Brush = RainbowColors.random(),
//动画时长
private val duration: Int = 3000
) {
private val radius = Animatable(0f)

suspend fun startAnim() = radius.animateTo(
targetValue = screenSize * 1.6f, // 关于 1.6f 后文说明
animationSpec = tween(
durationMillis = duration,
easing = FastOutSlowInEasing
)
)
}

animatedRainbows 列表


我们还需要一个集合来管理运行中的 AnimatedRainbow。这里我们使用 Compose 的 MutableStateList 作为集合容器,MutableStateList 中的元素发生增减时,可以被观察到,而当我们观察到新的 AnimatedRainbow 被添加时,为它启动动画。关键代码如下:


//MutableStateList 保存 AnimatedRainbow
val animatedRainbows = mutableStateListOf<AnimatedRainbow>()

//长按屏幕时,向列表加入 AnimtaedRainbow, 意味着增加一个新的彩虹
animatedRainbows.add(
AnimatedRainbow(
screenHeightPx.coerceAtLeast(screenWidthPx),
RainbowColors.random()
)
)

我们使用 LaunchedEffect + snapshotFlow 观察 animatedRainbows 的变化,代码如下:


LaunchedEffect(Unit) {
//监听到新添加的 AnimatedRainbow
snapshotFlow { animatedRainbows.lastOrNull() }
.filterNotNull()
.collect {
launch {
//启动 AnimatedRainbow 动画
val result = it.startAnim()
//动画结束后,从列表移除,避免泄露
if (result.endReason == AnimationEndReason.Finished) {
animatedRainbows.remove(it)
}
}
}
}

LaunchedEffectsnapshotFlow 都是 Compose 处理副作用的 API,由于不是本文重点就不做深入介绍了,这里只需要知道 LaunchedEffect 是一个提供了执行副作用的协程环境,而 snapshotFlow 可以将 animatedRainbows 中的变化转化为 Flow 发射给下游。当通过 Flow 收集到新加入的 AnimtaedRainbow 时,调用 startAnim 启动动画,这里充分发挥了挂起函数的优势,同步等待动画执行完毕,从 animatedRainbows 中移除 AnimtaedRainbow 即可。


值得一提的是,MutableStateList 的主要目的是在组合中观察列表的状态变化,本例子的动画不发生在组合中(只发生在重绘中),完全可以使用普通的集合类型替代,这里使用 MutableStateList 有两个好处:



  • 可以响应式地观察列表变化

  • 在 LaunchEffect 中响应变化并启动动画,协程可以随当前 Composable 的生命周期结束而终止,避免泄露。


3.2 内容绘制


我们在 Canvas 中遍历 animatedRainbows 所有的 AnimtaedRainbow 完成彩虹的绘制。彩虹的图形主要依靠 DrawScopedrawCircle 完成,比较简单。一点需要特别注意,彩虹动画结束时也要以一个圆形图案逐渐退出直至漏出底部内容,要实现这个效果,用到一个小技巧,我们的圆形绘制使用空心圆 (Stroke ) 而非 实心圆( Fill )


image.png



  • 出现彩虹:圆环逐渐铺满屏幕却不能漏出空心。这要求 StrokeWidth 宽度覆盖 ScreenSize,且始终保持 CircleRadius 的两倍

  • 结束彩虹:圆环空心部分逐渐覆盖屏幕。此时要求 CircleRadius 减去 StrokeWidth / 2 之后依然能覆盖 ScreenSize


基于以上原则,我们为 AnimatedRainbow 添加单个 AnnimatedRainbow 的绘制方法:


fun DrawScope.draw() {
drawCircle(
brush = color, //圆环颜色
center = center, //圆心:点赞位置
radius = radius.value,// Animtable 中变化的 radius 值,
style = Stroke((radius.value * 2).coerceAtMost(_screenSize)),
)
}

如上,StrokeWidth 覆盖 ScreenSize 之后无需继续增长,而 CircleRadius 的最终尺寸除去 ScreenSize 之外还要将 StrokeWidth 考虑进去,因此前面代码中将 Animtable 的 targetValue 设置为 ScreenSize 的 1.6 倍。


4. 表情动画


4.1 状态管理


表情动画又由三个子动画组成:旋转动画、透明度动画以及抛物线轨迹动画。像 AnimtaedRainbow 一样,我们定义 AnimatedEmoji 管理每个表情动画的状态,AnimatedEmoji 中通过多个 Animatable 分别管理前面提到的几个子动画


ezgif.com-gif-maker (21).gif


AnimatedEmoji


class AnimatedEmoji(
private val start: Offset, //表情抛点位置,即长按的屏幕位置
private val screenWidth: Float, //屏幕宽度
private val screenHeight: Float, //屏幕高度
private val duration: Int = 1500 //动画时长
) {

//抛出距离(x方向移动终点),在左右一个屏幕之间取随机数
private val throwDistance by lazy {
((start.x - screenWidth).toInt()..(start.x + screenWidth).toInt()).random()
}
//抛出高度(y方向移动终点),在屏幕顶端到抛点之间取随机数
private val throwHeight by lazy {
(0..start.y.toInt()).random()
}

private val x = Animatable(start.x)//x方向移动动画值
private val y = Animatable(start.y)//y方向移动动画值
private val rotate = Animatable(0f)//旋转动画值
private val alpha = Animatable(1f)//透明度动画值

suspend fun CoroutineScope.startAnim() {
async {
//执行旋转动画
rotate.animateTo(
360f, infiniteRepeatable(
animation = tween(_duration / 2, easing = LinearEasing),
repeatMode = RepeatMode.Restart
)
)
}
awaitAll(
async {
//执行x方向移动动画
x.animateTo(
throwDistance.toFloat(),
animationSpec = tween(durationMillis = duration, easing = LinearEasing)
)
},
async {
//执行y方向移动动画(上升)
y.animateTo(
throwHeight.toFloat(),
animationSpec = tween(
duration / 2,
easing = LinearOutSlowInEasing
)
)
//执行y方向移动动画(下降)
y.animateTo(
screenHeight,
animationSpec = tween(
duration / 2,
easing = FastOutLinearInEasing
)
)
},
async {
//执行透明度动画,最终状态是半透明
alpha.animateTo(
0.5f,
tween(duration, easing = CubicBezierEasing(1f, 0f, 1f, 0.8f))
)
}
)
}

infiniteRepeatable


上面代码中,旋转动画的 AnimationSpec 使用 infiniteRepeatable 创建了一个无限循环的动画,RepeatMode.Restart 表示它的从 0F 过渡到 360F 之后,再次重复这个过程。
除了旋转动画之外,其他动画都会在 duration 之后结束,它们分别在 async 中启动并行执行,awaitAll 等待它们全部结束。而由于旋转动画不会结束,因此不能放到 awaitAll 中,否则 startAnim 的调用方将永远无法恢复执行。


CubicBezierEasing


透明度动画中的 easing 指定了一个 CubicBezierEasing。easing 是动画衰减效果,即动画状态以何种速率逼近目标值。Compose 提供了几个默认的 Easing 类型可供使用,分别是:


//默认的 Easing 类型,以加速度起步,减速度收尾
val FastOutSlowInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 0.2f, 1.0f)
//匀速起步,减速度收尾
val LinearOutSlowInEasing: Easing = CubicBezierEasing(0.0f, 0.0f, 0.2f, 1.0f)
//加速度起步,匀速收尾
val FastOutLinearInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 1.0f, 1.0f)
//匀速接近目标值
val LinearEasing: Easing = Easing { fraction -> fraction }


上图横轴是时间,纵轴是逼近目标值的进度,可以看到除了 LinearEasing 之外,其它的的曲线变化都满足 CubicBezierEasing 三阶贝塞尔曲线,如果默认 Easing 不符合你的使用要求,可以使用 CubicBezierEasing,通过参数,自定义合适的曲线效果。比如例子中曲线如下:


image.png


这个曲线前半程状态值进度非常缓慢,临近时间结束才快速逼近最终状态。因为我们希望表情动画全程清晰可见,透明度的衰减尽量后置,默认 easiing 无法提供这种效果,因此我们自定义 CubicBezierEasing


抛物线动画


再来看一下抛物线动画的实现。通常我们可以借助抛物线公式,基于一些动画状态变量计算抛物线坐标来实现动画,但这个例子中我们借助 Easing 更加巧妙的实现了抛物线动画。
我们将抛物线动画拆解为 x 轴和 y 轴两个方向两个并行执行的位移动画,x 轴位移通过 LinearEasing 匀速完成,y 轴又拆分成两个过程



  • 上升到最高点,使用 LinearOutSlowInEasing 上升时速度加速衰减

  • 下落到屏幕底端,使用 FastOutLinearInEasing 下落时速度加速增加


上升和下降的 Easing 曲线互相对称,符合抛物线规律


animatedEmojis 列表


像彩虹动画一样,我们同样使用一个 MutableStateList 集合管理 AnimatedEmoji 对象,并在 LaunchedEffect 中监听新元素的插入,并执行动画。只是表情动画每次会批量增加多个


//MutableStateList 保存 animatedEmojis
val animatedEmojis = mutableStateListOf<AnimatedEmoji>()

//一次增加 EmojiCnt 个表情
animatedEmojis.addAll(buildList {
repeat(EmojiCnt) {
add(AnimatedEmoji(offset, screenWidthPx, screenHeightPx, res))
}
})

//监听 animatedEmojis 变化
LaunchedEffect(Unit) {
//监听到新加入的 EmojiCnt 个表情
snapshotFlow { animatedEmojis.takeLast(EmojiCnt) }
.flatMapMerge { it.asFlow() }
.collect {
launch {
with(it) {
startAnim()//启动表情动画,等待除了旋转动画外的所有动画结束
animatedEmojis.remove(it) //从列表移除
}
}
}
}

4.2 内容绘制


单个 AnimatedEmoji 绘制代码很简单,借助 DrawScopedrawImage 绘制表情素材即可


//当前 x,y 位移的位置
val offset get() = Offset(x.value, y.value)

//图片topLeft相对于offset的距离
val d by lazy { Offset(img.width / 2f, img.height / 2f) }


//绘制表情
fun DrawScope.draw() {
rotate(rotate.value, pivot = offset) {
drawImage(
image = img, //表情素材
topLeft = offset - dCenter,//当前位置
alpha = alpha.value, //透明度
)
}
}

注意旋转动画实际上是借助 DrawScoperotate 方法实现的,在 block 内部调用 drawImage 指定当前的 alphatopLeft 即可。


5. 烟花动画


5.1 状态管理


烟花动画紧跟在表情动画结束时发生,动画不涉及位置变化,主要是几个花瓣不断缩小的过程。花瓣用圆形绘制,动画状态值就是圆形半径,使用 Animatable 包装。


ezgif.com-gif-maker (22).gif


AnimatedFlower


烟花的绘制还要用到颜色等信息,我们定义 AnimatedFlower 保存包括 Animtable 在内的相关状态。


class AnimatedFlower(
private val intial: Float, //花瓣半径初始值,一般是表情的尺寸
private val duration: Int = 2500
) {
//花瓣半径
private val radius = Animatable(intial)

suspend fun startAnim() {
radius.animateTo(0f, keyframes {
durationMillis = duration
intial / 3 at 0 with FastOutLinearInEasing
intial / 5 at (duration * 0.95f).toInt()
})
}

keyframes


这里又出现了一种 AnimationSpec,即帧动画 keyframes,相对于 tween ,keyframes 可以更精确指定时间区间内的动画进度。比如代码中 radius / 3 at 0 表示 0 秒时状态值达到 intial / 3 ,相当于以初始值的 1/3 尺寸出现,这是一般的 tween 难以实现的。另外我们希望花瓣可以持久可见,所以使用 keyframe 确保时间进行到 95% 时,radius 的尺寸仍然清晰可见。



animatedFlower 列表


由于烟花动画设计是表情动画的延续,所以它紧跟表情动画执行,共享 CoroutienScope ,不需要借助 LaunchedEffect ,所以使用普通列表定义 animatedFlower 即可:


//animatedFlowers 使用普通列表创建
val animatedFlowers = mutableListOf<AnimatedFlower>()

launch {
with(it) {//表情动画执行
startAnim()
animatedEmojis.remove(it)
}
//创建 AnimatedFlower 动画
val anim = AnimatedFlower(
center = it.offset,
//使用 Palette 从表情图片提取烟花颜色
color = Palette.from(it.img.asAndroidBitmap()).generate().let {
arrayOf(
Color(it.getDominantColor(Color.Transparent.toArgb())),
Color(it.getVibrantColor(Color.Transparent.toArgb()))
)
},
initial = it.img.run { width.coerceAtLeast(height) / 2 }.toFloat()
)
animatedFlowers.add(anim) //添加进列表
anim.startAnim() //执行烟花动画
animatedFlowers.remove(anim) //移除动画
}

5.2 内容绘制


烟花的内容绘制,需要计算每个花瓣的位置,一共8个花瓣,各自位置计算如下:


//计算 sin45 的值
val sin by lazy { sin(Math.PI / 4).toFloat() }

val points
get() = run {
val d1 = initial - radius.value
val d2 = (initial - radius.value) * sin
arrayOf(
center.copy(y = center.y - d1), //0点方向
center.copy(center.x + d2, center.y - d2),
center.copy(x = center.x + d1),//3点方向
center.copy(center.x + d2, center.y + d2),
center.copy(y = center.y + d1),//6点方向
center.copy(center.x - d2, center.y + d2),
center.copy(x = center.x - d1),//9点方向
center.copy(center.x - d2, center.y - d2),
)
}

center 是烟花的中心位置,随着花瓣的变小,同时越来越远离中心位置,因此 d1d2 就是偏离 center 的距离,与 radius 大小成反比。
最后在 Canvas 中绘制这些 points 即可:


fun DrawScope.draw() {
points.forEachIndexed { index, point ->
drawCircle(color = color[index % 2], center = point, radius = radius.value)
}
}

6. 合体效果


最后我们定义一个 AnimatedLike 的 Composable ,整合上面代码


@Composable
fun AnimatedLike(modifier: Modifier = Modifier, state: LikeAnimState = rememberLikeAnimState()) {

LaunchedEffect(Unit) {
//监听新增表情
snapshotFlow { state.animatedEmojis.takeLast(EmojiCnt) }
.flatMapMerge { it.asFlow() }
.collect {
launch {
with(it) {
startAnim()
state.animatedEmojis.remove(it)
}
//添加烟花动画
val anim = AnimatedFlower(
center = it.offset,
color = Palette.from(it.img.asAndroidBitmap()).generate().let {
arrayOf(
Color(it.getDominantColor(Color.Transparent.toArgb())),
Color(it.getVibrantColor(Color.Transparent.toArgb()))
)
},
initial = it.img.run { width.coerceAtLeast(height) / 2 }.toFloat()
)
state.animatedFlowers.add(anim)
anim.startAnim()
state.animatedFlowers.remove(anim)
}
}
}


LaunchedEffect(Unit) {
//监听新增彩虹
snapshotFlow { state.animatedRainbows.lastOrNull() }
.filterNotNull()
.collect {
launch {
val result = it.startAnim()
if (result.endReason == AnimationEndReason.Finished) {
state.animatedRainbows.remove(it)
}
}
}
}

//绘制动画
Canvas(modifier.fillMaxSize()) {

//绘制彩虹
state.animatedRainbows.forEach { animatable ->
with(animatable) { draw() }
}

//绘制表情
state.animatedEmojis.forEach { animatable ->
with(animatable) { draw() }
}

//绘制烟花
state.animatedFlowers.forEach { animatable ->
with(animatable) { draw() }
}
}
}

我们使用 AnimatedLike 布局就可以为页面添加动画效果了,由于 Canvas 本身是基于 modifier.drawBehind 实现的,我们也可以将 AnimatedLike 改为 Modifier 修饰符使用,这里就不赘述了。
最后,复习一下本文例子中的内容:



  • Animatable :包装动画状态值,并且在协程中执行动画,同步返回动画结果

  • AnimationSpec:动画规格,可以配置动画时长、Easing 等,例子中用到了 tween,keyframes,infiniteRepeatable 等多个动画规格

  • Easing:动画状态值随时间变化的趋势,通常使用默认类型即可, 也可以基于 CubicBezierEasing 定制。


一个例子不可能覆盖到 Compose 所有的动画 API,但是我们只要掌握了上述几个关键知识点,再学习其他 API 就是水到渠成的事情了。


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

裁员新玩法,理想降薪表?最后哪个工资降得最多,就留下来哪个

近日,网上流传深圳某公司想给员工降薪,但不直接提出要求,而是让员工自己填写一份理想降薪表。公司也不说降薪数额、幅度、范围,员工根据自己的想法填写降薪幅度,让员工和员工之间竞标。最后,谁填写的工资降得最多,谁就留下。的确,今年以来,由于全球经济萎靡和疫情的持续影...
继续阅读 »

近日,网上流传深圳某公司想给员工降薪,但不直接提出要求,而是让员工自己填写一份理想降薪表。公司也不说降薪数额、幅度、范围,员工根据自己的想法填写降薪幅度,让员工和员工之间竞标。最后,谁填写的工资降得最多,谁就留下。


的确,今年以来,由于全球经济萎靡和疫情的持续影响,很多企业生产经营都受到不同程度的打击,同时今年又有超过一千万的大学毕业生进入社会,整个社会的就业环境确实很差,但是“竞标”降薪的这种搞法,无疑比直接裁员更让人觉得恶心。


按照正常逻辑,一般公司裁员,肯定是会优先裁掉能力较差的员工,最大限度的留住优质员工,而这种神操作带来的后果就是:劣币驱逐良币,能力差的员工自甘降薪的幅度肯定是大于能力强的员工,这无疑是要把优质员工直接送走。

运用这种变态方式裁员,只能说明在这家公司老板的心理,时常回荡着一句狠话:你们不干,有的是人干!而这也充分说明了这家公司毫无管理能力,基本上就是个小作坊模式,所谓的管理层,可能就老板一个光杆司令。


江山代有才人出,各领风骚数百年。现在的企业都在秀下限,大公司把裁员说成是“毕业”“为社会输送人才”,中等公司把降薪说成是“奋斗者计划”,小作坊更有趣直接是“竞标”降薪,员工都是无产者,是被拿捏的对象,没有自主权。企业这林林总总一系列让人眼花缭乱的操作,不是让员工之间竞争,而是老板们之间在进行一场“裁员表演秀”,目的就是看谁更没有下限,看谁能给劳动仲裁员枯燥乏味的工作带来更多的惊喜。


今年以来发生在劳动市场的各种奇葩事件,可以说是一种魔幻现实主义的真实写照,上演着让人瞠目结舌的职场淘汰潜规则明规则和各种职场“自愿”。


每个人都是社会的一员,每个人也都有活在世上最起码的尊严,如果企业确实经营出现困难,按照正常途径和相关规定,让员工体面的离开,何尝不是一种善良?而这种近乎侮辱的方式,只能让双方都不体面。

“我翻开历史一查,这历史没有年代,歪歪斜斜的每页上都写着‘仁义道德’四个字。我横竖睡不着,仔细看了半夜,才从字缝里看出字来,满本都写着两个字是‘吃人’!”——《狂人日记》

来源:http://www.163.com/dy/article/H8PD5INS0552IALL.html

收起阅读 »

Flutter——平台通信记录器 : channel_observer

前言 Flutter自身的定位,决定了基于其开发的项目在不断迭代的过程中,会有越来越多的平台通信。这些通信多来自各种平台端的sdk,而这些sdk一般是由不同人、团队甚至公司负责的,所以在sdk变动过程中,可能由于沟通不够及时、或者疏忽大意而未能及时通知到客户端...
继续阅读 »

前言


Flutter自身的定位,决定了基于其开发的项目在不断迭代的过程中,会有越来越多的平台通信。这些通信多来自各种平台端的sdk,而这些sdk一般是由不同人、团队甚至公司负责的,所以在sdk变动过程中,可能由于沟通不够及时、或者疏忽大意而未能及时通知到客户端。


例如,某个字段类型由int变为string,如果这个字段涉及到核心业务线那么可能会在测试中及时发现,而如果是在非核心业务线则不一定能及时发现。 这种错误,在抵达flutter侧时多为TypeCast Error。 初期, 我们的APM 会将此类错误进行上报,但是由于platform channel众多,很难确定是由哪个channel引起的,为此我们增加了channel observer用于记录最近n条的平台通信记录。 当APM再次上报类似错误后,会导出channel记录一同上报,藉此便可排查出bug点。


下面我简单的介绍一下具体原理与实现。


原理与实现


Flutter-平台通信简介


Flutter与平台端的通信连接层位于ServicesBinding中,其主要负责监听平台信息(系统/自定义)并将其转到defaultBinaryMessenger中处理,其内部初始化方法:


mixin ServicesBinding on BindingBase, SchedulerBinding {
@override
void initInstances() {
super.initInstances();
_instance = this;
_defaultBinaryMessenger = createBinaryMessenger();

//...无关代码
}

@protected
BinaryMessenger createBinaryMessenger() {
return const _DefaultBinaryMessenger._();
}
}

通过createBinaryMessenger 方法,创建了一个_DefaultBinaryMessenger对象,Flutter平台端通信都由此类来负责,其内部实现如下:


class _DefaultBinaryMessenger extends BinaryMessenger {
const _DefaultBinaryMessenger._();

///当我们在调用 xxxChannel.invokeMethod()方法时,最终会调用到send()方法,
@override
Future<ByteData?> send(String channel, ByteData? message) {
final Completer<ByteData?> completer = Completer<ByteData?>();

///channel : 通道名
///message : 你的参数
///通过engine中转到平台端
ui.PlatformDispatcher.instance.sendPlatformMessage(channel, message, (ByteData? reply){
try {
///reply : 平台端返回的结果
completer.complete(reply);
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'services library',
context: ErrorDescription('during a platform message response callback'),
));
}
});
return completer.future;
}

///此方法与上面的 send 方法相对应,是服务于平台端调用flutter的方法。
///
///当我们通过方法 :
/// channel.setMethodCallHandler(xxHandler)
///在flutter侧对 channel绑定一个回调用于处理平台端的调用时,
///最终会转到此方法。
///
///通过channelBuffers,会记录下你的channel name以及对应的handler,
///当平台端调用flutter方法时,会查找对应channel的handler并执行。
@override
void setMessageHandler(String channel, MessageHandler? handler) {
if (handler == null) {
ui.channelBuffers.clearListener(channel);
} else {
ui.channelBuffers.setListener(channel, (ByteData? data, ui.PlatformMessageResponseCallback callback) async {
ByteData? response;
try {
response = await handler(data);
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'services library',
context: ErrorDescription('during a platform message callback'),
));
} finally {
callback(response);
}
});
}
}
}

通过上面的了解我们便知道了入手点:只需增加一个_DefaultBinaryMessenger的代理类即可。


实现


首先,我们需要自定义WidgetsFlutterBinding以混入我们自定义的ServicesBinding :


class ChannelObserverBinding extends WidgetsFlutterBinding with ChannelObserverServicesBinding{
static WidgetsBinding ensureInitialized() {
if(WidgetsBinding.instance == null) {
ChannelObserverBinding();
}
return WidgetsBinding.instance!;
}
}

随后我们在自定义的ServicesBinding中,添加我们的代理类BinaryMessengerProxy


mixin ChannelObserverServicesBinding on BindingBase, ServicesBinding{

late BinaryMessengerProxy _proxy;

@override
BinaryMessenger createBinaryMessenger() {
_proxy = BinaryMessengerProxy(super.createBinaryMessenger());
return _proxy;
}
}

这样我们就可以在代理类中,对平台通信进行记录了:


class BinaryMessengerProxy extends BinaryMessenger{

BinaryMessengerProxy(this.origin);

///....省略代码

@override
Future<void> handlePlatformMessage(String channel, ByteData? data, PlatformMessageResponseCallback? callback) {
return origin.handlePlatformMessage(channel, data, callback);
}

///这里我们对flutter的调用做记录
@override
Future<ByteData?>? send(String channel, ByteData? message) async {
//记录channel通信
final ChannelModel model = _recordChannel(channel, message, true);
if(model.isAbnormal) {
return origin.send(channel, message);
}
final ByteData? result = await origin.send(channel, message);
_resolveResult(model, result);
return result;
}

///这里我们可以对平台端的调用做记录
/// * 对MessageHandler增加一个代理即可。
@override
void setMessageHandler(String channel, MessageHandler? handler) {
origin.setMessageHandler(channel, handler);
}

}

效果图


当我们捕捉到TypeCast error时,就可以将异常堆栈及channel的通信记录一同上传。开发同学便可借助堆栈信息和调用记录,定位到具体的异常channel


其他


项目地址


channel_observer_of_kit


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

视频直播技术干货:一文读懂主流视频直播系统的推拉流架构、传输协议等

本文由蘑菇街前端开发工程师“三体”分享,原题“蘑菇街云端直播探索——启航篇”,有修订。1、引言随着移动网络网速的提升与资费的降低,视频直播作为一个新的娱乐方式已经被越来越多的用户逐渐接受。特别是最近这几年,视频直播已经不仅仅被运用在传统的秀场、游戏类板块,更是...
继续阅读 »

本文由蘑菇街前端开发工程师“三体”分享,原题“蘑菇街云端直播探索——启航篇”,有修订。

1、引言

随着移动网络网速的提升与资费的降低,视频直播作为一个新的娱乐方式已经被越来越多的用户逐渐接受。特别是最近这几年,视频直播已经不仅仅被运用在传统的秀场、游戏类板块,更是作为电商的一种新模式得到迅速成长。

本文将通过介绍实时视频直播技术体系,包括常用的推拉流架构、传输协议等,让你对现今主流的视频直播技术有一个基本的认知。


2、蘑菇街的直播架构概览

目前蘑菇街直播推拉流主流程依赖于某云直播的服务。

云直播提供的推流方式有两种:

  • 1)一是通过集成SDK的方式进行推流(用于手机端开播);

  • 2)另一种是通过RTMP协议向远端服务器进行推流(用于PC开播端或专业控台设备开播)。

除去推拉流,该云平台也提供了云通信(IM即时通讯能力)和直播录制等云服务,组成了一套直播所需要的基础服务。

3、推拉流架构1:厂商SDK推拉流


如上题所示,这一种推拉流架构方式需要依赖腾讯这类厂商提供的手机互动直播SDK,通过在主播端APP和用户端APP都集成SDK,使得主播端和用户端都拥有推拉流的功能。

这种推拉流架构的逻辑原理是这样的:

  • 1)主播端和用户端分别与云直播的互动直播后台建立长连接;

  • 2)主播端通过UDT私有协议向互动直播后台推送音视频流;

  • 3)互动直播后台接收到音视频流后做转发,直接下发给与之建立连接的用户端。

这种推拉流方式有几点优势:

  • 1)只需要在客户端中集成SDK:通过手机就可以开播,对于主播开播的要求比较低,适合直播业务快速铺开;

  • 2)互动直播后台仅做转发:没有转码,上传CDN等额外操作,整体延迟比较低;

  • 3)主播端和用户端都可以作为音视频上传的发起方:适合连麦、视频会话等场景。

4、推拉流架构2:旁路推流

之前介绍了通过手机SDK推拉流的直播方式,看起来在手机客户端中观看直播的场景已经解决了。

那么问题来了:如果我想要在H5、小程序等其他场景下观看直播,没有办法接入SDK,需要怎么处理呢?

这个时候需要引入一个新的概念——旁路推流。

旁路推流指的是:通过协议转换将音视频流对接到标准的直播 CDN 系统上。

目前云直播开启旁路推流后,会通过互动直播后台将音视频流推送到云直播后台,云直播后台负责将收到音视频流转码成通用的协议格式并且推送到CDN,这样H5、小程序等端就可以通过CDN拉取到通用格式的音视频流进行播放了。


目前蘑菇街直播旁路开启的协议类型有HLS、FLV、RTMP三种,已经可以覆盖到所有的播放场景,在后续章节会对这几种协议做详细的介绍。

5、推拉流架构3:RTMP推流

随着直播业务发展,一些主播逐渐不满足于手机开播的效果,并且电商直播需要高保真地将商品展示在屏幕上,需要通过更加高清专业的设备进行直播,RTMP推流技术应运而生。

我们通过使用OBS等流媒体录影程序,对专业设备录制的多路流进行合并,并且将音视频流上传到指定的推流地址。由于OBS推流使用了RTMP协议,因此我们称这一种推流类型为RTMP推流。

我们首先在云直播后台申请到推流地址和秘钥,将推流地址和秘钥配置到OBS软件当中,调整推流各项参数,点击推流以后,OBS就会通过RTMP协议向对应的推流地址推送音视频流。

这一种推流方式和SDK推流的不同之处在于音视频流是直接被推送到了云直播后台进行转码和上传CDN的,没有直接将直播流转推到用户端的下行方式,因此相比SDK推流延迟会长一些。


总结下来RTMP推流的优势和劣势比较明显。

优势主要是:

  • 1)可以接入专业的直播摄像头、麦克风,直播的整体效果明显优于手机开播;

  • 2)OBS已经有比较多成熟的插件,比如目前蘑菇街主播常用YY助手做一些美颜的处理,并且OBS本身已经支持滤镜、绿幕、多路视频合成等功能,功能比手机端强大。

劣势主要是:

  • 1)OBS本身配置比较复杂,需要专业设备支持,对主播的要求明显更高,通常需要一个固定的场地进行直播;

  • 2)RTMP需要云端转码,并且本地上传时也会在OBS中配置GOP和缓冲,延时相对较长。

6、高可用架构方案:云互备

业务发展到一定阶段后,我们对于业务的稳定性也会有更高的要求,比如当云服务商服务出现问题时,我们没有备用方案就会出现业务一直等待服务商修复进度的问题。

因此云互备方案就出现了:云互备指的是直播业务同时对接多家云服务商,当一家云服务商出现问题时,快速切换到其他服务商的服务节点,保证业务不受影响。


直播业务中经常遇到服务商的CDN节点下行速度较慢,或者是CDN节点存储的直播流有问题,此类问题有地域性,很难排查,因此目前做的互备云方案,主要是备份CDN节点。

目前蘑菇街整体的推流流程已经依赖了原有云平台的服务,因此我们通过在云直播后台中转推一路流到备份云平台上,备份云在接收到了直播流后会对流转码并且上传到备份云自身的CDN系统当中。一旦主平台CDN节点出现问题,我们可以将下发的拉流地址替换成备份云拉流地址,这样就可以保证业务快速修复并且观众无感知。

7、视频直播数据流解封装原理

介绍流协议之前,先要介绍我们从云端拿到一份数据,要经过几个步骤才能解析出最终需要的音视频数据。


如上图所示,总体来说,从获取到数据到最终将音视频播放出来要经历四个步骤。

*第一步:*解协议。

协议封装的时候通常会携带一些头部描述信息或者信令数据,这一部分数据对我们音视频播放没有作用,因此我们需要从中提取出具体的音视频封装格式数据,我们在直播中常用的协议有HTTP和RTMP两种。

*第二步:*解封装。

获取到封装格式数据以后需要进行解封装操作,从中分别提取音频压缩流数据和视频压缩流数据,封装格式数据我们平时经常见到的如MP4、AVI,在直播中我们接触比较多的封装格式有TS、FLV。

*第三步:*解码音视频。

到这里我们已经获取了音视频的压缩编码数据。

我们日常经常听到的视频压缩编码数据有H.26X系列和MPEG系列等,音频编码格式有我们熟悉的MP3、ACC等。

之所以我们能见到如此多的编码格式,是因为各种组织都提出了自己的编码标准,并且会相继推出一些新的议案,但是由于推广和收费问题,目前主流的编码格式也并不多。

获取压缩数据以后接下来需要将音视频压缩数据解码,获取非压缩的颜色数据和非压缩的音频抽样数据。颜色数据有我们平时熟知的RGB,不过在视频的中常用的颜色数据格式是YUV,指的是通过明亮度、色调、饱和度确定一个像素点的色值。音频抽样数据通常使用的有PCM。

*第四步:*音视频同步播放。

最后我们需要比对音视频的时间轴,将音视频解码后的数据交给显卡声卡同步播放。

8、视频直播传输协议1:HLS

首先介绍一下HLS协议。HLS是HTTP Live Streaming的简写,是由苹果公司提出的流媒体网络传输协议。

从名字可以明显看出:这一套协议是基于HTTP协议传输的。

说到HLS协议:首先需要了解这一种协议是以视频切片的形式分段播放的,协议中使用的切片视频格式是TS,也就是我们前文提到的封装格式。

在我们获取TS文件之前:协议首先要求请求一个M3U8格式的文件,M3U8是一个描述索引文件,它以一定的格式描述了TS地址的指向,我们根据M3U8文件中描述的内容,就可以获取每一段TS文件的CDN地址,通过加载TS地址分段播放就可以组合出一整段完整的视频。


使用HLS协议播放视频时:首先会请求一个M3U8文件,如果是点播只需要在初始化时获取一次就可以拿到所有的TS切片指向,但如果是直播的话就需要不停地轮询M3U8文件,获取新的TS切片。

获取到M3U8后:我们可以看一下里面的内容。首先开头是一些通用描述信息,比如第一个分片序列号、片段最大时长和总时长等,接下来就是具体TS对应的地址列表。如果是直播,那么每次请求M3U8文件里面的TS列表都会随着最新的直播切片更新,从而达到直播流播放的效果。


HLS这种切片播放的格式在点播播放时是比较适用的,一些大的视频网站也都有用这一种协议作为播放方案。

首先:切片播放的特性特别适用于点播播放中视频清晰度、多语种的热切换。比如我们播放一个视频,起初选择的是标清视频播放,当我们看了一半觉得不够清晰,需要换成超清的,这时候只需要将标清的M3U8文件替换成超清的M3U8文件,当我们播放到下一个TS节点时,视频就会自动替换成超清的TS文件,不需要对视频做重新初始化。

其次:切片播放的形式也可以比较容易地在视频中插入广告等内容。


在直播场景下,HLS也是一个比较常用的协议,他最大的优势是苹果大佬的加持,对这一套协议推广的比较好,特别是移动端。将M3U8文件地址喂给video就可以直接播放,PC端用MSE解码后大部分浏览器也都能够支持。但是由于其分片加载的特性,直播的延迟相对较长。比如我们一个M3U8有5个TS文件,每个TS文件播放时长是2秒,那么一个M3U8文件的播放时长就是10秒,也就是说这个M3U8播放的直播进度至少是10秒之前的,这对于直播场景来说是一个比较大的弊端。


HLS中用到的TS封装格式,视频编码格式是通常是H.264或MPEG-4,音频编码格式为AAC或MP3。

一个ts由多个定长的packtet组成,通常是188个字节,每个packtet有head和payload组成,head中包含一些标识符、错误信息、包位置等基础信息。payload可以简单理解为音视频信息,但实际上下层还有还有两层封装,将封装解码后可以获取到音视频流的编码数据。


9、视频直播传输协议2:HTTP-FLV

HTTP-FLV协议,从名字上就可以明显看出是通过HTTP协议来传输FLV封装格式的一种协议。

FLV是Flash Video的简写,是一种文件体积小,适合在网络上传输的封包方式。FlV的视频编码格式通常是H.264,音频编码是ACC或MP3。


HTTP-FLV在直播中是通过走HTTP长连接的方式,通过分块传输向请求端传递FLV封包数据。


在直播中,我们通过HTTP-FLV协议的拉流地址可以拉取到一段chunked数据。

打开文件后可以读取到16进制的文件流,通过和FLV包结构对比,可以发现这些数据就是我们需要的FLV数据。

首先开头是头部信息:464C56转换ASCII码后是FLV三个字符,01指的是版本号,05转换为2进制后第6位和第8位分别代表是否存在音频和视频,09代表头部长度占了几个字节。

后续就是正式的音视频数据:是通过一个个的FLV TAG进行封装,每一个TAG也有头部信息,标注这个TAG是音频信息、视频信息还是脚本信息。我们通过解析TAG就可以分别提取音视频的压缩编码信息。

FLV这一种格式在video中并不是原生支持的,我们要播放这一种格式的封包格式需要通过MSE对影视片的压缩编码信息进行解码,因此需要浏览器能够支持MSE这一API。由于HTTP-FLV的传输是通过长连接传输文件流的形式,需要浏览器支持Stream IO或者fetch,对于浏览器的兼容性要求会比较高。

FLV在延迟问题上相比切片播放的HLS会好很多,目前看来FLV的延迟主要是受编码时设置的GOP长度的影响。

这边简单介绍一下GOP:在H.264视频编码的过程中,会生成三种帧类型:I帧、B帧和P帧。I帧就是我们通常说的关键帧,关键帧内包括了完整的帧内信息,可以直接作为其他帧的参考帧。B帧和P帧为了将数据压缩得更小,需要由其他帧推断出帧内的信息。因此两个I帧之间的时长也可以被视作最小的视频播放片段时长。从视频推送的稳定性考虑,我们也要求主播将关键帧间隔设置为定长,通常是1-3秒,因此除去其他因素,我们的直播在播放时也会产生1-3秒的延时。


10、视频直播传输协议3:RTMP

RTMP协议实际可以与HTTP-FLV协议归做同一种类型。

他们的封包格式都是FlV,但HTTP-FLV使用的传输协议是HTTP,RTMP拉流使用RTMP作为传输协议。

RTMP是Adobe公司基于TCP做的一套实时消息传输协议,经常与Flash播放器匹配使用。

RTMP协议的优缺点非常明显。

RTMP协议的优点主要是:

  • 1)首先和HTTP-FLV一样,延迟比较低;

  • 2)其次它的稳定性非常好,适合长时间播放(由于播放时借用了Flash player强大的功能,即使开多路流同时播放也能保证页面不出现卡顿,很适合监控等场景)。

但是Flash player目前在web端属于墙倒众人推的境地,主流浏览器渐渐都表示不再支持Flash player插件,在MAC上使用能够立刻将电脑变成烧烤用的铁板,资源消耗很大。在移动端H5基本属于完全不支持的状态,兼容性是它最大的问题。


11、视频直播传输协议4:MPEG-DASH

MPEG-DASH这一协议属于新兴势力,和HLS一样,都是通过切片视频的方式进行播放。

他产生的背景是早期各大公司都自己搞自己的一套协议。比如苹果搞了HLS、微软搞了 MSS、Adobe还搞了HDS,这样使用者需要在多套协议封装的兼容问题上痛苦不堪。

于是大佬们凑到一起,将之前各个公司的流媒体协议方案做了一个整合,搞了一个新的协议。

由于同为切片视频播放的协议,DASH优劣势和HLS类似,可以支持切片之间多视频码率、多音轨的切换,比较适合点播业务,在直播中还是会有延时较长的问题。


12、如何选择最优的视频直播传输协议

视频直播协议选择非常关键的两点,在前文都已经有提到了,即低延时和更优的兼容性。

首先从延时角度考虑:不考虑云端转码以及上下行的消耗,HLS和MPEG-DASH通过将切片时长减短,延时在10秒左右;RTMP和FLV理论上延时相当,在2-3秒。因此在延时方面HLS ≈ DASH > RTMP ≈ FLV。

从兼容性角度考虑:HLS > FLV > RTMP,DASH由于一些项目历史原因,并且定位和HLS重复了,暂时没有对其兼容性做一个详尽的测试,被推出了选择的考虑范围。

综上所述:我们可以通过动态判断环境的方式,选择当前环境下可用的最低延迟的协议。大致的策略就是优先使用HTTP-FLV,使用HLS作为兜底,在一些特殊需求场景下通过手动配置的方式切换为RTMP。

对于HLS和HTTP-FLV:我们可以直接使用 hls.jsflv.js 做做解码播放,这两个库内部都是通过MSE做的解码。首先根据视频封装格式提取出对应的音视频chunk数据,在MediaSource中分别对音频和视频创建SourceBuffer,将音视频的编码数据喂给SourceBuffer后SourceBuffer内部会处理完剩下的解码和音视频对齐工作,最后MediaSource将Video标签中的src替换成MediaSource 对象进行播放。


在判断播放环境时我们可以参照flv.js内部的判断方式,通过调用MSE判断方法和模拟请求的方式判断MSE和StreamIO是否可用:

// 判断MediaSource是否被浏览器支持,H.264视频编码和Acc音频编码是否能够被支持解码

window.MediaSource && window.MediaSource.isTypeSupported('video/mp4; codecs="avc1.42E01E,mp4a.40.2"');

如果FLV播放不被支持的情况下:需要降级到HLS,这时候需要判断浏览器环境是否在移动端,移动端通常不需要 hls.js 通过MSE解码的方式进行播放,直接将M3U8的地址交给video的src即可。如果是PC端则判断MSE是否可用,如果可用就使用hls.js解码播放。

这些判读可以在自己的逻辑里提前判断后去拉取对应解码库的CDN,而不是等待三方库加载完成后使用三方库内部的方法判断,这样在选择解码库时就可以不把所有的库都拉下来,提高加载速度。

13、同层播放如何解决


电商直播需要观众操作和互动的部分比起传统的直播更加多,因此产品设计的时候很多的功能模块会悬浮在直播视频上方减少占用的空间。这个时候就会遇到一个移动端播放器的老大难问题——同层播放。

同层播放问题:是指在移动端H5页面中,一些浏览器内核为了提升用户体验,将video标签被劫持替换为native播放器,导致其他元素无法覆盖于播放器之上。

比如我们想要在直播间播放器上方增加聊天窗口,将聊天窗口通过绝对定位提升z-index置于播放器上方,在PC中测试完全正常。但在移动端的一些浏览器中,video被替换成了native播放器,native的元素层级高于我们的普通元素,导致聊天窗口实际显示的时候在播放器下方。

要解决这个问题,首先要分多个场景。

首先在iOS系统中:正常情况下video标签会自动被全屏播放,但iOS10以上已经原生提供了video的同层属性,我们在video标签上增加playsinline/webkit-playsinline可以解决iOS系统中大部分浏览器的同层问题,剩下的低系统版本的浏览器以及一些APP内的webview容器(譬如微博),用上面提的属性并不管用,调用三方库iphone-inline-video可以解决大部分剩余问题。

在Android端:大部分腾讯系的APP内置的webview容器用的都是X5内核,X5内核会将video替换成原生定制的播放器已便于增强一些功能。X5也提供了一套同层的方案(该方案官方文档链接已无法打开),给video标签写入X5同层属性也可以在X5内核中实现内联播放。不过X5的同层属性在各个X5版本中表现都不太一样(比如低版本X5中需要使用X5全屏播放模式才能保证MSE播放的视频同层生效),需要注意区分版本。

在蘑菇街App中,目前集成的X5内核版本比较老,在使用MSE的情况下会导致X5同层参数不生效。但如果集成新版本的X5内核,需要对大量的线上页面做回归测试,成本比较高,因此提供了一套折中的解决方案。通过在页面URL中增加一个开关参数,容器读取到参数以后会将X5内核降级为系统原生的浏览器内核,这样可以在解决浏览器视频同层问题的同时也将内核变动的影响范围控制在单个页面当中。

来源:http://www.blogjava.net/jb2011/archive/2022/05/31/450754.html

收起阅读 »

节日献礼:Flutter图片库重磅开源!

去年,闲鱼新一代图片库 PowerImage 在经过一系列灰度、问题修复、代码调优后,已全量稳定应用于闲鱼。相对于上一代 IFImage,PowerImage 经过进一步的演进,适应了更多的业务场景与最新的 flutter 特性,解决了一系列痛点:比如,因为完...
继续阅读 »

背景:

去年,闲鱼新一代图片库 PowerImage 在经过一系列灰度、问题修复、代码调优后,已全量稳定应用于闲鱼。相对于上一代 IFImage,PowerImage 经过进一步的演进,适应了更多的业务场景与最新的 flutter 特性,解决了一系列痛点:比如,因为完全抛弃了原生的 ImageCache,在与原生图片混用的场景下,会让一些低频的图片反而占用了缓存;比如,我们在模拟器上无法展示图片;比如我们在相册中,需要在图片库之外再搭建图片通道。

简介:

PowerImage 是一个充分利用 native 原生图片库能力、高扩展性的flutter图片库。我们巧妙地将外接纹理与 ffi 方案组合,以更贴近原生的设计,解决了一系列业务痛点。

能力特点:

  • 支持加载 ui.Image 能力。在基于外接纹理的方案中,使用方无法拿到真正的 ui.Image 去使用,这导致图片库在这种特殊的使用场景下无能为力。

  • 支持图片预加载能力。正如原生precacheImage一样。这在某些对图片展示速度要求较高的场景下非常有用。

  • 新增纹理缓存,与原生图片库缓存打通!统一图片缓存,避免原生图片混用带来的内存问题。

  • 支持模拟器。在 flutter-1.23.0-18.1.pre之前的版本,模拟器无法展示 Texture Widget。

  • 完善自定义图片类型通道。解决业务自定义图片获取诉求。

  • 完善的异常捕获与收集。

  • 支持动图。(来自淘特的PR)

Flutter 原生方案:

在介绍新方案开始之前,先简单回忆一下 flutter 原生图片方案。


原生 Image Widget 先通过 ImageProvider 得到 ImageStream,通过监听它的状态,进行各种状态的展示。比如frameBuilderloadingBuilder,最终在图片加载成功后,会 rebuildRawImageRawImage 会通过 RenderImage 来绘制,整个绘制的核心是 ImageInfo 中的 ui.Image

  • Image:负责图片加载的各个状态的展示,如加载中、失败、加载成功展示图片等。

  • ImageProvider:负责 ImageStream 的获取,比如系统内置的 NetworkImage、AssetImage 等。

  • ImageStream:图片资源加载的对象。

在梳理 flutter 原生图片方案之后,我们发现是不是有机会在某个环节将 flutter 图片和 native 以原生的方式打通?

新一代方案:

我们巧妙地将 FFi 方案与外接纹理方案组合,解决了一系列业务痛点。

FFI:

正如开头说的那些问题,Texture 方案有些做不到的事情,这需要其他方案来互补,这其中核心需要的就是 ui.Image。我们把 native 内存地址、长度等信息传递给 flutter 侧,用于生成 ui.Image

首先 native 侧先获取必要的参数(以 iOS 为例):

_rowBytes = CGImageGetBytesPerRow(cgImage);
CGDataProviderRef dataProvider = CGImageGetDataProvider(cgImage);
CFDataRef rawDataRef = CGDataProviderCopyData(dataProvider);
_handle = (long)CFDataGetBytePtr(rawDataRef);
NSData *data = CFBridgingRelease(rawDataRef);
self.data = data;
_length = data.length;

dart 侧拿到后

@override  FutureOr createImageInfo(Map map) {
Completer completer = Completer();
int handle = map['handle'];
int length = map['length'];
int width = map['width'];
int height = map['height'];
int rowBytes = map['rowBytes'];
ui.PixelFormat pixelFormat = ui.PixelFormat.values[map['flutterPixelFormat'] ?? 0];
pointer.asTypedList(length);
ui.decodeImageFromPixels(pixels, width, height, pixelFormat,(ui.Image image) {
ImageInfo imageInfo = ImageInfo(image: image);
completer.complete(imageInfo);     //释放 native 内存
PowerImageLoader.instance.releaseImageRequest(options);
}, rowBytes: rowBytes);
return completer.future;
}

我们可以通过 ffi 拿到 native 内存,从而生成 ui.Image。这里有个问题,虽然通过 ffi 能直接获取 native 内存,但是由于 decodeImageFromPixels 会有内存拷贝,在拷贝解码后的图片数据时,内存峰值会更加严重。

这里有两个优化方向:

  1. 解码前的图片数据给 flutter,由 flutter 提供的解码器解码,从而削减内存拷贝峰值。

  2. 与 flutter 官方讨论,尝试从内部减少这次内存拷贝。

FFI 这种方式适合轻度使用、特殊场景使用,支持这种方式可以解决无法获取 ui.Image 的问题,也可以在模拟器上展示图片(flutter <= 1.23.0-18.1.pre),并且图片缓存将完全交给 ImageCache 管理。

Texture:

Texture 方案与原生结合有一些难度,这里涉及到没有 ui.Image 只有 textureId。这里有几个问题需要解决:

问题一:Image Widget 需要 ui.Image 去 build RawImage 从而绘制,这在本文前面的Flutter 原生方案介绍中也提到了。 问题二:ImageCache 依赖 ImageInfo 中 ui.Image 的宽高进行 cache 大小计算以及缓存前的校验。 问题三:native 侧 texture 生命周期管理

都有解决方案:

问题一:通过自定义 Image 解决,透出 imageBuilder 来让外部自定义图片 widget 问题二:为 Texture 自定义 ui.image,如下:

import 'dart:typed_data';
import 'dart:
ui'
as ui show Image;
import 'dart:ui';
class TextureImage implements ui.Image {
    int _width;
    int _height;
    int textureId;
   TextureImage(this.textureId, int width, int height)     : _width = width,       _height = height;
    @override void dispose() {
    // TODO: implement dispose }
     @override int get height => _height;
     @override Future
toByteData(     {ImageByteFormat format = ImageByteFormat.rawRgba}) {  
         // TODO: implement toByteData  
             throw UnimplementedError();
     }
     @override int get width => _width;
}

这样的话,TextureImage 实际上就是个壳,仅仅用来计算 cache 大小。 实际上,ImageCache 计算大小,完全没必要直接接触到 ui.Image,可以直接找 ImageInfo 取,这样的话就没有这个问题了。这个问题可以具体看 @皓黯 的 ISSUE 与 PR。

问题三:关于 native 侧感知 flutter image 释放时机的问题

修改的 ImageCache 释放如下(部分代码):

typedef void HasRemovedCallback(dynamic key, dynamic value);

class RemoveAwareMap<K, V> implements Map<K, V> {
HasRemovedCallback hasRemovedCallback;
...
}
//------
final RemoveAwareMap<Object, _PendingImage> _pendingImages = RemoveAwareMap<Object, _PendingImage>();
//------
void hasImageRemovedCallback(dynamic key, dynamic value) {
if (key is ImageProviderExt) {
waitingToBeCheckedKeys.add(key);
}
if (isScheduledImageStatusCheck) return;
isScheduledImageStatusCheck = true;
//We should do check in MicroTask to avoid if image is remove and add right away
scheduleMicrotask(() {
waitingToBeCheckedKeys.forEach((key) {
if (!_pendingImages.containsKey(key) &&
!_cache.containsKey(key) &&
!_liveImages.containsKey(key)) {
if (key is ImageProviderExt) {
key.dispose();
}
}
});
waitingToBeCheckedKeys.clear();
isScheduledImageStatusCheck = false;
});
}

整体架构:

我们将两种解决方案非常优雅地结合在了一起:


我们抽象出了 PowerImageProvider ,对于 external(ffi)、texture,分别生产自己的 ImageInfo 即可。它将通过对 PowerImageLoader 的调用,提供统一的加载与释放能力。

蓝色实线的 ImageExt 即为自定义的 Image Widget,为 texture 方式透出了 imageBuilder。

蓝色虚线 ImageCacheExt 即为 ImageCache 的扩展,仅在 flutter < 2.2.0 版本才需要,它将提供 ImageCache 释放时机的回调。

这次,我们也设计了超强的扩展能力。除了支持网络图、本地图、flutter 资源、native 资源外,我们提供了自定义图片类型的通道,flutter 可以传递任何自定义的参数组合给 native,只要 native 注册对应类型 loader,比如「相册」这种场景,使用方可以自定义 imageType 为 album ,native 使用自己的逻辑进行加载图片。有了这个自定义通道,甚至图片滤镜都可以使用 PowerImage 进行展示刷新。

除了图片类型的扩展,渲染类型也可进行自定义。比如在上面 ffi 中说的,为了降低内存拷贝带来的峰值问题,使用方可以在 flutter 侧进行解码,当然这需要 native 图片库提供解码前的数据。

数据:

FFI vs Texture:


机型:iPhone 11 Pro;图片:300 张网络图;行为:在listView中手动滚动到底部再滚动到顶部;native Cache20 maxMemoryCount; flutter Cache30MBflutter version 2.5.3; release 模式下

这里有两个现象:

FFI:   186MB波动Texture: 194MB波动

在 2.5.3 版本中,Texture 方案与 FFI,在内存水位上差异不大,内存波动上面与 flutter 1.22 结论相反。

图中棋格图,为打开 checkerboardRasterCacheImages 后所展示,可以看出,ffi方案会缓存整个cell,而texture方案,只有cell中的文字被缓存,RasterCache 会使得 ffi 在流畅度方面会有一定优势。

滚动流畅性分析:


设备: Android OnePlus 8t,CPU和GPU进行了锁频。case: GridView每行4张图片,300张图片,从上往下,再从下往上,滑动幅度从500,1000,1500,2000,2500,5轮滑动。重复20次。方式: for i in {1..20}; do flutter drive --target=test_driver/app.dart --profile; done 跑数据,获取TimeLine数据并分析。
复制代码

结论:

  • UI thread 耗时 texture 方式最好,PowerImage 略好于 IFImage,FFI方式波动比较大。

  • Raster thread 耗时 PowerImage 好于 IFImage。Origin 原生方式好是因为对图片 resize了,其他方式加载的是原图。

更精简的代码:


dart 侧代码有较大幅度的减少,这归功于技术方案贴合 flutter 原生设计,我们与原生图片共用较多代码。

FFI 方案补全了外接纹理的不足,遵循原生 Image 的设计规范,不仅让我们享受到 ImageCache 带来的统一管理,也带来了更精简的代码。

单测:


为了保证核心代码的稳定性,我们有着较为完善的单测,行覆盖率接近95%。

关于开源:

我们期待通过社区的力量让 PowerImage 更加完善与强大,也希望 PowerImage 能为大家在工程研发中带来收益。

Issues:

关于 issue,我们希望大家在使用 PowerImage 遇到问题与诉求时,积极交流,提出 issue 时尽可能提供详细的信息,以减少沟通成本。在提出 issue 前,请确保已阅读 readme。


对于 bug 的 issue,我们自定义了模板(Bug report),可以方便地填一些必要的信息。其他类型则可以选择 Open a blank issue

我们每周会花部分时间统一处理 issues,也期待大家的讨论与 PR。

PR:

为了保持 PowerImage 核心功能的稳定性,我们有着完善的单测,行覆盖率达到了 95%(power_image库)。

在提交PR时,请确保所提交的代码被单测覆盖到,并且涉及到的单测代码请同时提交。


得益于 Github 的 Actions 能力,我们在主分支 push 代码、对主分支进行 PR 操作时,都会触发 flutter test任务,只有单测通过才可合入。

未来:

开源是 PowerImage 的开始,而不是结束,PowerImage 可做的事情还有很多,有趣而丰富。比如第一个 issue 中描述的 loadingBuilder 如何实现?比如 ffi 方案如何支持动图?再比如Kotlin和Swift···

PowerImage 未来将持续演进,在当前 texture 方案与 ffi 方案共存的情况下,伴随着 flutter 本身的迭代,我们将更倾向于向 ffi 发展,正如在上文的对比中, ffi 方案可以天然享用 raster cache 所带来的流畅度的优势。

PowerImage 也会持续追随 flutter 的脚步,以始终贴合原生的设计理念,不断进步,我们希望更多的同学加入进来,共同成长。

其他四个Flutter开源项目: 闲鱼技术**公众号-闲鱼开源

PowerImage相关链接:

GitHub:(✅star🌟)

github.com/alibaba/pow…

Flutter pub:(✅like👍)

pub.dev/packages/po…

作者:闲鱼技术——新宿

收起阅读 »

MapperStruct:一款CURD神器

前言 相信绝大多数的业务开发同学,日常的工作都离不开写getter、setter方法。要么是将下游的RPC结果通过getter、setter方法进行获取组装。要么就是将自己系统内部的处理结果通过getter、setter方法处理成前端所需要的VO对象。publ...
继续阅读 »

前言

相信绝大多数的业务开发同学,日常的工作都离不开写getter、setter方法。要么是将下游的RPC结果通过getter、setter方法进行获取组装。要么就是将自己系统内部的处理结果通过getter、setter方法处理成前端所需要的VO对象。

public UserInfoVO originalCopyItem(UserDTO userDTO){
   UserInfoVO userInfoVO = new UserInfoVO();
   userInfoVO.setUserName(userDTO.getName());
   userInfoVO.setAge(userDTO.getAge());
   userInfoVO.setBirthday(userDTO.getBirthday());
   userInfoVO.setIdCard(userDTO.getIdCard());
   userInfoVO.setGender(userDTO.getGender());
   userInfoVO.setIsMarried(userDTO.getIsMarried());
   userInfoVO.setPhoneNumber(userDTO.getPhoneNumber());
   userInfoVO.setAddress(userDTO.getAddress());
   return userInfoVO;
}

传统的方法一般是采用硬编码,将每个对象的值都逐一设值。当然为了偷懒也会有采用一些BeanUtil简约代码的方式:

public UserInfoVO utilCopyItem(UserDTO userDTO){
   UserInfoVO userInfoVO = new UserInfoVO();
   //采用反射、内省机制实现拷贝
   BeanUtils.copyProperties(userDTO, userInfoVO);
   return userInfoVO;
}

但是,像BeanUtils这类通过反射、内省等实现的框架,在速度上会带来比较严重的影响。尤其是对于一些大字段、大对象而言,这个速度的缺陷就会越明显。针对速度这块我还专门进行了测试,对普通的setter方法、BeanUtils的拷贝以及本次需要介绍的mapperStruct进行了一次对比。得到的耗时结果如下所示:(具体的运行代码请见附录)

运行次数setter方法耗时BeanUtils拷贝耗时MapperStruct拷贝耗时
12921528(1)3973292(1.36)2989942(1.023)
102362724(1)66402953(28.10)3348099(1.417)
1002500452(1)71741323(28.69)2120820(0.848)
10003187151(1)157925125(49.55)5456290(1.711)
100005722147(1)300814054(52.57)5229080(0.913)
10000019324227(1)244625923(12.65)12932441(0.669)

以上单位均为毫微秒。括号内的为当前组件同Setter比较的比值。可以看到BeanUtils的拷贝耗时基本为setter方法的十倍、二十倍以上。而MapperStruct方法拷贝的耗时,则与setter方法相近。由此可见,简单的BeanUtils确实会给服务的性能带来很大的压力。而MapperStruct拷贝则可以很好的解决这个问题。

使用教程

maven依赖

首先要导入mapStruct的maven依赖,这里我们选择最新的版本1.5.0.RC1。

...
<properties>
   <org.mapstruct.version>1.5.0.RC1</org.mapstruct.version>
</properties>
...

//mapStruct maven依赖
<dependencies>
   <dependency>
       <groupId>org.mapstruct</groupId>
       <artifactId>mapstruct</artifactId>
       <version>${org.mapstruct.version}</version>
   </dependency>
</dependencies>
...
   
//编译的组件需要配置
<build>
   <plugins>
       <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-compiler-plugin</artifactId>
           <version>3.8.1</version>
           <configuration>
               <source>1.8</source> <!-- depending on your project -->
               <target>1.8</target> <!-- depending on your project -->
               <annotationProcessorPaths>
                   <path>
                       <groupId>org.mapstruct</groupId>
                       <artifactId>mapstruct-processor</artifactId>
                       <version>${org.mapstruct.version}</version>
                   </path>
                   <!-- other annotation processors -->
               </annotationProcessorPaths>
           </configuration>
       </plugin>
   </plugins>
</build>

在引入maven依赖后,我们首先来定义需要转换的DTO及VO信息,主要包含的信息是名字、年龄、生日、性别等信息。

@Data
public class UserDTO {
   private String name;

   private int age;

   private Date birthday;

   //1-男 0-女
   private int gender;

   private String idCard;

   private String phoneNumber;

   private String address;

   private Boolean isMarried;
}
@Data
public class UserInfoVO {
   private String userName;

   private int age;

   private Date birthday;

   //1-男 0-女
   private int gender;

   private String idCard;

   private String phoneNumber;

   private String address;

   private Boolean isMarried;
}

紧接着需要编写相应的mapper类,以便生成相应的编译类。

@Mapper
public interface InfoConverter {

   InfoConverter INSTANT = Mappers.getMapper(InfoConverter.class);

   @Mappings({
           @Mapping(source = "name", target = "userName")
  })
   UserInfoVO convert(UserDTO userDto);
}

需要注意的是,因为DTO中的name对应的其实是VO中的userName。因此需要在converter中显式声明。在编写完对应的文件之后,需要执行maven的complie命令使得IDE编译生成对应的Impl对象。(自动生成)

image-20220526161736140.png

到此,mapperStruct的接入就算是完成了~。我们就可以在我们的代码中使用这个拷贝类了。

public UserInfoVO newCopyItem(UserDTO userDTO, int times) {
   UserInfoVO userInfoVO = new UserInfoVO();
   userInfoVO = InfoConverter.INSTANT.convert(userDTO);
   return userInfoVO;
}

怎么样,接入是不是很简单~

FAQ

1、接入项目时,发现并没有生成对应的编译对象class,这个是什么原因?

答:可能的原因有如下几个:

  • 忘记编写对应的@Mapper注解,因而没有生成

  • 没有配置上述提及的插件maven-compiler-plugin

  • 没有执行maven的Compile,IDE没有进行相应编译

2、接入项目后发现,我项目内的Lombok、@Data注解不好使了,这怎么办呢?

由于Lombok本身是对AST进行修改实现的,但是mapStruct在执行的时候并不能检测到Lombok所做的修改,因此需要额外的引入maven依赖lombok-mapstruct-binding

......
   <org.mapstruct.version>1.5.0.RC1</org.mapstruct.version>
   <lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
   <lombok.version>1.18.20</lombok.version>
......

......
<dependency>
   <groupId>org.mapstruct</groupId>
   <artifactId>mapstruct</artifactId>
   <version>${org.mapstruct.version}</version>
</dependency>
<dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok-mapstruct-binding</artifactId>
   <version>${lombok-mapstruct-binding.version}</version>
</dependency>
<dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok</artifactId>
   <version>${lombok.version}</version>
</dependency>

更详细的,mapperStruct在官网中还提供了一个实现Lombok及mapStruct同时并存的案例

3、更多问题:

欢迎查看MapStruct官网文档,里面对各种问题都有更详细的解释及解答。

实现原理

在聊到mapstruct的实现原理之前,我们就需要先回忆一下JAVA代码运行的过程。大致的执行生成的流程如下所示:

image-20220529181541401.png 可以直观的看到,如果我们想不通过编码的方式对程序进行修改增强,可以考虑对抽象语法树进行相应的修改。而mapstruct也正是如此做的。具体的执行逻辑如下所示:

image-20220529181953035.png

为了实现该方法,mapstruct基于JSR 269实现了代码。JSR 269是JDK引进的一种规范。有了它,能够在编译期处理注解,并且读取、修改和添加抽象语法树中的内容。JSR 269使用Annotation Processor在编译期间处理注解,Annotation Processor相当于编译器的一种插件,因此又称为插入式注解处理。想要实现JSR 269,主要有以下几个步骤:

  1. 继承AbstractProcessor类,并且重写process方法,在process方法中实现自己的注解处理逻辑。

  2. 在META-INF/services目录下创建javax.annotation.processing.Processor文件注册自己实现的Annotation Processor。

通过实现AbstractProcessor,在程序进行compile的时候,会对相应的AST进行修改。从而达到目的。

public void compile(List<JavaFileObject> sourceFileObjects,
                   List<String> classnames,
                   Iterable<? extends Processor> processors)
{
   if (processors != null && processors.iterator().hasNext())
       explicitAnnotationProcessingRequested = true;
   // as a JavaCompiler can only be used once, throw an exception if
   // it has been used before.
   if (hasBeenUsed)
       throw new AssertionError("attempt to reuse JavaCompiler");
   hasBeenUsed = true;

   // forcibly set the equivalent of -Xlint:-options, so that no further
   // warnings about command line options are generated from this point on
   options.put(XLINT_CUSTOM.text + "-" + LintCategory.OPTIONS.option, "true");
   options.remove(XLINT_CUSTOM.text + LintCategory.OPTIONS.option);

   start_msec = now();

   try {
       initProcessAnnotations(processors);

       //此处会调用到mapStruct中的processor类的方法.
       delegateCompiler =
           processAnnotations(
               enterTrees(stopIfError(CompileState.PARSE, parseFiles(sourceFileObjects))),
               classnames);

       delegateCompiler.compile2();
       delegateCompiler.close();
       elapsed_msec = delegateCompiler.elapsed_msec;
  } catch (Abort ex) {
       if (devVerbose)
           ex.printStackTrace(System.err);
  } finally {
       if (procEnvImpl != null)
           procEnvImpl.close();
  }
}

关键代码,在mapstruct-processor包中,有个对应的类MappingProcessor继承了AbstractProcessor,并实现其process方法。通过对AST进行相应的代码增强,从而实现对最终编译的对象进行修改的方法。

@SupportedAnnotationTypes({"org.mapstruct.Mapper"})
@SupportedOptions({"mapstruct.suppressGeneratorTimestamp", "mapstruct.suppressGeneratorVersionInfoComment", "mapstruct.unmappedTargetPolicy", "mapstruct.unmappedSourcePolicy", "mapstruct.defaultComponentModel", "mapstruct.defaultInjectionStrategy", "mapstruct.disableBuilders", "mapstruct.verbose"})
public class MappingProcessor extends AbstractProcessor {
   public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
       if (!roundEnvironment.processingOver()) {
           RoundContext roundContext = new RoundContext(this.annotationProcessorContext);
           Set<TypeElement> deferredMappers = this.getAndResetDeferredMappers();
           this.processMapperElements(deferredMappers, roundContext);
           Set<TypeElement> mappers = this.getMappers(annotations, roundEnvironment);
           this.processMapperElements(mappers, roundContext);
      } else if (!this.deferredMappers.isEmpty()) {
           Iterator var8 = this.deferredMappers.iterator();

           while(var8.hasNext()) {
               MappingProcessor.DeferredMapper deferredMapper = (MappingProcessor.DeferredMapper)var8.next();
               TypeElement deferredMapperElement = deferredMapper.deferredMapperElement;
               Element erroneousElement = deferredMapper.erroneousElement;
               String erroneousElementName;
               if (erroneousElement instanceof QualifiedNameable) {
                   erroneousElementName = ((QualifiedNameable)erroneousElement).getQualifiedName().toString();
              } else {
                   erroneousElementName = erroneousElement != null ? erroneousElement.getSimpleName().toString() : null;
              }

               deferredMapperElement = this.annotationProcessorContext.getElementUtils().getTypeElement(deferredMapperElement.getQualifiedName());
               this.processingEnv.getMessager().printMessage(Kind.ERROR, "No implementation was created for " + deferredMapperElement.getSimpleName() + " due to having a problem in the erroneous element " + erroneousElementName + ". Hint: this often means that some other annotation processor was supposed to process the erroneous element. You can also enable MapStruct verbose mode by setting -Amapstruct.verbose=true as a compilation argument.", deferredMapperElement);
          }
      }

       return false;
  }
}

如何断点调试:

因为这个注解处理器是在解析->编译的过程完成,跟普通的jar包调试不太一样,maven框架为我们提供了调试入口,需要借助maven才能实现debug。所以需要在编译过程打开debug才可调试。

  • 在项目的pom文件所在目录执行mvnDebug compile

  • 接着用idea打开项目,添加一个remote,端口为8000

  • 打上断点,debug 运行remote即可调试。

image-20220529194616314.png

附录

测试代码如下,采用Spock框架 + JAVA代码实现。Spock框架作为当前最火热的测试框架,你值得学习一下。 Spock框架初体验:更优雅地写好你的单元测试

//    @Resource
   @Shared
   MapperStructService mapperStructService

   def setupSpec() {
       mapperStructService = new MapperStructService()
  }

   @Unroll
   def "test mapperStructTest times = #times"() {
       given: "初始化数据"
       UserDTO dto = new UserDTO(name: "笑傲菌", age: 20, idCard: "1234",
               phoneNumber: "18211932334", address: "北京天安门", gender: 1,
               birthday: new Date(), isMarried: false)

       when: "调用方法"
//       传统的getter、setter拷贝
       long startTime = System.nanoTime();
       UserInfoVO oldRes = mapperStructService.originalCopyItem(dto, times)
       Duration originalWasteTime = Duration.ofNanos(System.nanoTime() - startTime);

//       采用工具实现反射类的拷贝
       long startTime1 = System.nanoTime();
       UserInfoVO utilRes = mapperStructService.utilCopyItem(dto, times)
       Duration utilWasteTime = Duration.ofNanos(System.nanoTime() - startTime1);

       long startTime2 = System.nanoTime();
       UserInfoVO mapStructRes = mapperStructService.newCopyItem(dto, times)
       Duration mapStructWasteTime = Duration.ofNanos(System.nanoTime() - startTime2);

       then: "校验数据"
       println("times = "+ times)
       println("原始拷贝的消耗时间为: " + originalWasteTime.getNano())
       println("BeanUtils拷贝的消耗时间为: " + utilWasteTime.getNano())
       println("mapStruct拷贝的消耗时间为: " + mapStructWasteTime.getNano())
       println()

       where: "比较不同次数调用的耗时"
       times || ignore
       1     || null
       10    || null
       100   || null
       1000  || null
  }

测试的Service如下所示:

public class MapperStructService {

   public UserInfoVO newCopyItem(UserDTO userDTO, int times) {
       UserInfoVO userInfoVO = new UserInfoVO();
       for (int i = 0; i < times; i++) {
           userInfoVO = InfoConverter.INSTANT.convert(userDTO);
      }
       return userInfoVO;
  }

   public UserInfoVO originalCopyItem(UserDTO userDTO, int times) {
       UserInfoVO userInfoVO = new UserInfoVO();
       for (int i = 0; i < times; i++) {
           userInfoVO.setUserName(userDTO.getName());
           userInfoVO.setAge(userDTO.getAge());
           userInfoVO.setBirthday(userDTO.getBirthday());
           userInfoVO.setIdCard(userDTO.getIdCard());
           userInfoVO.setGender(userDTO.getGender());
           userInfoVO.setIsMarried(userDTO.getIsMarried());
           userInfoVO.setPhoneNumber(userDTO.getPhoneNumber());
           userInfoVO.setAddress(userDTO.getAddress());
      }
       return userInfoVO;
  }

   public UserInfoVO utilCopyItem(UserDTO userDTO, int times) {
       UserInfoVO userInfoVO = new UserInfoVO();
       for (int i = 0; i < times; i++) {
           BeanUtils.copyProperties(userDTO, userInfoVO);
      }
       return userInfoVO;
  }
}

参考文献

踩坑BeanUtils.copy**()导致的业务处理速度过慢

mapstruct原理解析

MapStruct官网

Mapstruct源码解析- 框架实现原理

作者:DrLauPen
来源:https://juejin.cn/post/7103135968256851976

收起阅读 »

跟我学企业级flutter项目:如何重新定制cached_network_image的缓存管理与Dio网络请求

前言 flutter中需要展示网络图片时候,不建议使用flutter原本Image.network(),建议最好还是采用cached_network_image这个三方库。那么我今天就按照它来展开说明,我再做企业级项目时如何重新定制cached_network...
继续阅读 »

前言


flutter中需要展示网络图片时候,不建议使用flutter原本Image.network(),建议最好还是采用cached_network_image这个三方库。那么我今天就按照它来展开说明,我再做企业级项目时如何重新定制cached_network_image。


由于我的项目网络请求采用Dio库,所以我希望我的图片库也采用Dio来网络请求,也是为了方便请求日志打印(在做APM监控时候可以看到网络请求状态,方便定位问题)。


前期准备


准备好mime_converter类,由于cached_network_image中的manager这个文件不是export的状态,那么我们需要准备好该类,以便我们自己实现网络请求修改。


实现mime_converter


创建mime_converter 类,代码如下:


import 'dart:io';

///将最常见的MIME类型转换为最期望的文件扩展名。
extension ContentTypeConverter on ContentType {
String get fileExtension => mimeTypes[mimeType] ?? '.$subType';
}

///MIME类型的来源:
/// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
///2020年3月20日时更新
const mimeTypes = {
'application/vnd.android.package-archive': '.apk',
'application/epub+zip': '.epub',
'application/gzip': '.gz',
'application/java-archive': '.jar',
'application/json': '.json',
'application/ld+json': '.jsonld',
'application/msword': '.doc',
'application/octet-stream': '.bin',
'application/ogg': '.ogx',
'application/pdf': '.pdf',
'application/php': '.php',
'application/rtf': '.rtf',
'application/vnd.amazon.ebook': '.azw',
'application/vnd.apple.installer+xml': '.mpkg',
'application/vnd.mozilla.xul+xml': '.xul',
'application/vnd.ms-excel': '.xls',
'application/vnd.ms-fontobject': '.eot',
'application/vnd.ms-powerpoint': '.ppt',
'application/vnd.oasis.opendocument.presentation': '.odp',
'application/vnd.oasis.opendocument.spreadsheet': '.ods',
'application/vnd.oasis.opendocument.text': '.odt',
'application/vnd.openxmlformats-officedocument.presentationml.presentation':
'.pptx',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
'.docx',
'application/vnd.rar': '.rar',
'application/vnd.visio': '.vsd',
'application/x-7z-compressed': '.7z',
'application/x-abiword': '.abw',
'application/x-bzip': '.bz',
'application/x-bzip2': '.bz2',
'application/x-csh': '.csh',
'application/x-freearc': '.arc',
'application/x-sh': '.sh',
'application/x-shockwave-flash': '.swf',
'application/x-tar': '.tar',
'application/xhtml+xml': '.xhtml',
'application/xml': '.xml',
'application/zip': '.zip',
'audio/3gpp': '.3gp',
'audio/3gpp2': '.3g2',
'audio/aac': '.aac',
'audio/x-aac': '.aac',
'audio/midi audio/x-midi': '.midi',
'audio/mpeg': '.mp3',
'audio/ogg': '.oga',
'audio/opus': '.opus',
'audio/wav': '.wav',
'audio/webm': '.weba',
'font/otf': '.otf',
'font/ttf': '.ttf',
'font/woff': '.woff',
'font/woff2': '.woff2',
'image/bmp': '.bmp',
'image/gif': '.gif',
'image/jpeg': '.jpg',
'image/png': '.png',
'image/svg+xml': '.svg',
'image/tiff': '.tiff',
'image/vnd.microsoft.icon': '.ico',
'image/webp': '.webp',
'text/calendar': '.ics',
'text/css': '.css',
'text/csv': '.csv',
'text/html': '.html',
'text/javascript': '.js',
'text/plain': '.txt',
'text/xml': '.xml',
'video/3gpp': '.3gp',
'video/3gpp2': '.3g2',
'video/mp2t': '.ts',
'video/mpeg': '.mpeg',
'video/ogg': '.ogv',
'video/webm': '.webm',
'video/x-msvideo': '.avi',
'video/quicktime': '.mov'
};

实现FileServiceResponse


FileServiceResponse是数据处理的关键,那么我们来实现该类



class DioGetResponse implements FileServiceResponse {
DioGetResponse(this._response);

final DateTime _receivedTime = clock.now();

final Response<ResponseBody> _response;

@override
int get statusCode => _response.statusCode!;


@override
Stream<List<int>> get content => _response.data!.stream;

@override
int? get contentLength => _getContentLength();

int _getContentLength() {
try {
return int.parse(
_header(HttpHeaders.contentLengthHeader) ?? '-1');
} catch (e) {
return -1;
}
}

String? _header(String name) {
return _response.headers[name]?.first;
}


@override
DateTime get validTill {
// Without a cache-control header we keep the file for a week

var ageDuration = const Duration(days: 7);
final controlHeader = _header(HttpHeaders.cacheControlHeader);
if (controlHeader != null) {
final controlSettings = controlHeader.split(',');
for (final setting in controlSettings) {
final sanitizedSetting = setting.trim().toLowerCase();
if (sanitizedSetting == 'no-cache') {
ageDuration = const Duration();
}
if (sanitizedSetting.startsWith('max-age=')) {
var validSeconds = int.tryParse(sanitizedSetting.split('=')[1]) ?? 0;
if (validSeconds > 0) {
ageDuration = Duration(seconds: validSeconds);
}
}
}
}

return _receivedTime.add(ageDuration);
}

@override
String? get eTag => _header(HttpHeaders.etagHeader);

@override
String get fileExtension {
var fileExtension = '';
final contentTypeHeader = _header(HttpHeaders.contentTypeHeader);
if (contentTypeHeader != null) {
final contentType = ContentType.parse(contentTypeHeader);
fileExtension = contentType.fileExtension;
}
return fileExtension;
}
}

实现FileService


实现FileService 参数为dio


class DioHttpFileService extends FileService {
final Dio _dio;

DioHttpFileService(this._dio);

@override
Future<FileServiceResponse> get(String url, {Map<String, String>? headers}) async {
Options options = Options(headers: headers ?? {}, responseType: ResponseType.stream);
Response<ResponseBody> httpResponse = await _dio.get<ResponseBody>(url, options: options);
return DioGetResponse(httpResponse);
}
}

制定框架缓存管理器


我在项目中,设定了缓存配置最多缓存 100 个文件,并且每个文件只应缓存 7天,如果需要使用日志拦截器的话,就在拦截器中增加日志拦截:


class LibCacheManager {
static const key = 'libCacheKey';

///缓存配置 {最多缓存 100 个文件,并且每个文件只应缓存 7天}
static CacheManager instance = CacheManager(
Config(
key,
stalePeriod: const Duration(days: 7),
maxNrOfCacheObjects: 100,
fileService : DioHttpFileService(Dio()))
),
);

}

项目中使用


使用如下


CachedNetworkImage(imageUrl: "https://t8.baidu.com/it/u=3845489932,4046458829&fm=74&app=80&size=f256,256&n=0&f=JPEG&fmt=auto?sec=1654102800&t=f6de842e1e7086ffc73536795d37fd2c",
cacheManager: LibCacheManager.instance,
width: 100,
height: 100,
placeholder: (context, url) => ImgPlaceHolder(),
errorWidget: (context, url, error) => ImgError(),
);

如上便是 如何重新定制cached_network_image的缓存管理与Dio网络请求

收起阅读 »

微前端乾坤使用过程中的坑

微前端乾坤使用过程中的坑乾坤在启动子应用的时候默认开启沙箱模式{sandbox: true},这样的情况下,乾坤节点下会生成一个 shadow dom,shadow dom 内的样式与外部样式是没有关联的,这样就会给子应用内的样式带来一系列问题。这其...
继续阅读 »

微前端乾坤使用过程中的坑

乾坤在启动子应用的时候默认开启沙箱模式{sandbox: true},这样的情况下,乾坤节点下会生成一个 shadow dom,shadow dom 内的样式与外部样式是没有关联的,这样就会给子应用内的样式带来一系列问题。这其中很多问题并不是乾坤造成的,而是 shadow dom 本身的特性导致的,乾坤还是不错的(不背锅)。随时补充

1.iconffont 字体在子应用无法加载

原因:shadow dom 是不支持@font-face 的,所以当引入 iconfont 的时候,尽管可以引入样式,但由于字体文件是不存在的,所以相对应的图标也无法展示。相关链接:@font-face doesn't work with Shadow DOM?Icon Fonts in Shadow DOM

方案:

  1. 把字体文件放在主应用加载
  2. 使用通用的字体文件,这样就不需要单独加载字体文件了(等于没说~

2.dom的查询方法找不到指定的元素

原因:shadow dom 内的元素是被隔离的元素,故 document下查询的方法例如,querySelector、getElementsById 等是获取不到 shadow dom 内元素的。

方案:代理 document 下各个查询元素的方法,使用子应用外面的 shadow dom 一层查询。如何获取子应用dom对象可以参考乾坤的这个方法 initGlobalState

3.组件库动态创建的元素无法使用自己的样式

原因:有些对话框或提示窗是通过document.body.appendChild添加的,所以 shadow dom 内引入的 CSS 是无法作用到外面元素的。方案:代理document.body.appendChild方法,即把新加的元素添加到 shadow dom容器下,而不是最外面的 body节点下。

补充:类似的问题都可以往这个方向靠,看是不是shadow dom节点或者dom方法的问题。

4.第三方引入的 JS 不生效

原因:有些 JS 文件本身是个立即执行函数,或者会动态的创建 scipt 标签,但是所有获取资源的请求是被乾坤劫持处理,所以都不会正常执行,也不会在 window 下面挂载相应的变量,自然在取值调用的时候也不存在这个变量。方案:参考乾坤的 issue,子应用向body添加script标签失败

5.webpack-dev-server 代理访问的接口 cookie 丢失

原因:在主应用的端口下请求子应用的端口,存在跨域,axios 默认情况下跨域是不携带 cookie 的,假如把 axios 的 withCredential设置为 true(表示跨域携带 cookie),那么子应用需要设置跨域访问头Access-Control-Allow-Origin(在 devServer 下配置 header)为指定的域名,但不能设置为*,这时候同时存在主应用和子应用端口发出的请求,而跨域访问头只能设置一个地址,就导致无法代理指定服务器接口。

方案:子应用接口请求的端口使用主应用接口请求的端口,使用主应用的配置代理请求

// 主应用

devServer:
{
...
port: 9600
proxy: {
// 代理配置
}
}

// 子应用
devServer: {
...
port: 9600, // 使用主应用的页面访问端口
}

原文:https://segmentfault.com/a/1190000037641251


收起阅读 »

Vue + qiankun 快速实现前端微服务

什么是微前端Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently. -...
继续阅读 »

什么是微前端

Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently. -- Micro Frontends

微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。

qiankun

qiankun 是蚂蚁金服开源的一套完整的微前端解决方案。具体描述可查看 文档 和 Github

下面将通过一个微服务Demo 介绍 Vue 项目如何接入 qiankun,代码地址:micro-front-vue)

二、配置主应用

  1. 使用 vue cli 快速创建主应用;
  2. 安装 qiankun
$ yarn add qiankun # 或者 npm i qiankun -S
  1. 调整主应用 main.js 文件:具体如下:
import Vue from "vue"
import App from "./App.vue"
import router from "./router"

import { registerMicroApps, setDefaultMountApp, start } from "qiankun"
Vue.config.productionTip = false
let app = null;
/**
* 渲染函数
* appContent 子应用html内容
* loading 子应用加载效果,可选
*/
function render({ appContent, loading } = {}) {
if (!app) {
app = new Vue({
el: "#container",
router,
data() {
return {
content: appContent,
loading
};
},
render(h) {
return h(App, {
props: {
content: this.content,
loading: this.loading
}
});
}
});
} else {
app.content = appContent;
app.loading = loading;
}
}

/**
* 路由监听
* @param {*} routerPrefix 前缀
*/
function genActiveRule(routerPrefix) {
return location => location.pathname.startsWith(routerPrefix);
}

function initApp() {
render({ appContent: '', loading: true });
}

initApp();

// 传入子应用的数据
let msg = {
data: {
auth: false
},
fns: [
{
name: "_LOGIN",
_LOGIN(data) {
console.log(`父应用返回信息${data}`);
}
}
]
};
// 注册子应用
registerMicroApps(
[
{
name: "sub-app-1",
entry: "//localhost:8091",
render,
activeRule: genActiveRule("/app1"),
props: msg
},
{
name: "sub-app-2",
entry: "//localhost:8092",
render,
activeRule: genActiveRule("/app2"),
}
],
{
beforeLoad: [
app => {
console.log("before load", app);
}
], // 挂载前回调
beforeMount: [
app => {
console.log("before mount", app);
}
], // 挂载后回调
afterUnmount: [
app => {
console.log("after unload", app);
}
] // 卸载后回调
}
);

// 设置默认子应用,与 genActiveRule中的参数保持一致
setDefaultMountApp("/app1");

// 启动
start();
  1. 修改主应用 index.html 中绑定的 id ,需与 el  绑定 dom 为一致;
  2. 调整 App.vue 文件,增加渲染子应用的盒子:
<template>
<div id="main-root">
<!-- loading -->
<div v-if="loading">loading</div>
<!-- 子应用盒子 -->
<div id="root-view" class="app-view-box" v-html="content"></div>
</div>
</template>

<script>
export default {
name: "App",
props: {
loading: Boolean,
content: String
}
};
</script>
  1. 创建 vue.config.js 文件,设置 port :
module.exports = {
devServer: {
port: 8090
}
}

三、配置子应用

  1. 在主应用同一级目录下快速创建子应用,子应用无需安装 qiankun
  2. 配置子应用 main.js:
import Vue from 'vue';
import VueRouter from 'vue-router';
import App from './App.vue';
import routes from './router';
import './public-path';

Vue.config.productionTip = false;

let router = null;
let instance = null;

function render() {
router = new VueRouter({
base: window.__POWERED_BY_QIANKUN__ ? '/app1' : '/',
mode: 'history',
routes,
});

instance = new Vue({
router,
render: h => h(App),
}).$mount('#app');
}

if (!window.__POWERED_BY_QIANKUN__) {
render();
}

export async function bootstrap() {
console.log('vue app bootstraped');
}

export async function mount(props) {
console.log('props from main app', props);
render();
}

export async function unmount() {
instance.$destroy();
instance = null;
router = null;
}
  1. 配置 vue.config.js
const path = require('path');
const { name } = require('./package');

function resolve(dir) {
return path.join(__dirname, dir);
}

const port = 8091; // dev port

module.exports = {
/**
* You will need to set publicPath if you plan to deploy your site under a sub path,
* for example GitHub Pages. If you plan to deploy your site to https://foo.github.io/bar/,
* then publicPath should be set to "/bar/".
* In most cases please use '/' !!!
* Detail: https://cli.vuejs.org/config/#publicpath
*/
outputDir: 'dist',
assetsDir: 'static',
filenameHashing: true,
// tweak internal webpack configuration.
// see https://github.com/vuejs/vue-cli/blob/dev/docs/webpack.md
devServer: {
// host: '0.0.0.0',
hot: true,
disableHostCheck: true,
port,
overlay: {
warnings: false,
errors: true,
},
headers: {
'Access-Control-Allow-Origin': '*',
},
},
// 自定义webpack配置
configureWebpack: {
resolve: {
alias: {
'@': resolve('src'),
},
},
output: {
// 把子应用打包成 umd 库格式
library: `${name}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${name}`,
},
},
};

其中有个需要注意的点:

  1. 子应用必须支持跨域:由于 qiankun 是通过 fetch 去获取子应用的引入的静态资源的,所以必须要求这些静态资源支持跨域;
  2. 使用 webpack 静态 publicPath 配置:可以通过两种方式设置,一种是直接在 mian.js 中引入 public-path.js 文件,一种是在开发环境直接修改 vue.config.js:
{
output: {
publicPath: `//localhost:${port}`;
}
}

public-path.js 内容如下:

if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}
至此,Vue 项目的前端微服务已经简单完成了。

但是在实际的开发过程中,并非如此简单,同时还存在应用间跳转、应用间通信等问题。


原文:https://segmentfault.com/a/1190000021872481


收起阅读 »

可部署于windows和Linux的即时通讯系统

系统概况信贸通即时通讯系统,一款跨平台可定制的 P2P 即时通信系统,为电子商务网站及各行业门户网站和企事业单位提供“一站式”定制解决方案,打造一个稳定,安全,高效,可扩展的即时通信系统,支持在线聊天、视频/语音对话、点对点断点续传文件、自定义皮肤等。软件能真...
继续阅读 »

系统概况
信贸通即时通讯系统,一款跨平台可定制的 P2P 即时通信系统,为电子商务网站及各行业门户网站和企事业单位提供“一站式”定制解决方案,打造一个稳定,安全,高效,可扩展的即时通信系统,支持在线聊天、视频/语音对话、点对点断点续传文件、自定义皮肤等。软件能真正无缝与电子商务网站整合,有效提高工作效率,节约成本。同时可根据用户的需求进行二次开发,并提供与其他软件整合或嵌入方案

系统架构
自研协议独立开发,采用高并发go语言开发的即时通讯及历史消息云存储通信系统。系统安全性高可扩展能力强,系统兼容性好。可快速无缝集成到各种应用系统,有效提高开发效率,节约成本。能轻松在线定制客户端。支持多平台客户端实现多端与多设备同步。

私有部署
整个系统部署在您自己的服务器上,可以部署在公网也可以部署在内网中,支持Windows服务和Linux服务器,硬件要求低(主流服务器和云服务器均可运行)。系统独立运行,完全自主管理和监控,最大程度上保障数据安全,避免信息泄露,安全性更高,带来更多的便捷和保障。

定制开发
可根据客户的需求量身定制符合客户实际应用的即时通聊天软件,可控性强、易扩展,系统集成度高。可以快速进行二次开发,简单方便来进行定制管理。

客户端 / 功能
支持windows,安卓,ios,主流浏览器,功能单聊,群聊,消息互通,朋友圈等主流功能,安全可靠。


http://www.51dn.top/wp-content/uploads/2022/04/QQ截图20220407104051-624x315.jpg 624w, http://www.51dn.top/wp-content/uploads/2022/04/QQ截图20220407104051.jpg 671w" sizes="(max-width: 500px) 100vw, 500px" style="vertical-align: baseline; border-radius: 3px; box-shadow: rgba(0, 0, 0, 0.2) 0px 1px 4px;">


收起阅读 »

使用自定义url发图片的坑

发送URL图片消息 App端需要开发者自己实现下载,Web端需要在 WebIMConfig.js中 设置 useOwnUploadFun: true。实际上还得在WEBIM里面再配置一下WebIM.conn = new WebIM.connect...
继续阅读 »


发送URL图片消息





App端需要开发者自己实现下载,Web端需要在 WebIMConfig.js中 设置 useOwnUploadFun: true

实际上还得在WEBIM里面再配置一下

WebIM.conn = new WebIM.connection({
appKey: WebIM.config.appkey,
isMultiLoginSessions: WebIM.config.isMultiLoginSessions,
https: typeof WebIM.config.https === "boolean" ? WebIM.config.https : location.protocol === "https:",
url: WebIM.config.xmppURL,
apiUrl: WebIM.config.apiURL,
isAutoLogin: false,
heartBeatWait: WebIM.config.heartBeatWait,
autoReconnectNumMax: WebIM.config.autoReconnectNumMax,
autoReconnectInterval: WebIM.config.autoReconnectInterval,
useOwnUploadFun: WebIM.config.useOwnUploadFun,
isDebug: false,
isHttpDNS:false
});







单聊通过URL发送图片消息的代码示例如下:


// 单聊通过URL发送图片消息
var sendPrivateUrlImg = function () {
var id = conn.getUniqueId(); // 生成本地消息id
var msg = new WebIM.message('img', id); // 创建图片消息
var option = {
body: {
type: 'file',
url: url,
size: {
width: msg.width,
height: msg.height,
},
length: msg.length,
filename: msg.file.filename,
filetype: msg.filetype
},
to: 'username', // 接收消息对象
};
msg.set(option);
conn.send(msg.body);
}
收起阅读 »

探究EventBus粘性事件实现机制

粘性事件观察者 @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) fun registerEventBus(o: Any) { } 发送粘性事件 EventBus.getDefault...
继续阅读 »
  1. 粘性事件观察者


@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun registerEventBus(o: Any) {
}


  1. 发送粘性事件


EventBus.getDefault().postSticky(Any())


  1. 注册EventBus


EventBus.getDefault().register(this)

接下来我们就来探究下EventBus的粘性事件是如何实现的。


postSticky()内部机制


image.png



  1. 如果是发送的粘性事件,会添加到stickyEvents中,看下这个属性的实现:


image.png

可以看到这个属性是一个Map集合,其中key为事件类型的class对象,value为对应的事件类型。



  1. 继续看下post(Event)方法:


image.png




  1. 首先将这个粘性事件添加到PostingThreadState(线程私有)的eventQueue集合中




  2. 通过isMainThread方法判断当前是否为主线程,最终会调用到我们熟悉的Looper.getMainLooper() == Looper.myLooper()进行判断




  3. 循环遍历eventQueue队列,不断的取出集合元素进行分发,看下postSinleEvent()方法如何实现:




image.png




  1. 如果eventInheritance为true,会查找当前发送的粘性事件类型的父类型,并返回查找到的集合




  2. 接下来就会调用postSingleEventForEventType()方法来进行最终粘性事件的分发,即通知通过@Subscribe注解注册的粘性事件观察者,看下具体实现:




image.png



  1. 调用subscriptionsByEventType获取注册该事件类型的所有订阅方法,但是由于这个时候我们是先发送的粘性事件再注册EventBus,而subscriptionsByEventType中集合元素的填充实在注册EventBus发生的,所以通过subscriptionsByEventType获取到的subscriptions将是null的,所以接下来肯定不会走下面的if代码块中的逻辑了。


postSticky()小结


上面这么多代码逻辑,其实只干了一件事,就是将这个粘性事件添加到了stickyEvents这个集合中。之后的逻辑虽多,但和粘性事件没啥关系。


register内部机制


image.png



  1. findSubscriberMethods()这个方法里面的逻辑就不带大家进行分析了,总之就干了一件事情:



查找当前类通过@Subscribe注册的所有事件订阅方法,并返回一个List<SubscriberMethod>集合,其中SubscriberMethod就是对每个注册的订阅方法和当前注册类的封装




  1. subscribe这个方法是关键,深入探究下:


image.png


image.png




  • 第1、2、3、4步中其实就干了两件事情:



    • 填充subscriptionsByEventType集合,key为事件类型,value为通过@Subscribe订阅了该事件类型的方法集合

    • 填充typesBySubscriber集合,key为注册EventBus的类,value为该类中所有@Subscribe注解修饰的方法集合




  • 第5步就是实现粘性事件分发的关键地方



    • 首先判断当前@Subscribe修饰的订阅方法是否为粘性,即@Subscribe(sticky = true)sticky等于true

    • 是的话就从stickyEvents集合中判断是否存在和订阅方法中注册的事件类型相同的事件:



    这个stickyEvents是不是很熟悉,就是我们之前发送粘性事件时,将粘性事件添加到的方法集合




    • 如果存在,则就执行该粘性事件的分发,即调用执行该订阅方法,最终会调用到invokeSubscriber()方法:




image.png



从上面可以看到,最终是通过反射来实现的订阅了粘性事件方法的执行。



register小结


该方法最终会判断当前是否存在注册EventBus前发送的粘性事件,且当前注册类中存在订阅该事件类型的方法,然后立即执行。


总结


以上就是EventBus粘性事件的内部实现机制,总体来说不算复杂,大家看着文章跟着源码一步步分析应该就很容易理解这部分实现逻辑了。


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

并发编程-线程的启动、死锁、线程安全、ThreadLocal

1 线程的启动方式 线程的启动方式只有两种。 方式1:继承Thread,然后调用start()启动。 private static class PrimeThread extends Thread { @Override public void...
继续阅读 »

1 线程的启动方式


线程的启动方式只有两种。


方式1:继承Thread,然后调用start()启动。


private static class PrimeThread extends Thread {
@Override
public void run() {
System.out.println("thread extend Thread---name:" + Thread.currentThread().getName());
}
}

PrimeThread thread = new PrimeThread();
thread.start();

方式2:实现Runnable,然后交给Thread去启动。


private static class PrimeRunnable implements Runnable {
@Override
public void run() {
System.out.println("thread implements Runnable---name:" + Thread.currentThread().getName());
}
}

PrimeRunnable runnable = new PrimeRunnable();
new Thread(runnable).start();

其他的比如线程池、FutureTask等都属于这两种的包装或封装。


并且Thread源码的注释中也清楚的写了有两种方式创建线程:


* <p>
* There are two ways to create a new thread of execution. One is to
* declare a class to be a subclass of <code>Thread</code>. This
* subclass should override the <code>run</code> method of class
* <code>Thread</code>. An instance of the subclass can then be
* allocated and started. For example, a thread that computes primes
* larger than a stated value could be written as follows:
* <hr><blockquote><pre>
* class PrimeThread extends Thread {
* long minPrime;
* PrimeThread(long minPrime) {
* this.minPrime = minPrime;
* }
*
* public void run() {
* // compute primes larger than minPrime
* &nbsp;.&nbsp;.&nbsp;.
* }
* }
* </pre></blockquote><hr>
* <p>
* The following code would then create a thread and start it running:
* <blockquote><pre>
* PrimeThread p = new PrimeThread(143);
* p.start();
* </pre></blockquote>
* <p>
* The other way to create a thread is to declare a class that
* implements the <code>Runnable</code> interface. That class then
* implements the <code>run</code> method. An instance of the class can
* then be allocated, passed as an argument when creating
* <code>Thread</code>, and started. The same example in this other
* style looks like the following:
* <hr><blockquote><pre>
* class PrimeRun implements Runnable {
* long minPrime;
* PrimeRun(long minPrime) {
* this.minPrime = minPrime;
* }
*
* public void run() {
* // compute primes larger than minPrime
* &nbsp;.&nbsp;.&nbsp;.
* }
* }
* </pre></blockquote><hr>
* <p>

2 线程的状态


Java中线程的状态分为6种:


1、初始(NEW):新创建了一个线程,但是还没有调用start()方法。


2、运行(RUNNABLE):Java线程中将就绪(READY)和运行中(RUNNING)两种装填笼统的称为“运行”。


线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法,该状态的线程位于可运行线程池中,获取CPU的使用权,此时处于就绪状态(READY),就绪状态的线程在获得CPU时间片后变为运行中状态(RUNNING)。


3、阻塞(BLOCKED):表示线程阻塞于锁。


4、等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。


5、超时等待(TIMED_WAITING):该状态不同于WATING,它可以在指定的时间后自行返回。


6、终止(TERMINATED):表示该线程已经执行完毕。


线程生命周期如下:



3 死锁


3.1 概念


死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。


死锁是必然发生在多操作者(M>=2个)情况下,争夺多个资源(N>=2个,且N<=M)才会发生这种情况。很明显,单线程自然不会有死锁。


死锁还有几个要求:



  1. 争夺资源的顺序不对,如果争夺资源的顺序是一样的,也不会产生死锁。

  2. 争夺者拿到资源不放手。


3.1.1 学术定义


死锁的发生必须具备以下四个必要条件。



  1. 互斥条件: 指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。

  2. 请求和保持条件: 指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。

  3. 不剥夺条件: 指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。

  4. 环路等待条件: 指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。


理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和解除死锁。只要打破四个必要条件之一就能有效预防死锁的发生。



  • 打破互斥条件:改造独占性资源为虚拟资源,大部分资源已无法改造。

  • 打破不可抢占条件:当一进程占有一独占性资源后又申请一独占性资源而无法满足,则退出原占有的资源。

  • 打破占有且申请条件:采用资源预先分配策略,即进程运行前申请全部资源,满足则运行,不然就等待,这样就不会占有且申请。

  • 打破循环等待条件:实现资源有序分配策略,对所有设备实现分类编号,所有进程只能采用按序号递增的形式申请资源。


避免死锁常见的算法有有序资源分配法、银行家算法。


示例代码:


/**
* @Description: 死锁的产生
* @CreateDate: 2022/3/15 2:31 下午
*/
public class NormalDeadLock {

/**
* 第1个锁
*/
private static final Object LOCK_1 = new Object();
/**
* 第2个锁
*/
private static final Object LOCK_2 = new Object();

/**
* 第1个拿锁的方法 先去拿锁1,再去拿锁2
*
* @throws InterruptedException 中断异常
*/
private static void method1() throws InterruptedException {
String threadName = Thread.currentThread().getName();
synchronized (LOCK_1) {
System.out.println(threadName + " get LOCK_1");
Thread.sleep(100);
synchronized (LOCK_2) {
System.out.println(threadName + " get LOCK_2");
}
}
}

/**
* 第2个拿锁的方法 先去拿锁2,再去拿锁1,这就导致方法1和方法2各拿一个锁,然后互不相让,都不释放自己的锁,造成了互斥,就产生了死锁
*
* @throws InterruptedException 中断异常
*/
private static void method2() throws InterruptedException {
String threadName = Thread.currentThread().getName();
synchronized (LOCK_2) {
System.out.println(threadName + " get LOCK_2");
Thread.sleep(100);
synchronized (LOCK_1) {
System.out.println(threadName + " get LOCK_1");
}
}
}

/**
* 子线程PrimeThread1
*/
private static class PrimeThread1 extends Thread {
private final String name;

public PrimeThread1(String name) {
this.name = name;
}

@Override
public void run() {
Thread.currentThread().setName(name);
try {
method1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

/**
* 子线程PrimeThread2
*/
private static class PrimeThread2 extends Thread {
private final String name;

public PrimeThread2(String name) {
this.name = name;
}

@Override
public void run() {
Thread.currentThread().setName(name);
try {
method2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) {
PrimeThread1 thread1 = new PrimeThread1("PrimeThread1");
PrimeThread2 thread2 = new PrimeThread2("PrimeThread2");

thread1.start();
thread2.start();
}

}

执行后,可以看到控制台没有结束运行,看不到Process finished with exit code 0,但是又一直处于静止状态。


PrimeThread1 get LOCK_1
PrimeThread2 get LOCK_2

3.2 危害



  1. 线程不工作了,但是整个程序还是活着的。

  2. 没有任何的异常信息可以供我们检查。

  3. 一旦程序发生了发生了死锁,是没有任何的办法恢复的,只能重启程序,对正式已发布程序来说,这是个很严重的问题。


3.3 解决方案


关键是保证拿锁的顺序一致。


两种解决方式:


1、内部通过顺序比较,确定拿锁的顺序。


比如上述示例代码中,可以让方法1和方法2同时都先拿锁1,然后再去拿锁2,就能解决死锁问题。


private static void method1() throws InterruptedException {
String threadName = Thread.currentThread().getName();
synchronized (LOCK_1) {
System.out.println(threadName + " get LOCK_1");
Thread.sleep(100);
synchronized (LOCK_2) {
System.out.println(threadName + " get LOCK_2");
}
}
}

private static void method2() throws InterruptedException {
String threadName = Thread.currentThread().getName();
synchronized (LOCK_1) {
System.out.println(threadName + " get LOCK_1");
Thread.sleep(100);
synchronized (LOCK_2) {
System.out.println(threadName + " get LOCK_2");
}
}
}

修改后后,可以看到程序能正常执行。


PrimeThread1 get LOCK_1
PrimeThread1 get LOCK_2
PrimeThread2 get LOCK_1
PrimeThread2 get LOCK_2

Process finished with exit code 0

2、采用尝试拿锁的机制。


示例代码:


/**
* @Description: 尝试拿锁,解决死锁问题
* @CreateDate: 2022/3/15 2:57 下午
*/
public class TryGetLock {
/**
* 第1个锁
*/
private static final Lock LOCK_1 = new ReentrantLock();
/**
* 第2个锁
*/
private static final Lock LOCK_2 = new ReentrantLock();

/**
* 方法1 先尝试拿锁1,再尝试拿锁2,拿不到锁2的话连同锁1一起释放
*
* @throws InterruptedException 中断异常
*/
private static void method1() throws InterruptedException {
String threadName = Thread.currentThread().getName();
Random r = new Random();
while (true) {
if (LOCK_1.tryLock()) {
System.out.println(threadName + " get LOCK_1");
try {
if (LOCK_2.tryLock()) {
try {
System.out.println(threadName + " get LOCK_2");
System.out.println("method1 do working...");
break;
} finally {
LOCK_2.unlock();
}
}
} finally {
LOCK_1.unlock();
}
}
//注意:这里需要给个很短的间隔时间去让其他线程拿锁,不然可能会造成活锁
Thread.sleep(r.nextInt(3));
}
}

/**
* 方法2 先尝试拿锁2,再尝试拿锁1,拿不到锁1的话连同锁2一起释放
*
* @throws InterruptedException 中断异常
*/
private static void method2() throws InterruptedException {
String threadName = Thread.currentThread().getName();
Random r = new Random();
while (true) {
if (LOCK_2.tryLock()) {
System.out.println(threadName + " get LOCK_2");
try {
if (LOCK_1.tryLock()) {
try {
System.out.println(threadName + " get LOCK_1");
System.out.println("method2 do working...");
break;
} finally {
LOCK_1.unlock();
}
}
} finally {
LOCK_2.unlock();
}
}
Thread.sleep(r.nextInt(3));
}
}

private static class PrimeThread1 extends Thread {
private final String name;

public PrimeThread1(String name) {
this.name = name;
}

@Override
public void run() {
Thread.currentThread().setName(name);
try {
method1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

private static class PrimeThread2 extends Thread {
private final String name;

public PrimeThread2(String name) {
this.name = name;
}

@Override
public void run() {
Thread.currentThread().setName(name);
try {
method2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) {
PrimeThread1 thread1 = new PrimeThread1("PrimeThread1");
PrimeThread2 thread2 = new PrimeThread2("PrimeThread2");

thread1.start();
thread2.start();
}
}

执行结果:


PrimeThread2 get LOCK_2
PrimeThread1 get LOCK_1
PrimeThread2 get LOCK_2
PrimeThread2 get LOCK_1
method2 do working...
PrimeThread1 get LOCK_1
PrimeThread1 get LOCK_2
method1 do working...

Process finished with exit code 0

4 其他线程安全问题


4.1 活锁


两个线程在尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生同一个线程总是拿到同一把锁,在尝试拿另一把锁时因为拿不到,而将本来已经持有的锁释放的过程。


解决办法:每个线程休眠随机数,错开拿锁的时间。


如上边的尝试拿锁示例代码中,如果不加随机sleep,就会造成活锁。


4.2 线程饥饿


低优先级的线程,总是拿不到执行时间。


5 ThreadLocal


5.1 与Synchonized的比较


ThreadLocalsynchonized都用于解决多线程并发訪问。但是ThreadLocalsynchronized有本质的差别。synchronized是利用锁的机制,使变量或代码块在某一时该仅仅能被一个线程访问。而ThreadLocal为每个线程都提供了变量的副本,使得每个线程在某一时间访问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。


5.2 ThreadLocal的使用


ThreadLocal类接口很简单,只有4个方法:



  • protected T initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。

  • public void set(T value)设置当前线程的线程局部变量。

  • public T get()返回当前线程所对应的线程局部变量。

  • public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。


public final static ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<String>();

THREAD_LOCAL代表一个能够存放String类型的ThreadLocal对象。此时不论什么一个线程能够并发访问这个变量,对它进行写入、读取操作,都是线程安全的。


示例代码:


/**
* @Description: 使用ThreadLocal
* @CreateDate: 2022/3/15 3:37 下午
*/
public class UseThreadLocal {

private static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();

private void startThreadArray() {
Thread[] threads = new Thread[3];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(new PrimeRunnable(i));
}
for (int i = 0; i < threads.length; i++) {
threads[i].start();
}
}

private static class PrimeRunnable implements Runnable {
private final int id;

public PrimeRunnable(int id) {
this.id = id;
}

@Override
public void run() {
String threadName = Thread.currentThread().getName();
THREAD_LOCAL.set("线程" + id);
System.out.println(threadName + ":" + THREAD_LOCAL.get());
}
}

public static void main(String[] args) {
UseThreadLocal useThreadLocal = new UseThreadLocal();
useThreadLocal.startThreadArray();
}
}

5.3 ThreadLocal的内部实现




    public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

    ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

ThreadLocal.ThreadLocalMap threadLocals = null;

上面先取到当前线程,然后调用getMap方法获取对应的ThreadLocalMapThreadLocalMapThreadLocal的静态内部类,然后Thread类中有一个这样类型成员,所以getMap是直接返回Thread的成员。


看下ThreadLocal的内部类ThreadLocalMap源码:


    static class ThreadLocalMap {

/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

//类似于map的key、value结构,key就是ThreadLocal,value就是要隔离访问的变量
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;

/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
* 用数组保存了Entry,因为可能有多个变量需要线程隔离访问
*/
private Entry[] table;

可以看到有个Entry内部静态类,它继承了WeakReference,总之它记录了两个信息,一个是ThreadLocal<?>类型,一个是Object类型的值。getEntry方法则是获取某个ThreadLocal对应的值,set方法就是更新或赋值相应的ThreadLocal对应的值。


        private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// Android-changed: Use refersTo()
if (e != null && e.refersTo(key))
return e;
else
return getEntryAfterMiss(key, i, e);
}

        private void set(ThreadLocal<?> key, Object value) {

// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.

Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);

for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();

if (k == key) {
e.value = value;
return;
}

if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}

tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

回顾get方法,其实就是拿到每个线程独有的ThreadLocalMap,然后再用ThreadLocal的当前实例,拿到Map中的相应的Entry,然后就可以拿到相应的值返回出去。当然,如果Map为空,还会先进行Map的创建,初始化等工作。


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

Flutter入口中的runApp方法解析

前言 开发中,如果在runApp方法执行之前设置Android沉浸式样式报错,需要先设置WidgetsFlutterBinding.ensureInitialized();这一行代码才行,为什么,接下来看下这一行代码具体做了啥。 点进去发现这个方法在runAp...
继续阅读 »

前言


开发中,如果在runApp方法执行之前设置Android沉浸式样式报错,需要先设置WidgetsFlutterBinding.ensureInitialized();这一行代码才行,为什么,接下来看下这一行代码具体做了啥。


点进去发现这个方法在runApp中进行了实现,并且还调用了WidgetsFlutterBinding的另两个方法,

方法体:


void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()
..scheduleAttachRootWidget(app)
..scheduleWarmUpFrame();
}

接下来我们对这三个方法进行一个一个进行分析。


1、WidgetsFlutterBinding.ensureInitialized()


代码:



/// widgets框架的具体绑定,将框架绑定到Flutter引擎的中间层。
/// A concrete binding for applications based on the Widgets framework.
/// This is the glue that binds the framework to the Flutter engine.
class WidgetsFlutterBinding extends BindingBase with GestureBinding,
SchedulerBinding, ServicesBinding, PaintingBinding,
SemanticsBinding, RendererBinding, WidgetsBinding {

/// 只有需要绑定时,才需要调用这个方法,在runApp之前调用。
/// You only need to call this method if you need the binding to be
/// initialized before calling [runApp].

static WidgetsBinding ensureInitialized() {
if (WidgetsBinding.instance == null)
WidgetsFlutterBinding();
return WidgetsBinding.instance!;
}
}

可以看到,WidgetsFlutterBinding 继承了 BindingBase,并混入了一些其他Binding


先看下BindingBase类,


/// 初始化 获取唯一window
ui.SingletonFlutterWindow get window => ui.window;
/// 初始化PlatformDispatcher实例,平台消息和配置的中心入口点,负责分发事件给Flutter引擎。
ui.PlatformDispatcher get platformDispatcher => ui.PlatformDispatcher.instance;

主要是获取了window实例和PlatformDispatcher实例。


再看下其他Binding解释:




  • GestureBinding:处理手势相关。




  • SchedulerBinding: 处理系统调度。




  • ServicesBinding:处理与原生的交互。




  • PaintingBinding:处理绘制相关。




  • SemanticsBinding:处理语义化。




  • RendererBinding:处理渲染相关。




  • WidgetsBindingWidgets相关。




Flutter框架层的相关基础绑定。


接着我们看下改变状态栏的代码。


改变样式核心代码:


if (_pendingStyle != _latestStyle) {
// 通过和原生平台进行通信 来改变具体平台状态样式
SystemChannels.platform.invokeMethod<void>(
'SystemChrome.setSystemUIOverlayStyle',
_pendingStyle!._toMap(),
);
_latestStyle = _pendingStyle;
}

通过 ensureInitialized的注释和修改样式的代码即可解决我们开头的疑问,因为设置状态栏样式是通过原生window窗口进行修改的,所以这里如果需要修改状态栏,就需要进行和原生绑定才能拿到原生的window窗口来进行修改。


从注释来看:WidgetsFlutterBinding是widgets框架的具体绑定,将框架绑定到Flutter引擎的中间层。


通过 ensureInitialized方法返回 一个WidgetsBinding单例类。


至此,WidgetsFlutterBinding.ensureInitialized();的工作已经结束。

就是做了初始化引擎绑定,返回WidgetsBinding


..scheduleAttachRootWidget(app)


上一个方法我们知道返回了WidgetsBinding类,那这个方法就是在WidgetsBinding这个类里,接下来先看下这个类。


/// widgets和Flutter引擎之间的粘合剂,中间层
/// The glue between the widgets layer and the Flutter engine.
mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, RendererBinding, SemanticsBinding {
@override
void initInstances() {
super.initInstances();
_instance = this;
// Initialization of [_buildOwner] has to be done after
// [super.initInstances] is called, as it requires [ServicesBinding] to
// properly setup the [defaultBinaryMessenger] instance.
_buildOwner = BuildOwner();
buildOwner!.onBuildScheduled = _handleBuildScheduled;

/// 略

@protected
void scheduleAttachRootWidget(Widget rootWidget) {
Timer.run(() {
attachRootWidget(rootWidget);
});
}

void attachRootWidget(Widget rootWidget) {
final bool isBootstrapFrame = renderViewElement == null;
_readyToProduceFrames = true;
_renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
container: renderView,
debugShortDescription: '[root]',
child: rootWidget,
).attachToRenderTree(buildOwner!, renderViewElement as RenderObjectToWidgetElement<RenderBox>?);
if (isBootstrapFrame) {
SchedulerBinding.instance!.ensureVisualUpdate();
}
}

可以看到这是一个mixin类,创建了BuildOwner的实例,这个类是用来管理Element的,在attachRootWidget方法中创建了RenderObjectToWidgetAdapter实例,并设置了我们的runApp中的参数根节点rootWigdt


/// 桥接 RenderObject 到 Element 
/// A bridge from a [RenderObject] to an [Element] tree.
RenderObjectToWidgetAdapter({
this.child,
required this.container,
this.debugShortDescription,
}) : super(key: GlobalObjectKey(container));

RenderObjectToWidgetAdapter 是桥接RenderObjectElement的,Element是持有Widget的具体实现,通过RenderObject进行渲染,也就是通过这个方法实现了 Widget、Element、RenderObject的初始及绑定关系。


..scheduleWarmUpFrame();


绑定之后,接下来就是将内容显示在屏幕上,从以下代码分别调用了handleBeginFramehandleDrawFrame方法,通过hadScheduledFrame判断是否调用handleBeginFrame触发scheduleFrame方法,调用 window.scheduleFrame(); 最终调用 platformDispatcher.scheduleFrame();通知引擎在合适的时机进行帧绘制。


void scheduleWarmUpFrame() {
if (_warmUpFrame || schedulerPhase != SchedulerPhase.idle)
return;
_warmUpFrame = true;
final TimelineTask timelineTask = TimelineTask()..start('Warm-up frame');
final bool hadScheduledFrame = _hasScheduledFrame;
// We use timers here to ensure that microtasks flush in between.
Timer.run(() {
assert(_warmUpFrame);
handleBeginFrame(null);
});
Timer.run(() {
assert(_warmUpFrame);
handleDrawFrame();

resetEpoch();
_warmUpFrame = false;
if (hadScheduledFrame)
scheduleFrame();
});



void scheduleFrame() {
window.scheduleFrame();
}

/// Requests that, at the next appropriate opportunity, the [onBeginFrame] and
/// [onDrawFrame] callbacks be invoked.
void scheduleFrame() => platformDispatcher.scheduleFrame();

小结


runApp中的三个方法执行的三步分别是:

1、初始化WidgetsFlutterBinding返回WidgetBinding实例。

2、初始化Widget、Elment、RenderObject三棵树并确定绑定关系。

3、通知引擎合适时机进行帧绘制。更快的将内容显示到屏幕中。


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

Kotlin - 改良装饰者模式

一、前言 装饰者模式 作用:在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能。 本质:该模式通过创建一个包装对象,来包裹真实的对象。 核心操作: 创建一个装饰类,包含一个被装饰类的实例 装饰类重写所有被装饰类的方法 在装饰类中对需要增强的功...
继续阅读 »

一、前言



  • 装饰者模式

    • 作用:在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能。

    • 本质:该模式通过创建一个包装对象,来包裹真实的对象。

    • 核心操作:

      • 创建一个装饰类,包含一个被装饰类的实例

      • 装饰类重写所有被装饰类的方法

      • 在装饰类中对需要增强的功能进行扩展






二、使用装饰者模式



  • 例子:枪支部件

  • 重点:装饰器类设计(实现被装饰类相同的接口,构造器接收被装饰类的接口实例对象)


像绝地求生这种大型射击游戏,里面的枪支系统是很复杂的,有很多种枪,而且几乎每种枪上都可以装配各种各样的部件,比如消声器、八倍镜之类的,部件的作用各不相同,有的可以增加火力,有的可以提高精确度,等等,现在我们来简单设计一下这个枪支系统,枪有很多种,所以需要定义一个接口来描述枪都有哪些能力,供后续扩展各种新枪:


/**
* 枪支接口
*
* @author GitLqr
*/
interface Gun {
/**
* 攻击力
*/
fun attack(): Float

/**
* 噪音
*/
fun noise(): Float

/**
* 生产日期
*/
fun prodDate(): String
}

/**
* Ump9
*
* @author GitLqr
*/
class Ump9Gun : Gun {
override fun attack() = 100f

override fun noise() = 20f

override fun prodDate() = "2020-02-18"
}

这里只实现了 Ump9 这个型号的枪,后续还可以根据需要扩展,现在来想想枪支部件怎么设计?在 Java 中,给一个类扩展行为有两种选择:



  • 设计一个继承它的子类

  • 使用装饰者模式对该类进行装饰


那么枪支部件合适用继承方式来设计吗?显然不合适,因为一个部件可以装配在不只一种枪上,所以继承这种方式排除。另一种方式,使用装饰者模式有一个很大的优势,在于符合“组合优于继承”的设计原则,我们知道,部件可以和任意枪组合,显示,使用装饰者模式来设计枪支部件是一个不错的选择:


/**
* 枪支部件
*
* @author GitLqr
*/
abstract class GunPart(protected val gun: Gun) : Gun

/**
* 消声器
*
* @author GitLqr
*/
class Muffler(gun: Gun) : GunPart(gun) {
override fun attack() = gun.attack() - 5

override fun noise() = 0f

override fun prodDate() = gun.prodDate()
}

/**
* 燃烧子弹
*
* @author GitLqr
*/
class FireBullet(gun: Gun) : GunPart(gun) {
override fun attack() = gun.attack() + 200

override fun noise() = gun.noise()

override fun prodDate() = gun.prodDate()
}

程序设计时,装饰器(部件)会引用被装饰实例(枪),并实现被装饰实例的所有接口,然后在需要增强的接口方法中加入增强逻辑。因为枪支部件 GunPart 接收 Gun 类型构造参数,而且本身也是 Gun 接口的实现类,所以,可以让多种枪支部件 GunPart 嵌套修饰枪实例:


// 使用
var ump9: Gun = Ump9Gun()
println("装配前:ump9 攻击力 ${ump9.attack()},噪音 ${ump9.noise()}")
ump9 = Muffler(FireBullet(ump9)) // 装配了 燃烧子弹、消声器 的ump9
println("装配后:ump9 攻击力 ${ump9.attack()},噪音 ${ump9.noise()}")

// 输出
装配前:ump9 攻击力 100.0,噪音 20.0
装配后:ump9 攻击力 295.0,噪音 0.0

三、改良装饰者模式



  • 例子:枪支部件

  • 重点:类委托(by 关键字)


在上面的例子中,装饰者模式可以很好的解决实例组合的情况,但是代码还是显得比较啰唆,因为需要重写所有的装饰对象方法,所以可能会存在大量样板代码。比如 FireBullet 只装饰增强 attack() 方法,而 noise()prodDate() 均不做修改,但还要是把这两个方法重写一遍。Kotlin 中有类委托特性,利用 by 关键字,将装饰类的所有方法委托给一个被装饰的类对象,然后只需覆写装饰的方法即可:


/**
* 枪支部件
*
* @author GitLqr
*/
abstract class GunPart(protected val gun: Gun) : Gun by gun

/**
* 消声器
*
* @author GitLqr
*/
class Muffler(gun: Gun) : GunPart(gun) {
override fun attack() = gun.attack() - 5
override fun noise() = 0f
}

/**
* 燃烧子弹
*
* @author GitLqr
*/
class FireBullet(gun: Gun) : GunPart(gun) {
override fun attack() = gun.attack() + 200
}

可以看到,使用类委托之后,装饰类 FireBullet 中的样板代码不用重写了,从而减少了代码量。


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

开源框架 Egg.js 文档未经授权被转载,原作者反成“恶人”在 v2ex 上被讨伐

5 月 26 日,Egg.js(阿里开源的企业级 Node.js 框架)核心开发者 @“天猪”在知乎发了一篇题为《关于我个人“恶意投诉”别人未授权转载事件的说明》的声明,对近期自己反成“恶人”在 v2ex 上被“讨伐”的事情表示困惑。开发者原文转载 MIT L...
继续阅读 »

5 月 26 日,Egg.js(阿里开源的企业级 Node.js 框架)核心开发者 @“天猪”在知乎发了一篇题为《关于我个人“恶意投诉”别人未授权转载事件的说明》的声明,对近期自己反成“恶人”在 v2ex 上被“讨伐”的事情表示困惑。


开发者原文转载 MIT License 协议文档

被知乎告侵权


原来在很多年前,@天猪 写了一篇关于 Egg.js 某个开源项目的某个特性的使用文档,并于 2018 年将该文档发布到了 2 个地方 —— Egg.js 知乎专栏(文档 A)和 Egg.js 的 GitHub repo 文档库(文档 B)。

其中,文档 A 的版权已授权给知乎,而发布在 GitHub 上的文档 B 则采用了 MIT License 协议。

文档地址:https://zhuanlan.zhihu.com/p/35334932

值得注意的是,发布到这两个地方的内容(文档 A 和文档 B )大部分是重合的。

2019 年,开发者 @an168bang521 在未告知原作者@“天猪”的前提下,从 GitHub 将 Egg.js(文档 B)原文转载到了其个人网站上。(现文章已删除)

地址:https://www.axihe.com/edu/egg/tutorials/typescript.html


但由于 Egg.js 文档(文档 B)使用的是 MIT License 协议,即“允许任何人在 MIT 协议下进行使用和操作”,因此开发者 @an168bang521 原封不动转载 该文档就引发了争议。

Eggjs 使用的 MIT LICENS 链接: https://github.com/eggjs/egg/blob/master/LICENSE

随后,开发者 @an168bang521 搬运自文档 B(采用了 MIT License)的个人网页收到来自知乎的 “侵权告知函” 。

因此,这位开发者 @an168bang521 才终于想起了 Egg.js 文档(文档 B)的原作者,并在知乎平台上发私信给@“天猪”。
 

未经授权被转载

Egg.js 文档原作者反成恶人

在 v2ex 上被“讨伐”


也就在发布这篇声明的前一天晚上,@“天猪” 刚刚收到了这位开发者 @an168bang521 的私信邮件。

在邮件中,该开发者称自己因在 2019 年摘抄了原作者 @“天猪” 的一篇“开源软件 Egg.js 在 GitHub 的技术文档”而被知乎告知侵权,且收到了知乎委托的公司发送的 “侵权告知函”。

开发者 @an168bang521 表示,因为文档 B 使用的是 MIT License 协议,因此自己“大段使用该仓库内的文档,是属于 MIT 里的使用、复制、修改、合并、发布、分发、再许可或出售”。

对此,Egg.js 核心维护者@“天猪”回应称,这因为他们在知乎的专栏(文档 A)已授权给平台的版权服务,(但由于文档 A 和 文档 B 的内容大部分重合)因此当知乎平台检测到对应的文章被未授权转载时,就会自动发送侵权通知。

让人意外的是,在收到该邮件的第二天,就在@“天猪”莫名其妙且感到困惑的时候,该开发者 @an168bang521 已经将该事件的帖子发布在了 v2ex 上,且遭到了来自评论区一堆回复者的“讨伐”。

事情发展到这里,作者@“天猪”才发现:自己的开源 Egg.js 技术文档未经授权被转载,现在自己反而被迫变成了“恶人和小丑”?

随后,@“天猪”开始重视该事件,且正式着手研究关于“基于MIT 协议的开源框架文档未授权被转载”的法律相关事宜。

目前,@“天猪”在该声明中已经附上了自己的“诉求”——“唯一的要求就是:事先跟我打招呼获取授权,注明原文出处,不要破坏文章结构以及加太多广告。”

@“天猪”表示,关于文档站的三方发布问题,自己的观点跟去年 Vue @尤雨溪的一样 —— 关于文档,协议和版权,主要期望的是:及时同步 + 注明非官方 + 出处 + 不要破坏文章结构以及加太多广告。

最后,@“天猪”也强调,正“因为我们都热爱开源,所以基本上都默认 MIT ,真的要用,我们也似乎没有太多的办法,如果三方有过度的行为,也只能倒逼我后续的开源项目都会重新考虑开源协议。”

关于该事件的后续发展,本站 Segmentfault 编辑部也将持续关注,如果您对该事件有相关看法,也欢迎在评论区留言互动。

参考链接:https://zhuanlan.zhihu.com/p/520119900
收起阅读 »

<版本>Android统一依赖管理

总结: 在多module的项目中,对版本的统一管理很重要,可以避免多个版本库的冲突问题,也方便日后的统一升级等等 Android的版本依赖的统一管理,有三种方式: 传统apply from的方式 buildsrc方式 composing builds方式 ...
继续阅读 »

总结:


在多module的项目中,对版本的统一管理很重要,可以避免多个版本库的冲突问题,也方便日后的统一升级等等


Android的版本依赖的统一管理,有三种方式:



  • 传统apply from的方式

  • buildsrc方式

  • composing builds方式


一、传统apply from的方式


在根目录新建一个config.gradle(或其他随意的xxx.gradle)文件
或在根目录的build.gradle定义一些变量
如:


ext {
android = [
compileSdkVersion: 30,
buildToolsVersion: "30",
minSdkVersion : 16,
targetSdkVersion : 28,
versionCode : 100,
versionName : "1.0.0"
]

versions = [
appcompatVersion : "1.1.0",
coreKtxVersion : "1.2.0",
supportLibraryVersion : "28.0.0",
glideVersion : "4.11.0",
okhttpVersion : "3.11.0",
retrofitVersion : "2.3.0",
constraintLayoutVersion: "1.1.3",
gsonVersion : "2.8",
//等等······
]

dependencies = [
//base
"constraintLayout" : "androidx.constraintlayout:constraintlayout:${version["constraintLayoutVersion"]}",
"appcompat" : "androidx.appcompat:appcompat:${version["appcompatVersion"]}",
"coreKtx" : "androidx.core:core-ktx:${version["coreKtxVersion"]}",
//等等······
]
}

在工程的根目录build.gradle添加:


apply from"config.gradle"

在需要依赖的modulebuild.gradle中,依赖的方式如下:


dependencies {
...
// 添加appcompatVersion依赖
api rootProject.ext.dependencies["appcompatVersion"]
...
}

【缺点】



  • 无法跟踪代码,需要手动搜索相关的依赖

  • 可读性很差


二、buildsrc方式


什么是buildsrc


当运行 gradle 时会检查项目中是否存在一个名为 buildsrc 的目录。然后 gradle 会自动编译并测试这段代码,并将其放入构建脚本的类路径中。


对于多项目构建,只能有一个 buildsrc 目录,该目录必须位于根项目目录中, buildsrc 是 gradle 项目根目录下的一个目录,它可以包含我们的构建逻辑。


与脚本插件相比,buildsrc 应该是首选,因为它更易于维护、重构和测试代码。


优缺点:


1】优点:



  • buildSrc是Android默认插件,共享 buildsrc 库工件的引用,全局只有这一个地方可以修改

  • 支持自动补全,支持跳转。


2】缺点:



  • 依赖更新将重新构建整个项目,项目越大,重新构建的时间就越长,造成不必要的时间浪费。


Gradle 文档



A change in buildSrc causes the whole project to become out-of-date. Thus, when making small incremental changes, the --no-rebuild command-line option is often helpful to get faster feedback. Remember to run a full build regularly or at least when you’re done, though.


buildSrc的更改会导致整个项目过时,因此,在进行小的增量更改时,-- --no-rebuild命令行选项通常有助于获得更快的反馈。不过,请记住要定期或至少在完成后运行完整版本。



使用方式:


参考:Kotlin + buildSrc for Better Gradle Dependency Management




  • 在项目根目录下新建一个名为 buildSrc 的文件夹(名字必须是 buildSrc,因为运行 Gradle 时会检查项目中是否存在一个名为 buildSrc 的目录)




  • 在 buildSrc 文件夹里创建名为 build.gradle.kts 的文件,添加以下内容




plugins {
`kotlin-dsl`
}
repositories{
jcenter()
}


  • 在 buildSrc/src/main/java/包名/ 目录下新建 Deps.kt 文件,添加以下内容


object Versions {
......

val appcompat = "1.1.0"

......
}

object Deps {
......

val appcompat = "androidx.appcompat:appcompat:${Versions.appcompat}"

......
}


  • 重启 Android Studio,项目里就会多出一个名为 buildSrc 的 module,实现效果


示意图


三、composing builds方式


摘自 Gradle 文档:复合构建只是包含其他构建的构建. 在许多方面,复合构建类似于 Gradle 多项目构建,不同之处在于,它包括完整的 builds ,而不是包含单个 projects



  • 组合通常独立开发的构建,例如,在应用程序使用的库中尝试错误修复时

  • 将大型的多项目构建分解为更小,更孤立的块,可以根据需要独立或一起工作


优缺点:


1】优点:



  • 支持单向跟踪

  • 自动补全

  • 依赖更新时,不会重新构建整个项目


2】缺点:



  • 需要在每一个module中都添加相应的插件引用


使用方式:


参考Gradle文档



  • 新建的 module 名称 VersionPlugin(名字随意)

  • 在 versionPlugin 文件夹下的 build.gradle 文件内,添加以下内容


buildscript {
repositories {
jcenter()
}
dependencies {
// 因为使用的 Kotlin 需要需要添加 Kotlin 插件
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10"
}
}

apply plugin: 'kotlin'
apply plugin: 'java-gradle-plugin'
repositories {
// 需要添加 jcenter 否则会提示找不到 gradlePlugin
jcenter()
}

gradlePlugin {
plugins {
version {
// 在 app 模块需要通过 id 引用这个插件
id = 'com.yu.plugin'
// 实现这个插件的类的路径
implementationClass = 'com.yu.versionplugin.VersionPlugin'
}
}
}


  • 在 VersionPlugin/src/main/java/包名/ 目录下新建 DependencyManager.kt 文件,添加相关的依赖配置,如:


package com.yu.versionplugin

/**
* 配置和 build相关的
*/
object BuildVersion {
const val compileSdkVersion = 29
const val buildToolsVersion = "29.0.2"
const val minSdkVersion = 17
const val targetSdkVersion = 26
const val versionCode = 102
const val versionName = "1.0.2"
}

/**
* 项目相关配置
*/
object BuildConfig {
//AndroidX
const val appcompat = "androidx.appcompat:appcompat:1.2.0"
const val constraintLayout = "androidx.constraintlayout:constraintlayout:2.0.4"
const val coreKtx = "androidx.core:core-ktx:1.3.2"
const val material = "com.google.android.material:material:1.2.1"
const val junittest = "androidx.test.ext:junit:1.1.2"
const val swiperefreshlayout = "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
const val recyclerview = "androidx.recyclerview:recyclerview:1.1.0"
const val cardview = "androidx.cardview:cardview:1.0.0"

//Depend
const val junit = "junit:junit:4.12"
const val espresso_core = "com.android.support.test.espresso:espresso-core:3.0.2"
const val guava = "com.google.guava:guava:24.1-jre"
const val commons = "org.apache.commons:commons-lang3:3.6"
const val zxing = "com.google.zxing:core:3.3.2"

//leakcanary
const val leakcanary = "com.squareup.leakcanary:leakcanary-android:2.4"

//jetPack
const val room_runtime = "androidx.room:room-runtime:2.2.5"
const val room_compiler = "androidx.room:room-compiler:2.2.5"
const val room_rxjava2 = "androidx.room:room-rxjava2:2.2.5"
const val lifecycle_extensions = "android.arch.lifecycle:extensions:1.1.1"
const val lifecycle_compiler = "android.arch.lifecycle:compiler:1.1.1"
const val rxlifecycle = "com.trello.rxlifecycle3:rxlifecycle:3.1.0"
const val rxlifecycle_components = "com.trello.rxlifecycle3:rxlifecycle-components:3.1.0"

//Kotlin
const val kotlinx_coroutines_core = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7"
//...
}


  • 在 VersionPlugin/src/main/java/包名/ 目录下新建 VersionPlugin.kt,实现Plugin接口,如下:


package com.yu.versionplugin

import org.gradle.api.Plugin
import org.gradle.api.Project

class VersionPlugin : Plugin<Project>{
override fun apply(p0: Project) {

}

companion object{

}
}

项目目录结构



  • settings.gradle 文件内添加如下代码,并重启 Android Studio


//注意是 includeBuild
includeBuild 'VersionPlugin'


  • app 模块 build.gradle 文件内 首行 添加以下内容


plugins{
// 这个 id 就是在 VersionPlugin 文件夹下 build.gradle 文件内定义的 id
id "com.yu.plugin"
}
// 定义的依赖地址
import com.yu.versionplugin.*


  • 使用如下:


import com.yu.versionplugin.*

plugins {
id 'com.android.application'
id 'kotlin-android'
id 'com.yu.plugin'
}

android {
compileSdk 32

defaultConfig {
applicationId "com.yu.versiontest"
minSdk BuildVersion.minSdkVersion
targetSdk BuildVersion.targetSdkVersion
versionCode BuildVersion.versionCode
versionName BuildVersion.versionName
}
//.....
}

dependencies {

implementation BuildConfig.coreKtx
implementation BuildConfig.appcompat
implementation BuildConfig.material
//......
}

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

如何解决Flutter的WebView白屏和视频自动播放

前言 众所周知,Flutter 的 WebView 不太友好,用起来不顺手。 我们 Flutter 开发常用的 WebView 库有2个,一个是 Flutter 官方自己出的 webview_flutter ,另一个是比较流行的 flutter_inappwe...
继续阅读 »

前言


众所周知,Flutter 的 WebView 不太友好,用起来不顺手。
我们 Flutter 开发常用的 WebView 库有2个,一个是 Flutter 官方自己出的 webview_flutter ,另一个是比较流行的 flutter_inappwebview 。这两个库其实差不多,flutter_inappwebview 功能比较丰富,封装了很多事件、方法等,但是很多问题这两个库都会遇到。本文以 webview_flutter 为基础库展开讲解相关问题以及解决方案。


问题


白屏、UI错乱




如上图所示



  • 测试的时候发现部分手机(如OPPO)会出现白屏现象(左图)

  • 原生与 Flutter 混编,打开页面会发现页面布局变了,顶部banner变小了(右图)


查阅网上的一些解决方案,千篇一律都是:


if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView();

但是,这样设置其实是不对的,还会出现以上问题,真正的解决方案是:


if (Platform.isAndroid) WebView.platform = AndroidWebView();

视频自动播放


由于需求需要,打开页面的时候,列表的第一个视频(YouTube/Facebook 视频)需要自动播放。
但是发现没法自动播放,如下图,会出现播放之后马上暂停的现象。



查阅资料得知,是谷歌浏览器的隐私政策导致的。



所以要想视频自动播放,有两种方案:



  • 静音播放。

    • 在 Web 端调用视频播放器的静音即可自动播放。



  • 模拟点击。

    • 给 WebView 设置一个 GlobalKey 。
      WebView(
      key: logic.state.videoGlobalKey,
      ......
      );
      }


    • 然后在 WebView 的 onPageFinished 方法里,通过 GlobalKey 获取 WebView 的位置,从而进行模拟点击,就可以自动播放视频了。
      var currentContext = state.videoGlobalKey.currentContext;
      var offset = (currentContext?.findRenderObject() as RenderBox)
      .localToGlobal(Offset.zero);
      //模拟点击
      var addPointer = PointerAddedEvent(
      pointer: 0,
      position: Offset(
      offset.dx + 92.w,
      offset.dy + 92.w),
      );
      var downPointer = PointerDownEvent(
      pointer: 0,
      position: Offset(
      offset.dx + 92.w,
      offset.dy + 92.w),
      );
      var upPointer = PointerUpEvent(
      pointer: 0,
      position: Offset(
      offset.dx + 92.w,
      offset.dy + 92.w),
      );
      GestureBinding.instance!.handlePointerEvent(addPointer);
      GestureBinding.instance!.handlePointerEvent(downPointer);
      GestureBinding.instance!.handlePointerEvent(upPointer);





这两种方案各有利弊,方案一无法播放声音(需要用户手动点击开启声音),方案二偶尔会有误触的操作。我们 APP 通过与产品商量最终选取的是方案一的解决方案。


另外 iOS 端自动播放会自动全屏,需要设置以下属性:


WebView(
key: logic.state.videoGlobalKey,
// 允许在线播放(解决iOS播放视频自动全屏)
allowsInlineMediaPlayback: true,
......
);
}

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

台积电多人离职:老婆受不了

作为全球晶圆代工的一哥,台积电这几年成了香饽饽,营收及盈利也大涨,员工待遇也是业内最高的,今年4月份还要全员加薪8%,然而高薪诱惑下还有大量员工离职,有员工爆料称这样的工作方式已经影响家庭和谐。有网友在论坛上发帖请教台积电内部员工,称自己的同学之前离开了台积电...
继续阅读 »

作为全球晶圆代工的一哥,台积电这几年成了香饽饽,营收及盈利也大涨,员工待遇也是业内最高的,今年4月份还要全员加薪8%,然而高薪诱惑下还有大量员工离职,有员工爆料称这样的工作方式已经影响家庭和谐。

有网友在论坛上发帖请教台积电内部员工,称自己的同学之前离开了台积电,现在看到加薪8%之后又有些后悔,想知道大家为什么在台积电有这么丰厚的收入还要离职呢?


很快这个帖子就变成了吐槽大会,台积电内部员工抱怨起台积电的工作制度了,直言钱已经不重要了。

11、12点下班隔天6点跟国外开会,我朋友姐夫是这样,老婆受不了。

生活品质很差,不然为何老外做不起来?

有钱没生活,当过半导体制造厂工程师就懂

新同事硕士毕业去2年肝指数上升很多

完全无法理解只有工作跟睡觉的生活

用命换的你是能待多久

里面的想出来,外面的想进去,离职时到七厂人资办理,真的是要排队

要不要去台积电就是钱/生活的选择而已

因为台积电实际上跟外商比起来给的不算高薪又血汗。

3月初有报道称,今年预计招募超过8000名新员工,其中,硕士毕业工程师平均年薪上看200万新台币,约合人民币45万元。


今年2月份,台积电董事会批准了2021年薪酬奖励,去年员工业绩奖金与酬劳(分红)合计712亿290万元,其中员工业绩奖金约新台币356亿145万元已于每季季后发放,而酬劳(分红)约新台币356亿145万元将于今年七月发放。另外,搜索公众号Linux就该这样学后台回复“git书籍”,获取一份惊喜礼包。

台积电去年的总奖励相比2020年增长了2.4%,按照5.7万员工总数来看,人均奖励约为124万新台币,约合28.2万元人民币。

网友表示:“用钱换命,可以啊!多少人命在消耗,钱没进口袋。”

你还有什么想要补充的吗?

版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢!

收起阅读 »

恶意技术时代下的负责任技术

从数百万美元的勒索软件赔款,到数亿用户私人信息的数据泄露,围绕恶意技术的头条新闻一直很吸引眼球,但它们并不能说明全部问题。真实情况是,恶意技术远不止蓄意的、有针对性的破坏系统或窃取用户数据。尽管黑客攻击、勒索软件攻击、数据泄露和 DDoS 攻击在这类新闻叙述中...
继续阅读 »

从数百万美元的勒索软件赔款,到数亿用户私人信息的数据泄露,围绕恶意技术的头条新闻一直很吸引眼球,但它们并不能说明全部问题。

真实情况是,恶意技术远不止蓄意的、有针对性的破坏系统或窃取用户数据。尽管黑客攻击、勒索软件攻击、数据泄露和 DDoS 攻击在这类新闻叙述中占据着主导地位,也的确造成了严重损害,但实际上它们只是沧海一粟。

认识所有形式的恶意技术行为

首先要摆正心态,不要对 "恶意技术 "一词感到过于恐慌。这里谈论的不仅是技术攻击本身,我们需要更广泛地思考这一问题。

恶意技术并不单纯指非法行为,我们甚至不是在谈论那些必然是恶意的事情。例如,有些人完全乐于接受在线监控,因为这可以帮助他们获得更精准、更具个性化的推荐。与此同时也有另外一些人会不遗余力地躲避任何数字监控的追踪,因为这在他们看来是不道德的。

有些技术看似恶意,其出发点却并不如此。例如,图像识别软件的开发者不会刻意让软件在识别到黑人妇女面孔时提供不一致的结果,只是由于软件现阶段有一个糟糕的数据集,才会呈现出看似有偏见的结果,而这些并非开发者的本意。这些都不是恶意的项目,当然也不归属于恶意的品牌。在许多情况下,它们甚至不是设计或规划上的失败,而是团队没有充分考虑到一项技术决定会对所有潜在的利益相关者群体产生怎样不同的影响。

这是一个关键点。在设计之初,软件通常有一个特定的利益相关者群体,设计者试图直接服务或满足这一群体的需求,却常常忽视了该产品对其他利益相关者的影响。比如训练一个自然语言处理模型所产生的二氧化碳排放足迹,与纽约和北京之间往返125次航班相同——很少有人考虑到这种环境上的影响。除此之外还有很多人忽视的公平、公正问题。例如,疫情期间很多教育转入线上,一些家庭没有良好的网络情况,根本无法同时支撑几个孩子共同参与线上课程,更不用说父母也要同时在家办公。许多人看到的是数字教育的精彩革命,但他们没有看到那些因数字不平等而被落下的人。

为什么现在是拥抱道德技术的正确时机?

疫情所暴露的数字不平等和正在发生的气候危机只是原因之二。现如今,技术几乎深深扎根于我们生活的各个方面——医疗决定、信贷决定、缓刑或判刑决定,所有这些对个人生活有巨大影响的事情,都受到技术选择的巨大影响。这其中的利害关系是非常真实的,而技术决策者在做这些决定时却很少做到全面考虑。这就是为什么对各种组织来说,树立负责任的技术思维是如此重要。

负责任技术的定义

负责任技术——有时也被称为道德技术或公平技术——是一个总括性的术语,包含了多种概念,总之就是用技术做正确的事情——这可能意味着任何事情,从采取措施使更容易获得一个应用程序,到实施政策以帮助持续提供公平的技术体验。

从字面上看,这是一个相对简单的概念,但仍然被误解所笼罩。我最近读到一些信息,说“责任”在土木工程等领域很容易定义,你的责任是确保建筑物稳定,不倒塌,不对相关居民的生活产生负面影响。其隐藏的含义是,在软件或技术领域,这一点很难定义。

是的,我们并不像医疗行业那样受到任何形式的“希波克拉底誓言”的约束。但是,当涉及到用技术做出道德以及负责任的决定时,我们常常给自己留了过多的余地。

我们可以采取哪些措施来变得更加负责?

为了减少技术所附带的无意伤害并做出负责任的决定,今天的企业需要着重关注的是,识别可能受到特定技术决定影响的潜在利益相关者。这意味着需要关注:

  • 受测群体——他们是否真正代表了预期中将使用该产品的终端用户群体?这个过程是否涵盖了所有利益相关群体,他们是否有发声渠道?

  • 服务所需数据集的质量和准确性——是否没有偏见,是否能够提供真实、全面的反馈和包容性体验?

  • 设计中是否考虑到了平等和易用性——以及复杂的特征或功能是否以整体可用性和可及性为代价?

  • 这一决策是否会对人类社会产生更广义的负面影响?例如,它是否与我们的可持续发展目标相一致,是否有可能破坏环境?

我希望鼓励组织做的另一件事是,明确声明组织关心什么,以及希望所采用的技术能帮助实现什么。正如《数学毁灭武器》的作者Cathy O'Neil所说,有些时候,你必须在公平和利润之间进行交易。

这取决于你想在这个光谱上处于什么位置,但重要的是要明确目标和意图。我曾与一个组织合作,该组织制定了一个框架,表达了他们在使用客户数据方面的价值观和原则,清楚地阐述了他们打算如何操作这些数据,以及为什么会做出这个决定。这花了几个月的时间,但这使他们的意图和道德立场十分清晰,并且容易保持一致。

减轻技术的无意识伤害

技术所产生的无意识伤害有多种形式,可能潜伏在任何技术决策中。作为技术管理者,我们有责任提出正确的问题,并考虑这些技术将如何被每个人使用,以及可能对使用者的生活和经历产生怎样的影响。

转向这种负责任的方法和心态在理论上相对简单,但在看到有意义的结果之前,整个行业的组织和专业人士需要作出真正的奉献。正如我们有责任保护客户数据免受恶意威胁一样,我们也有道德责任尽我们所能减少技术所产生的负面影响,并为所有人建立一个平等、可访问的数字世界。

来源: Thoughtworks洞见

收起阅读 »

IE浏览器6月退役 微软力推Edge:渲染时间减少85%、CPU内存占用大降

前不见微软宣布IE浏览器6月退役,这个始于Win95时代的浏览器在27年后还是要被放弃了,微软现在的重点放在了Edge浏览器上,不仅技术更新,而且性能更好,CPU及内存占用大幅下降。在日前的微软Build 2022大会上,微软介绍了Edge浏览器的进展,很快就...
继续阅读 »

前不见微软宣布IE浏览器6月退役,这个始于Win95时代的浏览器在27年后还是要被放弃了,微软现在的重点放在了Edge浏览器上,不仅技术更新,而且性能更好,CPU及内存占用大幅下降。

在日前的微软Build 2022大会上,微软介绍了Edge浏览器的进展,很快就会升级WebView2,届时Edge浏览器性能会得到更好的提升。

IE浏览器6月退役 微软力推Edge:渲染时间减少85%、CPU内存占用大降

微软也公布了对比结果,与使用Internet Explorer运行他们的解决方案相比,使用 Edge WebView2,可以将渲染时间减少了85%,CPU占用率降低33%,内存占用率也会降低32%,改善明显。

4月初,根据分析机构StatCounter的数据,Edge成功实现了对Safari的反超。

截止2022年3月,Edge获得了9.65%的市场份额,超过Safari的9.56%成功夺得第二名,但与Chrome堪称恐怖的67.29%相比,依旧存在明显的差距。

IE浏览器6月退役 微软力推Edge:渲染时间减少85%、CPU内存占用大降

来源:tech.ifeng.com/c/8GKXHf1ZEWO

收起阅读 »

微前端框架 qiankun 技术分析

如何加载子应用single-spa 通过 js entry 的形式来加载子应用。而 qiankun 采用了 html entry 的形式。这两种方式的优缺点我们在理解微前端技术原理中已经做过分析,这里不再赘述,我们看看 qiankun 是如何实现 html e...
继续阅读 »

如何加载子应用

single-spa 通过 js entry 的形式来加载子应用。而 qiankun 采用了 html entry 的形式。这两种方式的优缺点我们在理解微前端技术原理中已经做过分析,这里不再赘述,我们看看 qiankun 是如何实现 html entry 的。

qiankun 提供了一个 API registerMicroApps 来注册子应用,其内部调用 single-spa 提供的 registerApplication 方法。在调用 registerApplication 之前,会调用内部的 loadApp 方法来加载子应用的资源,初始化子应用的配置。

通过阅读 loadApp 的代码,我们发现,qiankun 通过 import-html-entry 这个包来加载子应用。import-html-entry 的作用就是通过解析子应用的入口 html 文件,来获取子应用的 html 模板、css 样式和入口 JS 导出的生命周期函数。

import-html-entry

import-html-entry 是这样工作的,假设我们有如下 html entry 文件:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>test</title>
</head>
<body>

<!-- mark the entry script with entry attribute -->
<script src="https://unpkg.com/mobx@5.0.3/lib/mobx.umd.js" entry></script>
<script src="https://unpkg.com/react@16.4.2/umd/react.production.min.js"></script>
</body>
</html>

我们使用 import-html-entry 来解析这个 html 文件:

import importHTML from 'import-html-entry';

importHTML('./subApp/index.html')
.then(res => {
console.log(res.template);

res.execScripts().then(exports => {
const mobx = exports;
const { observable } = mobx;
observable({
name: 'kuitos'
})
})
});

importHTML 的返回值有如下几个属性:

  • template 处理后的 HTML 模板
  • assetPublicPath 静态资源的公共路径
  • getExternalScripts 获取所有外部脚本的函数,返回脚本路径
  • getExternalStyleSheets 获取所有外部样式的函数,返回样式文件的路径
  • execScripts 执行脚本的函数

在 importHTML 的返回值中,除了几个工具类的方法,最重要的就是 template 和 execScripts 了。

importHTML('./subApp/index.html') 的整个执行过程代码比较长,我们只讲一下大概的执行原理,感兴趣的同学可以自行查看importHTML 的源码

importHTML 首先会通过 fetch 函数请求具体的 html 内容,然后在 processTpl 函数 中通过一系列复杂的正则匹配,解析出 html 中的样式文件和 js 文件。

importHTML 函数返回值为 { template, scripts, entry, styles },分别是 html 模板,html 中的 js 文件(包含内嵌的代码和通过链接加载的代码),子应用的入口文件,html 中的样式文件(同样是包含内嵌的代码和通过链接加载的代码)。

之后通过 getEmbedHTML 函数 将所有使用外部链接加载的样式全部转化成内嵌到 html 中的样式。getEmbedHTML 返回的 html 就是 importHTML 函数最终返回的 template 内容。

现在,我们看看 execScripts 是怎么实现的。

execScripts 内部会调用 getExternalScripts 加载所有 js 代码的文本内容,然后通过 eval("code") 的形式执行加载的代码。

注意,execScripts 的函数签名是这样的 (sandbox?: object, strictGlobal?: boolean, execScriptsHooks?: ExecScriptsHooks): Promise<unknown>。允许我们传入一个沙箱对象,如果子应用按照微前端的规范打包,那么会在全局对象上设置 mountunmount 这几个生命周期函数属性。execScripts 在执行 eval("code") 的时候,会巧妙的把我们指定的沙箱最为全局对象包装到 "code" 中,子应用能够运行在沙盒环境中。

在执行完 eval("code") 以后,就可以从沙盒对象上获取子应用导出的生命周期函数了。

loadApp

现在我们把视线拉回 loadApp 中,loadApp 在获取到 templateexecScripts 这些信息以后,会基于 template 生成 render 函数用于渲染子应用的页面。之后会根据需要生成沙盒,并将沙盒对象传给 execScripts 来获取子应用导出的声明周期函数。

之后,在子应用生命周期函数的基础上,构建新的生命周期函数,再调用 single-spa 的 API 启动子应用。

在这些新的生命周期函数中,会在不同时机负责启动沙盒、渲染子应用、清理沙盒等事务。

隔离

在完成子应用的加载以后,作为一个微前端框架,要解决好子应用的隔离问题,主要要解决 JS 隔离和样式隔离这两方面的问题。

JS 隔离

qiankun 为根据浏览器的能力创建两种沙箱,在老旧浏览器中会创建快照模式 的浏览器中创建 VM 模式的沙箱 ProxySandbox

篇幅限制,我们只看 ProxySandbox 的实现,在其构造函数中,我们可以看到具体的逻辑:首先会根据用户指定的全局对象(默认是 window)创建一个 fakeWindow,之后在这个 fakeWindow 上创建一个 proxy 对象,在子应用中,这个 proxy 对象就是全局变量 window

constructor(name: string, globalContext = window) {
const { fakeWindow, propertiesWithGetter } = createFakeWindow(globalContext);
const proxy = new Proxy(fakeWindow, {
set: (target: FakeWindow, p: PropertyKey, value: any): boolean => {},
get: (target: FakeWindow, p: PropertyKey): any => {},
has(target: FakeWindow, p: string | number | symbol): boolean {},

getOwnPropertyDescriptor(target: FakeWindow, p: string | number | symbol): PropertyDescriptor | undefined {},

ownKeys(target: FakeWindow): ArrayLike<string | symbol> {},

defineProperty(target: Window, p: PropertyKey, attributes: PropertyDescriptor): boolean {},

deleteProperty: (target: FakeWindow, p: string | number | symbol): boolean => {},

getPrototypeOf() {
return Reflect.getPrototypeOf(globalContext);
},
});
this.proxy = proxy;
}

其实 qiankun 中的沙箱分两个类型:

  • app 环境沙箱
    app 环境沙箱是指应用初始化过之后,应用会在什么样的上下文环境运行。每个应用的环境沙箱只会初始化一次,因为子应用只会触发一次 bootstrap 。子应用在切换时,实际上切换的是 app 环境沙箱。
  • render 沙箱
    子应用在 app mount 开始前生成好的的沙箱。每次子应用切换过后,render 沙箱都会重现初始化。

上面说的 ProxySandbox 其实是 render 沙箱。至于 app 环境沙箱,qiankun 目前只针对在应用 bootstrap 时动态创建样式链接、脚本链接等副作用打了补丁,保证子应用切换时这些副作用互不干扰。

之所以设计两层沙箱,是为了保证每个子应用切换回来之后,还能运行在应用 bootstrap 之后的环境下。

样式隔离

qiankun 提供了多种样式隔离方式,隔离效果最好的是 shadow dom,但是由于其存在诸多限制,qiankun 官方在将来的版本中将会弃用,转而推行 experimentalStyleIsolation 方案。

我们可以通过下面这段代码看到 experimentalStyleIsolation 方案的基本原理。

const styleNodes = appElement.querySelectorAll('style') || [];
forEach(styleNodes, (stylesheetElement: HTMLStyleElement) => {
css.process(appElement!, stylesheetElement, appInstanceId);
});

css.process 的核心逻辑,就是给读取到的子应用的样式添加带有子应用信息的前缀。效果如下:

/* 假设应用名是 react16 */
.app-main {
font-size: 14px;
}

div[data-qiankun-react16] .app-main {
font-size: 14px;
}

通过上面的隔离方法,基本可以保证子应用间的样式互不影响。

小结

qiankun 在 single-spa 的基础上根据实际的生产实践开发了很多有用的功能,大大降低了微前端的使用成本。

本文仅仅针对如何加载子应用和如何做好子应用间的隔离这两个问题,介绍了 qiankun 的实现。其实,在隔离这个问题上,qiankun 也仅仅是根据实际中会遇到的情况做了必要的隔离措施,并没有像 iframe 那样实现完全的隔离。我们可以说 qiankun 实现的隔离有缺陷,也可以说是 qiankun 在实际的业务需求和完全隔离的实现成本之间做的取舍。

原文:https://segmentfault.com/a/1190000041151414

收起阅读 »

pc端微信授权登录两种实现方式的总结

在开发pc端项目中,使用微信授权登录是种很常用的功能,目前在功能实现上有两种不同的方式,现根据两种方式做如下总结。一、跳转微信授权登录页面进行扫码授权这种方法实现非常简单只用跳转链接就可以实现微信授权登录window.location = https://op...
继续阅读 »

在开发pc端项目中,使用微信授权登录是种很常用的功能,目前在功能实现上有两种不同的方式,现根据两种方式做如下总结。

一、跳转微信授权登录页面进行扫码授权

这种方法实现非常简单只用跳转链接就可以实现微信授权登录

window.location = https://open.weixin.qq.com/connect/qrconnect?appid=${appid}&redirect_uri=${回调域名}/login&response_type=code&scope=snsapi_login&state=${自定义配置}#wechat_redirect

跳转之后进行微信扫码,之后微信会带着code,回调回你设置的回调域名,这之后拿到code再和后台进行交互,即可实现微信登陆。
这种方法相对来说实现起来非常简单,但是因为需要先跳转微信授权登录页面,在体验上来说可能不是太好。

二、在当前页面生成微信授权登录二维码

这种方法是需要引入wxLogin.js,动态生成微信登陆二维码,具体实现方法如下:

const s = document.createElement('script')
s.type = 'text/javascript'
s.src = 'https://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js'
const wxElement = document.body.appendChild(s)
wxElement.onload = function () {
var obj = new WxLogin({
id: 'wx_login_id', // 需要显示的容器id
appid: '', // 公众号appid
scope: 'snsapi_login', // 网页默认即可
redirect_uri:'', // 授权成功后回调的url
state: '', // 可设置为简单的随机数加session用来校验
style: 'black', // 提供"black"、"white"可选。二维码的样式
href: '' // 外部css(查看二维码的dom结构,根据类名进行样式覆盖)文件url,需要https
})
}

其中href参数项还可以通过node将css文件转换为data-url,实现方式如下:

var fs = require('fs');
function base64_encode(file) {
var bitmap = fs.readFileSync(file);
return 'data:text/css;base64,'+new Buffer(bitmap).toString('base64');
}
console.log(base64_encode('./qrcode.css'))

在终端对该js文件执行命令:

node qr.js

把打印出来的url粘贴到href即可。
这种实现方法避免了需要跳转新页面进行扫码,二维码的样式也可以进行更多的自定义设置,可能在体验上是更好的选择。

原文:https://segmentfault.com/a/1190000024492932


收起阅读 »

苹果:App自6月30日起支持删除账号,开发者相关问题都在这里了

今晨,苹果正式宣布自 2022 年 6 月 30 日起,提交至 App Store 且支持账号创建的应用,必须允许用户在应用内删除账号。6 月 30 日起,App 必须允许用户删除账号从 2022 年 6 月 30 日开始,App Store 内支持账号创建的...
继续阅读 »

今晨,苹果正式宣布自 2022 年 6 月 30 日起,提交至 App Store 且支持账号创建的应用,必须允许用户在应用内删除账号。

6 月 30 日起,App 必须允许用户删除账号

从 2022 年 6 月 30 日开始,App Store 内支持账号创建的应用,必须提供删除账号的功能。

1653473320(1).jpg

出海痛点很多?点击这里解决



开发者如需更新应用程序以完善删除账号功能,需要注意以下几点:

1)用户能在应用中快速找到删除账号的入口,一般可在账户设置中找到;

2)如果用户是通过 Apple ID 登录,需要在删除账号时使用 Sign in with Apple REST API 来撤销用户令牌;

发.png

3)用户删除账号不仅是暂时停用或禁用账号,苹果要求在应用内,所有与该账号相关的个人数据都可以被删除,以帮助用户更好地管理隐私数据;

4)受高度监管的应用可能需要提供额外的客户服务流程,以跟进账号删除过程;

5)遵守有关存储和保留用户账号信息以及处理账号删除的适用法律要求,包括遵守不同国家或地区的当地法律。

此外,如果用户需要访问网站以指引如何删除账号,开发者也需提供相关链接。

若删除账号需要额外的时间,或删除时应用购买问题需要另外解决,开发者也应告知用户。

App 删除账号功能相关问题

Q:开发者可以将用户引导到客户服务流程以完成账号删除吗?

A:受高度监管的应用,如中应用商店审查指南 5.1.1(ix)所述,可能会使用额外的客户服务流程来确认和促进账号删除过程。

不在高度监管的行业中运行的应用程序不应要求用户拨打电话、发送电子邮件或通过其他支持流程完成账号删除。

Q:开发者是否可以要求重新认证,或添加确认步骤以确保账号不会被意外删除或被账号持有人以外的人删除?

A:可以,确保删除动作是用户期望进行的。

开发者可以添加步骤来验证用户身份,并确认他们想要删除该账号(如通过输入已与该账号关联的电子邮件或电话号码)。

但是,给用户删除账号增加不必要的困难将不会通过审核。

Q:如应用使用 Sign in with Apple 为用户提供账号创建和身份验证,需要进行哪些更改?

A:支持 Sign in with Apple 的应用需要使用 Sign in with Apple REST API 来撤销用户令牌。更多信息,请查看苹果官方文档和设计建议。

Q:如果开发者的应用链接到默认网络浏览器以创建账号,是否仍需要在应用内提供账号删除功能?

A:是的。但请注意链接到默认 Web 浏览器进行登录或注册账号,会影响用户体验,具体可查看应用商店审查指南 4。

Q:应用会自动为用户创建一个账号,是否需要提供进行账号删除的选项?

A:是的。用户应该可以选择删除自动生成的账号(包括访客账号)以及与这些账号关联的数据。

同时,开发者需要确保应用中的任何账号创建都符合当地法律。

Q:账号删除是否必须立即自动完成?

A:不是,可以接受手动删除账号,并花费一些时间。

开发者需要通知用户删除账号需要多长时间,并在删除完成后提供确认,并确保删除账号所用的时间。

Q:删除账号后,用户产生的内容是否需要在共享的应用中删除?

A:是的。用户删除账号时,将删除与其账号关联的所有数据,包括与他人一起生成的内容,如照片、视频、文字帖子和评论等。

如果当地法律要求开发者维护某些数据,请另外告知用户。

Q:是否允许应用只在某些地方根据 CCPA、GDPR 或其他当地法律删除账号?

A:不可以。应该允许所有用户删除他们的账号,无论他们身在何处,开发者的账号删除流程也需要提供给所有用户。

Q:如何管理自动续订的用户,以免在用户删除账号后意外收费?

A:告知用户管理订阅,后续计费将通过 Apple 继续,并提醒用户在下一次收费前取消订阅。

开发者使用 App Store 自动续订的 Server Notifications,可以实时查看用户的订阅状态,或者使用订阅状态 API 进行识别。

同时,开发者可以提供 Apple 支持链接(https: //support.apple.com/en-us/HT204084),帮助用户提交退款请求。

此外,开发者还可以提供一个选项,即设置账号删除日期与订阅到期时间一致,但仍需提供可立即删除账号的选项。

应用更新过程中的更多常见问题,可访问以下网站了解:

https://developer.apple.com/support/offering-account-deletion-in-your-app

据悉,苹果去年就已宣布调整 App Store 的指导方针,要求应用允许用户删除自己的账户,但由于功能实现较复杂,苹果两度推迟实行。如今正式推行,预计未来一段时间内或将有大量应用进行更新。

收起阅读 »

西电女生毕设找代笔,事后玩起“仙人跳”被举报

这是一场无比荒谬的闹剧:5 月 22 日,一名网友在西安电子科技大学贴吧发文控诉,声称自己在为该校两名学生代写论文后,反遭对方利用平台漏洞,以投诉为名进行敲诈,自己忍无可忍将其曝光。这一带有 " 黑吃黑 " 元素的离奇事件,瞬间一石激起千层浪。次日,西安电子科...
继续阅读 »

这是一场无比荒谬的闹剧:5 月 22 日,一名网友在西安电子科技大学贴吧发文控诉,声称自己在为该校两名学生代写论文后,反遭对方利用平台漏洞,以投诉为名进行敲诈,自己忍无可忍将其曝光。

这一带有 " 黑吃黑 " 元素的离奇事件,瞬间一石激起千层浪。次日,西安电子科技大学计算机科学与技术学院紧急发表声明,表示已开始调查学生雷某某、卢某某涉嫌找人有偿代做毕业设计的相关问题,并决定暂停了两人的毕业设计答辩工作。

当下正值毕业季,无数学生仍在忙于毕业论文(设计)的定稿和答辩。这一突如其来的学术丑闻,或许会使各大高校再度提高论文的审核标准,让毕业难上加难。因此不少人开玩笑称," 天临四年 " 还没结束,又迎来了 " 雷卢元年 "。

事实上,代写论文等学术不端行为,早已是老生常谈的问题。所以为什么这一事件曝光后,还会在全网引发轩然大波呢?

首先是动机问题。根据这位爆料的 " 枪手 " 所言,雷某某和卢某某两名学生先是通过闲鱼平台联系自己代做项目文件,而等到自己做完后,对方才说这是她们的毕业设计。

一般来说,普通项目文件(论文)的代写不违反法律关于著作权的规定,而毕业论文(设计)这类学位论文的代写,则毋庸置疑属于学术不端。

根据教育部 2013 年开始施行的《学位论文作假处理办法》,代写学位论文不仅会处理购买方,取消其学位申请资格,3 年内不得再次申请,甚至直接开除学籍;而且出售方也会被追究责任,属于在读学生的要开除学籍,属于教师或其他工作人员的要开除或解聘。

因此,这位 " 枪手 " 认为西电学生欺骗自己的行为是故意构陷,并且无端带来了极高的法律风险。

其次是行为问题。从曝光的聊天记录中可以看出,雷某某在早期沟通需求时,先是转发给了 " 枪手 " 几篇用以参考的论文,又要求增加在线学习的 Qlearning 算法。在 " 枪手 " 完成后,又要改回普通的 Qlearning,根据导师的要求增加新的内容,最后则又是要求通过 Dqn 算法实现。

经过了数次修改后," 枪手 " 认为多次修改需要加钱,而雷某某却认为 " 枪手 " 没能满足自己需求,要求退款。遭到拒绝后,甚至将 " 枪手 " 举报至平台客服,并且威胁他 "(客服)接了(投诉)要处理不能拖 "。

不仅如此,卢某某同 " 枪手 " 也经历了类似的交涉,最终双方没能达成交易。其中到底经历了什么,发帖爆料人并未详细多说。

根据爆料人描述,卢某某先是以修改项目骗他代写毕业设计,完了之后又以向闲鱼举报为由要求他退款。她因为 " 枪手 " 没能秒回自己信息而发火,直言 " 我不想举报你,你赶紧转账过来 "、" 我钱不要了,和你死磕到底 "、" 我要去举报正经维权 " 等,甚至用两分钟倒计时威胁,不转账就直接举报。

" 枪手 " 在西安电子科技大学贴吧发文控诉后不久,就有评论区的网友人肉出了雷某某和卢某某两人的身份信息,包括姓名、专业、班级,并且向她们的指导老师发邮件举报其学术不端行为。然而卢某某似乎依然没能认识到自己的错误,她多次联系 " 枪手 ",要求删帖减小影响。

当她意识到事态无法挽回之时,只是悻悻表示:" 对你来说是经济损失,可是对我们来说是真的没办法毕业的事情。" ——堪比琼瑶剧里那句 " 你失去的只是一条腿,她失去的可是爱情啊 ",都有着令人无比迷惑的逻辑和三观。

雷某某卢某某清楚自己找人代写论文的行为将会导致多么严重的后果吗?

根据《西安电子科技大学本科生考试纪律与学术规范条例》和《西安电子科技大学学位论文作假行为处理实施细则》的规定,一旦两人行为属实,至少需要延迟毕业 1 年,如若从严处理,还会被开除学籍,并取消学位申请资格。

像雷某某和卢某某这样找 " 枪手 " 代写论文的行为,在如今的大学生中并不少见。2018 年,微信公众号「大学声」联合全国学业发展联盟,进行了一次大学生毕业论文(设计)情况调查发现,38.4% 的大学生听说过一两个同学找代写,33% 大学生身边有一些找代写的同学,仅 28.6% 大学生周围同学完全没有找代写的现象。

■ 图源公众号「大学声」

虽然学位论文的查重率要求越来越严格,代写论文却依然屡禁不止。不同于抄袭,随时会被数据库筛查出来,代写则完全属于一个 " 自己不说就没人知道 " 的灰色地带。

正规的学位论文从选题、开题、撰写乃至修改的过程中,学生都需要与指导老师有充分沟通,一篇论文往往是几个月时间长期打磨的结果。然而代写论文大多数是临时 " 抱佛脚 ",正如雷某某和卢某某这样,在答辩前一个月才找 " 枪手 " 匆忙代写。

这样质量低下的代写论文最终能够瞒天过海,除了学生失职,指导老师的工作也有疏忽。他们不仅没有给予学生相应的关注,也没能发现学生在撰写论文中的问题和猫腻。这样 " 重结果、轻过程 " 的评价机制,给了学生代写论文的机会和胆量。

需求催生了市场,校园厕所的小广告、网络群组,甚至闲鱼这类二手交易平台中隐藏着巨大的论文代写生意。即使部分网络平台屏蔽了 " 论文代写 " 这一关键词,转去搜索 " 文章指导 " 或 " 文章创作 " 等词条,仍然会出现大量的论文代写 " 枪手 "。

在论文代写这一 " 灰色产业 " 中,由于缺少监管,许多学生在购买了代写服务后还会遇到各式各样的问题。根据《法治日报》调查发现,许多论文代写商家质量低下,内容大多为简单复制;坐地起价,修改或查看都需要额外支付费用;虚假包装,甚至将大一新生包装成名校硕士。

如果购买服务的学生不满意,去找客服申诉,得到的结果大多数是不欢而散,要么被对方拉黑,要么反被对方威胁要将文章上传至网络、告诉学校老师等。最后,找代写的学生往往选择暗自吃下闷亏。

这或许就是此次西电学生代写论文事件爆火的原因——从前都是 " 枪手 " 举报学生,如今是学生举报 " 枪手 " ——让灰色产业中的权力关系直接颠倒过来了,上演了一出意想不到的 " 黑吃黑 "。

" 枪手 " 将雷某某和卢某某两人曝光后,据称为了防止舆论升级,已经将帖子删除。诚然,我们无法用他的单方面信源去还原事件的全貌。但雷某某和卢某某二人代写论文、投诉敲诈,无论如何都是极为恶劣的行为,是自身诚信和德行的全方面破产。

寻求有偿代写损害了自己在学术上的诚信,这样唯结果论的价值取向更是令人忧心。《中国科学报》调查发现,涉事的两位学生中,卢某某疑似已于 2021 年 10 月获得计算机科学与技术学院的 2022 年推免生录取名额。

在学校的经验分享活动中,卢某某甚至作为先锋模范,建议同校的大二同学要从保研政策、综合素质能力加分和第二课堂学分认证方面全面成长。且不谈风波过后,她是否还有机会保研。人前一套背后一套的行径,使得她早已透支了自己在人际关系中的诚信。

在成立调查工作组后,西安电子科技大学都需要尽快公布一份详尽的调查报告和处理方案。因为这不仅关乎她们个人的未来,更关乎整个教育系统的公平性和公正性。

这枚学术不端的震撼弹带来了极高的网络声量,无论是在贴吧还是微博,各个社交平台以及媒体报道中都能看到相关讨论。可是许多评论也走向了难以预料的方向,许多人开始认为这两位学生是生活中做作的 " 小仙女 ",为她们如今面临失学的人生窘境而弹冠相庆。

更有甚者,通过人肉搜索等方式找到了这两名学生的照片,发出来让人打分评价她们的外貌。无端揣测她们因为受到网暴而假装自己得抑郁症(玉玉症),以换取保研机会。甚至将她们看作是平时对男性喊打喊杀的 " 极端女拳 ",认为对她们的举报是一场正义的性别战争。

在这样一种集体狂躁的情绪中,隐私权甚至道德准则都不再重要,暴力成了解决事情的唯一手段。

但暴力真的能解决学术不端的问题吗?很显然不能。

学术不端一直都是教育和管理中难以铲除的顽疾,有人认为应该加强论文评审和答辩,用 " 宽进严出 " 来治理乱象;也有人认为根本上是要加强学生的基础研究能力,放宽本科论文要求,甚至取消本科学位论文 ……

罗翔认为,论文抄袭问题实质上和我们应该以何种信念作为我们人生以及学术的动力相关,这是每一个以学术为志业的人都应该认真思考的问题。

高校毕业季当前,这两名西电学生的经历再次敲响了学术诚信的警钟。越来越多的学生需要意识到,自己在学位论文的独创性声明里签下的不只是自己的名字,还是迈向更广阔世界前的一份承诺。

而对学术不端事件处理得越公开、越清晰,人们去触犯这些道德标准的可能性就越小。除了松一阵紧一阵的学风建设,很显然,我们的大学还需要做得更多。

作者 | 佳星
收起阅读 »

GitHub 上又一款开源的像素风字体,和元宇宙社交最搭啦

GitHub 上又一款开源的像素风字体:泛中日韩像素字体。支持 10、12 和 16 像素。目标是为开发「像素风游戏」,提供一套开箱即用的字体解决方案。项目不仅提供了全部的字形设计源文件,也提供了构建字体所需要的完整程序。GitHub:github.com/T...
继续阅读 »

GitHub 上又一款开源的像素风字体:泛中日韩像素字体。支持 10、12 和 16 像素。目标是为开发「像素风游戏」,提供一套开箱即用的字体解决方案。

项目不仅提供了全部的字形设计源文件,也提供了构建字体所需要的完整程序。



GitHub:github.com/TakWolf/ark-pixel-font   

收起阅读 »

搜狐全员收到“工资补助”诈骗邮件 大量员工余额被划走

据说,搜狐全体员工收到一封内部域名发来的诈骗邮件,说是工资补贴,基本上所有员工都点了,被骗人数和金额巨大……在上周,搜狐全体员工收到了一封来自“搜狐财务部”名为《5月份员工工资补助通知》的邮件,大量员工按照附件要求扫码,并填写了银行账号等信息,最终不但没有等到...
继续阅读 »

据说,搜狐全体员工收到一封内部域名发来的诈骗邮件,说是工资补贴,基本上所有员工都点了,被骗人数和金额巨大……


在上周,搜狐全体员工收到了一封来自“搜狐财务部”名为《5月份员工工资补助通知》的邮件,大量员工按照附件要求扫码,并填写了银行账号等信息,最终不但没有等到所谓的补助,工资卡内的余额也被划走。

图片中的聊天记录中有人表示,“点击后扫码,工资卡里的钱就被划光了”。


邮件发信地址为sohutv-legal@sohu-inc.com,确实为搜狐内部域名,能够通过内部邮箱发邮件,这样来看,要弄就是被黑了,要么就有“内鬼”。


这要是真的就很尴尬了,两种可能,要么是钓鱼拿到了内网邮箱,要么利用漏洞直接进入了邮件服务器,然后再全员批量发送邮件进行诱导盗刷。


有网友直呼:打工人真的好惨。也有网友表示不理解:不是每年都护网吗?一看就是平时没有做这方面的演练。还有网友表示同情:卡上的余额都没了,得有多绝望。

简而言之,搜狐公司员工遭遇了网络上最常见的诈骗方式。但因为邮件来源显示为搜狐公司内部域名,公司平时报销也存在需要员工银行账号的惯例,加上员工之间本身就有薪资保密的义务,搜狐几乎所有员工都没有对邮件内容产生怀疑,这才导致被骗人数和涉案金额巨大。


聊天记录显示,事后搜狐迅速采取了行动,包括立刻删除了相关邮件,并由ES部门出面汇总遭遇诈骗员工的信息到派出所报案。

事实上,类似的“工资补助”诈骗从去年开始就在全国发生过多起,搜狐新闻也进行过相关报道。

来源:mp.weixin.qq.com/s/AY1sisbn0MfO9NM1bhqpQQ

收起阅读 »

CAS以及Atomic原子操作详解

CAS以及Atomic原子操作详解 CAS 什么是CAS 针对一个变量,首先比较它在内存中的值与某个期望的值是否相同,如果相同就给它赋予新值 其原子性是直接在硬件层面得到保障的 CAS是一种无锁算法,在不使用锁的情况下实现多线程之间的变量同步 底层...
继续阅读 »

CAS以及Atomic原子操作详解


CAS




  • 什么是CAS



    • 针对一个变量,首先比较它在内存中的值与某个期望的值是否相同,如果相同就给它赋予新值

    • 其原子性是直接在硬件层面得到保障

    • CAS是一种无锁算法,在不使用锁的情况下实现多线程之间的变量同步




  • 底层: CAS的底层实现



    • 从JVM源码层面的看CAS

      • 原子性

        • 在单核处理器是通过cmpxchgl指令来保证原子性的,在多核处理器下无法保证了,通过lock前缀的加持变为lock cmpxchgl保证了原子性,这里lock前缀指令拥有保证后续指令的原子性的作用



      • 有序性

        • 通过C++关键字volatile禁止指令重排序保证有序性,对于C++关键字volatile有两个作用一个是禁止重排序,一个是防止代码被优化



      • 其中可见性在JVM源码层面是保证的了,因为多核处理器下会加lock前缀指令,但是Java代码层面实现的CAS不能保证get加锁标记和set加锁标记的可见性,比如Atomic类中需要通过volatile修饰state保证可见性






  • 缺陷: CAS的缺陷



    • 一般CAS都是配合自旋,自旋时间过长,可能会导致CPU满载,所以一般会选择自旋到一定次数去park

    • 每次只能保证一个共享变量进行原子操作

    • ABA问题

      • 问题: 什么是ABA问题

        • 当有多个线程对一个原子类进行操作时,某个线程在这段时间内将A修改到B,又马上将其修改为A,其他线程并不感知,还是会被修改成功



      • 问题: ABA问题的解决方案

        • 数据库有个锁是乐观锁,是一种通过版本号方式来进行数据同步,也就是每次更新的时候都会匹配这个版本号,只有符号才能更新成功,同样的ABA问题也是基于这种去解决的,相应的Java也提供了对应的原子类AtomicStampedRefrence,其内部reference就是我们实际存储的变量,stamp就是版本号,每次修改可以通过加1来保证版本的唯一性








  • 问题: CAS失败自旋的操作存在什么问题



    • CAS自旋时间过长不成功,会给CPU带来较大的开销




  • CAS的应用




    • CAS操作的是由Unsafe类提供支持,该类定义了三种针对不同类型变量的CAS操作


      public final native boolean compareAndSwapObject(Object o, long offset,Object expected,Object x);
      public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
      public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);





Atomic原子类



  • 在并发编程中很容易出现并发安全的问题,比如自增操作,有可能不能获取正确的值,一般情况想到的是synchronized来保证线程安全,但是由于它是悲观锁,并不是最高效的解决方案,所以Juc提供了乐观锁的方式去提升性能

    • 基本类型: AtomicInteger、AtomicLong、AtomicBoolean

    • 引用类型: AtomicReference、AtomicStampedRerence、AtomicMarkableReference

    • 数组类型: AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray

    • 对象属性原子修改器: AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater

    • 原子类型累加器(JDK8增加的类): DoubleAccumulator、DoubleAdder、LongAccumulator、LongAdder、Striped64




LongAdder和DoubleAdder


瓶颈详解



  • 对于高并发场景下,多个线程同时进行自旋操作,会出现大量失败并不断自旋的情况,此时AtomicLong自旋会成为瓶颈,LongAdder引入解决了高并发场景,AtomicInteger、AtomicLong的自旋瓶颈问题


LongAdder原理


image.png

  • AtomicLong中有个内部变量value保存着实际的值,所有的操作都是针对该变量进行,在高并发场景下,value变量其实就是一个热点,多个线程同时竞争这个热点,而这样冲突的概率就比较大了

  • 重点: LongAdder的基本思路就是分散热点,将value的值分散到一个数组中,不同线程会命中到这个数组的不同槽位中,各个线程只对自己槽位中的那个值进行CAS操作,这样就分散了热点,冲突的概率就小很多,如果要获取真正的值,只需要将各个槽位的值累加返回

  • LongAdder设计的精妙之处: 尽量减少热点冲突,不到最后万不得已,尽量将CAS操作延迟

  • 注意: LongAdder的sum方法会有线程安全的问题

    • 高并发场景下除非全局加锁,否则得不到程序运行中某个时刻绝对准确的值,由于计算总和时没有对Cell数组进行加锁,所以在累加过程中可能有其他线程对于Cell数组中的值因为线程安全无法保障进行了修改,也有可能对数组进行了扩容,所以sum返回的值并不是非常精确的,其返回值并不是一个调用sum方法的原子快照值




LongAdder逻辑


image.png

LongAccumulator



  • LongAccumulator是LongAdder的增强版本,LongAdder只针对数组值进行加减运算,而LongAccumulator提供了自定义的函数操作

  • LongAccumulator内部原理和LongAdder几乎完全一样,都是利用了父类Striped64的longAccumulate方法

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

从单例谈double-check必要性,多种单例各取所需

前言 前面铺掉了那么多都是在讲原则,讲图例。很多同学可能都觉得和设计模式不是很搭边。虽说设计模式也是理论的东西,但是设计原则可能对我们理解而言更加的抽象。不过好在原则东西不是很多,后面我们就可以开始转讲设计模式了。 我的思路是按照设计模式进行分类整理。期间穿...
继续阅读 »

前言



  • 前面铺掉了那么多都是在讲原则,讲图例。很多同学可能都觉得和设计模式不是很搭边。虽说设计模式也是理论的东西,但是设计原则可能对我们理解而言更加的抽象。不过好在原则东西不是很多,后面我们就可以开始转讲设计模式了。

  • 我的思路是按照设计模式进行分类整理。期间穿插相关的知识进行扩展从而保证我们学习的更加的全面。在正式开始前我现在这里立个Flag。争取在20周内完成我们设计模式章节的内容。期间可能会有别的学习,20周争取吧

  • 相信单例模式是大家第一个使用到的设计模式吧。不管你怎么样,我第一个使用的就是单例模式。其实单例模式也是分很多种的【饿汉式】、【懒汉式】。如果在细分还有线程安全和线程不安全版本的。


饿汉式



  • 顾名思义饿汉式就是对类需求很迫切。从Java角度看就是类随着JVM启动就开始创建,不管你是否使用到只要JVM启动就会创建。


 public class SingleFactory
 {
     private static Person person = new Person();
 
     private SingleFactory()
    {
    }
 
     public static Person getInstance()
    {
         return person;
    }
 }


  • 上面这段代码就是饿汉式单例模式。通过这单代码我们也能够总结出单例模式的几个特点






















  • 特点
    隐藏类的创建即外部无法进行创建
    内部初始化好一个完整的类
    提供一个可以访问到内部实例的方法,这里指的是getInstance



image-20220509183514066.png



  • 单例模式特点还是很容易区分的。饿汉式感觉挺好的,那为什么后面还会出现懒汉式及其相关的变形呢?下面我们就来看看饿汉式有啥缺点吧。

  • 首先上面我们提到饿汉式的标志性特点就是随着JVM 的启动开始生成实例对象。这是优点同时也是缺点。大家应该都用过Mybatis等框架,这些框架为了加快我们程序的启动速度纷纷推出各种懒加载机制。

  • 何为懒加载呢?就是用到的时候再去初始化相关业务,将和启动不相关的部分抽离出去,这样启动速度自然就快了起来了。在回到饿汉式,你不管三七二十一就把我给创建了这无疑影响了我的程序启动速度。如果这个单例模式你使用了倒还好,假如启动之后压根就没用到这个单例模式的类,那我岂不是吃力不讨好。不仅浪费了时间还浪费了我的空间。

  • 所以说,处于对性能的考虑呢?还是建议大家不要使用饿汉式单例。但是,存在即是合理的,我们不能一棒子打死一堆人。具体场景具体对待吧XDM。


🐶变形1


 public class SingleFactory
 {
     private static Person person ;
 
     static {
         person = new Person();
    }
 
     private SingleFactory()
    {
    }
 
     public static Person getInstance()
    {
         return person;
    }
 }


  • 咋一看好像和上面的没啥区别哦。仔细对比你就会发现我们这里并没有立刻创建Person这个类,而是放在静态代码块中初始化实例了。

  • 放在静态代码块和直接创建其实是一样的。都是通过类加载的方式来进行实例化的。基本同根同源没啥可说的 。

  • 关于Static关键字我们之前也有说过,他涉及到的是类加载的顺序。我们在类加载的最后阶段就是执行我们的静态代码块


懒汉式


 public class SingleFactory
 {
     private static Person person = null;
 
     private SingleFactory()
    {
    }
 
     public static Person getInstance()
    {
         try
        {
             Thread.sleep(30);
        }
         catch (InterruptedException e)
        {
            e.printStackTrace();
        }
         if(person==null){
             person=new Person();
        }
         return person;
    }
 }


  • 懒汉式就是将我们的对象创建放在最后一刻进行创建。并不是跟随类加载的时候生成对象的,这样会造成一定程度的内存浪费。懒汉式更加的提高了内存的有效利用。在getInstance方法中我们在获取对象前判断是否已经生成过对象。如果没有在生成对象。这种行为俗称懒,所以叫做懒汉式单例模式


🐱变形1



  • 上面懒汉式单例中我加入了睡眠操作。这是因为我想模拟出他的缺点。上面这种方式在高并发的场景下并不能保证系统中仅有一个实例对象。


 public class SingleFactory
 {
     private static Person person = null;
 
     private SingleFactory()
    {
    }
 
     public static Person getIstance()
    {
         try
        {
             Thread.sleep(30);
        }
         catch (InterruptedException e)
        {
             e.printStackTrace();
        }
         synchronized (SingleFactory.class)
        {
             if (person == null)
            {
                 person = new Person();
            }
        }
         return person;
    }
 }


  • 只需要加一把锁,就能保证线性操作了。但是仔细想想难道这样就真的安全了吗。


double-check



  • 在多线程下安全的单例模式应该非double-check莫属了吧。


 public class OnFactory {
     private static volatile OnFactory onFactory;
 
     public static OnFactory getInstance() {
         if (null == onFactory) {
             synchronized (OnFactory.class) {
                 if (null == onFactory) {
                     onFactory = new OnFactory();
                }
            }
        }
         return onFactory;
    }
 }


  • 这段代码是之前咱们学习double-check和volatile的时候写过的一段代码。在这里我们不仅在锁前后都判断了而且还加上了volatile进行内存刷新。关于volatile需要的在主页中搜索关键词即可找到。这里仅需要知道一点volatile必须存在否则线程不安全。

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

Kotlin - 改良责任链模式

一、前言 责任链模式 作用:避免请求的发送者和接收者之间的耦合关系,将这个对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。 举例:OKHttp 的拦截器、Servlet 中的 FilterChain 二、使用责任链模式 例子:学生...
继续阅读 »

一、前言



  • 责任链模式

    • 作用:避免请求的发送者和接收者之间的耦合关系,将这个对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。

    • 举例:OKHttp 的拦截器、Servlet 中的 FilterChain




二、使用责任链模式



  • 例子:学生会经费申请

  • 重点:1 个请求会在 n 个处理器组成的处理器链上传递


以学生会经费申请会例,学生会会有一些日常开销以及活动开支,需要向学院的学生会基金申请经费,如果金额在 100 元之内,由分部长审批;如果金额在 100 到 500 元之间,由会长审批;如果金额在 500 到 1000 元之间,由学院辅导员审批;而如果金额超过 1000 元,则默认打回申请。像这种需要一层层往后传递请求的情况,非常适合采用责任链模式来设计程序:


/**
* 经费申请事件
*
* @author GitLqr
*/
data class ApplyEvent(val money: Int, val title: String)

/**
* 经费审批处理器
*
* @author GitLqr
*/
interface ApplyHandler {
val successor: ApplyHandler?
fun handleEvent(event: ApplyEvent)
}


注意:责任链模式需要将处理器对象连成一条链,最简单粗暴的方式就是让前驱处理器持有后继处理器 successor



接着,根据案例需要,编写各个角色对应的处理器类:


/**
* 部长
*
* @author GitLqr
*/
class GroupLeader(override val successor: ApplyHandler?) : ApplyHandler {
override fun handleEvent(event: ApplyEvent) {
when {
event.money <= 100 -> println("Group Leader handled application: ${event.title}.")
successor != null -> successor.handleEvent(event)
else -> println("Group Leader: This application cannot be handled.")
}
}
}

/**
* 会长
*
* @author GitLqr
*/
class President(override val successor: ApplyHandler?) : ApplyHandler {
override fun handleEvent(event: ApplyEvent) {
when {
event.money <= 500 -> println("President handled application: ${event.title}.")
successor != null -> successor.handleEvent(event)
else -> println("President: This application cannot be handled.")
}
}
}

/**
* 学院
*
* @author GitLqr
*/
class College(override val successor: ApplyHandler?) : ApplyHandler {
override fun handleEvent(event: ApplyEvent) {
when {
event.money <= 1000 -> println("College handled application: ${event.title}.")
successor != null -> successor.handleEvent(event)
else -> println("College: This application cannot be handled.")
}
}
}

最后,创建各个角色处理器实例,并按顺序组成一条链,由链头开始接收、转发需要被处理的经费申请事件:


// 使用
// val college = College(null)
// val president = President(college)
// val groupLeader = GroupLeader(president)
val groupLeader = GroupLeader(President(College(null)))
groupLeader.handleEvent(ApplyEvent(10, "buy a pen")) // 买只钢笔
groupLeader.handleEvent(ApplyEvent(200, "team building")) // 团建
groupLeader.handleEvent(ApplyEvent(600, "hold a debate match")) // 举行辩论赛
groupLeader.handleEvent(ApplyEvent(1200, "annual meeting of the college")) // 学院年会

// 输出
Group Leader handled application: buy a pen.
President handled application: team building.
College handled application: hold a debate match.
College: This application cannot be handled.

从输出结果可以看到,经费申请事件会在处理器链上传递,直到被一个合适的处理器处理并终止。



注意:这话是针对当前案例说的,责任链模式没有硬性要求一个请求只能被一个处理器处理,你可以在前面的处理器中对请求进行加工,提取数据等等操作,并且可以选择是否放行,交由后面的处理器继续处理,这需要根据实际情况,灵活应变。



三、改良责任链模式



  • 例子:学生会经费申请

  • 重点:偏函数 Partial Function


在对上述案例进行改良之前,我们先来了解一下偏函数是什么,在不同的编程语言中,对偏函数的理解还不一样,在 Python 中,偏函数是使用 functools.partial 把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单。而在 Scala 中,偏函数是使用 PartialFunction 构建一个仅仅处理输入参数的部分分支的函数,换句话说,就是带有判断条件的函数,只有满足条件的参数,才会被函数处理。


以上结论来自以下两篇文章:




题外话:对 Scala 偏函数有兴趣的可以看一下上面的文章,写的很通透。



回过头来,责任链模式的核心机理是,整个链条上的每个处理环节都有对其输入的校验标准,当输入的参数处于某个责任链节的有效接收范围之内,该环节才能对其做出正常的处理操作。那么,我们是不是可以把链条上的每个处理环节看做是一个个的偏函数呢?是的,不过 Kotlin 中并没有内置偏函数 API,好在有一个第三方 Kotlin 函数库【funKTionale】,其中的 partialfunctions.kt 就有 Scala 中偏函数的类似实现:


// https://github.com/MarioAriasC/funKTionale/blob/master/funktionale-utils/src/main/kotlin/org/funktionale/utils/partialfunctions.kt

class PartialFunction<in P1, out R>(private val definetAt: (P1) -> Boolean, private val f: (P1) -> R) : (P1) -> R {
override fun invoke(p1: P1): R {
if (definetAt(p1)) {
return f(p1)
} else {
throw IllegalArgumentException("Value: ($p1) isn't supported by this function")
}
}

fun isDefinedAt(p1: P1) = definetAt(p1)
}

这个 PartialFunction 类第一眼看上去感觉好复杂,分成如下几步,方便理解:



  • PartialFunction 继承自一个函数类型 (P1) -> R,编译器会强制要求实现 invoke() 方法,这意味着 PartialFunction 实例对象可以像调用函数那样使用。

  • 构造参数 1 definetAt: (P1) -> Boolean 用于判断 P1 参数是否满足被处理的条件。

  • 构造参数 2 f: (P1) -> R 用于处理 P1 参数并返回 R 类型值。

  • 成员方法 invoke 中,当 P1 满足条件时,则将 P1 交给 构造参数 2 f: (P1) -> R 处理;否则抛出异常。

  • 成员方法 isDefinedAt 只是构造参数 1 definetAt 的拷贝。


所以,用一句话概括 PartialFunction 实例对象,就是一个带有判断条件的"函数",只有满足条件的参数,才会被"函数"处理。现在我们用一个个 PartialFunction 实例来代替处理器是完全没问题的,问题是怎么把它们链接起来呢?【funKTionale】中还为 PartialFunction 扩展了一个 orElse 函数,这就是把偏函数组合起来的关键:


// https://github.com/MarioAriasC/funKTionale/blob/master/funktionale-utils/src/main/kotlin/org/funktionale/utils/partialfunctions.kt

infix fun <P1, R> PartialFunction<P1, R>.orElse(that: PartialFunction<P1, R>): PartialFunction<P1, R> {
return PartialFunction({ this.isDefinedAt(it) || that.isDefinedAt(it) }) {
when {
this.isDefinedAt(it) -> this(it)
that.isDefinedAt(it) -> that(it)
else -> throw IllegalArgumentException("function not definet for parameter ($it)")
}
}
}

同样,也分成如下几步,方便理解:



  • orElsePartialFunction 的扩展函数,故内部可以使用 this 获取原本的 PartialFunction 实例(也就是 receiver)。

  • orElse 只接收一个 PartialFunction 类型参数 that,并且返回一个 PartialFunction 类型实例,故 orElse 可以嵌套调用。

  • orElse 返回值是一个使用了两个 PartialFunction 实例对象 (即 thisthat)组合出来的一个新的 PartialFunction 实例对象,

  • orElse 返回值的意图是,只要原本的 thisthat 中有一个条件成立,那么就让条件成立的那个来处理参数 P1 ,否则抛出异常。其实,这个 that 就相当于是责任链模式中的 successor

  • orElse 使用 infix 修饰,故支持中缀表达式写法。



注意:你可能一时看不懂 PartialFunction({ xxx }){ yyy } 这个奇怪的语法,其实很简单,在创建一个 PartialFunction 实例时,可以传入两个 Lambda 表达式,所以正常写法应该是这样的 PartialFunction({ xxx }, { yyy }) ,不过,在 Kotlin 中,当 Lambda 表达式作为最后一个参数传入时,可以写到函数外部,所以就出现了 PartialFunction({ xxx }){ yyy } 这种写法。



好了,现在用 PartialFunction 来改良原本的责任链模式代码:


/**
* 使用自运行Lambda来构建一个个 PartialFunction 实例:部长、会长、学院
*
* @author GitLqr
*/
val groupLeader = {
val definetAt: (ApplyEvent) -> Boolean = { it.money <= 200 }
val handler: (ApplyEvent) -> Unit = { println("Group Leader handled application: ${it.title}.") }
PartialFunction(definetAt, handler)
}()
val president = {
val definetAt: (ApplyEvent) -> Boolean = { it.money <= 500 }
val handler: (ApplyEvent) -> Unit = { println("President handled application: ${it.title}.") }
PartialFunction(definetAt, handler)
}()
val college = {
val definetAt: (ApplyEvent) -> Boolean = { true }
val handler: (ApplyEvent) -> Unit = {
when {
it.money <= 1000 -> println("College handled application: ${it.title}.")
else -> println("College: This application is refused.")
}
}
PartialFunction(definetAt, handler)
}()


注意:自运行 Lambda 相当于是 js 中的立即执行函数。



接下来就是用 orElse 将一个个 PartialFunction 实例链接起来:


// 使用
// val applyChain = groupLeader.orElse(president.orElse(college))
val applyChain = groupLeader orElse president orElse college // 中缀表达式
applyChain(ApplyEvent(10, "buy a pen")) // 买只钢笔
applyChain(ApplyEvent(200, "team building")) // 团建
applyChain(ApplyEvent(600, "hold a debate match")) // 举行辩论赛
applyChain(ApplyEvent(1200, "annual meeting of the college")) // 学院年会

// 输出
Group Leader handled application: buy a pen.
Group Leader handled application: team building.
College handled application: hold a debate match.
College: This application is refused.

使用 PartialFunction 之后,不仅可以不幅度减少代码量,结合 orElse 能获得更好的语法表达。以上,就是使用偏函数改良责任链模式的全部内容了。为了加深对偏函数的理解,这里引用数据工匠记的 Scala 《偏函数(Partial Function)》原文中的话:



为什么要用偏函数呢?以我个人愚见,还是一个重用粒度的问题。函数式的编程思想是以一种“演绎法”而非“归纳法”去寻求解决空间。也就是说,它并不是要去归纳问题然后分解问题并解决问题,而是看透问题本质,定义最原初的操作和组合规则,面对问题时,可以通过组合各种函数去解决问题,这也正是“组合子(combinator)”的含义。偏函数则更进一步,将函数求解空间中各个分支也分离出来,形成可以被组合的偏函数。


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

Flutter自绘组件:扇形图

简介 在开发过程中通常会遇到一些不规则的UI,比如不规则的线条,多边形,统计图表等等,用那些通用组件通过组合的方式无法进行实现,这就需要我们自己进行绘制。可以通过使用CuntomPaint组件并结合画笔CustomPainter去进行手动绘制各种图形。 Cus...
继续阅读 »

简介


在开发过程中通常会遇到一些不规则的UI,比如不规则的线条,多边形,统计图表等等,用那些通用组件通过组合的方式无法进行实现,这就需要我们自己进行绘制。可以通过使用CuntomPaint组件并结合画笔CustomPainter去进行手动绘制各种图形。


CustomPaint介绍


CustomPaint是一个继承SingleChildRenderObjectWidgetWidget,这里主要介绍几个重要参数:

childCustomPaint的子组件。
painter: 画笔,绘制的图形会显示在child后面。

foregroundPainter:前景画笔,绘制的图形会显示在child前面。

size:绘制区域大小。


CustomPainter介绍


CustomPainter是一个抽象类,通过自定义一个类继承自CustomPainter,重写paintshouldRepaint方法,具体绘制主要在paint方法里。


paint介绍


主要两个参数:
Canvas:画布,可以用于绘制各种图形。
Size:绘制区域的大小。


void paint(Canvas canvas, Size size)

shouldRepaint介绍


在Widget重绘前会调用该方法确定时候需要重绘,shouldRepaint返回ture表示需要重绘,返回false表示不需要重绘。


bool shouldRepaint(CustomPainter oldDelegate)

示例


这里我们通过绘制一个饼状图来演示绘制的整体流程。


pie_chart_view.gif


使用CustomPaint


首先,使用CustomPaint,绘制大小为父组件最大值,传入自定义painter


@override
Widget build(BuildContext context) {
return CustomPaint(
size: Size.infinite,
painter: PieChartPainter(),
);
}

自定义Painter


自定义PieChartPainter继承CustomPainter


class PieChartPainters extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {

}

@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return oldDelegate != this;
}
}

绘制


接着我们来实现paint方法进行绘制


@override
void paint(Canvas canvas, Size size) {
//移动到中心点
canvas.translate(size.width / 2, size.height / 2);
//绘制饼状图
_drawPie(canvas, size);
//绘制扇形分割线
_drawSpaceLine(canvas);
//绘制中心圆
_drawHole(canvas, size);
}

绘制饼状图

我们以整个画布的中点为圆点,然后计算出每个扇形的角度区域,通过canvas.drawArc绘制扇形。


pie_chart_view1.png


void _drawPie(Canvas canvas, Size size) {
var startAngle = 0.0;
var sumValue = models.fold<double>(0.0, (sum, model) => sum + model.value);
for (var model in models) {
Paint paint = Paint()
..style = PaintingStyle.fill
..color = model.color;
var sweepAngle = model.value / sumValue * 360;
canvas.drawArc(Rect.fromCircle(radius: model.radius, center: Offset.zero),
startAngle * pi / 180, sweepAngle * pi / 180, true, paint);

//为每一个区域绘制延长线和文字
_drawLineAndText(
canvas, size, model.radius, startAngle, sweepAngle, model);

startAngle += sweepAngle;
}
}

绘制延长线以及文本

延长线的起点为扇形区域边缘中点位置,长度为一个固定的长度,转折点坐标通过半径加这个固定长度和三角函数进行计算,然后通过转折点的位置决定横线终点的方向,而横线的长度则根据文字的宽度决定,然后通过canvas.drawLine进行绘制直线。

文本绘制使用TextPainter.paint进行绘制,paint方法里面最终是通过canvas.drawParagraph进行绘制的。

最后再在文字的前面通过canvas.drawCircle绘制一个小圆点。


pie_chart_view2.png


 void _drawLineAndText(Canvas canvas, Size size, double radius,
double startAngle, double sweepAngle, PieChartModel model) {
var ratio = (sweepAngle / 360.0 * 100).toStringAsFixed(2);

var top = Text(model.name);
var topTextPainter = getTextPainter(top);

var bottom = Text("$ratio%");
var bottomTextPainter = getTextPainter(bottom);

// 绘制横线
// 计算开始坐标以及转折点的坐标
var startX = radius * (cos((startAngle + (sweepAngle / 2)) * (pi / 180)));
var startY = radius * (sin((startAngle + (sweepAngle / 2)) * (pi / 180)));

var firstLine = radius / 5;
var secondLine =
max(bottomTextPainter.width, topTextPainter.width) + radius / 4;
var pointX = (radius + firstLine) *
(cos((startAngle + (sweepAngle / 2)) * (pi / 180)));
var pointY = (radius + firstLine) *
(sin((startAngle + (sweepAngle / 2)) * (pi / 180)));

// 计算坐标在左边还是在右边
// 并计算横线结束坐标
// 如果结束坐标超过了绘制区域,则改变结束坐标的值
var marginOffset = 20.0; // 距离绘制边界的偏移量
var endX = 0.0;
if (pointX - startX > 0) {
endX = min(pointX + secondLine, size.width / 2 - marginOffset);
secondLine = endX - pointX;
} else {
endX = max(pointX - secondLine, -size.width / 2 + marginOffset);
secondLine = pointX - endX;
}

Paint paint = Paint()
..style = PaintingStyle.fill
..strokeWidth = 1
..color = Colors.grey;

// 绘制延长线
canvas.drawLine(Offset(startX, startY), Offset(pointX, pointY), paint);
canvas.drawLine(Offset(pointX, pointY), Offset(endX, pointY), paint);

// 文字距离中间横线上下间距偏移量
var offset = 4;
var textWidth = bottomTextPainter.width;
var textStartX = 0.0;
textStartX =
_calculateTextStartX(pointX, startX, textWidth, secondLine, textStartX, offset);
bottomTextPainter.paint(canvas, Offset(textStartX, pointY + offset));

textWidth = topTextPainter.width;
var textHeight = topTextPainter.height;
textStartX =
_calculateTextStartX(pointX, startX, textWidth, secondLine, textStartX, offset);
topTextPainter.paint(canvas, Offset(textStartX, pointY - offset - textHeight));

// 绘制文字前面的小圆点
paint.color = model.color;
canvas.drawCircle(
Offset(textStartX - 8, pointY - 4 - topTextPainter.height / 2),
4,
paint);
}

绘制扇形分割线

在绘制完扇形之后,然后在扇形的开始的那条边上绘制一条直线,起点为圆点,长度为扇形半径,终点的位置根据半径和扇形开始的那条边的角度用三角函数进行计算,然后通过canvas.drawLine进行绘制。


pie_chart_view3.png


void _drawSpaceLine(Canvas canvas) {
var sumValue = models.fold<double>(0.0, (sum, model) => sum + model.value);
var startAngle = 0.0;
for (var model in models) {
_drawLine(canvas, startAngle, model.radius);
startAngle += model.value / sumValue * 360;
}
}

void _drawLine(Canvas canvas, double angle, double radius) {
var endX = cos(angle * pi / 180) * radius;
var endY = sin(angle * pi / 180) * radius;
Paint paint = Paint()
..style = PaintingStyle.fill
..color = Colors.white
..strokeWidth = spaceWidth;
canvas.drawLine(Offset.zero, Offset(endX, endY), paint);
}

绘制内部中心圆

这里可以通过传入的参数判断是否需要绘制这个圆,使用canvas.drawCircle进行绘制一个与背景色一致的圆。


pie_chart_view4.png


void _drawHole(Canvas canvas, Size size) {
if (isShowHole) {
holePath.reset();
Paint paint = Paint()
..style = PaintingStyle.fill
..color = Colors.white;
canvas.drawCircle(Offset.zero, holeRadius, paint);
}
}

触摸事件处理


接下来我们来处理点击事件,当我们点击某一个扇形区域时,此扇形需要突出显示,如下图:


pie_chart_view5.png


重写hitTest方法

注意

这个方法的返回值决定是否响应事件。

默认情况下返回null,事件不会向下传递,也不会进行处理;
如果返回true则当前组件进行处理事件;
如果返回false则当前组件不会响应点击事件,会向下一层传递;


我直接在这里处理点击事件,通过该方法传入的offset确定点击的位置,如果点击位置是在圆形区域内并且不在中心圆内则处理事件同时判断所点击的具体是哪个扇形,反之则恢复默认状态。


@override
bool? hitTest(Offset offset) {
if (oldTapOffset.dx==offset.dx && oldTapOffset.dy==offset.dy) {
return false;
}
oldTapOffset = offset;
for (int i = 0; i < paths.length; i++) {
if (paths[i].contains(offset) &&
!holePath.contains(offset)) {
onTap?.call(i);
oldTapOffset = offset;
return true;
}
}
onTap?.call(-1);
return false;
}

至此,我们通过onTap向上传递出点击的是第几个扇形,然后进行处理,更新UI就可以了。


动画实现


pie_chart_view.gif


这里通过Widget继承ImplicitlyAnimatedWidget来实现,ImplicitlyAnimatedWidget是一个抽象类,继承自StatefulWidget,既然是StatefulWidget那肯定还有一个StateState继承AnimatedWidgetBaseState(此类继承自ImplicitlyAnimatedWidgetState),感兴趣的小伙伴可以直接去看源码


实现AnimatedWidgetBaseState里面的forEachTween方法,主要是用于来更新Tween的初始值。


@override
void forEachTween(TweenVisitor<dynamic>visitor) {
customPieTween = visitor(customPieTween, end, (dynamic value) {
return CustomPieTween(begin: value, end: end);
}) as CustomPieTween;
}

自定义CustomPieTween继承自Tween,重写lerp方法,对需要做动画的参数进行处理


class CustomPieTween extends Tween<List<PieChartModel>> {
CustomPieTween({List<PieChartModel>? begin, List<PieChartModel>? end})
: super(begin: begin, end: end);

@override
List<PieChartModel> lerp(double t) {
List<PieChartModel> list = [];
begin?.asMap().forEach((index, model) {
list.add(model
..radius = lerpDouble(model.radius, end?[index].radius ?? 100.0, t));
});
return list;
}

double lerpDouble(double radius, double radius2, double t) {
if (radius == radius2) {
return radius;
}
var d = (radius2 - radius) * t;
var value = radius + d;
return value;
}
}

完整代码


感兴趣的小伙伴可以直接看源码
GitHub:chart_view


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

商业软件选型之困

以往,商业软件的选择是非常简单粗暴的。但是近些年,如果你为企业做过商业软件选型,那么就会明白,在科技飞速发展的现在,为企业挑选商业软件是非常困难且繁琐的。为什么会发生这样的变化呢?我们应当如何简化商业软件的挑选过程呢?PART 01 软件选择的复杂性不可否认,...
继续阅读 »

以往,商业软件的选择是非常简单粗暴的。但是近些年,如果你为企业做过商业软件选型,那么就会明白,在科技飞速发展的现在,为企业挑选商业软件是非常困难且繁琐的。为什么会发生这样的变化呢?我们应当如何简化商业软件的挑选过程呢?

PART 01 软件选择的复杂性

不可否认,为企业挑选商业软件是一个困难且耗时的过程。下面是当今社会出现这种变化的一些原因:

1、利基解决方案的出现

在过去的数十年中,我们见证了不同种类利基软件的爆发式增长。无论企业的需求是什么,大概率都会存在为此需求专门设计和构建的解决方案。甚至有可能专门为此类特定需求的软件创造冗长的名称和缩写。

例如,你知道CMMS(Computer Maintenance Management System,计算机维护管理系统)和EAM(Enterprise Asset Management,企业资产管理系统)软件在资产管理领域的差异吗?在大多数情况下,这其实是好现象。无论你的业务需求是什么,都可能有现成的软件方案能够解决这些问题。

更好的方面是,这些解决方案之间可能存在竞争,因此你可以不同的平台之间选择,因为此类平台都提供相似的功能。但是,这也为软件的选型决策带来困扰。

首先,你会为不同的业务需求选择不同的利基平台吗?你也可以选择能够提供多种功能的综合性平台。其次,你真的需要软件平台来处理小问题吗?还是说这些问题可以自己搞定?

2、海量的选择

软件开发是个利润丰厚的领域。如果你创建的软件平台满足了目标群体的实际需求,就会在经济层面得到丰厚的回报。这吸引了数百万企业家和开发者进入这个商业领域。反过来,这又为其他企业家创造了海量的商业机会。

即使你在寻找特定类型的平台,在开始搜寻的时候也会发现有大量的同类产品可供挑选。尽管我们倾向于更多的选择余地是个好事情,但这最终会使我们的选择过程陷入困境,并使我们对最终决定并不十分满意。

3、苹果和橙子的比较

如果有两个平台以完全迥异的方式实现了相同的目标,我们将如何比较他们呢?如果两个平台在功能上一致,但是其中一个的售价低20%,那么从两者中选择价格优惠的平台,是显而易见的英明决策。然而,如果它们有完全不同的使用体验、功能和报价标准呢?就如同将苹果与橙子进行比较,我们无法确认哪个是更好的选择。

4、官僚机构和委员会的决定

由于根深蒂固的官僚主义会影响委员会做出的决定,因此企业有时会将自己陷入异常困难和煎熬的决策过程。从某种程度来看,这是可以理解的。

为大型企业购买软件是一个重要的决策过程,因此避免过多的人员参与是很有必要的。当然,这也是一个能够影响很多部门和人员的决定。因此,这个决定不应当由领导者独自决策。然而,没有十全十美的决定。大部分时候,集体决策会耗费更长的时间,且决策仍然会导致意料之外的结果,这是无法完全避免的。

5、安全性和潜在漏洞

企业需要评估引入新软件带来的安全风险和潜在漏洞,并制定应对措施。购买软件带来的这部分风险非常复杂,企业无法忽视它。许多企业现在都有专门的风险评估团队,他们的唯一工作就是评估与软件相关的潜在安全风险。

6、合约和法律问题

签订软件服务合同是让人非常伤脑筋的事情,尤其是当你将自己限定在一份为期三年的协议中时。尽管许多软件平台非常乐意用户选择订阅制协议,但是对于重要项目和特殊平台,制定严谨有效的合同仍然是非常必要的。进行合同审查,意味着项目流程需要消耗更多的时间。此外,合同的条款将会让你更加头疼。

7、未知因素:业务需求

或许你已经明确了未来几年内的业务需求。但是,从现在起的十年内,业务需求会发生哪些变化呢?你认为这个平台能够与你的业务同步成长吗?你能够预知自己的业务是否会发生根本性的改变,最终不再需要该平台吗?

8、未知因素:软件开发

当今的软件产品不断更新迭代。软件开发人员根据需要添加和移除功能,并升级UI以改进用户体验。你能确定这个软件朝着正确的方向发展吗?当然不能,这根本是无法预知的。

PART 02 如何让软件选择变得简单呢?

如果你十分努力地为企业挑选软件,可以参考以下步骤来让这个过程变得轻松一些。

1、从需求评估开始

在为企业采购之前,先进行需求评估是最好的办法。有太多的企业家和采购人员在需求很模糊的时候就冒险进入市场。他们认为四处逛逛就可以解决问题、识别痛点和聚焦解决方案类型。

然而,这最终可能会使事情复杂化,造成误会并引入之前未曾考虑到的新需求,这些新需求很可能并非真实需求。相反,在团队的真实需求中投入精力,并记录需求,然后寻找能够满足这些需求的解决方案,才是最快速有效的办法。

2、缩小决策范围

尽可能缩小你的决策范围。如果你为一个没有约束条件的特殊需求选择软件平台,那么你将被各种可能性所淹没。相反,尝试立即消除一些选项;例如,为自己设定一个严格的预算,就可以排除超出预算价格的软件。你是否只考虑具备特定功能的软件平台?

3、优化灵活性和适应性

尽可能优化灵活性和适应性。如果你在仅剩的两个软件之间犹豫不决,就选择灵活性和适应性更强的那个。因为未来充满不确定性,所以良好的灵活性和适应性能够最大程度地适应不确定性带来的变化。

4、尽可能在同类产品中进行挑选

基于区块链的平台并不完全一样,尽管它们是依托于相同类型的基础架构设计开发的。因此,不要假定所有给定的利基软件都拥有同等水平的性能表现,无论它们的页面和功能是什么样的。以公平和直观的方式对比不同的平台,这通常并不容易,也不太现实。我们需要做的是尽可能在同类产品之间进行比较和挑选。

5、寻找可信的开发者

与其只评估产品,不如评估开发团队及其产品理念。通常来说,为值得信赖和称职的开发人员投出一票,是非常明智的选择,软件平台的表现在此时反而是次要的。

观察项目的领导力以及团队成员的经验和技术水平。是否为产品做好了长远的规划?开发人员是否为自己的工作成果感到自豪?

为企业挑选软件很繁琐,但它不应该是流程上的噩梦。如果你运用这些策略并且愿意保持适应能力且持续学习软件领域的相关技能,你会得到更大的收获——对自己的选择自信起来。

翻译:仇凯

原文:https://readwrite.com/why-choosing-software-is-such-a-tough-decision-in-the-modern-era/

收起阅读 »

零侵入性:一个注解,优雅的实现循环重试功能

前言在实际工作中,重处理是一个非常常见的场景,比如:发送消息失败。调用远程服务失败。争抢锁失败。这些错误可能是因为网络波动造成的,等待过后重处理就能成功.通常来说,会用try/catch,while循环之类的语法来进行重处理,但是这样的做法缺乏统一性,并且不是...
继续阅读 »

前言

在实际工作中,重处理是一个非常常见的场景,比如:

  1. 发送消息失败。

  2. 调用远程服务失败。

  3. 争抢锁失败。

这些错误可能是因为网络波动造成的,等待过后重处理就能成功.通常来说,会用try/catch,while循环之类的语法来进行重处理,但是这样的做法缺乏统一性,并且不是很方便,要多写很多代码.然而spring-retry却可以通过注解,在不入侵原有业务逻辑代码的方式下,优雅的实现重处理功能.

一、@Retryable是什么?

spring系列的spring-retry是另一个实用程序模块,可以帮助我们以标准方式处理任何特定操作的重试。在spring-retry中,所有配置都是基于简单注释的。

二、使用步骤

1.POM依赖

<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>

2.启用@Retryable

@EnableRetry
@SpringBootApplication
public class HelloApplication {
  public static void main(String[] args) {
      SpringApplication.run(HelloApplication.class, args);
  }
}

3.在方法上添加@Retryable

import com.mail.elegant.service.TestRetryService;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import java.time.LocalTime;

@Service
public class TestRetryServiceImpl implements TestRetryService {
  @Override
  @Retryable(value = Exception.class,maxAttempts = 3,backoff = @Backoff(delay = 2000,multiplier = 1.5))
  public int test(int code) throws Exception{
      System.out.println("test被调用,时间:"+LocalTime.now());
        if (code==0){
            throw new Exception("情况不对头!");
        }
      System.out.println("test被调用,情况对头了!");
      return 200;
  }
}

来简单解释一下注解中几个参数的含义:

  1. value:抛出指定异常才会重试

  2. include:和value一样,默认为空,当exclude也为空时,默认所有异常

  3. exclude:指定不处理的异常

  4. maxAttempts:最大重试次数,默认3次

  5. backoff:重试等待策略,默认使用@Backoff,@Backoff的value默认为1000L,我们设置为2000L;multiplier(指定延迟倍数)默认为0,表示固定暂停1秒后进行重试,如果把multiplier设置为1.5,则第一次重试为2秒,第二次为3秒,第三次为4.5秒。

当重试耗尽时还是失败,会出现什么情况呢?

当重试耗尽时,RetryOperations可以将控制传递给另一个回调,即RecoveryCallback。Spring-Retry还提供了@Recover注解,用于@Retryable重试失败后处理方法。如果不需要回调方法,可以直接不写回调方法,那么实现的效果是,重试次数完了后,如果还是没成功没符合业务判断,就抛出异常。

4.@Recover

@Recover
public int recover(Exception e, int code){
System.out.println("回调方法执行!!!!");
//记日志到数据库 或者调用其余的方法
  return 400;
}

可以看到传参里面写的是 Exception e,这个是作为回调的接头暗号(重试次数用完了,还是失败,我们抛出这个Exception e通知触发这个回调方法)。对于@Recover注解的方法,需要特别注意的是:

  1. 方法的返回值必须与@Retryable方法一致

  2. 方法的第一个参数,必须是Throwable类型的,建议是与@Retryable配置的异常一致,其他的参数,需要哪个参数,写进去就可以了(@Recover方法中有的)

  3. 该回调方法与重试方法写在同一个实现类里面

5. 注意事项

  1. 由于是基于AOP实现,所以不支持类里自调用方法

  2. 如果重试失败需要给@Recover注解的方法做后续处理,那这个重试的方法不能有返回值,只能是void

  3. 方法内不能使用try catch,只能往外抛异常

  4. @Recover注解来开启重试失败后调用的方法(注意,需跟重处理方法在同一个类中),此注解注释的方法参数一定要是@Retryable抛出的异常,否则无法识别,可以在该方法中进行日志处理。

总结

本篇主要简单介绍了Springboot中的Retryable的使用,主要的适用场景和注意事项,当需要重试的时候还是很有用的。

作者:Memory小峰

来源:blog.csdn.net/h254931252/article/details/109257998

收起阅读 »

与10倍开发者共事两年:他永远是对的,而我倍受煎熬

与 10 倍开发者共事,足以改变自己的职业生涯。  最近,我在网上看到不少关于 10 倍开发者的讨论。有些人想要成为这样的人,也有些人想远离这样的人。但在此之前,我们可能先要弄清楚这样一个问题:10 倍开发者真的存在、只是传说,或者仅仅是人们由于相对认知而感受...
继续阅读 »

与 10 倍开发者共事,足以改变自己的职业生涯。

  最近,我在网上看到不少关于 10 倍开发者的讨论。有些人想要成为这样的人,也有些人想远离这样的人。但在此之前,我们可能先要弄清楚这样一个问题:10 倍开发者真的存在、只是传说,或者仅仅是人们由于相对认知而感受到的概念?

  在得出结论之前,我想先给大家讲讲自己的经历。

与 10 倍开发者共事

  大约十年之前,公司的软件开发总监雇佣了一名三级软件工程师,我们都叫他 Gary。大概在同一时期,我们还雇用了一位名叫 Mitch 的二级软件工程师。最初几个月里,Gary 非常安静,总是一个人待着,努力解决一个个纯技术性的问题。当时我们的任务,是为实时 3D 机械训练软件制作空气等流体的流动动画。公司里的每个人都一直希望实现这个效果,但由于种种挑战,始终未能达成。而 Gary,成为帮助我们冲击难关的英雄。

  在他准备把功能提交给 QA 进行审查时,整个功能的观感至少比我想象中还要好,性能也超出预期,并拥有数千项单元测试断言作为支持。相比之下,我们的主体代码库根本就没经受过这么全面的测试。不用说了,各级管理人员都对这项睽违已久的功能感到非常满意。

我们的代码中有很多庞大,而且复杂得让人害怕的部分。

  不久之后,Gary 又组织了一次工程展示。展示内容主要集中在架构层面,即围绕对象生命周期、依赖倒置、ad-hoc 生命周期 / 明确限定范围的对象、某些分配反模式的危害、有碍单元测试覆盖的代码耦合,以及这些因素与很多内部工程问题之间的关联等等。这次展示让与会者们感到困惑,甚至感到颇为尴尬。毕竟这一切赤裸裸的批评,指向的正是那些最早加入公司、并一路构建起知识产权体系的老员工。

  我们的技术债务问题确实严重,但……也没有那么严重。虽然不会影响到生产力,但我们的代码中确实有很多庞大、而且复杂得让人害怕的部分。Gary 要做的正是揭露出这一切。但我们压力很大,因为我们每个人都是他提出的问题中的一部分。

他对现代软件设计的理解领先我们好几年。

  这个人的特点是,他永远是对的。不只是在争论当中,也包括在各种判断当中,他更像是个全知全能的神。虽然我一直以先弄清事实再发言的好习惯著称,但我也得承认,在整个共事期间我一共只揪出过他一到两次不太准确的表达。和这样的人共事压力很大,因为同事们总会发现一些自己本该了解、但却一无所知的重要知识。考虑到他们往往与 Gary 有着同样的职称和头衔,这就更让人感到无地自容。

  人性总有阴暗面,大家不喜欢那些特别聪明的人。特别是在对方提出的真知灼见既正确、又缺乏善意时,就更是让人不爽。所以同事们的普遍共识是,这家伙是个刻薄鬼。我个人并不觉得他是故意要让人难堪,但 Gary 在让人难堪这事上真的很有天赋。与此同时,他对现代软件设计的理解领先我们好几年,而这些心得还得在我们公司逐步实践,也许他觉得身边的同事真的让他很失望。

  公平地讲,我们沿用陈旧技术与方法是有原因的,而且也靠这些旧办法开发出了强大的产品。任何公司都可能存在类似的问题。

  Gary 强悍的技术实力加上对于敏捷流程的坚定拥护,最终挤走了雇用他的老领导,并由他自己上位。同事们震惊了一段时间,但很快就发现 Gary 主管带来了一系列令人兴奋的新变化。公司调整了自身产品各类,Mitch、我和另一位新任软件开发测试工程师(SDET)并纳入新团队中,尝试公司之前从未做过的工作。

  根据交流感受,Gary 一直以为我是二级软件工程师。但在发现我实际上只是一级时,他相当愤怒,并很快去找公司高层理论。几周之后,我就升职了。同样的,Mitch 虽然只是二级软件工程师,但他却拥有不逊于三级工程师的知识与技能。但没办法,他只能等……不知道在等什么,总之需要一段时间才能得到与自己水平相符的职称。

  有时候,Mitch 与 Gary 形影不离。我记得我们曾经花无数个小时在办公室里对未来新产品的架构设计组织头脑风暴与思维实验。到这个时候,我才意识到这两位的水平高到不知道哪里去了。有很长一段时间,他们两个人似乎开始用一种独特的语言交流。虽然他们之前从来没有协作过,但他们都认为公司内部缺少现代编程的基本概念。刚开始,人们不喜欢这两个人在那里说东说西;但事实证明,在他们碰头之后,两个人的编码效率确实高、质量也是真的稳定。

  我这个人比较擅长处理技术上的困难任务,Mitch 特别聪明,而 Gary 则拥有最强的编码质量。更让人稀奇的是,虽然 Gary 总是在全体大会和管理层会议中占用很长的时间,包括设计并记录新的标准流程、为各个开发者提供帮助与指导,但我到现在也不太确定他究竟是怎么在短时间内为公司带来这么显著的生产力提升的。总之在他的带领下,整个团队都不需要加班了,包括他自己。

让所有开发者拥有共同的价值观,是建立和谐团队与强大代码库的关键。

  尽管我已经有了几年的编程经验,但在 Gary 团队中度过的两年,绝对为我后续的高级开发者头衔奠定了良好的基础。他帮助我改掉了不少多年来养成的习惯——就是那种特别普遍,但并没什么用处,有时候甚至令人讨厌的习惯。相反,我们开始建立起更有前瞻性的视角,并积极使用先进工具与更高效的解决办法。而我从他身上学到的最重要一点,在于让所有开发者拥有共同的价值观,是建立和谐团队与强大代码库的关键。

  我们开发出的应用程序几乎没有缺陷,性能非常好、易于扩展,而且能够在之后的项目中重复使用。从各个方面来看,这都是我在入职以来见证到的最令人振奋的技术成功。

如果这样的状况都不能给公司敲响警钟,那管理层就太失败了。

  如果各位读者朋友也是那种重视工作、热爱工作的人,应该也曾被企业内的政治问题折磨得发狂。我怀疑 Gary 也是因为这个才决定离职,因为当时他并没有跳槽的打算。Mitch 在之后不到一年也选择离开,同样没有什么跳槽计划。两位最具才华的员工选择裸辞,这绝对是个强烈的信号。如果这样的状况都不能给公司敲响警钟,那管理层就太失败了——或者说,他们已经陷入了更大的问题当中。

  Gary 给我的临别忠告是,“你需要多多表达自己。”回顾我们一起奋斗的那段时间,Gary 和 Mitch 都特别善于表达自己,他们有时候甚至不给我说话的余地。但只要把话筒交给我,我说出来的就一定会是有意义的东西。在他们的引导下,我意识到这确实非常重要。

  我必须快速成长,帮助填补他们离去后留下的空白。虽然我的工作绩效同样非常出色,但最终我也离开了这家公司。我在这里度过了一段黄金岁月,也感激这家公司帮助我开启了职业生涯。但离别终有时,大家没必要总是强绑在一起。

  几年之后,我仍然在把自己从 Gary 身上学到的价值观带到其他岗位上,也努力让自己成为一个善于表达的人。事实证明,这种价值观确实让我在其他公司里也获得了尊重与广阔的发展空间。

要点汇总

  不知道大家在这个故事里有什么心得,下面我来谈谈自己的切身感受……

我们很难量化什么才是真正的 10 倍程序员,但这个问题其实没那么重要

真正重要的,是帮助你身边的人获得提升。

  有些人可能会争论某个同事到底是不是真正的 10 倍程序员。这样的 10 倍到底是在跟谁比?10 倍又具体体现在哪些方面?

  不少朋友都有过在一半的要求时间内完成了 4 倍工作量的经历,在项目中实现了更高的单元测试覆盖率以及更出色的代码质量,总体产出可以达到其他初级开发者的 10 倍以上等等。有时候,与具有一定经验的同行竞争时,您可能也凭借着更少的技术债务或者更强的特定领域专业知识达成了类似的优势。

  但这一切终究会被慢慢抹平,大家会凭借类似的从业经验、使用相同的工具、基于同一套代码库、以相同的理念 / 流程 / 设计模式处理同样的技术债务。在这样的前提下,开发者之间的效率仍有区别,但恐怕绝不可能有 10 倍那么夸张。

  问题的关键并不在于比其他人更强,而是帮助你身边的人获得提升。出色的开发者没有理由用自己的优势来打击其他同事,最重要的是为他人提供指导、发现阻碍生产力进步的因素、解决问题并防止其再次发生。这样,我们就能拥有一支真正有战斗力的队伍,而不只是围绕着一位开发明星原地打转。

成为专家,还是培养自己的专业性

自满实际是在沉默当中寻找安全感。

  我们不该因为某人出于长久以来的习惯、使用得到广泛证明的标准与既定技术,并由此以毫无追求的安全方法完成功能实现就对其横加指责。结合自己的经历,Gary 当初眼中的我们就像是这样一群业余爱好者。他不太注意自己的态度,只是他希望整个团队成长为软件开发专家的心情完全可以理解。

  但请千万不要忘记,其他人也是人,人总是有着种种缺陷。Gary 也是这样,他在第 100 次看到同样的错误时肯定要发脾气;只是这样的错误对其他人来讲属于“正常现象”。失去耐心的同时,你也失去了对同事们应有的尊重,这本身就是对专业性的践踏。

  软件领域的专业性像是一条微妙的线,我们不能随时越界,但在看到需要纠正的系统性问题时也不应视而不见。在此期间,你可能会引发混乱、可能会树敌,甚至威胁到自己的这只饭碗……但自满实际上是在沉默中寻找安全感。

  如果希望改变,请在社交层面找到完美的平衡点。要用心挑选提出建议的契机,更要用心挑选提出建议时的用语。

重视实践、技术与理念

如果能够做到,这一切将改变你的职业生涯。

  • 这些东西并不能保证把工作效率提升 10 倍。但我可以保证,只要培养起这样的能力,您会对软件开发拥有更加深刻的理解。

  • 严格遵循 SOLID 设计原则

  • 使用 MVC 模式进一步分离关注点

  • 命令查询职责分离

  • 通过实时代码覆盖工具完成单元测试覆盖

  • 使用行为驱动型开发发现需求细节,同时实现 UI 测试自动化

  • 明确定义并强制实施“已确认的定义”

  • 代码质量与分支策略,借此保证源代码控制系统拥有良好的清洁度与性能

  • 拥抱敏捷理念,但不必被动接受 SCRUM 中强调的一切流程

  在职业生涯中亲身实践这些目标并不容易,毕竟每个人都已经在成长过程中积累起了自己的一套工作方式。但如果能够做到,这一切将改变你的职业生涯。

10 倍程序员的背后,可能代表着 10 倍错误

这类开发者的根本问题,在于他们的顶头上司。

  公司里还有一位与众不同的开发者,我们叫他 James。从某种意义上说,他在公司已经拥有相当丰富的资历,非常擅长处理一部分编程任务。但他不愿意为自己的错误负责,经理多次批评还是无济于事。

  最要命的是,其他人的大部分工作都处于 James 团队开发成果的下游。所以如果他弄错了,每个人都能感觉到;而如果别人弄错了,对他几乎没有影响。这就是上下游依赖关系的基本特征,要求上游一方必须拥有强大的责任心。

  那么,为什么会陷入这么糟糕的状况呢?因为这位牛仔不相信单元测试,觉得这纯粹是在“浪费时间”,但其他人需要为他的武断买单。此外,他会反复把有问题的代码(包括无法编译或者存在严重阻塞问题的代码)添加到其他人正在使用的分支中,搞得公司内部民怨沸腾。

  这类开发者的根本问题,在于他们的顶头上司。这帮管理者没有建立良好的实践,甚至把这种独行侠式的坏习惯视为理所当然。

写在最后

  我觉得这个世界上的 10 倍开发者也分好几种,有自私型的、有乐于助人型的、有平易近人型的,也有令人生畏型的。如果大家有天分能够加入 10 倍开发者阵营,希望各位能认真选择自己想成为哪一种类型。

来源:http://k.sina.com.cn/article_1746173800_68147f6802700xlnz.html

收起阅读 »

我是一位10倍速开发者,但却感到很孤独

近日,有位网友(@100011_100001,下称11)在HN上倾诉了自己的烦恼。据11的描述,他是一个相当优秀的开发者,但他感觉很孤独。以下是11的自述内容:我是一个10x developer(10倍效率的开发者,简称10倍速开发者),但我讨厌这种称呼。如今...
继续阅读 »

当一个人在从事的领域能力越出众、站得越高,那“高处不胜寒”的感觉就会越强烈。就像搜狐创始人张朝阳在一次采访中说的:“我是真的什么都有,想有什么我都可以买,但是我居然这么痛苦。”

本以为商界大佬的这种苦恼无人能懂,但没想到在技术界,也有程序员在经历着能力出众带来的孤独感。

近日,有位网友(@100011_100001,下称11)在HN上倾诉了自己的烦恼。据11的描述,他是一个相当优秀的开发者,但他感觉很孤独。以下是11的自述内容:

1 “从事开发十年,我在团队中的地位越来越高”

我是一个10x developer(10倍效率的开发者,简称10倍速开发者),但我讨厌这种称呼。

故事开始并不是这样的。我在33岁的时候成为一名小开发者,人们一直认为我比看起来更有经验。我不确定是我的生活经历还是对自我完善的不懈追求,我一直在不断提高自己的能力。


大约在我38岁的时候,我觉得自己有了一定的能力。因为我的代码质量越来越高,只是速度相对会慢一点。这时,我受到了产品负责人的夸赞:“虽然你要多花些时间来完成任务,但我对你的完成质量完全放心。”

如今,我已经42岁了。现在我已能实现在代码只有极少Bug的情况下快速完成任务,而且工作时间也是标准的朝八晚五。当然,我现在参与了更多的会议、架构讨论、前沿的概念验证等等,但这并没有让我慢下来,反而使我的速度更快。

我从未打算成为一个超强的开发者,但在过去几年,我明显注意到了周围人对我的需求。比如,技术负责人邀请我参加会议,并让我发表看法;从未和我合作过的程序员也给我打电话,只因为“你可能知道答案”;我还被要求参与其他部门的代码审查。最近我在参加会议时发现,就连之前从未和我有过交流的人都知道我,甚至我的经理在介绍我时也只说 :“这是X,你可能听说过他。”

2 “10人团队,我一人完成71%的工作”

有人做了一个调查,看看我所在的应用程序组(大约350个开发者)按用户划分的git提交数量。结果发现,我写的一个执行各种自动化任务的脚本是第一名,我是第二名。这让我很吃惊,也让我无法压制住一些想法和感受,因此有了这篇长文。

虽然我经常提交、审查和合并代码,但我并不认为git提交的数量能证明什么。为了找到更具体的例子来证明我的“疏离感”,我查看了一下Jira故事和故事点。事实证明,在我的10人团队中(包括我自己),我在2022年完成了所有故事点的71%,其他9人负责另外29%,这与我的git提交数量相比也是一致的。

当然,我说这个话题的重点并不是为了吹牛(如果我有这种意思,我道歉)。我想表达的是,这种处境让我很孤独,很有压力。因为在这之前,我的一些决定还会受到质疑,这让我觉得很感激,因为质疑会让我创造出更好的解决方案。而现在,人们只是接受我说的任何东西,并认为我的方法是最好的。

在这种处境下,我感觉自己没有同伴,我只是在拖着我的整个团队和我周围的人一起走。这让我压力很大,感觉有些事情如果我不做,它就不会被完成,同时我也担心自己会因为受不到挑战而变得自满。


可怕的是,我发现自己有很大的控制权,因为团队负责的应用程序中,大约80%的代码库都是我编写的代码。我认为这明显不是一件好事,但他们好像一点也不以为意。最糟糕的是,我内心深处产生了一种挫败感,因为团队其他人的行动似乎都很缓慢。

总结一下,我是一个非常优秀的开发者,我喜欢写代码。然而我觉得自己很孤独,也害怕自己会对自身的能力而感到自负。我是唯一有这种感觉的人吗?我可以做些什么来改变现状?

3 另寻出路还是帮助团队进步?

对于11的烦恼,网友@ctvo的分析获得了最多的点赞:“我认为你是一个高于平均水平的开发者,在一家低于平均水平的公司工作。这可能是你所有问题的根源。你明知道同事没有负重前行,但还是要和他们一起工作,这实在是让人很沮丧。无论如何,我认为你不适合你目前的公司及其文化。”

网友@elviejo称:“如果你是房间里最聪明的人,那么你就进错了房间。但说真的,如果你喜欢编码,那你或许应该换一家公司,寻找更难的挑战。”


还有网友认为,11已经在多年的编码工作上取得了太多的成就,听完他的自述,明显是到了需要过渡的地步。完全可以减少编码工作,开始更多的教学

网友@symby:“我不认为找一份新工作可以解决问题,我建议你不要再自己做那么多贡献,而是开始帮助别人让他们做出贡献。‘大树底下长不出大树’,或许正是你远超他人的能力投射成了阴影,使你的同事难以成长?10倍速开发者很了不起,但很难被复刻,最好是让自己同事的能力也变强,扩大团队的产出。”

最后,你有过因为能力太强导致自己在团队里被过分“依赖”的经历吗?对这种因为“过于优秀”而产生的孤独感,你又有什么好的建议?欢迎在评论区留言~

参考链接:

  • news.ycombinator.com/item?id=31438426

来源:mp.weixin.qq.com/s/vljoD9c6-m7Jx9toGI5wog

收起阅读 »

Three.js控制物体显示与隐藏的方法

本文会讲解一下Three.js控制物体显示与隐藏的方法,主要包括以下几种方式:visible属性;layers属性。下面会分别通过简单的例子介绍下上述几个方式的简单使用方法和一些它们之间的区别。如果没有特殊说明,下面的源码以 r105 版本...
继续阅读 »

本文会讲解一下Three.js控制物体显示与隐藏的方法,主要包括以下几种方式:

  1. visible属性;
  2. layers属性。

下面会分别通过简单的例子介绍下上述几个方式的简单使用方法和一些它们之间的区别。如果没有特殊说明,下面的源码以 r105 版本为例:

visible属性

visible 是Object3D的属性。只有当 visible 是 true 的时候,该物体才会被渲染。任何继承 Object3D 的对象都可以通过该属性去控制它的显示与否,比如:MeshGroupSpriteLight等。

举个简单的例子:

// 控制单个物体的显示和隐藏
const geometry = new THREE.PlaneGeometry(1, 1) // 1*1的一个平面
const planeMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 }) // 红色平面
const plane = new THREE.Mesh(geometry, planeMaterial)
plane.visible = false // 不显示单个物体
scene.add(plane)
// 控制一组物体的显示和隐藏
const geometry = new THREE.PlaneGeometry(1, 1)
const planeMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 })
const plane = new THREE.Mesh(geometry, planeMaterial)
const group = new THREE.Group()
group.add(plane)
group.visible = false // 不显示一组物体
scene.add(group)

通过后面的例子可以看出,当我们想要控制一组物体的显示与隐藏,可以把这些物体放入一个 Group 中,只通过控制 Group 的显示与隐藏即可。

这块的代码逻辑是在WebGLRenderer.js的 projectObject 方法中实现的。

首先,在 render 方法中调用了 projectObject 方法:

this.render = function ( scene, camera ) {
// ...
projectObject( scene, camera, 0, _this.sortObjects );
// ...
}

projectObject 方法的定义如下:

function projectObject( object, camera, groupOrder, sortObjects ) {
if ( object.visible === false ) return; // 注释1:visible属性是false直接返回
// ...
var children = object.children; // 注释2:递归应用在children上

for ( var i = 0, l = children.length; i < l; i ++ ) {

projectObject( children[ i ], camera, groupOrder, sortObjects ); // 注释2:递归应用在children上

}
}

从注释1可以看出,如果 Group 的 visible 是 false,那么就不会在 children 上递归调用,所以就能达到通过 Group 控制一组对象的显示与隐藏的效果。

当 visible 是 false 的时候,Raycaster 的 intersectObject 或者 intersectObjects 也不会把该物体考虑在内。这块的代码逻辑是在 Raycaster.js

intersectObject: function ( object, recursive, optionalTarget ) {
// ...
intersectObject( object, this, intersects, recursive ); // 注释1:调用了公共方法intersectObject
// ...
},

intersectObjects: function ( objects, recursive, optionalTarget ) {
// ...

for ( var i = 0, l = objects.length; i < l; i ++ ) {

intersectObject( objects[ i ], this, intersects, recursive ); // 注释1:循环调用了公共方法intersectObject

}
// ...
}

// 注释1:公共方法intersectObject
function intersectObject( object, raycaster, intersects, recursive ) {

if ( object.visible === false ) return; // 注释1:如果visible是false,直接return

// ...
}

从注释1可以看出,如果 Group 或者单个物体的 visible 是 false ,就不做检测了。

layers属性

Object3D的layers属性 是一个 Layers 对象。任何继承 Object3D 的对象都有这个属性,比如 Camera 。Raycaster 虽然不是继承自 Object3D ,但它同样有 layers 属性(r113版本以上)。

和上面的 visible 属性一样,layers 属性同样可以控制物体的显示与隐藏、Raycaster 的行为。当物体和相机至少有一个同样的层的时候,物体就可见,否则不可见。同样,当物体和 Raycaster 至少有一个同样的层的时候,才会进行是否相交的测试。这里,强调了是至少有一个,是因为 Layers 可以设置多个层。

Layers 一共可以表示 32 个层,0 到 31 层。内部表示为:




Layers 可以设置同时拥有多个层:

  1. 可以通过 Layers 的 enable 和 disable 方法开启和关闭当前层,参数是上面表格中的 0 到 31 。
  2. 可以通过 Layers 的 set 方法 只开启 当前层,参数是上述表格中的 0 到 31
  3. 可以通过 Layers 的 test 的方法判断两个 Layers 对象是否存在 至少一个公共层 。

当开启多个层的时候,其实就是上述表格中的二进制进行 按位或 操作。比如 同时 开启 0231 层,那么内部存储的值就是 10000000000000000000000000000101

layers 属性默认只开启 0 层。

还是上面那个例子,我们看下怎么控制物体的显示和隐藏:

// 控制单个物体的显示和隐藏
const geometry = new THREE.PlaneGeometry(1, 1)
const planeMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 })
const plane = new THREE.Mesh(geometry, planeMaterial)
plane.layers.set(1) // 设置平面只有第1层,相机默认是在第0层,所以该物体不会显示出来
scene.add(plane)
// 控制一组物体的显示和隐藏
const geometry = new THREE.PlaneGeometry(1, 1)
const planeMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 })
const plane = new THREE.Mesh(geometry, planeMaterial)
const group = new THREE.Group()
group.layers.set(1) // 注释1: 设置group只有第一层,相机默认是在第0层,但是此时平面物体还是显示出来了?
group.add(plane)
scene.add(group)

设置单个物体的 layer 可以看到物体成功的没有显示出来。但是,当我们给 group 设置 layer 之后,发现 group 的 children(平面物体)还是显示了出来。那么,这是什么原因呢?让我们看下源码,同样还是上面的 projectObject 方法:

function projectObject( object, camera, groupOrder, sortObjects ) {

if ( object.visible === false ) return;

var visible = object.layers.test( camera.layers ); // 注释1:判断物体和相机是否存在一个公共层

if ( visible ) { // 注释1:如果存在,对物体进行下面的处理
// ...
}

var children = object.children; // 注释1:不管该物体是否和相机存在一个公共层,都会对children进行递归

for ( var i = 0, l = children.length; i < l; i ++ ) {

projectObject( children[ i ], camera, groupOrder, sortObjects );

}
}

从上述注释1可以看出,即使该物体和相机不存在公共层,也不影响该物体的 children 显示。这也就解释了上述为什么给 group 设置 layers ,但是平面物体还是能显示出来。从这一点上来看,layers 和 visible 属性在控制物体显示和隐藏的方面是不一样的。

和 visible 属性一样,接下来我们看下 Layers 对 Raycaster 的影响。同样我还是看了 Raycaster.js 文件,但是发现根本就没有 layers 字段。后来,我看了下最新版本 r140 的 Raycaster.js

function intersectObject( object, raycaster, intersects, recursive ) {

if ( object.layers.test( raycaster.layers ) ) { // 注释1:判断物体和Raycaster是否有公共层

object.raycast( raycaster, intersects );

}

if ( recursive === true ) { // 注释1:不管该物体和Raycaster是否有公共层,都不影响children

const children = object.children;

for ( let i = 0, l = children.length; i < l; i ++ ) {

intersectObject( children[ i ], raycaster, intersects, true );

}
}
}

不同于前面,visible 和 layers 都可以用来控制物体的显示与隐藏,visible 和 layers 只有一个可以用来控制 Raycaster 的行为,具体是哪一个生效,可以看下 Three.js的迁移指南

可以看到,从 r114 版本,废除了 visible ,开始使用 layers 控制 Raycaster 的行为:

r113 → r114
Raycaster honors now invisible 3D objects in intersection tests. Use the new property Raycaster.layers for selectively ignoring 3D objects during raycasting.

总结

从上面可以看出,visible 和 layers 在控制物体显示与隐藏、Raycaster 是否进行等方面是存在差异的。

当该物体的 visible 属性为 false 并且 layers 属性测试失败的时候,行为总结如下:


原文链接:https://segmentfault.com/a/1190000041881241

收起阅读 »

强制20天内开发APP后集体被裁,技术负责人怒用公司官微发文:祝“早日倒闭!”

身为一名程序员,你一定经历过为了准时上线项目而通宵敲代码,也一定经历过为了完善项目而连夜找BUG。需求、Deadline是代码之外,伴随程序员终身的两件大事。但当你因BUG被领导破口大骂、当你通宵达旦完成项目上线后却还被公司解雇,这时你会怎么做?近日,一家科技...
继续阅读 »

身为一名程序员,你一定经历过为了准时上线项目而通宵敲代码,也一定经历过为了完善项目而连夜找BUG。需求、Deadline是代码之外,伴随程序员终身的两件大事。

但当你因BUG被领导破口大骂、当你通宵达旦完成项目上线后却还被公司解雇,这时你会怎么做?

近日,一家科技公司的微信公众号发布了一篇祝公司“早日倒闭”的文章,疑似遭原技术团队的负责人盗号。以下为具体内容:

通宵加班,最终换来团队被集体解雇

文章中称,3月末,该团队被公司要求在20天内就开发出一款APP。但该负责人表示,稍微有点常识的人都知道,一款APP从设计到开发,最快也需要40天左右的时间。无奈之下,团队全员只能在居家办公、沟通不便的情况下天天通宵加班,最终在4月中旬开发出了新的APP。

在这么短时间内完成开发的产品,出现一部分BUG也是很正常的。“但是某李姓领导,在出现BUG后冲进我们的办公室,一通大骂,语言粗鄙不堪……”,“在修复BUG的过程中,李某不断施压,强制要求大家加班,有一次凌晨3点多给我打电话,一通指责……”

此外,该负责人还补充道,4月末,(李某)又要求增加一大堆狗屁不通的所谓新功能,比如im即时通讯功能要求在一周内完成。但负责人表示,这种自己开发的功能至少要两个月。

最后,团队负责人透露,他们努力了、付出了、熬夜了,最终换来的却是集体解雇


图源:微博

这篇文章发出后,经过不断地传播,阅读迅速达到了10万+。而后晚间,该文章才被删除。

公司回应:与事实严重不符,严重抹黑公司形象

舆论不断发酵,随后该公司连夜发布了回应声明:

  1. 该文章内容与事实严重不符,严重抹黑公司形象;

  2. 微信公众号文章发布后,已要求公司法务处理相关事宜,并与原技术取得沟通,现已妥善解决该问题;

  1. 因处理该问题需要时间,所以公众号文章未能及时删除,并非公司恶意炒作;

  1. 对各大平台及个人针对该文章进行断章取义的宣传,我公司保留追究其法律责任的权利;

  2. 我司对占用了大家的时间和公共资源表示歉意,请不要恶意转载。


图源:官方公众号

网友:“希望不要是什么营销手段”

目前,该公司回应“已妥善解决问题”,事实如何也无从得知。对此,网友表示:

  • “专业的事情一定要专业的人来做,如果让一个不懂行的人来弄,那肯定是乱套了。”

  • “记得我之前的领导经常这样说:‘不要告诉我过程,我要的是结果。’”

  • “人家辛辛苦苦研究出来了APP!然后遭到了解雇,有时候真的是不可想象。”

  • “如果是真的的话,公司也太卸磨杀驴了吧,打工人真的敢去吗?”

  • “如果真是被解雇了,是不是应该用法律武器来维护利益?”

最后,你对该事件有什么看法?

来源:程序人生(ID:coder_life)

收起阅读 »

源码阅读原则

不是绝对的,只是提供一种大致的思路大致的了解一个类、方法、字段所代表的含义明确你需要了解某个功能A的实现,越具体越好,列出切入点,然后从上至下的分析对于行数庞大、逻辑复杂的源码,我们在追踪时遇到非相关源码是必定的,可以简单追踪几个层级,给自己定一个界限,否则容...
继续阅读 »

源码阅读原则

不是绝对的,只是提供一种大致的思路

见名之意

大致的了解一个类、方法、字段所代表的含义

切入点

明确你需要了解某个功能A的实现,越具体越好,列出切入点,然后从上至下的分析

分支

对于行数庞大、逻辑复杂的源码,我们在追踪时遇到非相关源码是必定的,可以简单追踪几个层级,给自己定一个界限,否则容易丢失目标,淹没在源码的海洋中

分支字段

追踪有没有直接返回该字段的方法,通过方法注释,直接快速了解该字段的作用。

对于没有向外暴露的字段,我们追踪它的usage

  • 数量较少:可以通过各usage处的方法名大致了解,又或者是直接阅读源码

  • 数量较多:建议另辟蹊径,实在没办法再逐一攻破

分支方法

首先是阅读方法注释,有几种情况:

  • 涉及新术语:在类中搜索关键字找到相关方法或类

  • 涉及新的类:看分支类

  • 功能A相关:略过

分支类

先阅读理解类注释,有以下几种情况:

  • 涉及到新的领域:通过查看继承树的方式,大致了解它规模体系和作用

  • 不确定和功能A是否有关联:可查阅官方文档或者搜索引擎做确定

断点调试

动态分析的数据能够帮助我们去验证我们的理解是否正确,实践是检验真理的唯一标准

usage截止点

当你从某个方法出发,寻找它是在何处调用时,请记住你的目的,我们应该在脱离了强相关功能方法处截止,继续usage的意义不大。

比如RecyclerViewscrapOrRecycleView,我们的目的是:寻找什么时候触发了回收View

应该在onLayoutChildren处停止,再继续usage时,你的目的就变成了:寻找什么时候布置Adapter所有相关的子View


作者:土猫少侠
来源:juejin.cn/post/7100806273460863006

收起阅读 »

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

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

协程(coroutines)的封装

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

封装前例子

假如我们有个一个suspend函数

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

我们要实现的封装是:

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

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

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

4.要不,来个DSL风格

//
async{
  // 请求
  getTest()

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

},onComplete = {

})

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

封装开始

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

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

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

      try{          
          val result = this@await.await()    
          onSuccess(result)
           
      }catch (e:Exception){
          onError(e)
      }
      finally {
          onComplete?.invoke()
      }
  }
}

总结

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


作者:Pika
来源:https://juejin.cn/post/7100856445905666079

收起阅读 »

qiankun微前端

本文参考: 官网 你可能并不需要微前端什么是微前端?Techniques, strategies and recipes for building a modern web app with multiple teams that can ship fea...
继续阅读 »

本文参考
官网
你可能并不需要微前端

什么是微前端?

Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently. -- Micro Frontends 微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。

qiankun是怎么来的?

所有的技术都是为了解决当前的现实问题,然后通过思考和实践创造出来的。微前端本质上是为了解决组织和团队间协作带来的沟通和管理的问题

引用微前端作者的思想:

微前端是康威定律在前端架构上的映射。 康威定律指导思想:既然沟通是大问题,那么就不要沟通就好了

作者认为大型系统都逃不过熵增定律,宇宙的本质,所有的东西都会从有序走向无序。一个东西如果你不去管理,他就会变成一坨垃圾,所以你想要维持一个东西的有序性,就要付出努力去维护他。所以从中找到平衡,qiankun就诞生了。通过分治的手段,让上帝的归上帝,凯撒的归凯撒

什么情况下使用qiankun?

我们在开发中可能会碰到下面的问题

  • 旧的系统不能下,新的需求还在来

  • 公司内部有很多的系统,不同系统间可能需要展示同一个页面

  • 一个系统过于庞大,每个人分别管理一个模块,git分支比较混乱。想要把系统拆分开来

微前端首先解决的,是如何解构巨石应用

核心价值:技术栈无关,应用之间不应该有任何直接或间接的技术栈、依赖、以及实现上的耦合。

作者认为正确的微前端方案的目标应该是

方案上跟使用 iframe 做微前端一样简单,同时又解决了 iframe 带来的各种体验上的问题

qiankun的原理

qiankun 是一个基于 single-spa微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。

qiankun框架内部fetch请求资源,解析出js、css文件和HTML document,插入到主应用指定的容器中(使用HTML Entry接入方式)

  1. 调用import-html-entry模块的importEntry函数,获取到对应子应用的html文件、可执行脚本文件以及publicpath

  2. 调用getDefaultTplWrapper将子应用的html内容用div标签包裹起来

  3. 调用createElement函数生成剔除html、body、head标签后的子应用html内容(通过innerHTML达到过滤效果)

  4. 调用getRender函数得到render函数(所以子应用一定要有render函数)

  5. 调用第4步得到的render,将container内部清空,并将子应用的dom元素渲染到指定的contanter元素上

  6. 调用getAppWrapperGetter函数,生成一个可以获取处理过的子应用dom元素的函数initialAppWrapperGetter,以备后续使用子应用dom元素

  7. 如果sandbox为true,则调用createSandboxContainer函数

  8. 执行execScripts函数,执行子应用脚本

  9. 执行getMicroAppStateActions函数,获取onGlobalStateChange、setGlobalState、offGlobalStateChange,用于主子应用传递信息

  10. 执行parcelConfigGetter函数,包装mount和unmount

上述步骤的源码

qiankun如何实现隔离?

沙箱隔离

qiankun的沙箱有2种 JS沙箱 和 CSS沙箱

JS沙箱

JS沙箱又分为2种,快照沙箱(为了兼容IE)和 代理沙箱

快照沙箱 snapshotSandbox

基于diff实现,用来兼容不支持Proxy的浏览器,只适用单个子应用。会污染全局window

  1. 激活沙箱:将主应用window的信息存到windowSnapshot

  2. 根据

    modifyPropMap

    ,恢复为子应用的window信息

    读取和修改的是window中的数据,windowSnapshot是缓存的数据

  3. 退出沙箱:根据windowSnapshot把window恢复为主应用数据,将windowSnapshot和window进行diff,将变更的值存到modifyPropMap中,然后把window恢复为主应用数据

总结

  • windowSnapshot主应用的window信息

  • modifyPropMap子应用修改的window信息

相对应的源码

代理沙箱

代理沙箱也分为2种,单例和多例,都是由Proxy实现

单例沙箱 legacySandbox

为了兼容性 singular 模式下依旧使用该沙箱,等新沙箱稳定之后再切换。 创建 addedPropsMapInSandbox(沙箱期间新增的全局变量)、modifiedPropsOriginalValueMapInSandbox(沙箱期间更新的全局变量)、currentUpdatedPropsValueMap(持续记录更新的(新增和修改的)全局变量的 map 用于在任意时刻做 snapshot) 三个变量,前两个用来恢复主应用window,最后一个用来恢复子应用window。同样会污染window,但性能比快照沙箱稍好,不用遍历window

  1. 激活沙箱:根据currentUpdatedPropsValueMap还原子应用的window数据

  2. window只要变动,在

    currentUpdatedPropsValueMap

    中进行记录

    1. 判断addedPropsMapInSandbox中是否有对应 key 的记录,没有新增一条,有的话往下执行

    2. 判断modifiedPropsOriginalValueMapInSandbox中是否有对应 key 的记录,没有的话,记录从window中对应key/value,有的话继续往下执行

    3. 修改window对应的key/value

  3. 退出沙箱:根据addedPropsMapInSandboxmodifiedPropsOriginalValueMapInSandbox还原主应用的window信息

相对性的源码

多例沙箱 proxySandbox

主应用和子应用的window独立,不再共同维护一份window,终于JS沙箱也和qiankun微前端的思想统一了…实行了分治。不会污染全局window,支持多个子应用。

  1. 激活沙箱

  2. 取值,先从自己命名空间下的fakeWindow找key,没找到,找window

  3. 赋值,直接给自己命名空间下的fakeWindow赋值

  4. 退出沙箱

相对应的源码

CSS沙箱

严格沙箱 和 实验性沙箱

严格沙箱

在加载子应用时,添加strictStyleIsolation: true属性,会将整个子应用放到Shadow DOM内进行嵌入,完全隔离了主子应用


缺点:子应用中应用的一些弹框组件会因为找不到body而丢失

实验性沙箱

在加载子应用时,添加experimentalStyleIsolation: true属性,实现形式类似于vue中style标签中的scoped属性,qiankun会自动为子应用所有的样式增加后缀标签,如:div[data-qiankun=“xxx”],这里的XXX为注册子应用时name的值


缺点:子应用中应用的一些弹框组件会因为插入到了主应用到body而丢失样式

相对应的源码


作者:丙乙
来源:https://juejin.cn/post/7100825726424711204

收起阅读 »

v-for中diff算法

当没有key时获取新旧数组长度,取最短的数组(Math.min())进行比较,如果用长的数组进行比较,会发生越界错误以短数组进行for循环,从新旧数组各组一个值进行patch,如果内容一样就不进行更新,如果内容不一样,Vue源码会进行更深层次的比较,如果类型都...
继续阅读 »

当没有key时


获取新旧数组长度,取最短的数组(Math.min())进行比较,如果用长的数组进行比较,会发生越界错误


以短数组进行for循环,从新旧数组各组一个值进行patch,如果内容一样就不进行更新,如果内容不一样,Vue源码会进行更深层次的比较,如果类型都不一样的话,直接创建一个新类型,如果类型一样,值不同,就只更新值,效率会更高,当for循环完毕,新旧数组长度会进行比较,如果旧的长度大有新的长度,就会执行unmountChildren,删除多余的节点,如果新的长度大于旧的长度,就会执行mountChildren,创建新的节点

当有key时


第一步,从头部开始遍历


通过isSameVNodeType进行比较


如果type 和 key 都一样,继续遍历,如果不同,跳出循环,进入第二步

第二步,从尾部开始遍历


和第一步操作一致

如果不同,跳出循环进入第三步

第三步,果旧节点遍历完,依然有新的节点,就是添加节点操作,用一个null和新节点进行patch,n1为空值时,是添加



如果新节点遍历完了,旧节点还有就进入第四步

第四步,新节点遍历完毕,旧节点还有,就进行删除操作


第五步,如果是一个无序的节点,vue会从旧的节点里找到新的节点里相同的值并创建一个新的数组,根据key建立一个索引,找到了就放入新数组里,比较完之后,有多余的旧节点就删除,有没有比较过的新节点就添加


作者:啊哈呀呀呀呀
来源:juejin.cn/post/7100858461520560135

收起阅读 »

IP属地获取,前端获取用户位置信息

尝试获取用户的位置信息写在前面想要像一些平台那样显示用户的位置信息,例如某省市那样。那么这是如何做到的, 据说这个位置信息的准确性在通信网络运营商那里?先不管,先实践尝试下能不能获取。尝试一:navigator.geolocation尝试了使用 navigat...
继续阅读 »


尝试获取用户的位置信息

写在前面

想要像一些平台那样显示用户的位置信息,例如某省市那样。那么这是如何做到的, 据说这个位置信息的准确性在通信网络运营商那里?先不管,先实践尝试下能不能获取。

尝试一:navigator.geolocation

尝试了使用 navigator.geolocation,但未能成功拿到信息。

getGeolocation(){
 if ('geolocation' in navigator) {
   /* 地理位置服务可用 */
   console.log('地理位置服务可用')
   navigator.geolocation.getCurrentPosition(function (position) {
     console.dir('回调成功')
     console.dir(position) // 没有输出
     console.dir(position.coords.latitude, position.coords.longitude)
  }, function (error) {
     console.error(error)
  })
} else {
   /* 地理位置服务不可用 */
   console.error('地理位置服务可用')
}
}

尝试二:sohu 的接口

尝试使用 pv.sohu.com/cityjson?ie… 获取用户位置信息, 成功获取到信息,信息样本如下:

{"cip": "14.11.11.11", "cid": "440000", "cname": "广东省"}
// 需要做跨域处理
getIpAndAddressSohu(){
 // config 是配置对象,可按需设置,例如 responseType,headers 中设置 token 等
 const config = {
   headers: {
     Accept: 'application/json',
     'Content-Type': 'application/json;charset=UTF-8',
  },
}
 axios.get('/apiSohu/cityjson?ie=utf-8', config).then(res => {
   console.log(res.data) // var returnCitySN = {"cip": "14.23.44.50", "cid": "440000", "cname": "广东省"};
   const info = res.data.substring(19, res.data.length - 1)
   console.log(info) // {"cip": "14.23.44.50", "cid": "440000", "cname": "广东省"}
   this.ip = JSON.parse(info).cip
   this.address = JSON.parse(info).cname
})
}

调试的时候,做了跨域处理。

proxy: {
 '/apiSohu': {
   target: 'http://pv.sohu.com/', // localhost=>target
   changeOrigin: true,
   pathRewrite: {
   '/apiSohu': '/'
  }
},
}

下面是一张获取到位置信息的效果图:


尝试三:百度地图的接口

需要先引入百度地图依赖,有一个参数 ak 需要注意,这需要像管理方申请。例如下方这样

<script src="https://api.map.baidu.com/api?v=2.0&ak=3ufnnh6aD5CST"></script>
getLocation() { /*获取当前位置(浏览器定位)*/
const $this = this;
var geolocation = new BMap.Geolocation();//返回用户当前的位置
geolocation.getCurrentPosition(function (r) {
  if (this.getStatus() == BMAP_STATUS_SUCCESS) {
    $this.city = r.address.city;
    console.log(r.address) // {city: '广州市', city_code: 0, district: '', province: '广东省', street: '', …}
  }
});
}
function getLocationBaiduIp(){/*获取用户当前位置(ip定位)*/
function myFun(result){
  const cityName = result.name;
  console.log(result) // {center: O, level: 12, name: '广州市', code: 257}
}
var myCity = new BMap.LocalCity();
myCity.get(myFun);
}

成功用户的省市位置,以及经纬度坐标,但会先弹窗征求用户意见。



写在后面

尝试结果不太理想,sohu 的接口内部是咋实现的,这似乎没有弹起像下面那样的征询用户意见的提示。


而在 navigator.geolocation 和 BMap.Geolocation() 中是弹起了的。

用别人的接口总归是没多大意思,也不知道不用征求用户意见是咋实现的。

经实测 sohu 的接口和 new BMap.Geolocation() 都可以拿到用户的位置信息(省市、经纬度等)。

作者:灵扁扁

来源:https://juejin.cn/post/7100916925504421918

收起阅读 »

一种兼容、更小、易用的WEB字体API

如何使用 Google Fonts CSS API 有效地使用WEB字体?多年来,WEB字体技术发生了很多变化,过去在WEB中使用特殊字体的常用做法是图片或者Flash,这种借助图片或者Flash的实现方式不够灵活。随着 WEB 字体的出现,特别是 Googl...
继续阅读 »

如何使用 Google Fonts CSS API 有效地使用WEB字体?

多年来,WEB字体技术发生了很多变化,过去在WEB中使用特殊字体的常用做法是图片或者Flash,这种借助图片或者Flash的实现方式不够灵活。随着 WEB 字体的出现,特别是 Google Fonts CSS API 的普及,让在WEB中使用特殊字体变得简单、快速、灵活,当然更多的还是面向英文字体,对于做外贸或者英文网站的开发者来说是福音。

Google Fonts CSS API 在不断发展,以跟上WEB字体技术的变化。它从最初的价值主张——允许浏览器在所有使用API的网站上缓存常用字体,从而使网页加载更快,到现在已经有了很大的进步。现在不再是这样了,但API仍然提供了额外的优化方案,使网站加载迅速,字体工作性能更佳。

使用Google Fonts CSS API ,网站可以请求它需要的字体数据来保持它的CSS加载时间到最少,确保网站访问者可以尽可能快地加载内容。该API将以最佳的字体响应每个请求的web浏览器。

所有这一切都是通过在代码中包含一行 HTML 来实现的。

如何使用 Google Fonts CSS API

Google Fonts CSS API 文档很好地总结了它:

你不需要做任何编程;所要做的就是在 HTML 文档中添加一个特殊的样式表链接,然后在 CSS 样式中引用该字体。

需要做的最低限度是在 HTML 中包含一行,如下所示:

<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap" rel="stylesheet" />

复制代码

当从 API 请求字体时,可以指定想要的一个或多个系列,以及(可选)它们的权重、样式、子集和其他选项。然后 API 将通过以下两种方式之一处理请求:

  1. 如果请求使用 API 已有文件的通用参数,它会立即将 CSS 返回给用户,将定向到这些文件。

  2. 如果请求的字体带有 API 当前未缓存的参数,它将即时对字体进行子集化,使用 HarfBuzz 快速完成,并返回指向它们的 CSS。

字体文件可以很大,但不一定要很大

WEB 字体可以很大,在 WOFF2 中,仅一个 Noto Sans Japanese 的大小就几乎是 3.4MB ,将其下载给每一位用户将拖累页面加载时间。当每一毫秒都很重要并且每个字节都很宝贵时,需要确保只加载用户需要的数据。

Google Fonts CSS API 可以创建非常小的字体文件(称为子集),实时生成,只为用户提供网站所需的文本和样式。可以使用 text 参数请求特定字符,而不是提供整个字体。

<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap&text=RobtMn" rel="stylesheet" />

复制代码


CSS API 还自动为用户提供额外的WEB字体优化,无需设置任何 API 参数。该 API 将为用户提供已启用 unicode-range 的 CSS 文件(如果 Web 浏览器支持),因此只为网站需要的特定字符加载字体。

unicode-range CSS 描述符是一种现在可用于应对大字体下载的工具,这个 CSS 属性设置 @font-face 声明包含的 Unicode 字符范围。如果在页面上呈现这些字符之一,则下载该字体。这适用于所有类型的语言,因此可以采用包含拉丁文、希腊文或西里尔文字符的字体并制作更小的子集。在前面的图表中,可以看到如果必须加载所有这三个字符集,则将超过 600 个字形。


这也为 Web 启用了中文、日文和韩文 (CJK) 字体提供支持。在上图中,可以看到 CJK 字体覆盖的字符数是拉丁字符字体的 15-20 倍。 CJK 字体通常非常大,并且这些语言中的许多字符不像其他字体那样频繁使用。

使用 CSS API 和 unicode-range 可以减少大约 90% 的文件传输。使用 unicode-range 描述符,可以单独定义每个部分,并且只有在内容包含这些字符范围中的一个字符时才会下载每个切片。

例如只想在 Noto Sans JP 中设置单词 こんにちは ,则可以按照如下方式使用:

  • 自托管自己的 WOFF2 文件

  • 使用 CSS API 检索 WOFF2

  • 使用 CSS API 并将 text= 参数设置为 こんにちは


在此示例中,可以看到通过使用 CSS API,已经比自托管 WOFF2 字体节省了 97.5%,这要归功于 API 内置支持将大字体分隔到 unicode-range 中功能。通过更进一步并准确指定要显示的文本,可以进一步将字体大小减小到仅 CSS API 字体的 95.3% ,相当于比自托管字体小 99.9%

Google Fonts CSS API 将自动以用户浏览器支持的最小和最兼容格式提供字体。如果用户使用的是支持 WOFF2 的浏览器,API 将提供 WOFF2 中的字体,但如果他们使用的是旧版浏览器,API 将以该浏览器支持的格式提供字体。为了减少每个用户的文件大小,API 还会在不需要时从字体中删除数据。例如,将为浏览器不需要的用户删除提示数据。

使用 Google Fonts CSS API 让WEB字体面向未来

Google 字体团队还为新的 W3C 标准做出了贡献,这些标准继续创新网络字体技术,例如 WOFF2。当前的一个项目是增量字体传输,它允许用户在屏幕上使用字体文件时加载非常小的部分,并按需流式传输其余部分,超过了 unicode-range 的性能。当使用 WEB 字体API时,当用户在浏览器中可用时,就可以获得这些底层字体传输技术的优化改进。

这就是字体 API 的美妙之处:用户可以从每项新技术改进中受益,而无需对网站进行任何更改。新的WEB字体格式?没问题,新的浏览器或操作系统支持?它已经处理好了。因此,可以自由地专注于用户和内容,而不是陷入WEB字体维护的困境。

可变字体支持内置

可变字体是可以在多个轴之间存储一系列设计变化的字体文件,新版本的 Google Fonts CSS API 包括对它们的支持。添加一个额外的变化轴可以使字体具有新的灵活性,但它几乎可以使字体文件的大小增加一倍。

当 CSS API 请求更具体时,Google Fonts CSS API 可以仅提供网站所需的可变字体部分,以减少用户的下载大小。这使得可以为 WEB 使用可变字体,而不会导致页面加载时间过长。可以通过在轴上指定单个值或指定范围来执行此操作,甚至可以在一个请求中指定多个轴和多个字体系列, API 可以灵活地满足需求。

总结

Google Fonts CSS API 可帮助WEB提供以下字体:

  • 更兼容

  • 体积更小

  • 加载快速

  • 易于使用

有关 Google 字体的更多信息,请访问 fonts.google.com


作者:天行无忌
来源:juejin.cn/post/7100927964224700424

收起阅读 »

跟我学flutter:细细品Widget(五)Element

前言跟我学flutter系列:跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制跟我...
继续阅读 »

前言

跟我学flutter系列:
跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin
跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate
跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制

跟我学flutter:在国内如何发布自己的Plugin 或者 Package

跟我学flutter:Flutter雷达图表(一)如何使用kg_charts

跟我学flutter:细细品Widget(一)Widget&Element初识

跟我学flutter:细细品Widget(二)StatelessWidget&StatefulWidget

跟我学flutter:细细品Widget(三)ProxyWidget,InheritedWidget

跟我学flutter:细细品Widget(四)Widget 渲染过程 与 RenderObjectWidget

跟我学flutter:细细品Widget(五)Element

企业级篇目:
跟我学企业级flutter项目:用bloc手把手教你搭建用户认证系统
跟我学企业级flutter项目:dio网络框架增加公共请求参数&header
跟我学企业级flutter项目:如何用dio封装一套企业级可扩展高效的网络层
跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview
跟我学企业级flutter项目:手把手教你制作一款低耦合空页面widget


之前的文章都有简述Element,这篇将着重去讲Element
Widget是描述一个UI元素的配置数据,Element才真正代表屏幕显示元素


分类


在这里插入图片描述
如上图所示Element分为两类




  • ComponentElement : 组合类Element。这类Element主要用来组合其他更基础的Element,得到功能更加复杂的Element。开发时常用到的StatelessWidget和StatefulWidget相对应的Element:StatelessElement和StatefulElement,即属于ComponentElement。




  • RenderObjectElement : 渲染类Element,对应Renderer Widget,是框架最核心的Element。RenderObjectElement主要包括LeafRenderObjectElement(叶子无节点),SingleChildRenderObjectElement(单child),和MultiChildRenderObjectElement(多child)。




Element生命周期


Element有4种状态:initial,active,inactive,defunct。其对应的意义如下:



  • initial:初始状态,Element刚创建时就是该状态。

  • active:激活状态。此时Element的Parent已经通过mount将该Element插入Element Tree的指定的插槽处(Slot),Element此时随时可能显示在屏幕上。

  • inactive:未激活状态。当Widget Tree发生变化,Element对应的Widget发生变化,同时由于新旧Widget的Key或者的RunTimeType不匹配等原因导致该Element也被移除,因此该Element的状态变为未激活状态,被从屏幕上移除。并将该Element从Element Tree中移除,如果该Element有对应的RenderObject,还会将对应的RenderObject从Render Tree移除。但是,此Element还是有被复用的机会,例如通过GlobalKey进行复用。

  • defunct:失效状态。如果一个处于未激活状态的Element在当前帧动画结束时还是未被复用,此时会调用该Element的unmount函数,将Element的状态改为defunct,并对其中的资源进行清理。


Element4种状态间的转换关系如下图所示:


在这里插入图片描述


ComponentElement


在这里插入图片描述



State和StatefulElement是一一对应的,只有在初始化StatefulElement时,才会初始化对应的State并将其绑定到StatefulElement上



核心流程


一个Element的核心操作流程有,创建、更新、销毁三种,下面将分别介绍这三个流程。


创建


在这里插入图片描述
ComponentElement的创建起源与父Widget调用inflateWidget,然后通过mount将该Element挂载至Element Tree,并递归创建子节点。


更新


在这里插入图片描述
由父Element执行更新子节点的操作(updateChild),由于新旧Widget的类型和Key均未发生变化,因此触发了Element的更新操作,并通过performRebuild将更新操作传递下去。其核心函数updateChild之后会详细介绍。


销毁


在这里插入图片描述
由父Element或更上级的节点执行更新子节点的操作(updateChild),由于新旧Widget的类型或者Key发生变化,或者新Widget被移除,因此导致该Element被转为未激活状态,并被加入未激活列表,并在下一帧被失效。


核心函数



  • inflateWidget


Element inflateWidget(Widget newWidget, dynamic newSlot) {
final Key key = newWidget.key;
//复用GlobalKey对应的Element
if (key is GlobalKey) {
final Element newChild = _retakeInactiveElement(key, newWidget);
if (newChild != null) {
newChild._activateWithParent(this, newSlot);
final Element updatedChild = updateChild(newChild, newWidget, newSlot);
return updatedChild;
}
}
//创建Element,并挂载至Element Tree
final Element newChild = newWidget.createElement();
newChild.mount(this, newSlot);
return newChild;
}
复制代码


  1. 判断新Widget是否有GlobalKey,如果有GlobalKey,则从Inactive Elements列表中找到对应的Element并进行复用。(可能从树的另一个位置嫁接或重新激活)

  2. 无可复用Element,则根据新Widget创建对应的Element,并将其挂载至Element Tree。



  • mount


void mount(Element parent, dynamic newSlot) {
//更新_parent等属性,将元素加入Element Tree
_parent = parent;
_slot = newSlot;
_depth = _parent != null ? _parent.depth + 1 : 1;
_active = true;
if (parent != null) // Only assign ownership if the parent is non-null
_owner = parent.owner;
//注册GlobalKey
final Key key = widget.key;
if (key is GlobalKey) {
key._register(this);
}
_updateInheritance();
}
复制代码


  1. 将给Element加入Element Tree,更新_parent,_slot等树相关的属性。

  2. 如果新Widget有GlobalKey,将该Element注册进GlobalKey中,其作用下文会详细分析。

  3. ComponentElement的mount函数会调用_firstBuild函数,触发子Widget的创建和更新。



  • performRebuild


@override
void performRebuild()
{
//调用build函数,生成子Widget
Widget built;
built = build();
//根据新的子Widget更新子Element
_child = updateChild(_child, built, slot);
}
复制代码


  1. 调用build函数,生成子Widget。

  2. 根据新的子Widget更新子Element。



  • update


@mustCallSuper
void update(covariant Widget newWidget) {
_widget = newWidget;
}
复制代码


  1. 将对应的Widget更新为新的Widget。

  2. 在ComponentElement的各种子类中,还会调用rebuild函数触发对子Widget的重建。



  • updateChild


@protected
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
if (newWidget == null) {
//新的Child Widget为null,则返回null;如果旧Child Widget,使其未激活
if (child != null)
deactivateChild(child);
return null;
}
Element newChild;
if (child != null) {
//新的Child Widget不为null,旧的Child Widget也不为null
bool hasSameSuperclass = true;
if (hasSameSuperclass && child.widget == newWidget) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
newChild = child;
} else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)){
//Key和RuntimeType相同,使用update更新
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
child.update(newWidget);
newChild = child;
} else {
//Key或RuntimeType不相同,使旧的Child Widget未激活,并对新的Child Widget使用inflateWidget
deactivateChild(child);
newChild = inflateWidget(newWidget, newSlot);
}
} else {
//新的Child Widget不为null,旧的Child Widget为null,对新的Child Widget使用inflateWidget
newChild = inflateWidget(newWidget, newSlot);
}

return newChild;
}
复制代码

根据新的子Widget,更新旧的子Element,或者得到新的子Element。
逻辑如下(伪代码):


if(newWidget == null){
if(Child == null){
return null;
}else{
移除旧的子Element,返回null
}
}else{
if(Child == null){
返回新Element
}else{
如果Widget能更新,更新旧的子Element,并返回之;否则创建新的子Element并返回。
}
}

复制代码

该逻辑概括如下:



  1. 如果newWidget为null,则返回null,同时如果有旧的子Element则移除之。

  2. 如果newWidget不为null,旧Child为null,则创建新的子Element,并返回之。

  3. 如果newWidget不为null,旧Child不为null,新旧子Widget的Key和RuntimeType等都相同,则调用update方法更新子Element并返回之。

  4. 如果newWidget不为null,旧Child不为null,新旧子Widget的Key和RuntimeType等不完全相同,则说明Widget Tree有变动,此时移除旧的子Element,并创建新的子Element,并返回之。


RenderObjectElement


RenderObjectElement同核心元素Widget及RenderObject之间的关系如下图所示:
在这里插入图片描述
如图:


RenderObjectElement持有Parent Element,但是不一定持有Child Element,有可能无Child Element,有可能持有一个Child Element(Child),有可能持有多个Child Element(Children)。
RenderObjectElement持有对应的Widget和RenderObject,将Widget、RenderObject串联起来,实现了Widget、Element、RenderObject之间的绑定。


核心流程


如ComponentElement一样,RenderObjectElement的核心操作流程有,创建、更新、销毁三种,接下来会详细介绍这三种流程。



  • 创建


-在这里插入图片描述


RenderObjectElement的创建流程和ComponentElement的创建流程基本一致,其最大的区别是ComponentElement在mount后,会调用build来创建子Widget,而RenderObjectElement则是create和attach其RenderObject。



  • 更新


在这里插入图片描述
RenderObjectElement的更新流程和ComponentElement的更新流程也基本一致,其最大的区别是ComponentElement的update函数会调用build函数,重新触发子Widget的构建,而RenderObjectElement则是调用updateRenderObject对绑定的RenderObject进行更新。



  • 销毁


在这里插入图片描述
RenderObjectElement的销毁流程和ComponentElement的销毁流程也基本一致。也是由父Element或更上级的节点执行更新子节点的操作(updateChild),导致该Element被停用,并被加入未激活列表,并在下一帧被失效。其不一样的地方是在unmount Element的时候,会调用didUnmountRenderObject失效对应的RenderObject。


核心函数



  • inflateWidget


该函数和ComponentElement的inflateWidget函数完全一致,此处不再复述。



  • mount


void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_renderObject = widget.createRenderObject(this);
attachRenderObject(newSlot);
_dirty = false;
}
复制代码

该函数的调用时机和ComponentElement的一致,当Element第一次被插入Element Tree的时候,该方法被调用。其主要职责也和ComponentElement的一致,此处只列举不一样的职责,职责如下:



  1. 调用createRenderObject创建RenderObject,并使用attachRenderObject将RenderObject关联到Element上。

  2. SingleChildRenderObjectElement会调用updateChild更新子节点,MultiChildRenderObjectElement会调用每个子节点的inflateWidget重建所有子Widget。



  • performRebuild


@override
void performRebuild()
{
//更新renderObject
widget.updateRenderObject(this, renderObject);
_dirty = false;
}
复制代码

performRebuild的主要职责如下:


调用updateRenderObject更新对应的RenderObject。



  • update


@override
void update(covariant RenderObjectWidget newWidget) {
super.update(newWidget);
widget.updateRenderObject(this, renderObject);
_dirty = false;
}
复制代码

update的主要职责如下:



  1. 将对应的Widget更新为新的Widget。

  2. 调用updateRenderObject更新对应的RenderObject。



  • updateChild


@protected
List updateChildren(List oldChildren, List newWidgets, { Set forgottenChildren }) {
int newChildrenTop = 0;
int oldChildrenTop = 0;
int newChildrenBottom = newWidgets.length - 1;
int oldChildrenBottom = oldChildren.length - 1;

final List newChildren = oldChildren.length == newWidgets.length ?
oldChildren : List(newWidgets.length);

Element previousChild;

// 从顶部向下更新子Element
// Update the top of the list.
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
final Widget newWidget = newWidgets[newChildrenTop];
if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget))
break;
final Element newChild = updateChild(oldChild, newWidget, IndexedSlot(newChildrenTop, previousChild));
newChildren[newChildrenTop] = newChild;
previousChild = newChild;
newChildrenTop += 1;
oldChildrenTop += 1;
}

// 从底部向上扫描子Element
// Scan the bottom of the list.
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenBottom]);
final Widget newWidget = newWidgets[newChildrenBottom];
if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget))
break;
oldChildrenBottom -= 1;
newChildrenBottom -= 1;
}

// 扫描旧的子Element列表里面中间的子Element,保存Widget有Key的Element到oldKeyChildren,其他的失效
// Scan the old children in the middle of the list.
final bool haveOldChildren = oldChildrenTop <= oldChildrenBottom;
Map oldKeyedChildren;
if (haveOldChildren) {
oldKeyedChildren = {};
while (oldChildrenTop <= oldChildrenBottom) {
final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
if (oldChild != null) {
if (oldChild.widget.key != null)
oldKeyedChildren[oldChild.widget.key] = oldChild;
else
deactivateChild(oldChild);
}
oldChildrenTop += 1;
}
}

// 根据Widget的Key更新oldKeyChildren中的Element。
// Update the middle of the list.
while (newChildrenTop <= newChildrenBottom) {
Element oldChild;
final Widget newWidget = newWidgets[newChildrenTop];
if (haveOldChildren) {
final Key key = newWidget.key;
if (key != null) {
oldChild = oldKeyedChildren[key];
if (oldChild != null) {
if (Widget.canUpdate(oldChild.widget, newWidget)) {
// we found a match!
// remove it from oldKeyedChildren so we don't unsync it later
oldKeyedChildren.remove(key);
} else {
// Not a match, let's pretend we didn't see it for now.
oldChild = null;
}
}
}
}

final Element newChild = updateChild(oldChild, newWidget, IndexedSlot(newChildrenTop, previousChild));
newChildren[newChildrenTop] = newChild;
previousChild = newChild;
newChildrenTop += 1;
}

newChildrenBottom = newWidgets.length - 1;
oldChildrenBottom = oldChildren.length - 1;

// 从下到上更新底部的Element。.
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
final Element oldChild = oldChildren[oldChildrenTop];
final Widget newWidget = newWidgets[newChildrenTop];
final Element newChild = updateChild(oldChild, newWidget, IndexedSlot(newChildrenTop, previousChild));
newChildren[newChildrenTop] = newChild;
previousChild = newChild;
newChildrenTop += 1;
oldChildrenTop += 1;
}

// 清除旧子Element列表中其他所有剩余Element
// Clean up any of the remaining middle nodes from the old list.
if (haveOldChildren && oldKeyedChildren.isNotEmpty) {
for (final Element oldChild in oldKeyedChildren.values) {
if (forgottenChildren == null || !forgottenChildren.contains(oldChild))
deactivateChild(oldChild);
}
}

return newChildren;
}
复制代码

该函数的主要职责如下:



  1. 复用能复用的子节点,并调用updateChild对子节点进行更新。

  2. 对不能更新的子节点,调用deactivateChild对该子节点进行失效。


其步骤如下:



  1. 从顶部向下更新子Element。

  2. 从底部向上扫描子Element。

  3. 扫描旧的子Element列表里面中间的子Element,保存Widget有Key的Element到oldKeyChildren,其他的失效。

  4. 对于新的子Element列表,如果其对应的Widget的Key和oldKeyChildren中的Key相同,更新oldKeyChildren中的Element。

  5. 从下到上更新底部的Element。

  6. 清除旧子Element列表中其他所有剩余Element。


文章参考于:zhuanlan.zhihu.com/p/369286610


收起阅读 »

跟我学flutter:细细品Widget(四)Widget 渲染过程 与 RenderObjectWidget

前言跟我学flutter系列:跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制跟我...
继续阅读 »

前言

跟我学flutter系列:
跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin
跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate
跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制

跟我学flutter:在国内如何发布自己的Plugin 或者 Package

跟我学flutter:Flutter雷达图表(一)如何使用kg_charts

跟我学flutter:细细品Widget(一)Widget&Element初识

跟我学flutter:细细品Widget(二)StatelessWidget&StatefulWidget

跟我学flutter:细细品Widget(三)ProxyWidget,InheritedWidget

跟我学flutter:细细品Widget(四)Widget 渲染过程 与 RenderObjectWidget

跟我学flutter:细细品Widget(五)Element

企业级篇目:
跟我学企业级flutter项目:用bloc手把手教你搭建用户认证系统
跟我学企业级flutter项目:dio网络框架增加公共请求参数&header
跟我学企业级flutter项目:如何用dio封装一套企业级可扩展高效的网络层
跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview
跟我学企业级flutter项目:手把手教你制作一款低耦合空页面widget


StatelessWidget 和 StatefulWidget 只是用来组装控件的容器,并不负责组件最后的布局和绘制。在 Flutter 中,布局和绘制工作实际上是在 Widget 的另一个子类 RenderObjectWidget 内完成的。
RenderObjectWidget为RenderObjectElement提供配置信息。
RenderObjectElement包装了RenderObject,RenderObject为应用程序提供真正的渲染。


源码


abstract class RenderObjectWidget extends Widget {

const RenderObjectWidget({ Key? key }) : super(key: key);

@override
@factory
RenderObjectElement createElement();

@protected
@factory
RenderObject createRenderObject(BuildContext context);

@protected
void updateRenderObject(BuildContext context, covariant RenderObject renderObject) { }

@protected
void didUnmountRenderObject(covariant RenderObject renderObject) { }
}


  • createElement 需要返回一个继承RenderObjectElement的类

  • createRenderObject 创建 Render Widget 对应的 Render Object,同样子类需要重写该方法。该方法在对应的 Element 被挂载到树上时调用(Element.mount),即在 Element 挂载过程中同步构建了「Render Tree」

  • updateRenderObject 在 Widget 更新后,修改对应的 Render Object。该方法在首次 build 以及需要更新 Widget 时都会调用;

  • didUnmountRenderObject 「Render Object」从「Render Tree」上移除时调用该方法。


RenderObjectElement 源码


abstract class RenderObjectElement extends Element {
RenderObject _renderObject;

@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_renderObject = widget.createRenderObject(this);
attachRenderObject(newSlot);
_dirty = false;
}

@override
void update(covariant RenderObjectWidget newWidget) {
super.update(newWidget);
widget.updateRenderObject(this, renderObject);
_dirty = false;
}
...
}


  • mount: RenderObject 对象的创建,以及与渲染树的插入工作,插入到渲染树后的 Element 就可以显示到屏幕中了。

  • update: 如果 Widget 的配置数据发生了改变,那么持有该 Widget 的 Element 节点也会被标记为 dirty。在下一个周期的绘制时,Flutter 就会触发 Element 树的更新,并使用最新的 Widget 数据更新自身以及关联的 RenderObject 对象,接下来便会进入 Layout 和 Paint 的流程。而真正的绘制和布局过程,则完全交由 RenderObject 完成。



RenderObject 主要处理一些固定的操作,如:布局、绘制和 Hit testing。 与ComponentElement一样RenderObjectElement也是抽象类,不同的是ComponentElement不会直接创建RenderObject,而是间接通过创建其他Element创建RenderObject。



RenderObjectElement主要有三个系统的子类,分别处理renderObject作为child时的不同情况。



  1. LeafRenderObjectElement:叶子渲染对象对应的元素,处理没有children的renderObject。

  2. SingleChildRenderObjectElement:处理只有单个child的renderObject。

  3. MultiChildRenderObjectElement: 处理有多个children的渲染对象



有时RenderObject的child模型更复杂一些,比如多维数组的形式,则可能需要基于RenderObjectElement实现一个新的子类。



RenderObjectElement 充当widget与renderObject之间的中介者。需要进行方法覆盖,以便它们返回元素期望的特定类型,例如:


class FooElement extends RenderObjectElement {                                       

@override
Foo get widget => super.widget;

@override
RenderFoo get renderObject => super.renderObject;

}

widget返回Foo,renderObject 返回RenderFoo


系统常用组件与RenderObjectElement:



























常用组件Widget(父级)Element
Flex/Wrap/Flow/StackMultiChildRenderObjectWidgetMultiChildRenderObjectElement
RawImage(Imaget)/ErrorWidgetLeafRenderObjectWidgetLeafRenderObjectElement
Offstage/SizedBox/Align/PaddingSingleChildRenderObjectWidgetSingleChildRenderObjectElement

RenderObject源码


abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget {
...
void layout(Constraints constraints, { bool parentUsesSize = false }) {...}

void paint(PaintingContext context, Offset offset) { }
}

布局和绘制完成后,接下来的事情就交给 Skia 了。在 VSync 信号同步时直接从渲染树合成 Bitmap,然后提交给 GPU。


文章参考:http://www.jianshu.com/p/c3de443a7…

收起阅读 »

跟我学flutter:细细品Widget(三)ProxyWidget,InheritedWidget

前言跟我学flutter系列:跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制跟我...
继续阅读 »

前言

跟我学flutter系列:
跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin
跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate
跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制

跟我学flutter:在国内如何发布自己的Plugin 或者 Package

跟我学flutter:Flutter雷达图表(一)如何使用kg_charts

跟我学flutter:细细品Widget(一)Widget&Element初识

跟我学flutter:细细品Widget(二)StatelessWidget&StatefulWidget

跟我学flutter:细细品Widget(三)ProxyWidget,InheritedWidget

跟我学flutter:细细品Widget(四)Widget 渲染过程 与 RenderObjectWidget

跟我学flutter:细细品Widget(五)Element

企业级篇目:
跟我学企业级flutter项目:用bloc手把手教你搭建用户认证系统
跟我学企业级flutter项目:dio网络框架增加公共请求参数&header
跟我学企业级flutter项目:如何用dio封装一套企业级可扩展高效的网络层
跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview
跟我学企业级flutter项目:手把手教你制作一款低耦合空页面widget


ProxyWidget作为抽象基类本身没有任何功能,但他有两个实现类ParentDataWidget & InheritedElement


源码


abstract class ProxyWidget extends Widget {

const ProxyWidget({ Key? key, required this.child }) : super(key: key);

final Widget child;
}

InheritedWidget


InheritedWidget 用于在树上向下传递数据。


通过BuildContext.dependOnInheritedWidgetOfExactType可以获取最近的「Inherited Widget」,需要注意的是通过这种方式获取「Inherited Widget」时,当「Inherited Widget」状态有变化时,会导致该引用方 rebuild。


通常,为了使用方便会「Inherited Widget」会提供静态方法of,在该方法中调用BuildContext.dependOnInheritedWidgetOfExactType。of方法可以直接返回「Inherited Widget」,也可以是具体的数据。


有时,「Inherited Widget」是作为另一个类的实现细节而存在的,其本身是私有的(外部不可见),此时of方法就会放到对外公开的类上。最典型的例子就是Theme,其本身是StatelessWidget类型,但其内部创建了一个「Inherited Widget」:_InheritedTheme,of方法就定义在上Theme上:


  static ThemeData of(BuildContext context) {
final _InheritedTheme? inheritedTheme = context.dependOnInheritedWidgetOfExactType<_InheritedTheme>();
final MaterialLocalizations? localizations = Localizations.of(context, MaterialLocalizations);
final ScriptCategory category = localizations?.scriptCategory ?? ScriptCategory.englishLike;
final ThemeData theme = inheritedTheme?.theme.data ?? _kFallbackTheme;
return ThemeData.localize(theme, theme.typography.geometryThemeFor(category));
}

该of方法返回的是ThemeData类型的具体数据,并在其内部首先调用了BuildContext.dependOnInheritedWidgetOfExactType。


我们经常使用的「Inherited Widget」莫过于MediaQuery,同样提供了of方法:


  static MediaQueryData of(BuildContext context) {
assert(context != null);
assert(debugCheckHasMediaQuery(context));
return context.dependOnInheritedWidgetOfExactType()!.data;
}

在这里插入图片描述


源码


abstract class InheritedWidget extends ProxyWidget {

const InheritedWidget({ Key? key, required Widget child })
:
super(key: key, child: child)
;

@override
InheritedElement createElement() => InheritedElement(this);

@protected
bool updateShouldNotify(covariant InheritedWidget oldWidget);
}

createElement


「Inherited Widget」对应的 Element 为InheritedElement,一般情况下InheritedElement子类不用重写该方法;


updateShouldNotify


「Inherited Widget」rebuilt 时判断是否需要 rebuilt 那些依赖它的 Widget;


如下是MediaQuery.updateShouldNotify的实现,在新老Widget.data 不相等时才 rebuilt 那依赖的 Widget。


  @override
bool updateShouldNotify(MediaQuery oldWidget)
=> data != oldWidget.data;


依赖了 InheritedWidget 在数据变动的情况下 didChangeDependencies 会被调用,
依赖的意思是 使用 return context.dependOnInheritedWidgetOfExactType()
如果使用context.getElementForInheritedWidgetOfExactType().widget的话,只会用其中的数据,而不会重新rebuild



@override
InheritedElement getElementForInheritedWidgetOfExactType() {
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
return ancestor;
}
@override
InheritedWidget dependOnInheritedWidgetOfExactType({ Object aspect }) {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
//多出的部分
if (ancestor != null) {
return dependOnInheritedElement(ancestor, aspect: aspect) as T;
}
_hadUnsatisfiedDependencies = true;
return null;
}

我们可以看到,dependOnInheritedWidgetOfExactType() 比 getElementForInheritedWidgetOfExactType()多调了dependOnInheritedElement方法,dependOnInheritedElement源码如下:


  @override
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }) {
assert(ancestor != null);
_dependencies ??= HashSet();
_dependencies.add(ancestor);
ancestor.updateDependencies(this, aspect);
return ancestor.widget;
}

可以看到dependOnInheritedElement方法中主要是注册了依赖关系!看到这里也就清晰了,调用dependOnInheritedWidgetOfExactType() 和 getElementForInheritedWidgetOfExactType()的区别就是前者会注册依赖关系,而后者不会,所以在调用dependOnInheritedWidgetOfExactType()时,InheritedWidget和依赖它的子孙组件关系便完成了注册,之后当InheritedWidget发生变化时,就会更新依赖它的子孙组件,也就是会调这些子孙组件的didChangeDependencies()方法和build()方法。而当调用的是 getElementForInheritedWidgetOfExactType()时,由于没有注册依赖关系,所以之后当InheritedWidget发生变化时,就不会更新相应的子孙Widget。


文章内容参考:http://www.jb51.net/article/221…



收起阅读 »

什么是请求参数、表单参数、url参数、header参数、Cookie参数?一文讲懂

最近在工作中对 http 的请求参数解析有了进一步的认识,写个小短文记录一下。回顾下自己的情况,大概就是:有点点网络及编程基础,只需要加深一点点对 HTTP 协议的理解就能弄明白了。先分享一个小故事:我至今仍清晰地记得大三实习时的第一个工作任务,我需要调用其他...
继续阅读 »

最近在工作中对 http 的请求参数解析有了进一步的认识,写个小短文记录一下。

回顾下自己的情况,大概就是:有点点网络及编程基础,只需要加深一点点对 HTTP 协议的理解就能弄明白了。

先分享一个小故事:我至今仍清晰地记得大三实习时的第一个工作任务,我需要调用其他部门提供的 api 去完成某项业务。

那个 api 文档只告诉了我请求参数需要传什么,没有提及用什么方式传,比如这样:


其实如果有经验的话,直接在请求体或 url 里填参数试一下就知道了;另一个是新人有时候不太敢问问题,其实只要向同事确认一下就好的。

然而由于当时我掌握的编程知识有限,只会用表单提交数据。所以当我下载完同事安利的 api 调用调试工具 postman 后,我就在网上查怎么用 postman 发送表单数据,结果折腾了好久 api 还是没能调通。

当天晚上我向老同学求助,他问我上课是不是又睡过去了?

我说你怎么知道?

他说当然咯,你上课睡觉不学习又不是一天两天的事情......

后来他告诉我得好好学一下 http 协议,看看可以在协议的哪些位置放请求参数。

一个简单的 http 服务器还原

那么,在正式讲解之前,我们先简单搭建一个 http 服务器,阿菌沿用经典的 python 版云你好服务器进行讲解。

云你好服务器的代码很简单,服务器首先会获取 name 用户名这个参数,如果用户传了这个参数,就返回 Hello xxx,xxx 指的是 name 用户名;如果用户没有传这个参数则返回 Hello World

# 云你好服务源码
from flask import Flask
from flask import request

app = Flask(__name__)

# 云你好服务 API 接口
@app.get("/api/hello")
def hello():
   # 看用户是否传递了参数 name
   name = request.args.get("name", "")
   # 如果传了参数就向目标对象打招呼,输出 Hello XXX,否则输出 Hello World
   return f"Hello {name}" if name else "Hello World"

# 启动云你好服务
if __name__ == '__main__':
   app.run()

为了快速开发(大伙可以下载一个 python 把这个代码跑一下,用自己的语言实现一个类似的服务器也是可以的),阿菌这里使用了 flask 框架构建后端服务。

在具体获取参数的时候,我选择了在 request.args 中获取参数。这里提前剧透一下:在 flask 框架中,request.args 指的是从 url 中获取参数(不过这是我们后面讲解的内容,大家有个印象就好)

抓包查看 http 报文

有了 http 服务器后,我们开始深入讲解 http 协议,em...个人觉得只在学校上课看教材学计算机网络好像还欠缺了点啥,比较推荐大家下载一个像 Wireshark 这样的网络抓包软件,动手拆解网络包,深入学习各种网络协议。抓取网络包的示例视频

为了搞清楚什么是请求参数、表单参数、url 参数、Header 参数、Cookie 参数,我们先发一个 http 请求,然后抓取这个请求的网络包,看看一份 http 报文会携带哪些信息。

呼应开头,用户阿菌是个只会发表单数据的萌新,他使用 postman 向云你好 api 发送了一个 post 请求:


剧情发展正常,我们没能得到 Hello 阿菌(服务器会到 url 中获取参数,咱们用表单形式提交,所以获取不到)

由于咱们对请求体这个概念比较模糊,接下来我们重新发一个一模一样的请求,并且通过 Wireshark 抓包看一下:


可以看到强大的 Wireshark 帮助我们把请求抓取了下来,并把整个网络包的链路层协议,IP层协议,传输层协议,应用层协议全都解析好了。

由于咱们小码农一般都忙于解决应用层问题,所以我们把目光聚焦于高亮的 Hypertext Transfer Protocol 超文本传输协议,也就是大名鼎鼎的 HTTP 协议。

首先我们查看一下 HTTP 报文的完整内容:


可以看到,http 协议大概是这么组成的:

  • 第一行是请求的方式,比如 GET / POST / DELETE / PUT

  • 请求方式后面跟的是请求的路径,一般把这个叫 URI(统一资源标识符)

补充:URL 是统一资源定位符,见名知义,因为要定位,所以要指定协议甚至是位置,比如这样:http://localhost:5000/api/hello

  • 请求路径后面跟的是 HTTP 的版本,比如这里是 HTTP/1.1

完整的第一行如下:

POST /api/hello HTTP/1.1

第二行的 User-Agent 则用于告诉对方发起请求的客户端是啥,比如咱们用 Postman 发起的请求,Postman 就会自动把这个参数设置为它自己:

User-Agent: PostmanRuntime/7.28.4

第三行的 Accept 用于告诉对方我们希望收到什么类型的数据,这里默认是能接受所有类型的数据:

Accept: */*

第四行就非常值得留意,Postman-Token 是 Postman 自己传的参数,这个我们放到下面讲!

Postman-Token: ddd72e1a-0d63-4bad-a18e-22e38a5de3fc

第五行是请求的主机,网络上的一个服务一般用 ip 加端口作为唯一标识:

Host: 127.0.0.1:5000

第六行指定的是咱们请求发起方可以理解的压缩方式:

Accept-Encoding: gzip, deflate, br

第七行告诉对方处理完当前请求后不要关闭连接:

Connection: keep-alive

第八行告诉对方咱们请求体的内容格式,这个是本文的侧重点啦!比如我们这里指定的是一般浏览器的原生表单格式:

Content-Type: application/x-www-form-urlencoded

好了,下面大家要留意了,第九行的 Content-Length 给出的是请求体的大小。

而请求体,会放在紧跟着的一个空行之后。比如本请求的请求体内容是以 key=value 形式填充的,也就是我们表单参数的内容了:

Content-Length: 23

name=%E9%98%BF%E8%8F%8C

看到这里我们先简单小结一下,想要告诉服务器我们发送的是表单数据,一共需要两步:

  1. Content-Type 设置为 application/x-www-form-urlencoded

  2. 在请求体中按照 key=value 的形式填写请求参数

什么是协议?进一步了解 http

好了,接下来我们进一步讲解,大家试想一下,网络应用,其实就是端到端的交互,最常见的就是服务端和客户端交互模型:客户端发一些参数数据给服务端,通过这些参数数据告诉服务端它想得到什么或想干什么,服务端根据客户端传递的参数数据作出处理。

传输层协议通过 ip 和端口号帮我们定位到了具体的服务应用,具体怎么交互是由我们程序员自己定义的。

大概在 30 年前,英国计算机科学家蒂姆·伯纳斯-李定义了原始超级文本传输协议(HTTP),后续我们的 web 应用大都延续采用了他定义的这套标准,当然这套标准也在不断地进行迭代。

许多文献资料会把 http 协议描述得比较晦涩,加上协议这个词听起来有点高大上,初学者入门学习的时候往往感觉不太友好。

其实协议说白了就是一种格式,就好比我们写书信,约定要先顶格写个敬爱的 xxx,然后写个你好,然后换一个段落再写正文,可能最后还得加上日期署名等等。

我们只要按照格式写信,老师就能一眼看出来我们在写信;只要我们按协议格式发请求数据,服务器就能一眼看出来我们想要得到什么或想干什么。

当然,老师是因为老早就学过书信格式,所以他才能看懂书信格式;服务端程序也一样,我们要预先编写好 http 协议的解析逻辑,然后我们的服务器才能根据解析逻辑去获取一个 http 请求中的各种东西。

当然这个解析 http 协议的逻辑不是谁都能写出来的,就算能写出来,也未必写得好,所以我们会使用厉害的人封装好的脚手架,比如 java 里的 spring 全套、Go 语言里的 Gin 等等。

回到我们开头给出的示例:

from flask import Flask
from flask import request

app = Flask(__name__)

# 云你好服务 API 接口
@app.get("/api/hello")
def hello():
   # 看用户是否传递了参数 name
   name = request.args.get("name", "")
   # 如果传了参数就向目标对象打招呼,输出 Hello XXX,否则输出 Hello World
   return f"Hello {name}" if name else "Hello World"

# 启动云你好服务
if __name__ == '__main__':
   app.run()

阿菌的示例使用了 python 里的 flask 框架,在处理逻辑中使用了 request.args 获取请求参数,而 args 封装的就是框架从 url 中获取参数的逻辑。比如我们发送请求的 url 为:

http://127.0.0.1:5000/api/hello?name=ajun

框架会帮助我们从 url 中的 ? 后面开始截取,然后把 name=ajun 这些参数存放到 args 里。

切换一下,假设我们是云你好服务提供者,我们希望用户通过表单参数的形式使用云你好服务,我们只要把获取 name 参数的方式改成从表单参数里获取就可以了,flask 在 request.form 里封装了表单参数(关于框架是怎么在数行 http 请求中封装参数的,大家可以看自己使用的框架的具体逻辑,估计区别不大,只是存在一些语言特性上的差异):

@app.post("/api/hello")
def hello():
   # 看用户是否传递了参数 name
   name = request.form.get("name", "")
   # 如果传了参数就向目标对象打招呼,输出 Hello XXX,否则输出 Hello World
   return f"Hello {name}" if name else "Hello World"

思考:我们可以在 http 协议中传递什么参数?

最后,我们解释本文的标题,其实想要明白各种参数之间的区别,我们可以换一个角度思考:

咱们可以在一份 http 报文的哪些位置传递参数?

接下来回顾一下一个 http 请求的内容:

POST /api/hello HTTP/1.1
User-Agent: PostmanRuntime/7.28.4
Accept: */*
Postman-Token: fbf75035-a647-46dc-adc0-333751a9399e
Host: 127.0.0.1:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 23

name=%E9%98%BF%E8%8F%8C

大家看,咱们的 http 报文,也就是基于传输层之上的应用层报文,大概就长上面这样。

我们考虑两种情况,第一种情况,我们基于别人已经开发好的脚手架开发 http 服务器。

由于框架会基于 http 协议进行解析,所以框架会帮助我们解析好请求 url,各种 Header 头(比如:Cookie 等),以及具体的响应内容都帮我们封装解析好了(比如按照 key=value 的方式去读取请求体)。

那当我们开发服务端的时候,就可以指定从 url、header、响应体中获取参数了,比如:

  • url 参数:指的就是 url 中 ? 后面携带的 key value 形式参数

  • header 参数:指的就是各个 header 头,我们甚至可以自定义 header,比如 Postman-Token 就是 postman 这个软件自己携带的,我们服务端如果需要的话是可以指定获取这个参数的

  • Cookie 参数:其实就是名字为 Cookie 的请求头

  • 表单参数:指的就是 Content-Type 为 application/x-www-form-urlencoded 下请求体的内容,如果我们的表单需要传文件,还会有其他的 Content-Type

  • json 参数:指的就是 Content-Type 为 application/json 下请求体的内容(当然服务端可以不根据 Content-Type 直接解析请求体,但按照协议的规范工程项目或许会更好维护)

综上所述,请求参数就是对上面各种类型的参数的一个总称了。

大家会发现,不管什么 url 参数、header 参数、Cookie 参数、表单参数,其实就是换着法儿,按照一定的格式把数据放到应用层报文中。关键在于我们的服务端程序和客户端程序按照一种什么样的约定去传递和获取这些参数。这就是协议吧~

还有另一种情况,当然这只是开玩笑了,比如以后哪位大佬或者哪家企业定义了一种新的数据传输标准,推广至全球,比如叫 hppt 协议,这样是完全可以自己给各种形式参数下定义取名字的。这可能就是为啥我们说一流的企业、大佬制定标准,接下来的围绕标准研发技术,进而是基于技术卖产品,最后是围绕产品提供服务了。

一旦标准制定了,整个行业都围绕这个标准转了,而且感觉影响会越来越深远......

讲解参考链接

作者:胡涂阿菌
来源:juejin.cn/post/7100400494081736711

收起阅读 »

Base64编码解码原理

Base64编码与解码原理涉及的算法1、短除法短除法运算方法是先用一个除数除以能被它除尽的一个质数,以此类推,除到商是质数为止。通过短除法,十进制数可以不断除以2得到多个余数。最后,将余数从下到上进行排列组合,得到二进制数。实例:以字符n对应的ascII编码1...
继续阅读 »

Base64编码与解码

原理涉及的算法

1、短除法

短除法运算方法是先用一个除数除以能被它除尽的一个质数,以此类推,除到商是质数为止。通过短除法,十进制数可以不断除以2得到多个余数。最后,将余数从下到上进行排列组合,得到二进制数。

实例:以字符n对应的ascII编码110为例。

110 / 2  = 55...0
55 / 2 = 27...1
27 / 2 = 13...1
13 / 2 = 6...1
6   / 2 = 3...0
3   / 2 = 1...1
1   / 2 = 0...1

将余数从下到上进行排列组合,得到字符n对应的ascII编码110转二进制为1101110,因为一字节对应8位(bit), 所以需要向前补0补足8位,得到01101110。其余字符同理可得。

2、按权展开求和

按权展开求和, 8位二进制数从右到左,次数是0到7依次递增, 基数*底数次数,从左到右依次累加,相加结果为对应十进制数。我们已二进制数01101110转10进制为例:

(01101110)2=0∗20+1∗21+1∗22+1∗23+0∗24+1∗25+1∗26+0∗27(01101110)_2 = 0 * 2^0 + 1 * 2 ^ 1 + 1 * 2^2 + 1 * 2^3 + 0 * 2^4 + 1 * 2^5 + 1 * 2^6 + 0 * 2^7(01101110)2=0∗20+1∗21+1∗22+1∗23+0∗24+1∗25+1∗26+0∗27

3、位概念

二进制数系统中,每个0或1就是一个位(bit,比特),也叫存储单元,位是数据存储的最小单位。其中 8bit 就称为一个字节(Byte)。

4、移位运算符

移位运算符在程序设计中,是位操作运算符的一种。移位运算符可以在二进制的基础上对数字进行平移。按照平移的方向和填充数字的规则分为三种:<<(左移)、>>(带符号右移)和>>>(无符号右移)。在base64的编码和解码过程中操作的是正数,所以仅使用<<(左移)、>>(带符号右移)两种运算符。

  1. 左移运算:是将一个二进制位的操作数按指定移动的位数向左移动,移出位被丢弃,右边移出的空位一律补0。【左移相当于一个数乘以2的次方】

  2. 右移运算:是将一个二进制位的操作数按指定移动的位数向右移动,移出位被丢弃,左边移出的空位一律补0,或者补符号位,这由不同的机器而定。在使用补码作为机器数的机器中,正数的符号位为0,负数的符号位为1。【右移相当于一个数除以2的次方】

// 左移
01101000 << 2 -> 101000(左侧移出位被丢弃) -> 10100000(右侧空位一律补0)
// 右移
01101000 >> 2 -> 011010(右侧移出位被丢弃) -> 00011010(左侧空位一律补0)

5、与运算、或运算

与运算、或运算都是计算机中一种基本的逻辑运算方式。

  1. 与运算:符号表示为&。运算规则:两位同时为“1”,结果才为“1”,否则为0

  2. 或运算:符号表示为|。运算规则:两位只要有一位为“1”,结果就为“1”,否则为0

什么是base64编码

2^6=64\

\

Base64编码是将字符串以每3个8比特(bit)的字节子序列拆分成4个6比特(bit)的字节(6比特有效字节,最左边两个永远为0,其实也是8比特的字节)子序列,再将得到的子序列查找Base64的编码索引表,得到对应的字符拼接成新的字符串的一种编码方式。

每3个8比特(bit)的字节子序列拆分成4个6比特(bit)的字节的拆分过程如下图所示:


为什么base64编码后的大小是原来的4/3倍

因为6和8的最大公倍数是24,所以3个8比特的字节刚好可以拆分成4个6比特的字节,3 x 8 = 6 x 4。计算机中,因为一个字节需要8个存储单元存储,所以我们要把6个比特往前面补两位0,补足8个比特。如下图所示:


补足后所需的存储单元为32个,是原来所需的24个的4/3倍。这也就是base64编码后的大小是原来的4/3倍的原因。

为什么命名为base64呢?

因为6位(bit)的二进制数有2的6次方个,也就是二进制数(00000000-00111111)之间的代表0-63的64个二进制数。

不是说一个字节是用8位二进制表示的吗,为什么不是2的8次方?

因为我们得到的8位二进制数的前两位永远是0,真正的有效位只有6位,所以我们所能够得到的二进制数只有2的6次方个。

Base64字符是哪64个?

Base64的编码索引表,字符选用了"A-Z、a-z、0-9、+、/" 64个可打印字符来代表(00000000-00111111)这64个二进制数。即

let base64EncodeChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'

编码原理

要把3个字节拆分成4个字节可以怎么做?

流程图


思路

分析映射关系:abc → xyzi。我们从高位到低位添加索引来分析这个过程

  • x: (前面补两个0)a的前六位 => 00a7a6a5a4a3a2

  • y: (前面补两个0)a的后两位 + b的前四位 => 00a1a0b7b6b5b4

  • z: (前面补两个0)b的后四位 + c的前两位 => 00b3b2b1b0c7c6

  • i: (前面补两个0)c的后六位 => 00c5c4c3c2c1c0

通过上述的映射关系,得到实现思路:

  1. 将字符对应的AscII编码转为8位二进制数

  2. 将每三个8位二进制数进行以下操作

    • 将第一个数右移位2位,得到第一个6位有效位二进制数

    • 将第一个数 & 0x3之后左移位4位,得到第二个6位有效位二进制数的第一个和第二个有效位,将第二个数 & 0xf0之后右移位4位,得到第二个6位有效位二进制数的后四位有效位,两者取且得到第二个6位有效位二进制

    • 将第二个数 & 0xf之后左移位2位,得到第三个6位有效位二进制数的前四位有效位,将第三个数 & 0xC0之后右移位6位,得到第三个6位有效位二进制数的后两位有效位,两者取且得到第三个6位有效位二进制

    • 将第三个数 & 0x3f,得到第四个6位有效位二进制数

  3. 将获得的6位有效位二进制数转十进制,查找对呀base64字符

代码实现

以hao字符串为例,观察base64编码的过程,将上面转换通过代码逻辑分析实现

// 输入字符串
let str = 'hao'
// base64字符串
let base64EncodeChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
// 定义输入、输出字节的二进制数
let char1, char2, char3, out1, out2, out3, out4, out
// 将字符对应的ascII编码转为8位二进制数
char1 = str.charCodeAt(0) & 0xff // 104 01101000
char2 = str.charCodeAt(1) & 0xff // 97 01100001
char3 = str.charCodeAt(2) & 0xff // 111 01101111
// 输出6位有效字节二进制数
out1 = char1 >> 2 // 26 011010
out2 = (char1 & 0x3) << 4 | (char2 & 0xf0) >> 4 // 6 000110
out3 = (char2 & 0xf) << 2 | (char3 & 0xc0) >> 6 // 5 000101
out4 = char3 & 0x3f // 47 101111

out = base64EncodeChars[out1] + base64EncodeChars[out2] + base64EncodeChars[out3] + base64EncodeChars[out4] // aGFv

算法剖析

  1. out1: char1 >> 2

    01101000 -> 00011010
    复制代码
  2. out2 = (char1 & 0x3) << 4 | (char2 & 0xf0) >> 4

    // 且运算
    01101000       01100001
    00000011       11110000
    --------       --------
    00000000       01100000

    // 移位运算后得
    00000000       00000110

    // 或运算
    00000000
    00000110
    --------
    00000110
    复制代码

第三个字符第四个字符同理

整理上述代码,扩展至多字符字符串

// 输入字符串
let str = 'haohaohao'
// base64字符串
let base64EncodeChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'

// 获取字符串长度
let len = str.length
// 当前字符索引
let index = 0
// 输出字符串
let out = ''
while(index < len) {
  // 定义输入、输出字节的二进制数
  let char1, char2, char3, out1, out2, out3, out4
  // 将字符对应的ascII编码转为8位二进制数
  char1 = str.charCodeAt(index++) & 0xff // 104 01101000
  char2 = str.charCodeAt(index++) & 0xff // 97 01100001
  char3 = str.charCodeAt(index++) & 0xff // 111 01101111
  // 输出6位有效字节二进制数
  out1 = char1 >> 2 // 26 011010
  out2 = (char1 & 0x3) << 4 | (char2 & 0xf0) >> 4 // 6 000110
  out3 = (char2 & 0xf) << 2 | (char3 & 0xc0) >> 6 // 5 000101
  out4 = char3 & 0x3f // 47 101111

  out = out + base64EncodeChars[out1] + base64EncodeChars[out2] + base64EncodeChars[out3] + base64EncodeChars[out4] // aGFv
}

原字符串长度不是3的整倍数的情况,需要特殊处理

    ...
  char1 = str.charCodeAt(index++) & 0xff // 104 01101000
  if (index == len) {
      out2 = (char1 & 0x3) << 4
      out = out + base64EncodeChars[out1] + base64EncodeChars[out2] + '=='
      return out
  }
  char2 = str.charCodeAt(index++) & 0xff // 97 01100001
  if (index == len) {
      out1 = char1 >> 2 // 26 011010
      out2 = (char1 & 0x3) << 4 | (char2 & 0xf0) >> 4 // 6 000110
      out3 = (char2 & 0xf) << 2
      out = out + base64EncodeChars[out1] + base64EncodeChars[out2] + base64EncodeChars[out3] + '='
      return out
  }
  ...

全部代码

function base64Encode(str) {
// base64字符串
let base64EncodeChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'

// 获取字符串长度
let len = str.length
// 当前字符索引
let index = 0
// 输出字符串
let out = ''
while(index < len) {
// 定义输入、输出字节的二进制数
let char1, char2, char3, out1, out2, out3, out4
// 将字符对应的ascII编码转为8位二进制数
char1 = str.charCodeAt(index++) & 0xff
out1 = char1 >> 2
if (index == len) {
out2 = (char1 & 0x3) << 4
out = out + base64EncodeChars[out1] + base64EncodeChars[out2] + '=='
return out
}
char2 = str.charCodeAt(index++) & 0xff
out2 = (char1 & 0x3) << 4 | (char2 & 0xf0) >> 4
if (index == len) {
out3 = (char2 & 0xf) << 2
out = out + base64EncodeChars[out1] + base64EncodeChars[out2] + base64EncodeChars[out3] + '='
return out
}
char3 = str.charCodeAt(index++) & 0xff
// 输出6位有效字节二进制数
out3 = (char2 & 0xf) << 2 | (char3 & 0xc0) >> 6
out4 = char3 & 0x3f

out = out + base64EncodeChars[out1] + base64EncodeChars[out2] + base64EncodeChars[out3] + base64EncodeChars[out4]
}
return out
}
base64Encode('haohao') // aGFvaGFv
base64Encode('haoha') // aGFvaGE=
base64Encode('haoh') // aGFvaA==

解码原理

逆向推导,由每4个6位有效位的二进制数合并成3个8位二进制数,根据ascII编码映射到对应字符后拼接字符串

思路

分析映射关系 xyzi -> abc

  • a: x后六位 + y第三、四位 => x5x4x3x2x1x0y5y4

  • b: y后四位 + z第三、四、五、六位 => y3y2y1y0z5z4z3z2

  • c: z后两位 + i后六位 => z1z0i5i4i3i2i1i0

  1. 将字符对应的base64字符集的索引转为6位有效位二进制数

  2. 将每四个6位有效位二进制数进行以下操作

    1. 第一个二进制数左移位2位,得到新二进制数的前6位,第二个二进制数 & 0x30之后右移位4位,取或集得到第一个新二进制数

    2. 第二个二进制数 & 0xf之后左移位4位,第三个二进制数 & 0x3c之后右移位2位,取或集得到第二个新二进制数

    3. 第二个二进制数 & 0x3之后左移位6位,与第四个二进制数取或集得到第二个新二进制数

  3. 根据ascII编码映射到对应字符后拼接字符串

代码实现

// base64字符串
let str = 'aGFv'
// base64字符集
let base64CharsArr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('')
// 获取索引值
let char1 = base64CharsArr.findIndex(char => char==str[0]) & 0xff // 26 011010
let char2 = base64CharsArr.findIndex(char => char==str[1]) & 0xff // 6 000110
let char3 = base64CharsArr.findIndex(char => char==str[2]) & 0xff // 5 000101
let char4 = base64CharsArr.findIndex(char => char==str[3]) & 0xff // 47 101111
let out1, out2, out3, out
// 位运算
out1 = char1 << 2 | (char2 & 0x30) >> 4
out2 = (char2 & 0xf) << 4 | (char3 & 0x3c) >> 2
out3 = (char3 & 0x3) << 6 | char4
console.log(out1, out2, out3)
out = String.fromCharCode(out1) + String.fromCharCode(out2) + String.fromCharCode(out3)

遇到有用'='补过位的情况时

function base64decode(str) {
// base64字符集
let base64CharsArr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('')
let char1 = base64CharsArr.findIndex(char => char==str[0])
let char2 = base64CharsArr.findIndex(char => char==str[1])
let out1, out2, out3, out
if (char1 == -1 || char2 == -1) return out
char1 = char1 & 0xff
char2 = char2 & 0xff
let char3 = base64CharsArr.findIndex(char => char==str[2])
// 第三位不在base64对照表中时,只拼接第一个字符串
if (char3 == -1) {
out1 = char1 << 2 | (char2 & 0x30) >> 4
out = String.fromCharCode(out1)
return out
}
let char4 = base64CharsArr.findIndex(char => char==str[3])
// 第三位不在base64对照表中时,只拼接第一个和第二个字符串
if (char4 == -1) {
out1 = char1 << 2 | (char2 & 0x30) >> 4
out2 = (char2 & 0xf) << 4 | (char3 & 0x3c) >> 2
out = String.fromCharCode(out1) + String.fromCharCode(out2)
return out
}
// 位运算
out1 = char1 << 2 | (char2 & 0x30) >> 4
out2 = (char2 & 0xf) << 4 | (char3 & 0x3c) >> 2
out3 = (char3 & 0x3) << 6 | char4
console.log(out1, out2, out3)
out = String.fromCharCode(out1) + String.fromCharCode(out2) + String.fromCharCode(out3)
return out
}

解码整个字符串,整理代码后

function base64decode(str) {
// base64字符集
let base64CharsArr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.split('')
let i = 0
let len = str.length
let out = ''
while(i < len) {
let char1 = base64CharsArr.findIndex(char => char==str[i])
i++
let char2 = base64CharsArr.findIndex(char => char==str[i])
i++
let out1, out2, out3
if (char1 == -1 || char2 == -1) return out
char1 = char1 & 0xff
char2 = char2 & 0xff
let char3 = base64CharsArr.findIndex(char => char==str[i])
i++
// 第三位不在base64对照表中时,只拼接第一个字符串
out1 = char1 << 2 | (char2 & 0x30) >> 4
if (char3 == -1) {
out = out + String.fromCharCode(out1)
return out
}
let char4 = base64CharsArr.findIndex(char => char==str[i])
i++
// 第三位不在base64对照表中时,只拼接第一个和第二个字符串
out2 = (char2 & 0xf) << 4 | (char3 & 0x3c) >> 2
if (char4 == -1) {
out = out + String.fromCharCode(out1) + String.fromCharCode(out2)
return out
}
// 位运算
out3 = (char3 & 0x3) << 6 | char4
console.log(out1, out2, out3)
out = out + String.fromCharCode(out1) + String.fromCharCode(out2) + String.fromCharCode(out3)
}
return out
}
base64decode('aGFvaGFv') // haohao
base64decode('aGFvaGE=') // haoha
base64decode('aGFvaA==') // haoh

上述解码核心是字符与base64字符集索引的映射,网上看到过使用AscII编码索引映射base64字符索引的方法

let base64DecodeChars = [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1]
//
let char1 = 'hao'.charCodeAt(0) // h -> 104
base64DecodeChars[char1] // 33 -> base64编码表中的h

由此可见,base64DecodeChars对照accII编码表的索引存放的是base64编码表的对应字符的索引。

jdk1.8之前的方式

Base64编码与解码时,会使用到JDK里sun.misc包套件下的BASE64Encoder类和BASE64Decoder类

sun.misc包所提供的Base64编码解码功能效率不高,因此在1.8之后的jdk版本已经被删除了

// 编码器
final BASE64Encoder encoder = new BASE64Encoder();
// 解码器
final BASE64Decoder decoder = new BASE64Decoder();
final String text = "字串文字";
final byte[] textByte = text.getBytes("UTF-8");
//编码
final String encodedText = encoder.encode(textByte);
System.out.println(encodedText);
//解码
System.out.println(new String(decoder.decodeBuffer(encodedText), "UTF-8"));

Apache Commons Codec包的方式

Apache Commons Codec 有提供Base64的编码与解码功能,会使用到 org.apache.commons.codec.binary 套件下的Base64类别,用法如下

1、引入依赖

<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-compress</artifactId>
  <version>1.21</version>
</dependency>

2、代码实现

final Base64 base64 = new Base64();
final String text = "字串文字";
final byte[] textByte = text.getBytes("UTF-8");
//编码
final String encodedText = base64.encodeToString(textByte);
System.out.println(encodedText);
//解码
System.out.println(new String(base64.decode(encodedText), "UTF-8"));

jdk1.8之后的方式

与sun.misc包和Apache Commons Codec所提供的Base64编解码器方式来比较,Java 8提供的Base64拥有更好的效能。实际测试编码与解码速度,Java 8提供的Base64,要比 sun.misc 套件提供的还要快至少11倍,比 Apache Commons Codec 提供的还要快至少3倍。

// 解码器
final Base64.Decoder decoder = Base64.getDecoder();
// 编码器
final Base64.Encoder encoder = Base64.getEncoder();
final String text = "字串文字";
final byte[] textByte = text.getBytes(StandardCharsets.UTF_8);
//编码
final String encodedText = encoder.encodeToString(textByte);
System.out.println(encodedText);
//解码
System.out.println(new String(decoder.decode(encodedText), StandardCharsets.UTF_8));

总结

Base64 是一种数据编码方式,可做简单加密使用,可以t通过改变base64编码映射顺序来形成自己独特的加密算法进行加密解密。

编码表

Base64编码表


AscII码编码表


作者:loginfo
来源:juejin.cn/post/7100421228644532255

收起阅读 »

Python-可变和不可变类型

1. 不可变类型不可变类型,内存中的数据不允许被修改(一旦被定义,内存中分配了小格子,就不能再修改内容了):数字类型int,bool,float,complex,long(2,x)字符串str元组tuple2. 可变类型可变类型,内存中的数据可以被修改(可以通...
继续阅读 »

1. 不可变类型

不可变类型,内存中的数据不允许被修改(一旦被定义,内存中分配了小格子,就不能再修改内容了):

  • 数字类型intboolfloatcomplexlong(2,x)

  • 字符串str

  • 元组tuple

2. 可变类型

可变类型,内存中的数据可以被修改(可以通过变量名调用方法来修改列表和字典内部的内容,而内存地址不发生变化):

  • 列表list

  • 字典dict(注:字典中的key只能使用不可变类型的数据)

注:给变量赋新值的时候,只是改变了变量的引用地址,不是修改之前的内容

  1. 可变类型的数据变化,是通过方法来实现的

  2. 如果给一个可变类型的变量,复制了一个新的数据,引用会修改(变量从之前的数据上撕下来,贴到新赋值的数据上)

3. 代码演示

# 新建列表
a = [1, 2, 3]
print("列表a:", a)
print("列表a的地址:", id(a))
print("*"*50)
# 追加元素
a.append(999)
print("列表a:", a)
print("列表a的地址:", id(a))
print("*"*50)
# 移除元素
a.remove(2)
print("列表a:", a)
print("列表a的地址:", id(a))
print("*"*50)
# 清空列表
a.clear()
print("列表a:", a)
print("列表a的地址:", id(a))
print("*"*50)
# 将空列表赋值给变量a
a = []
print("列表a的地址:", id(a))   # 通过输出可以看出地址发生了变化
print("*"*50)
# 新建字典
d = {"name": "xiaoming"}
print("字典d为:", d)
print("字典d的地址:", id(d))
print("*"*50)
# 追加键值对
d["age"] = 18
print("字典d为:", d)
print("字典d的地址:", id(d))
print("*"*50)
# 删除键值对
d.pop("age")
print("字典d为:", d)
print("字典d的地址:", id(d))
print("*"*50)
# 清空所有键值对
d.clear()
print("字典d为:", d)
print("字典d的地址:", id(d))
print("*"*50)
# 对d赋值空字典
d = {}
print("字典d为:", d)
print("字典d的地址:", id(d))
print("*"*50)

4. 运行结果

可变类型(列表和字典)的数据变化,是通过方法(比如append,remove,pop等)来实现的,不会改变地址。而重新赋值后地址会改变。具体运行结果如下图所示:




作者:ZacheryZHANG
来源:juejin.cn/post/7100423532655411213

收起阅读 »

推荐一款超棒的SpringCloud 脚手架项目

之前接个私活,在网上找了好久没有找到合适的框架,不是版本低没人维护了,在不就是组件相互依赖较高。所以我自己搭建一个全新spingCloud框架,里面所有组件可插拔的,集成多个组件供大家选择,喜欢哪个用哪个一、系统架构图二、快速启动1.本地启动nacos: ht...
继续阅读 »

之前接个私活,在网上找了好久没有找到合适的框架,不是版本低没人维护了,在不就是组件相互依赖较高。所以我自己搭建一个全新spingCloud框架,里面所有组件可插拔的,集成多个组件供大家选择,喜欢哪个用哪个

一、系统架构图


二、快速启动

1.本地启动nacos: http://127.0.0.1:8848

sh startup.sh -m standalone

2.本地启动sentinel: http://127.0.0.1:9000

nohup java -Dauth.enabled=false -Dserver.port=9000 -jar sentinel-dashboard-1.8.1.jar &

3.本地启动zipkin: http://127.0.0.1:9411/

nohup java -jar zipkin-server-2.23.2-exec.jar &

三、项目概述

  • springboot+springcloud

  • 注册中心:nacos

  • 网关:gateway

  • RPC:feign

以下是可插拔功能组件

  • 流控熔断降级:sentinel

  • 全链路跟踪:sleth+zipkin

  • 分布式事务:seata

  • 封装功能模块:全局异常处理、日志输出打印持久化、多数据源、鉴权授权模块、zk(分布式锁和订阅者模式)

  • maven:实现多环境打包、直推镜像到docker私服。

这个项目整合了springcloud体系中的各种组件。以及集成配置说明。同时将自己平时使用的功能性的封装以及工具包都最为模块整合进来。可以避免某些技术点长时间不使用后的遗忘。

另一方面现在springboot springcloud 已经springcloud-alibaba的版本迭代速度越来越快。

为了保证我们的封装和集成方式在新版本中依然正常运行,需要用该项目进行最新版本的适配实验。这样可以更快的在项目中集合工程中的功能模块。

四、项目预览






五、新建业务工程模块说明

由于springboot遵循 约定大于配置的原则。所以本工程中所有的额类都在的包路径都在com.cloud.base下。

如果新建的业务项目有规定使用指定的基础包路径则需要在启动类增加包扫描注解将com.cloud.base下的所有类加入到扫描范围下。

@ComponentScan(basePackages = "com.cloud.base")

如果可以继续使用com.cloud.base 则约定将启动类放在该路径下即可。

六、模块划分

父工程:

cloud-base - 版本依赖管理 <groupId>com.cloud</groupId>
|
|--common - 通用工具类和包 <groupId>com.cloud.common</groupId>
|   |
|   |--core-common 通用包 该包包含了SpringMVC的依赖,会与WebFlux的服务有冲突
|   |
|   |--core-exception 自定义异常和请求统一返回类
|
|--dependency - 三方功能依赖集合 无任何实现 <groupId>com.cloud.dependency</groupId>
|   |
|   |--dependency-alibaba-cloud 关于alibaba-cloud的依赖集合
|   |
|   |--dependency-mybatis-tk 关于ORM mybatis+tk.mybatis+pagehelper的依赖集合
|   |
|   |--dependency-mybatis-plus 关于ORM mybatis+mybatis—plus+pagehelper的依赖集合
|   |
|   |--dependency-seata 关于分布式事务seata的依赖集合
|   |
|   |--dependency-sentinel 关于流控组件sentinel的依赖集合
|   |
|   |--dependency-sentinel-gateway 关于网关集成流控组件sentinel的依赖集合(仅仅gateway网关使用该依赖)
|   |
|   |--dependency-sleuth-zipkin 关于链路跟踪sleuth-zipkin的依赖集合
|
|--modules - 自定义自实现的功能组件模块 <groupId>com.cloud.modules</groupId>
|   |
|   |--modules-logger 日志功能封装
|   |
|   |--modules-multi-datasource 多数据功能封装
|   |
|   |--modules-lh-security 分布式安全授权鉴权框架封装
|   |
|   |--modules-youji-task 酉鸡-分布式定时任务管理模块
|   |
|
|  
|  
| 以下是独立部署的应用 以下服务启动后配合前端工程使用 (cloud-base-angular-admin)
|
|--cloud-gateway 应用网关
|
|--authorize-center 集成了modules-lh-security 的授权中心,提供统一授权和鉴权
|  
|--code-generator 代码生成工具
|
|--user-center 用户中心 提供用户管理和权限管理的相关服务
|
|--youji-manage-server 集成了modules-youji-task 的定时任务管理服务端

七、版本使用说明

<springboot.version>2.4.2</springboot.version>
<springcloud.version>2020.0.3</springcloud.version>
<springcloud-alibaba.version>2021.1</springcloud-alibaba.version>

八、多环境打包说明

在需要独立打包的模块resources资源目录下增加不同环境的配置文件

application-dev.yml
application-test.yml
application-prod.yml

修改application.yml

spring:
profiles:
  active: @profileActive@

在需要独立打包的模块下的pom文件中添加一下打包配置。

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${springboot.version}</version>
<configuration>
<fork>true</fork>
<addResources>true</addResources>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<delimiters>
<delimiter>@</delimiter>
</delimiters>
<useDefaultDelimiters>false</useDefaultDelimiters>
</configuration>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>

<profiles>
<profile>
<id>dev</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<profileActive>dev</profileActive>
</properties>
</profile>
<profile>
<id>test</id>
<properties>
<profileActive>test</profileActive>
</properties>
</profile>
<profile>
<id>prod</id>
<properties>
<profileActive>prod</profileActive>
</properties>
</profile>
</profiles>

mvn打包命令

# 打开发环境
mvn clean package -P dev -Dmaven.test.skip=ture
# 打测试环境
mvn clean package -P test -Dmaven.test.skip=ture
# 打生产环境
mvn clean package -P prod -Dmaven.test.skip=ture

九、构建Docker镜像

整合dockerfile插件,可直接将jar包构建为docker image 并推送到远程仓库

增加插件依赖

<!-- docker image build -->
<plugin>
  <groupId>com.spotify</groupId>
  <artifactId>dockerfile-maven-plugin</artifactId>
  <version>1.4.10</version>
  <executions>
      <execution>
          <id>default</id>
          <goals>
              <!--如果package时不想用docker打包,就注释掉这个goal-->
              <!--                       <goal>build</goal>-->
              <goal>push</goal>
          </goals>
      </execution>
  </executions>
  <configuration>
      <repository>49.232.166.94:8099/example/${project.artifactId}</repository>
      <tag>${profileActive}-${project.version}</tag>
      <username>admin</username>
      <password>Harbor12345</password>
      <buildArgs>
          <JAR_FILE>target/${project.build.finalName}.jar</JAR_FILE>
      </buildArgs>
  </configuration>
</plugin>

在pom.xml同级目录下增加Dockerfile

FROM registry.cn-hangzhou.aliyuncs.com/lh0811/lh0811-docer:lh-jdk1.8-0.0.1
MAINTAINER lh0811
ADD ./target/${JAR_FILE} /opt/app.jar
RUN chmod +x /opt/app.jar
CMD java -jar /opt/app.jar

十、源码获取

源码和开发笔记

作者:我先失陪了
来源:https://juejin.cn/post/7100457917115007013

收起阅读 »