注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

旋转、缩放、移动:掌握CSS Transform动画的终极指南!

在深入探讨CSS变形动画之前,让我们先探讨一下掌握它之后你可以实现哪些有趣的效果。学习了CSS变形动画之后,你将能够为你的网页添加引人注目的动态效果,例如创建一个立体的3D魔方,或者设计一个引人入胜的旋转菜单。这些仅仅是众多可能性中的一小部分,但或许可以勾起我...
继续阅读 »

在深入探讨CSS变形动画之前,让我们先探讨一下掌握它之后你可以实现哪些有趣的效果。

学习了CSS变形动画之后,你将能够为你的网页添加引人注目的动态效果,例如创建一个立体的3D魔方,或者设计一个引人入胜的旋转菜单。

Description
这些仅仅是众多可能性中的一小部分,但或许可以勾起我们的学习兴趣。

一、什么是CSS变形动画?

CSS变形动画是利用CSS3的transform属性创建的动画效果。它可以使元素旋转、缩放、倾斜甚至翻转,让静态的网页元素动起来,为用户带来更加丰富的交互体验。

坐标系统

首先我们要学习的变形动画,想达到在上图中出现的3D效果单纯的X与Y两个轴是实现不了的,还需要加入一条纵深轴,即Y轴的参与才有一个3D的视觉感受。

那么如何来理解X,Y,Z这三条轴的关系呢?可以看一下下面这张图。

Description

  • X轴代表水平轴

  • Y轴代表垂直轴

  • Z轴代表纵深轴

X和Y轴都非常好理解,怎么理解这个Z轴呢?

CSS的中文名称叫做层叠样式表,那么它肯定是一层一层的。之前学习过z-index就是用来设置层的优先级,优先级越高越在上面,也可以理解为离我们肉眼越近,它把优先级低的层给盖住了,所以Z轴可以理解为我们观察的视角与被观察物体之间的一条轴。

  • Z轴数值越大,说明观测距离越远。

  • Z轴的数值可以无限大,所以设置的时候一定要小心。

二、变形操作

使用 transform 来控制元素变形操作,包括控制移动、旋转、倾斜、3D转换等。

Description

下面我们通过一些例子来演示一下,比较常用的变形操作:

2.1 位移 translate()

translate()函数可以将元素向指定的方向移动,类似于position中的relative。或以简单的理解为,使用translate()函数,可以把元素从原来的位置移动,而不影响在X、Y轴上的任何Web组件。

想象一下,当你滚动页面时,一个元素平滑地从一个位置滑向另一个位置,这种流畅的过渡效果可以大大提升用户体验。

translate我们分为三种情况:

1)translate(x,y)水平方向和垂直方向同时移动(也就是X轴和Y轴同时移动)

2)translateX(x)仅水平方向移动(X轴移动)

3)translateY(Y)仅垂直方向移动(Y轴移动)

实例演示: 通过translate()函数将元素向Y轴下方移动50px,X轴右方移动100px。

HTML代码:

<div class="wrapper">
<div>我向右向下移动</div>
</div>

CSS代码:


.wrapper {
width: 200px;
height: 200px;
border: 2px dotted red;
margin: 20px auto;
}
.wrapper div {
width: 200px;
height: 200px;
line-height: 200px;
text-align: center;
background: orange;
color: #fff;
-webkit-transform: translate(50px,100px);
-moz-transform:translate(50px,100px);
transform: translate(50px,100px);
}

演示结果:

Description

2.2 旋转 rotate()

旋转rotate()函数通过指定的角度参数使元素相对原点进行旋转。旋转不仅可以是固定的度数,还可以是动态变化的,创造出无限的可能性。

它主要在二维空间内进行操作,设置一个角度值,用来指定旋转的幅度。如果这个值为正值,元素相对原点中心顺时针旋转;如果这个值为负值,元素相对原点中心逆时针旋转。如下图所示:

Description

HTML代码:

<div class="wrapper">
<div></div>
</div>

CSS代码:

.wrapper {
width: 200px;
height: 200px;
border: 1px dotted red;
margin: 100px auto;
}
.wrapper div {
width: 200px;
height: 200px;
background: orange;
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
}

演示结果:
Description

2.3 扭曲 skew()

扭曲skew()函数能够让元素倾斜显示。这种效果常常用于模拟速度感或者倾斜的视觉效果。

它可以将一个对象以其中心位置围绕着X轴和Y轴按照一定的角度倾斜。这与rotate()函数的旋转不同,rotate()函数只是旋转,而不会改变元素的形状。skew()函数不会旋转,而只会改变元素的形状。

Skew()具有三种情况:
1)skew(x,y)使元素在水平和垂直方向同时扭曲(X轴和Y轴同时按一定的角度值进行扭曲变形);

Description

第一个参数对应X轴,第二个参数对应Y轴。如果第二个参数未提供,则值为0,也就是Y轴方向上无斜切。

2)skewX(x)仅使元素在水平方向扭曲变形(X轴扭曲变形);
Description

3)skewY(y)仅使元素在垂直方向扭曲变形(Y轴扭曲变形)。
Description

示例演示:通过skew()函数将长方形变成平行四边形。

HTML代码:

<div class="wrapper">
<div>我变成平形四边形</div>
</div>

CSS代码:

.wrapper {
width: 300px;
height: 100px;
border: 2px dotted red;
margin: 30px auto;
}
.wrapper div {
width: 300px;
height: 100px;
line-height: 100px;
text-align: center;
color: #fff;
background: orange;
-webkit-transform: skew(45deg);
-moz-transform:skew(45deg)
transform:skew(45deg);
}

演示结果:
Description

2.4 缩放 scale()

缩放 scale()函数 让元素根据中心原点对对象进行缩放。这不仅可以用来模拟放大镜效果,还可以创造出元素的进入和退出动画,比如一个图片慢慢缩小直至消失。

缩放 scale 具有三种情况:

1) scale(X,Y)使元素水平方向和垂直方向同时缩放(也就是X轴和Y轴同时缩放)。

Description
例如:

div:hover {
-webkit-transform: scale(1.5,0.5);
-moz-transform:scale(1.5,0.5)
transform: scale(1.5,0.5);
}

注意:Y是一个可选参数,如果没有设置Y值,则表示X,Y两个方向的缩放倍数是一样的。

2)scaleX(x)元素仅水平方向缩放(X轴缩放)
Description
3)scaleY(y)元素仅垂直方向缩放(Y轴缩放)
Description
HTML代码:

<div class="wrapper">
<div>我将放大1.5倍</div>
</div>

CSS代码:


.wrapper {
width: 200px;
height: 200px;
border:2px dashed red;
margin: 100px auto;
}
.wrapper div {
width: 200px;
height: 200px;
line-height: 200px;
background: orange;
text-align: center;
color: #fff;
}
.wrapper div:hover {
opacity: .5;
-webkit-transform: scale(1.5);
-moz-transform:scale(1.5)
transform: scale(1.5);
}

演示结果:
Description
注意: scale()的取值默认的值为1,当值设置为0.01到0.99之间的任何值,作用使一个元素缩小;而任何大于或等于1.01的值,作用是让元素放大。


想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!点这里前往学习哦!

2.5 矩阵 matrix()

matrix() 是一个含六个值的(a,b,c,d,e,f)变换矩阵,用来指定一个2D变换,相当于直接应用一个[a b c d e f]变换矩阵。就是基于水平方向(X轴)和垂直方向(Y轴)重新定位元素。

此属性值使用涉及到数学中的矩阵,我在这里只是简单的说一下CSS3中的transform有这么一个属性值,如果需要深入了解,需要对数学矩阵有一定的知识。

示例演示:通过matrix()函数来模拟transform中translate()位移的效果。
HTML代码:

<div class="wrapper">
<div></div>
</div>

CSS代码:

.wrapper {
width: 300px;
height: 200px;
border: 2px dotted red;
margin: 40px auto;
}
.wrapper div {
width:300px;
height: 200px;
background: orange;
-webkit-transform: matrix(1,0,0,1,50,50);
-moz-transform:matrix(1,0,0,1,50,50);
transform: matrix(1,0,0,1,50,50);
}

演示结果:

Description

2.6 原点 transform-origin

任何一个元素都有一个中心点,默认情况之下,其中心点是居于元素X轴和Y轴的50%处。如下图所示:

Description

在没有重置transform-origin改变元素原点位置的情况下,CSS变形进行的旋转、位移、缩放,扭曲等操作都是以元素自己中心位置进行变形。

但很多时候,我们可以通过transform-origin来对元素进行原点位置改变,使元素原点不在元素的中心位置,以达到需要的原点位置。

transform-origin取值和元素设置背景中的background-position取值类似,如下表所示:

Description

示例演示:

通过transform-origin改变元素原点到左上角,然后进行顺时旋转45度。

HTML代码:

<<div class="wrapper">
<div>原点在默认位置处</div>
</div>
<div class="wrapper transform-origin">
<div>原点重置到左上角</div>
</div>

CSS代码:

.wrapper {
width: 300px;
height: 300px;
float: left;
margin: 100px;
border: 2px dotted red;
line-height: 300px;
text-align: center;
}
.wrapper div {
background: orange;
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
}
.transform-origin div {
-webkit-transform-origin: left top;
transform-origin: left top;
}

演示结果:
Description

以上就是css动画中几种基本的变形技巧了,掌握这些我们可以操控我们的网页元素实现我们想要的一些基本动画效果。

在这个充满创造力的时代,CSS变形动画是每个前端开发者必备的技能。它不仅能提升用户体验,更能激发设计师和开发者的创意火花。所以,不妨尝试一下,让你的网页动起来,给用户留下深刻的印象吧!

收起阅读 »

🎉🎉 环信 IM 客户端将适配鸿蒙 HarmonyOS

自华为推出了自主研发操作系统鸿蒙 HarmonyOS 后,国内许多应用软件开始陆续全面兼容和接入鸿蒙操作系统。环信 IM 客户端计划将全面适配统鸿蒙 HarmonyOS ,助力开发者快速实现社交娱乐、语聊房、在线教育、智能硬件、社交电商、在线金融、线上医疗等广...
继续阅读 »

自华为推出了自主研发操作系统鸿蒙 HarmonyOS 后,国内许多应用软件开始陆续全面兼容和接入鸿蒙操作系统。环信 IM 客户端计划将全面适配统鸿蒙 HarmonyOS ,助力开发者快速实现社交娱乐、语聊房、在线教育、智能硬件、社交电商、在线金融、线上医疗等广泛场景的即时消息互动。




环信 IM 客户端适配后,将为开发者提供鸿蒙 HarmonyOS 系统上单聊、群聊、会话等能力和服务,具体覆盖到以下:

  • 消息管理:收发消息、全消息类型支持、会话管理、消息回执、消息撤回等
  • 用户属性:用户头像、用户昵称、自定义属性等
  • 群组管理:群成员管理、群管理员、群文件、群公告等
  • 离线推送:主流推送厂商支持,推送模板设置、推送通知等
  • 多设备同步:多设备消息和事件同步,支持设备同步策略设置等

作为一直深耕在即时通信领域的老兵,在平台框架开发支持上,环信 IM 已经支持 Android、iOS、macOS、Windows、Linux、Web、Flutter、Unity、Electron、React Native、Uni-App、APICloud 等。尤其海外开发者重点关注的 React Native、Flutter 平台,游戏开发者关注的 Unity 平台,以及原生桌面应用 Windows 平台,环信保持了持续更新迭代。

此次将增加鸿蒙HarmonyOS 的 SDK 适配,后续会持续优化和适配鸿蒙系统特性,助力开发者在鸿蒙HarmonyOS 系统上实现更加稳定、优质的即时消息互动。

环信 IM 目前已支持的平台:


在移动端 SDK 性能上,环信能做到小包体 SDK,提升终端设备运行性能,并且 SDK 崩溃率低于 0.005%,远低于行业平均水平 0.01%。

iOS 引入 SDK 前后 App 大小对比


Android 引入 SDK 前后 App 大小对比



在消息传输的延时性上,环信拥有新加坡、美国、德国等几大数据中心,全球部署近千终端网络加速节点,覆盖全球230多个国家和地区,实现全球端到端时延<100毫秒,网络连通率>99.95%,并支持多链路智能路由,遇到运营商网络故障自动切换路由。

扩展:

收起阅读 »

Android应用内版本更新:使用BasicUI库的简单实现

在移动应用开发中,应用内版本更新是一项重要的功能。它允许开发者轻松地向用户提供新的应用版本,以修复错误、改进性能,或者引入新功能。这篇文章将介绍如何使用 BasicUI 库,一个Android库,来实现应用内版本更新的功能。我们将演示如何从远程服务器下载APK...
继续阅读 »

在移动应用开发中,应用内版本更新是一项重要的功能。它允许开发者轻松地向用户提供新的应用版本,以修复错误、改进性能,或者引入新功能。这篇文章将介绍如何使用 BasicUI 库,一个Android库,来实现应用内版本更新的功能。我们将演示如何从远程服务器下载APK文件并进行安装。


BasicUI库简介


BasicUI 是一个功能强大且易于使用的Android库,用于实现各种常见UI和网络操作,其中包括文件下载和更新功能。这个库提供了一些便捷的方法来简化Android应用开发中的一些常见任务,包括版本更新。


要开始使用BasicUI库,你需要在你的项目中添加相应的依赖,可以在官方GitHub仓库中找到详细的文档和示例。


GitHub库链接: BasicUI


应用内部升级弹窗的流程图


image.png


代码实现应用内版本更新


下面是一个简单的代码示例,演示了如何使用BasicUI库来实现应用内版本更新。这段代码将从远程服务器下载APK文件,并在下载完成后进行安装。请确保你已经添加了BasicUI库的依赖。


val file = File(cacheDir, "update.apk")
if (file.exists()) {
file.delete()
}
mDialog.apply {
setOnCancelListener {
HttpUtils.cancel()
}
}.show()
with(this@OkHttpActivity)
.url("http://example.com/your_update.apk") // 替换成实际的APK下载链接
.downloadSingle()
.file(file)
.exectureDownload(object : DownloadCallback {
override fun onFailure(e: Exception?) {
LogUtils.e(e!!.message)
mDialog.dismiss()
}

override fun onSucceed(file: File?) {
ToastUtils.showShort("文件下载完成")
LogUtils.e("文件保存的位置:" + file!!.absolutePath)
mProgressBar!!.visibility = View.GONE
mProgressBar!!.progress = 0
installApk(file)
mDialog.dismiss()
}

override fun onProgress(progress: Int) {
LogUtils.e("单线程下载APK的进度:$progress")
mProgressBar!!.progress = progress
mProgressBar!!.visibility = View.VISIBLE
}
})

上述代码的主要步骤包括:



  1. 创建一个用于保存下载APK文件的本地文件,要使用cacheDir目录,原因是可以不需要读写权限。

  2. 如果之前存在同名文件,先进行删除。

  3. 创建一个对话框,其中包括一个取消监听器,用于在用户取消下载时取消网络请求。

  4. 使用BasicUI库的网络操作类(HttpUtils)创建一个下载请求,指定下载地址、下载完成后保存的文件,以及下载回调接口。

  5. 在下载回调接口中处理下载成功、失败和进度更新的情况。


请注意,你需要将示例代码中的下载链接替换为实际的APK下载链接。这段代码提供了一个简单而有效的方式来执行应用内版本更新,但你还可以根据你的需求进行进一步的定制化。


结语


在本文中,我们演示了如何使用BasicUI库来实现Android应用内版本更新的功能。这是一个快速、方便的解决方案,可以帮助你轻松地向用户提供最新版本的应用程序。请记住,版本更新是确保用户始终使用最新、最稳定版本的应用的关键步骤。


为了更好地满足你的需求,你可以根据实际情况进一步定制版本更新流程,例如添加灰度发布、自动检测新版本等功能。希望这篇文章对你有所帮助,使你能够更好地满足用户的需求和提供卓越的应用体验。




这篇文章演示了如何使用 BasicUI 库来实现应用内版本更新的功能。你可以根据自己的需求进一步定制这个流程,以满足特定的应用程序要求。希望这篇文章对你有所帮助!


作者:peakmain9
来源:juejin.cn/post/7293401255053819941
收起阅读 »

个人或个体户,如何免费使用微信小程序授权登录

web
需求 个人或个体户,如何免费使用微信小程序授权,快速登录进系统内部? 微信授权登录好处: 不用自己开发一个登录模块,节省开发和维护成本 安全性得到了保障,安全验证完全交由腾讯验证,超级可靠哇 可能有的人会问,为何不用微信公众号授权登录?原因很简单,因为一年...
继续阅读 »

需求


个人或个体户,如何免费使用微信小程序授权,快速登录进系统内部?


微信授权登录好处:



  1. 不用自己开发一个登录模块,节省开发和维护成本

  2. 安全性得到了保障,安全验证完全交由腾讯验证,超级可靠哇


可能有的人会问,为何不用微信公众号授权登录?原因很简单,因为一年要300元,小公司得省钱啊!


实现步骤说明


所有的步骤里包含四个对象,分别是本地后台本地微信小程序本地网页、以及第三方微信后台



  1. 本地后台调用微信后台https://api.weixin.qq.com/cgi-bin/token接口,get请求,拿到返回的access_token

  2. 本地后台根据拿到的access_token,调用微信后台https://api.weixin.qq.com/wxa/getwxacodeunlimit接口,得到二维码图片文件,将其输出传递给本地网页显示

  3. 本地微信小程序本地网页的二维码图片,跳转至小程序登录页面,通过wx.login方法,在success回调函数内得到code值,并将该值传递给本地后台

  4. 本地后台拿到code值后,调用微信后台https://api.weixin.qq.com/sns/jscode2session接口,get请求,得到用户登录的openid即可。



注意点:



  1. 上面三个微信接口/cgi-bin/token/getwxacodeunlimit/jscode2session必须由本地后台调用,微信小程序那边做了前端限制;

  2. 本地网页如何得知本地微信小程序已扫码呢?


本地微信小程序code,通过A接口,将值传给后台,后台拿到openid后,再将成功结果返回给本地微信小程序;同时,本地网页不断地轮询A接口,等待后台拿到openid后,便显示登录成功页面。



微信小程序核心代码


Page({
data: {
theme: wx.getSystemInfoSync().theme,
scene: "",
jsCode: "",
isLogin: false,
loginSuccess: false,
isChecked: false,
},
onLoad(options) {
const that = this;
wx.onThemeChange((result) => {
that.setData({
theme: result.theme,
});
});
if (options !== undefined) {
if (options.scene) {
wx.login({
success(res) {
if (res.code) {
that.setData({
scene: decodeURIComponent(options.scene),
jsCode: res.code,
});
}
},
});
}
}

},
handleChange(e) {
this.setData({
isChecked: Boolean(e.detail.value[0]),
});
},
formitForm() {
const that = this;
if (!this.data.jsCode) {
wx.showToast({
icon: "none",
title: "尚未微信登录",
});
return;
}
if (!this.data.isChecked) {
wx.showToast({
icon: "none",
title: "请先勾选同意用户协议",
});
return;
}
wx.showLoading({
title: "正在加载",
});
let currentTimestamp = Date.now();
let nonce = randomString();
wx.request({
url: `A接口?scene=${that.data.scene}&js_code=${that.data.jsCode}`,
header: {},
method: "POST",
success(res) {
wx.hideLoading();
that.setData({
isLogin: true,
});
if (res.statusCode == 200) {
that.setData({
loginSuccess: true,
});
} else {
if (res.statusCode == 400) {
wx.showToast({
icon: "none",
title: "无效请求",
});
} else if (res.statusCode == 500) {
wx.showToast({
icon: "none",
title: "服务内部错误",
});
}
that.setData({
loginSuccess: false,
});
}
},
fail: function (e) {
wx.hideLoading();
wx.showToast({
icon: "none",
title: e,
});
},
});
},
});


scene为随机生成的8位数字


本地网页核心代码


    let isInit = true
function loginWx() {
isInit = false
refreshQrcode()
}
function refreshQrcode() {
showQrLoading = true
showInfo = false
api.get('/qrcode').then(qRes => {
if (qRes.status == 200) {
imgSrc = `${BASE_URL}${qRes.data}`
pollingCount = 0
startPolling()
} else {
showToast = true
toastMsg = '二维码获取失败,请点击刷新重试'
showInfo = true
}
}).finally(() => {
showQrLoading = false
})
}

// 开始轮询
// 1000毫秒轮询一次
function startPolling() {
pollingInterval = setInterval(function () {
pollDatabase()
}, 1000)
}
function pollDatabase() {
if (pollingCount >= maxPollingCount) {
clearInterval(pollingInterval)
showToast = true
toastMsg = '二维码已失效,请刷新'
showInfo = true
return
}
pollingCount++
api.get('/result').then(res => {
if (res.status == 200) {
clearInterval(pollingInterval)
navigate('/os', { replace: true })
} else if (res.status == 408) {
clearInterval(pollingInterval)
showToast = true
toastMsg = '二维码已失效,请刷新'
showInfo = true
}
})
}



html的部分代码如下所示


     <button class="btn" on:click={loginWx}>微信登录</button>
<div id="qrcode" class="relative mt-10">
{#if imgSrc}
<img src={imgSrc} alt="二维码图片"/>
{/if}
{#if showQrLoading}
<div class="mask absolute top-0 left-0 w-full h-full z-10">
<Loading height="12" width="12"/>
</div>
{/if}
</div>

尾声


若需要完整代码,或想知道如何申请微信小程序,欢迎大家关注或私信我哦~~


作者:zwf193071
来源:juejin.cn/post/7351649413401493556
收起阅读 »

🚫为了防止狗上沙发,写了一个浏览器实时识别目标功能📷

web
背景 家里有一条狗🐶,很喜欢乘人不备睡沙发🛋️,恰好最近刚搬家 + 狗迎来了掉毛期 不想让沙发上很多毛。所以希望能识别到狗,然后播放“gun 下去”的音频📣。 需求分析 需要一个摄像头📷 利用 chrome 浏览器可以调用手机摄像头,获取权限,然后利用 ...
继续阅读 »

背景



家里有一条狗🐶,很喜欢乘人不备睡沙发🛋️,恰好最近刚搬家 + 狗迎来了掉毛期 不想让沙发上很多毛。所以希望能识别到狗,然后播放“gun 下去”的音频📣。


需求分析



  • 需要一个摄像头📷

    • 利用 chrome 浏览器可以调用手机摄像头,获取权限,然后利用 video 将摄像头的内容绘制到 video 上。



  • 通过摄像头实时识别画面中的狗🐶

    • 利用 tensorflow 和预训练的 COCO-SSD MobileNet V2 模型进行对象检测。

    • 将摄像头的视频流转化成视频帧图像传给模型进行识别



  • 录制一个音频

    • 识别到目标(狗)后播放音频📣



  • 需要部署在一个设备上

    • 找一个不用的旧手机📱,Android 系统

    • 安装 termux 来实现开启本地 http 服务🌐




技术要点



  1. 利用浏览器 API 调用手机摄像头,将视频流推给 video


    const stream = await navigator.mediaDevices.getUserMedia({
    // video: { facingMode: "environment" }, // 摄像头后置
    video: { facingMode: "user" },
    });

    const videoElement = document.getElementById("camera-stream");
    videoElement.srcObject = stream;


  2. 加载模型,实现识别


    let dogDetector;

    async function loadDogDetector() {
    // 加载预训练的SSD MobileNet V2模型
    const model = await cocoSsd.load();
    dogDetector = model; // 将加载好的模型赋值给dogDetector变量
    }


  3. 监听 video 的播放,将视频流转换成图像传入模型检测


    videoElement.addEventListener("play", async () => {
    requestAnimationFrame(processVideoFrame);
    });

    async function processVideoFrame() {
    if (!videoElement.paused && !videoElement.ended) {
    canvas.width = videoElement.videoWidth;
    canvas.height = videoElement.videoHeight;
    ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height);

    // 获取当前帧图像数据
    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

    // 对帧执行预测
    let predictionClasses = "";
    const predictions = await dogDetector.detect(imageData);
    // 处理预测结果,比如检查是否有狗被检测到
    for (const prediction of predictions) {
    predictionClasses += `${prediction.class}\n`; // 组装识别的物体名称
    if (prediction.class === "dog") {
    // 播放声音
    playDogBarkSound();
    }
    }
    nameContainer.innerText = predictionClasses.trim(); // 移除末尾的换行符

    requestAnimationFrame(processVideoFrame);
    }
    }


  4. 播放音频


    async function playDogBarkSound() {
    if (playing) return;
    playing = true;
    const audio = new Audio(dogBarkSound);
    audio.addEventListener("ended", () => {
    playing = false;
    });
    audio.volume = 0.5; // 调整音量大小
    await audio.play();
    }


  5. 手机开启本地 http 服务



    • 安装 termux

    • 安装 python3

    • 运行 python3 -m http.server 8000



  6. 将项目上传到 termux 的目录





项目代码(改为 html 文件后)


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mobile Dog Detector</title>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4.17.0/dist/tf.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/coco-ssd@2.2.3/dist/coco-ssd.min.js"></script>
<style>
#camera-stream {
width: 200px;
height: auto;
}
#name {
height: 200px;
overflow-y: auto;
font-family: Arial, sans-serif;
}
</style>
</head>
<body>
<video id="camera-stream" autoplay playsinline></video>
<div id="name" style="height: 200px"></div>

<script>
let playing = false;
let dogDetector;

async function loadDogDetector() {
// 加载预训练的SSD MobileNet V2模型
const model = await cocoSsd.load();
dogDetector = model; // 将加载好的模型赋值给dogDetector变量
console.log("dogDetector", dogDetector);
startCamera();
}
// 调用函数加载模型
loadDogDetector();

async function startCamera() {
const stream = await navigator.mediaDevices.getUserMedia({
// video: { facingMode: "environment" }, // 摄像头后置
video: { facingMode: "user" },
});
const nameContainer = document.getElementById("name");
const videoElement = document.getElementById("camera-stream");
videoElement.srcObject = stream;

const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");

videoElement.addEventListener("play", async () => {
requestAnimationFrame(processVideoFrame);
});
async function processVideoFrame() {
if (!videoElement.paused && !videoElement.ended) {
canvas.width = videoElement.videoWidth;
canvas.height = videoElement.videoHeight;
ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height);

const imageData = ctx.getImageData(
0,
0,
canvas.width,
canvas.height
);

let predictionClasses = "";
const predictions = await dogDetector.detect(imageData);
for (const prediction of predictions) {
predictionClasses += `${prediction.class}\n`;
if (prediction.class === "dog") {
// 修改为检测到狗时播放声音
playDogBarkSound();
}
}
nameContainer.innerText = predictionClasses.trim();

requestAnimationFrame(processVideoFrame);
}
}

async function playDogBarkSound() {
if (playing) return;
playing = true;
const audio = new Audio("./getout.mp3");
audio.addEventListener("ended", () => {
playing = false;
});
audio.volume = 0.5; // 调整音量大小
await audio.play();
}
}
</script>
</body>
</html>

实现效果


效果很好👍,用旧手机开启摄像头后,检测到狗就播放声音了。


但是,家里夫人直接做了一个围栏晚上给狗圈起来了🚫



实现总结


该方案通过以下步骤实现了一个基于网页的实时物体检测系统,专门用于识别画面中的狗并播放特定音频以驱赶它离开沙发。具体实现过程包括以下几个核心部分:



  • 调用摄像头:


使用浏览器提供的 navigator.mediaDevices.getUserMedia API 获取用户授权后调用手机摄像头,并将视频流设置给 video 元素展示。



  • 加载物体检测模型:


使用 TensorFlow.js 和预训练的 COCO-SSD MobileNet V2 模型进行对象检测,加载模型后赋值给 dogDetector 变量。
处理视频流与图像识别:


监听 video 元素的播放事件,通过 requestAnimationFrame 循环逐帧处理视频。
将当前视频帧绘制到 canvas 上,然后从 canvas 中提取图像数据传入模型进行预测。
在模型返回的预测结果中,如果检测到“dog”,则触发播放音频函数。



  • 播放音频反馈:


定义一个异步函数 playDogBarkSound 来播放指定的音频文件,确保音频只在前一次播放结束后才开始新的播放。



  • 部署环境准备:


使用旧 Android 手机安装 Termux,创建本地 HTTP 服务器运行项目代码。
上传项目文件至 Termux 目录下并通过访问 localhost:8000 启动应用。


通过以上技术整合,最终实现了在旧手机上部署一个能够实时检测画面中狗的网页应用,并在检测到狗时播放指定音频。


作者:前端小蜗
来源:juejin.cn/post/7345672631323394098
收起阅读 »

Androidmanifest文件加固和对抗

前言 恶意软件为了不让我们很容易反编译一个apk,会对androidmanifest文件进行魔改加固,本文探索androidmanifest加固的常见手法以及对抗方法。这里提供一个恶意样本的androidmanifest.xml文件,我们学完之后可以动手实践。...
继续阅读 »

前言


恶意软件为了不让我们很容易反编译一个apk,会对androidmanifest文件进行魔改加固,本文探索androidmanifest加固的常见手法以及对抗方法。这里提供一个恶意样本的androidmanifest.xml文件,我们学完之后可以动手实践。


1、Androidmanifest文件组成


这里贴一张经典图,主要描述了androidmanifest的组成


image


androidmanifest文件头部仅仅占了8个字节,紧跟其后的是StringPoolType字符串常量池


(为了方便我们观察分析,可以先安装一下010editor的模板,详细见2、010editor模板)


Magic Number


这个值作为头部,是经常会被魔改的,需要重点关注


image


StylesStart


该值一般为0,也是经常会发现魔改


image


StringPool


image


寻找一个字符串,如何计算?


1、获得字符串存放开放位置:0xac(172),此时的0xac是不带开头的8个字节


所以需要我们加上8,最终字符串在文件中的开始位置是:0xb4


2、获取第一个字符串的偏移,可以看到,偏移为0


image


3、计算字符串最终存储的地方: 0xb4 = 0xb4 + 0


读取字符串,以字节00结束


image


读取到的字符为:theme


帮助网安学习,全套资料S信领取:


① 网安学习成长路径思维导图

② 60+网安经典常用工具包

③ 100+SRC漏洞分析报告

④ 150+网安攻防实战技术电子书

⑤ 最权威CISSP 认证考试指南+题库

⑥ 超1800页CTF实战技巧手册

⑦ 最新网安大厂面试题合集(含答案)

⑧ APP客户端安全检测指南(安卓+IOS)


总结:


stringpool是紧跟在文件头后面的一块区域,用于存储文件所有用到的字符串


这个地方呢,也是经常发生魔改加固的,比如:将StringCount修改为0xFFFFFF无穷大


在经过我们的手动计算和分析后,我们对该区域有了更深的了解。


2、010editor模板


使用010editor工具打开,安装模板库


image


搜索:androidmanifest.bt


image


安装完成且运行之后:


image


会发现完整的结构,帮助我们分析


3、使用AXMLPrinter2进行的排错和修复


用法十分简单:


java -jar AXMLPrinter2.jar AndroidManifest_origin.xml

会有一系列的报错,但是不要慌张,根据这些报错来对原androidmanifest.xml进行修复


image​​


意思是:出乎意料的0x80003(正常读取的数据),此时却读取到:0x80000


按照小端序,正常的数据应该是: 03 00 08


使用 010editor 打开


image


将其修复


image


保存,再次尝试运行AXMLPrinter2


image


好家伙还有错误,这个-71304363,不方便我们分析,将其转换为python的hex数据


NegativeArraySizeException 表示在创建数组的时候,数组的大小出现了负数。


androidmanifest加固后文件与正常的androidmanifest文件对比之后就可以发现魔改的地方。


image


将其修改回去


image


运行仍然报错,是个新错误:


image


再次去分析:


image


stringoffsets如此离谱,并且数组的大小变为了0xff


image


image


根据报错的信息,尝试把FF修改为24


image


image


再次运行


image


成功拿到反编译后的androidmanifest.xml文件


总结:


这个例子有三个魔改点经常出现在androidmanifest.xml加固


恶意软件通过修改这些魔改点来对抗反编译


作者:合天网安实验室
来源:juejin.cn/post/7324011299272310811
收起阅读 »

前端在线预览播放视频方案,dpPlayer

web
华为云生成obs链接时,可以做配置。 视频是用来预览的 视频是用来下载的 一般我们播放本地视频都是使用vedio标签,但是vedio标签只支持三种视频格式:MP4、WebM、Ogg,对于在线视频直接使用vedio不支持播放。 故,上述 2 中的视频,在ve...
继续阅读 »

华为云生成obs链接时,可以做配置。



  1. 视频是用来预览

  2. 视频是用来下载


一般我们播放本地视频都是使用vedio标签,但是vedio标签只支持三种视频格式:MP4、WebM、Ogg,对于在线视频直接使用vedio不支持播放。
故,上述 2 中的视频,在vedio中不支持播放,浏览器访问链接,直接就下载了。


先介绍几个概念:


流协议: 流协议就是在两个通信系统之间传输多媒体文件的一套规则,它定义了视频文件将如何分解为小数据包以及它们在互联网上传输的顺序,RTMP与 RTSP 是比较常见的流媒体协议。


HLS: HLS (HTTP Live Streaming)是Apple的动态码率自适应技术。主要用于PC和Apple终端的音视频服务。包括一个m3u(8)的索引文件,TS媒体分片文件和key加密串文件。参考:HLS。简单来说,HLS是一种协议,如果你的视频源是http://xxxx.m3u8这种,就选择这种协议,.m3u8是个文本文件,直播时,他的内容实时变更,内部指向一个或多个.ts文件。


HTTP-FLV: HTTP-FLV 是将音视频数据以 FLV 文件格式进行封装,再将 FLV 格式数据封装在 HTTP 协议中进行传输的一种流媒体传输方式。HTTP-FLV 的实现原理: HTTP-FLV 利用 HTTP/1.1 分块传输机制发送 FLV 数据。虽然直播服务器无法知道直播流的长度,但是 HTTP/1.1 分块传输机制可以不填写 conten-length 字段而是携带 Transfer-Encoding: chunked 字段,这样客户端就会一直接受数据。参考:FLV 和 HTTP-FLV

简单来说就是你的视频源是直播且是xxxx.flv,就选择这种协议播放。还有个websocket-flv,是基于websocket的。


RTMP与RTSP: 什么是RTMP 和 RTSP?它们之间有什么区别?


H264(AVC)与H265(HEVC): 都是视频编码,是视频压缩格式,由于视频本身的码流太大,所以需要经过压缩然后再通过网络进行传输,其中H265是H264的升级版,很多播放器无法播放H265视频。




xgplayer


vue2的系统,本来用xgplayer 版本:2.32.5。无奈本地可以展示,测试环境不能用,报错不明显,粗略看了一下是插件底层,内部报错,故放弃xgpalyer插件。


ps.我在vue3的系统中,用过xgpalyer插件,挺好用的


优点如下:



  • 官网教程非常简单清晰,上手快

  • 使用起来体验感很好

  • 支持直播点播,支持hls、http+flv、dash、WebRTC直播,还有音乐播放器 。

  • 提供在线可调试demo


dpplayer


然后,我就换了 dppalyer插件来展示。点击查看中文文档


这个插件,我去github查了一下,15k星星,用的人还是挺多,但是,个人感觉不如 xgplayer好用。


安装npm install dplayer --save


在页面中引用


import DPlayer from 'dplayer';

const dp = new DPlayer(options);

dpplayer实现是通过生成iframe页面,将视频嵌套到其中。


刚开始给容器写了样式,宽100% 高100%,结果它不能自适应屏幕,很难受。后面我强行定宽420px。高度自动获取当前容器高度,定了一个最大高度。


但其实没有用,它会根据宽度,自己按比例缩放高度。
所以我在视频渲染出来后,自动调了一下全屏功能dpPlayer.fullScreen.request('web');
勉强解决了这个问题。


贴一下我的完整代码


<template>
<div class="vedio-wrapper" :style="{'max-height': winH}">
<el-empty v-if="!player" description="暂无数据"></el-empty>
<div :id="id" allowfullscreen="allowfullscreen" />
</div>

</template>


<script>
import DPlayer from 'dplayer';

import { getParam } from '@/utils/utils'
import {
getBucketObsFileUrl
} from '@/api/common'

export default {
name: 'previewMedia',
components:{},
data() {
return {
winH: '300px',
id: 'dpPlayerDom',
player: null
}

},
created() {
const winH = window.innerHeight
this.winH = winH + 'px'
},
mounted() {
this.getFileUrl()
},
methods: {

async getFileUrl() {
try {
const filePath = getParam('filePath')
const type = getParam('type') ? parseInt(getParam('type')) : 1
if (!filePath) return
const params = {
objectKey: filePath,
type
}
const data = await getBucketObsFileUrl(params);
this.setVedioplayerConfig(data)
} catch (e) {
console.error(e)
}
},

setVedioplayerConfig(url) {
if (!url) return

const tmpConfig = {
container: document.getElementById('dplayer'),
screenshot: false,
video: {
url: url,
thumbnails: 'thumbnails.jpg',
},
contextmenu: []

}

this.$nextTick(() => {
tmpConfig.container = document.getElementById(this.id)
const dpPlayer = new DPlayer(tmpConfig);
this.player = dpPlayer

dpPlayer.fullScreen.request('web');
})

}
}
}
</script>

<style scoped lang="scss">
.vedio-wrapper {
width: 400px;
height: 100%;
margin: 0 auto;
}
</style>



作者:山间板栗
来源:juejin.cn/post/7355456165244239912
收起阅读 »

使用RecyclerView实现三种阅读器翻页样式

一、整体逻辑 为何直接对RecyclerView进行扩展而不使用ViewPager/ViewPager2?原因如下: Scroll Model(垂直滑动)需要自定义自动滑动(对指定页进行吸附) Flip Mode(仿真翻页)需要获取各种情况下的方向信息,以...
继续阅读 »

一、整体逻辑


image.png


为何直接对RecyclerView进行扩展而不使用ViewPager/ViewPager2?原因如下:



  1. Scroll Model(垂直滑动)需要自定义自动滑动(对指定页进行吸附)

  2. Flip Mode(仿真翻页)需要获取各种情况下的方向信息,以实现更好的控制

  3. RecyclerView方便拓展,同时三种模式同时使用RecyclerView实现,便于复用


实现逻辑:三种滑动模式都在RecyclerView地基础上更改其滑动行为,横向滑动需要修改子View层级,仿真翻页需要再覆盖一层仿真动画


二、横向覆盖滑动(Slide Mode)


横向.gif


Slide Mode 最适合直接使用 ViewPager,不过我们还是以 RecyclerView 为基础来实现,让三种模式统一实现方式。实现思路:先实现跨页吸附,再实现覆盖翻页效果


1、跨页吸附


实现跨页吸附,需要在手指离开屏幕时对 RecyclerView 进行复位吸附操作,有两种情况:


(1)Scroll Idle


拖拽发生后,RecyclerView 滑动状态变为 SCROLL_STATE_IDLE 时,需要进行复位吸附操作


// OrientationHelper为系统提供的辅助类,LayoutManager的包装类
// 可以让我们方便的计算出RecyclerView相关的各种宽高,计算结果和LayoutManager方向相关
open fun snapToTargetExistingView(helper: OrientationHelper): Pair<Int, Int>? {
val lm = mRecyclerView.layoutManager ?: return null
val childCount = lm.childCount // 可见数量
if (childCount < 1) return null

var closestChild: View? = null
var absClosest = Int.MAX_VALUE
var scrollDistance = 0
// RecyclerView中心点,LayoutManager为竖向则是Y轴坐标,为横向则是X轴坐标
val containerCenter = helper.startAfterPadding + helper.totalSpace / 2

// 从可见Item中找到距RecyclerView离中心最近的View
for (i in 0 until childCount) {
val child = lm.getChildAt(i) ?: continue
if (consumeSnap(i, child)) return null // consumeSnap 默认返回false,竖直滑动模式才使用
val childCenter = (helper.getDecoratedStart(child)
+ helper.getDecoratedMeasurement(child) / 2)
val absDistance = abs(childCenter - containerCenter)
if (absDistance < absClosest) {
absClosest = absDistance
closestChild = child
scrollDistance = childCenter - containerCenter
}
}
closestChild ?: return null

// 滑动
when (orientation) {
VERTICAL -> mRecyclerView.smoothScrollBy(0, scrollDistance)
HORIZONTAL -> mRecyclerView.smoothScrollBy(scrollDistance, 0)
}
return Pair(scrollDistance, lm.getPosition(closestChild))
}


(2)Fling


可以通过 RecyclerView 提供的OnFlingListener消费掉Fling,将其转化为 SmoothScroll ,滑动到指定位置


①、找到吸附目标的位置(adapter position)


open fun findTargetSnapPosition(
lm: RecyclerView.LayoutManager,
velocity: Int,
helper: OrientationHelper
)
: Int {
val itemCount: Int = lm.itemCount
if (itemCount == 0) return RecyclerView.NO_POSITION

// 中心点以前距离最近的View
var closestChildBeforeCenter: View? = null
var distanceBefore = Int.MIN_VALUE // 中心点以前,距离为负数
// 中心点以后距离最近的View
var closestChildAfterCenter: View? = null
var distanceAfter = Int.MAX_VALUE // 中心点以后,距离为正数
val containerCenter = helper.startAfterPadding + helper.totalSpace / 2

val childCount: Int = lm.childCount
for (i in 0 until childCount) {
val child = lm.getChildAt(i) ?: continue
if (consumeSnap(i, child)) return RecyclerView.NO_POSITION // consumeSnap 默认返回false,竖直滑动模式才使用

val childCenter = (helper.getDecoratedStart(child)
+ helper.getDecoratedMeasurement(child) / 2)
val distance = childCenter - containerCenter

// Fling需要考虑方向,先获取两个方向最近的View
if (distance in (distanceBefore + 1)..0) {
distanceBefore = distance
closestChildBeforeCenter = child
}
if (distance in 0 until distanceAfter) {
distanceAfter = distance
closestChildAfterCenter = child
}
}

// 根据方向选择Fling到哪个View
val forwardDirection = velocity > 0
if (forwardDirection && closestChildAfterCenter != null) {
return lm.getPosition(closestChildAfterCenter)
} else if (!forwardDirection && closestChildBeforeCenter != null) {
return lm.getPosition(closestChildBeforeCenter)
}

// 边界情况处理
val visibleView =
(if (forwardDirection) closestChildBeforeCenter else closestChildAfterCenter)
?: return RecyclerView.NO_POSITION
val visiblePosition: Int = lm.getPosition(visibleView)
val snapToPosition = (visiblePosition - 1)

return if (snapToPosition < 0 || snapToPosition >= itemCount) {
RecyclerView.NO_POSITION
} else snapToPosition
}

②、使用RecyclerView的「LinearSmoothScroller」完成吸附动画


private fun createScroller(
oh: OrientationHelper
)
: LinearSmoothScroller {
return object : LinearSmoothScroller(mRecyclerView.context) {
override fun onTargetFound(
targetView: View,
state: RecyclerView.State,
action: Action
)
{
val d = distanceToCenter(targetView, oh)
val time = calculateTimeForDeceleration(abs(d))
if (time > 0) {
when (orientation) {
VERTICAL -> action.update(0, d, time, mDecelerateInterpolator)
HORIZONTAL -> action.update(d, 0, time, mDecelerateInterpolator)
}
}
}

override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics) =
100f / displayMetrics.densityDpi

override fun calculateTimeForScrolling(dx: Int) =
100.coerceAtMost(super.calculateTimeForScrolling(dx))
}
}

protected fun distanceToCenter(targetView: View, helper: OrientationHelper): Int {
val childCenter = (helper.getDecoratedStart(targetView)
+ helper.getDecoratedMeasurement(targetView) / 2)
val containerCenter = helper.startAfterPadding + helper.totalSpace / 2
return childCenter - containerCenter
}

完整操作:


protected fun snapFromFling(
lm: RecyclerView.LayoutManager,
velocity: Int,
helper: OrientationHelper
)
: Pair<Boolean, Int> {
val targetPosition = findTargetSnapPosition(lm, velocity, helper)
if (targetPosition == RecyclerView.NO_POSITION) return Pair(false, 0)
val smoothScroller = createScroller(helper)
smoothScroller.targetPosition = targetPosition
lm.startSmoothScroll(smoothScroller)
return Pair(true, targetPosition) // 消费fling
}

2、覆盖效果实现


(1)如果使用PageTransform实现


如果使用ViewPagerPageTransform,是可以实现覆盖动画的,实现思路:使可见View的第二个View跟随屏幕滑动


image.png


假设上图蓝色透明矩形为屏幕,其他为ItemView,图片上半部分正常滑动的状态,下半部分为 translate view 之后的状态。可以看到,在横向滑动过程中,最多可见2个View(蓝色透明方框最多覆盖2个View),此时将第二个View跟随屏幕,其他View保持跟随画布滑动,即可达到效果。在OnPageScroll回调中实现这个逻辑:


for (i in 0 until layoutManager.childCount) {
layoutManager.getChildAt(i)?.also { view ->
if (i == 1) {
// view.left是个负数,offsetPx(=-view.left)是个正数
view.translationX = offsetPx.toFloat() - view.width // 需要translate的距离(向前移需要负数)
} else {
// 恢复其余位置的translate
view.translationX = 0f
}
}
}

(2)扩展RecyclerView实现覆盖翻页


知道如何通过 PageTransfrom 实现后,我们来看看直接使用 RecyclerView 如何实现。观看ViewPager2源码可知PageTransfrom的实现方式


image.png


故我们直接copy代码,在OnScrollListener中自行实现onPageScrolled回调即可实现覆盖翻页效果。


但是此时还有一个问题,就是子View的层级问题,你会发现上面的滑动示意图中,绿色View会在黄色View之上,如何解决这个问题呢?我们需要控制View的绘制顺序,前面的View后绘制,保证前面地View在后面的View的绘制层级之上。


观看源码会发现,RecyclerView其实提供了一个回调ChildDrawingOrderCallback,可以很方便地实现这个效果:


override fun attach() {
super.attach()
mRecyclerView.setChildDrawingOrderCallback(this)
}

override fun onGetChildDrawingOrder(childCount: Int, i: Int) = childCount - i - 1 // 反向绘制

三、竖直滑动(Scroll Mode)


垂直.gif


竖直滑动需要滑动到跨章的位置时才吸附(自动回滚到指定位置),需要实现两个效果:跨章吸附、跨章Fling阻断。我们可以在横向覆盖滑动(Slide Mode)的基础上做一个减法,首先将LayoutManager改为竖向的,然后实现上述两个效果。


1、跨章吸附


实现跨章吸附,我们先在 RecyclerViewAdapter 中对每个View进行一个标记:


companion object {
const val TYPE_NONE = 100 // 其他
const val TYPE_FIRST_PAGE = 101 // 首页
const val TYPE_LAST_PAGE = 102 // 末页
}


fun bind() { // onBindViewHolder 时调用
itemView.tag = when {
textPage.isLastPage -> TYPE_LAST_PAGE
textPage.isFirstPage -> TYPE_FIRST_PAGE
else -> TYPE_NONE
}
......
}

其次我们实现横向覆盖滑动(Slide Mode)中的一段代码(做一个减法):


// 如果不是章节的最后一页,则消费Snap(不进行吸附操作)
override fun consumeSnap(index: Int, child: View) =
index == 0 && child.tag != ReadBookAdapter.TYPE_LAST_PAGE

这样就可以实现不是跨越章节的翻页不进行吸附,而跨越章节的滑动会自动吸附。


2、跨章Fling阻断


在滑动过程中,基于可见View只有两个的情况:



  • 如果向上滑动,判断第一个可见View是否「末页」,如果是,smoothScroll到第二个可见View

  • 如果向下滑动,判断第二个可见View是否「首页」,如果是,smoothScroll到第一个可见View


private var inFling = false     // 正在fling,在OnFlingListener中设置为true
private var inBlocking = false // 阻断fling


override val mScrollListener = object : RecyclerView.OnScrollListener() {
var mScrolled = false

override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
when (newState) {
RecyclerView.SCROLL_STATE_DRAGGING -> {
inFling = false // 重置inFling
}
RecyclerView.SCROLL_STATE_IDLE -> {
inFling = false // 重置inFling
if (inBlocking) {
inBlocking = false // 忽略阻断造成的IDLE
} else if (mScrolled) {
mScrolled = false
snapToTargetExistingView(orientationHelper.value)
}
}
}
}

override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (dy != 0) {
if (!mScrolled) {
this@VSnapHelper.mCallback.onScrollBegin()
mScrolled = true
}
val lm = mRecyclerView.layoutManager ?: return
// fling阻断
if (inFling && !inBlocking) {
val child: View?
val type: Int
if (dy > 0) { // 向上滑动
child = lm.getChildAt(0)
type = ReadBookAdapter.TYPE_LAST_PAGE
} else {
child = lm.getChildAt(lm.childCount - 1)
type = ReadBookAdapter.TYPE_FIRST_PAGE
}
child?.let {
if (it.tag == type) {
inBlocking = true
val d = distanceToCenter(it, orientationHelper.value)
mRecyclerView.smoothScrollBy(0, d)
}
}
}
}
}
}

四、仿真页(Flip Mode)


仿真.gif


仿真页在横向覆盖滑动(Slide Mode)基础之上实现,我们还需要实现:



  1. 确认手指滑动方向

  2. 所有可见View都跟随屏幕

  3. 绘制次序根据拖拽方向改变,保证目标页在当前页之上

  4. 绘制仿真页

  5. 手指抬起后的翻页动画(确认Fling、Scroll Idle产生的两种Snap的方向,因为手指会来回滑动导致方向判断错误)


1、确认手指滑动方向


滑动方向不能直接在 onTouchdispatchTouchEvent 这些方法中直接判断,
因为极微小的滑动都会决定方向,这样会造成轻微触碰就判定了方向,导致页面内容闪动、抖动等问题。
我们需要在滑动了一定距离后确定方向,最好的选择就是在 onPageScroll 中进行判断,系统为我们保证了ScrollState已变为DRAGGING,此时用户100%已经在滑动。可以看下源码真正触发「onPageScroll」的条件有哪些


image.png


我们实现的判断方向的代码:


// 在onScrolled中调用
// mCurrentItem:onPageSelected中赋值,代表当前Item
// position:第一个可见View的位置
// offsetPx:第一个可见View的left取负
// mForward:方向,true为画布向左滑动(向尾部滑动),false画布向右滑动(向头部滑动)
private fun dispatchScrolled(position: Int, offsetPx: Int) {
if (mScrollState == RecyclerView.SCROLL_STATE_DRAGGING) {
mForward = mCurrentItem == position
}
mCallback.onPageScrolled(position, mCurrentItem, offsetPx, mForward)
}

image.png


不过这个规则在超快速滑动时会判断错误,即settling直接变dragging的时候,所以会对滑动做一点限制


override fun dispatchTouchEvent(e: MotionEvent): Boolean {
if (snapHelper.mScrollState == RecyclerView.SCROLL_STATE_SETTLING) {
return true // sellting过程中禁止滑动
}
delegate.onTouch(e)
return super.dispatchTouchEvent(e)
}

2、遮盖效果


所有可见View都跟随屏幕,横向覆盖滑动(Slide Mode)的增强版,因为给 RecyclerView设置了 offScreenLimit=1 的效果,所以 LayoutManagerchild 数量最多会有4个
(参照 ViewPager2 # LinearLayoutManagerImpl 实现,这里设置是为了滑动时可以第一时间生成目标页的截图)


// onPageScrolled中调用
private fun transform(offsetPx: Int, firstVisible: Int) {
val count = layoutManager.childCount
if (count == 2 || (count == 3 && offsetPx == 0)) {
// 可见View只有一个的时候,全部复位
for (i in 0 until count) {
layoutManager.getChildAt(i)?.also { view ->
view.translationX = 0f
}
}
} else {
var target = 1
if (count == 3 && firstVisible == 0) target-- // 首位适配,currentItem=0且存在滑动的时候
for (i in 0 until layoutManager.childCount) {
layoutManager.getChildAt(i)?.also { view ->
when (i) {
target -> view.translationX = offsetPx.toFloat()
target + 1 -> view.translationX = offsetPx.toFloat() - view.width
else -> view.translationX = 0f
}
}
}
}
}

3、绘制次序根据拖拽方向改变


保证目标页在当前页之上,防止绘制的仿真页消失时出现闪屏(瞬间显示了不正确的页)


// 画布左移则反向绘制,右移则正想绘制
override fun getDrawingOrder(childCount: Int, i: Int) =
if (snapHelper.mForward) childCount - i - 1 else i

4、绘制仿真页


我们在 RecyclerView 的父View上直接覆盖绘制一层仿真页Bitmap


(1)生成截图


如上面所说,实现了 offScreenLimit=1 的效果,我们在首次获取到方向时生成截图:


// 生成截图方法
fun View.screenshot(): Bitmap? {
return runCatching {
val screenshot = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
val c = Canvas(screenshot)
c.translate(-scrollX.toFloat(), -scrollY.toFloat())
draw(c)
screenshot
}.getOrNull()
}
private var isBeginDrag = false

override fun onPageStateChange(state: Int) {
when (state) {
RecyclerView.SCROLL_STATE_DRAGGING -> {
isBeginDrag = true
}
}
}

override fun onPageScrolled(firstVisible: Int, current: Int, offsetPx: Int, forward: Boolean) {
if (isBeginDrag) {
isBeginDrag = false
delegate.apply {
if (forward) {
nextBitmap?.recycle()
nextBitmap = layoutManager.findViewByPosition(current + 1)?.screenshot()
curBitmap?.recycle()
curBitmap = layoutManager.findViewByPosition(current)?.screenshot()
} else {
prevBitmap?.recycle()
prevBitmap = layoutManager.findViewByPosition(current - 1)?.screenshot()
curBitmap?.recycle()
curBitmap = layoutManager.findViewByPosition(current)?.screenshot()
}
setDirection(if (forward) AnimDirection.NEXT else AnimDirection.PREV)
}
invalidate()
}
}

(2)绘制仿真页


绘制仿真页参考 gedoor/legadoSimulationPageDelegate



  • 基础知识:三角函数、Android的矩阵、贝塞尔曲线、canvas.clipPath的 XOR & INTERSECT 模式

  • 绘制方法:Android仿真翻页:cnblogs.com

  • 计算方法:使用手指触摸点和触摸点对应的角位置(比如触摸点靠近右下角,角位置就是右下角),这两个点可以算出所有参数


确认方向后,我们只用通过修改手指触碰点的参数即可控制整个动画(根据点击位置实时计算即可)


5、动画控制


手指抬起后的翻页动画通过 Scroller+invalidate实现


override fun computeScroll() {
if (scroller.computeScrollOffset()) {
setTouchPoint(scroller.currX.toFloat(), scroller.currY.toFloat())
} else if (isStarted) {
stopScroll()
}
}

对于FlingScroll Idle产生的吸附效果,我们需要各自回调方向:


// 选中时开始动画,此时position改变
override fun onPageSelected(position: Int) {
val page = adapter.data[position]
ReadBook.onPageChange(page)
if (canDraw) {
delegate.onAnimStart(300, false)
}
}

// position未改变的情况
override fun onSnap(isFling: Boolean, forward: Boolean, changePosition: Boolean) {
if (!changePosition) {
delegate.onAnimStart(
300,
true,
// 未改变方向,向前则播放向后动画
if (forward) AnimDirection.PREV else AnimDirection.NEXT
)
}
}

Scroll Idle通过 SmoothScroll 所需要滑动的距离正负判断方向:


// Scroll
override fun snapToTargetExistingView(helper: OrientationHelper): Pair<Int, Int>? {
mSnapping = true
super.snapToTargetExistingView(helper)?.also {
// first为滑动距离,second为目标Item的position
mCallback.onSnap(false, it.first > 0, mCurrentItem != it.second)
return it
}
return null
}

// Fling
override val mFlingListener = object : RecyclerView.OnFlingListener() {
override fun onFling(velocityX: Int, velocityY: Int): Boolean {
val lm = mRecyclerView.layoutManager ?: return false
mRecyclerView.adapter ?: return false
val minFlingVelocity = mRecyclerView.minFlingVelocity
val result = snapFromFling(
lm,
velocityX,
orientationHelper.value
)
val consume = abs(velocityX) > minFlingVelocity && result.first
if (consume) {
mSnapping = true
// second为目标Item的position,这里直接通过速度正负来判断方向
mCallback.onSnap(true, velocityX > 0, result.second != mCurrentItem)
}
return consume
}
}

(以上为所有关键点,只截取了部分代码,提供一个思路)


作者:尉迟涛
来源:juejin.cn/post/7244819106343829564
收起阅读 »

值得使用Lambda的8个场景,别再排斥它了!

前言 可能对不少人来说,Lambda显得陌生又复杂,觉得Lambda会导致代码可读性下降,诟病Lambda语法,甚至排斥。 其实所有的这些问题,在尝试并熟悉后,可能都不是问题。 对Lambda持怀疑态度的人,也许可以采取渐进式使用Lambda的策略。在一些简单...
继续阅读 »

前言


可能对不少人来说,Lambda显得陌生又复杂,觉得Lambda会导致代码可读性下降,诟病Lambda语法,甚至排斥。


其实所有的这些问题,在尝试并熟悉后,可能都不是问题。


对Lambda持怀疑态度的人,也许可以采取渐进式使用Lambda的策略。在一些简单和低风险的场景下先尝试使用Lambda,逐渐增加Lambda表达式的使用频率和范围。


毕竟2023年了,JDK都出了那么多新版本,是时候试试Lambda了!


耐心看完,你一定有所收获。


giphy.gif


正文


1. 对集合进行遍历和筛选:


使用Lambda表达式结合Stream API可以在更少的代码量下实现集合的遍历和筛选,更加简洁和易读。


原来的写法:


List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
for (Integer num : numbers) {
if (num % 2 == 0) {
System.out.println(num);
}
}

优化的Lambda写法:


List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.stream()
.filter(num -> num % 2 == 0)
.forEach(System.out::println);

2. 对集合元素进行排序:


使用Lambda表达式可以将排序逻辑以更紧凑的形式传递给sort方法,使代码更加简洁。


原来的写法:


List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
Collections.sort(names, new Comparator<String>() {
public int compare(String name1, String name2) {
return name1.compareTo(name2);
}
});

优化的Lambda写法:


List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
names.sort((name1, name2) -> name1.compareTo(name2));

3. 集合的聚合操作:


Lambda表达式结合Stream API可以更优雅地实现对集合元素的聚合操作,例如求和、求平均值等。


原来的写法:


List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = 0;
for (Integer num : numbers) {
sum += num;
}

优化的Lambda写法:


List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.reduce(0, Integer::sum);

4. 条件过滤和默认值设置:


使用Lambda的Optional类可以更加优雅地处理条件过滤和默认值设置的逻辑。


原来的写法:


String name = "Alice";
if (name != null && name.length() > 0) {
System.out.println("Hello, " + name);
} else {
System.out.println("Hello, Stranger");
}

Lambda写法:


String name = "Alice";
name = Optional.ofNullable(name)
.filter(n -> n.length() > 0)
.orElse("Stranger");
System.out.println("Hello, " + name);

5. 简化匿名内部类:


可以简化代码,同时提高代码可读性。


举个创建Thread的例子,传统方式使用匿名内部类来实现线程,语法较为冗长,而Lambda表达式可以以更简洁的方式达到相同的效果。


原来的写法:


new Thread(new Runnable() {
public void run() {
System.out.println("Thread is running.");
}
}).start();

Lambda写法:


new Thread(() -> System.out.println("Thread is running.")).start();

new Thread(() -> {
// 做点什么
}).start();

这种写法也常用于简化回调函数,再举个例子:


假设我们有一个简单的接口叫做Calculator,它定义了一个单一的方法calculate(int a, int b)来执行数学运算:


// @FunctionalInterface: 标识接口是函数式接口,只包含一个抽象方法,从而能够使用Lambda表达式来实现接口的实例化
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
}

现在,让我们创建一个名为CallbackExample的类。该类有一个名为operate的方法,它接受两个整数和一个Calculator接口作为参数。该方法将使用提供的Calculator接口执行计算并返回结果:


public class CallbackExample {

public static int operate(int a, int b, Calculator calculator) {
return calculator.calculate(a, b);
}

public static void main(String[] args) {
int num1 = 10;
int num2 = 5;

// 使用Lambda作为回调
int sum = operate(num1, num2, (x, y) -> x + y);
int difference = operate(num1, num2, (x, y) -> x - y);
int product = operate(num1, num2, (x, y) -> x * y);
int division = operate(num1, num2, (x, y) -> x / y);

System.out.println("Sum: " + sum);
System.out.println("Difference: " + difference);
System.out.println("Product: " + product);
System.out.println("Division: " + division);
}
}

通过在方法调用中直接定义计算的行为,我们不再需要为每个运算创建多个实现Calculator接口的类,使得代码更加简洁和易读


giphy (1).gif


6. 集合元素的转换:


使用Lambda的map方法可以更优雅地对集合元素进行转换,提高代码的可读性


原来的写法:


List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<String> uppercaseNames = new ArrayList<>();
for (String name : names) {
uppercaseNames.add(name.toUpperCase());
}

Lambda写法:


List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<String> uppercaseNames = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());

7. 对集合进行分组和统计:


以更紧凑的形式传递分组和统计的逻辑,避免了繁琐的匿名内部类的声明和实现。


通过groupingBy、counting、summingInt等方法,使得代码更加流畅、直观且优雅。


传统写法:



List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Amy", "Diana");

// 对名字长度进行分组
Map<Integer, List<String>> namesByLength = new HashMap<>();
for (String name : names) {
int length = name.length();
if (!namesByLength.containsKey(length)) {
namesByLength.put(length, new ArrayList<>());
}
namesByLength.get(length).add(name);
}
System.out.println("Names grouped by length: " + namesByLength);

// 统计名字中包含字母'A'的个数
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Amy", "Diana");
int namesWithA = 0;
for (String name : names) {
if (name.contains("A")) {
namesWithA++;
}
}
System.out.println("Number of names containing 'A': " + namesWithA);

Lambda写法:


List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Amy", "Diana");

// 使用Lambda表达式对名字长度进行分组
Map<Integer, List<String>> namesByLength = names.stream()
.collect(Collectors.groupingBy(String::length));
System.out.println("Names grouped by length: " + namesByLength);

// 使用Lambda表达式统计名字中包含字母'A'的个数
long namesWithA = names.stream()
.filter(name -> name.contains("A"))
.count();
System.out.println("Number of names containing 'A': " + namesWithA);

8. 对大数据量集合的并行处理


当集合的数据量很大时,通过Lambda结合Stream API可以方便地进行并行处理,充分利用多核处理器的优势,提高程序的执行效率。


假设我们有一个包含一百万个整数的列表,我们想要计算这些整数的平均值:


import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;

public class ParallelStreamExample {
public static void main(String[] args) {
// 创建一个包含一百万个随机整数的列表
List<Integer> numbers = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
numbers.add(ThreadLocalRandom.current().nextInt(100));
}

// 顺序流的处理
long startTimeSeq = System.currentTimeMillis();
double averageSequential = numbers.stream()
.mapToInt(Integer::intValue)
.average()
.getAsDouble();
long endTimeSeq = System.currentTimeMillis();
System.out.println("Sequential Average: " + averageSequential);
System.out.println("Time taken (Sequential): " + (endTimeSeq - startTimeSeq) + "ms");

// 并行流的处理
long startTimePar = System.currentTimeMillis();
double averageParallel = numbers.parallelStream()
.mapToInt(Integer::intValue)
.average()
.getAsDouble();
long endTimePar = System.currentTimeMillis();
System.out.println("Parallel Average: " + averageParallel);
System.out.println("Time taken (Parallel): " + (endTimePar - startTimePar) + "ms");
}
}

分别使用顺序流和并行流来计算列表中整数的平均值:



  • 顺序流:通过stream()方法获取流,使用mapToInt将Integer转换为int,然后使用average()方法计算平均值

  • 并行流:使用parallelStream()方法获取并行流,其他步骤与顺序流相同


查看输出结果:


Sequential Average: 49.517461
Time taken (Sequential): 10ms
Parallel Average: 49.517461
Time taken (Parallel): 3ms

可以看出,顺序流和并行流得到了相同的平均值,但并行流的处理时间明显少于顺序流。因为并行流能够将任务拆分成多个小任务,并在多个处理器核心上同时执行这些任务。


当然并行流也有缺点:



  • 对于较小的数据集,可能并行流更慢

  • 数据处理本身的开销较大,比如复杂计算、大量IO操作、网络通信等,可能并行流更慢

  • 可能引发线程安全问题


收尾


Lambda的使用场景远不止这些,在多线程、文件操作等场景中也都能灵活运用,一旦熟悉后可以让代码更简洁,实现精准而优雅的编程。


写代码时,改变偏见需要我们勇于尝试和付诸行动。有时候,我们可能会对某种编程语言、框架或设计模式持有偏见,认为它们不适合或不好用。但是,只有尝试去了解和实践,我们才能真正知道它们的优点和缺点。


当我们愿意打破旧有的观念,敢于尝试新的技术和方法时,我们就有机会发现新的可能性和解决问题的新途径。不要害怕失败或犯错,因为每一次尝试都是我们成长和进步的机会。


只要我们保持开放的心态,不断学习和尝试,我们就能够超越偏见,创造出更优秀的代码和解决方案。


所以,让我们在编程的路上,积极地去挑战和改变偏见。用行动去证明,只有不断地尝试,我们才能取得更大的进步和成功。让我们敢于迈出第一步,勇往直前,一同创造出更美好的编程世界!


ab4cb34agy1g4sgjkrgxlj20j60ahgm2.jpg


作者:一只叫煤球的猫
来源:juejin.cn/post/7262737716852473914
收起阅读 »

Android - 你可能需要这样一个日志库

前言 目前大多数库api设计都是Log.d("tag", "msg")这种风格,而且支持自定义日志存储的比较少, 所以作者想自己造一个轮子。 这种api风格有什么不好呢? 首先,它的tag是一个字符串,需要开发人员严格管理tag,要不然可能各种硬编码的tag满...
继续阅读 »

前言


目前大多数库api设计都是Log.d("tag", "msg")这种风格,而且支持自定义日志存储的比较少,
所以作者想自己造一个轮子。


这种api风格有什么不好呢?


首先,它的tag是一个字符串,需要开发人员严格管理tag,要不然可能各种硬编码的tag满天飞。


另外,它也可能导致性能陷阱,假设有这么一段代码:


// 打印一个List
Log.d("tag", list.joinToString())

此处使用Debug打印日志,生产模式下调高日志等级,不打印这一行日志,但是list.joinToString()这一行代码仍然会被执行,有可能导致性能问题。


下文会分析作者期望的api是什么样的,本文演示代码都是用kotlin,库中好用的api也是基于kotlin特性来实现的。


作者写库有个习惯,对外开放的类或者全局方法都会加一个前缀f,一个是为了避免命名冲突,另一个是为了方便代码检索,以下文章中会出现,这里做一下解释。


期望


什么样的api才能解决上面的问题呢?我们看一下方法的签名和打印方式


inline fun <reified T : FLogger> flogD(block: () -> Any)

interface AppLogger : FLogger

flogD {
list.joinToString { it }
}

flogD方法打印Debug日志,传一个Flogger的子类AppLogger作为日志标识,同时传一个block来返回要打印的日志内容。


日志标识是一个类或者接口,所以管理方式比较简单不会造成tag混乱的问题,默认tag是日志标识类的短类名。生产模式下调高日志等级后,block就不会被执行了,避免了可能的性能问题。


实现分析


日志库的完整实现已经写好了,放在这里xlog



  • 支持限制日志大小,例如限制每天只能写入10MB的日志

  • 支持自定义日志格式

  • 支持自定义日志存储,即如何持久化日志


这一节主要分析一下实现过程中遇到的问题。


问题:如果App运行期间日志文件被意外删除了,怎么处理?


在Android中,用java.io的api对一个文件进行写入,如果文件被删除,继续写入的话不会抛异常,这样会导致日志丢失,该如何解决?


有同学说,在写入之前先检查文件是否存在,如果存在就继续写入,不存在就创建后写入。


检查一个文件是否存在通常是调用java.io.File.exist()方法,但是它比较耗性能,我们来做一个测试:


measureTime {
repeat(1_0000) {
file.exists()
}
}.let {
Log.i("MainActivity", "time:${it.inWholeMilliseconds}")
}

14:50:33.536 MainActivity            com.sd.demo.xlog                I  time:39
14:50:35.872 MainActivity com.sd.demo.xlog I time:54
14:50:38.200 MainActivity com.sd.demo.xlog I time:43
14:50:40.028 MainActivity com.sd.demo.xlog I time:53
14:50:41.693 MainActivity com.sd.demo.xlog I time:58

可以看到1万次调用的耗时在50毫秒左右。


我们再测试一下对文件写入的耗时:


val output = filesDir.resolve("log.txt").outputStream().buffered()
val log = "1".repeat(50).toByteArray()
measureTime {
repeat(1_0000) {
output.write(log)
output.flush()
}
}.let {
Log.i("MainActivity", "time:${it.inWholeMilliseconds}")
}

14:57:56.092 MainActivity            com.sd.demo.xlog                I  time:38
14:57:56.558 MainActivity com.sd.demo.xlog I time:57
14:57:57.129 MainActivity com.sd.demo.xlog I time:57
14:57:57.559 MainActivity com.sd.demo.xlog I time:46
14:57:58.054 MainActivity com.sd.demo.xlog I time:54

可以看到1万次调用,每次写入50个字符的耗时也在50毫秒左右。如果每次写入日志前都判断一下文件是否存在,那么实际上相当于2次写入的性能成本,这显然很不划算。


还有同学说,开一个线程,定时判断文件是否存在,这样子虽然不会损耗单次写入的性能,但是又多占用了一个线程资源,显然也不符合作者的需求。


其实Android已经给我们提供了这种场景的解决方案,那就是android.os.MessageQueue.IdleHandler,关于IdleHandler这里就不展开讨论了,简单来说就是当你在主线程注册一个IdleHandler后,它会在主线程空闲的时候被执行。


我们可以在每次写入日志之后注册IdleHandler,等IdleHandler被执行的时候检查一下日志文件是否存在,如果不存在就关闭输出流,这样子在下一次写入的时候就会重新创建文件写入了。


这里要注意每次写入日志之后注册IdleHandler,并不是每次都创建新对象,要判断一下如果原先的对象还未执行的话就不用注册一个新的IdleHandler,库中大概的代码如下:


private class LogFileChecker(private val block: () -> Unit) {
private var _idleHandler: IdleHandler? = null

fun register(): Boolean {
// 如果当前线程没有Looper则不注册,上层逻辑可以直接检查文件是否存在,因为是非主线程
Looper.myLooper() ?: return false

// 如果已经注册过了,直接返回
_idleHandler?.let { return true }

val idleHandler = IdleHandler {
// 执行block检查任务
libTryRun { block() }

// 重置变量,等待下次注册
_idleHandler = null
false
}

// 保存并注册idleHandler
_idleHandler = idleHandler
Looper.myQueue().addIdleHandler(idleHandler)
return true
}
}

这样子文件被意外删除之后,就可以重新创建写入了,避免丢失大量的日志。


问题:如何检测文件大小是否溢出


库支持对每天的日志大小做限制,例如限制每天最多只能写入10MB,每次写入日志之后都会检查日志大小是否超过限制,通常我们会调用java.io.File.length()方法获取文件的大小,但是它也比较耗性能,我们来做一个测试:


val file = filesDir.resolve("log.txt").apply {
this.writeText("hello")
}
measureTime {
repeat(1_0000) {
file.length()
}
}.let {
Log.i("MainActivity", "time:${it.inWholeMilliseconds}")
}

16:56:04.090 MainActivity            com.sd.demo.xlog                I  time:61
16:56:05.329 MainActivity com.sd.demo.xlog I time:80
16:56:06.382 MainActivity com.sd.demo.xlog I time:72
16:56:07.496 MainActivity com.sd.demo.xlog I time:79
16:56:08.591 MainActivity com.sd.demo.xlog I time:78

可以看到耗时在60毫秒左右,相当于上面测试中1次文件写入的耗时。


库中支持自定义日志存储,在日志存储接口中定义了size()方法,上层通过此方法来判断当前日志的大小。


如果自定义了日志存储,避免在此方法中每次调用java.io.File.length()来返回日志大小,应该维护一个表示日志大小的变量,变量初始化的时候获取一下java.io.File.length(),后续通过写入的数量来增加这个变量的值,并在size()方法中返回。库中默认的日志存储实现类就是这样实现的,有兴趣的可以看这里


问题:文件大小溢出后怎么处理?


假设限制每天最多只能写入10MB,那超过10MB后如何处理?有同学说直接删掉或者清空文件,重新写入,这也是一种策略,但是会丢失之前的所有日志。


例如白天写了9.9MB,到晚上的时候写满10MB,清空之后,白天的日志都没了,这时候用户反馈白天遇到的一个bug,需要上传日志,那就芭比Q了。


有没有办法少丢失一些呢?可以把日志分多个文件存储,为了便于理解假设分为2个文件存储,一天10MB,那1个文件最多只能写入5MB。具体步骤如下:



  1. 写入文件20231128.log

  2. 20231128.log写满5MB的时候关闭输出流,并把它重命名为20231128.log.1


这时候继续写日志的话,发现20231128.log文件不存在就会创建,又跳到了步骤1,就这样一直重复1和2两个步骤,到晚上写满10MB的时候,至少还有5MB的日志内容保存在20231128.log.1文件中避免丢失全部的日志。


分的文件数量越多,保留的日志就越多,实际上就是拿出一部分空间当作中转区,满了就向后递增数字重命名备份。目前库中只分为2个文件存储,暂时不开放自定义文件数量。


问题:打印日志的性能


性能,是这个库最关心的问题,通常来说文件写入操作是性能开销的大头,目前是用java.io相关的api来实现的,怎样提高写入性能作者也一直在探索,在demo中提供了一个基于内存映射的日志存储方案,但是稳定性未经测试,后续测试通过后可能会转正。有兴趣的读者可以看看这里


还有一个比较影响性能的就是日志的格式化,通常要把一个时间戳转为某个日期格式,大部分人都会用java.text.SimpleDateFormat来格式化,用它来格式化年:月:日的时候问题不大,但是如果要格式化时:分:秒.毫秒那它就比较耗性能,我们来做一个测试:


val format = SimpleDateFormat("HH:mm:ss.SSS")
val millis = System.currentTimeMillis()
measureTime {
repeat(1_0000) {
format.format(millis)
}
}.let {
Log.i("MainActivity", "time:${it.inWholeMilliseconds}")
}

16:05:26.920 MainActivity            com.sd.demo.xlog                I  time:245
16:05:27.586 MainActivity com.sd.demo.xlog I time:227
16:05:28.324 MainActivity com.sd.demo.xlog I time:212
16:05:29.370 MainActivity com.sd.demo.xlog I time:217
16:05:30.157 MainActivity com.sd.demo.xlog I time:193

可以看到1万次格式化耗时大概在200毫秒左右。


我们再用java.util.Calendar测试一下:


val calendar = Calendar.getInstance()
// 时间戳1
val millis1 = System.currentTimeMillis()
// 时间戳2
val millis2 = millis1 + 1000
// 切换时间戳标志
var flag = true
measureTime {
repeat(1_0000) {
calendar.timeInMillis = if (flag) millis1 else millis2
calendar.run {
"${get(Calendar.HOUR_OF_DAY)}:${get(Calendar.MINUTE)}:${get(Calendar.SECOND)}.${get(Calendar.MILLISECOND)}"
}
flag = !flag
}
}.let {
Log.i("MainActivity", "time:${it.inWholeMilliseconds}")
}

16:11:25.342 MainActivity            com.sd.demo.xlog                I  time:35
16:11:26.209 MainActivity com.sd.demo.xlog I time:35
16:11:27.316 MainActivity com.sd.demo.xlog I time:37
16:11:28.057 MainActivity com.sd.demo.xlog I time:25
16:11:28.825 MainActivity com.sd.demo.xlog I time:18


这里解释一下为什么要用两个时间戳,因为Calendar内部有缓存,如果用同一个时间戳测试的话,没办法评估它真正的性能,所以这里每次格式化之后就切换到另一个时间戳,避免缓存影响测试。


可以看到1万次的格式化耗时在30毫秒左右,差距很大。如果要自定义日志格式的话,建议用Calendar来格式化时间,有更好的方案欢迎和作者交流。


问题:日志的格式如何显示


手机的存储资源是宝贵的,如何定义日志格式也是一个比较重要的细节。



  • 优化时间显示


目前库内部是以天为单位来命名日志文件的,例如:20231128.log,所以在格式化时间戳的时候只保留了时:分:秒.毫秒,避免冗余显示当天的日期。



  • 优化日志等级显示


打印的时候提供了4个日志等级:Verbose, Debug, Info, Warning, Error,一般最常用的记录等级是Info,所以在格式化的时候如果等级是Info则不显示等级标志,规则如下:


private fun FLogLevel.displayName(): String {
return when (this) {
FLogLevel.Verbose -> "V"
FLogLevel.Debug -> "D"
FLogLevel.Warning -> "W"
FLogLevel.Error -> "E"
else -> ""
}
}


  • 优化日志标识显示


如果连续2条或多条日志都是同一个日志标识,那么就只有第1条日志会显示日志tag



  • 优化线程ID显示


如果是主线程的话,不显示线程ID,只有非主线程才显示线程ID


经过上面的优化之后,日志打印的格式是这样的:


flogI { "1" }
flogI { "2" }
flogW { "3" }
flogI { "user debug" }
thread {
flogI { "thread" }
}

19:19:43.961[AppLogger] 1
19:19:43.974 2
19:19:43.975[W] 3
19:19:43.976[UserLogger] user debug
19:19:43.977[12578] thread

API


这一节介绍一下库的API,调用FLog.init()方法初始化,初始化如果不想打印日志,可以调用FLog.setLevel(FLogLevel.Off)关闭日志


常用方法


// 初始化
FLog.init(
//(必传参数)日志文件目录
directory = filesDir.resolve("app_log"),

//(可选参数)自定义日志格式
formatter = AppLogFormatter(),

//(可选参数)自定义日志存储
storeFactory = AppLogStoreFactory(),

//(可选参数)是否异步发布日志,默认值false
async = false,
)

// 设置日志等级 All, Verbose, Debug, Info, Warning, Error, Off 默认日志等级:All
FLog.setLevel(FLogLevel.All)

// 限制每天日志文件大小(单位MB),小于等于0表示不限制大小,默认限制每天日志大小100MB
FLog.setLimitMBPerDay(100)

// 设置是否打打印控制台日志,默认打开
FLog.setConsoleLogEnabled(true)

/**
* 删除日志,参数saveDays表示要保留的日志天数,小于等于0表示删除全部日志,
* 此处saveDays=1表示保留1天的日志,即保留当天的日志
*/

FLog.deleteLog(1)

打印日志


interface AppLogger : FLogger

flogV { "Verbose" }
flogD { "Debug" }
flogI { "Info" }
flogW { "Warning" }
flogE { "Error" }

// 打印控制台日志,不会写入到文件中,不需要指定日志标识,tag:DebugLogger
fDebug { "console debug log" }

配置日志标识


可以通过FLog.config方法修改某个日志标识的配置信息,例如下面的代码:


FLog.config {
// 修改日志等级
this.level = FLogLevel.Debug

// 修改tag
this.tag = "AppLoggerAppLogger"
}

自定义日志格式


class AppLogFormatter : FLogFormatter {
override fun format(record: FLogRecord): String {
// 自定义日志格式
return record.msg
}
}

interface FLogRecord {
/** 日志标识 */
val logger: Class<out FLogger>

/** 日志tag */
val tag: String

/** 日志内容 */
val msg: String

/** 日志等级 */
val level: FLogLevel

/** 日志生成的时间戳 */
val millis: Long

/** 日志是否在主线程生成 */
val isMainThread: Boolean

/** 日志生成的线程ID */
val threadID: String
}

自定义日志存储


日志存储是通过FLogStore接口实现的,每一个FLogStore对象负责管理一个日志文件。
所以需要提供一个FLogStore.Factory工厂为每个日志文件提供FLogStore对象。


class AppLogStoreFactory : FLogStore.Factory {
override fun create(file: File): FLogStore {
return AppLogStore(file)
}
}

class AppLogStore(file: File) : FLogStore {
// 添加日志
override fun append(log: String) {}

// 返回当前日志的大小
override fun size(): Long = 0

// 关闭
override fun close() {}
}

结束


库目前还处于alpha阶段,如果有遇到问题可以及时反馈给作者,最后感谢大家的阅读。


作者:Sunday1990
来源:juejin.cn/post/7306423214493270050
收起阅读 »

布局升级秘籍:掌握CSS Grid网格布局,打造响应式网页设计

随着现代网页设计的不断演进,传统的布局方式已经逐渐不能满足设计师和开发者们对于高效、灵活且强大布局系统的追求。而CSS Grid网格布局,正是在这样的背景下应运而生的。今天,我们就来深入探讨CSS Grid布局的魅力所在,带你解锁这项强大的设计工具,让网页布局...
继续阅读 »

随着现代网页设计的不断演进,传统的布局方式已经逐渐不能满足设计师和开发者们对于高效、灵活且强大布局系统的追求。而CSS Grid网格布局,正是在这样的背景下应运而生的。

今天,我们就来深入探讨CSS Grid布局的魅力所在,带你解锁这项强大的设计工具,让网页布局变得更加简单和高效。

一、什么是CSS Grid布局?

CSS Grid布局,简称为Grid,是CSS的一个二维布局系统,它能够处理行和列,使得网页布局变得更加直观和强大。与传统的布局方式相比,Grid能够轻松实现复杂的页面结构,而无需繁琐的浮动、定位或是使用多个嵌套容器。

Grid网格布局是一种基于网格的布局系统,它允许我们通过定义行和列的大小、位置和排列方式来创建复杂的网页布局。

Description

这与之前讲到的flex一维布局不相同。

设置display:grid/inline-grid的元素就是网格布局容器,这样就能触发浏览器渲染引擎的网格布局算法。

<div>
<div class="item item-1">
<p></p >
</div>
<div class="item item-2"></div>
<div class="item item-3"></div>
</div>

上述代码实例中,.container元素就是网格布局容器,.item元素就是网格的项目,由于网格元素只能是容器的顶层子元素,所以p元素并不是网格元素。

二、Grid的基本概念

首先,我们来了解一下CSS Grid布局的核心概念:

容器(Container):

设置了display: grid;的元素成为容器。它是由一组水平线和垂直线交叉构成,就如同我们所在的地区是由小区和各个路构成。

项目(Item):

容器内的直接子元素,称为项目。

网格线(Grid Lines):

划分行和列的线条,可以想象成坐标轴。正常情况下n行会有n+1根横向网格线,m列有m+1根纵向网格线。比如田字就好像是一个三条水平线和三条垂直线构成的网格元素。

Description

上图是一个 2 x 3 的网格,共有3根水平网格线和4根垂直网格线。

行:

即两个水平网格线之间的空间,也就是水平轨道,就好比我们面朝北边东西方向横向排列的楼房称为行。

列:

即两个垂直网格线之间的空间,也就是垂直轨道,也就是南北方向排列的楼房。

单元格:

由水平线和垂直线交叉构成的每个区域称为单元格,网络单元格是CSS网格中的最小单元。也就是说东西和南北方向的路交叉后划分出来的土地区域。

网格轨道(Grid Tracks):

两条相邻网格线之间的空间。

网格区域(Grid Area):

四条网格线围成的空间,可以是行或列。本质上,网格区域一定是矩形的。例如,不可能创建T形或L形的网格区域。

三、Grid的主要属性

CSS Grid网格布局的主要属性包括:

  • display:设置元素为网格容器或网格项。

  • grid-template-columns 和 grid-template-rows:用于定义网格的列和行的大小。

  • grid-column-gap 和 grid-row-gap:用于定义网格的列和行的间距。

  • grid-template-areas:用于定义命名区域,以便在网格中引用。

  • grid-auto-flow:用于控制网格项的排列方式,可以是行(row)或列(column)。

  • grid-auto-columns 和 grid-auto-rows:用于定义自动生成的列和行的大小。

  • grid-column-start、grid-column-end、grid-row-start 和 grid-row-end:用于定义网格项的位置。

  • justify-items、align-items 和 place-items:用于对齐网格项。

  • grid-template:一个复合属性,用于一次性定义多个网格布局属性。

下面将详细介绍这些属性的概念及作用:

3.1 display

通过给元素设置:display:grid | inline-grid,可以让一个元素变成网格布局元素。

语法:

display: grid | inline-grid;

display: grid:表示把元素定义为块级网格元素,单独占一行;

display:inline-grid:表示把元素定义为行内块级网格元素,可以和其他块级元素在同一行。

3.2 grid-template-columns和grid-template-rows

grid-template-columns和grid-template-rows:用于定义网格的列和行的大小。

  • grid-template-columns 属性设置列宽

  • grid-template-rows 属性设置行高

.wrapper {
display: grid;
/* 声明了三列,宽度分别为 200px 200px 200px */
grid-template-columns: 200px 200px 200px;
grid-gap: 5px;
/* 声明了两行,行高分别为 50px 50px */
grid-template-rows: 50px 50px;
}

以上表示固定列宽为 200px 200px 200px,行高为 50px 50px。

上述代码可以看到重复写单元格宽高,我们也可以通过使用repeat()函数来简写重复的值。

  • 第一个参数是重复的次数

  • 第二个参数是重复的值

所以上述代码可以简写成:

.wrapper {
display: grid;
grid-template-columns: repeat(3,200px);
grid-gap: 5px;
grid-template-rows:repeat(2,50px);
}

除了上述的repeact关键字,还有:

auto-fill: 表示自动填充,让一行(或者一列)中尽可能的容纳更多的单元格。

grid-template-columns: repeat(auto-fill, 200px)

表示列宽是 200 px,但列的数量是不固定的,只要浏览器能够容纳得下,就可以放置元素。

fr: 片段,为了方便表示比例关系。

grid-template-columns: 200px 1fr 2fr

表示第一个列宽设置为 200px,后面剩余的宽度分为两部分,宽度分别为剩余宽度的 1/3 和 2/3。

minmax: 产生一个长度范围,表示长度就在这个范围之中都可以应用到网格项目中。第一个参数就是最小值,第二个参数就是最大值。

minmax(100px, 1fr)

表示列宽不小于100px,不大于1fr。

auto: 由浏览器自己决定长度。

grid-template-columns: 100px auto 100px

表示第一第三列为 100px,中间由浏览器决定长度。

3.3 grid-row-gap 属性, grid-column-gap 属性, grid-gap 属性

grid-column-gap和grid-row-gap,用于定义网格的列间距和行间距。grid-gap 属性是两者的简写形式。

  • grid-row-gap: 10px 表示行间距是 10px

  • grid-column-gap: 20px 表示列间距是 20px

  • grid-gap: 10px 20px 等同上述两个属性

3.4 grid-auto-flow 属性

grid-auto-flow,用于控制网格项的排列方式,可以是行(row)或列(column)。

  • 划分网格以后,容器的子元素会按照顺序,自动放置在每一个网格。

  • 顺序就是由grid-auto-flow决定,默认为行,代表"先行后列",即先填满第一行,再开始放入第二行。

Description

当修改成column后,放置变为如下:

Description

3.5 justify-items 属性, align-items 属性, place-items 属性

justify-items、align-items和place-items,用于定义网格项目的对齐方式。

  • justify-items 属性设置单元格内容的水平位置(左中右)

  • align-items 属性设置单元格的垂直位置(上中下)

.container {
justify-items: start | end | center | stretch;
align-items: start | end | center | stretch;
}

属性对应如下:

  • start:对齐单元格的起始边缘

  • end:对齐单元格的结束边缘

  • center:单元格内部居中

  • stretch:拉伸,占满单元格的整个宽度(默认值)

  • place-items属性是align-items属性和justify-items属性的合并简写形式。

3.6 justify-content 属性, align-content 属性, place-content 属性

  • justify-content属性是整个内容区域在容器里面的水平位置(左中右)

  • align-content属性是整个内容区域的垂直位置(上中下)

.container {
justify-content: start | end | center | stretch | space-around | space-between | space-evenly;
align-content: start | end | center | stretch | space-around | space-between | space-evenly;
}

两个属性的写法完全相同,都可以取下面这些值:

  • start - 对齐容器的起始边框

  • end - 对齐容器的结束边框

  • center - 容器内部居中

Description

  • space-around - 每个项目两侧的间隔相等。所以,项目之间的间隔比项目与容器边框的间隔大一倍。

  • space-between - 项目与项目的间隔相等,项目与容器边框之间没有间隔。

  • space-evenly - 项目与项目的间隔相等,项目与容器边框之间也是同样长度的间隔。

  • stretch - 项目大小没有指定时,拉伸占据整个网格容器。

Description

3.7 grid-auto-columns 属性和 grid-auto-rows 属性

有时候,一些项目的指定位置,在现有网格的外部,就会产生显示网格和隐式网格。

比如网格只有3列,但是某一个项目指定在第5行。这时,浏览器会自动生成多余的网格,以便放置项目。超出的部分就是隐式网格。

而grid-auto-rows与grid-auto-columns就是专门用于指定隐式网格的宽高。

3.8 grid-column-start 属性、grid-column-end 属性、grid-row-start 属性以及grid-row-end 属性

指定网格项目所在的四个边框,分别定位在哪根网格线,从而指定项目的位置。

  • grid-column-start 属性:左边框所在的垂直网格线

  • grid-column-end 属性:右边框所在的垂直网格线

  • grid-row-start 属性:上边框所在的水平网格线

  • grid-row-end 属性:下边框所在的水平网格线

<style>
#container{
display: grid;
grid-template-columns: 100px 100px 100px;
grid-template-rows: 100px 100px 100px;
}
.item-1 {
grid-column-start: 2;
grid-column-end: 4;
}
</style>

<div id="container">
<div class="item item-1">1</div>
<div class="item item-2">2</div>
<div class="item item-3">3</div>
</div>

通过设置grid-column属性,指定1号项目的左边框是第二根垂直网格线,右边框是第四根垂直网格线。

Description

3.9 grid-area 属性

grid-area 属性指定项目放在哪一个区域。

.item-1 {
grid-area: e;
}

意思为将1号项目位于e区域

grid-area属性一般与上述讲到的grid-template-areas搭配使用。

想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!

点这里前往学习哦!

3.10 justify-self 属性、align-self 属性以及 place-self 属性

  • justify-self属性设置单元格内容的水平位置(左中右),跟justify-items属性的用法完全一致,但只作用于单个项目。

  • align-self属性设置单元格内容的垂直位置(上中下),跟align-items属性的用法完全一致,也是只作用于单个项目。

.item {
justify-self: start | end | center | stretch;
align-self: start | end | center | stretch;
}

这两个属性都可以取下面四个值。

  • start:对齐单元格的起始边缘。

  • end:对齐单元格的结束边缘。

  • center:单元格内部居中。

  • stretch:拉伸,占满单元格的整个宽度(默认值)

四、Grid网格布局应用场景

CSS Grid网格布局的应用场景非常广泛,包括但不限于:

创建复杂的网页布局:

CSS Grid网格布局可以轻松创建出复杂的网页布局,如多列布局、不规则布局等。

创建响应式设计:

CSS Grid网格布局可以轻松实现响应式设计,通过调整网格的大小和间距,可以适应不同的屏幕尺寸。

创建复杂的组件布局:

CSS Grid网格布局也可以用于创建复杂的组件布局,如卡片布局、轮播图布局等。

总的来说,CSS Grid网格布局是一种强大的布局工具,可以帮助网页设计者轻松创建出各种复杂的网页布局。

CSS Grid布局为我们提供了一个全新的视角来思考页面布局,它让复杂布局的实现变得简单明了。随着浏览器支持度的提高,未来的网页设计将更加灵活和富有创意。

掌握了CSS Grid布局,你就已经迈出了成为前端设计高手的重要一步。不断实践,不断探索,你会发现更多Grid的神奇之处。

收起阅读 »

腾讯云:颜面尽失的草台班子

昨天下午,2024年04月08日,腾讯云出现了一场全球性的大故障,用腾讯云官方的说法,崩了 74 分钟(15:31 - 16:45),波及全球 17 个区域与数十款服务。事实影响是什么但这与我观察到的事实不符 —— 从故障范围上来说,这次的故障几乎是去年阿里云...
继续阅读 »

昨天下午,2024年04月08日,腾讯云出现了一场全球性的大故障,用腾讯云官方的说法,崩了 74 分钟(15:31 - 16:45),波及全球 17 个区域与数十款服务。

事实影响是什么

但这与我观察到的事实不符 —— 从故障范围上来说,这次的故障几乎是去年阿里云双十一史诗级大故障的翻版 —— 小道消息是整个管控面 GG,云 API 挂了,所以现象与去年阿里云如出一辙:依赖云 API 的云产品控制台不能用了。

被管控的纯资源,如云服务器 CVM,云数据库 RDS, 设置了公开读写访问对象存储 COS 不受影响可以继续使用。然而依赖认证与API 的各种云 PaaS 服务,例如标准的私有读写的对象存储 COS,就抓瞎了。

因为阿里云至今没有做一个像样的事后故障复盘,因此在《我们能从阿里云史诗级故障中学到什么》中,我为阿里云的这次故障做了非官方的技术复盘。同样的判断逻辑完全也适用于这次故障 —— 这样的爆炸半径,根因出在 Auth 上的概率很大。目前,腾讯云仍然没有给出官方的事后故障复盘报告,也可能不会有了。


忽悠人的状态页

我的朋友杨攀曾写过一篇《中国云服务走向全球?先把 Status Page 搞定》,讨论了 Status Page (服务健康状态页)对于公有云服务的重要性,各家本土云厂商也跟进了这一特性,包括腾讯云。—— 状态页能在服务宕机的情况下有效减少客户的焦虑,降低沟通成本,但它的核心价值在于 “建立与客户的信任关系”。

看上去,腾讯云与阿里云的 Status Page 反应都比较迟缓,在故障发生后三四十分钟才开始更新。而不是像Cloudflare 等产品一样及时更新故障,或采用自动化方式监测到故障后立即推送。但不同于阿里云 —— 虽慢却诚实地标记了所有服务受到影响,腾讯云的 Status Page 连基本的真实性与准确性都堪称稀烂。

例如,受到影响的对象存储 COS 服务,在有用户上报问题的几个可用区中,我并没有看到 Status 标红。而这样的例子还有更多。事实上如果问题真出在管控 API 上,那么影响的范围应该和阿里云一样 —— 所有服务的控制面。因此,这样鸡贼的做法只会给客户留下:“不透明、有猫腻“ 的负面印象。


撒谎的三无公告

在故障出现 40 ~ 50 分钟后,腾讯云终于发出了第一份故障公告,也是截止到目前 Status Page 上唯一一份公告。但其内容就一句话 —— 三无公告:无时间(故障时间),无地点(可用区/AZ),无范围(影响服务)。而且姗姗来迟,比我替它发的公告《【腾讯】云计算史诗级二翻车来了》还晚了十分钟。

但这份公告最致命的问题是真实性与准确性:首先,故障绝对不仅仅是“控制台”,而是整个控制面。作为一个专业的云计算服务供应商,一字之差天壤之别,混淆两者区别的原因,要么是蠢(缺乏专业素养,台面混为一谈)。要么是坏(避重就轻,推卸责任)。

请问,一个全身休克的人,说他 “面色异常”,这是一个真诚的回复吗?请问,一台被砸烂的笔记本电脑,说它“敲击键盘没有反应”是一个有意义的描述吗?同理,一个控制面爆炸的公有云,说自己“控制台异常”,是一个认真的回复吗?

其次,从事后官微的发布与用户群的反馈来看,在这个时间,“目前故障已恢复”  是在撒谎。至少相当一部分服务的可用性事件是在 16:45 标记恢复的,在17 点前后,腾讯云产品吐槽群中也仍然有一些问题上报。

我认为这份对腾讯云带来的伤害远比服务宕机要大的多 —— 首先,在及时性,准确性上体现出了极差的专业素养。其次,在真实性上有意做手脚,会伤及公有云,或者说一切生意的根本 —— 诚信这对品牌形象是一个摧毁性打击。


灾难级别的公关


按理说,出现了这么严重的故障,应当用诚恳认真的态度去处理,但腾讯云官方微博居然还在抖机灵 —— 堪称灾难级别的公关水平

这条微博也再次扇了腾讯云自己官网公告的大嘴巴子 —— 16:45 分发第一条帖子时,“工程师仍在紧急修复中”,17:16,距离第一次报告故障的 15:31已经过去近两个小时,“已经整体恢复”。然而,根据腾讯云官网 16:21 发布的公告[1]声称:“故障已恢复”。从实际情况来看,再次证明了官网公告在说谎

阿里云双十一大故障的时候,刚刚开完云栖大会,打脸了吹下的极致高可用的牛逼,但毕竟隔了一周了。而腾讯云这次大故障的同时还在开发布会吹牛逼,还找特大号发了一篇软文:太意外了!国内80%大模型都存在鹅厂!》,发布时间 16:19,2分钟后官网发出故障通告,堪称光速打脸二次方

与之形成鲜明对照的是,去年 11 月 Cloudflare 的故障,Cloudflare CEO Matthew 亲自出来对故障进行道歉与复盘,相比之下,国内云厂商的危机公关堪称灾难级别 —— 彻底做实了草台班子的称号。

实锤的草台班子

请允许我引用瑞典马工的一句名言 :“阿里云是个工程质量差劲的正经云,但腾讯云是一群业余销售加业务码农玩游戏”。所谓光鲜亮丽的大厂,在里面也不过是一个又一个的草台班子。

Reference

公告: https://cloud.tencent.com/announce/detail/1995

https://www.oschina.net/news/286685

https://www.v2ex.com/t/1030638

https://www.v2ex.com/t/103061


云计算泥石流
曾几何时,“上云“近乎成为技术圈的政治正确,整整一代应用开发者的视野被云遮蔽。就让我们用实打实的数据分析与亲身经历,讲清楚公有云租赁模式的价值与陷阱 —— 在这个降本增效的时代中,供您借鉴与参考。



作者:非法加冯
来源:mp.weixin.qq.com/s/PgduTGIvWSUgHZhVfnb7Bg
收起阅读 »

WiFi万能钥匙突然更新,网友炸了

时至今日,机哥已记不清,七八年前曾用过哪些家喻户晓的手机软件。若不是最近看到这样一篇新闻。我都差点忘了,有一个名为“WiFi万能钥匙”的App曾风靡全国。当时机哥身边的亲朋好友,只要是有智能手机的。基本都会安装上这个App。原因倒是不复杂,在那个流量资费偏高的...
继续阅读 »

时至今日,机哥已记不清,七八年前曾用过哪些家喻户晓的手机软件。


若不是最近看到这样一篇新闻。


我都差点忘了,有一个名为“WiFi万能钥匙”的App曾风靡全国。


当时机哥身边的亲朋好友,只要是有智能手机的。


基本都会安装上这个App。

原因倒是不复杂,在那个流量资费偏高的年代。


咱们上网冲浪,主打一个“能蹭WiFi,绝不用流量


恰好,WiFi万能钥匙对用户最大的贡献,也是帮忙蹭网。


不管是人流量爆满的商城,还是学校办公室。


WiFi万能钥匙,总是能如它的名字般神奇,帮用户成功连上WiFi。


关键是,这软件还免费使用。


多少给当时懵懂的机哥,来了点小小的互联网震撼。



也是靠着“随时随地,免费上网”的优势。


WiFi万能钥匙发布不到三年,就拥有超过5亿的激活用户。


公司发的年终奖更是重量级。


给所有入职超过4个月的员工,送一台价值近百万元的特斯拉跑车。


什么叫风头无两啊,就是。


可随着时间推移。


越来越多用户也发现了,所谓的“免费蹭网”,是需要付出代价的。


在这些年的发展中,WiFi万能钥匙翻车过好几次。


包括被官方点名批评。


被华为小米轮番整治。


当时两大手机厂商,标记它为恶意应用,还把它赶出了自家应用商店。


再加上App内部,出现了各种离谱的广告。


不仅在形式上,集百家之所长。


摇一摇跳转、伪装跳过按钮、多图层套娃全凑齐了。


具体到广告内容,更是大杂烩乱炖。


不知道的,还以为下了个病毒软件呢...


尽管官方最近宣布,给WiFi万能钥匙减少70%广告。


但它这些年,积攒起来的崩坏口碑。


可不是一两个优化减负,就能抹掉的。


当然啦,如果只是广告讨人嫌。


那WiFi万能钥匙,还不至于被喷成这程度。


整个App的争议点,就在于它那“共享热点”模式。


没错,虽然它大名叫WiFi万能钥匙。


但它能帮咱们连上各种场合的WiFi,原理并不是暴力破解。


而是从自家密码数据库中,找到与该WiFi相匹配的密码。


等配对成功后,我们就成功蹭上别人的网络了。


官方也很清晰明了地介绍过,该App的运行原理:


软件基于共享经济,利用热点主人分享的闲置WiFi资源,向所有用户提供免费上网服务。

听起来,似乎是个不错的模式对吧。


这就好比,我在某个餐厅输入密码连上了WiFi。


然后WiFi万能钥匙,又把我手机记录下来的WiFi密码,上传到云端数据库,下次别人再来这家餐厅,直接打开App就能连上。


你帮我,我帮你,天下就没有难办的事儿了。


但理想很丰满,现实很骨感。


这共享模式,实际是很难落实下来的。


机哥举个例子啊。


在知乎上,有一个问题写着:


“如果每个人都给我一块钱,那我不就有13亿了吗?”


而且每人只需掏一块钱,也不是啥很大的损失对吧。


可这事儿就和共享WiFi密码一样,有一个大前提:


凭什么?


我凭什么无缘无故,给一个陌生人一块钱?


我又凭什么无缘无故,给一个陌生人,提供自家的WiFi密码?


更何况,是密码上传到一个装机量8亿的App。


对于WiFi万能钥匙来说,运营初期就面对着这个问题。


不过出来混,总得有两把刷子。


很快啊,就有网友对Wi-Fi万能钥匙做出了分析。


他表示,App可以直接从用户手机拿到WiFi密码。


搞机佬都知道,安卓系统在获取Root权限后,可以通过使用Re管理器等App,直接查看存放WiFi密码的文件。


可谓是明文存放,点开就送。


当然,能访问到这个文件夹的前提是,手机得有Root权限。


可很凑巧的是,早期的安卓手机获取权限非常简单。


随便在网上下载个“一键Root”工具,重启手机就完事儿。


所以在那个时候,用户第一次安装打开WiFi万能钥匙,都会被这App索取Root权限。


紧接着,最关键的问题来了。


它到底有没有,通过申请Root权限,来查看用户手机里的WiFi密码保存文件呢?


当时有位知乎老哥,特意反编译了1.0版本的WiFi万能钥匙。


发现了以下这几行代码。


作为一个,只会输入“Hello World”的代码废柴。


机哥还是很自觉地,把代码交给了AI去分析。


结果AI给出的分析,和那位知乎老哥的结论,几乎一模一样。


WiFi万能钥匙1.0版本,会在获取Root权限后,把手机上的WiFi配置文件,复制到了自己的缓存文件夹中。

嗯?难道说...


不过在后续的版本更新中。


WiFi万能钥匙的玩法严谨得多,主打一手正儿八经的“共享”。


比如把用户主动输入的密码存到云端库。


或者和运营商合作,把一些公共区域的免费WiFi给收录进去。


如果实在遇到一些,数据库里配对不上的WiFi。


WiFi万能钥匙还会提示你,可以尝试一下【深度连接】。


而所谓的【深度连接】呢,是App用内置的几千个弱密码,逐个连接同一个WiFi。


机友们都懂的,其实很多家庭路由器,密码都设置得很简单。


诸如12345678、1122334等朗朗上口的密码,简直不要太常见。


所以在很多时候,【深度连接】还真能帮你连上WiFi。


但后来的事情,咱们都知道了。


流量资费便宜了,用户对蹭WiFi的需求日渐下降。


再加上手机厂商和路由器厂商,也开始注重隐私安全。


Wi-Fi万能钥匙作为一个工具类应用,也就失去了解决问题的场景。


用户量减少、入下滑,都是板上钉钉的事儿。


为了维持收支平衡,WiFi万能钥匙加大了软件招商的力度。


我们可是有9亿用户总量的,欢迎来合作啊喂。


具体到可以塞广告的位置。


不能说克制,只能说处处皆是广告位。


横幅、图文、弹窗,基本上能塞内容的位置,都有广告的一席之地。


而WiFi万能钥匙,对于广告的内容筛选,更是让人汗流浃背。


比如说,以美女为诱惑,吸引你点开广告。


早期还有用户吐槽,App内部的信息流推送,有很多擦边低俗资讯。


讲道理,以它如此庞大的用户总量。


这么多广告的接入,肯定能让它在短时间内,赚得盆满钵满。


但这操作,多少有点饮鸩止渴的味道。


更何况,现在早就不是,流氓App能随意践踏手机的时代了。


你看这两年,手机厂商都在集中整治,WiFI类和清理类App存在的问题:


包括违规收集个人信息、频繁弹窗自动下载第三方软件等。


可能是意识到,只靠广告营收走不通。


WiFi万能钥匙在宣布改版后,广告确实少了很多。


那它现在又能靠啥维持生计呢?


机哥带着好奇,安装了新版打开体验。


结果发现,它现在往App塞了个短剧板块。


emmm...机哥也没啥好说的,祝它一切顺利吧。



作者:好机友
来源:mp.weixin.qq.com/s/9IfrA6ilpOit4dVAjH6U4Q
收起阅读 »

Kotlin中 for in 是有序的吗?forEach呢?

我们要遍历一个数组、一个列表,经常会用到kotlin的 for in 语法,但是 for in 是不是有序的呢?forEach是不是有序的呢?这就需要看一下它们的本质了。 数组的 for in // 调用: val arr = arrayOf(1, 2, 3)...
继续阅读 »

我们要遍历一个数组、一个列表,经常会用到kotlin的 for in 语法,但是 for in 是不是有序的呢?forEach是不是有序的呢?这就需要看一下它们的本质了。


数组的 for in


// 调用:
val arr = arrayOf(1, 2, 3)
for (ele in arr) {
println(ele)
}

反编译成Java是个什么东西呢?


Integer[] arr = new Integer[]{1, 2, 3};
Integer[] var4 = arr;
int var5 = arr.length;

for(int var3 = 0; var3 < var5; ++var3) {
int ele = var4[var3];
System.out.println(ele);
}

总结:从Java代码可以看出,实际就是一个普通的for循环,是从下标0开始遍历到结束的,所以是有序的。


列表的 for in


// 调用:
val list = listOf(1, 2, 3)
for (ele in list) {
println(ele)
}

反编译成Java:


List list = CollectionsKt.listOf(new Integer[]{1, 2, 3});
Iterator var3 = list.iterator();

while(var3.hasNext()) {
int ele = ((Number)var3.next()).intValue();
System.out.println(ele);
}

可以看出列表的for in是通过iterator实现的,和数组不一样,那这个iterator遍历是否是有序的呢?首先我们要知道这个iterator怎么来的:


// iterator 是通过调用 list.iterator() 得到的,那么这个list又是什么呢?
Iterator var3 = list.iterator();

// list
List list = CollectionsKt.listOf(new Integer[]{1, 2, 3});

// list是通过数组elements.asList()得到的
public fun <T> listOf(vararg elements: T): List<T> = if (elements.size > 0) elements.asList() else emptyList()

// 这里有个expect,找到对应的actual
public expect fun <T> Array<out T>.asList(): List<T>

// 对应的actual
public actual fun <T> Array<out T>.asList(): List<T> {
return ArraysUtilJVM.asList(this)
}

// 最终调用了Arrays.asList(array)
class ArraysUtilJVM {
static <T> List<T> asList(T[] array) {
return Arrays.asList(array);
}
}

public class Arrays {

// 从这里看到最终拿到的list是 Arrays 类中的 ArrayList
// 然后我们找到里面的 iterator() 方法
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}

private static class ArrayList<E> extends AbstractList<E>
implements RandomAccess, java.io.Serializable
{
private final E[] a;

@Override
public Iterator<E> iterator() {
// 最终得到的iterator是ArrayItr
// 这里的a是一个数组,也就是我们一开始传进来的1,2,3
return new ArrayItr<>(a);
}
}

private static class ArrayItr<E> implements Iterator<E> {
private int cursor;
private final E[] a;

ArrayItr(E[] a) {
this.a = a;
}

@Override
public boolean hasNext() {
return cursor < a.length;
}

@Override
public E next() {
int i = cursor;
if (i >= a.length) {
throw new NoSuchElementException();
}
cursor = i + 1;
return a[i];
}
}
}

总结:列表的for in是通过iterator实现的,这个iterator是ArrayItr,从里面的next()方法可以看出,这也是有序的,从cursor开始,cursor默认是0,也就是从下标0开始遍历。
注:这里只是分析了Arrays.ArrayList的iterator,具体的集合类需要具体分析,比如ArrayList、LinkedList等,不过从正常思维来看,iterator是一个迭代器,就应该有序的把数据一个一个丢出来。


数组的 forEach


// 调用:
val arr = arrayOf(1, 2, 3)
arr.forEach {
println(it)
}

// 点进去forEach看:
// 其实也是调用了for in,所以也是有序的。
public inline fun <T> Array<out T>.forEach(action: (T) -> Unit): Unit {
for (element in this) action(element)
}

列表的 forEach


// 调用:
val list = listOf(1, 2, 3)
list.forEach {
println(it)
}

// 点进去forEach看:
// 其实也是调用了for in,所以也是有序的。
public inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit {
for (element in this) action(element)
}

作者:linq
来源:juejin.cn/post/7304562756429611046
收起阅读 »

一次接手外包公司前端代码运行踩坑过程

web
背景 外包项目结束后,代码交给我公司需要存起来,因为后期还会有迭代开发任务,以后的事情肯定是我们公司内部来维护了,那就需要把代码运行起来,这过程中运行前端项目遇到的几个问题和处理过程简单记录下。 主要问题是,外包公司建有自己UI组件库,所有里面很多包是他们np...
继续阅读 »

背景


外包项目结束后,代码交给我公司需要存起来,因为后期还会有迭代开发任务,以后的事情肯定是我们公司内部来维护了,那就需要把代码运行起来,这过程中运行前端项目遇到的几个问题和处理过程简单记录下。


主要问题是,外包公司建有自己UI组件库,所有里面很多包是他们npm私有仓库的托管,我们无法访问到他们的私服仓库,思路是从 node_modules中 把私有包迁移到我们公司自己内网仓库


代码


我拿到的两个项目代码,共有两个项目代码,下面这是web的代码,处理思路是一样的


image.png


第一步运行看是否正常


因为观察到项目中有 node_modules ,因为外包公司是把整个项目文件都拷贝过来了,里面还包括 .git 目录,如果能直接运行起来,那万事大吉。


显示看下图,是运行报错的,缺少包和相关命令,所以我们还是得自己来重新安装 node_modules ,但是问题是私有包如何解决?


image.png


第二次尝试重新安装包


我们尝试重新直接安装包,安装失败,因为访问不到私有仓库域名


image.png


正式迁移包


我们公司也是用verdaccio搭建过私有仓库的,所以要把外包项目的私有包上传到我们公司内部



  • package.json中找到私有包

  • 拷贝私有包成独立项目

  • 推送到我们公司内部verdaccio仓库(没有私有仓库就传到npm上也一样,但是外包公司自己的包还是别外传)

  • 项目中配置.npmrc锁定包来源

  • 锁定项目中版本号


package.json中找到私有包


通过判定看到下图的包在 http://www.npmjs.com/ 中查找不到,所以下面这些 @iios前缀的包是需要迁移到包


image.png


拷贝私有包成独立项目



我们从 node_modules 中拷贝出来这些文件夹



image.png



观察到所有包都是完整的,都有package.json文件



image.png


推送到我们公司内部verdaccio仓库


这里这么多包,如果简化可以使用lerna或者shell脚本来统一处理版本问题,但是我们简化就按个包执行推送命令即可


image.png


后续所有包同理操作即可


image.png


后面就不一一列举了,检查verdaccio是否推送成功


image.png


项目中配置.npmrc锁定包来源


现在私有包都上传完成了,所以需要回到主项目,安装包就行了,但是因为有私有包,于是需要执行 .npmrc 规定各种包的安装路径


image.png


锁定项目中版本号


这一步是我习惯,在package.json中,版本号固定写死,而不是 ^前缀开头自动更新此版本


而且更重要的是,外包项目已经在线上运行,万一以后要三方包变化导致一些莫名其妙问题就很麻烦,锁定版本号是非常有必要的,才能以后很久之后打开发布代码也是没有问题的


image.png


image.png


删除node_modules 和 yarn.lock ,重新安装包


image.png


image.png


重新运行


一切都搞完了,重新运行成功


image.png


image.png


作者:一个不会重复的id
来源:juejin.cn/post/7348090716578824230
收起阅读 »

Android文件存储

前言在Android中,对于持久化有如下4种:本篇文章主要介绍一些容易出错的地方,以及访问对应空间的API。正文先来看看内部存储空间。内部存储空间由上面图可以看出内部存储空间主要由3个部分组成,其中内部存储的目录就是/data/data/<包名>/...
继续阅读 »

前言

在Android中,对于持久化有如下4种:

持久化.jpg

本篇文章主要介绍一些容易出错的地方,以及访问对应空间的API。

正文

先来看看内部存储空间。

内部存储空间

由上面图可以看出内部存储空间主要由3个部分组成,其中内部存储的目录就是/data/data/<包名>/,对应的目录如下:

内部存储空间.jpg

内部存储空间有如下特点:

  • 每个应用独占一个以包名命名的私有文件夹。
  • 在应用卸载时被删除。
  • 对MediaScanner不可见。
  • 适用于私密数据。

对于内部存储空间,里面有一些默认的文件夹,而对于不同文件的访问,有着不同的API,如下:

  1. 对于data/data/<包名>/目录:
方法描述
Context#getDir(String name,int mode): File获取内部存储根目录下的文件夹,不存在则创建
  1. 对于data/data/<包名>/files/目录:
方法描述
Context#getFilesDir():File!返回files文件夹
Context#fileList(): Array!列举files目录下所有文件和文件夹,String类型为文件或者文件夹的名字
Context#openFileInput(String name):FileInputStream打开files文件下的某个文件的输入流,不存在则抛出异常:FileNotFoundException
Context#openFileOut(String name,int mode):FileOutputStream打开files文件下的某个文件的输入流,文件不存在则新建
Context#deleteFile(String name): Boolean删除文件或文件夹
  1. 对于data/data/<包名>/cache/目录:
方法描述
Context#getCacheDir():File返回cache文件夹
  1. 对于data/data/<包名>/code_cache目录:
方法描述
Context#getCodeCacheDir():File返回优化过的代码目录,如JIT优化

上述方法测试代码如下:

        val testDir = getDir("rootDir", MODE_PRIVATE)
//打印为:/data/user/0/com.wayeal.ocr/app_rootDir    
Logger.t("testFile").d("testDir = ${testDir.absolutePath}")
//打印为:/data/user/0/com.wayeal.ocr/files  
Logger.t("testFile").d("filesDir = ${filesDir.absolutePath}")
//在files目录下新建文件
val fileOutputStream = openFileOutput("filesTest", MODE_PRIVATE)
//打印为:[datastore, bugly_last_us_up_tm, local_crash_lock, filesTest]
Logger.t("testFile").d("fileList = ${fileList().toMutableList()}")
File(filesDir,"haha").createNewFile()
//打印为:[datastore, bugly_last_us_up_tm, haha, filesTest]
Logger.t("testFile").d("fileList = ${fileList().toMutableList()}")

外部存储空间

对于外部存储空间在使用前一般要判断是否挂载,因为早期的的Android手机是有SD卡的,是可以进行卸载SD卡的。

对于外部存储空间,也有严格的划分,如下:

外部存储空间划分.jpg

这里可以发现外部存储空间分为了公共目录和私有目录,对于公共目录特点:

  • 外部存储中除了私有目录外的其他空间。
  • 所有应用共享。
  • 在应用卸载时不会被卸载。
  • 对MediaScanner可见。
  • 适用于非私密数据,不需要随应用卸载删除。

对于私有目录,有如下特点:

  • 目录名为Android。
  • 在media和data等目录中,以包名区分各个应用。
  • 在应用卸载时被删除。
  • 对MediaScanner不可见。(对多媒体文件夹例外,要求API 21)
  • 适用于非私密数据,需要在应用卸载时删除。

这里对于公共目录storage/emulated/0/来说,其API主要是Environment类来完成,如下:

方法描述
Environment.getExternalStorageDirectory(): File获取外部存储目录
Environment.getExternalStoragePublicDirectory(name: String): File外部存储根目录下的某个文件夹
Environment.getExternalStorageState(): String外部存储的状态

对于外部空间的私有目录storage/emulated/0/Android/data/<包名>/来说,其API还是由Context,主要是方法名都携带external字样,如下:

方法描述
Context.getExternalCacheDir(): File获取cache文件夹
Context.getExternalCacheDirs(): Array多部分cache文件夹(API 18),因为外部存储空间可能有多个
Context.getExternalFilesDir(type: String): File获取files文件夹
Context.getExternalFilesDirs(type: String): Array获取多部分的files文件夹
Context.getExternalMediaDirs(): Array获取多部分多媒体文件(API 21)

上述方法测试代码和log如下:

        Logger.t("testFile")
          .d("外部公共存储根目录 = ${Environment.getExternalStorageDirectory().absolutePath}")
//外部公共存储根目录 = /storage/emulated/0
       Logger.t("testFile")
          .d("外部公共存储Pictures目录 = ${Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).absolutePath}")
//外部公共存储Pictures目录 = /storage/emulated/0/Pictures
       Logger.t("testFile")
          .d("外部公共存储状态 = ${Environment.getExternalStorageState()}")
//外部公共存储状态 = mounted
       Logger.t("testFile")
          .d("外部存储私有缓存目录 = ${externalCacheDir?.absolutePath}")
//外部存储私有缓存目录 = /storage/emulated/0/Android/data/com.wayeal.ocr/cache
       Logger.t("testFile")
          .d("外部存储私有多部分缓存目录 = ${externalCacheDirs?.toMutableList()}")
//外部存储私有多部分缓存目录 = [/storage/emulated/0/Android/data/com.wayeal.ocr/cache]
       Logger.t("testFile")
          .d("外部存储私有files的Pictures目录 = ${getExternalFilesDir(Environment.DIRECTORY_PICTURES)}")
//外部存储私有files的Pictures目录 = /storage/emulated/0/Android/data/com.wayeal.ocr/files/Pictures
       Logger.t("testFile")
          .d("外部存储私有媒体多部分目录 = ${externalMediaDirs.toMutableList()}")
//外部存储私有媒体目录 = [/storage/emulated/0/Android/media/com.wayeal.ocr]

总结

对于不同的存储空间的特点以及API要了解,在需要保存文件时选择适当的存储空间。


作者:yuanhao
来源:juejin.cn/post/7158365077488271367

收起阅读 »

借某次写需求谈Android文件存储

前言 某天,我导让我写一个“崩溃日志本地收集”的功能,可以方便测试和开发查看崩溃原因。然后故事就开始了。 Round 1 哥们一开始,用context.getFilesDir()获取存储目录,这个方法返回的文件夹路径是data/data/包名/files,也就...
继续阅读 »

前言


某天,我导让我写一个“崩溃日志本地收集”的功能,可以方便测试和开发查看崩溃原因。然后故事就开始了。


Round 1


哥们一开始,用context.getFilesDir()获取存储目录,这个方法返回的文件夹路径是data/data/包名/files,也就是Android的内部存储空间。


然后哥们很顺利的啊,把这个功能做出来了。


第二天开站会,测试提出了致命疑问:我们测试要怎么看到报错信息呢?


众所周知啊,这个路径手机不root是无法查看的。所以我看向我导:“手机root一下不就行了”


image.png


我导:改!


Round 2


哥们吸取教训啊,咱不存在内部,咱存外面还不行吗。这次我用context.getExternalFilesDir()获取存储目录,也就是外部存储的应用私有目录,路径是storage/emulated/0/Android/data/包名/files。


改个路径的事情,瞬间写好了。


我们这个日志搜集,一个是搜集Native层的报错,一个是搜集Jvm层的报错。然后经过测试,发现Jvm层的报错信息有权限取出来,而Native层的报错信息却没权限取出来


我们当时就震惊了:啊?同一个目录下存东西居然会出现两套权限?


image.png


然后另外一个Android开发的前辈就想通过adb强行把这个报错信息拿出来,但是问题是没有root过没法用su命令啊,所以这件事又绕回去了。


然后我导就让我改到根目录下。


行,哥们改!


Round 3


既然内部存储不行,存到外部存储的私有目录也不行,就只能存在公共目录了。也就是我们使用手机文件管理应用看,Music和Movie的那一层。


获取存储路径用Environment.getExternalStorageDirectory(),得到的路径是storage/emulated/0。


改完后我又发现,Native层的权限正常了,Jvm的报错信息写不进去了。


报错信息是:


java.io.FileNotFoundException:...(Opration not permitted)


我心想:啊?这个目录难道没有写权限?那Natvie的报错信息怎么写进去的?


当时复制粘贴进百度,看到了一名CSDN老哥的回答:


img_v3_027e_6922fad6-53b9-4b94-b35f-c5445a90a4eg.jpg


其实我当时就对这个回答存疑的,因为明显我能mkdir,但是.txt文本信息却写不进去。


终于,我在Stack Overflow看到了正解:


image.png


没错,真相只有一个,是文件名有问题。我将.txt改成了.log就能成功存储了。


至此,终于可以下班。


image.png


总结


Android的文件存储和权限管理是真的*蛋。


实习的每一天做需求,都像在拍走进科学,哎。


顺便复习一下Android文件存储吧:Android文件存储


作者:leiteorz
来源:juejin.cn/post/7327920541989781504
收起阅读 »

动态代理在Android中的运用

动态代理是一种在编程中非常有用的设计模式,它允许你在运行时创建一个代理对象来代替原始对象,以便在方法调用前后执行额外的逻辑。在Android开发中,动态代理可以用于各种用例,如性能监控、AOP(面向切面编程)和事件处理。本文将深入探讨Android动态代理的原...
继续阅读 »

动态代理是一种在编程中非常有用的设计模式,它允许你在运行时创建一个代理对象来代替原始对象,以便在方法调用前后执行额外的逻辑。在Android开发中,动态代理可以用于各种用例,如性能监控、AOP(面向切面编程)和事件处理。本文将深入探讨Android动态代理的原理、用途和实际示例。


什么是动态代理?


动态代理是一种通过创建代理对象来代替原始对象的技术,以便在方法调用前后执行额外的操作。代理对象通常实现与原始对象相同的接口,但可以添加自定义行为。动态代理是在运行时生成的,因此它不需要在编译时知道原始对象的类型。


动态代理的原理


动态代理的原理涉及两个关键部分:



  1. InvocationHandler(调用处理器):这是一个接口,通常由开发人员实现。它包含一个方法 invoke,在代理对象上的方法被调用时会被调用。在 invoke 方法内,你可以定义在方法调用前后执行的逻辑。

  2. Proxy(代理类):这是Java提供的类,用于创建代理对象。你需要传递一个 ClassLoader、一组接口以及一个 InvocationHandlerProxy.newProxyInstance 方法,然后它会生成代理对象。


下面是一个示例代码,演示了如何创建一个简单的动态代理:


import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method
import java.lang.reflect.Proxy

// 接口
interface MyInterface {
fun doSomething()
}

// 实现类
class MyImplementation : MyInterface {
override fun doSomething() {
println("Original method is called.")
}
}

// 调用处理器
class MyInvocationHandler(private val realObject: MyInterface) : InvocationHandler {
override fun invoke(proxy: Any, method: Method, args: Array<Any?>?): Any? {
println("Before method is called.")
val result = method.invoke(realObject, *(args ?: emptyArray()))
println("After method is called.")
return result
}
}

fun main() {
val realObject = MyImplementation()
val proxyObject = Proxy.newProxyInstance(
MyInterface::class.java.classLoader,
arrayOf
(MyInterface::class.java),
MyInvocationHandler
(realObject)
) as MyInterface

proxyObject.doSomething()
}

运行上述代码会输出:


Before method is called.
Original method is called.
After method is called.

这里,MyInvocationHandler 拦截了 doSomething 方法的调用,在方法前后添加了额外的逻辑。


Android中的动态代理


在Android中,动态代理通常使用Java的java.lang.reflect.Proxy类来实现。该类允许你创建一个代理对象,该对象实现了指定接口,并且可以拦截接口方法的调用以执行额外的逻辑。在Android开发中,常见的用途包括性能监控、权限检查、日志记录和事件处理。


动态代理的用途


性能监控


你可以使用动态代理来监控方法的执行时间,以便分析应用程序的性能。例如,你可以创建一个性能监控代理,在每次方法调用前记录当前时间,然后在方法调用后计算执行时间。


import android.util.Log

class PerformanceMonitorProxy(private val target: Any) : InvocationHandler {
override fun invoke(proxy: Any, method: Method, args: Array<Any?>?): Any? {
val startTime = System.currentTimeMillis()
val result = method.invoke(target, *(args ?: emptyArray()))
val endTime = System.currentTimeMillis()
val duration = endTime - startTime
Log.d("Performance", "${method.name} took $duration ms to execute.")
return result
}
}

AOP(面向切面编程)


动态代理也是AOP的核心概念之一。AOP允许你将横切关注点(如日志记录、事务管理和安全性检查)从业务逻辑中分离出来,以便更好地维护和扩展代码。通过创建适当的代理,你可以将这些关注点应用到多个类和方法中。


事件处理


Android中常常需要处理用户界面上的各种事件,例如点击事件、滑动事件等。你可以使用动态代理来简化事件处理代码,将事件处理逻辑从Activity或Fragment中分离出来,使代码更加模块化和可维护。


实际示例


下面是一个简单的示例,演示了如何在Android中使用动态代理来处理点击事件:


import android.util.Log
import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method
import java.lang.reflect.Proxy
import android.view.View

class ClickHandlerProxy(private val target: View.OnClickListener) : InvocationHandler {
override fun invoke(proxy: Any, method: Method, args: Array<Any?>?): Any? {
if (method.name == "onClick") {
Log.d("ClickHandler", "Click event intercepted.")
// 在事件处理前可以执行自定义逻辑
}
return method.invoke(target, *args.orEmpty())
}
}

// 使用示例
val originalClickListener = View.OnClickListener {
// 原始的点击事件处理逻辑
}

val proxyClickListener = Proxy.newProxyInstance(
originalClickListener::class.java.classLoader,
originalClickListener::
class.java.interfaces,
ClickHandlerProxy
(originalClickListener)
) as View.OnClickListener

button.setOnClickListener(proxyClickListener)

通过这种方式,你可以在原始的点击事件处理逻辑前后执行自定义逻辑,而无需修改原始的OnClickListener实现。


结论


动态代理是Android开发中强大的工具之一,它允许你在不修改原始对象的情况下添加额外的行为。在性能监控、AOP和事件处理等方面,动态代理都有广泛的应用。通过深入理解动态代理的原理和用途,你可以更好地设计和维护Android应用程序。




作者:午后一小憩
来源:juejin.cn/post/7275185537815183360
收起阅读 »

靠维护老项目度中年危机

最近靠维护老项目度过中年危机的话题挺火,刚好最近也在维护一个PHP开发的CRM的老项目,项目由于数据量比较大, 导致查询速度很慢, 经常出现超时的情况, 下面记录一下具体的优化过程。 优化老项目,老生常淡的几点: 1. 数据库优化 2. 代码结构优化 3. 缓...
继续阅读 »

最近靠维护老项目度过中年危机的话题挺火,刚好最近也在维护一个PHP开发的CRM的老项目,项目由于数据量比较大, 导致查询速度很慢, 经常出现超时的情况, 下面记录一下具体的优化过程。


优化老项目,老生常淡的几点:


1. 数据库优化
2. 代码结构优化
3. 缓存优化
4. 资源优化
...

数据库优化


众所周知, MySQL 优化第一步,就是建索引, 看了一下整个系统的表, 发现有大量的表都没有索引, 建了索引的表,索引名称有点花里胡哨, 如下:


contractId	`contacts_id`	NORMAL	BTREE	27599	A		0		
customer_id `customer_id` NORMAL BTREE 27599 A 0

--

index_group `role_id`, `callDate` NORMAL BTREE 4359069 A 0
business_id `business_id` NORMAL BTREE 518 A 0
status_id `status_id` NORMAL BTREE 43 A 0


于是,优化第一步,规范一下索引的命名,MySQL索引的命名虽然没有硬性的规范,但是修改一下自己看着舒服, 个人理解:


普通索引:idx_字段1_字段2
唯一索引:uk_字段1_字段2
主键索引:pk_字段1_字段2


于是 上面的索引改成了:


idx_contacts_id	`contacts_id`	NORMAL	BTREE	27599	A		0		
idx_customer_id `customer_id` NORMAL BTREE 27599 A 0

--

idx_role_id_callDate `role_id`, `callDate` NORMAL BTREE 4359069 A 0
idx_business_id `business_id` NORMAL BTREE 518 A 0
idx_status_id `status_id` NORMAL BTREE 43 A 0


一下看起来舒服多了, 于是, 优化第二步, 就是给没有索引的表加上索引, 这个工作量比较大, 先把几个 常用功能模块的 表给加上索引, 于是 吭哧吭哧的 分析了 2天的 慢日志, 给需要加索引的表加上索引,本以为 加完索引后, 查询速度会快很多,结果发现, 并没有什么卵用. 一个页面 虽然快了点, 但是 不是太明显.


本着能加 配置 绝不改代码的原则,先去问了一下运维 Mysql 运行的机器内存是多大 64G. 这么大,那好办,先分析一下 数据库中的表引擎. 上了一段代码:


/** * Author: PFinal南丞 * Date: 2023/12/28 * Email:  *//** 确保这个函数只能运行在 shell 中 **/if (!str_starts_with(php_sapi_name(), "cli")) {    die("此脚本只能在cli模式下运行.\n");}/** 关闭最大执行时间限制 */set_time_limit(0);error_reporting(E_ALL);ini_set('display_errors', 1);const MAX_SLEEP_TIME = 10;$hostname   = '';$username   = '';$password   = '';$connection = mysqli_connect($hostname, $username, $password);if (!$connection) {    die('Could not connect: ' . mysqli_error($connection));}$query  = "SELECT table_name,engine FROM informati0n—schema.tables WHERE table_schema = 'smm';";$result = mysqli_query($connection, $query);if (!$result) {    die("Query failed: " . mysqli_error($connection));}$InnoDB_num = 0;$MyISAM_num = 0;while ($process = mysqli_fetch_assoc($result)) {    echo $process['table_name'] . " " . $process['engine'] . PHP_EOL;    if ($process['engine'] == 'InnoDB') {        $InnoDB_num++;    }    if ($process['engine'] == 'MyISAM') {        $MyISAM_num++;    }}echo "InnoDB " . $InnoDB_num . " MyISAM " . $MyISAM_num . PHP_EOL;mysqli_close($connection);

得出结果:


表引擎 MyISAM 的表 176 张 InnoDB的表引擎 88张. 要了一份 线上MySql 的配置发现:


...

key_buffer_size = 512M
innodb_buffer_pool_size = 2048M

...


都知道 innodb_buffer_pool_size 针对的 是 InnoDB的表引擎,key_buffer_size 针对的 是 MyISAM的表引擎. 这配置不得修改一下. 果断打申请, 申请修改线上配置.


...

key_buffer_size = 2048M
innodb_buffer_pool_size = 2048M

...


重启服务后,果然比原来快了好多.能撑到 同事不在群里 打报告了.


艰巨的长征路迈出了第一步,接下来,本着 死道友不死贫道的原则, 厚着脸皮,让运维帮忙整了一台mysql 的机器,来做了个主从分离。 速度一下,不影响业务的正常使用了.


接着 开启漫长的 优化之路.


缓存优化



  1. 项目没有开启数据缓存, 只有 代码编译的缓存


所以这一块是一个大的工程, 所以先不动, 只是 给 几个 常用的功能加了一个 数据 的 缓存。后续的思路是:


  a. 加一个 redis, 使用 把代码中的统计数据 缓存到 redis 中

b. 把客户信息,客户的关联信息,组合到一起, 然后缓存到 redis中.
....


代码结构优化


开始挖开代码, 看看 查询慢的 功能 代码是咋写的,不看不知道,一看直接上头:



  1. 几乎全是 foreach 中 的 SQL 查询:


    foreach($customer_list as $key=>$value){        # ......        $customer_list[$key]['customer_name'] = $this->customer_model->get_customer_name($value['customer_id']);        $customer_list[$key]['customer_phone'] = $this->customer_model->get_customer_phone($value['customer_id']);        $customer_list[$key]['customer_address'] = $this->customer_model->get_customer_address($value['customer_id']);                # ......    }


  2. 由于 ORM 的方便复用, 大量的 表关联模型 复用,导致查询的 废字段特别多.比如:


    <?php    class CustomerViewModel extends ViewModel {        protected $viewFields;  public function _initialize(){   $main_must_field = array('customer_id','owner_role_id','is_locked','creator_role_id','contacts_id','delete_role_id','create_time','delete_time','update_time','last_relation_time','get_time','is_deleted','business_license');   $main_list = array_unique(array_merge(M('Fields')->where(array('model'=>'customer','is_main'=>1,'warehouse_id'=>0))->getField('field', true),$main_must_field));   $data_list = M('Fields')->where(array('model'=>'customer','is_main'=>0,'warehouse_id'=>0))->getField('field', true);   $data_list['_on'] = 'customer.customer_id = customer_data.customer_id';   $data_list['_type'] = "LEFT";   //置顶逻辑   $data_top = array('set_top','top_time');   $data_top['_on'] = "customer.customer_id = top.module_id and top.module = 'customer' and top.create_role_id = ".session('role_id');   $data_top['_type'] = "LEFT";   //首要联系人(姓名、电话)   $data_contacts = array('name'=>'contacts_name', 'telephone'=>'contacts_telephone');   $data_contacts['_on'] = "customer.contacts_id = contacts.contacts_id";   // 检查是否存在部门库字段            $warehouse_id = I('warehouse_id', '', 'intval');            if ($warehouse_id) {                $warehouse_id = D('Fields')->isExistsWarehouseTable(1, $warehouse_id);                if ($warehouse_id) {                    $customer_warehouse_data_table = customer_warehouse_table($warehouse_id);                    $warehouse_data_list = M('Fields')->where(array('model'=>'customer','is_main'=>0,'warehouse_id'=>$warehouse_id))->getField('field', true);                    $warehouse_data_list['_on'] = 'customer.customer_id = ' . $customer_warehouse_data_table .'.customer_id';                    $warehouse_data_list['_type'] = "LEFT";                    $this->viewFields = array('customer'=>$main_list,'customer_data'=>$data_list,$customer_warehouse_data_table=>$warehouse_data_list,'top'=>$data_top,'contacts'=>$data_contacts);                } else {                    $this->viewFields = array('customer'=>$main_list,'customer_data'=>$data_list,'top'=>$data_top,'contacts'=>$data_contacts);                }            } else {                $this->viewFields = array('customer'=>$main_list,'customer_data'=>$data_list,'top'=>$data_top,'contacts'=>$data_contacts);            }  }    ?>


  3. 代码中的业务逻辑一直再叠加,导致废代码量特别的大需要重新梳理逻辑


针对以上的代码做修改:


a. 第一点, 把所有foreach 中的 sql拆出来,先去查询到内存中,然后组合减少sql语句 

b. 第二点, 简化 ORM的乱用,比如只需要查询一个字段的 就直接用原生sql或者新的一个不关联的orm 来处理

资源优化



  1. 由于录音文件过大, 找运维 做了一个专门的文件服务器,移到了文件服务器上


最后


最后,给加了个定时任务告警的功能, 方便及时发现异常, 优化的 第一步 勉强交活。剩下的 优化 需要再花点时间了,慢慢来了.


作者:PFinal社区_南丞
来源:juejin.cn/post/7353475049418260517
收起阅读 »

弱智吧成最好中文AI训练数据:大模型变聪明,有我一份贡献

web
在中文网络上流传着这样一段话:弱智吧里没有弱智。百度「弱智吧」是个神奇的地方,在这里人人都说自己是弱智,但大多聪明得有点过了头。最近几年,弱智吧的年度总结文章都可以顺手喜提百度贴吧热度第一名。所谓总结,其实就是给当年吧里的弱智发言排个名。各种高质量的段子在这里...
继续阅读 »


在中文网络上流传着这样一段话:弱智吧里没有弱智。

百度「弱智吧」是个神奇的地方,在这里人人都说自己是弱智,但大多聪明得有点过了头。最近几年,弱智吧的年度总结文章都可以顺手喜提百度贴吧热度第一名。所谓总结,其实就是给当年吧里的弱智发言排个名。

各种高质量的段子在这里传入传出,吸引了无数人的围观和转载,这个贴吧的关注量如今已接近 300 万。你网络上看到的最新流行词汇,说不定就是弱智吧老哥的杰作。

随着十几年的发展,越来越多的弱智文学也有了奇怪的风格,有心灵鸡汤,有现代诗,甚至有一些出现了哲学意义。

最近几天,一篇人工智能领域论文再次把弱智吧推上了风口浪尖。

引发 AI 革命的大模型因为缺乏数据,终于盯上了弱智吧里无穷无尽的「数据集」。有人把这些内容拿出来训练了 AI,认真评测对比一番,还别说,效果极好。

接下来,我们看看论文讲了什么。
最近,大型语言模型(LLM)取得了重大进展,特别是在英语方面。然而,LLM 在中文指令调优方面仍然存在明显差距。现有的数据集要么以英语为中心,要么不适合与现实世界的中国用户交互模式保持一致。 
为了弥补这一差距,一项由 10 家机构联合发布的研究提出了 COIG-CQIA(全称 Chinese Open Instruction Generalist - Quality Is All You Need),这是一个高质量的中文指令调优数据集。数据来源包括问答社区、维基百科、考试题目和现有的 NLP 数据集,并且经过严格过滤和处理。
此外,该研究在 CQIA 的不同子集上训练了不同尺度的模型,并进行了深入的评估和分析。本文发现,在 CQIA 子集上训练的模型在人类评估以及知识和安全基准方面取得了具有竞争力的结果。
研究者表示,他们旨在为社区建立一个多样化、广泛的指令调优数据集,以更好地使模型行为与人类交互保持一致。
本文的贡献可以总结如下:

提出了一个高质量的中文指令调优数据集,专门用于与人类交互保持一致,并通过严格的过滤程序实现;

探讨了各种数据源(包括社交媒体、百科全书和传统 NLP 任务)对模型性能的影响。为从中国互联网中选择训练数据提供了重要见解;

各种基准测试和人工评估证实,在 CQIA 数据集上微调的模型表现出卓越的性能,从而使 CQIA 成为中国 NLP 社区的宝贵资源。


  • 论文地址:https://arxiv.org/pdf/2403.18058.pdf
  • 数据地址:https://huggingface.co/datasets/m-a-p/COIG-CQIA
  • 论文标题:COIG-CQIA: Quality is All You Need for Chinese Instruction Fine-tuning


COIG-CQIA 数据集介绍

为了保证数据质量以及多样性,本文从中国互联网内的优质网站和数据资源中手动选择了数据源。这些来源包括社区问答论坛、、内容创作平台、考试试题等。此外,该数据集还纳入了高质量的中文 NLP 数据集,以丰富任务的多样性。具体来说,本文将数据源分为四种类型:社交媒体和论坛、世界知识、NLP 任务和考试试题。


社交媒体和论坛:包括知乎、SegmentFault 、豆瓣、小红书、弱智吧。

世界知识:百科全书、四个特定领域的数据(医学、经济管理、电子学和农业)。

NLP 数据集:COIG-PC 、COIG Human Value 等。

考试试题:中学和大学入学考试、研究生入学考试、逻辑推理测试、中国传统文化。
表 1 为数据集来源统计。研究者从中国互联网和社区的 22 个来源总共收集了 48,375 个实例,涵盖从常识、STEM 到人文等领域。

图 2 说明了各种任务类型,包括信息提取、问答、代码生成等。

图 3 演示了指令和响应的长度分布。

为了分析 COIG-CQIA 数据集的多样性,本文遵循先前的工作,使用 Hanlp 工具来解析指令。

实验结果

该研究在不同数据源的数据集上对 Yi 系列模型(Young et al., 2024)和 Qwen-72B(Bai et al., 2023)模型进行了微调,以分析数据源对模型跨领域知识能力的影响,并使用 Belle-Eval 上基于模型(即 GPT-4)的自动评估来评估每个模型在各种任务上的性能。
表 2、表 3 分别显示了基于 Yi-6B、Yi-34B 在不同数据集上进行微调得到的不同模型的性能。模型在头脑风暴、生成和总结等生成任务中表现出色,在数学和编码方面表现不佳。


下图 4 显示了 CQIA 和其他 5 个基线(即 Yi-6B-Chat、Baichuan2-7B-Chat、ChatGLM2-6B、Qwen-7B-Chat 和 InternLM-7B-Chat)的逐对比较人类评估结果。结果表明,与强基线相比,CQIA-Subset 实现了更高的人类偏好,至少超过 60% 的响应优于或与基线模型相当。这不仅归因于 CQIA 能够对人类问题或指令生成高质量的响应,还归因于其响应更符合现实世界的人类沟通模式,从而导致更高的人类偏好。

该研究还在 SafetyBench 上评估了模型的安全性,结果如下表 4 所示:

在 COIG Subset 数据上训练的模型性能如下表 5 所示:





作者:APPSO
来源:mp.weixin.qq.com/s/BN52IrDg-xNosxkJ6MbNvA
收起阅读 »

Geoserver:小程序巨丝滑渲染海量点位

web
文章最后有效果图 需求 在小程序上绘制 40000+ 的点位。 难点 众所周知小程序的 map 组件性能低下,同时渲染几百个 marker 就会卡顿,一旦加上 callout 弹窗,这个数量可能会降到几十个,如果使用了 自定义弹窗(custom-callou...
继续阅读 »

文章最后有效果图



需求


在小程序上绘制 40000+ 的点位。


难点


众所周知小程序的 map 组件性能低下,同时渲染几百个 marker 就会卡顿,一旦加上 callout 弹窗,这个数量可能会降到几十个,如果使用了 自定义弹窗(custom-callout) 会更卡,所以渲染 4w+ 的点,用常规方法是不可能实现的。


方案


按需加载


按需加载即按屏幕坐标加载,只显示视野范围内的点位,需要后端配合在接口中新增 bbox(Bounding box) 参数,再从数据库中查出范围内的点。


小程序端需要使用视野变化监听方法实时更新,虽然请求和渲染频繁,但是在缩放等级较大时,有很高的性能:


<map bindregionchange="regionChanged" markers="{{markers}}">

regionChanged(e){
this.data.bbox = [     [e.detail.region.southwest.longitude, e.detail.region.southwest.latitude],
    [e.detail.region.northeast.longitude, e.detail.region.northeast.latitude],
  ]
   // 执行获取点、渲染点的操作
}

需要注意的是,目前的微信版本(8.0.47),基础库3.3.4该方法不可用,见 微信开放社区


如果遇到 bindregionchange 不可用时,可以用 bind:touchend 方法代替,手动获取范围


    setBbox() {
     mapCtx = wx.createMapContext('map', this)
     mapCtx.getRegion({
       success: (res) => {
         let bbox = [
          [res.southwest.longitude, res.southwest.latitude],
          [res.northeast.longitude, res.northeast.latitude],
        ]
         // 执行获取点、渲染点的操作
        })
    })
  }

使用了按需渲染后,在缩放等级较大时,已经可以有很好的效果,移动屏幕时基本可以秒加载出新的点,同时清除掉屏幕范围外的点。


然而,在点位多的时候,我们收到了 setData 长度超出的报错,页面也异常卡顿。


优化渲染方式


小程序的 setData 方法最多只能更新 1M 的数据,超过这个数据会报错,并严重卡顿,即使不超过,在数据量较大时,也会非常卡顿,为了解决这个问题,我们不能再使用 setData 去渲染数据。


小程序提供了专门渲染点的方法: addMarkers


// 执行获取点、渲染点的操作处,使用该方法,并设置 clear: true 。这样就达到了上面说的,更新点时,旧的点会被清除。


然而,这并没有解决根本问题,我们现在可以做到渲染远远大于1M的数据,并渲染时不会报错,但是由于小程序 map 组件的渲染策略,我们的点会一个一个渲染上去,我们知道更新 canvas 代价是很大的,尤其是像 marker 这种携带很多必要信息的东西。


这里我们尝试将 marker 携带的参数压缩到极致,仅保留经纬度、颜色状态信息、id、callout,效果依然差强人意。


并且,由于小程序 marker 的 callout 不是互斥的,且没有给我们预留参数去设置这一点,所以在我们切换 marker 选中状态时,需要把 marker 数组完全遍历一遍,移除其他的 callout , 并添加新的 callout,这个开销也是巨大不可接受的。


优化选中策略


为了解决切换 marker 选中状态时的开销问题,我们想了一个绝妙的主意,就是将 marker 数组中的 callout 完全移除,只保留 id 等必要字段,在点击时,添加一个新的带 callout 点上去,盖住原来的点,这样看起来就是原有的点被选中了,这样既压缩了 marker 携带参数,又解决了切换选中时必须遍历 marker 数组的问题。


height: 20,
width: 17,
iconPath: this.data.markerIcons[this.getMarkerType(item)],
latitude: item.point[1],
longitude: item.point[0],
id: this.getUniqueNumber(item.uid), //id 必须是数字
storeCode: item.uid,
//callout:{...} // 不要此项
customCallout: {} //必须加,不然会有一个没有内容的弹窗,这个可以阻止默认弹窗弹出

优化海量点渲染策略


经过上面的优化,我们的小程序已经可以高性能的显示点位了,但是当缩放等级低时(12以下),点位多起来了,我们目前的方法就显得力不从心了。


如果点位无限多,我们又该如何优化呢?


聚合


聚合指的是将临近的点位聚合成一个大点,从而达到渲染点数变少、提高性能的方法。


此方法经过实测,发现当点达到一定量级的时候,用了反而比不用还卡,因为每当你缩放地图时,都需要计算聚合,当计算压力大于渲染压力时,聚合反而成了一种负担,而不是优化了。


所以我们不用聚合。


小程序个性化图层


小程序提供了付费功能:个性化图层,可以上传海量数据并生成一个小程序支持加载的图层。遗憾的是这种方法只适合静态数据,对于经常需要变动的数据,这种方法的实时性得不到保证,只能通过手动在后台更新数据。


所以此方案也不可用。


瓦片


小程序 map 是不支持瓦片(个性化图层除外)加载的,但是我们知道,瓦片就是一张图片而已,那么小程序可以在地图上放图片吗,答案是可以:addGroundOverlay


我们决定朝着此方案努力,请看下文。


搭建 geoserver


首先到 geoserver官网 下载geoserver本体,geoserver是为数不多几个推荐 windows 平台的大型工具软件,下载前注意,geoserver对 jdk 版本有要求,版本不一致会导致 geoserver 启动失败等问题。


image.png
我们的服务器是 linux ,所以下载了linux版本,到服务器找个位置 直接 unzip 就可以了。


安装完之后,需要先编辑 start.ini 调整一个合适的空闲端口,作为后面web端管理页面的地址端口。别忘了在防火墙开启此端口。


最后在 bin 中有一个 startup.sh , 使用 nohup 命令设置后台运行。


此时在浏览器输入服务器地址和你刚刚设置的端口号,最后加上 /geoserver,即可看到geoserver的管理页面。


image.png
初始用户名密码:admin geoserver


登录完成后可以看到全部功能


image.png
点击数据存储 -> 添加新的数据存储,即可添加数据并发布图层。


可以看到支持 PostGis,使用 PostGis 作为数据源,图层会实时更新,也就是说,当数据变化时,无需任何代码和人工干预。


当数据源添加完成后,需要新建一个图层,并指定为刚刚新建的数据源。


此时,在图层预览页面即可看到刚刚创建的图层了,当然此时的图层使用的是默认样式,需要编写SLD(xml格式)的样式文件去指定样式,这对于我们来说无疑是一种负担。


好在 geoserver 有 css 插件,安装此插件并重启geoserver,即可使用 css 编写图层样式。


* {
 mark-size:8px;
}
[control_sts == 1] {
 mark:url("https://entropy.xxx.cn/xx/dotgreen.png");
}
[control_sts == 0] {
 mark:url("https://entropy.xxx.cn/xx/dotgray.png");
}

可以看到,它与标准css还是有一些差异的,像mark、mark-size在标准css中是不存在的。


指定样式后,在图层预览页面,可以看到效果


image.png


打开控制台,可以看到网络请求中的地址长这样:


http://xxx:8089/geoserver/cite/wms?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&FORMAT=image%2Fpng&TRANSPARENT=true&STYLES&LAYERS=cite%3Axc_store_geo&exceptions=application%2Fvnd.ogc.se_inimage&SRS=EPSG%3A4326&WIDTH=670&HEIGHT=768&BBOX=114.4720458984375%2C37.7874755859375%2C118.1524658203125%2C42.0062255859375

放到浏览器窗口打开,发现是一张png图片,那么我们刚好可以使用小程序的 addGroundOverlay 添加到地图上。


SERVICE: WMS
VERSION: 1.1.1
REQUEST: GetMap
FORMAT: image/png
TRANSPARENT: true
STYLES:
LAYERS: xx:xxxx
exceptions: application/vnd.ogc.se_inimage
SRS: EPSG:4326
WIDTH: 670
HEIGHT: 768
BBOX: 114.4720458984375,37.7874755859375,118.1524658203125,42.0062255859375

看一下这些参数,出了 BBOX ,其他的写固定值就可以了。


这里注意,宽高值,需要设置为小程序中地图元素的大小,单位是 px。


在小程序中拼装WMS地址


比较简单,直接看代码:


    setTileImage(params: { LAYERS: string[], BBOX: string, SCREEN_WIDTH: number, SCREEN_HEIGHT: number, CQL_FILTER: string }) {
     mapCtx = wx.createMapContext('map', this)
     this.removeTileImage().then(() => {
       for (let index in params.LAYERS) {
         let id = +(9999 + index)
         !this.data.groundOverlayIds.includes(id) && this.data.groundOverlayIds.push(id)
         let data: any = {
           id: +(9999 + index),
           zIndex: 999,
           src: `http://xxx:8089/geoserver/cite/wms?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&LAYERS=${params.LAYERS[index]}&STYLES=&exceptions=application/vnd.ogc.se_inimage&FORMAT=image/png&TRANSPARENT=true&FORMAT_OPTIONS=antialias:full&SRS=EPSG:4326&BBOX=${params.BBOX}&WIDTH=${params.SCREEN_WIDTH * 2}&HEIGHT=${params.SCREEN_HEIGHT * 2}&CQL_FILTER=${params.CQL_FILTER}`,
           bounds: {
             southwest: {
               latitude: +params.BBOX.split(',')[1],
               longitude: +params.BBOX.split(',')[0]
            },
             northeast: {
               latitude: +params.BBOX.split(',')[3],
               longitude: +params.BBOX.split(',')[2]
            }
          }
        }
         mapCtx.addGroundOverlay({
           ...data,
        })
      }
    })
  },

我这里封装了一个可以接受多个图层的方法,这里值得注意的是,我没有使用 updateGroundOverlay 方法去更新图层,而是先使用 removeGroundOverlay 移除,再重新添加的,这是因为updateGroundOverlay有一个bug,我不说,你可以自己试试。


完成


f42f89c10e3c044f3b8e0200d7dfa52a.webp
至此已经完全实现了小程序的海量点的渲染,无论点有多少,我们都只需要渲染一张图片而已,性能好的一批。


作者:德莱厄斯
来源:juejin.cn/post/7348363874965028864
收起阅读 »

错过Android主线程空闲期,你可能损失的不仅仅是性能

在Android应用程序的开发过程中,性能优化一直是开发者关注的焦点之一。在这个背景下,Android系统提供了一项强大的工具——IdleHandler,它能够帮助开发者在应用程序的空闲时段执行任务,从而提高应用的整体性能。IdleHandler的机制基于An...
继续阅读 »

在Android应用程序的开发过程中,性能优化一直是开发者关注的焦点之一。在这个背景下,Android系统提供了一项强大的工具——IdleHandler,它能够帮助开发者在应用程序的空闲时段执行任务,从而提高应用的整体性能。IdleHandler的机制基于Android主线程的空闲状态,使得开发者能够巧妙地利用这些空闲时间执行一些耗时的操作,而不影响用户界面的流畅性。


在深入研究IdleHandler之前,让我们先了解一下它的基本原理,以及为何它成为Android性能优化的重要组成部分。


IdleHandler的基本原理


Android应用的主线程通过一个消息循环(Message Loop)来处理各种事件和任务。当主线程没有新的消息需要处理时,它就处于空闲状态。这就是IdleHandler发挥作用的时机。


通过注册IdleHandler来告诉系统在主线程空闲时执行特定的任务。当主线程进入空闲状态时,系统会依次调用注册的IdleHandler,执行相应的任务。


IdleHandler与Handler和MessageQueue密切相关。它通过MessageQueue的空闲时间来执行任务。每当主线程处理完一个消息后,系统会检查是否有注册的IdleHandler需要执行。


空闲状态的定义


了解什么时候主线程被认为是空闲的至关重要。一般情况下,Android系统认为主线程在处理完所有消息后即处于空闲状态。IdleHandler通过这个定义,能够在保证不影响用户体验的前提下执行一些耗时的操作。


	// 没有消息,判断是否有IdleHandler
if (pendingIdleHandlerCount < 0
&& (mMessages == null || now < mMessages.when)) {
pendingIdleHandlerCount = mIdleHandlers.size();
}
if (pendingIdleHandlerCount <= 0) {
// No idle handlers to run. Loop and wait some more.
mBlocked
= true;
continue;
}

if (mPendingIdleHandlers == null) {
mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
}
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);

....

// 执行IdleHandler
for (int i = 0; i < pendingIdleHandlerCount; i++) {
final IdleHandler idler = mPendingIdleHandlers[i];
mPendingIdleHandlers[i] = null; // release the reference to the handler

boolean keep = false;
try {
keep = idler.queueIdle();
} catch (Throwable t) {
Log.wtf(TAG, "IdleHandler threw exception", t);
}

if (!keep) {
synchronized (this) {
mIdleHandlers.remove(idler);
}
}
}

如何使用IdleHandler


使用IdleHandler可以执行一些轻量级的任务,例如加载数据、更新UI等。以下是使用IdleHandler的几个使用技巧:



  1. 注册IdleHandler:


Looper.myQueue().addIdleHandler(MyIdleHandler())

class MyIdleHandler : MessageQueue.IdleHandler {
override fun queueIdle(): Boolean {
// 在主线程空闲时执行的任务逻辑
performIdleTask()
// 返回 false,表示任务处理完毕,不再执行
return false
}

private fun performIdleTask() {
// 具体的任务逻辑
// ...
}
}


  1. 取消注册


当不需要继续执行任务时,可以通过removeIdleHandler方法取消注册


Looper.myQueue().removeIdleHandler(idleHandler);

IdleHandler的适用场景



  • 轻量级任务:IdleHandler主要用于执行轻量级的任务。由于它是在主线程空闲时执行,所以不适合执行耗时的任务。

  • 主线程空闲时执行:IdleHandler通过在主线程空闲时被调用,避免了主线程的阻塞。因此,适用于需要在主线程执行的任务,并且这些任务对于用户体验的影响较小。

  • 优先级较低的任务:如果有多个任务注册了IdleHandler,系统会按照注册的顺序调用它们的queueIdle方法。因此,适用于需要在较低优先级下执行的任务。


总的来说IdleHandler适用于需要在主线程空闲时执行的轻量级任务,以提升应用的性能和用户体验。


高级应用



  1. 性能监控与优化
    利用 IdleHandler 可以实现性能监控和优化,例如统计每次空闲时的内存占用情况,或者执行一些内存释放操作。

  2. 预加载数据
    在用户操作前,通过 IdleHandler 提前加载一些可能会用到的数据,提高用户体验。

  3. 动态资源加载
    利用空闲时间预加载和解析资源,减轻在用户操作时的资源加载压力。


性能优化技巧


虽然IdleHandler提供了一个方便的机制来在主线程空闲时执行任务,但在使用过程中仍需注意一些性能方面的问题。



  1. 任务的轻量级处理: 确保注册的IdleHandler中的任务是轻量级的,不要在空闲时执行过于复杂或耗时的操作,以免影响主线程的响应性能。

  2. **避免频繁注册和取消IdleHandler: **频繁注册和取消IdleHandler可能会引起性能问题,因此建议在应用的生命周期内尽量减少注册和取消的操作。可以在应用启动时注册IdleHandler,在应用退出时取消注册。

  3. **合理设置任务执行频率: **根据任务的性质和执行需求,合理设置任务的执行频率。不同的任务可能需要在不同的时间间隔内执行,这样可以更好地平衡性能和功能需求。


结语


通过深度解析 IdleHandler 的原理和高级应用,让我们更好地利用这一工具进行性能优化。在实际项目中,灵活运用 IdleHandler 可以有效提升应用的响应速度和用户体验。希望本文能够激发大家对于Android性能优化的更多思考和实践。




作者:午后一小憩
来源:juejin.cn/post/7307471896693522471
收起阅读 »

永不生锈的螺丝钉!一款简洁好用的数据库表结构文档生成器

大家好,我是 Java陈序员。 在企业级开发中,我们经常会有编写数据库表结构文档的需求,常常需要手写维护文档,很是繁琐。 今天,给大家介绍一款数据库表结构文档生成工具。 关注微信公众号:【Java陈序员】,获取开源项目分享、AI副业分享、超200本经典计算机...
继续阅读 »

大家好,我是 Java陈序员


在企业级开发中,我们经常会有编写数据库表结构文档的需求,常常需要手写维护文档,很是繁琐。


今天,给大家介绍一款数据库表结构文档生成工具。



关注微信公众号:【Java陈序员】,获取开源项目分享、AI副业分享、超200本经典计算机电子书籍等。



项目介绍


screw —— 螺丝钉(代表企业级开发中一颗永不生锈的螺丝钉),是一款简洁好用的数据库表结构文档生成工具。



screw 主打简洁、轻量,支持多种数据库、多种格式文档,可自定义模板进行灵活拓展。



  • 支持 MySQL、MariaDB、TIDB、Oracle 多种数据库




  • 支持生成 HTML、Word、MarkDown 三种格式的文档



快速上手


screw 普通方式Maven 插件的两种方式来生成文档。


普通方式


1、引入依赖


<!-- 引入数据库驱动,这里以 MySQL 为例 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.29</version>
</dependency>

<!-- 引入 screw -->
<dependency>
<groupId>cn.smallbun.screw</groupId>
<artifactId>screw-core</artifactId>
<version>1.0.5</version>
</dependency>

2、编写代码


public class DocumentGeneration {

/**
* 文档生成
*/

@Test
public void documentGeneration() {

// 文档生成路径
String fileOutputPath = "D:\\database";

// 数据源
HikariConfig hikariConfig = new HikariConfig();
// 指定数据库驱动
hikariConfig.setDriverClassName("com.mysql.cj.jdbc.Driver");
// 设置数据库连接地址
hikariConfig.setJdbcUrl("jdbc:mysql://localhost:3306/database");
// 设置数据库用户
hikariConfig.setUsername("root");
// 设置数据库密码
hikariConfig.setPassword("root");
// 设置可以获取 tables remarks 信息
hikariConfig.addDataSourceProperty("useInformationSchema", "true");
hikariConfig.setMinimumIdle(2);
hikariConfig.setMaximumPoolSize(5);

DataSource dataSource = new HikariDataSource(hikariConfig);
// 生成配置
EngineConfig engineConfig = EngineConfig.builder()
// 生成文件路径
.fileOutputDir(fileOutputPath)
// 打开目录
.openOutputDir(true)
// 文件类型 HTML、WORD、MD 三种类型
.fileType(EngineFileType.HTML)
// 生成模板实现
.produceType(EngineTemplateType.freemarker)
// 自定义文件名称
.fileName("Document")
.build();

// 忽略表
ArrayList<String> ignoreTableName = new ArrayList<>();
ignoreTableName.add("test_user");
ignoreTableName.add("test_group");

//忽略表前缀
ArrayList<String> ignorePrefix = new ArrayList<>();
ignorePrefix.add("test_");

//忽略表后缀
ArrayList<String> ignoreSuffix = new ArrayList<>();
ignoreSuffix.add("_test");

ProcessConfig processConfig = ProcessConfig.builder()
// 指定生成逻辑、当存在指定表、指定表前缀、指定表后缀时,将生成指定表,其余表不生成、并跳过忽略表配置
// 根据名称指定表生成
.designatedTableName(new ArrayList<>())
// 根据表前缀生成
.designatedTablePrefix(new ArrayList<>())
// 根据表后缀生成
.designatedTableSuffix(new ArrayList<>())
// 忽略表名
.ignoreTableName(ignoreTableName)
// 忽略表前缀
.ignoreTablePrefix(ignorePrefix)
// 忽略表后缀
.ignoreTableSuffix(ignoreSuffix)
.build();
//配置
Configuration config = Configuration.builder()
// 版本
.version("1.0.0")
// 描述
.description("数据库设计文档生成")
// 数据源
.dataSource(dataSource)
// 生成配置
.engineConfig(engineConfig)
// 生成配置
.produceConfig(processConfig)
.build();

//执行生成
new DocumentationExecute(config).execute();
}
}

3、执行代码输出文档



Maven 插件


1、引入依赖


<build>
<plugins>
<plugin>
<groupId>cn.smallbun.screw</groupId>
<artifactId>screw-maven-plugin</artifactId>
<version>1.0.5</version>
<dependencies>
<!-- HikariCP -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>3.4.5</version>
</dependency>
<!--mysql driver-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.20</version>
</dependency>
</dependencies>
<configuration>
<!-- 数据库用户名 -->
<username>root</username>
<!-- 数据库密码 -->
<password>password</password>
<!-- 数据库驱动 -->
<driverClassName>com.mysql.cj.jdbc.Driver</driverClassName>
<!-- 数据库连接地址 -->
<jdbcUrl>jdbc:mysql://127.0.0.1:3306/xxxx</jdbcUrl>
<!-- 生成的文件类型 HTML、WORD、MD 三种类型 -->
<fileType>HTML</fileType>
<!-- 打开文件输出目录 -->
<openOutputDir>false</openOutputDir>
<!-- 生成模板 -->
<produceType>freemarker</produceType>
<!-- 文档名称 为空时:将采用[数据库名称-描述-版本号]作为文档名称 -->
<fileName>数据库文档</fileName>
<!-- 描述 -->
<description>数据库文档生成</description>
<!-- 版本 -->
<version>${project.version}</version>
<!-- 标题 -->
<title>数据库文档</title>
</configuration>
<executions>
<execution>
<phase>compile</phase>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

2、执行插件



3、使用 Maven 插件执行的方式会将文档输出到项目根目录的 doc 目录下



文档截图


HTML 类型文档



Word 类型文档



MarkDown 类型文档



自从用了 screw 后,编写数据库文档信息就很方便了,一键生成,剩下的时间就可以用来摸鱼了~


大家如果下次有需要编写数据库文档,可以考虑使用 screw ,建议先把本文收藏起来,下次就不会找不到了~


最后,贴上项目地址:


https://github.com/pingfangushi/screw

最后


推荐的开源项目已经收录到 GitHub 项目,欢迎 Star


https://github.com/chenyl8848/great-open-source-project

或者访问网站,进行在线浏览:


https://chencoding.top:8090/#/


大家的点赞、收藏和评论都是对作者的支持,如文章对你有帮助还请点赞转发支持下,谢谢!



作者:Java陈序员
来源:juejin.cn/post/7354922285093683252
收起阅读 »

统一公司的项目规范

web
初始化项目 vscode 里下好插件:eslint,prettier,stylelint 官网模版创建项目:pnpm create vite react-starter --template react-swc-ts 安装依赖:pnpm i 后面有可能遇到 ...
继续阅读 »

初始化项目



  • vscode 里下好插件:eslint,prettier,stylelint

  • 官网模版创建项目:pnpm create vite react-starter --template react-swc-ts

  • 安装依赖:pnpm i

  • 后面有可能遇到 ts 类型错误,可以提前安装一个pnpm i @types/node -D


配置 npm 使用淘宝镜像



  • 配置npmrc


    registry = "https://registry.npmmirror.com/"



配置 node 版本限制提示



  • package.json 中配置


    "engines": {
    "node": ">=16.0.0"
    },



配置 eslint 检查代码规范



eslint 处理代码规范,prettier 处理代码风格
eslint 选择只检查错误不处理风格,这样 eslint 就不会和 prettier 冲突
react 官网有提供一个 hook 的 eslint (eslint-plugin-react-hooks),用处不大就不使用了




  • 安装:pnpm i eslint -D

  • 生成配置文件:eslint --init(如果没eslint,可以全局安装一个,然后使用npx eslint --init)


    - To check syntax and find problems  //这个选项是eslint默认选项,这样就不会和pretter起风格冲突
    - JavaScript modules (import/export)
    - React
    - YES
    - Browser
    - JSON
    - Yes
    - pnpm


  • 配置eslintrc.json->rules里配置不用手动引入 react,和配置不可以使用 any

  • 注意使用 React.FC 的时候如果报错说没有定义 props 类型,那需要引入一下 react


    "rules": {
    //不用手动引入react
    "react/react-in-jsx-scope": "off",
    //使用any报错
    "@typescript-eslint/no-explicit-any": "error",
    }


  • 工作区配置.vscode>settings.json,配置后 vscode 保存时自动格式化代码风格


    比如写了一个 var a = 100,会被自动格式化为 const a = 100


    {
    "editor.codeActionsOnSave": {
    // 每次保存的时候将代码按照 eslint 格式进行修复
    "source.fixAll.eslint": true,
    //自动格式化
    "editor.formatOnSave": true
    }
    }


  • 配置.eslintignore,eslint 会自动过滤 node_modules


    dist


  • 掌握eslint格式化命令,后面使用 lint-staged 提交代码的时候需要配置


    为什么上面有 vscode 自动 eslint 格式化,还需要命令行: 因为命令行能一次性爆出所有警告问题,便于找到位置修复


    npx eslint . --fix//用npx使用项目里的eslint,没有的话也会去使用全局的eslint
    eslint . --fix //全部类型文件
    eslint . --ext .ts,.tsx --fix //--ext可以指定文件后缀名s

    eslintrc.json 里配置



  • "env": {
    "browser": true,
    "es2021": true,
    "node": true // 因为比如配置vite的时候会使用到
    },



配置 prettier 检查代码风格



prettier 格式化风格,因为使用 tailwind,使用 tailwind 官方插件




  • 安装:pnpm i prettier prettier-plugin-tailwindcss -D

  • 配置.prettierrc.json


    注释要删掉,prettier 的配置文件 json 不支持注释


    {
    "singleQuote": true, // 单引号
    "semi": false, // 分号
    "trailingComma": "none", // 尾随逗号
    "tabWidth": 2, // 两个空格缩进
    "plugins": ["prettier-plugin-tailwindcss"] //tailwind插件
    }


  • 配置.prettierignore


    dist
    pnpm-lock.yaml


  • 配置.vscode>settings.json,配置后 vscode 保存时自动格式化代码风格


    {
    "editor.codeActionsOnSave": {
    // 每次保存的时候将代码按照 eslint 格式进行修复
    "source.fixAll.eslint": true
    },
    //自动格式化
    "editor.formatOnSave": true,
    //风格用prettier
    "editor.defaultFormatter": "esbenp.prettier-vscode"
    }


  • 掌握prettier命令行


    可以让之前没有格式化的错误一次性暴露出来


    npx prettier --write .//使用Prettier格式化所有文件



配置 husky 使用 git hook



记得要初始化一个 git 仓库,husky 能执行 git hook,在 commit 的时候对文件进行操作




  • 安装


    sudo pnpm dlx husky-init


    pnpm install


    npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"',commit-msg 使用 commitlint


    npx husky add .husky/pre-commit "npm run lint-staged",pre-commit 使用 lint-staged



配置 commitlint 检查提交信息



提交规范参考:http://www.conventionalcommits.org/en/v1.0.0/




  • 安装pnpm i @commitlint/cli @commitlint/config-conventional -D

  • 配置.commitlintrc.json


    { extends: ['@commitlint/config-conventional'] }



配置 lint-staged 增量式检查



  • 安装pnpm i -D lint-staged

  • 配置package.json


    "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
    "prepare": "husky install",
    "lint-staged": "npx lint-staged"//新增,对应上面的husky命令
    },


  • 配置.lintstagedrc.json


    {
    "*.{ts,tsx,json}": ["prettier --write", "eslint --fix"],
    "*.css": ["stylelint --fix", "prettier --write"]
    }



配置 vite(代理/别名/drop console 等)



如果有兼容性考虑,需要使用 legacy 插件,vite 也有 vscode 插件,也可以下载使用




  • 一些方便开发的配置


    import { defineConfig } from 'vite'
    import react from '@vitejs/plugin-react-swc'
    import path from 'path'

    // https://vitejs.dev/config/
    export default defineConfig({
    esbuild: {
    drop: ['console', 'debugger']
    },
    css: {
    // 开css sourcemap方便找css
    devSourcemap: true
    },
    plugins: [react()],
    server: {
    // 自动打开浏览器
    open: true
    proxy: {
    '/api': {
    target: 'https://xxxxxx',
    changeOrigin: true,
    rewrite: (path) => path.replace(/^\/api/, '')
    }
    }
    },
    resolve: {
    // 配置别名
    alias: { '@': path.resolve(__dirname, './src') }
    },
    //打包路径变为相对路径,用liveServer打开,便于本地测试打包后的文件
    base: './'
    })


  • 配置打包分析,用 legacy 处理兼容性


    pnpm i rollup-plugin-visualizer -D


    pnpm i @vitejs/plugin-legacy -D,实际遇到了再看官网用


    import { defineConfig } from 'vite'
    import react from '@vitejs/plugin-react-swc'
    import { visualizer } from 'rollup-plugin-visualizer'
    import legacy from '@vitejs/plugin-legacy'
    import path from 'path'
    // https://vitejs.dev/config/
    export default defineConfig({
    css: {
    // 开css sourcemap方便找css
    devSourcemap: true
    },
    plugins: [
    react(),
    visualizer({
    open: false // 打包完成后自动打开浏览器,显示产物体积报告
    }),
    //考虑兼容性,实际遇到了再看官网用
    legacy({
    targets: ['ie >= 11'],
    additionalLegacyPolyfills: ['regenerator-runtime/runtime']
    })
    ],
    server: {
    // 自动打开浏览器
    open: true
    },
    resolve: {
    // 配置别名
    alias: { '@': path.resolve(__dirname, './src') }
    },
    //打包路径变为相对路径,用liveServer打开,便于本地测试打包后的文件
    base: './'
    })


  • 如果想手机上看网页,可以pnpm dev --host

  • 如果想删除 console,可以按h去 help 帮助,再按c就可以 clear console


配置 tsconfig



  • tsconfig.json 需要支持别名


    {
    "compilerOptions": {
    "target": "ESNext",
    "useDefineForClassFields": true,
    "lib": ["DOM", "DOM.Iterable", "ESNext"],
    "allowJs": false,
    "skipLibCheck": true,
    "esModuleInterop": false,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "ESNext",
    "moduleResolution": "Node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "baseUrl": "./",
    "paths": {
    "@/*": ["src/*"]
    }
    },
    "include": ["src"],
    "references": [{ "path": "./tsconfig.node.json" }]
    }



配置 router



  • 安装:pnpm i react-router-dom

  • 配置router->index.ts


    import { lazy } from 'react'
    import { createBrowserRouter } from 'react-router-dom'
    const Home = lazy(() => import('@/pages/home'))
    const router = createBrowserRouter([
    {
    path: '/',
    element: <Home></Home>
    }
    ])
    export default router


  • 配置main.tsx


    import { RouterProvider } from 'react-router-dom'
    import ReactDOM from 'react-dom/client'
    import './global.css'
    import router from './router'

    ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
    <RouterProvider router={router} />
    )



配置 zustand 状态管理



  • 安装pnpm i zustand

  • store->index.ts


    import { create } from 'zustand'

    interface appsState {
    nums: number
    setNumber: (nums: number) => void
    }

    const useAppsStore = create<appsState>((set) => ({
    nums: 0,
    setNumber: (num) => {
    return set(() => ({
    nums: num
    }))
    }
    }))

    export default useAppsStore


  • 使用方法


    import Button from '@/comps/custom-button'
    import useAppsStore from '@/store/app'
    const ZustandDemo: React.FC = () => {
    const { nums, setNumber } = useAppsStore()
    const handleNum = () => {
    setNumber(nums + 1)
    }
    return (
    <div className="p-10">
    <h1 className="my-10">数据/更新</h1>
    <Button click={handleNum}>点击事件</Button>
    <h1 className="py-10">{nums}</h1>
    </div>

    )
    }

    export default ZustandDemo



配置 antd



  • 新版本的 antd,直接下载就可以用,如果用到它的图片再单独下载pnpm i antd

  • 注意 antd5 版本的 css 兼容性不好,如果项目有兼容性要求,需要去单独配置


配置 Tailwind css


pnpm i tailwindcss autoprefixer postcss


tailwind.config.cjs


// 打包后会有1kb的css用不到的,没有影响
// 用了antd组件关系也不大,antd5的样式是按需的
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: 'class',
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {
// colors: {
// themeColor: '#ff4132',
// textColor: '#1a1a1a'
// },
// 如果写自适应布局,可以指定设计稿为1000px,然后只需要写/10的数值
// fontSize: {
// xs: '3.3vw',
// sm: '3.9vw'
// }
}
},
plugins: []
}

postcss.config.cjs


module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
}

我喜欢新建一个 apply.css 引入到全局


@tailwind base;
@tailwind components;
@tailwind utilities;

.margin-center {
@apply mx-auto my-0;
}

.flex-center {
@apply flex justify-center items-center;
}

.absolute-center {
@apply absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2;
}

封装 fetch 请求



这个封装仅供参考,TS 类型有点小问题



// 可以传入这些配置
interface BaseOptions {
method?: string
credentials?: RequestCredentials
headers?: HeadersInit
body?: string | null
}

// 请求方式
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD'

// 第一层出参
interface ResponseObject {
ok: boolean
error: boolean
status: number
contentType: string | null
bodyText: string
response: Response
}

// 请求头类型
type JSONHeader = {
Accept: string
'Content-Type': string
}

// 创建类
class Request {
private baseOptions: BaseOptions = {}

// 根据传入的 baseOptions 做为初始化参数
constructor(options?: BaseOptions) {
this.setBaseOptions(options || {})
}

public setBaseOptions(options: BaseOptions): BaseOptions {
this.baseOptions = options
return this.baseOptions
}

// 也提供获取 baseOption 的方法
public getBaseOptions(): BaseOptions {
return this.baseOptions
}

// 核心请求 T 为入参类型,ResponseObject 为出参类型
public request<T>(
method: HttpMethod,
url: string,
data?: T, //支持使用get的时候配置{key,value}的query参数
options?: BaseOptions //这里也有个 base 的 method
): Promise<ResponseObject> {
// 默认 baseOptions
const defaults: BaseOptions = {
method
// credentials: 'same-origin'
}

// 收集最后要传入的配置
const settings: BaseOptions = Object.assign(
{},
defaults,
this.baseOptions,
options
)

// 如果 method 格式错误
if (!settings.method || typeof settings.method !== 'string')
throw Error('[fetch-json] HTTP method missing or invalid.')

// 如果 url 格式错误
if (typeof url !== 'string')
throw Error('[fetch-json] URL must be a string.')

// 支持大小写
const httpMethod = settings.method.trim().toUpperCase()

// 如果是GET
const isGetRequest = httpMethod === 'GET'

// 请求头
const jsonHeaders: Partial<JSONHeader> = { Accept: 'application/json' }

// 如果不是 get 设置请求头
if (!isGetRequest && data) jsonHeaders['Content-Type'] = 'application/json'

// 收集最后的headers配置
settings.headers = Object.assign({}, jsonHeaders, settings.headers)

// 获取query参数的key
const paramKeys = isGetRequest && data ? Object.keys(data) : []

// 获取query参数的值
const getValue = (key: keyof T) => (data ? data[key] : '')

// 获取query key=value
const toPair = (key: string) =>
key + '=' + encodeURIComponent(getValue(key as keyof T) as string)

// 生成 key=value&key=value 的query参数
const params = () => paramKeys.map(toPair).join('&')

// 收集最后的 url 配置
const requestUrl = !paramKeys.length
? url
: url + (url.includes('?') ? '&' : '?') + params()

// get没有body
settings.body = !isGetRequest && data ? JSON.stringify(data) : null

// 做一层res.json()
const toJson = (value: Response): Promise<ResponseObject> => {
// 拿到第一次请求的值
const response = value

const contentType = response.headers.get('content-type')
const isJson = !!contentType && /json|javascript/.test(contentType)

const textToObj = (httpBody: string): ResponseObject => ({
ok: response.ok,
error: !response.ok,
status: response.status,
contentType: contentType,
bodyText: httpBody,
response: response
})

const errToObj = (error: Error): ResponseObject => ({
ok: false,
error: true,
status: 500,
contentType: contentType,
bodyText: 'Invalid JSON [' + error.toString() + ']',
response: response
})

return isJson
? // 如果是json,用json()
response.json().catch(errToObj)
: response.text().then(textToObj)
}

// settings做一下序列化
const settingsRequestInit: RequestInit = JSON.parse(
JSON.stringify(settings)
)

// 最终请求fetch,再通过then就能取到第二层res
return fetch(requestUrl, settingsRequestInit).then(toJson)
}

public get<T>(
url: string,
params?: T,
options?: BaseOptions
): Promise<ResponseObject> {
return this.request<T>('GET', url, params, options)
}

public post<T>(
url: string,
resource: T,
options?: BaseOptions
): Promise<ResponseObject> {
return this.request<T>('POST', url, resource, options)
}

public put<T>(
url: string,
resource: T,
options?: BaseOptions
): Promise<ResponseObject> {
return this.request<T>('PUT', url, resource, options)
}

public patch<T>(
url: string,
resource: T,
options?: BaseOptions
): Promise<ResponseObject> {
return this.request<T>('PATCH', url, resource, options)
}

public delete<T>(
url: string,
resource: T,
options?: BaseOptions
): Promise<ResponseObject> {
return this.request<T>('DELETE', url, resource, options)
}
}

const request = new Request()

export { request, Request }


如果用 axios 请求


request.ts


import axios from 'axios'
import { AxiosInstance } from 'axios'
import { errorHandle, processData, successHandle } from './resInterceptions'
import { defaultRequestInterception } from './reqInterceptions'
const TIMEOUT = 5 * 1000

class Request {
instance: AxiosInstance
constructor() {
this.instance = axios.create()
this.init()
}

private init() {
this.setDefaultConfig()
this.reqInterceptions()
this.resInterceptions()
}
private setDefaultConfig() {
this.instance.defaults.baseURL = import.meta.env.VITE_BASE_URL
this.instance.defaults.timeout = TIMEOUT
}
private reqInterceptions() {
this.instance.interceptors.request.use(defaultRequestInterception)
}
private resInterceptions() {
this.instance.interceptors.response.use(processData)
this.instance.interceptors.response.use(successHandle, errorHandle)
}
}

export default new Request().instance

reqInterceptions.ts


import type { InternalAxiosRequestConfig } from 'axios'

const defaultRequestInterception = (config: InternalAxiosRequestConfig) => {
// TODO: 全局请求拦截器: 添加token
return config
}

export { defaultRequestInterception }

resInterceptions.ts


import { AxiosError, AxiosResponse } from 'axios'
import { checkStatus } from './checkStatus'

const processData = (res: AxiosResponse) => {
// TODO:统一处理数据结构
return res.data
}

const successHandle = (res: AxiosResponse) => {
// TODO:处理一些成功回调,例如请求进度条
return res.data
}

const errorHandle = (err: AxiosError) => {
if (err.status) checkStatus(err.status)
else return Promise.reject(err)
}

export { processData, successHandle, errorHandle }

checkStatus.ts


export function checkStatus(status: number, msg?: string): void {
let errMessage = ''

switch (status) {
case 400:
errMessage = `${msg}`
break
case 401:
break
case 403:
errMessage = ''
break
// 404请求不存在
case 404:
errMessage = ''
break
case 405:
errMessage = ''
break
case 408:
errMessage = ''
break
case 500:
errMessage = ''
break
case 501:
errMessage = ''
break
case 502:
errMessage = ''
break
case 503:
errMessage = ''
break
case 504:
errMessage = ''
break
case 505:
errMessage = ''
break
default:
}
if (errMessage) {
// TODO:错误提示
// createErrorModal({title: errMessage})
}
}

api.ts


import request from '@/services/axios/request'
import { ReqTitle } from './type'

export const requestTitle = (): Promise<ReqTitle> => {
return request.get('/api/一个获取title的接口')
}

type.ts


export type ReqTitle = {
title: string
}

配置 mobx(可不用)



  • 安装pnpm i mobx mobx-react-lite

  • 配置model->index.ts


    import { makeAutoObservable } from 'mobx'

    const store = makeAutoObservable({
    count: 1,
    setCount: (count: number) => {
    store.count = count
    }
    })

    export default store


  • 使用方法举个 🌰


    import store from '@/model'
    import { Button } from 'antd'
    import { observer, useLocalObservable } from 'mobx-react-lite'
    const Home: React.FC = () => {
    const localStore = useLocalObservable(() => store)
    return (
    <div>
    <Button>Antd</Button>
    <h1>{localStore.count}</h1>
    </div>

    )
    }

    export default observer(Home)



配置 changelog(可不用)


pnpm i conventional-changelog-cli -D


第一次先执行conventional-changelog -**p** angular -**i** CHANGELOG.md -s -r 0全部生成之前的提交信息


配置个脚本,版本变化打 tag 的时候可以使用


"scripts": {
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s"
}

配置 editorConfig 统一编辑器(可不用)



editorConfig,可以同步编辑器差异,其实大部分工作 prettier 做了,需要下载 editorConfig vscode 插件
有编辑器差异的才配置一下,如果团队都是 vscode 就没必要了




  • 配置editorconfig


    #不再向上查找.editorconfig
    root = true
    # *表示全部文件
    [*]
    #编码
    charset = utf-8
    #缩进方式
    indent_style = space
    #缩进空格数
    indent_size = 2
    #换行符lf
    end_of_line = lf



配置 stylelint 检查 CSS 规范(可不用)



stylelint 处理 css 更专业,但是用了 tailwind 之后用处不大了




  • 安装:pnpm i -D stylelint stylelint-config-standard

  • 配置.stylelintrc.json


    {
    "extends": "stylelint-config-standard"
    }


  • 配置.vscode>settings.json,配置后 vscode 保存时自动格式化 css


    {
    "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true, // 每次保存的时候将代码按照 eslint 格式进行修复
    "source.fixAll.stylelint": true //自动格式化stylelint
    },
    "editor.formatOnSave": true, //自动格式化
    "editor.defaultFormatter": "esbenp.prettier-vscode" //风格用prettier
    }


  • 掌握stylelint命令行


    npx stylelint "**/*.css" --fix//格式化所有css,自动修复css



下面是 h5 项目(可不用)


配置vconsole(h5)



  • 安装pnpm i vconsole -D

  • main.tsx里新增


    import VConsole from 'vconsole'
    new VConsole({ theme: 'dark' })



antd 换成 mobile antd(h5)



  • pnpm remove antd

  • pnpm add antd-mobile


配置 postcss-px-to-viewport(废弃)



  • 把蓝湖设计稿尺寸固定为 1000px(100px我试过蓝湖直接白屏了),然后你点出来的值比如是 77px,那你只需要写 7.7vw 就实现了自适应布局,就不再需要这个插件了

  • 安装:pnpm i postcss-px-to-viewport -D

  • 配置postcss.config.cjs


    module.exports = {
    plugins: {
    'postcss-px-to-viewport': {
    landscape: false, // 是否添加根据 landscapeWidth 生成的媒体查询条件 @media (orientation: landscape)
    landscapeUnit: 'vw', // 横屏时使用的单位
    landscapeWidth: 568, // 横屏时使用的视口宽度
    unitToConvert: 'px', // 要转化的单位
    viewportWidth: 750, // UI设计稿的宽度
    unitPrecision: 5, // 转换后的精度,即小数点位数
    propList: ['*'], // 指定转换的css属性的单位,*代表全部css属性的单位都进行转换
    viewportUnit: 'vw', // 指定需要转换成的视窗单位,默认vw
    fontViewportUnit: 'vw', // 指定字体需要转换成的视窗单位,默认vw
    selectorBlackList: ['special'], // 指定不转换为视窗单位的类名,
    minPixelValue: 1, // 默认值1,小于或等于1px则不进行转换
    mediaQuery: true, // 是否在媒体查询的css代码中也进行转换,默认false
    replace: true, // 是否转换后直接更换属性值
    exclude: [/node_modules/] // 设置忽略文件,用正则做目录名匹配
    }
    }
    }



作者:imber
来源:juejin.cn/post/7241875166887444541
收起阅读 »

技术总监写的十个方法,让我精通了lambda表达式

前公司的技术总监写了工具类,对Java Stream 进行二次封装,使用起来非常爽,全公司都在用。 我自己照着写了一遍,改了名字,分享给大家。 一共整理了10个工具方法,可以满足 Collection、List、Set、Map 之间各种类型转化。例如 将 C...
继续阅读 »

前公司的技术总监写了工具类,对Java Stream 进行二次封装,使用起来非常爽,全公司都在用。


我自己照着写了一遍,改了名字,分享给大家。


一共整理了10个工具方法,可以满足 Collection、List、Set、Map 之间各种类型转化。例如



  1. Collection<OrderItem> 转化为 List<OrderItem>

  2. Collection<OrderItem> 转化为 Set<OrderItem>

  3. List<OrderItem> 转化为 List<Long>

  4. Set<OrderItem> 转化为 Set<Long>

  5. Collection<OrderItem> 转化为 List<Long>

  6. Collection<OrderItem> 转化为 Set<Long>

  7. Collection<OrderItem>中提取 Key, Map 的 Value 就是类型 OrderItem

  8. Collection<OrderItem>中提取 Key, Map 的 Value 根据 OrderItem 类型进行转化。

  9. Map<Long, OrderItem> 中的value 转化为 Map<Long, Double>

  10. value 转化时,lamada表达式可以使用(v)->{}, 也可以使用 (k,v)->{ }


Collection 集合类型到 Map类型的转化。


Collection 转化为 Map


由于 List 和 Set 是 Collection 类型的子类,所以只需要实现Collection 类型转化为 Map 类型即可。
Collection转化为 Map 共分两个方法



  1. Collection<OrderItem> Map<Key, OrderItem>,提取 Key, Map 的 Value 就是类型 OrderItem

  2. Collection<OrderItem>Map<Key,Value> ,提取 Key, Map 的 Value 根据 OrderItem 类型进行转化。


使用样例


代码示例中把Set<OrderItem> 转化为 Map<Long, OrderItem>Map<Long ,Double>


@Test
public void testToMap() {
Collection<OrderItem> collection = coll;
Set<OrderItem> set = toSet(collection);

Map<Long, OrderItem> map = toMap(set, OrderItem::getOrderId);
}

@Test
public void testToMapV2() {
Collection<OrderItem> collection = coll;
Set<OrderItem> set = toSet(collection);

Map<Long, Double> map = toMap(set, OrderItem::getOrderId, OrderItem::getActPrice);
}

代码展示


public static <T, K> Map<K, T> toMap(Collection<T> collection, Function<? super T, ? extends K> keyMapper) {
return toMap(collection, keyMapper, Function.identity());
}

public static <T, K, V> Map<K, V> toMap(Collection<T> collection,
Function<? super T, ? extends K> keyFunction,
Function<? super T, ? extends V> valueFunction)
{
return toMap(collection, keyFunction, valueFunction, pickSecond());
}

public static <T, K, V> Map<K, V> toMap(Collection<T> collection,
Function<? super T, ? extends K> keyFunction,
Function<? super T, ? extends V> valueFunction,
BinaryOperator<V> mergeFunction)
{
if (CollectionUtils.isEmpty(collection)) {
return new HashMap<>(0);
}

return collection.stream().collect(Collectors.toMap(keyFunction, valueFunction, mergeFunction));
}

public static <T> BinaryOperator<T> pickFirst() {
return (k1, k2) -> k1;
}
public static <T> BinaryOperator<T> pickSecond() {
return (k1, k2) -> k2;
}

Map格式转换


转换 Map 的 Value



  1. 将 Map<Long, OrderItem> 中的value 转化为 Map<Long, Double>

  2. value 转化时,lamada表达式可以使用(v)->{}, 也可以使用 (k,v)->{ }。


测试样例


@Test
public void testConvertValue() {
Collection<OrderItem> collection = coll;
Set<OrderItem> set = toSet(collection);

Map<Long, OrderItem> map = toMap(set, OrderItem::getOrderId);

Map<Long, Double> orderId2Price = convertMapValue(map, item -> item.getActPrice());
Map<Long, String> orderId2Token = convertMapValue(map, (id, item) -> id + item.getName());

}

代码展示


public static <K, V, C> Map<K, C> convertMapValue(Map<K, V> map, 
BiFunction<K, V, C> valueFunction,
BinaryOperator<C> mergeFunction)
{
if (isEmpty(map)) {
return new HashMap<>();
}
return map.entrySet().stream().collect(Collectors.toMap(
e -> e.getKey(),
e -> valueFunction.apply(e.getKey(), e.getValue()),
mergeFunction
));
}

public static <K, V, C> Map<K, C> convertMapValue(Map<K, V> originMap, BiFunction<K, V, C> valueConverter) {
return convertMapValue(originMap, valueConverter, Lambdas.pickSecond());
}

public static <T> BinaryOperator<T> pickFirst() {
return (k1, k2) -> k1;
}
public static <T> BinaryOperator<T> pickSecond() {
return (k1, k2) -> k2;
}

集合类型转化


Collection 和 List、Set 的转化



  1. Collection<OrderItem> 转化为 List<OrderItem>

  2. Collection<OrderItem> 转化为 Set<OrderItem>


public static <T> List<T> toList(Collection<T> collection) {
if (collection == null) {
return new ArrayList<>();
}
if (collection instanceof List) {
return (List<T>) collection;
}
return collection.stream().collect(Collectors.toList());
}

public static <T> Set<T> toSet(Collection<T> collection) {
if (collection == null) {
return new HashSet<>();
}
if (collection instanceof Set) {
return (Set<T>) collection;
}
return collection.stream().collect(Collectors.toSet());
}

测试样例


@Test//将集合 Collection 转化为 List
public void testToList() {
Collection<OrderItem> collection = coll;
List<OrderItem> list = toList(coll);
}

@Test//将集合 Collection 转化为 Set
public void testToSet() {
Collection<OrderItem> collection = coll;
Set<OrderItem> set = toSet(collection);
}

List和 Set 是 Collection 集合类型的子类,所以无需再转化。


List、Set 类型之间的转换


业务中有时候需要将 List<A> 转化为 List<B>。如何实现工具类呢?


public static <T, R> List<R> map(List<T> collection, Function<T, R> mapper) {
return collection.stream().map(mapper).collect(Collectors.toList());
}

public static <T, R> Set<R> map(Set<T> collection, Function<T, R> mapper) {
return collection.stream().map(mapper).collect(Collectors.toSet());
}

public static <T, R> List<R> mapToList(Collection<T> collection, Function<T, R> mapper) {
return collection.stream().map(mapper).collect(Collectors.toList());
}

public static <T, R> Set<R> mapToSet(Collection<T> collection, Function<T, R> mapper) {
return collection.stream().map(mapper).collect(Collectors.toSet());
}

测试样例



  1. List<OrderItem> 转化为 List<Long>

  2. Set<OrderItem> 转化为 Set<Long>

  3. Collection<OrderItem> 转化为 List<Long>

  4. Collection<OrderItem> 转化为 Set<Long>


@Test
public void testMapToList() {
Collection<OrderItem> collection = coll;
List<OrderItem> list = toList(coll);

List<Long> orderIdList = map(list, (item) -> item.getOrderId());
}

@Test
public void testMapToSet() {
Collection<OrderItem> collection = coll;
Set<OrderItem> set = toSet(coll);

Set<Long> orderIdSet = map(set, (item) -> item.getOrderId());
}

@Test
public void testMapToList2() {
Collection<OrderItem> collection = coll;

List<Long> orderIdList = mapToList(collection, (item) -> item.getOrderId());
}

@Test
public void testMapToSetV2() {
Collection<OrderItem> collection = coll;

Set<Long> orderIdSet = mapToSet(collection, (item) -> item.getOrderId());

}

总结一下 以上样例包含了如下的映射场景



  1. Collection<OrderItem> 转化为 List<OrderItem>

  2. Collection<OrderItem> 转化为 Set<OrderItem>

  3. List<OrderItem> 转化为 List<Long>

  4. Set<OrderItem> 转化为 Set<Long>

  5. Collection<OrderItem> 转化为 List<Long>

  6. Collection<OrderItem> 转化为 Set<Long>

  7. Collection<OrderItem>中提取 Key, Map 的 Value 就是类型 OrderItem

  8. Collection<OrderItem>中提取 Key, Map 的 Value 根据 OrderItem 类型进行转化。

  9. Map<Long, OrderItem> 中的value 转化为 Map<Long, Double>

  10. value 转化时,lamada表达式可以使用(v)->{}, 也可以使用 (k,v)->{ }


作者:五阳
来源:juejin.cn/post/7305572311812587531
收起阅读 »

如何及时发现网页的隐形错误

web
在上一篇文章前端监控究竟有多重要?大家了解了前端监控系统的重要性以及前端监控的组成部分、常见的监控指标、埋点方式。 接下来这篇文章我们就来详细学习一下前端监控系统中的,异常监控。 想要进行异常监控之前,肯定先要了解有哪些异常才能进行监控。 异常的类型 一般来说...
继续阅读 »

在上一篇文章前端监控究竟有多重要?大家了解了前端监控系统的重要性以及前端监控的组成部分、常见的监控指标、埋点方式。


接下来这篇文章我们就来详细学习一下前端监控系统中的,异常监控


想要进行异常监控之前,肯定先要了解有哪些异常才能进行监控。


异常的类型


一般来说,浏览器端的异常分为两种类型:



  • JavaScript 错误,一般都是来自代码的原因。

  • 静态资源错误,一般都是来着资源加载的原因


而这里面我们又有各自的差异


JavaScript 错误


先来说说JavaScript的错误类型,ECMA-262 定义了 7 种错误类型,说明如下:



  • EvalError :eval() 函数的相关的错误

  • RangeError :使用了超出了 JavaScript 的限制或范围的值。

  • ReferenceError: 引用了未定义的变量或对象

  • TypeError: 类型错误

  • URIError: URI操作错误

  • SyntaxError: 语法错误 (这个错误WebIDL中故意省略,保留给ES解析器使用)

  • Error: 普通异常,通常与 throw 语句和 try/catch 语句一起使用,利用属性 name 可以声明或了解异常的类型,利用message 属性可以设置和读取异常的详细信息。


如果想更详细了解可以看详细错误罗列这篇文章


静态资源错误



  • 通过 XMLHttpRequest、Fetch() 的方式来请求的 http 资源时。

  • 利用
收起阅读 »

Java程序员快速提高代码质量建议

1、概述 相同的业务需求不同层级的程序员实现方式不一样,经验稍微欠缺一点的新手程序员,可能单纯的实现功能,经验丰富的程序员,开发的代码可能会具有很好的扩展性、易读性、健壮性。相信很多小伙伴在工作团队中,有时候会一起code review,互相review代码,...
继续阅读 »
1、概述

相同的业务需求不同层级的程序员实现方式不一样,经验稍微欠缺一点的新手程序员,可能单纯的实现功能,经验丰富的程序员,开发的代码可能会具有很好的扩展性、易读性、健壮性。相信很多小伙伴在工作团队中,有时候会一起code review,互相review代码,其实review代码时大家保持开放包容心态,是一种团队进度的方式。
今天分享的内容主要帮助大家从代码规范的角度,梳理出快速提升代码质量的建议,学完之后可以帮助大家在团队code review时,提供建议,帮大家写出高质量代码。


2、什么样的代码是高质量代码

如何评价一段代码的好与坏,其实是有一定主观性的,不同人有不同的标准和看法,但是总的概括下来优秀的代码一般具有如下特点:


高质量代码特点.png


3、如何提高代码质量

这里主要代码规范角度,小伙伴们可以快速理解掌握,并快速使用。


3.1 代码命名

项目名、模块名、包名、类名、接口名、变量名、参数名等,都会涉及命名,良好的代码命名是程序员的基本素养,对代码可读性非常重要。



  • 命名原则
    1、Java采用驼峰命名,代码命名要使用通俗易懂的词汇,不要采用生僻单词;
    2、团队内部或者项目中风格要统一,例如查询类方法,要么都使用findByXXX方式,或者queryByXXX、getByXXX等,不要几种混用,风格保持一致;
    3、命名长度:个人建议有时候为了易于理解,可以将命名适当长一些,例如:如下方法,一看就知道是上传照片到阿里云服务器,


public void uploadPhotoImageToAliyun(String userPhotoImageUri){}

可以利用上下文语义简化变量命名长度,如下用户实体类变量命名可以简化,更简洁


public class User {
private String userName;
private String userPassword;
private String userGender;
}

public class User {
private String name;
private String password;
private String gender;
}

4、抽象类通常带有Abstract前缀,接口命名和实现类命名,通常类似这样RoleService,实现类跟一个Impl,如RoleServiceImpl



  • 注释
    1、良好的代码注释对于可读性很重要,虽然有小伙伴可能会觉得好的命名可以替代注释;
    2、个人觉得注释很重要,注释可以起到代码分隔作用,代码块总结作用,文档作用;
    3、部分程序设计核心关键点,可以通过注释帮助其他研发人员理解;
    4、注释是否越多越好呢,然而并不是这样,太多注释反而让人迷惑,增加维护成本,代码变动之后也需要对注释进行修改。


3.2 代码风格

良好的代码风格,可以提升代码可读性,主要梳理以下几点:


良好的代码风格.png


3.3 实用代码技巧


  • 将代码分隔成多个单元
    代码逻辑太长不易阅读,将代码分隔成多个小的方法单元,更好理解和复用,如下所示,用户注册接口,包含账号、手机号校验及用户保存操作


public Long registerUser(String Account, String mobile, String password){
// 校验账号是否重复
if(StringUtils.isNotBlank(Account)){
User user = userService.getUserByName(Account);
AssertUtils.isNull(user, "用户账号已存在,不能重复");
}
// 校验手机号是否重复
if(StringUtils.isNotBlank(mobile)){
User user = userService.getUserByMobile(mobile);
AssertUtils.isNull(user, "手机号已存在,不能重复");
}
// 保存用户到DB
return userService.insert(Account, mobile, password);
}

重构之后的代码如下:


public Long registerUser(String Account, String mobile, String password){
// 校验账号是否重复
checkAccountIsExists(Account);
// 校验手机号是否重复
checkMobileIsExists(mobile);
// 保存用户到DB
return userService.insert(Account, mobile, password);
}

private void checkAccountIsExists(String Account){
if(StringUtils.isNotBlank(Account)){
User user = userService.getUserByName(Account);
AssertUtils.isNull(user, "用户账号已存在,不能重复");
}
}
private void checkMobileIsExists(String mobile){
if(StringUtils.isNotBlank(mobile)){
User user = userService.getUserByMobile(mobile);
AssertUtils.isNull(user, "手机号已存在,不能重复");
}
}



  • 避免方法太多参数
    方法太多参数影响代码可读性,当方法参数太多时可以采取将方法抽取为几个私有方法,如下所示:


public User getUser(String username, String telephone, String email);

// 拆分成多个函数
public User getUserByUsername(String username);
public User getUserByTelephone(String telephone);
public User getUserByEmail(String email);

也可以将参数封装为对象,通过抽取为对象对于C端项目还能更好兼容,如果是对外暴露的接口,可以避免新老接口兼容问题


public User getUser(String username, String telephone, String email);

// 重构后将方法入参封装为对象
public class SearchUserRequest{
private String username;
private String telephone;
private String email;
}
public User getUser(SearchUserRequest searchUserReq重构后将方法入参封装为对象


  • 不要使用参数null及boolean来判断
    使用参数非空和为空作为代码的if、else分支,以及boolean参数作为代码分支,这些都不建议,如果可以尽量拆分为多个细小的私有方法;当然也不是绝对的,实际情况具体分析;

  • ** 方法设计遵守单一职责**
    方法设计不要追求大而全,尽量做到职责单一,粒度细,更易理解和复用,如下所示:


public boolean checkUserIfExisting(String telephone, String username, String email)  { 
if (!StringUtils.isBlank(telephone)) {
User user = userRepo.selectUserByTelephone(telephone);
return user != null;
}

if (!StringUtils.isBlank(username)) {
User user = userRepo.selectUserByUsername(username);
return user != null;
}

if (!StringUtils.isBlank(email)) {
User user = userRepo.selectUserByEmail(email);
return user != null;
}

return false;
}

// 拆分成三个函数
public boolean checkUserIfExistingByTelephone(String telephone);
public boolean checkUserIfExistingByUsername(String username);
public boolean checkUserIfExistingByEmail(String email);


  • 避免嵌套逻辑太深
    避免if else太多的方法,可以使用卫语句,将满足条件的结果提前返回,或者使用枚举、策略模式、switch case等;
    对于for循环太深嵌套,可以使用continue、break、return等提前结束循环,或者优化代码逻辑。

  • 使用解释性变量
    尽量不要使用魔法值,要使用常量来管理,代码中复杂的判断逻辑可以使用解释性变量,如下所示:


public double CalculateCircularArea(double radius) {
return (3.1415) * radius * radius;
}

// 常量替代魔法数字
public static final Double PI = 3.1415;
public double CalculateCircularArea(double radius) {
return PI * radius * radius;
}

if (date.after(SPRING_START) && date.before(SPRING_END)) {
// ...
} else {
// ...
}

// 引入解释性变量后逻辑更加清晰
boolean isSpring = date.after(SPRING_START)&&date.before(SPRING_END);
if (isSpring) {
// ...
} else {
// ...
}



作者:美丽的程序人生
来源:juejin.cn/post/7352079427863920651
收起阅读 »

身份认证的尽头竟然是无密码 ?

概述 几乎所有的系统都会面临安全认证相关的问题,但是安全相关的问题是一个很麻烦的事情。因为它不产生直接的业务价值,而且处理起来复杂繁琐,所以很多时都容易被忽视。很多后期造成重大的安全隐患,往往都是前期的不重视造成的。但庆幸的是安全问题是普遍存在的,而且大家面临...
继续阅读 »

概述


几乎所有的系统都会面临安全认证相关的问题,但是安全相关的问题是一个很麻烦的事情。因为它不产生直接的业务价值,而且处理起来复杂繁琐,所以很多时都容易被忽视。很多后期造成重大的安全隐患,往往都是前期的不重视造成的。但庆幸的是安全问题是普遍存在的,而且大家面临的问题几乎相同,所以可以制定行业标准来规范处理,甚至是可以抽出专门的基础设施(例如:AD、LDAP 等)来专门解决这类共性的问题。总之,关于安全问题非常复杂而且麻烦,对于大多数 99% 的系统来说,不要想着在安全问题领域上搞发明和创新,容易踩坑。而且行业的标准解决方案已经非常成熟了。经过长时间的检验。所以在安全领域,踏踏实实的遵循规范和标准就是最好的安全设计。


HTTP 认证


HTTP 认证协议的最初是在 HTTP/1.1标准中定义的,后续由 IETF 在 RFC 7235 中进行完善。HTTP 协议的主要涉及两种的认证机制。


HTTP 认证的对话框


基本认证


常见的叫法是 HTTP Basic,是一种对于安全性不高,以演示为目的的简单的认证机制(例如你家路由器的登录界面),客户端用户名和密码进行 Base64 编码(注意是编码,不是加密)后,放入 HTTP 请求的头中。服务器在接收到请求后,解码这个字段来验证用户的身份。示例:


GET /some-protected-resource HTTP/1.1
Host: example.com
Authorization: Basic dXNlcjpwYXNzd29yZA==

虽然这种方式简单,但并不安全,因为 base64 编码很容易被解码。建议仅在 HTTPS 协议下使用,以确保安全性。


摘要认证


主要是为了解决 HTTP Basic 的安全问题,但是相对也更复杂一些,摘要认证使用 MD5 哈希函数对用户的密码进行加密,并结合一些盐值(可选)生成一个摘要值,然后将这个值放入请求头中。即使在传输过程中被截获,攻击者也无法直接从摘要中还原出用户的密码。示例:


GET /dir/index.html HTTP/1.1
Host: example.com
Authorization: Digest username="user", realm="example.com", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", uri="/dir/index.html", qop=auth, nc=00000001, cnonce="0a4f113b", response="6629fae49393a05397450978507c4ef1", opaque="5ccc069c403ebaf9f0171e9517f40e41"

**补充:**另在 RFC 7235 规范中还定义当用户没有认证访问服务资源时应返回 401 Unauthorized 状态码,示例:


HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="Restricted Area"

这一规范目前应用在所有的身份认证流程中,并且沿用至今。


Web 认证


表单认证


虽然 HTTP 有标准的认证协议,但目前实际场景中大多应用都还是基于表单认证实现,具体步骤是:



  1. 前端通过表单收集用户的账号和密码

  2. 通过协商的方式发送服务端进行验证的方式。


常见的表单认证页面通常如下:


html>
<html>
<head>
    <title>Login Pagetitle>
head>
<body>
    <h2>Login Formh2>
    <form action="/perform_login" method="post">
        <div class="container">
            <label for="username"><b>Usernameb>label>
            <input type="text" placeholder="Enter Username" name="username" required>
            
            <label for="password"><b>Passwordb>label>
            <input type="password" placeholder="Enter Password" name="password" required>
            
            <button type="submit">Loginbutton>
        div>
    form>
body>
html>

为什么表单认证会成为主流 ?主要有以下几点原因:



  • 界面美化:开发者可以创建定制化的登录界面,可以与应用的整体设计风格保持一致。而 HTTP 认证通常会弹出一个很丑的模态对话框让用户输入凭证。

  • 灵活性:可以在表单里面自定义更多的逻辑和流程,比如多因素认证、密码重置、记住我功能等。这些功能对于提高应用的安全性和便利性非常重要。

  • 安全性:表单认证可以更容易地结合现代的安全实践,背后也有 OAuth 2 、Spring Security 等框架的主持。


表单认证传输内容和格式基本都是自定义本没啥规范可言。但是在 2019 年之后 web 认证开始发布标准的认证协议。


WebAuthn


WebAuthn 是一种彻底抛弃传统密码的认证,完全基于生物识别技术和实体密钥作为身份识别的凭证(有兴趣的小伙伴可以在 github 开启 Webauhtn 的 2FA 认证体验一下)。在 2019 年 3 月,W3C 正式发布了 WebAuthn 的第一版规范。


webauthn registration


相比于传统的密码,WebAuthn 具有以下优势:



  1. 减少密码泄露:传统的用户名和密码登录容易受到钓鱼攻击和数据泄露的影响。WebAuthn,不依赖于密码,不存在密码丢失风险。

  2. 提高用户体验:用户不需要记住复杂的密码,通过使用生物识别等方式可以更快捷、更方便地登录。

  3. 多因素认证:WebAuthn 可以作为多因素认证过程中的一部分,进一步增强安全性。使用生物识别加上硬件密钥的方式进行认证,比短信验证码更安全。


总的来说,WebAuthn 是未来的身份认证方式,通过提供一个更安全、更方便的认证方式,目的是替代传统的基于密码的登录方法,从而解决了网络安全中的一些长期问题。WebAuthn 目前已经得到流程的浏览器厂商(Chrome、Firefox、Edge、Safari)、操作系统(WIndows、macOS、Linux)的广泛支持。


实现效果


当你的应用接入 WebAuthn 后,用户便可以通过生物识别设备进行认证,效果如下:


WebAuthn login


实现原理


WebAuthn 实现较为复杂,这里不做详细描述,具体可参看权威的官方文档,大概交互过程可以参考以下时序图:


webauthn 交互时序图


登录流程大致可以分为以下步骤:



  1. 用户访问登录页面,填入用户名后即可点击登录按钮。

  2. 服务器返回随机字符串 Challenge、用户 UserID。

  3. 浏览器将 Challenge 和 UserID 转发给验证器。

  4. 验证器提示用户进行认证操作。

  5. 服务端接收到浏览器转发来的被私钥加密的 Challenge,以此前注册时存储的公钥进行解密,如果解密成功则宣告登录成功。


WebAuthn 采用非对称加密的公钥、私钥替代传统的密码,这是非常理想的认证方案,私钥是保密的,只有验证器需要知道它,连用户本人都不需要知道,也就没有人为泄漏的可能;



备注:你可以通过访问 webauthn.me 了解到更多消息的信息



文章不适合加入过多的演示代码,想要手上体验的可以参考 okta 官方给出基于 Java 17 和 Maven 构建的 webauthn 示例程序,如下:



作者:小二十七
来源:juejin.cn/post/7354632375446061083
收起阅读 »

【教程】快速为App打造Android端聊天室,节省80%开发成本(一)

前言环信 ChatroomUIKit 提供 UIKit 的各种组件,能帮助开发者根据实际业务需求快速搭建聊天室应用,有效节约开发成本!通过该 UIKit,聊天室中的用户可实时交互,发送普通弹幕消息、打赏消息和全局广播等功能。本文详细教大家如何集成Chatroo...
继续阅读 »

前言

环信 ChatroomUIKit 提供 UIKit 的各种组件,能帮助开发者根据实际业务需求快速搭建聊天室应用,有效节约开发成本!通过该 UIKit,聊天室中的用户可实时交互,发送普通弹幕消息、打赏消息和全局广播等功能。

本文详细教大家如何集成ChatroomUIKit,以及集成中常见报错如何解决。

官方地址

导入

  1. 从github下载的附件打开以后 会有两个文件,一个是ChatRoomService ,另外一个是ChatroomUIKit

    2.先导入UIkit的本地库(引导的内容可以参考标题1. 的绿色箭头第二个文件夹)

    3.然后再导入ChatRoomservice 选择文件后也点击Finish 注: 一共两个文件 都需要导入

    4.填写settings.gradle
include(":ChatroomUIKit")
include(":ChatroomService")


添加:build.gradle(app)

implementation(project(mapOf("path" to ":ChatroomUIKit")))


如果遇到报错如下:

Dependency ‘androidx.activity:activity:1.8.0’ requires libraries and
applications that depend on it to compile against version 34 or later
of the Android APIs. :app is currently compiled against android-33.
Also, the maximum recommended compile SDK version for Android Gradle
plugin 7.4.2 is 33. Recommended action: Update this project’s version
of the Android Gradle plugin to one that supports 34, then update this
project to use compileSdkVerion of at least 34. Note that updating a
library or application’s compileSdkVersion (which allows newer APIs to
be used) can be done separately from updating targetSdkVersion (which
opts the app in to new runtime behavior) and minSdkVersion (which
determines which devices the app can be installed

解决方案: 注意一下自己app的 targetSDK版本号以及compilesdk 都给到 34 大概在报错信息也能提示到是 需要强制到34

5.初始化UIkit



(1)appkey管理后台位置

6.客户端登录调用

ChatroomUIKitClient.getInstance().login("4","YWMtFTJV-OXGEe6LxEWLvu_JdPqlsNlfrUUAh3km7oObq2HVh7Pgj9ER7JuEZ0XLQ13UAwMAAAGOVbV_AAWP1AB9sFv_7oIlDyK7Jay0Coha-HnF5o0PnXttL7r4gxryCA", onSuccess = {
val intent = Intent(this@MainActivity, As::class.java)
startActivity(intent)

}, onError = {
code, error ->


})


(1)参数管理后台具体位置 ,每次点击查看token的token内容都是不同的,这个不必担心。


(2)跳转到Asactivity 后遇到了一个问题!
继承ComponentActivity() 无法拿到setContent

解决办法:将这个依赖升级到 1.8.0 刚才用了1.7.0版本 无法拿到这个setContent

7.展示进入聊天室逻辑

class As : ComponentActivity() {

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

setContent{

ComposeChatroom(roomId = "242681589596161",roomOwner = UserInfoProtocol)

}

(1)参数roomId 在管理后台可以查看


(2)roomOwner 为 UserInfoProtocol 类型 ,可以自己定义编辑属性将参数存入方法内

总结

通过以上步骤,我们已经完成了ChatroomUIKit的集成。欢迎大家参考官方文档,进一步对聊天室其他功能进行完善~

我们将在下一期教程中介绍如何修改各个ui位置

相关文档:

收起阅读 »

CSS弹性布局:Flex布局及属性完全指南,点击解锁新技能!

Flex 布局是一种新型的 CSS 布局模式,它主要用于弹性盒子布局。相比于传统的布局方式,它更加灵活,易于调整,也更加适应不同的设备和屏幕尺寸。下面我们就来详细解析 Flex 布局及其属性,帮助大家深入理解和运用 Flex 布局。一、什么是Flex布局?在介...
继续阅读 »

Flex 布局是一种新型的 CSS 布局模式,它主要用于弹性盒子布局。相比于传统的布局方式,它更加灵活,易于调整,也更加适应不同的设备和屏幕尺寸。

下面我们就来详细解析 Flex 布局及其属性,帮助大家深入理解和运用 Flex 布局。

一、什么是Flex布局?

在介绍Flex布局之前,我们不得不提到它的前辈——浮动和定位。它们曾是布局的主力军,但随着响应式设计的兴起,它们的局限性也愈发明显。

Flex布局的出现,正是为了解决这些局限性,它允许我们在一个容器内对子元素进行灵活的排列、对齐和空间分配。

Description

Flex全称为 “Flexible Box Layout”,即 “弹性盒布局”,旨在提供一种更有效的方式来布局、对齐和分配容器中项目之间的空间,即使它们的大小未知或动态变化。

声明定义

容器里面包含着项目元素,使用 display:flex 或 display:inline-flex 声明为弹性容器。

.container {
display: flex | inline-flex;
}

flex布局的作用

  • 在父内容里面垂直居中一个块内容。

  • 使容器的所有子项占用等量的可用宽度/高度,而不管有多少宽度 / 高度可用。

  • 使多列布局中的所有列采用相同的高度,即使它们包含的内容量不同。

二、Flex布局的核心概念

要理解Flex布局,我们必须先了解几个核心概念:

2.1 容器与项目

容器(Container):设置了display: flex;的元素成为Flex容器。容器内的子元素自动成为Flex项目。

.container{
display: flex;
}
<div class="container">
<div class="item"> </div>
<div class="item">
<p class="sub-item"> </p>
</div>
<div class="item"> </div>
</div>

上面代码中, 最外层的 div 就是容器,内层的三个 div 就是项目。

注意: 项目只能是容器的顶层子元素(直属子元素),不包含项目的子元素,比如上面代码的 p 元素就不是项目。flex布局只对项目生效。

2.2 主轴(Main Axis)和交叉轴(Cross Axis)

主轴是Flex项目的排列方向,交叉轴则是垂直于主轴的方向。

Description

主轴(main axis)

沿其布置子容器的从 main-start 开始到 main-end ,请注意,它不一定是水平的;这取决于 flex-direction 属性(见下文), main size 是它可放置的宽度,是容器的宽或高,取决于 flex-direction。

交叉轴(cross axis)

垂直于主轴的轴称为交叉轴,它的方向取决于主轴方向,是主轴写满一行后另起一行的方向,从 cross-start 到 cross-end , cross size 是它可放置的宽度,是容器的宽或高,取决于 flex-direction。

三、Flex布局的基本属性

3.1容器属性

Description

容器的属性主要包括:

  • flex-direction:定义了主轴的方向,可以是水平或垂直,以及其起始和结束的方向。

  • flex-wrap:决定了当容器空间不足时,项目是否换行。

  • flex-flow:这是flex-direction和flex-wrap的简写形式。

  • justify-content:设置项目在主轴上的对齐方式。

  • align-items:定义了项目在交叉轴上的对齐方式。

  • align-content:定义了多根轴线时,项目在交叉轴上的对齐方式。

  • gap row-gap、column-gap:设置容器内项目间的间距。

3.1.1 主轴方向 flex-direction

定义主轴的方向,也就是子项目元素排列的方向。

  • row (默认):从左到右 ltr ;从右到左 rtl

  • row-reverse :从右到左 ltr ;从左到右 rtl

  • column: 相同, row 但从上到下

  • column-reverse: 相同, row-reverse 但从下到上

.container {
flex-direction: row | row-reverse | column | column-reverse;
}

Description

Description

3.1.2 换行 flex-wrap

设置子容器的换行方式,默认情况下,子项目元素都将尝试适合一行nowrap。

  • nowrap (默认)不换行

  • wrap 一行放不下时换行

  • wrap-reverse 弹性项目将从下到上换行成多行

.container {
flex-wrap: nowrap | wrap | wrap-reverse;
}

Description

3.1.3 简写 flex-flow

flex-direction 和 flex-wrap 属性的简写,默认值为 row nowrap。

.container {
flex-flow: column wrap;
}

取值情况:

Description

3.1.4 项目群对齐 justify-content与align-items

justify-c ontent 决定子元素在主轴方向上的对齐方式,默认是 flex-start。

.container {
justify-content: flex-start | flex-end | center | space-between | space-around | space-evenly | start | end | left | right ... + safe | unsafe;
}

Description

align-items 决定子元素在交叉轴方向上的对齐方式,默认是 stretch。

.container {
align-items: stretch | flex-start | flex-end | center | baseline | first baseline | last baseline | start | end | self-start | self-end + ... safe | unsafe;
}

Description

3.1.5多行对齐 align-content

align-content属性定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用。

  • flex-start:与交叉轴的起点对齐。

  • flex-end:与交叉轴的终点对齐。

  • center:与交叉轴的中点对齐。

  • space-between:与交叉轴两端对齐,轴线之间的间隔平均分布。

  • space-around:每根轴线两侧的间隔都相等。所以,轴线之间的间隔比轴线与边框的间隔大一倍。

  • stretch(默认值):轴线占满整个交叉轴。

.container {
align-content: flex-start | flex-end | center | space-between | space-around | space-evenly | stretch | start | end | baseline | first baseline | last baseline + ... safe | unsafe;
}

Description

3.1.6 间距 gap row-gap column-gap

设置容器内项目之间的间距,只控制项目与项目的间距,对项目与容器的间距不生效。

.container {
display: flex;
...
gap: 10px;
gap: 10px 20px; /* row-gap column gap */
row-gap: 10px;
column-gap: 20px;
}

Description
这设置的是最小间距,因为 just-content 导致的间距变大。

3.2项目属性

Description

项目item 的属性包括:

  • order:指定了项目的排列顺序。

  • flex-grow:定义了在有可用空间时的放大比例。

  • flex-shrink:定义了在空间不足时的缩小比例。

  • flex-basis:指定了项目在分配空间前的初始大小。

  • flex:这是flex-grow、flex-shrink和flex-basis的简写形式。

  • align-self:允许单个项目独立于其他项目在交叉轴上对齐。

3.2.1 排序位置 order

  • 每个子容器的order属性默认为0

  • 通过设置order属性值,改变子容器的排列顺序

  • 可以是负值,数值越小的话,排的越靠前

.item1 {
order: 3; /* default is 0 */
}

Description

3.2.2 弹性成长 flex-grow

在容器主轴上存在剩余空间时, flex-grow才有意义。

定义的是可放大的能力,0 (默认)禁止放大,大于 0 时按占的比重分放大,负数无效。

.container{
border-left:1.2px solid black;
border-top:1.2px solid black;
border-bottom: 1.2px solid black;
width: 100px;
height: 20px;
display: flex;
}
.item{
border-right:1.2px solid black;
width: 20px;height: 20px;
}
.item1{
/* 其他的都是0,这一个是1,1/1所以能所有剩下的空间都是item1的 */
flex-grow: 1; /* default 0 */
}
<div>
<div class="item item1" style="background-color: #7A42A8;"></div>
<div style="background-color: #8FAADC;"></div>
<div style="background-color: #DAE3F3;"></div>
</div>

Description

3.2.3 弹性收缩 flex-shrinik

当容器主轴 “空间不足” 且 “禁止换行” 时, flex-shrink才有意义。

定义的是可缩小的能力,1 (默认)等大于 0 的按比例权重收缩, 0 为禁止收缩,负数无效。

.container{
width: 100px;
height: 20px;
display: flex;
flex-wrap: nowrap;
}
.item{
width: 50px;height: 20px;
}
.item1{/*收缩权重1/3,总空间50,所以它占33.33,为原本的2/3*/
flex-shrink: 1; /* default 1 */
}
.item2{/*收缩权重2/3,总空间50,所以它占16.67,为原本的1/3*/
flex-shrink: 2; /* default 1 */
}
.item3{
flex-shrink: 0; /* default 1 */
}
<div>
<div class="item item1" style="background-color: #7A42A8;"></div>
<div class="item item2" style="background-color: #8FAADC;"></div>
<div class="item item3" style="background-color: #DAE3F3;"></div>
</div>

Description

3.2.4 弹性基值 flex-basis

flex-basis 指定了 flex 元素在主轴方向上的初始尺寸,它可以是长度(例如 20% 、 5rem 等)或关键字。felx-wrap根据它计算是否换行,默认值为 auto ,即项目的本来大小。它会覆盖原本的width 或 height。

.item {
flex-basis: <length> | auto; /* default auto */
}

3.2.5 弹性简写flex

flex-grow , flex-shrink 和 flex-basis 组合的简写,默认值为 0 1 auto。

.item {
flex: none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ]
}

取值情况:
Description

3.2.6自我对齐 align-self

这允许为单个弹性项目覆盖默认的交叉轴对齐方式 align-items。

.item {
align-self: auto | flex-start | flex-end | center | baseline | stretch;
}

Description

注意: flexbox布局和原来的布局是两个概念,部分css属性在flexbox盒子里面不起作用,eg:float , clear 和 vertical-align 等等。

想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!

点这里前往学习哦!

四、实战演练

让我们通过一个简单的例子来实践一下Flex布局的魅力。假设我们有6张图片,我们希望在不同的屏幕尺寸下,它们能够自适应排列。

1、设置容器属性:

对于包含图片的容器,首先将其display属性设置为flex,从而启用Flex布局。

2、确定排列方向:

根据设计需求,可以通过设置flex-direction属性来确定图片的排列方向。例如,如果希望图片在小屏幕上水平排列,可以设置flex-direction: row;如果希望图片垂直排列,则设置flex-direction: column。

3、调整对齐方式:

使用justify-content和align-items属性来调整图片的对齐方式。例如,如果想让图片在主轴上均匀分布,可以设置justify-content: space-around;如果想让图片在交叉轴上居中对齐,可以设置align-items: center。

4、允许换行显示:

如果需要图片在小屏幕上换行显示,可以添加flex-wrap: wrap属性。

5、优化空间分配:

通过调整flex-grow、flex-shrink和flex-basis属性来优化空间分配。例如,可以设置图片的flex-basis为calc(100% / 3 - 20px),这样每张图片会占据三分之一的宽度减去20像素的间距。

示例代码如下:

<!DOCTYPE html>
<html>
<head>
<style>
.image-grid {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
}
.image-grid img {
flex-basis: calc(100% / 3 - 20px);
}
@media screen and (max-width: 800px) {
.image-grid img {
flex-basis: calc(100% / 2 - 20px);
}
}
@media screen and (max-width: 400px) {
.image-grid img {
flex-basis: calc(100% - 20px);
}
}
</style>
</head>
<body>
<div>
<img src="image1.jpg" alt="Image 1">
<img src="image2.jpg" alt="Image 2">
<img src="image3.jpg" alt="Image 3">
<img src="image4.jpg" alt="Image 4">
<img src="image5.jpg" alt="Image 5">
<img src="image6.jpg" alt="Image 6">
</div>
</body>
</html>

将上述代码保存为一个HTML文件,并将image1.jpg、image2.jpg等替换为你自己的图片路径。然后在浏览器中打开该HTML文件,你将看到一个响应式的图片网格布局,图片会根据屏幕尺寸自适应排列。

Flex布局以其简洁明了的属性和强大的适应性,已经成为现代网页设计不可或缺的工具。掌握了Flex布局,你将能够轻松应对各种复杂的页面布局需求,让你的设计更加灵活、美观。现在,就打开你的代码编辑器,开始你的Flex布局之旅吧!

收起阅读 »

前端可玩性UP项目:大屏布局和封装

web
前言 autofit.js 发布马上要一年了,也收获了一批力挺用户,截至目前它在github上有1k 的 star,npm 上有超过 13k 的下载量。 这篇文章主要讲从设计稿到落地开发大屏应用,大道至简,这篇文章能帮助各位潇洒自如的开发大屏。 分析设计稿 分...
继续阅读 »

前言


autofit.js 发布马上要一年了,也收获了一批力挺用户,截至目前它在github上有1k 的 star,npm 上有超过 13k 的下载量。


这篇文章主要讲从设计稿到落地开发大屏应用,大道至简,这篇文章能帮助各位潇洒自如的开发大屏。


分析设计稿


分析设计稿之前先吐槽一下大屏这种展现形式,这简直就是自欺欺人、面子工程的最直接的诠释,是吊用没有,只为了好看,如果设计的再不好看啊,这就纯纯是屎。在我的理解中,这就像把PPT放到了web端,仅此而已。



但是王哥告诉我:"你看似没有用的东西,其实都有用,很多想真正做出有用的产品的企业,没钱,就要先把面子工程做好,告诉别人他们要做一件什么事,这样投资人才会看到,后面才有机会发展。"



布局方案


image.png
上图展示了一个传统意义上且比较普遍的大屏形态,分为四个部分,分别是


头部


头部经常放标题、功能菜单、时间、天气


左右面板


左右面板承载了各种数字和报表,还有视频、轮播图等等


中间


中间部分一般放地图,这其中又分假地图(一张图片)、图表地图(如echarts)、地图引擎(如:leaflet、mapbox、高德、百度)。或者有的还会放3D场景,一般有专门的同事去做3D场景,然后导入到web端。


大屏的设计通常的分辨率是 1920*1080 的,这也是迄今为止应用最广泛的显示器配置,当然也有基于客户屏幕做的异形分辨率,这就五花八门了。


但是万变不离其宗,分辨率的变化不会影响它的基本结构,根据上面的图,我们可以快速构建结构代码


  <div class='Box'>
   <div class="header"></div>
   <div class="body">
     <div class="leftPanel"></div>
     <div class="mainMap"></div>
     <div class="rightPanel"></div>
   </div>
 </div>

上面的代码实现了最简单的上下(Box)+左右(body)的布局结构,完全不需要任何定位策略。


要实现上图的效果,只需最简单的CSS即可完成布局。


组件方案


大屏虽然是屎,但是是一种可玩性很强的项目,想的越复杂,做起来就越复杂,想的越简单,做起来就越简单。


可以疯狂封装炫技,因为大屏里面的可玩组件简直太多了,且涵盖的太全了,想怎么玩都可以,包括但不限于 各类图表库的封装(echarts、highCharts、vChart)、轮播图(swiper)、地图引擎、视频库(包括直播流)等等。


如果想简单,甚至可以不用封装,可以看到结构甚至简单到不用CSS几行就可以搭建出基本框架,只把header、leaftPanel、rightPanel、map封装一下就可以了。


这里还有一个误区,就是大家都喜欢把 大型的组件库 拉到大屏里来用,结果做完了发现好像只用了一个 toast 和一个下拉组件,项目打包后却增大了几十倍的体积,其实像这种简单的组件,完全可以手写,或者找小的独立包来用,一方面会减小体积,不至于让项目臃肿,另一方面可以锻炼自己的手写能力,这才是有必要的封装。


适配


目前主流的适配方案,依然是 rem 方案,其原理就是根据根元素的 font-size 自动计算大小,但是此方法需要手动计算 rem 值,或者使用第三方插件如postcss等,但是此方案还有一个弊端,就是无法向下兼容,因为浏览器中最小的文字大小是12px。


vh/vw方案就不再赘述了,原理基本和 rem/em 相似,都涉及到单位的转换。


autofit.js


主要讲一下使用 autofit.js 如何快速实现适配。


不支持的场景


首先 autofit.js 不支持 elementUI(plus)、ant-design等组件库,具体是不支持包含popper.js的组件,popper.js 在计算弹出层位置时,不会考虑 scale 后的元素的视觉大小,所以会造成弹出元素的位置偏移。


其次,不支持 百度地图,百度地图对窗口缩放事件没有任何处理,有同学反馈说,即使使用了resize属性,百度地图在和autofit.js共同使用时,也会有事件热区偏移的问题。而且百度地图使用 bd-09 坐标系,和其他图商不通用,引擎的性能方面也差点意思,个人不推荐在开发中使用百度地图。


然后一些拖拽库,如甘特图插件,可能也不支持,他们在计算鼠标位置时同样没有考虑 scale 后的元素的视觉大小。


用什么单位


不支持的单位:vh、vw、rem、em


让我诧异的是,老有人问我该用什么单位,主要徘徊在 px 和 % 之间,加群的同学多数是因为用了相对单位,导致留白了。不过人各有所长,跟着各位大佬我也学到了很多。


看下图


image.png
假如有两个宽度为1000的元素,他们内部都有一个子元素,第一个用百分比设置为 width:50%;left:1% , 第二个设置为 wdith:500px;left:10px 。此时,只要外部的1000px的容器宽度不变,这两个内部元素在视觉上是一模一样的,且在实际数值上也是一模一样的,他们宽度都为500px,距离左侧10px。


但是如果外部容器变大了,来看一下效果:


image.png
在样式不变的情况下,仅改变外部容器大小,差异就出来了,由上图可知,50%的元素依然占父元素的一半,实际宽度变成了 1000px,距离左侧的实际距离变成了 20px。


这当然不难理解,百分比单位是根据 最近的、有确定大小的父级元素计算的。


所以,应该用什么单位其实取决于想做什么,举个例子:在1920*1080基础上开发,中间的地图写成了宽度为 500px ,这在正常情况下,看起来没有任何问题,它大概占屏幕的 26%,当屏幕分辨率达到4096*2160时,它在屏幕上只占 12%,看起来就是缩在一角。而当你设置宽度为26%时,无论显示器如何变化,它始终占屏幕26%。


autofit.js 所干的事,就是把1000px 变成了 2000px或者把2000px变成了1000px,并给它设置了一个合适的缩放大小。


图表、图片拉伸


背景或各种图片按需设置 object-fit: cover;即可


图表如echarts一般推荐使用百分比,且监听窗口变化事件做resize()


结语


再次感慨,大道至简,事情往往没有那么复杂,祝各位前程似锦。


作者:德莱厄斯
来源:juejin.cn/post/7344625554530779176
收起阅读 »

高龄程序员转换开发语言的心酸历程

高龄程序员转换开发语言的心酸历程 35岁,对于大多数程序员来说,正处于职业生涯的黄金时期。然而,对于我来说,却是一个充满挑战的转折点。在做了10年的PHP开发工程师之后,我决定转战Java开发。 初衷: 做出这个决定并非易事。一方面,我对PHP已经积累了...
继续阅读 »

高龄程序员转换开发语言的心酸历程


35岁,对于大多数程序员来说,正处于职业生涯的黄金时期。然而,对于我来说,却是一个充满挑战的转折点。在做了10年的PHP开发工程师之后,我决定转战Java开发。


初衷:


做出这个决定并非易事。一方面,我对PHP已经积累了丰富的经验,拥有稳定的工作和收入。另一方面,随着互联网技术的不断发展,Java以其强大的性能和广泛的应用前景,逐渐成为主流开发语言。为了不落后于时代,我意识到学习Java是势在必行的。


挑战:


然而,转行并非一帆风顺。与初出茅庐的年轻人相比,我面临着更大的挑战:




  • 学习压力:Java与PHP有着很大的不同,需要从头开始学习新的语法、框架和生态系统。


  • 时间成本:除了日常工作,我还要抽出时间学习Java,这对我来说是一项巨大的考验。


  • 心理压力:年龄和经验带来的压力,让我一度怀疑自己是否能够成功转行。


心酸:


在学习的过程中,我经历了许多心酸的时刻:





  • 为了理解一个概念,我连续熬夜几天,最终还是一头雾水。



  • 在面试中,被年轻的求职者比下去,让我感到深深的自卑。


坚持:


尽管困难重重,但我从未想过放弃。我深知,只有坚持才能实现自己的目标。





  • 我制定了详细的学习计划,并严格执行。



  • 我积极参加技术交流活动,向优秀的程序员学习。



  • 我不断给自己打气,鼓励自己坚持下去。


转机:


上天不负有心人,经过一年的努力,我终于掌握了Java开发的基本技能。





  • 我顺利通过了Java工程师的面试,获得了新的工作机会。



  • 我在新的岗位上快速成长,得到了同事和领导的认可。



  • 我用自己的经历证明了,年龄不是转行的障碍,只要坚持不懈,就一定能够成功。


感悟:


这次转行经历让我深刻地体会到:





  • 学习是程序员的终身事业,只有不断学习才能保持竞争力。



  • 年龄不是问题,只要有决心和毅力,就能够克服任何困难。



  • 坚持不懈是成功的关键,只有坚持才能实现自己的目标。


希望我的经历能够鼓励更多高龄程序员勇敢追梦,在职场上取得更大的成就!


忠告:


如果你也想转行,以下几点建议或许对你有所帮助:





  • 做好充分的准备,了解目标语言的技术体系和发展前景。



  • 制定详细的学习计划,并严格执行。



  • 积极参加技术交流活动,向优秀的程序员学习。



  • 不要害怕失败,坚持不懈才能实现目标。


最后,我想说的是,年龄只是一个数字,只要你有一颗热爱编程的心,就永远不会被时代淘汰!


作者:源梦倩影
来源:mdnice.com/writing/48740efaaeaf48128397471867705c9a
收起阅读 »

一碗水永远不可能端平

和一些朋友聊天的时候,他们总是会说领导对自己是怎么不公平了,脏活累活丢给自己干,好处还一个也没拿到,而对别人是如何如何好了,表现得自己很无辜,很委屈的样子。 我觉得如果感觉不舒服,要么寻求其他方式来平衡,要么趁早离开,因为想寻求所谓的公平,那简直是说笑话。 很...
继续阅读 »

图片


和一些朋友聊天的时候,他们总是会说领导对自己是怎么不公平了,脏活累活丢给自己干,好处还一个也没拿到,而对别人是如何如何好了,表现得自己很无辜,很委屈的样子。


我觉得如果感觉不舒服,要么寻求其他方式来平衡,要么趁早离开,因为想寻求所谓的公平,那简直是说笑话。


很现实的问题,一个女人生下两个孩子,虽然表面都会公平对待,但是内心肯定都会更加喜欢长得好的那个,这就是人性的私心。


读书的时候,你成绩好不一定能赢得老师的喜欢和照顾,但是如果你家境很好,并且父母经常给老师拿烟拿酒,经常约老师出去吃饭喝酒,那么大多数老师肯定都会对你照顾。


这就是人性,哪里有那么多公平对待,无非是价值提供得多不多而已。


这个社会无论情感还是物质,都是十分倾斜的,钱都是流向越有钱的,爱都是流向越不缺爱的。


你说以前的政策是农村包围城市,先富带动后富,大家都觉得行,但是现在看一下,真的带动了吗?


从教育资源就能看出来,北京的一所公立学校和贵州乌蒙山区的一所公立学校的基础设施,经费,师资力量,那简直是一个在天上,一个在地下。


社会层面尚且都不能做到一碗水端平,更何况个人呢?


记得刚工作的那一年,我和一个女同事同一天入职,另外一个架构师比我们早十来天,但是到了第二年,发年终奖的时候唯独没有我们两个,并不是我们工作不辛苦,不努力,但是不给你就是不给你。


当时我还找领导理论,说为啥不一碗水端平,为啥要区别对待?


但是有用吗?合同上也没有明确规定要给你发年终奖,所以你再怎么说也没用,只有不欢而散。


另外一个早入职的同事虽然拿了年终奖,但是是别的同事四分之一,并不是他工作不努力,做的活没别人多,奉献的力量没别人大。


但是在人性和资本面前,并不是你像一头老牛一样辛苦你就能得到和别人一样的对待。


可能有一些没做啥事,但是会来事,在关键节点上会露面的人,人家却得到了不错的对待。


这时候你作为一个底层的小员工,可有可无的人,你去谈公平对待是会显得很无力的。


所以要尽早走出一碗水端平这个误区。


无论是工作,家庭还是社交,把价值进行可视化才能占风头,低头苦干只有感动自己。


因为我们如果把角色进行转换,自己可能还不如别人做得好,只是自己段位比较低,容易去做一些无力的咆哮而已。


因为人这种生物在自己没有受益的时候,都是会说别人不好,别人不公平公正,但是自己受益了,别人怎么做都觉得对。


就像我们经常说有些人在某些岗位上拿着钱不作为,简直是吃皇粮不干事,无情的骂别人。


但是当你到了那个位置,你就能保证你有作为,你认真负责,也不见得吧!


所以想清楚这一点就不会活得那么累,就会泰然自若面对身边发生的事情,就会摒弃那些自以为是和幼稚的想法!


作者:苏格拉的底牌
来源:juejin.cn/post/7337188759059824655
收起阅读 »

经历定时任务事故,我学到了什么?一个案例的全面回顾

前情提要最近离职在家休息,手里的资金又比较有限,水费,电费,燃气费都比较头疼,有时候电费欠费断电了才去交,然后要等5-10分钟才重新送电,再加上家里有电压保护器,就更久了,水费,燃气亦是如此。事发突然对于我这种一般不会一次性充很多或者每月固定缴费的人来说,我没...
继续阅读 »

前情提要

最近离职在家休息,手里的资金又比较有限,水费,电费,燃气费都比较头疼,有时候电费欠费断电了才去交,然后要等5-10分钟才重新送电,再加上家里有电压保护器,就更久了,水费,燃气亦是如此。

854f1e58ly1hi20we1vr6j20u00u0wik.jpg

事发突然

对于我这种一般不会一次性充很多或者每月固定缴费的人来说,我没办法做到按时固定查看,可以说我有点懒。于是就想起家里有台服务器,只挂了一个NAS服务在上面,感觉到有点浪费,于是就看到宝塔面板上有定时任务管理器,前期用的感觉还不错,但是!!问题出现了,我有一次出远门直接拉闸,结果回家之后合闸听见服务器风扇狂转......

src=http __safe-img.xhscdn.com_bw1_3bc10c30-ee6d-4a7d-9acd-f3501b24c694 imageView2_2_w_1080_format_jpg&refer=http __safe-img.xhscdn.webp

于是我立刻打开电脑去看宝塔面板,首先是要我登录账号,我就有点汗流浃背了。登录之后立刻点到定时任务面板里去看,结果全没了,我以为是宝塔没了,但是思索片刻之后发现,宝塔面板的定时任务是设置到Linux的crontab命令中的。接着我抱着试试看的心态登录SSH查询了一下,确实有那么几条不认识的(看着完全不像我的)定时任务在控制台。

我想:既然有记录,那不是能正常执行? 果然猜的没错,可以运行,直到我想调整定时周期,给我整暴躁了。但有人可能就说:“你为什么不直接用命令控制台呢?”,“你为什么要用图形化界面?”,“你Linux命令都不熟,怎么做开发的?”诸如此类,可是我用图形化的东西不就是图个方便么?

思考

为什么宝塔面板的定时任务查不到?

设计缺陷?容错设计?我并不清楚

为什么SSH查询的定时任务我一个都不认识?

宝塔做了一次唯一编码转换

在Linux中的定时任务是怎么保存的?

我在宝塔面板的www目录下找到了一个cron的文件夹,并发现了成对出现的定时文件,名称和SSH界面查询出来的一模一样,用文本编辑器打开,果不其然,就是我设置的定时脚本内容

既然在特定目录下,为何宝塔不识别?

我尝试添加新的定时任务,cron文件夹中又出现了新的文件。猜测是宝塔的数据和文件是分开的,就意味着不是根据动态扫描配置来实现,而是单独储存数据映射


我想到一件事,既然Linux有crontab,那Windows是不是也有类似的东西可以支持?

确实是这样

微软提供了一个图形化操作界面来管理定时任务:

图片.png

图片.png

但是,这里又有一个问题回归本质。

我现在既需要定时任务功能帮我定时查询水电燃气费,但我又得省电,用过Win的都非常清楚,一旦超过24H不关机或重启,系统就会出点小毛病,就像安卓,但我服务器又是Linux,所以我得找个解决办法......

于是,我想到了另一个问题,既然crontab系统提供的这么方便,为什么软件开发不用?(脑子抽了) 因为:集成度不高且不方便定制

图片.png

解决之路

于是我就开始看定时任务框架,想到了之前面试经常提到的Quartz框架。

马上就下载源码看了起来。

看了一圈发现,Quartz框架使用了多线程技术来实现任务调度。

又回归到多线程,好好好!

图片.png

那就顺带狠狠的让我康康!

以下是Quartz框架的一些核心组成部分及其实现原理:

  1. Scheduler(调度器) :负责整个定时任务系统的调度工作。内部通过线程池来进行任务的执行和调度管理。
  2. Trigger(触发器) :定义了调度任务的时间规则,决定何时触发任务执行。Quartz支持多种类型的触发器,如SimpleTrigger、CronTrigger等。
  3. Job(任务) :实际执行的工作单元,通常实现了特定的接口以定义任务内容。
  4. JobDetail(任务详情) :保存了Job的实例和相关的配置信息。
  5. 线程池:Quartz使用线程池来管理和执行任务,这样可以有效地复用线程资源,提高系统性能。
  6. 数据存储:Quartz允许将Trigger和Job的相关信息存储在数据库中,以实现任务的持久化,确保即使在系统宕机后,任务也能恢复执行。
  7. 集群支持:Quartz还支持集群环境下的任务调度,能够在多个节点之间协调任务的执行。
  8. 容错机制:Quartz框架提供了一些容错机制,比如在任务执行过程中发生异常时,可以记录日志并尝试重新执行任务。
  9. 负载均衡:在集群环境中,Quartz可以通过一定的策略进行负载均衡,确保任务在各个节点上均匀分配。

综上所述,Quartz框架通过这些组件和机制,提供了一个强大而灵活的任务调度平台,广泛应用于需要定时或周期性执行任务的Java应用程序中。

好嘛,这里问题又来了,多线程。如果我的定时任务体量足够大,或者说我就是喜欢玩变态的,纯靠定时任务执行逻辑,是不是又遇到了面试的经典场景?

图片.png

那么,来回顾一下吧!

多线程应用在CPU占用中通常通过抢占时间片来执行任务的。

在多线程环境中,CPU的时间被分割成许多小的时间片,每个线程轮流使用这些时间片来执行任务。这种机制称为时间片轮转(Time Slice Scheduling) 。以下是多线程执行的一些关键点:

  1. 线程状态:线程可以处于就绪状态、运行状态或阻塞状态。在就绪状态下,线程准备好执行并等待CPU时间片。一旦抢到时间片,线程就会进入运行状态。
  2. 抢占式多任务:为了防止线程独占CPU,操作系统采用抢占式多任务策略,允许其他线程公平地分享CPU执行时间。这意味着即使一个线程仍在运行,CPU也可能强制中断它,让其他线程执行。
  3. 线程优先级:线程的优先级影响它们抢占时间片的概率。高优先级的线程更有可能被调度执行,但这并不意味着低优先级的线程永远不会执行。
  4. 多核CPU:在多核CPU的情况下,单进程的多线程可以并发执行,而多进程的线程也可以并行执行。每个核心上的线程按照时间片轮转,但一个线程在同一时间只能运行在一个核心上。

综上所述,多线程应用确实依赖于时间片轮转机制来实现多任务并行处理,这是现代操作系统中实现多线程并发执行的基础。通过这种方式,操作系统能够有效地管理多个线程,确保CPU资源的合理分配和充分利用。

线程过多会引发什么问题呢?

线程过多确实可能导致操作系统性能的下降。当系统中存在大量线程时,可能会引发以下问题:

  • 上下文切换开销增大:操作系统需要更频繁地在线程之间切换,这种上下文切换会消耗CPU时间,降低整体的CPU利用率。
  • 内存占用增加:每个线程都有自己的栈空间,大量的线程意味着需要更多的内存来存储这些栈空间,这可能导致内存资源紧张,甚至出现内存不足的情况。
  • 垃圾回收压力增大:在Java等环境中,过多的线程会增加垃圾回收器的工作压力,进一步影响程序性能。
  • 系统稳定性降低:过多的线程竞争CPU资源时可能产生其他性能开销,严重时可能导致系统不稳定,甚至出现OutOfMemoryError异常。

为了解决这些问题,可以采取以下措施:

  • 使用线程池:线程池可以有效地管理线程资源,避免频繁创建和销毁线程的开销,同时可以控制线程数量和任务队列,提高系统性能和可靠性。
  • 合理配置线程数:根据系统的硬件配置和应用需求,合理设置线程池的核心线程数和最大线程数,以达到最优的系统吞吐量和响应时间。
  • 动态调整参数:根据实际情况动态调节线程池的参数,确保线程池处于合适的状态,避免任务堆积导致死锁或长时间停滞。

综上所述,虽然多线程可以提高程序的并发性能,但是线程数量过多确实会给操作系统带来额外的负担,可能导致性能下降。因此,合理配置和管理线程是提高系统性能的关键。

所以Quartz用的就是线程池,那线程池怎么玩?

这道题的核心就是:任务密集型和CPU密集型分别如何设置线程池

图片.png

先写一个解,解代表人的自信

解: 针对CPU密集型任务,线程池的设置应侧重于核心数匹配;而针对任务密集型(通常指IO密集型),线程池可配置更多的线程以利用IO等待时间。具体设置如下:

  1. CPU密集型任务
  • 线程数量:一般建议将核心线程数 (corePoolSize) 和最大线程数 (maximumPoolSize) 设置为与CPU的核心数相等。这样可以避免过多的上下文切换,因为CPU密集型任务会持续占用CPU资源进行计算。
  • 存活时间:对于CPU密集型任务,线程的存活时间不需要设置太长,因为线程通常会一直忙碌。
  1. 任务密集型(IO密集型)任务
  • 线程数量:可以设置为核心数的两倍,即如果机器有N个CPU,那么线程数可以设置为2N。这是因为在执行IO操作时,线程会经常处于等待状态,此时可以处理其他任务,所以增加线程数可以更充分地利用CPU资源。
  • 存活时间:对于IO密集型任务,可以根据实际情况适当增加线程的存活时间,以保证在需要时能够快速响应。

此外,如果任务既包含计算工作又包含IO工作,可以考虑使用两个线程池分别处理不同类型的任务,以避免相互干扰。 综上所述,合理设置线程池参数可以帮助系统高效运行,减少资源争用和性能瓶颈。

是不是一下就清晰明了,面试题也不用死记硬背了?

那回归到上面说的,我是一个变态,我就是喜欢用定时任务去执行所有逻辑,就是喜欢定时任务多到离谱,那么这个时候因为任务多到离谱,所以任务执行会有时间差,但我又要精准执行怎么办?

答:买个线程撕裂者(笑)

哥们要是那么有钱,我为什么不直接挂Win,然后再多搞几台电脑?

解决方案

手搓一个定时任务执行系统+文件系统 MySQL5+SpringBoot2.x+Quartz+Linux

后续如果大家也有这需求,我看情况开源给大家用

引申思考

在实际生产中,由于都是分布式的架构,那么Quartz自然就慢慢的没办法满足需求了。

甚至有些系统需要专门为定时服务准备一台专用服务器

为了解决这一问题,众多定时框架应运而生,例如:XXL-job

相比之下他们之间有什么差异呢?

QuartzXXL-job
优点支持集群部署,能够实现高可用性和负载均衡。 是Java生态中广泛使用的定时任务标准,社区活跃,文档齐全。 可以通过数据库实现作业的高可用性。提供了可视化的管理界面,便于任务的监控和管理。 支持集群部署,且维护成本低,提供错误预警功能。 支持分片、故障转移等分布式场景下的关键特性。 相对Quartz来说,上手更容易,适用于分布式环境。
缺点缺少自带的管理界面,对用户而言不够直观便捷。 调度逻辑和执行任务耦合在一起,维护时需要重启服务,影响系统的连续性。 相对于其他分布式调度框架,如elastic-job,缺少分布式并行调度的功能。需要单独部署调度中心,相对于Quartz来说,增加了部署的复杂性。

不过在现代几乎都是容器开发的方式,部署的复杂程度已经没有那么高了。

结尾

至此

祝各位工作顺利,钱多事少离家近!!!

祝各位jy们清明安康!!!

图片.png


作者:小白858
来源:juejin.cn/post/7353208973879853106

收起阅读 »

Android:优雅的处理首页弹框逻辑:责任链模式

背景 随着业务的发展,首页的弹窗越来越多,隐私政策弹窗,广告弹窗,好评弹窗,应用内更新弹窗等等。 并且弹框显示还有要求,比如: 用户本次使用app,只能显示一个弹框,毕竟谁都不愿意打开app就看到一堆弹框 这些弹框有优先级:如隐私政策弹窗优先级肯定比好评弹窗...
继续阅读 »

背景


随着业务的发展,首页的弹窗越来越多,隐私政策弹窗,广告弹窗,好评弹窗,应用内更新弹窗等等。
并且弹框显示还有要求,比如:



  • 用户本次使用app,只能显示一个弹框,毕竟谁都不愿意打开app就看到一堆弹框

  • 这些弹框有优先级:如隐私政策弹窗优先级肯定比好评弹窗高,所以希望优先级高的优先显示

  • 广告弹框只展示一次

  • 等等


如何优雅的处理这个逻辑呢?请出我们的主角:责任链模式。


责任链模式


举个栗子🌰


一位男性在结婚之前有事要和父母请示,结婚之后要请示妻子,老了之后就要和孩子们商量。作为决策者的父母、妻子或孩子,只有两种选择:要不承担起责任来,允许或不允许相应的请求; 要不就让他请示下一个人,下面来看如何通过程序来实现整个流程。


先看一下类图:
未命名文件.png
类图非常简单,IHandler上三个决策对象的接口。


//决策对象的接口
public interface IHandler {
//处理请求
void HandleMessage(IMan man);
}

//决策对象:父母
public class Parent implements IHandler {
@Override
public void HandleMessage(IMan man) {
System.out.println("孩子向父母的请求是:" + man.getRequest());
System.out.println("父母的回答是:同意");
}
}

//决策对象:妻子
public class Wife implements IHandler {
@Override
public void HandleMessage(IMan man) {
System.out.println("丈夫向妻子的请求是:" + man.getRequest());
System.out.println("妻子的回答是:同意");
}
}

//决策对象:孩子
public class Children implements IHandler{
@Override
public void HandleMessage(IMan man) {
System.out.println("父亲向孩子的请求是:" + man.getRequest());
System.out.println("孩子的回答是:同意");
}
}

IMan上男性的接口:


public interface IMan {
int getType(); //获取个人状况
String getRequest(); //获取个人请示(这里就简单的用String)
}

//具体男性对象
public class Man implements IMan {
/**
* 通过一个int类型去描述男性的个人状况
* 0--幼年
* 1--成年
* 2--年迈
*/

private int mType = 0;
//请求
private String mRequest = "";

public Man(int type, String request) {
this.mType = type;
this.mRequest = request;
}

@Override
public int getType() {
return mType;
}

@Override
public String getRequest() {
return mRequest;
}
}

最后我们看下一下场景类:


public class Client {
public static void main(String[] args) {
//随机生成几个man
Random random = new Random();
ArrayList<IMan> manList = new ArrayList<>();
for (int i = 0; i < 5; i++) {
manList.add(new Man(random.nextInt(3), "5块零花钱"));
}
//定义三个请示对象
IHandler parent = new Parent();
IHandler wife = new Wife();
IHandler children = new Children();
//处理请求
for (IMan man: manList) {
switch (man.getType()) {
case 0:
System.out.println("--------孩子向父母发起请求-------");
parent.HandleMessage(man);
break;
case 1:
System.out.println("--------丈夫向妻子发起请求-------");
wife.HandleMessage(man);
break;
case 2:
System.out.println("--------父亲向孩子发起请求-------");
children.HandleMessage(man);
break;
default:
break;
}
}
}
}

首先是通过随机方法产生了5个男性的对象,然后看他们是如何就要5块零花钱这件事去请示的,运行结果如下所示:


--------丈夫向妻子发起请求-------
丈夫向妻子的请求是:5块零花钱
妻子的回答是:同意
--------丈夫向妻子发起请求-------
丈夫向妻子的请求是:5块零花钱
妻子的回答是:同意
--------父亲向孩子发起请求-------
父亲向孩子的请求是:5块零花钱
孩子的回答是:同意
--------孩子向父母发起请求-------
孩子向父母的请求是:5块零花钱
父母的回答是:同意
--------丈夫向妻子发起请求-------
丈夫向妻子的请求是:5块零花钱
妻子的回答是:同意


发没发现上述的代码是不是有点不舒服,有点别扭,有点想重构它的感觉?那就对了!这段代码有以下几个问题:



  • 职责界定不清晰



对孩子提出的请示,应该在父母类中做出决定,父母有责任、有义务处理孩子的请示,



因此Parent类应该是知道孩子的请求自己处理,而不是在Client类中进行组装出来,
也就是说 原本应该是父亲这个类做的事情抛给了其他类进行处理,不应该是这样的。



  • 代码臃肿



我们在Client类中写了if...else的判断条件,而且能随着能处理该类型的请示人员越多,
if...else的判断就越多,想想看,臃肿的条件判断还怎么有可读性?!




  • 耦合过重



这是什么意思呢,我们要根据Man的type来决定使用IHandler的那个实现类来处理请



求。有一个问题是:如果IHandler的实现类继续扩展怎么办?修改Client类?
与开闭原则违背了!【开闭原则:软件实体如类,模块和函数应该对扩展开放,对修改关闭】
http://www.jianshu.com/p/05196fac1…



  • 异常情况欠考虑



丈夫只能向妻子请示吗?丈夫向自己的父母请示了,父母应该做何处理?
我们的程序上可没有体现出来,逻辑失败了!


既然有这么多的问题,那我们要想办法来解决这些问题,我们先来分析一下需求,男性提出一个请示,必然要获得一个答复,甭管是同意还是不同意,总之是要一个答复的,而且这个答复是唯一的,不能说是父母作出一个决断,而妻子也作出了一个决断,也即是请示传递出去,必然有一个唯一的处理人给出唯一的答复,OK,分析完毕,收工,重新设计,我们可以抽象成这样一个结构,男性的请求先发送到父亲,父母一看是自己要处理的,就作出回应处理,如果男性已经结婚了,那就要把这个请求转发到妻子来处理,如果男性已经年迈,那就由孩子来处理这个请求,类似于如图所示的顺序处理图。
未命名文件 (1).png
父母、妻子、孩子每个节点有两个选择:要么承担责任,做出回应;要么把请求转发到后序环节。结构分析得已经很清楚了,那我们看怎么来实现这个功能,类图重新修正,如图 :
未命名文件 (2).png
从类图上看,三个实现类Parent、Wife、Children只要实现构造函数和父类中的抽象方法 response就可以了,具体由谁处理男性提出的请求,都已经转移到了Handler抽象类中,我们 来看Handler怎么实现,


public abstract class Handler {
//处理级别
public static final int PARENT_LEVEL_REQUEST = 0; //父母级别
public static final int WIFE_LEVEL_REQUEST = 1; //妻子级别
public static final int CHILDREN_LEVEL_REQUEST = 2;//孩子级别

private Handler mNextHandler;//下一个责任人

protected abstract int getHandleLevel();//具体责任人的处理级别

protected abstract void response(IMan man);//具体责任人给出的回应

public final void HandleMessage(IMan man) {
if (man.getType() == getHandleLevel()) {
response(man);//当前责任人可以处理
} else {
//当前责任人不能处理,如果有后续处理人,将请求往后传递
if (mNextHandler != null) {
mNextHandler.HandleMessage(man);
} else {
System.out.println("-----没有人可以请示了,不同意该请求-----");
}
}
}

public void setNext(Handler next) {
this.mNextHandler = next;
}
}

再看一下具体责任人的实现:Parent、Wife、Children


public class Parent extends Handler{

@Override
protected int getHandleLevel() {
return Handler.PARENT_LEVEL_REQUEST;
}

@Override
protected void response(IMan man) {
System.out.println("----------孩子向父母提出请示----------");
System.out.println(man.getRequest());
System.out.println("父母的回答是:同意");
}
}

public class Wife extends Handler{
@Override
protected int getHandleLevel() {
return Handler.WIFE_LEVEL_REQUEST;
}

@Override
protected void response(IMan man) {
System.out.println("----------丈夫向妻子提出请示----------");
System.out.println(man.getRequest());
System.out.println("妻子的回答是:同意");
}
}

public class Children extends Handler{
@Override
protected int getHandleLevel() {
return Handler.CHILDREN_LEVEL_REQUEST;
}

@Override
protected void response(IMan man) {
System.out.println("----------父亲向孩子提出请示----------");
System.out.println(man.getRequest());
System.out.println("孩子的回答是:同意");
}
}

那么再看一下场景复现:
在Client中设置请求的传递顺序,先向父母请示,不是父母应该解决的问题,则由父母传递到妻子类解决,若不是妻子类解决的问题则传递到孩子类解决,最终的结果必然有一个返回,其运行结果如下所示。


----------孩子向父母提出请示----------
15块零花钱
父母的回答是:同意
----------丈夫向妻子提出请示----------
15块零花钱
妻子的回答是:同意
----------父亲向孩子提出请示----------
15块零花钱
孩子的回答是:同意
----------丈夫向妻子提出请示----------
15块零花钱
妻子的回答是:同意
----------父亲向孩子提出请示----------
15块零花钱
孩子的回答是:同意

结果也正确,业务调用类Client也不用去做判断到底是需要谁去处理,而且Handler抽象类的子类可以继续增加下去,只需要扩展传递链而已,调用类可以不用了解变化过程,甚至是谁在处理这个请求都不用知道。在这种模式就是责任链模式


定义


Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request.Chain the receiving objects and pass the request along the chain until an object handles it.
(使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。) 责任链模式的重点是在“链”上,由一条链去处理相似的请求在链中决定谁来处理这个请 求,并返回相应的结果,其通用类图如图所示
未命名文件 (3).png
最后总结一下,责任链的模版:
包含四个对象,Handler,Request,Level,Response:


public class Request {
//请求的等级
public Level getRequestLevel(){
return null;
}
}

public class Level {
//请求级别
}


public class Response {
//处理者返回的数据
}

//抽象处理者
public abstract class Handler {
private Handler mNextHandler;

//每个处理者都必须对请求做出处理
public final Response handleMessage(Request request) {
Response response = null;
if (getHandlerLevel().equals(request.getRequestLevel())) {
//是自己处理的级别,自己处理
response = echo(request);
} else {
//不是自己处理的级别,交给下一个处理者
if (mNextHandler != null) {
response = mNextHandler.echo(request);
} else {
//没有处理者能处理,业务自行处理
}
}
return response;
}

public void setNext(Handler next) {
this.mNextHandler = next;
}

@NotNull
protected abstract Level getHandlerLevel();

protected abstract Response echo(Request request);
}

实际应用


我们回到开篇的问题:如何设计弹框的责任链?


//抽象处理者
abstract class AbsDialog(private val context: Context) {
private var nextDialog: AbsDialog? = null

//优先级
abstract fun getPriority(): Int

//是否需要展示
abstract fun needShownDialog(): Boolean

fun setNextDialog(dialog: AbsDialog?) {
nextDialog = dialog
}

open fun showDialog() {
//这里的逻辑,我们就简单点,具体逻辑根据业务而定
if (needShownDialog()) {
show()
} else {
nextDialog?.showDialog()
}
}

protected abstract fun show()

// Sp存储, 记录是否已经展示过
open fun needShow(key: String): Boolean {
val sp: SharedPreferences = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
return sp.getBoolean(key, true)
}

open fun setShown(key: String, show: Boolean) {
val sp: SharedPreferences = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
sp.edit().putBoolean(key, !show).apply()
}

companion object {
const val LOG_TAG = "Dialog"
const val SP_NAME = "dialog"
const val POLICY_DIALOG_KEY = "policy_dialog"
const val AD_DIALOG_KEY = "ad_dialog"
const val PRAISE_DIALOG_KEY = "praise_dialog"
}
}

/**
* 模拟 隐私政策弹窗
* */

class PolicyDialog(context: Context) : AbsDialog(context) {
override fun getPriority(): Int = 0

override fun needShownDialog(): Boolean {
// 这里可以根据业务逻辑判断是否需要显示弹窗,如接口控制等等
// 这里通过Sp存储来模拟
return needShow(POLICY_DIALOG_KEY)
}

override fun show() {
Log.d(LOG_TAG, "显示隐私政策弹窗")
setShown(POLICY_DIALOG_KEY, true) //记录已经显示过
}
}

/**
* 模拟 广告弹窗
* */

class AdDialog(private val context: Context) : AbsDialog(context) {
private val ad = DialogData(1, "XX广告弹窗") // 模拟广告数据

override fun getPriority(): Int = 1

override fun needShownDialog(): Boolean {
// 广告数据通过接口获取,广告id应该是唯一的,所以根据id保持sp
return needShow(AD_DIALOG_KEY + ad.id)
}

override fun show() {
Log.d(LOG_TAG, "显示广告弹窗:${ad.name}")
setShown(AD_DIALOG_KEY + ad.id, true)
}
}

/**
* 模拟 好评弹窗
* */

class PraiseDialog(context: Context) : AbsDialog(context) {
override fun getPriority(): Int = 2

override fun needShownDialog(): Boolean {
// 这里可以根据业务逻辑判断是否需要显示弹窗,如用户使用7天等
// 这里通过Sp存储来模拟
return needShow(PRAISE_DIALOG_KEY)
}

override fun show() {
Log.d(LOG_TAG, "显示好评弹窗")
setShown(PRAISE_DIALOG_KEY, true)
}
}

//模拟打开app
val dialogs = mutableListOf<AbsDialog>()
dialogs.add(PolicyDialog(this))
dialogs.add(PraiseDialog(this))
dialogs.add(AdDialog(this))
//根据优先级排序
dialogs.sortBy { it.getPriority() }
//创建链条
for (i in 0 until dialogs.size - 1) {
dialogs[i].setNextDialog(dialogs[i + 1])
}
dialogs[0].showDialog()

第一次打开
image.png


第二次打开
image.png


第三次打开
image.png


总结:



  • 优点


责任链模式非常显著的优点是将请求和处理分开。请求者可以不用知道是谁处理的,处理者可以不用知道请求的全貌,两者解耦,提高系统的灵活性。



  • 缺点


责任链有两个非常显著的缺点:一是性能问题,每个请求都是从链头遍历到链尾,特别是在链比较长的时候,性能是一个非常大的问题。二是调试不很方便,特别是链条比较长, 环节比较多的时候,由于采用了类似递归的方式,调试的时候逻辑可能比较复杂。



  • 注意事项


链中节点数量需要控制,避免出现超长链的情况,一般的做法是在Handler中设置一个最大节点数量,在setNext方法中判断是否已经是超过其阈值,超过则不允许该链建立,避免无意识地破坏系统性能。


作者:蹦蹦蹦
来源:juejin.cn/post/7278239421706633252
收起阅读 »

JWT:你真的了解它吗?

       大家好,我是石头~        在数字化时代,网络安全和用户隐私保护成为了我们无法忽视的关键议题,也是我...
继续阅读 »

       大家好,我是石头~


       在数字化时代,网络安全和用户隐私保护成为了我们无法忽视的关键议题,也是我们作为一个后端开发的必修课。


       而在这个领域中,JWT(JSON Web Token)作为一种现代、安全且高效的会话管理机制,在各类Web服务及API接口中得到了广泛应用。


       那么,什么是JWT?


下载 (3).jfif


1、初识JWT


       JWT,全称为JSON Web Token,是一种开放标准(RFC 7519),用于在网络应用环境间安全地传输信息。


       它本质上是一个经过数字签名的JSON对象,能够携带并传递状态信息(如用户身份验证、授权等)。


       了解了JWT之后,那么它的组成结构又是怎样的?


2、JWT的结构


u=2288314449,1048843062&fm=253&fmt=auto&app=138&f=JPEG.webp


       如上图,JWT由三部分组成,通过点号(.)连接,这三部分分别是头部(Header)、载荷(Payload)和签名(Signature)。



  • 头部(Header):声明了JWT的类型(通常是JWT)以及所使用的加密算法(例如HMAC SHA256或RSA)

  • 载荷(Payload):承载实际数据的部分,可以包含预定义的声明(如iss(签发者)、exp(过期时间)、sub(主题)等)以及其它自定义的数据。这些信息都是铭文的,但不建议存放敏感信息。

  • 签名(Signature):通过对前两部分进行编码后的信息,使用指定的密钥通过头部(Header)中声明的加密算法生成,拥有验证数据完整性和防止篡改。


3、JWT的常规认证流程


2020040121153580.png


       JWT的认证流程如上图。当用户登录时,服务器通过验证用户名和密码后,会生成一个JWT,并将其发送给客户端。这个JWT中可能包含用户的认证信息、权限信息以及其它必要的业务数据。


       客户端在接收到JWT后,通常将其保存在本地(如Cookie、LocalStorage或者SessionStorage)。


       客户端在后续的请求中,携带此JWT(通常是附在HTTP请求头中),无需再次提交用户名和密码。服务器只需对收到的JWT进行解码并验证签名,即可完成用户身份的确认和权限验证。


4、JWT的完整认证流程


       在上面的JWT常规认证流程中,我们可以正常完成登陆、鉴权等认证,但是你会发现在这个流程中,我们无法实现退出登陆。


       当服务端将JWT发放给客户端后,服务端就失去了对JWT的控制权,只能等待这些发放出去的JWT超过有效期,自然失效。


       为了解决这个问题,我们引入了缓存,如下图。


2020040121022176.png


       当服务端生成JWT之后,在返回给客户端之前,先将JWT存入缓存中。要鉴权的时候,需要检验缓存中是否存在这个JWT。


       这样的话,如果用户退出登陆,我们只需要将缓存中的JWT删除,即可保证发放出去的JWT无法再通过鉴权。


5、JWT的优势与挑战


       JWT的主要优点在于无状态性,服务器无需存储会话状态,减轻了服务器压力,同时提高了系统的可扩展性和性能。


       此外,由于JWT的有效期限制,增强了安全性。


       然而,JWT也面临一些挑战,比如密钥的安全保管、JWT过期策略的设计以及如何处理丢失或被盗用的情况。


       因此,在实际应用中,需要综合考虑业务场景和技术特性来合理运用JWT。


6、JWT示例


       概念讲完了,我们最后来看个实例吧。


// Java代码示例
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

// 假设有一个User类,其中包含用户ID
public class User {
private String id;
// 其他属性和方法...
}

// 创建JWT
public String generateJWT(User user) {
// 设置秘钥(在此处使用的是HMAC SHA-256算法)
String secret = "your-secret-key"; // 在实际场景中应当从安全的地方获取秘钥
long ttlMillis = 60 * 60 * 1000; // JWT的有效期为1小时

// 构建载荷,包含用户ID和其他相关信息
Map<String, Object> claims = new HashMap<>();
claims.put("userId", user.getId());
claims.put("exp", System.currentTimeMillis() + ttlMillis); // 设置过期时间

// 生成JWT
    String jwt = Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS256, secret.getBytes(StandardCharsets.UTF_8))
.compact();
    // TODO JWT写入缓存
    return jwt;
}

// 验证JWT
public boolean validateJWT(String jwtToken, String secretKey) {
boolean flag = false;
try {
Jwts.parser().setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8)).parseClaimsJws(jwtToken);
// 如果没有抛出异常,则JWT验证通过
flag = true;
} catch (ExpiredJwtException e) {
// 如果Token过期
System.out.println("JWT已过期");
} catch (UnsupportedJwtException | MalformedJwtException | SignatureException | IllegalArgumentException e) {
// 其他非法情况,如格式错误、签名无效等
System.out.println("JWT验证失败:" + e.getMessage());
}
if (flag) {
        // TODO 校验缓存中是否有该JWT
}
return false;
}

// 使用示例
public static void main(String[] args) {
User user = new User("123"); // 假设这是合法用户的ID
String token = generateJWT(user); // 生成JWT
System.out.println("生成的JWT Token: " + token);

// 验证生成的JWT
boolean isValid = validateJWT(token, "your-secret-key");
if (isValid) {
System.out.println("JWT验证通过!");
} else {
System.out.println("JWT验证未通过!");
}
}



作者:石头聊技术
来源:juejin.cn/post/7354308608044072996
收起阅读 »

记一次安卓广播引起的ANR死锁问题

年初遇到个bug,设备安装应用宝之后打开使用一段时间(浏览和安装应用),大概率会卡死,然后整个系统重启(表现为卡死后过一段时间开机动画出现,重新进入系统)。非常容易复现。 看到这种软重启首先考虑ANR,分析抓到的log   到日志中查找watchdog关键词 ...
继续阅读 »

年初遇到个bug,设备安装应用宝之后打开使用一段时间(浏览和安装应用),大概率会卡死,然后整个系统重启(表现为卡死后过一段时间开机动画出现,重新进入系统)。非常容易复现。


看到这种软重启首先考虑ANR,分析抓到的log


  到日志中查找watchdog关键词
01-25 11:02:42.032 22774 22796 W Watchdog: *** WATCHDOG KILLING SYSTEM PROCESS: Blocked in handler on foreground thread (android.fg), Blocked in handler on main thread (main), Blocked in handler on ActivityManager (ActivityManager), Blocked in handler on PowerManagerService (PowerManagerService)
01-25 11:02:42.033 22774 22796 W Watchdog: android.fg annotated stack trace:
01-25 11:02:42.037 22774 22796 W Watchdog: at com.android.server.am.ActivityManagerService.bindServiceInstance(ActivityManagerService.java:12853)
01-25 11:02:42.037 22774 22796 W Watchdog: - waiting to lock <0x02af09e1> (a com.android.server.am.ActivityManagerService)
01-25 11:02:42.037 22774 22796 W Watchdog: at com.android.server.am.ActivityManagerService.bindServiceInstance(ActivityManagerService.java:12810)
01-25 11:02:42.037 22774 22796 W Watchdog: at android.app.ContextImpl.bindServiceCommon(ContextImpl.java:2035)
01-25 11:02:42.037 22774 22796 W Watchdog: at android.app.ContextImpl.bindService(ContextImpl.java:1958)
01-25 11:02:42.037 22774 22796 W Watchdog: at com.android.internal.infra.ServiceConnector$Impl.bindService(ServiceConnector.java:343)
01-25 11:02:42.037 22774 22796 W Watchdog: at com.android.internal.infra.ServiceConnector$Impl.enqueueJobThread(ServiceConnector.java:462)
01-25 11:02:42.037 22774 22796 W Watchdog: at com.android.internal.infra.ServiceConnector$Impl.lambda$enqueue$1$com-android-internal-infra-ServiceConnector$Impl(ServiceConnector.java:445)
01-25 11:02:42.037 22774 22796 W Watchdog: at com.android.internal.infra.ServiceConnector$Impl$$ExternalSyntheticLambda2.run(Unknown Source:4)
01-25 11:02:42.037 22774 22796 W Watchdog: at android.os.Handler.handleCallback(Handler.java:942)
01-25 11:02:42.037 22774 22796 W Watchdog: at android.os.Handler.dispatchMessage(Handler.java:99)
01-25 11:02:42.037 22774 22796 W Watchdog: at android.os.Looper.loopOnce(Looper.java:201)
01-25 11:02:42.037 22774 22796 W Watchdog: at android.os.Looper.loop(Looper.java:288)
01-25 11:02:42.037 22774 22796 W Watchdog: at android.os.HandlerThread.run(HandlerThread.java:67)
01-25 11:02:42.037 22774 22796 W Watchdog: at com.android.server.ServiceThread.run(ServiceThread.java:44)
01-25 11:02:42.038 22774 22796 W Watchdog: main annotated stack trace:
01-25 11:02:42.038 22774 22796 W Watchdog: at com.android.server.alarm.AlarmManagerService$AlarmHandler.handleMessage(AlarmManagerService.java:4993)
01-25 11:02:42.038 22774 22796 W Watchdog: - waiting to lock <0x0edf48f2> (a java.lang.Object)
01-25 11:02:42.038 22774 22796 W Watchdog: at android.os.Handler.dispatchMessage(Handler.java:106)
01-25 11:02:42.038 22774 22796 W Watchdog: at android.os.Looper.loopOnce(Looper.java:201)
01-25 11:02:42.038 22774 22796 W Watchdog: at android.os.Looper.loop(Looper.java:288)
01-25 11:02:42.038 22774 22796 W Watchdog: at com.android.server.SystemServer.run(SystemServer.java:968)
01-25 11:02:42.038 22774 22796 W Watchdog: at com.android.server.SystemServer.main(SystemServer.java:653)
01-25 11:02:42.038 22774 22796 W Watchdog: at java.lang.reflect.Method.invoke(Native Method)
01-25 11:02:42.038 22774 22796 W Watchdog: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
01-25 11:02:42.038 22774 22796 W Watchdog: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:920)
01-25 11:02:42.038 22774 22796 W Watchdog: ActivityManager annotated stack trace:
01-25 11:02:42.038 22774 22796 W Watchdog: at com.android.server.am.BroadcastQueue.processNextBroadcast(BroadcastQueue.java:1154)
01-25 11:02:42.038 22774 22796 W Watchdog: - waiting to lock <0x02af09e1> (a com.android.server.am.ActivityManagerService)
01-25 11:02:42.038 22774 22796 W Watchdog: at com.android.server.am.BroadcastQueue.-$$Nest$mprocessNextBroadcast(Unknown Source:0)
01-25 11:02:42.038 22774 22796 W Watchdog: at com.android.server.am.BroadcastQueue$BroadcastHandler.handleMessage(BroadcastQueue.java:224)
01-25 11:02:42.038 22774 22796 W Watchdog: at android.os.Handler.dispatchMessage(Handler.java:106)
01-25 11:02:42.038 22774 22796 W Watchdog: at android.os.Looper.loopOnce(Looper.java:201)
01-25 11:02:42.038 22774 22796 W Watchdog: at android.os.Looper.loop(Looper.java:288)
01-25 11:02:42.038 22774 22796 W Watchdog: at android.os.HandlerThread.run(HandlerThread.java:67)
01-25 11:02:42.038 22774 22796 W Watchdog: at com.android.server.ServiceThread.run(ServiceThread.java:44)
01-25 11:02:42.039 22774 22796 W Watchdog: PowerManagerService annotated stack trace:
01-25 11:02:42.039 22774 22796 W Watchdog: at com.android.server.power.PowerManagerService.handleSandman(PowerManagerService.java:3257)
01-25 11:02:42.039 22774 22796 W Watchdog: - waiting to lock <0x0f550be5> (a java.lang.Object)
01-25 11:02:42.039 22774 22796 W Watchdog: at com.android.server.power.PowerManagerService.-$$Nest$mhandleSandman(Unknown Source:0)
01-25 11:02:42.039 22774 22796 W Watchdog: at com.android.server.power.PowerManagerService$PowerManagerHandlerCallback.handleMessage(PowerManagerService.java:5103)
01-25 11:02:42.039 22774 22796 W Watchdog: at android.os.Handler.dispatchMessage(Handler.java:102)
01-25 11:02:42.039 22774 22796 W Watchdog: at android.os.Looper.loopOnce(Looper.java:201)
01-25 11:02:42.039 22774 22796 W Watchdog: at android.os.Looper.loop(Looper.java:288)
01-25 11:02:42.039 22774 22796 W Watchdog: at android.os.HandlerThread.run(HandlerThread.java:67)
01-25 11:02:42.039 22774 22796 W Watchdog: at com.android.server.ServiceThread.run(ServiceThread.java:44)
01-25 11:02:42.039 22774 22796 W Watchdog: *** GOODBYE!

可以很清楚的看到watchdog提示waiting to lock </xxxxx/> 关键词已经确定大概率是死锁问题了。
导出Anr日志继续进行分析,一般先从group="main"分析起


"main" prio=5 tid=1 Blocked
| group="main" sCount=1 ucsCount=0 flags=1 obj=0x71c515d0 self=0xb4000078c0a0abe0
| sysTid=2257 nice=-2 cgrp=foreground sched=0/0 handle=0x7a80db94f8
| state=S schedstat=( 67235248988 24733272110 200587 ) utm=3966 stm=2756 core=7 HZ=100
| stack=0x7fca926000-0x7fca928000 stackSize=8188KB
| held mutexes=
at com.android.server.am.ActivityManagerService.broadcastIntentWithFeature(ActivityManagerService.java:14615)
- waiting to lock <0x0185c3b1> (a com.android.server.am.ActivityManagerService) held by thread 16
at android.app.ActivityManager.broadcastStickyIntent(ActivityManager.java:4620)
at android.app.ActivityManager.broadcastStickyIntent(ActivityManager.java:4610)
at com.android.server.BatteryService.lambda$sendBatteryChangedIntentLocked$0(BatteryService.java:780)
at com.android.server.BatteryService.$r8$lambda$r64V5AVg_Okl7PnB1VjeN4oyo1I(unavailable:0)
at com.android.server.BatteryService$$ExternalSyntheticLambda5.run(unavailable:2)
at android.os.Handler.handleCallback(Handler.java:942)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loopOnce(Looper.java:201)
at android.os.Looper.loop(Looper.java:288)
at com.android.server.SystemServer.run(SystemServer.java:968)
at com.android.server.SystemServer.main(SystemServer.java:653)
at java.lang.reflect.Method.invoke(Native method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:920)

main它在等待thread 16的lock <0x0185c3b1>,所以我们去tid=16再看看


"android.display" prio=5 tid=16 Blocked
| group="main" sCount=1 ucsCount=0 flags=1 obj=0x14340db8 self=0xb4000078c0a40a10
| sysTid=2349 nice=-3 cgrp=top-app sched=0/0 handle=0x76c8933cb0
| state=S schedstat=( 1805684706 1655206467 5034 ) utm=99 stm=81 core=7 HZ=100
| stack=0x76c8830000-0x76c8832000 stackSize=1039KB
| held mutexes=
at com.android.server.wm.ActivityTaskManagerService$LocalService.onProcessAdded(ActivityTaskManagerService.java:5740)
- waiting to lock <0x033a7977> (a com.android.server.wm.WindowManagerGlobalLock) held by thread 198
at com.android.server.am.ProcessList$MyProcessMap.put(ProcessList.java:699)
at com.android.server.am.ProcessList.addProcessNameLocked(ProcessList.java:2946)
- locked <0x09f229e4> (a com.android.server.am.ActivityManagerProcLock)
at com.android.server.am.ProcessList.newProcessRecordLocked(ProcessList.java:3039)
at com.android.server.am.ProcessList.startProcessLocked(ProcessList.java:2487)
at com.android.server.am.ActivityManagerService.startProcessLocked(ActivityManagerService.java:2854)
at com.android.server.am.ActivityManagerService$LocalService.startProcess(ActivityManagerService.java:17450)
- locked <0x0185c3b1> (a com.android.server.am.ActivityManagerService)
at com.android.server.wm.ActivityTaskManagerService$$ExternalSyntheticLambda11.accept(unavailable:27)
at com.android.internal.util.function.pooled.PooledLambdaImpl.doInvoke(PooledLambdaImpl.java:363)
at com.android.internal.util.function.pooled.PooledLambdaImpl.invoke(PooledLambdaImpl.java:204)
at com.android.internal.util.function.pooled.OmniFunction.run(OmniFunction.java:97)
at android.os.Handler.handleCallback(Handler.java:942)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loopOnce(Looper.java:201)
at android.os.Looper.loop(Looper.java:288)
at android.os.HandlerThread.run(HandlerThread.java:67)
at com.android.server.ServiceThread.run(ServiceThread.java:44)

可以看到waiting to lock <0x033a7977>,它在等待threa198的锁释放。因此再去tid=198看看


"binder:2257_1C" prio=5 tid=198 Blocked
| group="main" sCount=1 ucsCount=0 flags=1 obj=0x143443a0 self=0xb4000078c0bb49f0
| sysTid=6550 nice=-10 cgrp=foreground sched=0/0 handle=0x7638d60cb0
| state=S schedstat=( 12455767235 6173702511 27824 ) utm=695 stm=550 core=4 HZ=100
| stack=0x7638c69000-0x7638c6b000 stackSize=991KB
| held mutexes=
at com.android.server.display.DisplayManagerService.setDisplayPropertiesInternal(DisplayManagerService.java:2019)
- waiting to lock <0x0faa6d02> (a com.android.server.display.DisplayManagerService$SyncRoot) held by thread 130
at com.android.server.display.DisplayManagerService.-$$Nest$msetDisplayPropertiesInternal(unavailable:0)
at com.android.server.display.DisplayManagerService$LocalService.setDisplayProperties(DisplayManagerService.java:3811)
at com.android.server.wm.DisplayContent.applySurfaceChangesTransaction(DisplayContent.java:4878)
at com.android.server.wm.RootWindowContainer.applySurfaceChangesTransaction(RootWindowContainer.java:1022)
at com.android.server.wm.RootWindowContainer.performSurfacePlacementNoTrace(RootWindowContainer.java:824)
at com.android.server.wm.RootWindowContainer.performSurfacePlacement(RootWindowContainer.java:785)
at com.android.server.wm.WindowSurfacePlacer.performSurfacePlacementLoop(WindowSurfacePlacer.java:177)
at com.android.server.wm.WindowSurfacePlacer.performSurfacePlacement(WindowSurfacePlacer.java:126)
at com.android.server.wm.WindowManagerService.relayoutWindow(WindowManagerService.java:2501)
- locked <0x033a7977> (a com.android.server.wm.WindowManagerGlobalLock)
at com.android.server.wm.Session.relayout(Session.java:253)
at com.android.server.wm.Session.relayoutAsync(Session.java:267)
at android.view.IWindowSession$Stub.onTransact(IWindowSession.java:757)
at com.android.server.wm.Session.onTransact(Session.java:178)
at android.os.Binder.execTransactInternal(Binder.java:1285)
at android.os.Binder.execTransact(Binder.java:1244)

198在等待130来释放锁,waiting to lock <0x0faa6d02>,不要嫌麻烦再跟去130


"binder:2257_4" prio=5 tid=130 Blocked
| group="main" sCount=1 ucsCount=0 flags=1 obj=0x15288e48 self=0xb4000078c0b32400
| sysTid=2541 nice=0 cgrp=foreground sched=0/0 handle=0x76489a8cb0
| state=S schedstat=( 8323462229 6484203506 25835 ) utm=482 stm=349 core=7 HZ=100
| stack=0x76488b1000-0x76488b3000 stackSize=991KB
| held mutexes=
at com.android.server.am.ActivityManagerService.registerReceiverWithFeature(ActivityManagerService.java:13285)
- waiting to lock <0x0185c3b1> (a com.android.server.am.ActivityManagerService) held by thread 16
at android.app.ContextImpl.registerReceiverInternal(ContextImpl.java:1816)
at android.app.ContextImpl.registerReceiver(ContextImpl.java:1750)
at android.app.ContextImpl.registerReceiver(ContextImpl.java:1738)
at com.android.server.display.DisplayPowerController.<init>(DisplayPowerController.java:674)
at com.android.server.display.DisplayManagerService.addDisplayPowerControllerLocked(DisplayManagerService.java:2710)
at com.android.server.display.DisplayManagerService.handleLogicalDisplayAddedLocked(DisplayManagerService.java:1561)
at com.android.server.display.DisplayManagerService.-$$Nest$mhandleLogicalDisplayAddedLocked(unavailable:0)
at com.android.server.display.DisplayManagerService$LogicalDisplayListener.onLogicalDisplayEventLocked(DisplayManagerService.java:2811)
at com.android.server.display.LogicalDisplayMapper.sendUpdatesForDisplaysLocked(LogicalDisplayMapper.java:759)
at com.android.server.display.LogicalDisplayMapper.updateLogicalDisplaysLocked(LogicalDisplayMapper.java:733)
at com.android.server.display.LogicalDisplayMapper.handleDisplayDeviceAddedLocked(LogicalDisplayMapper.java:580)
at com.android.server.display.LogicalDisplayMapper.onDisplayDeviceEventLocked(LogicalDisplayMapper.java:201)
at com.android.server.display.DisplayDeviceRepository.sendEventLocked(DisplayDeviceRepository.java:214)
at com.android.server.display.DisplayDeviceRepository.handleDisplayDeviceAdded(DisplayDeviceRepository.java:158)
- locked <0x0faa6d02> (a com.android.server.display.DisplayManagerService$SyncRoot)
at com.android.server.display.DisplayDeviceRepository.onDisplayDeviceEvent(DisplayDeviceRepository.java:87)
at com.android.server.display.DisplayManagerService.createVirtualDisplayLocked(DisplayManagerService.java:1418)
at com.android.server.display.DisplayManagerService.createVirtualDisplayInternal(DisplayManagerService.java:1381)
- locked <0x0faa6d02> (a com.android.server.display.DisplayManagerService$SyncRoot)
at com.android.server.display.DisplayManagerService.-$$Nest$mcreateVirtualDisplayInternal(unavailable:0)
at com.android.server.display.DisplayManagerService$BinderService.createVirtualDisplay(DisplayManagerService.java:3180)
at android.hardware.display.IDisplayManager$Stub.onTransact(IDisplayManager.java:659)
at android.os.Binder.execTransactInternal(Binder.java:1280)
at android.os.Binder.execTransact(Binder.java:1244)

130在等待16释放锁,这下子就明朗了,画个它们之间的关系图,这样就很明朗了他们三个之间死锁引起main主线程等待超时。


graph TD
binder2257_4 --> binder2257_1c --> android.display --> binder2257_4 & main主线程

前面这里只是分析出问题原因。下面来说问题为什么会发生。从main主线程开始追到binder2257_4然后开始循环。合理怀疑问题是先从binder2257_4出现的。都是binder是因为他们之间是用binder通信的。
看到binder2257_4这个anr日志中出现


at com.android.server.display.DisplayPowerController.<init>(DisplayPowerController.java:674)

竟然是在displaypowercontroller init中出现的,去到这个文件的674行,发现这里在注册广播接收器。


image.png


尝试退回这条提交之后果然问题没有复现了,分析这条提交,它是想在displaypowercontroller中注册一个广播接收器。用于接收挂电话的时候发出的广播。当收到这条广播之后就会通知displaypowercontroller亮屏。其实就是实现了一个通话时对方挂断电话之后自动亮屏。它是在创建阶段进行的注册。要分析为什么这个会导致死锁。


作者:用户8081391597591
来源:juejin.cn/post/7353158088730165259
收起阅读 »

调了个方法导致接口变慢好多,真实原因有点坑。

前言 这篇文章是按照我的记忆梳理的,然后解决方法其实很简单,主要是梳理一下当时的排查思路,所以请大家多多指教。错误之处或表述不清楚的地方,欢迎评论指出或建议,感谢。 背景 事情发生在一个正常的下午,领导对我说:有项目组汇报说一个创建的接口变的比较慢,让我...
继续阅读 »

前言



这篇文章是按照我的记忆梳理的,然后解决方法其实很简单,主要是梳理一下当时的排查思路,所以请大家多多指教。错误之处或表述不清楚的地方,欢迎评论指出或建议,感谢。



背景



事情发生在一个正常的下午,领导对我说:有项目组汇报说一个创建的接口变的比较慢,让我尽快排查一下。我一听,这个功能不是我写的,心里首先放松一下,那就排查吧!



验证线上


首先,当然是和领导确认,什么情况下,访问这个接口慢,因为有时候可能在某个特殊的场景下才会导致此类问题,或者是偶发情况,确定了这一点,也方便排查,然后我就按照领导说的,找到所属的数据页面,进行同样的操作。发现果然比较慢(这是我工作中学到的,不管别人怎么说,自己验证一遍),F12看了下,返回时间差不多快2s了,这个速度确实是有点慢的,因为这个添加的处理逻辑按理来说是不复杂的,不应该这么慢。


本地排查


接下来,就是排查的步骤,首先本地先启动一下,连接测试库看看是否有慢的问题,如果本地也慢,就比较方便一些,可以通过日志来打印每一部分花费的时间(我一般用StopWatch类),就可以知道哪里慢了。但是遗憾的是本地没有复现这种情况。


代码排查


既然无法复现,就只能看代码了,对比了下涉及到这个文件的提交记录,大概伪代码如下:


...
...
String name = xxxService.getMember(name);
...
Project project = prjService.getProject(code);
...
project.getProjectId();
...
...

看见的第一眼,我直接忽略了这一段,觉得没啥问题啊,然后事实证明,事出反常必有妖。


最终原因


其实慢的原因就是我直觉忽略了的这一段代码中的Project project = prjService.getProject(code);,当我点进去后我才发现,里面别有洞天,我没法给大家复制代码,还是给大家一个伪代码:


....
Project project = prjDao.getObject(code);
String pId = project.getProjectId();
List<Task> list = prjDao.queryTaskList(pId);
for (Task task : list){
String taskId = task.getTaskId();
Work w = prjDao.getObject(taskId);
....
s = s.apend(w.getName());
}
project.setExtend(s);
....


以上代码存在的问题主要就是在for循环中进行对数据库的查询,如果list的长度很长的话,就会导致查询慢的问题,当时发现的时候真的是无语了,你如果有扩展,可以在方法中进行清晰的标识(比如:queryprojectExtend),并且代码也不必这么写,可以提取所有的taskId到一个list中,然后作为参数进行查询。


另外还有一个坑的点就是,其实原来的人调用这段方法本身其实就是为了拿一个project.getProjectId();,所以完全可以另外写一个简单获取的方法即可。


解决并验证


因为知道了原因,所以从数据库找了数据比较多的一个来进行复现,果然复现成功,也证明了问题就是出在这里。
首先把代码进行修复,修复后的伪代码如下:


....
Project project = prjDao.getObject(code);
String pId = project.getProjectId();
List<Task> list = prjDao.queryTaskList(pId);
List<String> idList = list.getIdLIst(list);
List<Work> works = prjDao.queryObjectsByIds(idList);
for (Work task : list){
s = s.apend(w.getName());
}
project.setExtend(s);
....

以上虽然也使用了for循环,但是内部只是做了字符串的拼接,也可使用Stream,会更简洁。
更改完后,继续测试该接口,发现是正常的。


思考总结



其实这次的问题很简单,没有涉及到高大上的一些问题来调整,要避免其实也很简单,调用其他人写的方法时,大概点进去观察一下。写方法的时候多想一下,是否会造成查询慢的问题,就可以了。




  • for循环查询这种情况,尽量避免。

  • 调用他人方法时需要知道内部大概的逻辑,切勿望文生义。

  • 不要想当然,觉得没问题就不排查。


致谢


感谢你的耐心阅读,如果我的分享对你有所启发或帮助,就给个赞呗,很开心能帮到别人。


作者:bramble
来源:juejin.cn/post/7348842402826321961
收起阅读 »

脱敏工具?整个全局的吧

咱又手痒造轮子啦!Hutool工具包有这个一个类DesensitizedUtil实现了一些常见业务数据类型的脱敏,像手机号,中文名,身-份-证号,银彳亍卡号等。那咱就基于它写一个全局切面,需要脱敏的用注解标识,思路有了说干就干。 咱先定义一个切入点注解@Dat...
继续阅读 »

咱又手痒造轮子啦!Hutool工具包有这个一个类DesensitizedUtil实现了一些常见业务数据类型的脱敏,像手机号,中文名,身-份-证号,银彳亍卡号等。那咱就基于它写一个全局切面,需要脱敏的用注解标识,思路有了说干就干。


咱先定义一个切入点注解@DataDesensitized


@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataDesensitized {
}

然后咱再定义一个注解标识字段脱敏@Desensitized


@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Desensitized {
//加上hutool类中的脱敏类型枚举选择脱敏策略
DesensitizedUtil.DesensitizedType type();
}

最后写切面类


@Aspect
@Component
@Slf4j
public class DataDesensitizedAspect {
@AfterReturning(pointcut = "@annotation(dd)", returning = "result")
public void doAfterReturning(JoinPoint joinPoint, DataDesensitized dd, Object result) {
//TODO 这里可以根据组织架构角色控制是否脱敏
boolean need = true;
if (!need) {
return;
}
//方法响应一般有分页实体,list集合,单实体对象,那就分类处理
if (result instanceof PageInfo) {
PageInfo page = (PageInfo) result;
List records = page.getList();
for (Object record : records) {
objReplace(record);
}
} else if (result instanceof List) {
List list = (List) result;
for (Object obj : list) {
objReplace(obj);
}
} else {
objReplace(result);
}
}

public static <T> void objReplace(T t) {
try {
Field[] declaredFields = ReflectUtil.getFields(t.getClass());
for (Field field : declaredFields) {
Desensitized des = field.getAnnotation(Desensitized.class);
//被脱敏注解修饰且string类型
if (des != null &&
"class java.lang.String".equals(field.getGenericType().toString())) {
Object fieldValue = ReflectUtil.getFieldValue(t, field);
if (fieldValue == null || StringUtils.isEmpty(fieldValue.toString())) {
continue;
}
DesensitizedUtil.DesensitizedType type = des.type();
String hide = DesensitizedUtil.desensitized(fieldValue.toString(),type);
ReflectUtil.setFieldValue(t, field, hide);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

在业务方法上标识切入点注解


@Override
@DataProtection
public PageInfo<OrderDetailsVo> queryOrderDetails(QueryParam param) {
return mapper.queryOrderDetails(param);
}

vo实体中需要脱敏的字段加@Desensitized


@Data
public class OrderDetailsVo {
private String orderNo;
private String sn;

@Desensitized(type = DesensitizedUtil.DesensitizedType.CHINESE_NAME)
private String username;

@Desensitized(type = DesensitizedUtil.DesensitizedType.MOBILE_PHONE)
private String mobile;

@Desensitized(type = DesensitizedUtil.DesensitizedType.ID_CARD)
private String idCard;
}

完成!


次日,产品经理要求这个20位的sn字符串从第五位脱敏到第十八位,hutool工具没有这个类型的枚举!成!咱再把轮子改造一下


自己写一个脱敏策略枚举DesensitizedType,对比hutool只加了CUSTOM自定义脱敏类型


public enum DesensitizedType {
//自定义脱敏标识
CUSTOM,
//用户id
USER_ID,
//中文名
CHINESE_NAME,
//身-份-证号
ID_CARD,
//座机号
FIXED_PHONE,
//手机号
MOBILE_PHONE,
//地址
ADDRESS,
//电子邮件
EMAIL,
//密码
PASSWORD,
//中国大陆车牌,包含普通车辆、新能源车辆
CAR_LICENSE,
//银彳亍卡
BANK_CARD
}

@Desensitized改造


@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Desensitized {
//替换成自己定义的枚举
DesensitizedType type() default DesensitizedType.CUSTOM;

/**
* 当type不指定时,可自定义脱敏起始位置(包含)
*/

int startInclude() default 0;

/**
* 当type不指定时,可自定义脱敏结束位置(不包含) ,-1代表字符串长度
*/

int endExclude() default -1;
}

切面类改造


public static <T> void objReplace(T t) {
try {
Field[] declaredFields = ReflectUtil.getFields(t.getClass());
for (Field field : declaredFields) {
Desensitized des = field.getAnnotation(Desensitized.class);
//被脱敏注解修饰且string类型
if (des != null &&
"class java.lang.String".equals(field.getGenericType().toString())) {
Object fieldValue = ReflectUtil.getFieldValue(t, field);
if (fieldValue == null || StringUtils.isEmpty(fieldValue.toString())) {
continue;
}
String value = fieldValue.toString();
String hide = "";
if (des.type() == DesensitizedType.CUSTOM) {
int startInclude = des.startInclude();
int endExclude = des.endExclude();
if (endExclude == -1) {
endExclude = value.length();
}
hide = StrUtil.hide(value, startInclude, endExclude);
} else {
DesensitizedUtil.DesensitizedType type =
DesensitizedUtil.DesensitizedType.valueOf(des.type().toString());
hide = DesensitizedUtil.desensitized(value, type);
}
ReflectUtil.setFieldValue(t, field, hide);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}

实体标识脱敏字段


@Data
public class OrderDetailsVo {
private String orderNo;

@Desensitized(startInclude = 5,endExclude = 18)
private String sn;

@Desensitized(type = DesensitizedType.CHINESE_NAME)
private String username;

@Desensitized(type = DesensitizedType.MOBILE_PHONE)
private String mobile;

@Desensitized(type = DesensitizedType.ID_CARD)
private String idCard;
}

这下可以开开心心把轮子给小伙伴用啦开心😘😘😘


作者:开大会汪汪队
来源:juejin.cn/post/7348830480789962787
收起阅读 »

js检测网页空闲状态(一定时间内无操作)

web
1. 背景 最近开发项目时,常碰到“用户在一定时间内无任何操作时,跳转到某个页面”的需求。 网上冲浪后,也没有找到一个比较好的js封装去解决这个问题,从而决定自己实现。 2. 如何判断页面是否空闲 首先,我们要知道什么是空闲?用户一定时间内,没有对网页进行任何...
继续阅读 »

1. 背景


最近开发项目时,常碰到“用户在一定时间内无任何操作时,跳转到某个页面”的需求。


网上冲浪后,也没有找到一个比较好的js封装去解决这个问题,从而决定自己实现。


2. 如何判断页面是否空闲


首先,我们要知道什么是空闲?用户一定时间内,没有对网页进行任何操作,则当前网页为空闲状态。


用户操作网页,无非就是通过鼠标键盘两个输入设备(暂不考虑手柄等设备)。因而我们可以监听相应的输入事件,来判断网页是否空闲(用户是否有操作网页)。



  1. 监听鼠标移动事件mousemove

  2. 监听键盘按下事件mousedown

  3. 在用户进入网页后,设置延时跳转,如果触发以上事件,则移除延时器,并重新开始。


3. 网页空闲检测实现


3.1 简易实现


以下代码,简单实现了一个判断网页空闲的方法:


const onIdleDetection = (callback, timeout = 15, immediate = false) => {
let pageTimer;

const onClearTimer = () => {
pageTimer && clearTimeout(pageTimer);
pageTimer = undefined;
};
const onStartTimer = () => {
onClearTimer();
pageTimer = setTimeout(() => {
callback();
}, timeout * 1000);
};

const startDetection = () => {
onStartTimer();
document.addEventListener('mousedown', onStartTimer);
document.addEventListener('mousemove', onStartTimer);
};
const stopDetection = () => {
onClearTimer();
document.removeEventListener('mousedown', onStartTimer);
document.removeEventListener('mousemove', onStartTimer);
};
const restartDetection = () => {
onClearTimer();
onStartTimer();
};

if (immediate) {
startDetection();
}

return {
startDetection,
stopDetection,
restartDetection
};
};

也许你注意到了,我并没有针对onStartTimer事件进行防抖,那这是不是会对性能有影响呢?


是的,肯定有那么点影响,那我为啥不添加防抖呢?


这是因为添加防抖后,形成了setTimeout嵌套,嵌套setTimeout会有精度问题(参考)。


或许你还会说,非活动标签页(网页被隐藏)的setTimeout的执行和精度会有问题(参考非活动标签的超时)。


确实存在以上问题,接下来我们就来一一解决吧!


3.2 处理频繁触发问题


我们可以通过添加一个变量记录开始执行时间,当下一次执行与当前的时间间隔小于某个值时直接退出函数,从而解决这个问题(节流思想应用)。


const onIdleDetection = (callback, timeout = 15, immediate = false) => {
let pageTimer;
// 记录开始时间
let beginTime = 0;
const onStartTimer = () => {
// 触发间隔小于100ms时,直接返回
const currentTime = Date.now();
if (pageTimer && currentTime - beginTime < 100) {
return;
}

onClearTimer();
// 更新开始时间
beginTime = currentTime;
pageTimer = setTimeout(() => {
callback();
}, timeout * 1000);
};
const onClearTimer = () => {
pageTimer && clearTimeout(pageTimer);
pageTimer = undefined;
};

const startDetection = () => {
onStartTimer();
document.addEventListener('mousedown', onStartTimer);
document.addEventListener('mousemove', onStartTimer);
};
const stopDetection = () => {
onClearTimer();
document.removeEventListener('mousedown', onStartTimer);
document.removeEventListener('mousemove', onStartTimer);
};
const restartDetection = () => {
onClearTimer();
onStartTimer();
};

if (immediate) {
startDetection();
}

return {
startDetection,
stopDetection,
restartDetection
};
};

3.3 处理页面被隐藏的情况(完整实现)


我们可以监听visibilitychange事件,在页面隐藏时移除延时器,然后页面显示时继续计时,从而解决这个问题。


/**
* 网页空闲检测
* @param {() => void} callback 空闲时执行,即一定时长无操作时触发
* @param {number} [timeout=15] 时长,默认15s,单位:秒
* @param {boolean} [immediate=false] 是否立即开始,默认 false
* @returns
*/

const onIdleDetection = (callback, timeout = 15, immediate = false) => {
let pageTimer;
let beginTime = 0;
const onClearTimer = () => {
pageTimer && clearTimeout(pageTimer);
pageTimer = undefined;
};
const onStartTimer = () => {
const currentTime = Date.now();
if (pageTimer && currentTime - beginTime < 100) {
return;
}

onClearTimer();
beginTime = currentTime;
pageTimer = setTimeout(() => {
callback();
}, timeout * 1000);
};

const onPageVisibility = () => {
// 页面显示状态改变时,移除延时器
onClearTimer();

if (document.visibilityState === 'visible') {
const currentTime = Date.now();
// 页面显示时,计算时间,如果超出限制时间则直接执行回调函数
if (currentTime - beginTime >= timeout * 1000) {
callback();
return;
}
// 继续计时
pageTimer = setTimeout(() => {
callback();
}, timeout * 1000 - (currentTime - beginTime));
}
};

const startDetection = () => {
onStartTimer();
document.addEventListener('mousedown', onStartTimer);
document.addEventListener('mousemove', onStartTimer);
document.addEventListener('visibilitychange', onPageVisibility);
};

const stopDetection = () => {
onClearTimer();
document.removeEventListener('mousedown', onStartTimer);
document.removeEventListener('mousemove', onStartTimer);
document.removeEventListener('visibilitychange', onPageVisibility);
};

const restartDetection = () => {
onClearTimer();
onStartTimer();
};

if (immediate) {
startDetection();
}

return {
startDetection,
stopDetection,
restartDetection
};
};

通过以上代码,我们就完整地实现了一个网页空闲状态检测的方法。


4. 扩展阅读


chrome浏览器其实提供了一个Idle DetectionAPI,来实现网页空闲状态的检测,但是这个API还是一个实验性特性,并且Firefox与Safari不支持。API参考


作者:求知若饥
来源:juejin.cn/post/7344670957405405223
收起阅读 »

为什么马斯克能扭曲现实

好多人看完Elon的新传记开始扯现实扭曲立场,聊虚头巴脑的东西都没用,关键是为什么Elon有这种来扭曲现实的能力。   马斯克手下的工程师的bar都非常高,早期在SpaceX为了不雇二流工程师宁愿自己上,想要push下面这些人拼命工作,要没点真本事基本不可能,...
继续阅读 »

好多人看完Elon的新传记开始扯现实扭曲立场,聊虚头巴脑的东西都没用,关键是为什么Elon有这种来扭曲现实的能力。


 


马斯克手下的工程师的bar都非常高,早期在SpaceX为了不雇二流工程师宁愿自己上,想要push下面这些人拼命工作,要没点真本事基本不可能,分享几个例子(在实际的工程和制造过程中大概率是常态):


 


Gega Factory流水线上,本来用6颗螺丝固定的地方,Elon觉得4颗就够了,下面的工程师解释说再少的话强度不够,Elon说他在脑子里模拟了一下冲压强度,感觉4颗螺丝就顶得住,改完后一切正常运行,这种判断需要大量的工程经验和物理直觉。


 


SpaceX开始制造星舰的时候,火箭材料的选择上争议很多,团队告诉他用不锈钢会比用猎鹰9号上的锂铝合金或者碳纤维更重,Elon的直觉告诉他不是这样,要求团队把具体数字算出来,结果Elon是对的,而且不锈钢在极寒条件下强度会增加50%,更适合装载超低温液氧和液氮,再者他可以雇佣没有碳纤维经验的工人(用工的成本更低),更进一步不锈钢的熔点高,星舰的外壁不用额外设置隔热层,最后焊接还更容易(锂铝合金需要超净焊接环境,不锈钢直接露天大棚咔咔焊)。更细节的是,要确认星舰外壁的厚度时,一线的焊接工人给Elon的反馈是4.8mm,Elon觉得4mm就行,不顾一线人员的担忧拍板了,后来证明没问题。


 


Elon在SpaceX内部普及了一个概念叫"白痴指数",指的是一个零件或组件的总成本和原材料成本之间的比值。他在会上问下面的工程师根据⽩痴指数,猛禽发动机中做得最好的部分是什么,结果下面的人答不上来,试探性地答了一个喷管护套,说成本大概13000美元,由一种钢整块制成,Elon追问这种原材料的价格是多少,对方回答说大约几千美元,Elon立马纠正说200美元现场打脸并表示对方错的离谱。


 


现实扭曲立场是什么?就是你老板在你的工作领域很多地方比你还懂,角度比你还新奇,考虑比你还全面,对细节数据掌握比你还扎实,在过往的技术方向,工程方案,成本控制的争论中超出大家的预期,证明他更正确。假想一下这种老板要求员工把某个指标在某一期限内优化xx%,或者彻底拿出一个更好的方案,员工说不可能,然后老板自己住在工厂加班最后有理有据prove you wrong是什么感觉?下一次他再提一个看起来是扯淡的需求,团队就会觉得这是可能的,然后work hard on it,这就是现实扭曲立场。


 


如果leader本人没有这种身先士卒的精神,以及让最好的工程师都赞叹的专业能力,能扭曲个啥?喊几句cheap slogan谁都会


 


不是说Elon是神,他也会犯错误,比如2018年Model3产能爬坡的时候在工厂自动化上面做的过于激进,反而降低了效率(比如某些机械臂在搬运物件的时候出现意外导致整个产线停滞,用手工可靠得多),最后为了最大化生产效率又开始"去自动化"。类似地,在收购SolarCity早期,他疯狂push下面的人增加屋顶太阳能面板的安装效率,搞了半天也没什么成效后来不了了之。但这些错误都是可以接受的,都不妨碍他在团队塑造硬核文化然后drive整个公司往前走。


 


那些故事性很强的鸡汤鸡血都是扯淡,事实很简单,就是Elon在技术和商业上认知几乎比所有人都好。


作者:数据智能老司机
来源:juejin.cn/post/7351430820715266074
收起阅读 »

生命里每一段记忆,可能都伴随着DNA的断裂与修复

周末的时候刷到一篇论文,可能揭示了人类长期记忆的形成机制。而这机制的背后似乎是生命的本质。 Nature上的新研究 这是一篇新鲜热乎的Paper,3月27号刚发的。(P.S.:Paper原地址我放原文链接里了) 图源:Nature ...
继续阅读 »

周末的时候刷到一篇论文,可能揭示了人类长期记忆的形成机制。而这机制的背后似乎是生命的本质


Nature上的新研究


这是一篇新鲜热乎的Paper,3月27号刚发的。(P.S.:Paper原地址我放原文链接里了)



图源:Nature

图源:Nature


这个题目是说记忆构成和TLR9通路中的DNA感知的关系





不是说最近大模型Kimi很火,支持200万字无损上下文了么,于是我直接上Kimi试一下。



图源:Kimi

图源:Kimi


嗯,上传上去稍微问一下整体架构和结论,还是挺有意思的。说的似乎是记忆和DNA的关系。但来来回回问Kimi感觉还是挺花时间,算了还是自己看看吧。


我尽量试着给大家解释解释。


长期记忆和DNA损伤


这篇Paper里的核心是一个反直觉的结论是:在人类大脑海马体形成记忆的过程中,一些细胞会产生损伤,尤其是双瓣DNA会产生断裂和重组


也就是说,记忆的产生本身对人体来说,并不是获得,而是损伤。


是不是非常反直觉?


这个Paper里的研究思路很明确。结论也非常直接。


就是在记忆形成的短期内(大概1-2个小时内),大脑中负责记忆的部分会经历一个“破坏的过程”。这个过程堪称惨烈。比如双链DNA会产生断裂、细胞的瓣膜会产生破裂,甚至还有一些细胞核里的组蛋白会流到细胞核外,同时伴随着一些“炎症反应”。



图源:Nature

图源:Nature


很多人看到“炎症反应”会觉得是发炎,但其实不完全是。学术上的炎症反应指的是“损伤、抗损伤和修复的动态过程”。


那么,在记忆产生这个撕裂的过程中的炎症反应里扮演重要角色的就是这个论文的主角:TLR9


它是一个特定的免疫机制,正是它在这个过程里去修复DNA断裂,修补破裂的细胞并且应对炎症反应。



图源:工作细胞

图源:工作细胞


这也就是说,每一次记忆的形成,都是一个DNA断裂、细胞破损、然后TLR9处理炎症反应、最后身体恢复正常的一个过程。


研究人员还尝试了移除TLR9。于是发现在没有TLR9的情况下是无法形成长期记忆的,也就是说这个作用链条没有TLR9的参与就没法闭环。


听起来这有点像健身锻炼肌肉,肌肉的增长前提是轻微肌肉撕裂


但不同的是,对于锻炼肌肉而言,只有在训练和运动的时候才会出现这样的情况,并且是轻微肌肉撕裂。对于记忆过程中这都不是轻微撕裂了,这是双链DNA断链再重组。


锻炼=断链了属于是。


Viva La vida


我并不是一个专业的医学生,也不是生物专业的研究人员。我只是看到这篇Paper,突然对人类的生命再一次肃然起敬。


我们都觉得体育运动和锻炼带来的酸爽是一种正向反馈,因为这是你的身体在经受一次又一次的挑战与自我修复。


但是在我们看不见的地方,在你记住一些知识、一些任务和一些事情的时候,每一分每一秒身体里都有数以万计的细胞再断裂、产生炎症、再重组。


涅槃在时时刻刻都确实发生着。原来不思考的生命就没有意义,这不是一句鸡汤。


生命的自然机制本身就在挑战、摧毁并重塑自己。我的身体比我自己对自己可狠多了。


Viva la vida是一句西班牙语,意思是生命万岁/致敬生命。



图源:Google

图源:Google


这幅画的名字就叫做《Viva La Vida》,受这幅画和一些历史故事的启发,ColdPlay乐队创作了他们的经典专辑《Viva La Vida》.


所以现在我知道了,那些所谓刻在脑子里的事情,是真的literally的“”在了我的生命里。


作者:wayne3200
来源:mdnice.com/writing/84234c4f5f1c479a920b9103bf1f09f3
收起阅读 »

研究生真的会勾心斗角嘛?

背景 我感觉我是一个有点大大咧咧的人,有时候看不出来别人对我耍心眼,大学期间也有几次是事后才反应过来,而且我很容易对别人真心相待,我觉得我对他好他也会对我好,起码不会坏我,但是并不是这样,最近被好多研究生勾心斗角的言论吓到了,真的很怕 编者语 但什么东西...
继续阅读 »

背景


我感觉我是一个有点大大咧咧的人,有时候看不出来别人对我耍心眼,大学期间也有几次是事后才反应过来,而且我很容易对别人真心相待,我觉得我对他好他也会对我好,起码不会坏我,但是并不是这样,最近被好多研究生勾心斗角的言论吓到了,真的很怕


编者语


但什么东西有利益分配的时候,那么心眼就已经在耍起来了。


大家虽然,但也不会有啥坏心眼,不至于为了自己利益给其他人使绊子。


所以,放宽心,一切都是纸老虎。勇敢面对。


对了。多数人都很单纯,大家都是好朋友,以心换心,你是什么样的,你身边的人也会是什么样的。





把“嘛”字去掉





答案是肯定的,研究生真的会勾心斗角!


有人的地方就会有江湖,你的寝室就四个人还会有好几个小群,更何况一个有好多人的课题组呢?


举个例子:某校大牛课题组学生加职工共计100人,其中小导5个,博士10个,科研助理类工作人员3个,剩下的都是硕士和本科实习生。


大导分配任务给小导的时候会把同一个课题给到两个人,这不就出现矛盾了么?这两个人都需要成果,肯定谁做的快谁能留下,那他们带的研究生不就被迫跟着站队了么?难道你敢私下跟另一个导师的同学说你们组课题进展?或者说实验计划?


加上课题组内部的博士名额有限,想要读博的硕士本身就有很大的竞争关系,这不要靠谁的科研成果多,或者说谁能更得导师的心来决定了?


然后就是奖学金啊、课题组里面的资源啦、实验排期啦、仪器使用啦………能产生利益冲突的不要太多了好不好。


没有永远的朋友,只有永远的利益不仅仅适用于国家之间的关系,在职场或者读书阶段也是如此。


9个女生,1个男生


我们组研123年级一共9个女生,1个男生。1女博5。勾心斗角情况数不胜数。尤其是读到博士阶段且在校时间很长的尸姐(兄),及其擅长利用师弟师妹帮其做实验push 你控制欲极强,然后和老板聊天笑着就把你告了。说你经常找不到人做实验粗去玩。。。


大虎不怕,苍蝇难防


大部分人都是好的。但难免遇到一两个苍蝇。一个人能做到让组内的其他人都对他有意见有看法,是真的不容易。


大勇若怯,大智如愚。


会有的,而且同门有可能会成为打到你的主谋。我读研的时候,当年考研初试复试第一的女生就因为锋芒尽显,在上课做pre和各种提问中锋芒毕露,在组会上也比较积极,给人一种远超同门的气场。结果就被同门联合她的舍友诱导说了一些质疑导师的话,被录音给了她的导师,直接被逼退学,后面运作了好久才转组成功(也没有科研前途了,去给别人当耗材牛马去了,成果不给她的那种)。


大勇若怯,大智如愚。我反正就不会在没啥用的课堂演讲上锋芒毕露或者提问把同学问的下不来台。自己学点喜欢的东西,不争不抢,期末考试考好点,科研做好点,能在没人指导和导师起副作用情况下能写出大小论文通过答辩就行了。


研究生是否勾心斗角甚至是导学关系是否勾心斗角很大程度上取决于学院的整体生态和科研环境。如果你的学院和课题组大部分人都容易沟通、实事求是、科研风气积极向上,那必然不需要花很多时间处理所谓的人际关系。但如果你所在的学院和课题组学风不正,大部分事情讲的是丛林法则,凡事一看关系二看派系三看资源的。那大概率是要花很多时间在人际关系、派系斗争和做局坑人上。毕竟你勤勤恳恳做1000个小时得到的结果,可能别人花2小时编一个谣言或者给导师拍拍马屁就可以直接抢走,换谁会愿意真正的干活呢?


恶心


自己的idea被实验室同学拿去发了论文,顶会,作者没有我,甚至都没有收到一句感谢,并且所有场合提到这个idea的时候都变成了他的,老师不知道会当着我的面说那个同学idea怎么怎么好可以用在什么什么上拓展一下,我该如何自处?!实验室的同学还劝我大度,我大度你个鬼啊!不知道算不算恶心?


还有人明明知道某个代码怎么回事,还故意告诉你错的,实验室有锅往你身上甩,有功劳自己抢的可快了,肆无忌惮的的欺骗,偷抢功劳,算不算恶心?


本人一直秉着实验室都是好同学的思想,大家有求一定100%努力相助,从不欺骗谁,不抢别人功劳,觉得实验室应该互相帮助,一起科研进步,最后却还是遇到上述那些人,呵呵呵。


最难的地方是老板


有感而发。


说几句吧。


国外实验室呆过,感觉就是没有什么勾心斗角。


一个团队的人都很乐于互相帮助。


发文章一块发,有工作贡献就可以。当然主要作者都是博士生。


实验室的人际关系,很简单,很和谐。从无欺负人现象。


听说,国内什么都很卷。


就想说说,到底什么是卷。


有的人说,是蛋糕不够分。


有的人说,是美国卡脖子。


什么的。各种说法都有。


看过各位的回答,有点明白到底什么是内卷了。


就是一个实验室的人,没有爱心,也缺乏互帮互助的精神。


当然,很多精力就要花费在维护人际关系,维护实验室基本操作规范方面了。


这个比较普通与简单的方面,其实都没人重视,也不认真去履行。


实际上,是实验室人员缺乏职业道德,缺乏职业精神。


另外,感觉国内实验室人员缺乏主人翁意识,别人的事情不管,其实别人的事情很大程度也是与自己有关的。因为一个实验室嘛,互帮互助,这是人性。


还有,精力浪费在人际关系方面,就是没有很多精力用于科技研发,科研想法的创新了。


这是个人理解的所谓内卷。


我觉得不是硬件的问题,是软件的问题,就是人之根本的问题。


科学的发展,其实是人的发展。


科学之所以在西方,欧美发展的繁荣,也是因为这些地方,首先是人得到了发展。


人性得到了发展,人的本能得到了发挥与发扬。


人有哪些本能呢?


作为国外观察,我觉得人的本能有很多就是根本性的,却在国内一些实验室可能严重缺乏了。


比如,敬业,负责,有爱心,互帮互助,团队精神,自我批判与质疑,对待他人宽容与谅解,等等。


这些人的本能是维护一个实验室正常高效运转的基本要素,实验仪器其实反而是次要。


实验室里最宝贵的财富,是人。仪器什么的,都可以造,买,借。


只有人,如果本能受到了破坏,则就很难修复。想让一个不负责任各种撒谎的人突然就负责然后特别的敬业严谨,可能需要好几年的培养。而一个出了问题的仪器,花钱修理或者直接购买就行了。


可见,人的本能,难以重建与塑造。


那么发生在一个实验室里的各种内卷情况,则会层出不穷。


一个人的环境良好的实验室,哪怕实验仪器老旧,只需要多付出一点精心维护,实验室还是可以正常运作的。甚至可以发挥出更大的价值。


一个人的环境不好的实验室,哪怕是几百万几千万的大价钱购买的顶尖仪器,恐怕也难以得到足够的维护与修养,从而得不到一个实验仪器应有的功效。


那么如何解决实验室勾心斗角难题呢?


我觉得挺难的,不过还是可以尝试一些办法去解决。


第一,多去国外实验室打工,交流,学习。谦虚点,多观察,尤其多观察国外实验室他们如何管理的。人与人如何搞关系的。肯定和国内不一样。另外,不要觉得在国外,类似国内不好的事情也一样发生,就对国外实验室或科研产生看不起瞧不上的心态,这样只能自己心浮气躁,什么都学不到。最多混个履历或者文章。


第二,提高一下心性。其实冥想之类的挺管用。别人的批评听听,多吸取积极的批评,负面的批评就早点忘记,不要影响自身的积极性。


第三,多阅读一些有关,心灵,精神,方面的书籍。最好还是看国外翻译的。国内很多都是比较假的鸡汤。


第四,组织实验室共同学习,共同进步,多互相帮助,做事有规则有原则,就可以减少很多根本不必要的勾心斗角与内卷。


第五,以身作则,遇到原则问题需要自己去维护自己的利益,同时,要影响实验室其他成员给积极的影响。


当然,最难的部分实际上是实验室老板。这个,我们就无法解决了。如果老板愿意听你的,可以自己去与老板谈谈如何更好构建实验室。也是一个办法。


如果老板固执的不行,只能换地方了。


往大了说,这个内卷问题其实是一个文化问题。


这就太难解决了。谁也解决不了。


到了自己头上


本来以为自己不会有这样的问题的,一年以后发现实验室可真是卧虎藏龙小型帮派现场啊。


实验室大概有两个势力集团,一靠权力,二靠暴力,剩下我们普通人苦苦挣扎,现在新生还好真怕和他们的师兄师姐变得一样。作为有很多老师的大课题组,财产掌握在刚刚留校的实验室大师兄手中,暴力是指实验室有两位博士(男女朋友)逐渐将财产私有化专门化并欺压师弟师妹们。还有其它普通人分在别的老师名下我觉得我们同病相怜惨兮兮。还有师妹实验数据的文件夹被整个删了不知道是谁。


先说大师兄手下的特权,因为大师兄享有采购权所以在导师极度抠门的情况下,资源优先分配到人家那里。一个实验室手套都不能随时供应你敢信?三角瓶也不够要到处借,洗衣粉没了也不及时买(我去一楼借洗衣粉人家说只有洗衣液留下了羡慕的泪水。)试剂随时就没了,还有各种耗材没有是正常操作,(称量纸没了撕笔记本,洗手液没了就不用,甘油管没了找别的实验室借,枪头没了洗洗灭菌,橡皮筋没了把断了的打个结继续用,冰箱保菌装甘油管盒子没了拿硬纸盒装)


但是到了大师兄手下就不一样了,反正我一问就是没有了买了还没回来,但是人家就有一箱一箱的新三角瓶,随时可用的手套,有充足理由实验需要采购各种东西。还有年度大会两三千的奖励为实验室做贡献而其它师兄师姐比如负责每晚安全检查和新生培训的就没有。


那两口子不属于大师兄管但是是博士入学久,所以就自己给自己特权。两口子有自己专属的超净工作台,工作台要预订,我们也可以用但是只要人家要用没定也会在旁边赶你。PCr要预订,一个学弟延伸时间久,师姐就直接把他的PCr管扔了出去扔了出去还给负责学弟的师兄(有事不在学校)打电话骂了半个小时问反应体系延伸时间是放迟了还是超时了,此时还有半小时反应完成。他们组所有公用的东西自己都会私藏一份作为人家专用的东西,小到量筒,浮漂,放三角瓶的框,大到蛋白胶的电泳槽,写了他们名字的别人都不能用,用完放自己柜子里,→_→可是这些都是公用的。至于抢别人定的摇床也是正常操作,以至于他们带的本科生师弟也飞扬跋扈,在下午抢了一个博士师姐的电泳槽说他着急看结果,师姐生气说着急为什么不中午看才立马道歉。两口子的厚脸皮程度就连大师兄也要退让三分,更何况他们还会在大组会上怼大老板自己的导师你敢信?导师给提意见会打断说自己知道。


不过两口子的实验能力有目共睹,师姐洗瓶子要洗到没有肉眼可见的一点点污渍反复清洗十几遍不聚成水滴也不成股流下,这一点是我们做不到的。所以两口子实验效率特别高。


作者:时问桫椤
来源:mdnice.com/writing/d841b81020b94d57821fa484883a2e14
收起阅读 »

灰度发布策略在前端无损升级中的应用

web
为了提升浏览器加载页面资源的性能,对于js、css、图片等静态资源,web服务器往往会通过Cache-Control、ETag/If--Match、Last-Modified/If-Modified-Since、Pragma、Expires、Date、Age等...
继续阅读 »

为了提升浏览器加载页面资源的性能,对于js、css、图片等静态资源,web服务器往往会通过Cache-Control、ETag/If--Match、Last-Modified/If-Modified-Since、Pragma、Expires、Date、Age等头部来控制、管理、检测这类资源对缓存机制的使用情况。同时,为了使新版本的js、css等资源立即生效,一种比较通行的做法是为js、css这些文件名添加一个hash值。这样当js、css内容发生变化时,浏览器获取的是不同的js、css文件。在这种情况下,旧版本的index.html文件可能是这样的:


<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8>
<title>测试</title>
<meta http-equiv=X-UA-Compatible content="IE=Edge">
<meta name=viewport content="width=device-width,minimum-scale=1,maximum-scale=1">
<link href=/static/css/main.4656e35c1e8d4345f5bf.css rel=stylesheet>
</head>
<style>
html, body {
width: 100%;
}
</style>
<body>
<div id=newMain></div>
<script type=text/javascript src=/static/js/main-4656e35c1e8d4345f5bf.js></script>
</body>
</html>

当项目的js、css内容发生了变化时,新版本的index.html文件内容变成这样的(js和css文件名携带了新的hash值1711528240049):


<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8>
<title>测试</title>
<meta http-equiv=X-UA-Compatible content="IE=Edge">
<meta name=viewport content="width=device-width,minimum-scale=1,maximum-scale=1">
<link href=/static/css/main.1711528240049.css rel=stylesheet>
</head>
<style>
html, body {
width: 100%;
}
</style>
<body>
<div id=newMain></div>
<script type=text/javascript src=/static/js/main-1711528240049.js></script>
</body>
</html>

因为index.html文件一般会设置为不缓存,这样用户每次访问首页时,都会从web服务器重新获取index.html,然后根据index.html中的资源文件是否变化,从而决定是否使用缓存的文件。这样既能让用户立即获取最新的js、css等静态资源文件,又能充分地使用缓存。index.html的响应头大概长这样:


847fa51ec16bbfa14a5865482b23f396.png


但是为了保证系统的高可用,web后端往往由多个实例提供服务,用户请求会在多个服务实例间进行负载均衡。而系统升级过程中,会存在多个版本共存的现象。这时,如果用户从旧版本实例上获取了index.html文件,然后再去获取旧版本的js、css文件(main-4656e35c1e8d4345f5bf.jsmain.4656e35c1e8d4345f5bf.css),但是请求却分发到了新版本服务实例上,这时因为新版本服务实例只有main-1711528240049.jsmain.1711528240049.css文件,就会导致访问失败。反过来,如果从新版本实例上获取了index.html文件,在请求相应的js、css文件时,也可能被分发到旧版本实例上,也导致访问失败。


解决方法:


1)首先,改造一下index.html文件中引用js、css等静态资源的路径,添加一个版本号,如v1、v2,这样index.html文件对js、css的引用变为:


<link href=/static/v1/css/main.1711528240049.css rel=stylesheet>
<script type=text/javascript src=/static/v1/js/main-1711528240049.js></script>

2)使用灰度发布策略升级系统,具体步骤如下(假设系统包含A、B两个服务实例)



  1. 升级前(稳态),在应用网关(代理)上配置路由策略V1,该路由策略的功能为:匹配路径前缀/static/v1的请求负载均衡分发到A、B两个服务实例

  2. 将待升级的服务实例A从路由策略V1中摘除掉,这时用户请求只会发送给实例B

  3. 待实例A上所有进行中的请求都处理完后,就可以安全的停掉旧的服务,替换为新的服务,这时还不会有请求分发到实例A

  4. 待实例B测试功能正常后,在应用网关(代理)上新增一条路由策略V2,该路由策略的功能为:匹配路径前缀/static/v2的请求分发到服务实例A。这时,从服务实例A上获取的index.html文件引发的后续js、css请求,都会分发到服务实例A,从服务实例B上获取的index.html文件引发的后续js、css请求,都会分发到服务实例B

  5. 继续将实例B从路由策略V1中摘掉,然后升级实例B,将实例B添加到路由策略V2中

  6. 所有的流量都切换到了路由策略V2中,下线路由策略V1。完成整个升级过程,实现了前端的无损升级


作者:movee
来源:juejin.cn/post/7353069220827856946
收起阅读 »

Android使用Hilt依赖注入,让人看不懂你代码

前言 之前接手的一个项目里有些代码看得云里雾里的,找了半天没有找到对象创建的地方,后来才发现原来使用了Hilt进行了依赖注入。Hilt相比Dagger虽然已经比较简洁,但对初学者来说还是有些门槛,并且网上的许多文章都是搬自官网,入手容易深入难,如果你对Hilt...
继续阅读 »

前言


之前接手的一个项目里有些代码看得云里雾里的,找了半天没有找到对象创建的地方,后来才发现原来使用了Hilt进行了依赖注入。Hilt相比Dagger虽然已经比较简洁,但对初学者来说还是有些门槛,并且网上的许多文章都是搬自官网,入手容易深入难,如果你对Hilt不了解或是想了解得更多,那么接下来的内容将助力你玩转Hilt。


通过本篇文章,你将了解到:




  1. 什么是依赖注入?

  2. Hilt 的引入与基本使用

  3. Hilt 的进阶使用

  4. Hilt 原理简单分析

  5. Android到底该不该使用DI框架?



1. 什么是依赖注入?


什么是依赖?


以手机为例,要组装一台手机,我们需要哪些部件呢?

从宏观上分类:软件+硬件。

由此我们可以说:手机依赖了软件和硬件。

而反映到代码的世界:


class FishPhone(){
val software = Software()
val hardware = Hardware()
fun call() {
//打电话
software.handle()
hardware.handle()
}
}
//软件
class Software() {
fun handle(){}
}
//硬件
class Hardware() {
fun handle(){}
}

FishPhone 依赖了两个对象:分别是Software和Hardware。

Software和Hardware是FishPhone的依赖(项)。


什么是注入?


上面的Demo,FishPhone内部自主构造了依赖项的实例,考虑到依赖的变化挺大的,每次依赖项的改变都要改动到FishPhone,容易出错,也不是那么灵活,因此考虑从外部将依赖传进来,这种方式称之为:依赖注入(Dependency Injection 简称DI)

有几种方式:




  1. 构造函数传入

  2. SetXX函数传入

  3. 从其它对象间接获取



构造函数依赖注入:


class FishPhone(val software: Software, val hardware: Hardware){
fun call() {
//打电话
software.handle()
hardware.handle()
}
}

FishPhone的功能比较纯粹就是打电话功能,而依赖项都是外部传入提升了灵活性。


为什么需要依赖注入框架?


手机制造出来后交给客户使用。


class Customer() {
fun usePhone() {
val software = Software()
val hardware = Hardware()
FishPhone(software, hardware).call()
}
}

用户想使用手机打电话,还得自己创建软件和硬件,这个手机还能卖出去吗?

而不想创建软件和硬件那得让FishPhone自己负责去创建,那不是又回到上面的场景了吗?


你可能会说:FishPhone内部就依赖了两个对象而已,自己负责创建又怎么了?


解耦


再看看如下Demo:


interface ISoftware {
fun handle()
}

//硬件
interface IHardware {
fun handle()
}

//软件
class SoftwareImpl() : ISoftware {
override fun handle() {}
}

//硬件
class HardwareImpl : IHardware {
override fun handle() {}
}

class FishPhone() {
val software: ISoftware = SoftwareImpl()
val hardware: IHardware = HardwareImpl()
fun call() {
//打电话
software.handle()
hardware.handle()
}
}

FishPhone 只关注软件和硬件的接口,至于具体怎么实现它不关心,这就达到了解耦的目的。
既然要解耦,那么SoftwareImpl()、HardwareImpl()就不能出现在FishPhone里。

应该改为如下形式:


class FishPhone(val software: ISoftware, val hardware: IHardware) {
fun call() {
//打电话
software.handle()
hardware.handle()
}
}

消除模板代码


即使我们不考虑解耦,假若HardwareImpl里又依赖了cpu、gpu、disk等模块:


//硬件
class HardwareImpl : IHardware {
val cpu = CPU(Regisgter(), Cal(), Bus())
val gpu = GPU(Image(), Video())
val disk = Disk(Block(), Flash())
//...其它模块
override fun handle() {}
}

现在仅仅只是三个模块,若是依赖更多的模块或者模块的本身也需要依赖其它子模块,比如CPU需要依赖寄存器、运算单元等等,那么我们就需要写更多的模板代码,要是我们只需要声明一下想要使用的对象而不用管它的创建就好了。


class HardwareImpl(val cpu: CPU, val gpu: GPU, val disk: Disk) : IHardware {
override fun handle() {}
}

可以看出,下面的代码比上面的简洁多了。




  1. 从解耦和消除模板代码的角度看,我们迫切需要一个能够自动创建依赖对象并且将依赖注入到目标代码的框架,这就是依赖注入框架

  2. 依赖注入框架能够管理依赖对象的创建,依赖对象的注入,依赖对象的生命周期

  3. 使用者仅仅只需要表明自己需要什么类型的对象,剩下的无需关心,都由框架自动完成



先想想若是我们想要实现这样的框架需要怎么做呢?

相信很多小伙伴最朴素的想法就是:使用工厂模式,你传参告诉我想要什么对象我给你构造出来。

这个想法是半自动注入,因为我们还要调用工厂方法去获取,而全自动的注入通常来说是使用注解标注实现的。


2. Hilt 的引入与基本使用


Hilt的引入


从Dagger到Dagger2再到Hilt(Android专用),配置越来越简单也比较容易上手。

前面说了依赖注入框架的必要性,我们就想迫不及待的上手,但难度可想而知,还好大神们早就造好了轮子。

以AGP 7.0 以上为例,来看看Hilt框架是如何引入的。


一:project级别的build.gradle 引入如下代码:


plugins {
//指定插件地址和版本
id 'com.google.dagger.hilt.android' version '2.48.1' apply false
}

二:module级别的build.gradle引入如下代码:


plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
//使用插件
id 'com.google.dagger.hilt.android'
//kapt生成代码
id 'kotlin-kapt'
}
//引入库
implementation 'com.google.dagger:hilt-android:2.48.1'
kapt 'com.google.dagger:hilt-compiler:2.48.1'

实时更新最新版本以及AGP7.0以下的引用请参考:Hilt最新版本配置


Hilt的简单使用


前置步骤整好了接下来看看如何使用。


一:表明该App可以使用Hilt来进行依赖注入,添加如下代码:


@HiltAndroidApp
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
}
}

@HiltAndroidApp 添加到App的入口,即表示依赖注入的环境已经搭建好。


二:注入一个对象到MyApp里:

有个类定义如下:


class Software {
val name = "fish"
}

我们不想显示的构造它,想借助Hilt注入它,那得先告诉Hilt这个类你帮我注入一下,改为如下代码:


class Software @Inject constructor() {
val name = "fish"
}

在构造函数前添加了@Inject注解,表示该类可以被注入。

而在MyApp里使用Software对象:


@HiltAndroidApp
class MyApp : Application() {
@Inject
lateinit var software: Software

override fun onCreate() {
super.onCreate()
println("inject result:${software.name}")
}
}

对引用的对象使用@Inject注解,表示期望Hilt帮我将这个对象new出来。

最后查看打印输出正确,说明Software对象被创建了。


这是最简单的Hilt应用,可以看出:




  1. 我们并没有显式地创建Software对象,而Hilt在适当的时候就帮我们创建好了

  2. @HiltAndroidApp 只用于修饰Application



如何注入接口?


一:错误示范
上面提到过,使用DI的好处之一就是解耦,而我们上面注入的是类,现在我们将Software抽象为接口,很容易就会想到如下写法:


interface ISoftware {
fun printName()
}

class SoftwareImpl @Inject constructor(): ISoftware{
override fun printName() {
println("name is fish")
}
}

@HiltAndroidApp
class MyApp : Application() {
@Inject
lateinit var software: ISoftware

override fun onCreate() {
super.onCreate()
println("inject result:${software.printName()}")
}
}

不幸的是上述代码编译失败,Hilt提示说不能对接口使用注解,因为我们并没有告诉Hilt是谁实现了ISoftware,而接口本身不能直接实例化,因此我们需要为它指定具体的实现类。


二:正确示范

再定义一个类如下:


@Module
@InstallIn(SingletonComponent::class)
abstract class SoftwareModule {
@Binds
abstract fun bindSoftware(impl: SoftwareImpl):ISoftware
}



  1. @Module 表示该类是一个Hilt的Module,固定写法

  2. @InstallIn 表示模块在哪个组件生命周期内生效,SingletonComponent::class指的是全局

  3. 一个抽象类,类名随意

  4. 抽象方法,方法名随意,返回值是需要被注入的对象类型(接口),而参数是该接口的实现类,使用@Binds注解标记,



如此一来我们就告诉了Hilt,SoftwareImpl是ISoftware的实现类,于是Hilt注入ISoftware对象的时候就知道使用SoftwareImpl进行实例化。
其它不变运行一下:
image.png


可以看出,实际注入的是SoftwareImpl。



@Binds 适用在我们能够修改类的构造函数的场景



如何注入第三方类


上面的SoftwareImpl是我们可以修改的,因为使用了@Inject修饰其构造函数,所以可以在其它地方注入它。

在一些时候我们不想使用@Inject修饰或者说这个类我们不能修改,那该如何注入它们呢?


一:定义Provides模块


@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {
@Provides
fun provideHardware():Hardware {
return Hardware()
}
}



  1. @Module和@InstallIn 注解是必须的

  2. 定义object类

  3. 定义函数,方法名随意,返回类型为我们需要注入的类型

  4. 函数体里通过构造或是其它方式创建具体实例

  5. 使用@Provides注解函数



二:依赖使用

而Hardware定义如下:


class Hardware {
fun printName() {
println("I'm fish")
}
}

在MyApp里引用Hardware:

在这里插入图片描述


虽然Hardware构造函数没有使用@Inject注解,但是我们依然能够使用依赖注入。


当然我们也可以注入接口:


interface IHardware {
fun printName()
}

class HardwareImpl : IHardware {
override fun printName() {
println("name is fish")
}
}

想要注入IHardware接口,需要定义provides模块:


@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {
@Provides
fun provideHardware():IHardware {
return HardwareImpl()
}
}


@Provides适用于无法修改类的构造函数的场景,多用于注入第三方的对象



3. Hilt 的进阶使用


限定符


上述 ISoftware的实现类只有一个,假设现在有两个实现类呢?

比如说这些软件可以是美国提供,也可以是中国提供的,依据上面的经验我们很容易写出如下代码:


class SoftwareChina @Inject constructor() : ISoftware {
override fun printName() {
println("from china")
}
}

class SoftwareUS @Inject constructor() : ISoftware {
override fun printName() {
println("from US")
}
}

@Module
@InstallIn(SingletonComponent::class)
abstract class SoftwareModule {
@Binds
abstract fun bindSoftwareCh(impl: SoftwareChina):ISoftware

@Binds
abstract fun bindSoftwareUs(impl: SoftwareUS):ISoftware
}

//依赖注入:
@Inject
lateinit var software: ISoftware

兴高采烈的进行编译,然而却报错:
image.png


也就是说Hilt想要注入ISoftware,但不知道选择哪个实现类,SoftwareChina还是SoftwareUS?没人告诉它,所以它迷茫了,索性都绑定了。


这个时候我们需要借助注解:@Qualifier 限定符注解来对实现类进行限制。

改造一下:


@Module
@InstallIn(SingletonComponent::class)
abstract class SoftwareModule {
@Binds
@China
abstract fun bindSoftwareCh(impl: SoftwareChina):ISoftware

@Binds
@US
abstract fun bindSoftwareUs(impl: SoftwareUS):ISoftware
}

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class US

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class China

定义新的注解类,使用@Qualifier修饰。

而后在Module里,分别使用注解类修饰返回的函数,如bindSoftwareCh函数指定返回SoftwareChina来实现ISoftware接口。


最后在引用依赖注入的地方分别使用@China @US修饰。


    @Inject
@US
lateinit var software1: ISoftware

@Inject
@China
lateinit var software2: ISoftware

此时,虽然software1、software2都是ISoftware类型,但是由于我们指定了限定符@US、@China,因此最后真正的实现类分别是SoftwareChina、SoftwareUS。



@Qualifier 主要用在接口有多个实现类(抽象类有多个子类)的注入场景



预定义限定符


上面提及的限定符我们还可以扩展其使用方式。

你可能发现了,上述提及的可注入的类构造函数都是无参的,很多时候我们的构造函数是需要有参数的,比如:


class Software @Inject constructor(val context: Context) {
val name = "fish"
fun getWindowService(): WindowManager?{
return context.getSystemService(Context.WINDOW_SERVICE) as? WindowManager
}
}
//注入
@Inject
lateinit var software: Software

这个时候编译会报错:

image.png
意思是Software依赖的Context没有进行注入,因此我们需要给它注入一个Context。


由上面的分析可知,Context类不是我们可以修改的,只能通过@Provides方式提供其注入实例,并且Context有很多子类,我们需要使用@Qualifier指定具体实现类,因此很容易我们就想到如下对策。

先定义Module:


@Module
@InstallIn(SingletonComponent::class)
object MyContextModule {
@Provides
@GlobalContext
fun provideContext(): Context? {
return MyApp.myapp
}
}

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class GlobalContext

再注入Context:


class Software @Inject constructor(@GlobalContext val context: Context?) {
val name = "fish"
fun getWindowService(): WindowManager?{
return context?.getSystemService(Context.WINDOW_SERVICE) as? WindowManager
}
}

可以看出,借助@Provides和@Qualifier,可以实现全局的Context。

当然了,实际上我们无需如此麻烦,因为这部分工作Hilt已经预先帮我们弄了。

与我们提供的限定符注解GlobalContext类似,Hilt预先提供了:


@Qualifier
@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
public @interface ApplicationContext {}

因此我们只需要在需要的地方引用它即可:


class Software @Inject constructor(@ApplicationContext val context: Context?) {
val name = "fish"
fun getWindowService(): WindowManager?{
return context?.getSystemService(Context.WINDOW_SERVICE) as? WindowManager
}
}

如此一来我们无需重新定义Module。




  1. 除了提供Application级别的上下文:@ApplicationContext,Hilt还提供了Activity级别的上下文:@ActivityContext,因为是Hilt内置的限定符,因此称为预定义限定符。

  2. 如果想自己提供限定符,可以参照GlobalContext的做法。



组件作用域和生命周期


Hilt支持的注入点(类)


以上的demo都是在MyApp里进行依赖,MyApp里使用了注解:@HiltAndroidApp 修饰,表示当前App支持Hilt依赖,Application就是它支持的一个注入点,现在想要在Activity里使用Hilt呢?


@AndroidEntryPoint
class SecondActivity : AppCompatActivity() {

除了Application和Activity,Hilt内置支持的注入点如下:
image.png


除了Application和ViewModel,其它注入点都是通过使用@AndroidEntryPoint修饰。



注入点其实就是依赖注入开始的点,比如Activity里需要注入A依赖,A里又需要注入B依赖,B里又需要注入C依赖,从Activity开始我们就能构建所有的依赖



Hilt组件的生命周期


什么是组件?在Dagger时代我们需要自己写组件,而在Hilt里组件都是自动生成的,无需我们干预。
依赖注入的本质实际上就是在某个地方悄咪咪地创建对象,这个地方的就是组件,Hilt专为Android打造,因此势必适配了Android的特性,比如生命周期这个Android里的重中之重。

因此Hilt的组件有两个主要功能:




  1. 创建、注入依赖的对象

  2. 管理对象的生命周期



Hilt组件如下:
image.png


可以看出,这些组件的创建和销毁深度绑定了Android常见的生命周期。

你可能会说:上面貌似没用到组件相关的东西,看了这么久也没看懂啊。

继续看个例子:


@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {
@Provides
fun provideHardware():IHardware {
return HardwareImpl()
}
}

@InstallIn(SingletonComponent::class) 表示把模块安装到SingletonComponent组件里, SingletonComponent组件顾名思义是全局的,对应的是Application级别。因此安装的这个模块可在整个App里使用。


问题来了:SingletonComponent是不是表示@Provides修饰的函数返回的实例是同一个?

答案是否定的。


这就涉及到组件的作用域。


组件的作用域


想要上一小结的代码提供全局唯一实例,则可用组件作用域注解修饰函数:


@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {
@Provides
@Singleton
fun provideHardware():IHardware {
return HardwareImpl()
}
}

当我们在任何地方注入IHardware时,获取到的都是同一个实例。

除了@Singleton表示组件的作用域,还有其它对应组件的作用域:

image.png


简单解释作用域:

@Singleton 被它修饰的构造函数或是函数,返回的始终是同一个实例

@ActivityRetainedScoped 被它修饰的构造函数或是函数,在Activity的重建前后返回同一实例

@ActivityScoped 被它修饰的构造函数或是函数,在同一个Activity对象里,返回的都是同一实例

@ViewModelScoped 被它修饰的构造函数或是函数,与ViewModel规则一致




  1. Hilt默认不绑定任何作用域,由此带来的结果是每一次注入都是全新的对象

  2. 组件的作用域要么不指定,要指定那必须和组件的生命周期一致



以下几种写法都不符合第二种限制:


@Module
@InstallIn(SingletonComponent::class)
object HardwareModule {
@Provides
@ActivityScoped//错误,和组件的作用域不一致
fun provideHardware():IHardware {
return HardwareImpl()
}
}

@Module
@InstallIn(ActivityComponent::class)
object HardwareModule {
@Provides
@Singleton//错误,和组件的作用域不一致
fun provideHardware():IHardware {
return HardwareImpl()
}
}

@Module
@InstallIn(ActivityRetainedComponent::class)
object HardwareModule {
@Provides
@ActivityScoped//错误,和组件的作用域不一致
fun provideHardware():IHardware {
return HardwareImpl()
}
}

除了修饰Module,作用域还可以用于修饰构造函数:


@ActivityScoped
class Hardware @Inject constructor(){
fun printName() {
println("I'm fish")
}
}

@ActivityScoped表示不管注入几个Hardware,在同一个Activity里注入的实例都是一致的。


构造函数里无法注入的字段


一个类的构造函数如果被@Inject注入,那么构造函数的其它参数都需要支持注入。


class Hardware @Inject constructor(val context: Context) {
fun printName() {
println("I'm fish")
}
}

以上代码是无法编译通过的,因为Context不支持注入,而通过上面的分析可知,我们可以使用限定符:


class Hardware @Inject constructor(@ApplicationContext val context: Context) {
fun printName() {
println("I'm fish")
}
}

这就可以成功注入了。


再看看此种场景:


class Hardware @Inject constructor(
@ApplicationContext val context: Context,
val version: String,
) {
fun printName() {
println("I'm fish")
}
}

很显然String不支持注入,当然我们可以向@ApplicationContext 一样也给String提供一个@Provides和@Qualifier注解,但可想而知很麻烦,关键是String是动态变化的,我们确实需要Hardware构造的时候传入合适的String。


由此引入新的写法:辅助注入


class Hardware @AssistedInject constructor(
@ApplicationContext val context: Context,
@Assisted
val version: String,
) {

//辅助工厂类
@AssistedFactory
interface Factory{
//不支持注入的参数都可以放这,返回值为待注入的类型
fun create(version: String):Hardware
}

fun printName() {
println("I'm fish")
}
}

在引用注入的地方不能直接使用Hardware,而是需要通过辅助工厂进行创建:


@AndroidEntryPoint
class SecondActivity : AppCompatActivity() {
private lateinit var binding: ActivitySecondBinding
@Inject
lateinit var hardwareFactory : Hardware.Factory

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySecondBinding.inflate(layoutInflater)
setContentView(binding.root)

val hardware = hardwareFactory.create("3.3.2")
println("${hardware.printName()}")
}
}

如此一来,通过辅助注入,我们还是可以使用Hilt,值得一提的是辅助注入不是Hilt独有,而是从Dagger继承来的功能。


自定义注入点


Hilt仅仅内置了常用的注入点:Application、Activity、Fragment、ViewModel等。

思考一种场景:小明同学写的模块都是需要注入:


class Hardware @Inject constructor(
val gpu: GPU,
val cpu: CPU,
) {
fun printName() {
println("I'm fish")
}
}

class GPU @Inject constructor(val videoStorage: VideoStorage){}

//显存
class VideoStorage @Inject constructor() {}

class CPU @Inject constructor(val register: Register) {}

//寄存器
class Register @Inject() constructor() {}

此时小刚需要引用Hardware,他有两种选择:




  1. 使用注入方式很容易就引用了Hardware,可惜的是他没有注入点,仅仅只是工具类。

  2. 不选注入方式,则需要构造Hardware实例,而Hardware依赖GPU和CPU,它们又分别依赖VideoStorage和Register,想要成功构造Hardware实例需要将其它的依赖实例都手动构造出来,可想而知很麻烦。



这个时候适合小刚的方案是:



自定义注入点



方案实施步骤:

一:定义入口点


@InstallIn(SingletonComponent::class)
interface HardwarePoint {
//该注入点负责返回Hardware实例
fun getHardware(): Hardware
}

二:通过入口点获取实例


class XiaoGangPhone {
fun getHardware(context: Context):Hardware {
val entryPoint = EntryPointAccessors.fromApplication(context, HardwarePoint::class.java)
return entryPoint.getHardware()
}
}

三:使用Hardware


        val hardware = XiaoGangPhone().getHardware(this)
println("${hardware.printName()}")

注入object类


定义了object类,但在注入的时候也需要,可以做如下处理:


object MySystem {
fun getSelf():MySystem {
return this
}
fun printName() {
println("I'm fish")
}
}

@Module
@InstallIn(SingletonComponent::class)
object MiddleModule {
@Provides
@Singleton
fun provideSystem():MySystem {
return MySystem.getSelf()
}
}
//使用注入
class Middleware @Inject constructor(
val mySystem:MySystem
) {
}

4. Hilt 原理简单分析


@AndroidEntryPoint
class SecondActivity : AppCompatActivity() {}

Hilt通过apt在编译时期生成代码:


public abstract class Hilt_SecondActivity extends AppCompatActivity implements GeneratedComponentManagerHolder {

private boolean injected = false;

Hilt_SecondActivity() {
super();
//初始化注入监听
_initHiltInternal();
}

Hilt_SecondActivity(int contentLayoutId) {
super(contentLayoutId);
_initHiltInternal();
}

private void _initHiltInternal() {
addOnContextAvailableListener(new OnContextAvailableListener() {
@Override
public void onContextAvailable(Context context) {
//真正注入
inject();
}
});
}

protected void inject() {
if (!injected) {
injected = true;
//通过manager获取组件,再通过组件注入
((SecondActivity_GeneratedInjector) this.generatedComponent()).injectSecondActivity(UnsafeCasts.<SecondActivity>unsafeCast(this));
}
}
}

在编译期,SecondActivity的父类由AppCompatActivity变为Hilt_SecondActivity,因此当SecondActivity构造时就会调用父类的构造器监听create()的回调,回调调用时进行注入。



由此可见,Activity.onCreate()执行后,Hilt依赖注入的字段才会有值



真正注入的过程涉及到不少的类,都是自动生成的类,有兴趣可以对着源码查找流程,此处就不展开说了。


5. Android到底该不该使用DI框架?


有人说DI比较复杂,还不如我直接构造呢?

又有人说那是你项目不复杂,用不到,在后端流行的Spring全家桶,依赖注入大行其道,Android复杂的项目也需要DI来解耦。


从个人的实践经验看,Android MVVM/MVI 模式还是比较适合引入Hilt的。
image.png


摘抄官网的:现代Android 应用架构

通常来说我们这么设计UI层到数据层的架构:


class MyViewModel @Inject constructor(
val repository: LoginRepository
) :ViewModel() {}

class LoginRepository @Inject constructor(
val rds : RemoteDataSource,
val lds : LocalDataSource
) {}

//远程来源
class RemoteDataSource @Inject constructor(
val myRetrofit: MyRetrofit
) {}

class MyRetrofit @Inject constructor(
) {}

//本地来源
class LocalDataSource @Inject constructor(
val myDataStore: MyDataStore
) {}

class MyDataStore @Inject constructor() {}

可以看出,层次比较深,使用了Hilt简洁了许多。


本文基于 Hilt 2.48.1

参考文档:

dagger.dev/hilt/gradle…

developer.android.com/topic/archi…

repo.maven.apache.org/maven2/com/…


作者:小鱼人爱编程
来源:juejin.cn/post/7294965012749320218
收起阅读 »

特效炸裂:小米 SU7 在线特效网站技术不完全揭秘!!!

web
哈喽,大家好 我是 xy👨🏻‍💻。用 Three.js 实现 小米 SU7 在线体验,特效相当炸裂!!! 前言 最近一位叫 @GameMCU的大佬用 Webgl、Three.js 等技术实现了一个 小米 SU7 在线体验网站:https://gamemcu....
继续阅读 »

哈喽,大家好 我是 xy👨🏻‍💻。用 Three.js 实现 小米 SU7 在线体验,特效相当炸裂!!!



前言


最近一位叫 @GameMCU的大佬用 WebglThree.js 等技术实现了一个 小米 SU7 在线体验网站:https://gamemcu.com/su7/被广大网友疯传,效果相当炸裂!


网站首发当天由于访问量过大导致奔溃, 后来可能获得了某里云官方支持!!! 这一波真的要给某里云点赞!



更有网友评论: 这效果和交互完全可以吊打官方和各种卖车的网站了啊



并且 @小米汽车官方:求求了,收编了吧,这能极大提升小米su7的逼格,再用到公司其他产品,能提升整体公司的逼格



废话不多说,直接上效果!!!


效果展示



  • 模拟在汽车在道路行驶特效,宛如身临其境




  • 流线型车身设计,彰显速度与激情的完美融合。每一处细节都经过精心打磨,只为给你带来最纯粹的驾驶体验。




  • 在高速行驶的过程中,风阻是影响车速的重要因素。我们的特效模拟器通过先进的算法,真实还原了风阻对车辆的影响。当你长按鼠标,感受那股扑面而来的气流,仿佛置身于真实的驾驶环境中。




  • 雷达实时探测功能可以帮你轻松掌握周围车辆的情况,让你在驾驶过程中更加安心



视频


是怎么实现的


在线体验完@GameMCU大佬的网站之后, 我很好奇大佬是使用什么技术去实现的, 身为前端开发的我, 第一步当然是 F12 打开控制台查看



发现使用的是 Three.js r150 版本开发, 并且还用了一个叫 xviewer.js 的插件,


于是乎我找到了@GameMCU大佬的 github 主页, 在主页中介绍了 xviewer.js:



xviewer.js是一个基于 three.js 的插件式渲染框架,它对 three.js 做了一层简洁优雅的封装,包含了大量实用的组件和插件,目标是让前端开发者能更简单地应用webgl技术。



比较遗憾的是 xviewer.js 目前还没有开源, 不过按照作者的意思是可能会在近期开源。


虽然目前 小米 SU7 在线体验网站没有开源, 但是作者主页开源了另外一个项目: three.js复刻原神启动, 也是一个基于 xviewer.js 开发的在线网站。


通过源码发现作者在项目中写了大量的 Shader, Shader 对于实现复杂的视觉效果和图形渲染技术至关重要,它们使得开发者能够创建出令人印象深刻的3D场景动画



Shader 是一种在计算机图形学中使用的程序,它运行在图形处理单元(GPU)上,用于处理渲染过程中的光照、颜色、纹理等视觉效果。


Shader 通常被用于 3D 图形渲染中,以增强视觉效果,使得图像更加逼真和吸引人。


在 Three.js 中, Shader 通常分为两类:顶点着色器(Vertex Shader)和片元着色器(Fragment Shader)。


顶点着色器负责处理顶点数据,如位置颜色纹理坐标等,而片元着色器则负责处理像素颜色,包括光照材质属性


总之,Shader 在 Three.js 中扮演着至关重要的角色,它们为开发者提供了强大的工具来创建丰富、动态和引人入胜的 3D 图形内容。通过学习和掌握 Shader 编程,开发者可以极大地扩展 Three.js 的应用范围和创作能力。


那么作为一名前端开发人员, 应该怎么快速入门 Shader, 并且用 Shader 创造令人惊叹的交互体验呢???


三个学习 Shader 网站推荐


1. The Book of Shaders



网址: https://thebookofshaders.com/?lan=ch


The Book of Shaders 是一个在线学习 Shader 的网站(电子书),它提供了一系列关于 Shader 的基础教程和示例代码,堪称入门级指南


2.Shadertoy



网址:https://www.shadertoy.com/


Shadertoy 是一个基于 WebGL 的在线实时渲染平台,主要用于编辑分享查看 shader 程序及其实现的效果。


在这个平台上,用户可以创作和分享自己的 3D 图形效果。它提供了一个简单方便的环境,让用户可以轻松编辑自己的片段着色器,并实时查看修改的效果。


同时,Shadertoy 上有许多大佬分享他们制作的酷炫效果的代码,这些代码是完全开源的,用户可以在这些代码的基础上进行修改和学习。


除此之外,Shadertoy 还允许用户选择声音频道,将当前帧的声音信息转变成纹理(Texture),传入 shader 当中,从而根据声音信息来控制图形。这使得 Shadertoy 在视觉和听觉的结合上有了更多的可能性。


3.glsl.app



网址:https://glsl.app/


glsl.app 是一个在线的 GLSL (OpenGL Shading Language) 编辑器。GLSL 是一种用于图形渲染的着色语言,特别是在 OpenGL 图形库中。这种语言允许开发者为图形硬件编写着色器程序,这些程序可以运行在 GPU 上,用于计算图像的各种视觉效果。


在 glsl.app 上,你可以:



  • 编写和编辑着色器代码:直接在网页上编写顶点着色器、片元着色器等。

  • 实时预览:当你编写或修改着色器代码时,可以立即在右侧的预览窗口中看到效果。

  • 分享你的作品:完成你的着色器后,你可以获得一个链接,通过这个链接与其他人分享你的作品。

  • 学习:如果你是初学者,该网站还提供了很多示例和教程,帮助你了解如何编写各种着色器效果。


参考连接:



作者:前端开发爱好者
来源:juejin.cn/post/7352797634556706831
收起阅读 »

高并发下单加锁吗?

一个简单的下单流程包括,商品校验,订单计价,扣库存,保存订单。其中扣库存的并发问题是整个流程中最麻烦,最复杂的环节,可以说聚集了所有的智慧和头发。 解决扣库存并发问题,很容易让人想到加锁,加锁的目的是为了限制同步代码块并发,进一步的保证原子性,可见性和重排序...
继续阅读 »

一个简单的下单流程包括,商品校验,订单计价,扣库存,保存订单。其中扣库存的并发问题是整个流程中最麻烦,最复杂的环节,可以说聚集了所有的智慧和头发。
image.png


解决扣库存并发问题,很容易让人想到加锁,加锁的目的是为了限制同步代码块并发,进一步的保证原子性,可见性和重排序,实现数据一致性。


单机加 jvm 锁,分布式加分布式锁。这让我不禁想起分布式系统一句黑话,分布式系统中,没有什么问题是不能通过增加中间环节解决的,但解决一个问题常常会带来另外的问题,是的,你没听错,以空间换时间,以并发换数据一致性,在这里,锁粒度和范围对并发影响是最直接的,设计的时候尽可能的缩小锁粒度和范围,一般粒度是 skuId,范围尽量减小。


锁时长,锁过期是另外两个不得不考虑的问题。最麻烦的锁过期,常用解决方案是依赖 redission 的看门狗机制,相当于定时任务给锁续命,但粗暴续命会增加 rt,同时增加其他请求的阻塞时长。


尽量避免牺牲并发的方案!

尽量避免牺牲并发的方案!

尽量避免牺牲并发的方案!


一次偶然的机会,我的同事向我推荐了 Google 的 chubby。为什么我们不能用悲观锁+乐观锁的组合呢?在锁过期的时候,乐观锁兜底,不影响请求 rt,也能保证数据一致性。这是个不错的方案,适合简单的场景!


一次偶然的机会,一条公式冲击我的大脑,redis = 高性能 + 原子性。机智的你肯定知道加锁就是为了保证原子性,基于 redis 实现分布式锁也是因为 redis 的原子性和高性能(想想什么情况用 mysql 和 zk),如果我用 redis 代替锁,是不是既能保证扣库存的原子性,同时因为没有锁,又不需要考虑加锁带来的问题。


说干就干,马上画个图。(图片被掘金压缩,有点糊,我上传到图床,点击能跳转到图床看清晰的,如果看不清楚图片,联系我,给我留言
pCGSEE6.png
我把订单流程分为5大块,有点复杂,且听我细细道来。


Order process:



扣库存是限制订单并发的瓶颈,依靠 redis 的原子性保证数据一致性,高性能提升并发



2pc


基于二阶段提交思想,第一步首先插入 INIT 状态的订单


冷热路由


第二步有个路由,冷门商品走 mysql 下单,热门商品并发大,依靠 redis 撑。


如何知道商品冷热,答案是 bitMap,所以我们还需要一个定时任务(job4)维护 bitMap。冷热数据的统计来源一般是购物车,埋点统计。大电商平台来源更丰富,大数据追踪,算法推荐等。


故障处理


lua 扣减库存时,需要考虑请求超时和 redis 宕机。请求超时比较好解决,可以 catch 超时异常,依据业务选择重试或返回。redis 宕机比较棘手,后面分析。


降级

这里说一下降级。redis 宕机之后,走冷门订单流程。但是这里的设计会变的很复杂,因为需要解决两个问题,如何断定 redis 宕机,走冷门路由会不会把 mysql 压垮?这两个问题继续谈论下去会衍生出更多,比如走冷门路由的时机,冷门路由会不会把 mysql 压垮等,所以这里触发熔断还需要马上开启限流,展开真的很复杂,下次有机会说。


扣库存后续动作突然变得顺畅,插入订单库存流水,修改订单状态 UNPAY,发送核销优惠券 mq,日志记录等。这几个步骤中,



  • 流水用于记录订单和库存的绑定,重建商品库存缓存会用到

  • 核销优惠券选择异步,发核销优惠券的 mq,需要考虑消息丢失和重复消费,上游订单服务记录本地消息表,同时有个定时任务(job1)扫描重发,下游做好幂等

  • 我们还需要关注该流程可能会出现 jvm 宕机,这是很严重的事故,按理说没有顺利走完订单流程的订单属于异常订单,异常订单的库存需要返还 redis,所以还需要一个定时任务处理异常订单。


JOB2



redis 没有库存流水,被扣库存 x 无法得知



订单流程有几处宕机需要考虑,一处是执行 lua 脚本时 redis 宕机,另一处是扣完库存之后,jvm 宕机。无论是 redis 还是 jvm 宕机,这些订单都会返回异常信息到前端,所以这些订单的是无效的,需要还库存到 redis。


mysql 和 redis 的流水描述同一件事情,即记录该笔订单所扣库存。在异常情况下,可能只有 redis 有流水,依然可以作为断定库存已经扣减的依据,在极端异常的情况,lua 脚本刚扣完库存,redis 进程死了或者宕机,虽然 lua 是原子性的,但宕机可不是原子性,库存 x 已经扣了,没有流水记录,无法知道 x (redis 的单点问题可以通过 redis ha 保证)。


如果 redis 恢复了,但数据没了,怎么办?

如果 redis 恢复了,但数据丢失了(库存变化还没持久化就宕机,redis 重启恢复的是旧数据),怎么办?


Rebuild stock cache of sku



剩余库存 = (库存服务的总库存减去预占库存) - (mysql 和 redis 流水去重,计算的库存)



把目光锁定到右下角,重建 sku 库存缓存的逻辑。一般地,在 redis 扣完库存,会发个 mq 消息到库存服务,持久化该库存变动。库存服务采用 a/b 库存的设计,分别记录商品总库存和预占库存,为的是解决高并发场景业务操作库存和用户下单操作库存时的锁冲突问题。库存服务里的库存是延迟的,订单服务没发的消息和库存服务没消费的消息造成延迟。


我们既然把库存缓存到 redis,不妨想一下如何准确计算库存的数量。



  • 在刚开始启动服务的时候,redis 没有数据,这时候库存 t = a - b(a/b库存)

  • 服务运行一段时间,redis 有库存 t, 此时 t = a - b - (库存服务还没消费的扣库存消息),所以拿 mysql 和 redis 的流水去重,计算出已扣未消费库存。redis 宕机后,会有一个未知已扣库存 x, x 几乎没有算出来的可能(鄙人尽力了),也没必要算出来,你想,当你 redis 异常了,库存 x 对应的订单是异常订单,异常订单不会返回给用户,用户只会收到下单异常的返回,所以库存 x 是无效的,丢掉就好。


Payment process


用户支付之后,才发扣库存消息到库存服务落地。落地库存服务的流程很简单,不再阐述。重点说说新增库存和减少库存。新增库存不会造成超卖,简单粗暴的加就好。减少库存相当于下单,需要小心超卖问题,所以现在 redis 扣了库存,再执行本地事务,简简单单,凄凄惨惨戚戚,乍暖还寒时候,最难将息,三杯两盏淡酒,咋敌...


多说两句


纵观整幅图,对比简单下单流程,可以发现,为了解决高并发下单,引入一个中间环节,而引入中间环节的副作用需要我们处理。虽然订单流程变复杂了,但并发提高了。一般来说,redis qps 10万,实际上没有10万,如果你的业务 qps 超过单机 redis 限制,记住,分布式的核心思想就是拆,把库存均匀打散到多台 redis。


打散之后需要解决库存倾斜问题,可能实例 a 已经卖完了,实例 b 还有部分库存,但部分用户请求打到实例 a,就会造成明明有货,但下单失败。这个问题也很棘手,感兴趣的同学可以自行研究,学会教教我。


上述流程经过简化,真实情况会更复杂,不一定适合实际场景。如果有错误的地方,烦请留言讨论,多多交流,互相学习,一起进步。


还有个问题需要提,流程中的事务问题。可以发现,订单流程是没有事务控制的。一方面我们认为,数据很宝贵,不分正常异常。异常的订单数据可作为分析系统架构缺陷的依据,另一方面接口尽量避免长事务,特别是高并发下,事务越短越好。


回答几个问题


为什么感觉拿掉分布式锁之后,流程变得很复杂?


其实我大可给订单流程前后包裹一个分布式锁,新的设计就变成下图,可以看到,核心库存扣减逻辑并没有变化,所以分布式锁的存在并不是让流程变复杂的原因。


image.png


为什么流程突然变的很复杂?



  • 为保证数据一致性,加了几个定时任务和一个重建缓存接口;为提高性能,加了冷热路由;为减少复杂度,把库存扣减消息延迟到支付,总体流程比简单下单流程多了几道工序

  • 因为引入异构数据库,数据源由一变多,就需要维护数据源数据一致性。可以说,这些流程纯纯是为了保证多个数据源的数据一致性。如果以前我们在 mysql 做库存扣减,基于 mysql 事务就能保证数据一致性。但是 mysql 的 qps 并不高,他适合并发不高的情况,所以我才会让冷门商品走 mysql 下单流程,因为冷门商品几乎没有并发

  • 所以流程变得复杂的原因是维护数据一致性


总结


场景一:并发较低,MySQL可承受


如果业务量不大,且并发只有几十或百来个,那么 MySQL 可以胜任。为了保证数据一致性,需要在外层套上分布式锁。同时,在使用 MySQL 时需要注意锁粒度和锁区间。此外,避免订单请求把 MySQL 连接数打满,影响其他业务,可以考虑使用 Sentinel 进行限流。


场景二:并发量大,MySQL存在瓶颈


当营销变得复杂时,不仅仅是普通的订单流程,还有秒杀、限时特价和热销推广等复杂场景,此类业务的并发集中在特定的 SKU 上。在这种情况下,接口并发可能没有太大问题,因为分布式锁有限流的作用。但对于用户而言,大量购买失败就会带来严重后果。此类场景的瓶颈在于 MySQL,在理论上,将库存打散到其他 MySQL 实例可以解决问题,但我们不会这样做,因为 MySQL 是有状态的,所以更推荐的做法是基于 Redis 扣库存。


场景三:商品数量过亿


如果有幸业务发展到亿级商品数量,此时如果将所有商品的库存都存储在 Redis,可能会带来非常大的内存开销。一般来说,库存的结构为 {SKU ID: 数量},每个 SKU 只需要占用两个 int(8个字节)的空间,因此在性能方面没有大问题。根据二八原则,非热销商品大约占80%,这些商品可能很久都没人买,把库存存到 redis 实属浪费 700多 m。基于分布式的拆分思想,以热度维度分流商品库存,热门商品库存存储到 Redis,冷门的商品库存存储到 MySQL


此外,可以参照 redis cluster,修改路由算法将商品库存分配到不同的 Redis 实例。不过从实际来说,当你商品过亿,也不差钱搭个 redis cluster。如果你细想,冷热路由相当于把库存分散到多个实例,这会带来一些问题,比如用户购买多件商品的库存跨了多个实例,如果确定扣库存顺序,如何解决库存不足的资损,还有库存的逆向流程等,这些问题展开很复杂,有机会讨论


最后,对于库存的消息落库问题,如果上游订单很多,而下游的库存服务处理速度较慢,可能会出现消息堆积现象。针对这种情况,可以采用生产者-消费者模型,通过合并数据并批量提交的方式来加快落库速度。这种优化方式可以有效地避免消息堆积现象,提高系统的性能和稳定性


作者:勤奋的Raido
来源:juejin.cn/post/7245753944181817403
收起阅读 »

领导问我:为什么一个点赞功能你做了五天?

领导希望做一个给文章点赞的功能,在文章列表页与文章详情页需要看到该文章的点赞总数,以及当前登录的用户有没有对该文章点赞,即用户与文章的点赞关系 前言 可乐是一名前端切图仔,最近他们团队需要做一个文章社区平台。由于人手不够,前后端部分都是由前端同学来实现,后端...
继续阅读 »

领导希望做一个给文章点赞的功能,在文章列表页与文章详情页需要看到该文章的点赞总数,以及当前登录的用户有没有对该文章点赞,即用户与文章的点赞关系



前言


可乐是一名前端切图仔,最近他们团队需要做一个文章社区平台。由于人手不够,前后端部分都是由前端同学来实现,后端部分用的技术栈是 nest.js


某一个周一,领导希望做一个给文章点赞的功能,在文章列表页与文章详情页需要看到该文章的点赞总数,以及当前登录的用户有没有对该文章点赞,即用户与文章的点赞关系。


交代完之后,领导就去出差了。等领导回来时已是周五,他问可乐:这期的需求进展如何?


可乐回答:点赞的需求我做完了,其他的还没开始。


领导生气的说:为什么点赞这样的一个小功能你做了五天才做完???


可乐回答:领导息怒。。请听我细细道来


往期文章


仓库地址



初步设计


对于上面的这个需求,我们提炼出来有三点最为重要的功能:



  1. 获取点赞总数

  2. 获取用户的点赞关系

  3. 点赞/取消点赞


所以这里容易想到的是在文章表中冗余一个点赞数量字段 likes ,查询文章的时候一起把点赞总数带出来。


idcontentlikes
1文章A10
2文章B20

然后建一张 article_lile_relation 表,建立文章点赞与用户之间的关联关系。


idarticle_iduser_idvalue
1100120011
2100120020

上面的数据就表明了 id2001 的用户点赞了 id1001 的文章; id2002 的用户对 id1001 的文章取消了点赞。


这是对于这种关联关系需求最容易想到的、也是成本不高的解决方案,但在仔细思考了一番之后,我放弃了这种方案。原因如下:



  1. 由于首页文章流中也需要展示用户的点赞关系,这里获取点赞关系需要根据当前文章 id 、用户 id 去联表查询,会增加数据库的查询压力。

  2. 有关于点赞的信息存放在两张表中,需要维护两张表的数据一致性。

  3. 后续可能会出现对摸鱼帖子点赞、对用户点赞、对评论点赞等需求,这样的设计方案显然拓展性不强,后续再做别的点赞需求时可能会出现大量的重复代码。


基于上面的考虑,准备设计一个通用的点赞模块,以拓展后续各种业务的点赞需求。


表设计


首先来一张通用的点赞表, DDL 语句如下:


CREATE TABLE `like_records` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) DEFAULT NULL,
`target_id` int(11) DEFAULT NULL,
`type` int(4) DEFAULT NULL,
`created_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`value` int(4) DEFAULT '0',
PRIMARY KEY (`id`),
KEY `like_records_target_id_IDX` (`target_id`,`user_id`,`type`) USING BTREE,
KEY `like_records_user_id_IDX` (`user_id`,`target_id`,`type`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;

解释一下上面各个字段的含义:



  • id :点赞记录的主键 id

  • user_id :点赞用户的 id

  • target_id :被点赞的文章 id

  • type :点赞类型:可能有文章、帖子、评论等

  • value :是否点赞, 1 点赞, 0 取消点赞

  • created_time :创建时间

  • updated_time :更新时间


前置知识


在设计好数据表之后,再来捋清楚这个业务的一些特定属性与具体实现方式:



  1. 我们可以理解这是一个相对来说读比写多的需求,比如你看了 10 篇掘金的文章,可能只会对 1 篇文章点赞

  2. 应该设计一个通用的点赞模块,以供后续各种点赞需求的接入

  3. 点赞数量与点赞关系需要频繁地获取,所以需要读缓存而不是读数据库

  4. 写入数据库与同步缓存需考虑数据一致性


所以可乐针对这样的业务特性上网查找了一些资料,发现有一些前置知识是他所欠缺的,我们一起来看看。


mysql事务


mysql 的事务是指一系列的数据库操作,这些操作要么全部成功执行,要么全部失败回滚。事务是用来确保数据库的完整性、一致性和持久性的机制之一。


mysql 中,事务具有以下四个特性,通常缩写为 ACID



  1. 原子性: 事务是原子性的,这意味着事务中的所有操作要么全部成功执行,要么全部失败回滚。

  2. 一致性: 事务执行后,数据库从一个一致的状态转换到另一个一致的状态。这意味着事务执行后,数据库中的数据必须满足所有的约束、触发器和规则,保持数据的完整性。

  3. 隔离性: 隔离性指的是多个事务之间的相互独立性。即使有多个事务同时对数据库进行操作,它们之间也不会相互影响,每个事务都感觉到自己在独立地操作数据库。 mysql 通过不同的隔离级别(如读未提交、读已提交、可重复读和串行化)来控制事务之间的隔离程度。

  4. 持久性: 持久性指的是一旦事务被提交,对数据库的改变将永久保存,即使系统崩溃也不会丢失。 mysql 通过将事务的提交写入日志文件来保证持久性,以便在系统崩溃后能够恢复数据。


这里以商品下单创建订单并扣除库存为例,演示一下 nest+typeorm 中的事务如何使用:


import { Injectable } from '@nestjs/common';
import { InjectEntityManager } from '@nestjs/typeorm';
import { EntityManager } from 'typeorm';
import { Order } from './order.entity';
import { Product } from './product.entity';

@Injectable()
export class OrderService {
constructor(
@InjectEntityManager()
private readonly entityManager: EntityManager,
) {}

async createOrderAndDeductStock(productId: number, quantity: number): Promise<Order> {
return await this.entityManager.transaction(async transactionalEntityManager => {
// 查找产品并检查库存是否充足
const product = await transactionalEntityManager.findOne(Product, productId);
if (!product || product.stock < quantity) {
throw new Error('Product not found or insufficient stock');
}

// 创建订单
const order = new Order();
order.productId = productId;
order.quantity = quantity;
await transactionalEntityManager.save(order);

// 扣除库存
product.stock -= quantity;
await transactionalEntityManager.save(product);

return order;
});
}
}


this.entityManager.transaction 创建了一个事务,在异步函数中,如果发生错误, typeorm 会自动回滚事务;如果没有发生错误,typeorm 会自动提交事务。


在这个实例中,尝试获取库存并创建订单和减库存,如果任何一个地方出错异常抛出,则事务就会回滚,这样就保证了多表间数据的一致性。


分布式锁



分布式锁是一种用于在分布式系统中协调多个节点并保护共享资源的机制。在分布式系统中,由于涉及多个节点并发访问共享资源,因此需要一种机制来确保在任何给定时间只有一个节点能够访问或修改共享资源,以防止数据不一致或竞争条件的发生。



对于同一个用户对同一篇文章频繁的点赞/取消点赞请求,可以加分布式锁的机制,来规避一些问题:



  1. 防止竞态条件: 点赞/取消点赞操作涉及到查询数据库、更新数据库和更新缓存等多个步骤,如果不加锁,可能会导致竞态条件,造成数据不一致或错误的结果。

  2. 保证操作的原子性: 使用分布式锁可以确保点赞/取消点赞操作的原子性,即在同一时间同一用户只有一个请求能够执行操作,从而避免操作被中断或不完整的情况发生。

  3. 控制并发访问: 加锁可以有效地控制并发访问,限制了频繁点击发送请求的数量,从而减少系统负载和提高系统稳定性。


redis 中实现分布式锁通常使用的是基于 SETNX 命令和 EXPIRE 命令的方式:



  1. 使用 SETNX 命令尝试将 lockKey 设置为 lockValue ,如果 lockKey 不存在,则设置成功并返回 1;如果 lockKey 已经存在,则设置失败并返回 0

  2. 如果 SETNX 成功,说明当前客户端获得了锁,可以执行相应的操作;如果 SETNX 失败,则说明锁已经被其他客户端占用,当前客户端需要等待一段时间后重新尝试获取锁。

  3. 为了避免锁被永久占用,可以使用 EXPIRE 命令为锁设置一个过期时间,确保即使获取锁的客户端在执行操作时发生故障,锁也会在一定时间后自动释放。


  async getLock(key: string) {
const res = await this.redis.setnx(key, 'lock');
if (res) {
// 10秒锁过期
await this.redis.expire(key, 10);
}
return res;
}

async unLock(key: string) {
return this.del(key);
}

redis中的set结构


redis 中的 set 是一种无序集合,用于存储多个不重复的字符串值,set 中的每个成员都是唯一的。


我们存储点赞关系的时候,需要用到 redis 中的 set 结构,存储的 keyvalue 如下:


article_1001:[uid1,uid2,uid3]


这就表示文章 id1001 的文章,有用户 iduid1uid2uid3 这三个用户点赞了。


常用的 set 结构操作命令包括:



  • SADD key member [member ...]: 将一个或多个成员加入到集合中。

  • SMEMBERS key: 返回集合中的所有成员。

  • SISMEMBER key member: 检查成员是否是集合的成员。

  • SCARD key: 返回集合元素的数量。

  • SREM key member [member ...]: 移除集合中一个或多个成员。

  • SPOP key [count]: 随机移除并返回集合中的一个或多个元素。

  • SRANDMEMBER key [count]: 随机返回集合中的一个或多个元素,不会从集合中移除元素。

  • SUNION key [key ...]: 返回给定所有集合的并集。

  • SINTER key [key ...]: 返回给定所有集合的交集。

  • SDIFF key [key ...]: 返回给定所有集合的差集。


下面举几个点赞场景的例子



  1. 当用户 iduid1 给文章 id1001 的文章点赞时:sadd 1001 uid1

  2. 当用户 iduid1 给文章 id1001 的文章取消点赞时:srem 1001 uid1

  3. 当需要获取文章 id1001 的点赞数量时:scard 1001


redis事务


redis 中,事务是一组命令的有序序列,这些命令在执行时会被当做一个单独的操作来执行。即事务中的所有命令要么全部执行成功,要么全部执行失败,不存在部分执行的情况。


以下是 redis 事务的主要命令:



  1. MULTI: 开启事务,在执行 MULTI 命令后,后续输入多个命令来组成一个事务。

  2. EXEC: 执行事务,在执行 EXEC 命令时,redis 会执行客户端输入的所有事务命令,如果事务中的所有命令都执行成功,则事务执行成功,返回事务中所有命令的执行结果;如果事务中的某个命令执行失败,则事务执行失败,返回空。

  3. DISCARD: 取消事务,在执行 DISCARD 命令时,redis 会取消当前事务中的所有命令,事务中的命令不会被执行。

  4. WATCH: 监视键,在执行 WATCH 命令时,redis 会监听一个或多个键,如果在执行事务期间任何被监视的键被修改,事务将会被打断。


比如说下面的代码给集合增加元素,并更新集合的过期时间,可以如下使用 redis 的事务去执行它:


  const pipeline = this.redisService.multi();
const setKey = this.getSetKey(targetId, type);
if (value === ELike.LIKE) {
pipeline.sadd(setKey, userId);
} else {
pipeline.srem(setKey, userId);
}
pipeline.expire(setKey, this.ttl);
await pipeline.exec();

流程图设计


在了解完这些前置知识之后,可乐开始画一些实现的流程图。


首先是点赞/取消点赞接口的流程图:


image.png


简单解释下上面的流程图:



  1. 先尝试获取锁,获取不到的时候等待重试,保证接口与数据的时序一致。

  2. 判断这个点赞关系是否已存在,比如说用户对这篇文章已经点过赞,其实又来了一个对此篇文章点赞的请求,直接返回失败

  3. 开启 mysql 的事务,去更新点赞信息表,同时尝试去更新缓存,在缓存更新的过程中,会有3次的失败重试机会,如果缓存更新都失败,则回滚mysql事务;整体更新失败

  4. mysql 更新成功,缓存也更新成功,则整个操作都成功


然后是获取点赞数量和点赞关系的接口


image.png


简单解释下上面的流程图:



  1. 首先判断当前文章 id 对应的点赞关系是否在 redis 中存在,如果存在,则直接从缓存中读取并返回

  2. 如果不存在,此时加锁,准备读取数据库并更新 redis ,这里加锁的主要目的是防止大量的请求一下子打到数据库中。

  3. 由于加锁的时候,可能很多接口已经在等待,所以在锁释放的时候,再加多一次从 redis 中获取的操作,此时 redis 中已经有值,可以直接从缓存中读取。


代码实现


在所有的设计完毕之后,可以做最后的代码实现了。分别来实现点赞操作与点赞数量接口。这里主要关注 service 层的实现即可。


点赞/取消点赞接口


  async toggleLike(params: {
userId: number;
targetId: number;
type: ELikeType;
value: ELike;
}
) {
const { userId, targetId, type, value } = params;
const LOCK_KEY = `${userId}::${targetId}::${type}::toggleLikeLock`;
const canGetLock = await this.redisService.getLock(LOCK_KEY);
if (!canGetLock) {
console.log('获取锁失败');
await wait();
return this.toggleLike(params);
}
const record = await this.likeRepository.findOne({
where: { userId, targetId, type },
});
if (record && record.value === value) {
await this.redisService.unLock(LOCK_KEY);
throw Error('不可重复操作');
}

await this.entityManager.transaction(async (transactionalEntityManager) => {
if (!record) {
const likeEntity = new LikeEntity();
likeEntity.targetId = targetId;
likeEntity.type = type;
likeEntity.userId = userId;
likeEntity.value = value;
await transactionalEntityManager.save(likeEntity);
} else {
const id = record.id;
await transactionalEntityManager.update(LikeEntity, { id }, { value });
}
const isSuccess = await this.tryToFreshCache(params);

if (!isSuccess) {
await this.redisService.unLock(LOCK_KEY);
throw Error('操作失败');
}
});
await this.redisService.unLock(LOCK_KEY);
return true;
}

private async tryToFreshCache(
params: {
userId: number;
targetId: number;
type: ELikeType;
value: ELike;
},
retry = 3,
) {
if (retry === 0) {
return false;
}
const { targetId, type, value, userId } = params;
try {
const pipeline = this.redisService.multi();
const setKey = this.getSetKey(targetId, type);
if (value === ELike.LIKE) {
pipeline.sadd(setKey, userId);
} else {
pipeline.srem(setKey, userId);
}
pipeline.expire(setKey, this.ttl);
await pipeline.exec();
return true;
} catch (error) {
console.log('tryToFreshCache error', error);
await wait();
return this.tryToFreshCache(params, retry - 1);
}
}


可以参照流程图来看这部分实现代码,基本实现就是使用 mysql 事务去更新点赞信息表,然后去更新 redis 中的点赞信息,如果更新失败则回滚事务,保证数据的一致性。


获取点赞数量、点赞关系接口


  async getLikes(params: {
targetId: number;
type: ELikeType;
userId: number;
}
) {
const { targetId, type, userId } = params;
const setKey = this.getSetKey(targetId, type);
const cacheExsit = await this.redisService.exist(setKey);
if (!cacheExsit) {
await this.getLikeFromDbAndSetCache(params);
}
const count = await this.redisService.getSetLength(setKey);
const isLike = await this.redisService.isMemberOfSet(setKey, userId);
return { count, isLike };
}

private async getLikeFromDbAndSetCache(params: {
targetId: number;
type: ELikeType;
userId: number;
}
) {
const { targetId, type, userId } = params;
const LOCK_KEY = `${targetId}::${type}::getLikesLock`;
const canGetLock = await this.redisService.getLock(LOCK_KEY);
if (!canGetLock) {
console.log('获取锁失败');
await wait();
return this.getLikeFromDbAndSetCache(params);
}
const setKey = this.getSetKey(targetId, type);
const cacheExsit = await this.redisService.exist(setKey);
if (cacheExsit) {
await this.redisService.unLock(LOCK_KEY);
return true;
}
const data = await this.likeRepository.find({
where: {
targetId,
userId,
type,
value: ELike.LIKE,
},
select: ['userId'],
});
if (data.length !== 0) {
await this.redisService.setAdd(
setKey,
data.map((item) => item.userId),
this.ttl,
);
}
await this.redisService.unLock(LOCK_KEY);
return true;
}

由于读操作相当频繁,所以这里应当多使用缓存,少查询数据库。读点赞信息时,先查 redis 中有没有,如果没有,则从 mysql 同步到 redis 中,同步的过程中也使用到了分布式锁,防止一开始没缓存时请求大量打到 mysql


同时,如果所有文章的点赞信息都同时存在 redis 中,那 redis 的存储压力会比较大,所以这里会给相关的 key 设置一个过期时间。当用户重新操作点赞时,会更新这个过期时间。保障缓存的数据都是相对热点的数据。


通过组装数据,获取点赞信息的返回数据结构如下:


image.png


返回一个 map ,其中 key 文章 idvalue 里面是该文章的点赞数量以及当前用户是否点赞了这篇文章。


前端实现


文章流列表发生变化的时候,可以监听列表的变化,然后去获取点赞的信息:


useEffect(() => {
if (!article.list) {
return;
}
const shouldGetLikeIds = article.list
.filter((item: any) => !item.likeInfo)
.map((item: any) => item.id);
if (shouldGetLikeIds.length === 0) {
return;
}
console.log("shouldGetLikeIds", shouldGetLikeIds);
getLikes({
targetIds: shouldGetLikeIds,
type: 1,
}).then((res) => {
const map = res.data;
const newList = [...article.list];
for (let i = 0; i < newList.length; i++) {
if (!newList[i].likeInfo && map[newList[i].id]) {
newList[i].likeInfo = map[newList[i].id];
}
}
const newArticle = { ...article };
newArticle.list = newList;
setArticle(newArticle);
});
}, [article]);

image.png


点赞操作的时候前端也需要加锁,接口执行完毕了再把锁释放。


   <Space
onClick={(e) => {
e.stopPropagation();
if (lockMap.current?.[item.id]) {
return;
}
lockMap.current[item.id] = true;
const oldValue = item.likeInfo.isLike;
const newValue = !oldValue;
const updateValue = (value: any) => {
const newArticle = { ...article };
const newList = [...newArticle.list];
const current = newList.find(
(_) => _.id === item.id
);
current.likeInfo.isLike = value;
if (value) {
current.likeInfo.count++;
} else {
current.likeInfo.count--;
}
setArticle(newArticle);
};
updateValue(newValue);
toggleLike({
targetId: item.id,
value: Number(newValue),
type: 1,
})
.catch(() => {
updateValue(oldValue);
})
.finally(() => {
lockMap.current[item.id] = false;
});
}}
>
<LikeOutlined
style={
item.likeInfo.isLike ? { color: "#1677ff" } : {}
}
/>

{item.likeInfo.count}
Space>

Kapture 2024-03-23 at 22.49.08.gif


解释


可乐:从需求分析考虑、然后研究网上的方案并学习前置知识,再是一些环境的安装,最后才是前后端代码的实现,领导,我这花了五天不过份吧。


领导(十分无语):我们平台本来就没几个用户、没几篇文章,本来就是一张关联表就能解决的问题,你又搞什么分布式锁又搞什么缓存,还花了那么多天时间。我不管啊,剩下没做的需求你得正常把它正常做完上线,今天周五,周末你也别休息了,过来加班吧。


最后


以上就是本文的全部内容,如果你觉得有意思的话,点点关注点点赞吧~


作者:可乐鸡翅kele
来源:juejin.cn/post/7349437605858066443
收起阅读 »

环信IM集成教程——Web端UIKit快速集成与消息发送

写在前面千呼万唤始出来,环信Web端终于出UIKit了!🎉🎉🎉文档地址:https://doc.easemob.com/uikit/chatuikit/web/chatuikit_overview.html环信单群聊 UIKit 是基于环信即时通讯云 IM S...
继续阅读 »

写在前面

千呼万唤始出来,环信Web端终于出UIKit了!🎉🎉🎉

环信单群聊 UIKit 是基于环信即时通讯云 IM SDK 开发的一款即时通讯 React UI 组件库。该组件库提供了聊天相关的组件,包括会话列表、聊天界面、联系人列表和群组设置等组件,组件内部集成了 IM SDK,可以帮助开发者不考虑内部实现和数据管理就能根据实际业务需求快速搭建包含 UI 界面的即时通讯应用。现在就让我们一起探索如何集成吧!本文介绍如何快速实现在单聊会话中发送消息


准备工作

  1. React 环境:需要 React 16.8.0 或以上版本;React DOM 16.8.0 或以上版本。

  2. 即时通讯 IM 项目:已在环信即时通讯云控制台创建了有效的环信即时通讯 IM 开发者账号,并获取了 App Key

  3. 环信用户:在环信控制台创建 IM 用户,并获取用户 ID 和密码或 token。

  4. 好友关系:双方需要先 添加好友 才可以聊天



集成UIKit

准备工作完成就开始集成!在此先奉上环信Web端UIKit源码

第一步:创建一个UIKit项目

# 安装 CLI 工具。
npm install create-react-app
# 构建一个 my-app 的项目。
npx create-react-app my-app
cd my-app

第二步:安装 easemob-chat-uikit

cd my-app
  • 使用 npm 安装 easemob-chat-uikit 包
npm install easemob-chat-uikit --save
  • 使用 yarn 安装 easemob-chat-uikit 包
yarn add easemob-chat-uikit

第三步:引入uikit组件

在你的 React 项目中,引入 UIKit 提供的组件和样式:

// 导入组件
import {
UIKitProvider,
Chat,
ConversationList,
// ...
} from "easemob-chat-uikit";

// 导入样式
import "easemob-chat-uikit/style.css";

第四步:初始化配置

easemob-chat-uikit 提供 UIKitProvider 组件管理数据。UIKitProvider 不渲染任何 UI, 只用于为其他组件提供全局的 context,自动监听 SDK 事件, 在组件树中向下传递数据来驱动组件更新。单群聊 UIKit 中其他组件必须用 UIKitProvider 包裹。

import "./App.css";
import { UIKitProvider} from "easemob-chat-uikit";
import "easemob-chat-uikit/style.css";
function App() {
return (
<div>
<UIKitProvider
initConfig={{
appKey: "your app key", // 你的 app key
userId: "userId", // 用户 ID
password: "password", // 如果使用密码登录,传入密码。
translationTargetLanguage: "zh-Hans", // 翻译功能的目标语言
useUserInfo: true, // 是否使用用户属性功能展示头像昵称(UIKit 内部会获取用户属性,需要用户自己设置)
}}
local={{
fallbackLng: "zh",
lng: "zh",
resources: {
zh: {
translation: {
hello: "欢迎使用",
conversationTitle: "会话列表",
deleteCvs: "删除会话",
//...
},
},
},
}}
>
</UIKitProvider>
</div>
);
}

export default App;


第五步:引入组件

根据自己的项目引入所需组件,组件文档,本文只介绍如何快速实现在单聊会话中发送消息,为了方便快速体验,一定要确保准备工作的第四条双方已经互为好友

import "./App.css";
import { UIKitProvider} from "easemob-chat-uikit";
import "easemob-chat-uikit/style.css";
function App() {
return (
<div>
<UIKitProvider
initConfig={{
appKey: "your app key", // 你的 app key
userId: "userId", // 用户 ID
password: "password", // 如果使用密码登录,传入密码。
translationTargetLanguage: "zh-Hans", // 翻译功能的目标语言
useUserInfo: true, // 是否使用用户属性功能展示头像昵称(UIKit 内部会获取用户属性,需要用户自己设置)
}}
local={{
fallbackLng: "zh",
lng: "zh",
resources: {
zh: {
translation: {
hello: "欢迎使用",
conversationTitle: "会话列表",
deleteCvs: "删除会话",
//...
},
},
},
}}
>
<div style={{ display: "flex" }}>
<div style={{ width: "40%", height: "100%" }}>
<ContactList
onItemClick={(data) => {
rootStore.conversationStore.addConversation({
chatType: "singleChat",
conversationId: data.id,
lastMessage: {},
unreadCount: "",
});
}}
/>
</div>//联系人组件,点击某个好友通过‘rootStore.conversationStore.addConversation’创建会话
<div style={{ width: "30%", height: "100%" }}>
<ConversationList />//会话列表组件
</div>
<div style={{ width: "30%", height: "100%" }}>
<Chat />//聊天消息组件
</div>
</div>
</UIKitProvider>
</div>
);
}

export default App;

第六步:运行并测试

1、运行项目

npm run start

2、点击好友并发送一条消息



总结

通过以上步骤,你已经成功集成了环信单聊 UIKit 并实现了基本的即时通讯功能,接下来继续根据 UIKit 提供的组件和 API 文档进行进一步开发吧~

相关文档

收起阅读 »

提升你的CSS技能:深入理解伪类选择器和伪元素选择器!

在CSS的世界里,有些选择器并不像它们的名字那样直接。今天,我们要探索的是两种特殊的选择器:伪类选择器和伪元素选择器。它们虽然名字相似,但功能和用途却大有不同。下面就让我们一起来了解一下它们是如何在我们的页面布局中扮演着不可或缺的角色的吧。一、伪类选择器1、什...
继续阅读 »

在CSS的世界里,有些选择器并不像它们的名字那样直接。今天,我们要探索的是两种特殊的选择器:伪类选择器和伪元素选择器。它们虽然名字相似,但功能和用途却大有不同。

下面就让我们一起来了解一下它们是如何在我们的页面布局中扮演着不可或缺的角色的吧。

一、伪类选择器

1、什么是伪类选择器

伪类选择器,顾名思义,是一种特殊的选择器,它用来选择DOM元素在特定状态下的样式。这些特定状态并不是由文档结构决定的,而是由用户行为(如点击、悬停)或元素的状态(如被访问、被禁用)来定义的。

例如,我们可以用伪类选择器来改变链接在不同状态下的颜色,从而给用户以视觉反馈。

2、伪类选择器的语法

selector:pseudo-class {
property: value;
}

a:link {
color: #FF0000;
}

input:focus {
background-color: yellow;
}

注意:伪类名称对大小写不敏感。

3、常用的伪类选择器

下面分别介绍一下比较常用几类伪类选择器:

3.1 动态伪类选择器

这类选择器主要用于描述用户与元素的交互状态。例如:

1):hover: 当鼠标悬停在元素上时的样式。

代码示例:将链接的文本颜色改为红色

a:hover {
color: red;
}

2):active:当元素被用户激活(如点击)时的样式。

代码示例:将按钮的背景色改为蓝色

button:active {
background-color: blue;
}

3):focus: 当元素获得焦点(如输入框被点击)时的样式。

代码示例:将输入框的边框颜色改为绿色

input:focus {
border-color: green;
}

4):visited: 用于设置已访问链接的样式,通常与:link一起使用来区分未访问和已访问的链接。

代码示例:将已访问链接的颜色改为紫色

a:visited {
color: purple;
}

3.2 UI元素状态伪类选择器

这类选择器用于描述元素在用户界面中的状态。例如:

1):enabled和:disabled: 用于表单元素,表示元素是否可用。

示例:将禁用的输入框的边框颜色改为灰色

input:disabled {
border-color: gray;
}

2):checked: 用于单选框或复选框,表示元素是否被选中。

示例:将选中的单选框的背景色改为黄色

input[type="radio"]:checked {
background-color: yellow;
}

3):nth-child(n): 选取父元素中第n个子元素。

示例:将列表中的奇数位置的项目的背景色改为蓝色:

li:nth-child(odd) {
background-color: blue;
}

3.4 否定伪类选择器

这类选择器用于排除符合特定条件的元素。例如:

:not(selector): 选取不符合括号内选择器的所有元素。

示例:将不是段落的元素的背景色改为灰色:

*:not(p) {
background-color: gray;
}

4、常见应用

  • 设置鼠标悬停在元素上时的样式;

  • 为已访问和未访问链接设置不同的样式;

  • 设置元素获得焦点时的样式;


// 示例:a 标签的四种状态,分别对应 4 种伪类;

/* 未访问的链接 */
a:link {
color: blue;
}

/* 已访问的链接 */
a:visited {
color: red;
}

/* 鼠标悬停链接 */
a:hover {
color: orange;
}

/* 已选择的链接(鼠标点击但不放开时) */
a:active {
color: #0000FF;
}

注意:

  • a 标签的 4 个伪类(4种状态)必须按照一定顺序书写,否则将会失效;

  • a:hover 必须在 CSS 定义中的 a:link 和 a:visited 之后,才能生效;

  • a:active 必须在 CSS 定义中的 a:hover 之后才能生效;

  • 书写顺序为:a:link、a:visited、a:hover、a:active;

  • 记忆方法:love hate - “爱恨准则”;

二、伪元素选择器

1、什么是伪元素选择器

与伪类选择器不同,伪元素选择器是用来选择DOM元素的特定部分,而不是整个元素。它们通常用于处理那些不是由HTML标签直接表示的内容,比如首行文字、首字母或者生成的内容(如内容前面的编号)。

伪元素选择器允许我们对页面上的某些部分进行精确的样式控制,而这些部分在HTML结构中并不存在。

2、伪元素选择器语法

selector::pseudo-element {
property: value;
}

p::first-line {
color: #ff0000;
}

h1::before {
content: '♥';
}

3、常用伪元素选择器

伪元素选择器并不是针对真正的元素使用的选择器,而是针对CSS中已经定义好的伪元素使用的选择器,CSS中有如下四种常用伪元素选择器:first-line、 first-letter、 before、after。

3.1 ::first-line

::first-line表示第一行(第一行内容根据屏幕大小来决定显示多少字),例如:p::first-line{}。
代码示例:

    <style>
p::first-line{
color: blue;
}
</style>

Description

3.2 ::first-letter

::first-letter表示第一个字母,例如:p::first-letter{}。

代码示例:

<style>
p::first-letter{
font-size: 30px;
color: blueviolet;
}
</style>

Description

3.3 ::before和::after

::before表示元素的开始,::after表示元素的最后,before和after必须结合content属性来使用。

代码示例:

 <style>
p::after{
content: "hahaha";
color: red;
}
p::before{
content: "hehehe";
color: coral;
}
</style>

Description

注意:

  • before和after创建一个元素,但是属于行内元素。
  • 新创建的这个元素在文档中是找不到的,所以我们称为伪元素。
  • before在父元素内容的前面创建元素,after在父元素内容的后面插入元素。
  • 伪元素选择器和标签选择器一样,权重为1。

想要快速入门前端开发吗?推荐一个前端开发基础课程,这个老师讲的特别好,零基础学习无压力,知识点结合代码,边学边练,可以免费试看试学,还有各种辅助工具和资料,非常适合新手!

点这里前往学习哦!

三、伪类与伪元素选择器的区别

CSS中的伪类选择器和伪元素选择器都是用来选取DOM中特定元素的选择器。具体区别如下:

伪类的操作对象是文档树中已有的元素,而伪元素则创建了一个文档数外的元素。因此,伪类与伪元素的区别在于:有没有创建一个文档树之外的元素;

  • 伪类本质上是为了弥补常规CSS选择器的不足,以便获取到更多信息;

  • 伪元素本质上是创建了一个有内容的虚拟容器;

  • CSS3 中伪类和伪元素的语法不同;

  • 在 CSS3 中,已经明确规定了伪类用一个冒号来表示,而伪元素则用两个冒号来表示;

  • 可以同时使用多个伪类,而只能同时使用一个伪元素。

总的来说,伪类选择器关注的是元素在特定状态下的样式变化,而伪元素选择器则是通过创建新的元素来实现特定的样式效果。两者都是CSS中非常强大的工具,可以帮助开发者实现复杂的页面布局和动态效果。

伪类选择器和伪元素选择器虽然不是真正的元素,但它们在CSS中扮演着极其重要的角色。了解并熟练运用它们,可以让你的网页更加生动、互动性更强,同时也能更好地控制页面的布局和内容的表现。

收起阅读 »