注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

封装v-loading指令 从此释放双手

web
封装v-loading指令 从此释放双手 前言 ​ 大家好, 我是旋风冲锋 - 小瑜, 又到了周六~~ 没错, 是卷王们疯狂成长的日子, 今天早上突发奇想, 想去自习室体验一下敲代码的快感, 心想着卷到下午,面对着窗口看着夕阳西下的场景, 然后可以...
继续阅读 »

封装v-loading指令 从此释放双手


前言


​ 大家好, 我是旋风冲锋 - 小瑜, 又到了周六~~ 没错, 是卷王们疯狂成长的日子, 今天早上突发奇想, 想去自习室体验一下敲代码的快感, 心想着卷到下午,面对着窗口看着夕阳西下的场景, 然后可以拍个照片发朋友圈装逼.


​ 但是想象很美好, 坐着地铁到了自习室, 发现大家伙都是在非常安静, 唯一能出声音的就是翻书或者写字的声音. 为了不影响其他人故意挑一个靠窗户的位置,接着对着电脑开始疯狂进攻. 我的键盘声很快传遍了整见屋子. 虽然别人没有说什么, 但是自己觉得好像是故意来捣乱的, 今天的键盘声音显得格外的大声. 没多久我知趣的溜溜球. 美团体验卷直接gg, 哈哈哈, 问题不大~


​ 言归正传, 今天给大家分享的是利用 VUE3 实现v-loading的加载效果 , 先看一下实现效果吧~


2023-09-24-00-48-48.gif


这一类效果在使用组件库, 例如饿了么中出现的频率很高, 使用方法也很简单, 给对应的结构添加上


v-loading="布尔值"即可, 是不是很好奇是怎么实现的? 那么就和旋风冲锋小瑜开始冲!


实现思路



  • loading肯定也是一个组件, 其中包含加载效果还有提示文字, 并且使用的时候可以去修改文字以及开启或者关闭加载动画

  • 实现的周期是在 异步开始前, 开启loading, 在异步处理[数据加载]完成后 关闭loading

  • 既然是在模版中通过 v-xxx来实现的, 那么肯定就是一个自定义指令, Vue提供指令, 也就是去操作DOM[组件实例]


那么按照以上的实现思路, 一步一步去完成, 首先搭设一个Demo的模版结构和样式


搭设基本模版


利用Vue3搭设demo架子, 头部tab栏, 切换路由 , main区域的显示内容


App.vue


<script setup lang="ts"></script>

<template>
<div class="container">
// Tab栏
<Tabs></Tabs>
// 一级路由出口
<router-view></router-view>
</div>
</template>


<style lang="scss">
.container {
width: 100vw;
height: 100vh;
background-color: #1e1e1e;
}
</style>


router路由


 routes: [
{
path: '/',
redirect: '/huawei'
},
{
path: '/huawei',
component: () => import('@/views/Huawei/index.vue'),
meta: {
title: '华为'
}
},
{
path: '/rongyao',
component: () => import('@/views/Rongyao/index.vue'),
meta: {
title: '荣耀'
}
},
{
path: '/xiaomi',
component: () => import('@/views/Xiaomi/index.vue'),
meta: {
title: '小米'
}
},
{
path: '/oppo',
component: () => import('@/views/Oppo/index.vue'),
meta: {
title: 'oppo'
}
}
]

Tabs组件


<script setup lang="ts">
import { ref } from 'vue'
const tabList = ref([
{ id: 1, text: '华为', path: '/huawei' },
{ id: 2, text: '荣耀', path: '/rongyao' },
{ id: 3, text: '小米', path: '/xiaomi' },
{ id: 4, text: 'oppo', path: '/oppo' }
])
const activeIndex = ref(0)
</script>

<template>
<div class="tabs-box">
<router-link
class="tab-item"
:to="item.path"
v-for="(item, index) in tabList"
:key="item.id"
>

<span
class="tab-link"
:class="{ active: activeIndex === index }"
@click="activeIndex = index"
>

{{ item.text }}
</span>
</router-link>
</div>
</template>


<style lang="scss" scoped>
.tabs-box {
width: 100%;
display: flex;
justify-content: space-around;
.tab-item {
padding: 10px;
&.router-link-active {
.tab-link {
transition: border 0.3s;
color: gold;
padding-bottom: 5px;
border-bottom: 2px solid gold;
&.active {
border-bottom: 2px solid gold;
color: gold;
}
}
}
}
}
</style>


按照路由去创建4个文件夹,这里按照huawei做举例


<script setup lang="ts">
const src = ref('')
</script>

<template>
<div class="box" >
<div class="img-box">
<img :src="src" alt="" />
</div>
</div>
</template>


创建Loading组件


首先按照最直接的方式, 利用 v-if 以及组件通讯, 实现组件方式的实现


注意 这里是通过 position: absolute; 通过定位的方式进行垂直水平居中, 先埋下伏笔


<script setup lang="ts">

defineProps({
title: {
type: String,
default: '正在加载中...'
}
})

</script>

<template>
<div class="loading-box">
<div class="loading-content">
// loading 动图
<img src="./loading.gif" />
// 底部提示文字
<p class="desc">{{ title }}</p>
</div>
</div>
</template>


<style lang="scss" scoped>
.loading-box {
position: absolute;
top: 50%;
left: 50%;
transform: translate3d(-50%, -50%, 0);
.loading-content {
text-align: center;
img {
width: 35px !important;
height: 35px !important;
}
.desc {
line-height: 20px;
font-size: 12px;
color: #fff;
position: relative;
}
}
}
</style>


在对应组件中使用Loading组件, 利用延时器模拟异步操作


<script>
const src = ref('')
const title = ref('华为加载中...')
const showLoading = ref(true) // 控制loading的显示和隐藏

onMounted(() => {
showLoading.value = true
// 模拟异步请求
window.setTimeout(() => {
src.value =
'https://ms.bdimg.com/pacific/0/pic/-1284887113_-1109246585.jpg?x=0&y=0&h=340&w=510&vh=340.00&vw=510.00&oh=340.00&ow=510.00'
showLoading.value = false
}, 1000)
})
</script>

<template>
<div class="box">
<div class="img-box" v-if="!showLoading">
<img :src="src" alt="" />
</div>
</div>
<Loading v-if="showLoading"></Loading>
</template>


效果一样可以出来, 接下来就利用指令的方式来优化


011.png


V-loading 指令实现


思路:



  • 在dom挂载完成后, 创建Loading实例, 需要挂载到写在具体指令结构上

  • loading需要知道传递的显示文字, 这里通过指令动态的参数传递

  • 当loading组件参数更新后卸载, 关闭loading


1. 使用自定义指令的参数


和内置指令类似,自定义指令的参数也可以是动态的, 下面是Vue官网的截图


动态指令参数.png


在模版中使用 v-loading:[title]="showLoading"


const title = ref('华为加载中...')

<template>
<div class="box" v-loading:[title]="showLoading">
...
</div>

<!-- <Loading v-if="showLoading"></Loading> -->
</template>

2. 利用插件注册指令


在Loading文件下创建js文件


import Loading from './index.vue' // 导入.vue文件
const loadingDirective = {
}
export default loadingDirective

components下创建index.js文件


import loading from '@/components/Loading/index'
export default {
install: (app: App) => {
app.directive('loading', loading)
}

入口文件中注册插件


import MyLoading from '@/components/index'
app.use(MyLoading)

3. 指令 - 节点都挂载完成后调用



  • createApp 作用: 创建一个应用实例 - 创建loading

  • app.mount 作用: 将应用实例挂载在一个容器元素中

  • mounted 参数 el=>获取dom

  • mounted 参数 binding.value => 控制开启和关闭loding 也就是 showLoading

  • mounted 参数 binding.arg => loading显示的文字 [例如华为加载中...]


const loadingDirective = {
/* 节点都挂载完成后调用 */
mounted(el: any, binding: DirectiveBinding) {
/*
value 控制开启和关闭loding
arg loading显示的文字
*/

const { value, arg } = binding
/* 创建loading实例,并挂载 */
const app = createApp(Loading)
// 这一步 instance === loading.vue
// 此时就可以视同loading.vue 也就是组件实例的方法和属性
const instance = app.mount(document.createElement('div'))
/* 为了让elAppend获取到创建的div元素 */
el.instance = instance
/* 如果传入了自定义的文字就添加title */
if (arg) {
instance.setTitle(arg)
}
/* 如果showLoading为true将loading实例挂载到指令元素中 */
if (value) {
// 添加方法方法, 看下文
handleAppend(el)
}
},
}

可以从控制台查看binding中的title以及showLoading的值


02.png


instance.setTitle(arg) 这里既然使用到了组件实例的setTitle方法, 就需要在loading中对应的方法


注意: 在vue3中需要利用defineExpose抛出事件, 让外界可以访问或使用


Loading.vue


const title = ref('')
const setTitle = (val: string) => {
title.value = val
}
// defineProps({ 组件通讯就使用不到了, 注释即可
// title: {
// type: String,
// default: '正在加载中...'
// }
// })

defineExpose({
setTitle,
title
})

<template>
<div class="loading-box">
<div class="loading-content">
<img src="./loading.gif" />
<p class="desc">{{ title }}</p>
</div>
</div>

</template>

4. 指令 - handleAppend(el)方法实现


/* 将loading添加到指令所在DOM */
const handleAppend = (el: any) => {
console.log(el.instance.$el, 'el.instance.$el')
el.appendChild(el.instance.$el)
}

04.png


5. 指令 - updated() 更新后挂载还是消除的逻辑


在第四步中, loading已经可以通过指令显示了, 此时还需要让showLoading为false的时候, 或者这么理解: 当新的值不等于老值的是够关闭loading


此时就可以利用指令中updated钩子去执行这一段关闭的逻辑, 一下是官网的说明


05.png


  /* 更新后调用 */
updated(el: any, binding: DirectiveBinding) {
const { value, oldValue, arg } = binding
if (value !== oldValue) {
/* 更新标题 */
if (arg) {
el.instance.setTitle(arg)
}
// 是显示吗? 如果是就添加 : 如果不是就删除
value ? handleAppend(el) : c(el)
}
}

6. 指令 - handleRemove()方法实现


/* 将loading在DOM中移除 */
const handleRemove = (el: any) => {
removeClass(el, relative as any)
el.removeChild(el.instance.$el)
}

此时基本已经完成了需求, 但是上文我提到了坑点, 原因是loading是通过绝对定位的方式进行水平居中, 那么比如我要在图片中显示loading呢? 我们来实现下这个坑点


7. 坑点的说明


<template>
<div class="box" //关闭 v-loading:[title]="showLoading">
<div class="img-box" v-loading:[title]="showLoading">
<img :src="src" alt="" />
</div>
</div>

<!-- <Loading v-if="true"></Loading> -->
</template>

06.png


很明显发现, 执行现在图片这个盒子上, 并没有水平居中, 审查元素其实也很明显, css样式中是根据子绝父相, 但是此时大盒子并没有提供相对定位, 自然就无法水平居中


那么如何修改呢? 其实只要给绑定指令的盒子添加position: relative;属性即可, 当然absolute或者fixed效果一样可以居中


问题已找到了, 那么在appendChild时判断当前是否存在relative | absolute | fixed 的其中一个, 如果没有就需要classList.add进行添加, 同时在removeChild删除添加的relative | absolute | fixed 即可


8. 完善坑点, 实现水平居中


getComputedStyle() 在MDN上的说明:


方法返回一个对象,该对象在应用活动样式表并解析这些值可能包含的任何基本计算后报告元素的所有 CSS 属性的值。私有的 CSS 属性值可以通过对象提供的 API 或通过简单地使用 CSS 属性名称进行索引来访问。


/* 将loading添加到指令所在DOM */
const relative = 'relative'
const handleAppend = (el: any) => {
const style = getComputedStyle(el)
if (!['absolute', 'relative', 'fixed'].includes(style.position)) {
addClass(el, relative as any)
}

el.appendChild(el.instance.$el)
}
// 添加relative
const addClass = (el: any, className: string) => {
if (!el.classList.contains(className)) {
el.classList.add(className)
}
}
// 删除relative
const removeClass = (el: any, className: string) => {
el.classList.remove(className)
}

结尾


夜深人静又是卷到凌晨1点, 只管努力, 其他交给天意~ 旋风冲锋手动撒花 ✿✿ヽ(°▽°)ノ✿


demo地址: gitee.com/tcwty123/v-…


作者:不知名小瑜
来源:juejin.cn/post/7281825352530296843
收起阅读 »

今年的年终奖开了个寂寞

大家好啊,我是董董灿。 年底了,又到了一些公司开年终奖的时候了,往年这个时候,网上都是争相"炫富"的声音。 还记得去年某公司,在春节前一下子开出了十几个月的年终奖,让我羡慕了好长时间。 可是今年的形势好像不太乐观,我最近一直在关注年终奖的消息,但怎么感觉,今年...
继续阅读 »

大家好啊,我是董董灿。


年底了,又到了一些公司开年终奖的时候了,往年这个时候,网上都是争相"炫富"的声音。


还记得去年某公司,在春节前一下子开出了十几个月的年终奖,让我羡慕了好长时间。


可是今年的形势好像不太乐观,我最近一直在关注年终奖的消息,但怎么感觉,今年的年终奖开了个寂寞呢?



我也是一个在职场上摸爬滚打的打工人,遇到这种情况,总是会感慨一下。


今年就想和各位小伙伴来聊一个每个打工人可能都会遇到的问题,那就是关于年终奖和工资的那些事儿


1、年终奖本来就是不确定的


我们经常在网上看到一些说法,发了 offer 后给的包有多大,这里说的包指的是薪资总包,先看下什么是总包?


总包一般说的是年薪,它包括基础月薪绩效奖金年终奖金福利待遇以及股票期权等。


基础月薪雷打不动,只要你在公司工作,每个月就会给发,而且是受法律保护的工资。


但绩效奖金或年终奖却不是这样的,这种奖金公司拥有最终解释权。


如果公司效益不好,年终奖可能会打折,甚至直接不发。



当然也有例外。


如果在入职签合同时,合同上明确写了会发 xx 个月的年终奖,但公司又以某种理由不发。


那么这就属于是违反约定,可以通过一些法律途径来解决。


说到这你可能就明白了,只要不是付诸到纸面上的数字,尤其是年终奖,都有不发的风险,而且对公司而言,是有正当理由的。


绩效奖金和年终奖,本来就不是确定的,能拿到多少一方面看自己的能力,另一方面还要看公司的心情。


所以,如果有两个公司提供以下两种薪资待遇,你会选择哪个呢?



  • 月薪 4 万 * (12 + 3) = 60 w, 其中 3 个月工资为绩效浮动奖金

  • 月薪 4.5 万 * (12 + 1) = 58.5 w, 其中 1个月工资为绩效浮动奖金


2、 选现金还是选股票?


除了上面的例子,还有一种比较常见的 offer 选择: 你是要现金还是要股票?


不少人在入职新公司的时候,都会遇到选择薪资方案的情况。


一般情况下,公司会提供两种薪资方案让你选择:高股票方案和高现金方案。


这两种薪资包的总包一般都是一样的,不同的就是在总包中,到底是股票占的多一些还是现金占的多一些。


比如 100 w 的总包,有以下两种方案来选择:



  • 基础月薪 4 万 * (12 + 3) + 40 万股票或期权

  • 基础月薪 5 万 * (12 + 3) + 25 万股票或期权


股票还好一些,如果公司已经上市了,那么股票价值就可以直接根据股价来确定。


那如果公司没有上市,给的是期权,那么就会按照公司估值来计算期权价值。


假设给你价值 30万 的期权,每股估值 1 元,那么共计就是 30 万股。


天知道这家公司的估值到底值不值每股 1 元,可能实际估值只有 0.1 元,那么你的 30万股,可能实际只值 3 万。


而且在公司上市或者可以买卖前,就是一张废纸,没办法变现。


所以,如果你看中了公司的长远价值,并且坚信公司未来会有很好的发展,有信心可以将期权持有到公司上市。


那么就什么都不要想,All in 期权,有多少选多少。


但如果你没有信心,那还是老老实实选择现金比较靠谱,毕竟在现在的环境下,落袋为安才是王道。


我有一个朋友,最近就陷入了这样的两难选择,他前几天来找我随便聊了聊。



我想说的是,我们普通人其实很少有那种眼界,可以看到一个公司可以走多远,未来是否真的可以上市。


选择高期权方案,大部分还是抱着赌一赌的态度,而如果你是稳健型投资选手,我还是建议选择高现金方案。


少年不知现金好,却把期权当成宝。


不知道各位小伙伴怎么看待这个问题的呢,可以在评论区留言讨论。


作者:董董灿是个攻城狮
来源:juejin.cn/post/7324351711659982875
收起阅读 »

前端对接电子秤、扫码枪设备serialPort 串口使用教程

web
因为最近工作项目中用到了电子秤,需要对接电子秤设备。以前也没有对接过这种设备,当时也是一脸懵逼,脑袋空空。后来就去网上搜了一下前端怎么对接,然后就发现了SerialPort串口。 Serialport 官网地址:serialport.io/ Github:g...
继续阅读 »

因为最近工作项目中用到了电子秤,需要对接电子秤设备。以前也没有对接过这种设备,当时也是一脸懵逼,脑袋空空。后来就去网上搜了一下前端怎么对接,然后就发现了SerialPort串口。



Serialport


官网地址:serialport.io/


Github:github.com/serialport/…


官方描述:使用 JavaScript 访问串行端口。Linux、OSX 和 Windows。



SerialPort是什么?



SerialPort 是一个用于在 Node.js 环境中进行串口通信的库。它允许开发者通过 JavaScript 或 TypeScript 代码与计算机上的串口设备进行交互。SerialPort 库提供了丰富的 API,使得在串口通信中能够方便地进行设置、监听和发送数据。



一般我们的设备(电子秤/扫码枪)会有一根线插入到电脑的USB口或者其他口,电脑上的这些插口就是叫串口。设备上的数据会通过这根线传输到电脑里面,比如电子秤传到电脑里的就是重量数值。那么我们前端怎么接收解析到这些数据的呢?SerialPort的作用就是用来帮我们接收设备传输过来的数据,也可以向设备发送数据。


简单概括一下:SerialPort就是我们前端和设备之间的翻译官,可以接收设备传输过来的数据,也可以向设备发送数据。


SerialPort怎么用?


SerialPort可以在Node项目中使用,也可以在Electron项目中使用,我们一般都是用在Electron项目中,接下来讲一下在Electron项目中SerialPort怎么下载和引入


1、创建Electron项目


mkdir my-electron-app && cd my-electron-app
npm init -y
npm i --save-dev electron

网上有很多Electron教程,这里不再详细说了


在package.json中看一下自己的Electron的版本,下一步会用到


2、下载SerialPort


这里先看一下自己使用的Electron对应的Node版本是什么,打开下面electron官网看表格中的Node那一列


Electron发行时间表:http://www.electronjs.org/zh/docs/lat…


image-20240113215800193.png


如果你Electron对应的Node版本高于v12.0.0,直接下载就行


npm install serialport

如果你Electron对应的Node版本低于或等于v12.0.0,请用对应的Node版本对应下面的serialport版本下载



serialport.io/docs/next/g…




  • 对于 Node.js 版本0.100.12,最后一个正常运行的版本是serialport@4

  • 对于 Node.js 版本4.0,最后一个正常运行的版本是serialport@6.

  • 对于 Node.js 版本8.0,最后一个正常运行的版本是serialport@8.

  • 对于 Node.js 版本10.0,最后一个正常运行的版本是serialport@9.

  • 对于 Node.js 版本12.0,最后一个正常运行的版本是serialport@10.



我项目的Electron版本是11.5.0,对应的Node版本号是12.0,对应的serialport版本号是serialport@10.0.0



3、编译Serialport



  • 安装node-gyp 用于调用其他语言编写的程序(如果已安装过请忽略这一步)


    npm install -g node-gyp




  • 进入@serialport目录


    cd ./node_modules/@serialport/bindings


  • 进行编译,target后面换成当前Electron的版本号


    node-gyp rebuild --target=11.5.0



如果编译的时候报错了就将自己电脑的Node版本切换成当前Electron对应的版本号再编译一次


查看Electron对应Node版本号:http://www.electronjs.org/zh/docs/lat…


编译成功以后就可以在代码里使用Serialport了


4、使用Serialport



serialport官网使用教程:serialport.io/docs/next/g…



4.1、引入Serialport


const { SerialPort } = require('serialport')
// or
import { SerialPort } from 'serialport'

4.2、创建串口(重点!)


创建串口有两种写法,新版本是这样写法new SerialPort(params, callback)


const port = new SerialPort({
 path: 'COM1',  // 串口号
 baudRate: 9600, // 波特率
 autoOpen: true,  // 是否自动打开端口
}, function (err) {
 if (err) {
   return console.log('打开失败: ', err.message)
}
 console.log('打开成功')
})

旧版本是下面这样的写法new Serialport(path, params, callback),我用的是serialport@10.0.0版本就是这样的写法


const port = new Serialport('COM1', {
 baudRate: 9600,
 autoOpen: true,  // 是否自动打开端口
}, function (err) {
 if (err) {
   return console.log('打开失败: ', err.message)
}
 console.log('打开成功')
})

创建串口的时候需要传入两个重要的参数是path和baudRate,path是串口号,baudRate是波特率。最后一个参数是回调函数



不知道怎么查看串口号和波特率看这篇文章


如何查看串口号和波特率?



4.3、手动打开串口


如果autoOpen参数是false,需要使用port.open()方法手动打开


const port = new SerialPort({
 path: 'COM1',  // 串口号
 baudRate: 9600, // 波特率
 autoOpen: false,  // 是否自动打开端口, 默认true
})
// autoOpen参数是false,需要使用port.open()方法手动打开
port.open(function (err) {
 if (err) {
   return console.log('打开失败', err.message)
}
 console.log('打开成功')
})

4.4、接收数据(重点!)


接收到的data是一个Buffer,需要转换为字符串进行查看


port.on('data', function (data) {
 // 接收到的data是一个Buffer,需要转换为字符串进行查看
 console.log('Data:', data.toString('utf-8'))
})

接收过来的data就是设备传输过来的数据,转换后的字符串就是我们需要的数据,字符串里面可能有多个数据,我们把自己需要的数据截取出来就可以了


假设通过电子秤设备获取到的数据就是"205 000 000",中间是四个空格分割的,第一个数字205就是获取的重量,需要把这个重量截取出来。下面是我的示例代码


port.on('data', function (data) {
 try {
     // 获取的data是一个Buffer
     // 1.将 Buffer 转换为字符串 dataString.toString('utf-8')
     let weight = data.toString('utf-8')
     // 2.将字符串分割转换成数组,取数组的第一个值.split('   ')[0]
     weight = weight.split('   ')[0]
     // 3.将取的值 去掉前后空格
     weight = weight.trim()
     // 4.最后转换成数字,获取到的数字就是重量
     weight = Number(weight)
     console.log('获取到重量:'+ weight);
} catch (err) {
   console.error(`
     重量获取报错:${err}
     获取到的Buffer: ${data}
     Buffer转换后的值:${data.toString('utf-8')}
   `
);
}
})

4.5、写入数据


port.write('Hi Mom!')
port.write(Buffer.from('Hi Mom!'))

4.6、实时获取(监听)所有串口


const { SerialPort } = require('serialport')

SerialPort.list().then((ports, err) => {
   // 串口列表
   console.log('获取所有串口列表', ports);
})

更多内容


serialport官网教程:serialport.io/docs/next/g…


作者:Yaoqi
来源:juejin.cn/post/7323464381172301860
收起阅读 »

从framework角度看app保活问题

问题背景 最近在群里看到群友在讨论app保活的问题,回想之前做应用(运动类)开发时也遇到过类似的需求,于是便又来了兴趣,果断加入其中,和群友展开了激烈的讨论 不少群友的想法和我当初的想法一样,这特么保活不是看系统的心情么,系统想让谁活谁才能活,作为app开发...
继续阅读 »

问题背景


最近在群里看到群友在讨论app保活的问题,回想之前做应用(运动类)开发时也遇到过类似的需求,于是便又来了兴趣,果断加入其中,和群友展开了激烈的讨论


保活


不少群友的想法和我当初的想法一样,这特么保活不是看系统的心情么,系统想让谁活谁才能活,作为app开发者,根本无能为力,可真的是这样的吗?


保活方案


首先,我整理了从古到今,app开发者所使用过的以及当前还在使用的保活方式,主要思路有两个:保活和复活


保活的方案有:


  • 1像素惨案

  • 后台无声音乐

  • 前台service

  • 心跳机制

  • socket长连接

  • 无障碍服务

  • ......


复活的方案有:


  • 双进程守护(java层和native层)

  • JobScheduler定时任务

  • 推送/相互唤醒

  • ......


不难看出,app开发者为了能让自己的应用多存活一会儿,可谓是绞尽脑汁,但即使这样,随着Android系统升级,尤其是进入8.0之后,系统对应用的限制越来越高,传统的保活方式已经不生效,这让Android开发者手足无措,于是乎,出现了一种比较和谐的保活方式:



  • 引导用户开启手机白名单


这也是目前绝大多数应用所采用的的方式,相对于传统黑科技而言,此方式显得不那么流氓,比较容易被用户所接受。


但跟微信这样的国民级应用比起来,保活效果还是差了一大截,那么微信是怎么实现保活的呢?或者回到我们开头的问题,应用的生死真的只能靠系统调度吗?开发者能否干预控制呢?


进程调度原则


解开这个疑问之前,我们需要了解一下Android系统进程调度原则,主要介绍framework中承载四大组件的进程是如何根据组件状态而动态调节自身状态的。进程有两个比较重要的状态值:



  • oom_adj,定义在frameworks/base/services/core/java/com/android/server/am/ProcessList.java当中

  • procState,定义在frameworks/base/core/java/android/app/ActivityManager.java当中


OOM_ADJ

以Android10的源码为例,oom_adj划分为20级,取值范围[-10000,1001],Android6.0以前的取值范围是[-17,16]



  • oom_adj值越大,优先级越低

  • oom_adj<0的进程都是系统进程。


public final class ProcessList {
static final String TAG = TAG_WITH_CLASS_NAME ? "ProcessList" : TAG_AM;

// The minimum time we allow between crashes, for us to consider this
// application to be bad and stop and its services and reject broadcasts.
static final int MIN_CRASH_INTERVAL = 60 * 1000;

// OOM adjustments for processes in various states:

// Uninitialized value for any major or minor adj fields
static final int INVALID_ADJ = -10000;

// Adjustment used in certain places where we don't know it yet.
// (Generally this is something that is going to be cached, but we
// don't know the exact value in the cached range to assign yet.)
static final int UNKNOWN_ADJ = 1001;

// This is a process only hosting activities that are not visible,
// so it can be killed without any disruption.
static final int CACHED_APP_MAX_ADJ = 999;
static final int CACHED_APP_MIN_ADJ = 900;

// This is the oom_adj level that we allow to die first. This cannot be equal to
// CACHED_APP_MAX_ADJ unless processes are actively being assigned an oom_score_adj of
// CACHED_APP_MAX_ADJ.
static final int CACHED_APP_LMK_FIRST_ADJ = 950;

// Number of levels we have available for different service connection group importance
// levels.
static final int CACHED_APP_IMPORTANCE_LEVELS = 5;

// The B list of SERVICE_ADJ -- these are the old and decrepit
// services that aren't as shiny and interesting as the ones in the A list.
static final int SERVICE_B_ADJ = 800;

// This is the process of the previous application that the user was in.
// This process is kept above other things, because it is very common to
// switch back to the previous app. This is important both for recent
// task switch (toggling between the two top recent apps) as well as normal
// UI flow such as clicking on a URI in the e-mail app to view in the browser,
// and then pressing back to return to e-mail.
static final int PREVIOUS_APP_ADJ = 700;

// This is a process holding the home application -- we want to try
// avoiding killing it, even if it would normally be in the background,
// because the user interacts with it so much.
static final int HOME_APP_ADJ = 600;

// This is a process holding an application service -- killing it will not
// have much of an impact as far as the user is concerned.
static final int SERVICE_ADJ = 500;

// This is a process with a heavy-weight application. It is in the
// background, but we want to try to avoid killing it. Value set in
// system/rootdir/init.rc on startup.
static final int HEAVY_WEIGHT_APP_ADJ = 400;

// This is a process currently hosting a backup operation. Killing it
// is not entirely fatal but is generally a bad idea.
static final int BACKUP_APP_ADJ = 300;

// This is a process bound by the system (or other app) that's more important than services but
// not so perceptible that it affects the user immediately if killed.
static final int PERCEPTIBLE_LOW_APP_ADJ = 250;

// This is a process only hosting components that are perceptible to the
// user, and we really want to avoid killing them, but they are not
// immediately visible. An example is background music playback.
static final int PERCEPTIBLE_APP_ADJ = 200;

// This is a process only hosting activities that are visible to the
// user, so we'd prefer they don't disappear.
static final int VISIBLE_APP_ADJ = 100;
static final int VISIBLE_APP_LAYER_MAX = PERCEPTIBLE_APP_ADJ - VISIBLE_APP_ADJ - 1;

// This is a process that was recently TOP and moved to FGS. Continue to treat it almost
// like a foreground app for a while.
// @see TOP_TO_FGS_GRACE_PERIOD
static final int PERCEPTIBLE_RECENT_FOREGROUND_APP_ADJ = 50;

// This is the process running the current foreground app. We'd really
// rather not kill it!
static final int FOREGROUND_APP_ADJ = 0;

// This is a process that the system or a persistent process has bound to,
// and indicated it is important.
static final int PERSISTENT_SERVICE_ADJ = -700;

// This is a system persistent process, such as telephony. Definitely
// don't want to kill it, but doing so is not completely fatal.
static final int PERSISTENT_PROC_ADJ = -800;

// The system process runs at the default adjustment.
static final int SYSTEM_ADJ = -900;

// Special code for native processes that are not being managed by the system (so
// don't have an oom adj assigned by the system).
static final int NATIVE_ADJ = -1000;

// Memory pages are 4K.
static final int PAGE_SIZE = 4 * 1024;

//省略部分代码
}

ADJ级别取值说明(可参考源码注释)
INVALID_ADJ-10000未初始化adj字段时的默认值
UNKNOWN_ADJ1001缓存进程,无法获取具体值
CACHED_APP_MAX_ADJ999不可见activity进程的最大值
CACHED_APP_MIN_ADJ900不可见activity进程的最小值
CACHED_APP_LMK_FIRST_ADJ950lowmemorykiller优先杀死的级别值
SERVICE_B_ADJ800旧的service的
PREVIOUS_APP_ADJ700上一个应用,常见于应用切换场景
HOME_APP_ADJ600home进程
SERVICE_ADJ500创建了service的进程
HEAVY_WEIGHT_APP_ADJ400后台的重量级进程,system/rootdir/init.rc文件中设置
BACKUP_APP_ADJ300备份进程
PERCEPTIBLE_LOW_APP_ADJ250受其他进程约束的进程
PERCEPTIBLE_APP_ADJ200可感知组件的进程,比如背景音乐播放
VISIBLE_APP_ADJ100可见进程
PERCEPTIBLE_RECENT_FOREGROUND_APP_ADJ50最近运行的后台进程
FOREGROUND_APP_ADJ0前台进程,正在与用户交互
PERSISTENT_SERVICE_ADJ-700系统持久化进程已绑定的进程
PERSISTENT_PROC_ADJ-800系统持久化进程,比如telephony
SYSTEM_ADJ-900系统进程
NATIVE_ADJ-1000native进程,不受系统管理

可以通过cat /proc/进程id/oom_score_adj查看目标进程的oom_adj值,例如我们查看电话的adj


dialer_oom_adj


值为935,处于不可见进程的范围内,当我启动电话app,再次查看


dialer_oom_adj_open


此时adj值为0,也就是正在与用户交互的进程


ProcessState

process_state划分为23类,取值范围为[-1,21]


@SystemService(Context.ACTIVITY_SERVICE)
public class ActivityManager {
//省略部分代码
/** @hide Not a real process state. */
public static final int PROCESS_STATE_UNKNOWN = -1;

/** @hide Process is a persistent system process. */
public static final int PROCESS_STATE_PERSISTENT = 0;

/** @hide Process is a persistent system process and is doing UI. */
public static final int PROCESS_STATE_PERSISTENT_UI = 1;

/** @hide Process is hosting the current top activities. Note that this covers
* all activities that are visible to the user. */

@UnsupportedAppUsage
public static final int PROCESS_STATE_TOP = 2;

/** @hide Process is hosting a foreground service with location type. */
public static final int PROCESS_STATE_FOREGROUND_SERVICE_LOCATION = 3;

/** @hide Process is bound to a TOP app. This is ranked below SERVICE_LOCATION so that
* it doesn't get the capability of location access while-in-use. */

public static final int PROCESS_STATE_BOUND_TOP = 4;

/** @hide Process is hosting a foreground service. */
@UnsupportedAppUsage
public static final int PROCESS_STATE_FOREGROUND_SERVICE = 5;

/** @hide Process is hosting a foreground service due to a system binding. */
@UnsupportedAppUsage
public static final int PROCESS_STATE_BOUND_FOREGROUND_SERVICE = 6;

/** @hide Process is important to the user, and something they are aware of. */
public static final int PROCESS_STATE_IMPORTANT_FOREGROUND = 7;

/** @hide Process is important to the user, but not something they are aware of. */
@UnsupportedAppUsage
public static final int PROCESS_STATE_IMPORTANT_BACKGROUND = 8;

/** @hide Process is in the background transient so we will try to keep running. */
public static final int PROCESS_STATE_TRANSIENT_BACKGROUND = 9;

/** @hide Process is in the background running a backup/restore operation. */
public static final int PROCESS_STATE_BACKUP = 10;

/** @hide Process is in the background running a service. Unlike oom_adj, this level
* is used for both the normal running in background state and the executing
* operations state. */

@UnsupportedAppUsage
public static final int PROCESS_STATE_SERVICE = 11;

/** @hide Process is in the background running a receiver. Note that from the
* perspective of oom_adj, receivers run at a higher foreground level, but for our
* prioritization here that is not necessary and putting them below services means
* many fewer changes in some process states as they receive broadcasts. */

@UnsupportedAppUsage
public static final int PROCESS_STATE_RECEIVER = 12;

/** @hide Same as {@link #PROCESS_STATE_TOP} but while device is sleeping. */
public static final int PROCESS_STATE_TOP_SLEEPING = 13;

/** @hide Process is in the background, but it can't restore its state so we want
* to try to avoid killing it. */

public static final int PROCESS_STATE_HEAVY_WEIGHT = 14;

/** @hide Process is in the background but hosts the home activity. */
@UnsupportedAppUsage
public static final int PROCESS_STATE_HOME = 15;

/** @hide Process is in the background but hosts the last shown activity. */
public static final int PROCESS_STATE_LAST_ACTIVITY = 16;

/** @hide Process is being cached for later use and contains activities. */
@UnsupportedAppUsage
public static final int PROCESS_STATE_CACHED_ACTIVITY = 17;

/** @hide Process is being cached for later use and is a client of another cached
* process that contains activities. */

public static final int PROCESS_STATE_CACHED_ACTIVITY_CLIENT = 18;

/** @hide Process is being cached for later use and has an activity that corresponds
* to an existing recent task. */

public static final int PROCESS_STATE_CACHED_RECENT = 19;

/** @hide Process is being cached for later use and is empty. */
public static final int PROCESS_STATE_CACHED_EMPTY = 20;

/** @hide Process does not exist. */
public static final int PROCESS_STATE_NONEXISTENT = 21;
//省略部分代码
}

state级别取值说明(可参考源码注释)
PROCESS_STATE_UNKNOWN-1不是真正的进程状态
PROCESS_STATE_PERSISTENT0持久化的系统进程
PROCESS_STATE_PERSISTENT_UI1持久化的系统进程,并且正在操作UI
PROCESS_STATE_TOP2处于栈顶Activity的进程
PROCESS_STATE_FOREGROUND_SERVICE_LOCATION3运行前台位置服务的进程
PROCESS_STATE_BOUND_TOP4绑定到top应用的进程
PROCESS_STATE_FOREGROUND_SERVICE5运行前台服务的进程
PROCESS_STATE_BOUND_FOREGROUND_SERVICE6绑定前台服务的进程
PROCESS_STATE_IMPORTANT_FOREGROUND7对用户很重要的前台进程
PROCESS_STATE_IMPORTANT_BACKGROUND8对用户很重要的后台进程
PROCESS_STATE_TRANSIENT_BACKGROUND9临时处于后台运行的进程
PROCESS_STATE_BACKUP10备份进程
PROCESS_STATE_SERVICE11运行后台服务的进程
PROCESS_STATE_RECEIVER12运动广播的后台进程
PROCESS_STATE_TOP_SLEEPING13处于休眠状态的进程
PROCESS_STATE_HEAVY_WEIGHT14后台进程,但不能恢复自身状态
PROCESS_STATE_HOME15后台进程,在运行home activity
PROCESS_STATE_LAST_ACTIVITY16后台进程,在运行最后一次显示的activity
PROCESS_STATE_CACHED_ACTIVITY17缓存进程,包含activity
PROCESS_STATE_CACHED_ACTIVITY_CLIENT18缓存进程,且该进程是另一个包含activity进程的客户端
PROCESS_STATE_CACHED_RECENT19缓存进程,且有一个activity是最近任务里的activity
PROCESS_STATE_CACHED_EMPTY20空的缓存进程,备用
PROCESS_STATE_NONEXISTENT21不存在的进程

进程调度算法

frameworks/base/services/core/java/com/android/server/am/OomAdjuster.java中,有三个核心方法用于计算和更新进程的oom_adj值



  • updateOomAdjLocked():更新adj,当目标进程为空,或者被杀则返回false,否则返回true。

  • computeOomAdjLocked():计算adj,计算成功返回true,否则返回false。

  • applyOomAdjLocked():应用adj,当需要杀掉目标进程则返回false,否则返回true。


adj更新时机

也就是updateOomAdjLocked()被调用的时机。通俗的说,只要四大组件被创建或者状态发生变化,或者当前进程绑定了其他进程,都会触发adj更新,具体可在源码中查看此方法被调用的地方,比较多,这里就不列举了


adj的计算过程

computeOomAdjLocked()计算过程相当复杂,将近1000行代码,这里就不贴了,有兴趣可自行查看,总体思路就是根据当前进程的状态,设置对应的adj值,因为状态值很多,所以会有很多个if来判断每个状态是否符合,最终计算出当前进程属于哪种状态。


adj的应用

计算得出的adj值将发送给lowmemorykiller(简称lmk),由lmk来决定进程的生死,不同的厂商,lmk的算法略有不同,下面是源码中对lmk的介绍


/* drivers/misc/lowmemorykiller.c
*
* The lowmemorykiller driver lets user-space specify a set of memory thresholds
* where processes with a range of oom_score_adj values will get killed. Specify
* the minimum oom_score_adj values in
* /sys/module/lowmemorykiller/parameters/adj and the number of free pages in
* /sys/module/lowmemorykiller/parameters/minfree. Both files take a comma
* separated list of numbers in ascending order.
*
* For example, write "0,8" to /sys/module/lowmemorykiller/parameters/adj and
* "1024,4096" to /sys/module/lowmemorykiller/parameters/minfree to kill
* processes with a oom_score_adj value of 8 or higher when the free memory
* drops below 4096 pages and kill processes with a oom_score_adj value of 0 or
* higher when the free memory drops below 1024 pages.
*
* The driver considers memory used for caches to be free, but if a large
* percentage of the cached memory is locked this can be very inaccurate
* and processes may not get killed until the normal oom killer is triggered.
*
* Copyright (C) 2007-2008 Google, Inc.
*
* This software is licensed under the terms of the GNU General Public
* License version 2, as published by the Free Software Foundation, and
* may be copied, distributed, and modified under those terms.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*/


保活核心思路


根据上面的Android进程调度原则得知,我们需要尽可能降低app进程的adj值,从而减少被lmk杀掉的可能性,而我们传统的保活方式最终目的也是降低adj值。而根据adj等级分类可以看出,通过应用层的方式最多能将adj降到100~200之间,我分别测试了微信、支付宝、酷狗音乐,启动后返回桌面并息屏,测试结果如下


微信测试结果:


weixin_oom_adj


微信创建了两个进程,查看这两个进程的adj值均为100,对应为adj等级表中的VISIBLE_APP_ADJ,此结果为测试机上微信未登录状态测试结果,当换成我的小米8测试后发现,登录状态下的微信有三个进程在运行


weixin_login_oom_adj


后查阅资料得知,进程名为com.tencent.soter.soterserver的进程是微信指纹支付,此进程的adj值居然为-800,上面我们说过,adj小于0的进程为系统进程,那么微信是如何做到创建一个系统进程的,我和我的小伙伴都惊呆了o.o,为此,我对比了一下支付宝的测试结果


支付宝测试结果:


alipay_oom_adj


支付宝创建了六个进程,查看这六个进程的adj值,除了一个为915,其余均为0,怎么肥事,0就意味着正在与用户交互的前台进程啊,我的世界要崩塌了,只有一种可能,支付宝通过未知的黑科技降低了adj值。


酷狗测试结果:


kugou_oom_adj.png


酷狗创建了两个进程,查看这两个进程的adj值分别为700、200,对应为adj等级表中的PREVIOUS_APP_ADJPERCEPTIBLE_APP_ADJ,还好,这个在意料之中。


测试思考


通过上面三个app的测试结果可以看出,微信和支付宝一定是使用了某种保活手段,让自身的adj降到最低,尤其是微信,居然可以创建系统进程,简直太逆天了,这是应用层绝对做不到的,一定是在native层完成的,但具体什么黑科技就不得而知了,毕竟反编译技术不是我的强项。


正当我郁郁寡欢之时,我想起了前两天看过的一篇文章《当 App 有了系统权限,真的可以为所欲为?》,文章讲述了第三方App如何利用CVE漏洞获取到系统权限,然后神不知鬼不觉的干一些匪夷所思的事儿,这让我茅塞顿开,或许这些大厂的app就是利用了系统漏洞来保活的,不然真的就说不通了,既然都能获取到系统权限了,那创建个系统进程不是分分钟的事儿吗,还需要啥厂商白名单。


总结


进程保活是一把双刃剑,增加app存活时间的同时牺牲的是用户手机的电量,内存,cpu等资源,甚至还有用户的忍耐度,作为开发者一定要合理取舍,不要为了保活而保活,即使需要保活,也尽量采用白色保活手段,别让用户手机变板砖,然后再来哭爹骂娘。


参考资料:


探讨Android6.0及以上系统APP常驻内存(保活)实现-争宠篇


探讨Android6.0及以上系统APP常驻内存(保活)实现-复活篇


探讨一种新型的双进程守护应用保活


史上最强Android保活思路:深入剖析腾讯TIM的进程永生技术


当 App 有了系统权限,真的可以为所欲为?


「 深蓝洞察 」2022 年度最“不可赦”漏洞


作者:小迪vs同学
来源:juejin.cn/post/7210375037114138680
收起阅读 »

flex布局之美,以后就靠它来布局了

web
写在前面 在很久很久以前,网页布局基本上通过table 元素来实现。通过操作table 中单元格的align 和valign可以实现水平垂直居中等 再后来,由于CSS 不断完善,便演变出了:标准文档流、浮动布局和定位布局 3种布局 来实现水平垂直居中等各种布局...
继续阅读 »

写在前面


在很久很久以前,网页布局基本上通过table 元素来实现。通过操作table 中单元格的alignvalign可以实现水平垂直居中等


再后来,由于CSS 不断完善,便演变出了:标准文档流浮动布局定位布局 3种布局 来实现水平垂直居中等各种布局需求。


下面我们来看看实现如下效果,各种布局是怎么完成的


image-20240114134424060


实现这样的布局方式很多,为了方便演示效果,我们在html代码种添加一个父元素,一个子元素,css样式种添加一个公共样式来设置盒子大小,背景颜色


<div class="parent">
  <div class="child">我是子元素</div>
</div>

/* css公共样式代码 */
.parent{
   background-color: orange;
   width: 300px;
   height: 300px;
}
.child{
   background-color: lightcoral;
   width: 100px;
   height: 100px;
}

①absolute + 负margin 实现


/* 此处引用上面的公共代码 */

/* 定位代码 */
.parent {
   position: relative;
}
.child {
   position: absolute;;
   top: 50%;
   left: 50%;
   margin-left: -50px;
   margin-top: -50px;
}

②absolute + transform 实现


/* 此处引用上面的公共代码 */

/* 定位代码 */
.parent {
   position: relative;
}
.child {
   position: absolute;
   top: 50%;
   left: 50%;
   transform: translate(-50%, -50%);
}

③ flex实现


.parent {
   display: flex;
   justify-content: center;
   align-items: center;
}

通过上面三种实现来看,我们应该可以发现flex 布局是最简单了吧。


对于一个后端开发人员来说,flex布局算是最友好的了,因为它操作简单方便


一、flex 布局简介



flex 全称是flexible Box,意为弹性布局 ,用来为盒状模型提供布局,任何容器都可以指定为flex布局。


通过给父盒子添加flex属性即可开启弹性布局,来控制子盒子的位置和排列方式。


父容器可以统一设置子容器的排列方式,子容器也可以单独设置自身的排列方式,如果两者同时设置,以子容器的设置为准



flex布局


二、flex基本概念



flex的核心概念是 容器,容器包括外层的 父容器 和内层的 子容器,轴包括 主轴辅轴



<div class="parent">
   <div class="child">我是子元素</div>
</div>

2.1 轴



  • 在 flex 布局中,是分为主轴和侧轴两个方向,同样的叫法有 : 行和列、x 轴和y 轴,主轴和交叉轴

  • 默认主轴方向就是 x 轴方向,水平向右

  • 默认侧轴方向就是 y 轴方向,水平向下


    主轴和侧轴



注:主轴和侧轴是会变化的,就看 flex-direction 设置谁为主轴,剩下的就是侧轴。而我们的子元素是跟着主轴来排列的


--flex-direction 值--含义
row默认值,表示主轴从左到右
row-reverse表示主轴从右到左
column表示主轴从上到下
column-reverse表示主轴从下到上

2.2 容器



容器的属性可以作用于父容器(container)或者子容器(item)上



①父容器(container)-->属性添加在父容器上



  • flex-direction 设置主轴的方向

  • justify-content 设置主轴上的子元素排列方式

  • flex-wrap 设置是否换行

  • align-items 设置侧轴上的子元素排列方式(单行 )

  • align-content 设置侧轴上的子元素的排列方式(多行)


②子容器(item)-->属性添加在子容器上



  • flex 属性 定义子项目分配剩余空间,用flex来表示占多少份数

  • align-self控制子项自己在侧轴上的排列方式

  • order 属性定义项目的排列顺序


三、主轴侧轴设置


3.1 flex-direction: row



flex-direction: row 为默认属性,主轴沿着水平方向向右,元素从左向右排列。



row


3.2 flex-direction: row-reverse



主轴沿着水平方向向左,子元素从右向左排列



row-reverse


3.3 flex-direction: column



主轴垂直向下,元素从上向下排列



column


3.4 flex-direction: column-reverse



主轴垂直向下,元素从下向上排列



column-reverse


四、父容器常见属性设置


4.1 主轴上子元素排列方式


4.1.1 justify-content


justify-content 属性用于定义主轴上子元素排列方式


justify-content: flex-start|flex-end|center|space-between|space-around



flex-start:起始端对齐


flex-start


flex-end:末尾段对齐


flex-end


center:居中对齐


center


space-around:子容器沿主轴均匀分布,位于首尾两端的子容器到父容器的距离是子容器间距的一半。


space-around


space-between:子容器沿主轴均匀分布,位于首尾两端的子容器与父容器相切。


space-between


4.2 侧轴上子元素排列方式


4.2.1 align-items 单行子元素排列


这里我们就以默认的x轴作为主轴



align-items:flex-start:起始端对齐


flex-start


align-items:flex-end:末尾段对齐


flex-end


align-items:center:居中对齐


center


align-items:stretch 侧轴拉伸对齐



如果设置子元素大小后不生效



stretch


4.2.2 align-content 多行子元素排列


设置子项在侧轴上的排列方式 并且只能用于子项出现 换行 的情况(多行),在单行下是没有效果的


我们需要在父容器中添加 flex-wrap: wrap;


flex-wrap: wrap; 是啥意思了,具体会在下一小节中细说,就是当所有子容器的宽度超过父元素时,换行显示



align-content: flex-start 起始端对齐


 /* 父容器添加如下代码 */
display: flex;
align-content: flex-start;
flex-wrap: wrap;

align-content: flex-start


align-content: flex-end :末端对齐


/* 父容器添加如下代码 */
display: flex;
align-content: flex-end;
flex-wrap: wrap;

align-content: flex-end


align-content: center: 中间对齐


/* 父容器添加如下代码 */
display: flex;
align-content: center;
flex-wrap: wrap;

align-content: center


align-content: space-around: 子容器沿侧轴均匀分布,位于首尾两端的子容器到父容器的距离是子容器间距的一半


/* 父容器添加如下代码 */
display: flex;
align-content: space-around;
flex-wrap: wrap;

align-content: space-around


align-content: space-between:子容器沿侧轴均匀分布,位于首尾两端的子容器与父容器相切。


/* 父容器添加如下代码 */
display: flex;
align-content: space-between;
flex-wrap: wrap;

image-20240114171606954


align-content: stretch: 子容器高度平分父容器高度


/* 父容器添加如下代码 */
display: flex;
align-content: stretch;
flex-wrap: wrap;

align-content: stretch


4.3 设置是否换行



默认情况下,项目都排在一条线(又称”轴线”)上。flex-wrap属性定义,flex布局中默认是不换行的。



flex-wrap: nowrap :不换行


/* 父容器添加如下代码 */
display: flex;
flex-wrap: nowrap;

flex-wrap: nowrap


flex-wrap: wrap: 换行


/* 父容器添加如下代码 */
display: flex;
flex-wrap: wrap;

flex-wrap: wrap


4.4 align-content 和align-items区别



  • align-items 适用于单行情况下, 只有上对齐、下对齐、居中和 拉伸

  • align-content适应于换行(多行)的情况下(单行情况下无效), 可以设置 上对齐、下对齐、居中、拉伸以及平均分配剩余空间等属性值。

  • 总结就是单行找align-items 多行找 align-content


五、子容器常见属性设置



  • flex子项目占的份数

  • align-self控制子项自己在侧轴的排列方式

  • order属性定义子项的排列顺序(前后顺序)


5.1 flex 属性



flex 属性定义子项目分配剩余空间,用flex来表示占多少份数。



① 语法


.item {
   flex: <number>; /* 默认值 0 */
}

②将1号、3号子元素宽度设置成80px,其余空间分给2号子元素


flex:1


5.2 align-self 属性



align-self 属性允许单个项目有与其他项目不一样的对齐方式,可覆盖 align-items 属性。


默认值为 auto,表示继承父元素的 align-items 属性,如果没有父元素,则等同于 stretch。



align-self: flex-start 起始端对齐


/* 父容器添加如下代码 */
display: flex;
align-items: center;
/*第一个子元素*/
align-self: flex-start;

align-self: flex-start


align-self: flex-end 末尾段对齐


/* 父容器添加如下代码 */
display: flex;
align-items: center;
/*第一个子元素*/
align-self: flex-end;

align-self: flex-end


align-self: center 居中对齐


/* 父容器添加如下代码 */
display: flex;
align-items: flex-start;
/*第一个子元素*/
align-self: center;

align-self: center


align-self: stretch 拉伸对齐


/* 父容器添加如下代码 */
display: flex;
align-items: flex-start;
/*第一个子元素 未指定高度才生效*/
align-self: stretch;

align-self: stretch


5.3 order 属性



数值越小,排列越靠前,默认为0。



① 语法:


.item {
   order: <number>;
}

② 既然默认是0,那我们将第二个子容器order:-1,那第二个元素就跑到最前面了


/* 父容器添加如下代码 */
display: flex;
/*第二个子元素*/
order: -1;

order


六、小案例


最后我们用flex布局实现下面常见的商品列表布局


商品列表


<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>简单商品布局</title>
   <style>
       .goods{
           display: flex;
           justify-content: center;
      }
       p{
           text-align: center;
      }
       span{
           margin: 0;
           color: red;
           font-weight: bold;
      }
       .goods001{
           width: 230px;
           height: 322px;
           margin-left: 5px;
      }
       .goods002{
           width: 230px;
           height: 322px;
           margin-left: 5px;
      }
       .goods003{
           width: 230px;
           height: 322px;
           margin-left: 5px;
      }
       .goods004{
           width: 230px;
           height: 322px;
           margin-left: 5px;
      }

   
</style>
</head>
<body>

   <div class="goods">
       <div class="goods001">
           <img src="./imgs/goods001.jpg" >
           <p>松下(Panasonic)洗衣机滚筒</p>
           <span>¥3899.00</span>
       </div>
       <div class="goods002">
           <img src="./imgs/goods002.jpg" >
           <p>官方原装浴霸灯泡</p>
           <span>¥17.00</span>
       </div>
       <div class="goods003">
           <img src="./imgs/goods003.jpg" >
           <p>全自动变频滚筒超薄洗衣机</p>
           <span>¥1099.00</span>
       </div>
       <div class="goods004">
           <img src="./imgs/goods004.jpg" >
           <p>绿联 车载充电器</p>
           <span>¥28.90</span>
       </div>
   </div>
</body>
</html>

以上就是本期内容的全部,希望对你有所帮助。我们下期再见 (●'◡'●)


作者:xiezhr
来源:juejin.cn/post/7323539673346375719
收起阅读 »

如何选择 Android 唯一标识符

前言 大家好,我是未央歌,一个默默无闻的移动开发搬砖者~ 本文针对 Android 各种标识符做了统一收集,方便大家比对,以供选择适合大家的唯一标识符。 标识符 IMEI 从 Android 6.0 开始获取 IMEI 需要权限,并且从 Android 10...
继续阅读 »

前言


大家好,我是未央歌,一个默默无闻的移动开发搬砖者~


本文针对 Android 各种标识符做了统一收集,方便大家比对,以供选择适合大家的唯一标识符。


标识符


IMEI



  • 从 Android 6.0 开始获取 IMEI 需要权限,并且从 Android 10+ 开始官方取消了获取 IMEI 的 API,无法获取到 IMEI 了


fun getIMEI(context: Context): String {
val telephonyManager = context
.getSystemService(TELEPHONY_SERVICE) as TelephonyManager
return telephonyManager.deviceId
}

Android ID(SSAID)



  • 无需任何权限

  • 卸载安装不会改变,除非刷机或重置系统

  • Android 8.0 之后签名不同的 APP 获取的 Android ID 是不一样的

  • 部分设备由于制造商错误实现,导致多台设备会返回相同的 Android ID

  • 可能为空


fun getAndroidID(context: Context): String {
return Settings.System.getString(context.contentResolver,Settings.Secure.ANDROID_ID)
}

MAC 地址



  • 需要申请权限,Android 12 之后 BluetoothAdapter.getDefaultAdapter().getAddress()需要动态申请 android.permission.BLUETOOTH_CONNECT 权限

  • MAC 地址具有全局唯一性,无法由用户重置,在恢复出厂设置后也不会变化

  • 搭载 Android 10+ 的设备会报告不是设备所有者应用的所有应用的随机化 MAC 地址

  • 在 Android 6.0 到 Android 9 中,本地设备 MAC 地址(如 WLAN 和蓝牙)无法通过第三方 API 使用 会返回 02:00:00:00:00:00,且需要 ACCESS_FINE_LOCATION 或 ACCESS_COARSE_LOCATION 权限


Widevine ID



  • DRM 数字版权管理 ID ,访问此 ID 无需任何权限

  • 对于搭载 Android 8.0 的设备,Widevine 客户端 ID 将为每个应用软件包名称和网络源(对于网络浏览器)返回一个不同的值

  • 可能为空


fun getWidevineID(): String {
try {
val WIDEVINE_UUID = UUID(-0x121074568629b532L, -0x5c37d8232ae2de13L)
val mediaDrm = MediaDrm(WIDEVINE_UUID)
val widevineId = mediaDrm.getPropertyByteArray(MediaDrm.PROPERTY_DEVICE_UNIQUE_ID);
val sb = StringBuilder();
for (byte in widevineId) {
sb.append(String.format("x", byte))
}
return sb.toString();
} catch (e: Exception) {
} catch (e: Error) {
}
return ""
}

AAID



  • 无需任何权限

  • Google 推出的广告 ID ,可由用户重置的标识符,适用于广告用例

  • 系统需要自带 Google Play Services 才支持,且用户可以在系统设置中重置



重置后,在未获得用户明确许可的情况下,新的广告标识符不得与先前的广告标识符或由先前的广告标识符所衍生的数据相关联。




还要注意,Google Play 开发者内容政策要求广告 ID“不得与个人身份信息或任何永久性设备标识符(例如:SSAID、MAC 地址、IMEI 等)相关联。”




在支持多个用户(包括访客用户在内)的 Android 设备上,您的应用可能会在同一设备上获得不同的广告 ID。这些不同的 ID 对应于登录该设备的不同用户。



OAID



  • 无需任何权限

  • 国内移动安全联盟出台的“拯救”国内移动广告的广告跟踪标识符

  • 基本上是国内知名厂商 Android 10+ 才支持,且用户可以在系统设置中重置


UUID



  • 生成之后本地持久化保存

  • 卸载后重新安装、清除应用缓存 会改变


如何选择


同个开发商需要追踪对比旗下应用各用户的行为



  • 可以采用 Android ID(SSAID),并且不同应用需使用同一签名

  • 如果获得的 Android ID(SSAID)为空,可以用 UUID 代替【 OAID / AAID 代替也可,但需要引入第三方库】

  • 在 Android 8.0+ 中, Android ID(SSAID)提供了一个在由同一开发者签名密钥签名的应用之间通用的标识符


希望限制应用内的免费内容(如文章)



  • 可以采用 UUID ,作用域是应用范围,用户要想规避内容限制就必须重新安装应用


用户群体主要是大陆



  • 可以采用 OAID ,低版本配合采用 Android ID(SSAID)/ UUID

  • 可以采用 Android ID(SSAID),空的时候配合采用 UUID 等


用户群体在海外



  • 可以采用 AAID

  • 可以采用 Android ID(SSAID),空的时候配合采用 UUID 等

作者:未央歌
来源:juejin.cn/post/7262558218169008188
收起阅读 »

Android 逆向从入门到入“狱”

免责声明 本次技术分享仅用于逆向技术的交流与学习,请勿用于其他非法用途;技术是把双刃剑,请善用它。 逆向是什么、可以做什么、怎么做 简单讲,就是将别人打包好的 apk 进行反编译,得到源码并分析代码逻辑,最终达成自己的目的。 可以做的事: 修改 smali...
继续阅读 »

免责声明


本次技术分享仅用于逆向技术的交流与学习,请勿用于其他非法用途;技术是把双刃剑,请善用它。


逆向是什么、可以做什么、怎么做



  • 简单讲,就是将别人打包好的 apk 进行反编译,得到源码并分析代码逻辑,最终达成自己的目的。

  • 可以做的事:



    • 修改 smali 文件,使程序达到自己想要的效果,重新编译签名安装,如去广告、自动化操作、电商薅羊毛、单机游戏修改数值、破解付费内容、汉化、抓包等

    • 阅读源码,借鉴别人写好的技术实践

    • 破解:小组件盒子:http://www.coolapk.com/apk/io.ifte…



  • 怎么做:



    • 这是门庞杂的技术活,需要知识的广度、经验、深度

    • 需要具体问题,具体分析,有针对性的学习与探索

    • 了解打包原理、ARM、Smali汇编语言

    • 加固、脱壳

    • Xposed、Substrate、Fridad等框架

    • 加解密

    • 使用好工具## 今日分享涉及工具



  • apktool:反编译工具



    • 反编译:apktool d <apkPath> o <outputPath>

    • 重新打包:apktool b <fileDirPath> -o <apkPath>

    • 安装:brew install apktool



  • jadx:支持命令行和图形界面,支持apk、dex、jar、aar等格式的文件查看




  • apksigner:签名工具




  • Charles:抓包工具



    • http://www.charlesproxy.com/

    • Android 7 以上抓包 HTTPS ,需要手机 Root 后将证书安装到系统中

    • Android 7 以下 HTTPS 直接抓




正题




  • 正向编译



    • java -> class -> dex -> apk



  • 反向编译



    • apk -> dex -> smali -> java



  • Smali 是 Android 的 Dalvik 虚拟机所使用的一种 dex 格式的中间语言

  • 官方文档source.android.com/devices/tec…

  • code.flyleft.cn/posts/ac692…

  • 正题开始,以反编译某瓣App为例:



    • jadx 查看 Java 源码,找到想修改的代码

    • 反编译得到 smali 源码:apktool d douban.apk -o doubancode --only-main-classes

    • 修改:找到 debug 界面入口并打开

    • 将修改后的 smali 源码正向编译成 apk:apktool b doubancode -o douban_mock1.apk

    • 重签名:jarsigner -verbose -keystore keys.jks test.apk key0

    • 此时的包不能正常访问接口,因为豆瓣 API 做了签名校验,而我们的新 apk 是用了新的签名,看接口抓包

    • 怎么办呢?

    • 继续分析代码,修改网络请求中的 apikey

    • 来看看新的 apk



  • 也可以做爬虫等


启发与防范



  • 混淆

  • 加固

  • 加密

  • 运行环境监测

  • 不写敏感信息或操作到客户端

  • App 运行签名验证

  • Api 接口签名验证


One More Thing



作者:Sinyu1012
来源:juejin.cn/post/7202573260659163195
收起阅读 »

前任开发在代码里下毒了,支付下单居然没加幂等

分享是最有效的学习方式。 故事 又是一个风和日丽没好的一天,小猫戴着耳机,安逸地听着音乐,撸着代码,这种没有会议的日子真的是巴适得板。 不料祸从天降,组长火急火燎地跑过来找到了小猫。“快排查一下,目前有A公司用户反馈积分被多扣了”。 小猫回忆了一下“不对啊,...
继续阅读 »

分享是最有效的学习方式。



故事


又是一个风和日丽没好的一天,小猫戴着耳机,安逸地听着音乐,撸着代码,这种没有会议的日子真的是巴适得板。


不料祸从天降,组长火急火燎地跑过来找到了小猫。“快排查一下,目前有A公司用户反馈积分被多扣了”。


小猫回忆了一下“不对啊,这接口我也没动过啊,前几天对外平台的老六直接找我要个支付接口,我就给他了的,以前的代码,我都没有动过的......”。


于是小猫一边疑惑一边翻看着以前的代码,越看脸色越差......


42175B273A64E95B1B5B66D392256552.jpg


小猫做的是一个标准的积分兑换商城,以前和客户合作的时候,客户直接用的是小猫单位自己定制的h5页面。这次合作了一家公司有点特殊,由于公司想要定制化自己个性化的H5,加上本身A公司自己有开发能力,所以经过讨论就以接口的方式直接将相关接口给出去,A客户H5开发完成之后自己来对接。


慢慢地,原因也水落石出,之前好好的业务一直没有问题是因为商城的本身H5页面做了防重复提交,由于量小,并且一般对接方式用的都是纯H5,所以都没有什么问题,然后这次是直接将接口给出去了,完了接口居然没有加幂等......


小猫躺枪,数据订正当然是少不了了,事故报告当然也少不了了。


正所谓前人挖坑,后人遭殃,前人锅后人背。


聊聊幂等


接口幂等梗概


这个案例其实就是一个典型的接口幂等案例。那么老猫就和大家从以下几个方面好好剖析一下接口幂等吧。


interfacemd.png


什么是接口幂等


比较专业的术语:其任意多次执行所产生的影响均与第一次执行的影响相同。
大白话:多次调用的情况下,接口最终得到的结果是一致的。


那么为什么需要幂等呢?



  1. 用户进行提交动作的时候,由于网络波动等原因导致后端同步响应不及时,这样用户就会一直点点点,这样机会发生重复提交的情况。

  2. 分布式系统之间调用的情况下,例如RPC调用,为了防止网络波动超时等造成的请求失败,都会添加重试机制,导致一个请求提交多次。

  3. 分布式系统经常会用到消息中间件,当由于网络原因,mq没有收到ack的情况下,就会导致消息的重复投递,从而就会导致重复提交行为。

  4. 还有就是恶意攻击了,有些业务接口做的比较粗糙,黑客找到漏洞之后会发起重复提交,这样就会导致业务出现问题。打个比方,老猫曾经干过,邻居小孩报名了一个画画比赛,估计是机构培训发起的,功能做的也差,需要靠投票赢得某些礼品,然后老猫抓到接口信息之后就模拟投票进行重复刷了投票。


那么哪些接口需要做幂等呢?


首先我们说是不是所有的接口都需要幂等?是不是加了幂等就好呢?显然不是。
因为接口幂等的实现某种意义上是要消耗系统性能的,我们没有必要针对所有业务接口都加上幂等。


这个其实并不能做一个完全的定义说哪个就不用幂等,因为很多时候其实还是得结合业务逻辑一起看。但是其中也是有规律可循的。


既然我们说幂等就是多次调用,接口最终得到结果一致,那么很显然,查询接口肯定是不要加幂等的,另外一些简单删除数据的接口,无论是逻辑删除还是物理删除,看场景的情况下其实也不用加幂等。


但是大部分涉及到多表更新行为的接口,咱们最好还是得加上幂等。


接口幂等实战方案


前端防抖处理


前端防抖主要可以有两种方案,一种是技术层面的,一种是产品层面的:



  1. 技术层面:例如提交控制在100ms内,同一个用户最多只能做一次订单提交的操作。

  2. 产品层面:当然用户点击提交之后,按钮直接置灰。


基于数据库唯一索引



  1. 利用数据库唯一索引。我们具体来看一下流程,咱们就用小猫遇到的例子。如下:


unique-key.png


过程描述:



  • 建立一张去重表,其中某个字段需要建立唯一索引,例如小猫这个场景中,咱们就可以将订单提交流水单号作为唯一索引存储到我们的数据库中,就模型上而言,可以将其定义为支付请求流水表。

  • 客户端携带相关流水信息到后端,如果发现编号重复,那么此时就会插入失败,报主键冲突的错误,此时我们针对该错误做一下业务报错的二次封装给到客户另一个友好的提示即可。


数据库乐观锁实现


什么是乐观锁,它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。
说得直白一点乐观锁就是一个马大哈。总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,只在更新的时候会判断一下在此期间别人有没有去更新这个数据。


例如提交订单的进行支付扣款的时候,本来可能更新账户金额扣款的动作是这样的:


update Account set balance = balance-#{payAmount} where accountCode = #{accountCode}

加上版本号之后,咱们的代码就是这样的。


update Account set balance = balance-#{payAmount},version=version +1 where accountCode = #{accountCode} and version = #{currVersion}

这种情况下其实就要求客户端每次在请求支付下单的时候都需要上层客户端指定好当前的版本信息。
不过这种幂等的处理方式,老猫用的比较少。


数据库悲观锁实现


悲观锁的话具有强烈的独占和排他特性。大白话谁都不信的主。所以我们就用select ... for update这样的语法进行行锁,当然老猫觉得单纯的select ... for update只能解决同一时刻大并发的幂等,所以要保证单号重试这样非并发的幂等请求还是得去校验当前数据的状态才行。就拿当前的小猫遇到的场景来说,流程如下:


pessimistic.png


begin;  # 1.开始事务
select * from order where order_code='666' for update # 查询订单,判断状态,锁住这条记录
if(status !=处理中){
//非处理中状态,直接返回;
return ;
}
## 处理业务逻辑
update order set status='完成' where order_code='666' # 更新完成
update stock set num = num - 1 where spu='xxx' # 库存更新
commit; # 5.提交事务

这里老猫一再想要强调的是在校验的时候还是得带上本身的业务状态去做校验,select ... for update并非万能幂等。


后端生成token


这个方案的本质其实是引入了令牌桶的机制,当提交订单的时候,前端优先会调用后端接口获取一个token,token是由后端发放的。当然token的生成方式有很多种,例如定时刷新令牌桶,或者定时生成令牌并放到令牌池中,当然目的只有一个就是保住token的唯一性即可。


生成token之后将token放到redis中,当然需要给token设置一个失效时间,超时的token也会被删除。


当后端接收到订单提交的请求的时候,会先判断token在缓存中是否存在,第一次请求的时候,token一定存在,也会正常返回结果,但是第二次携带同一个token的时候被拒绝了。


流程如下:


token.png


有个注意点大家可以思考一下:
如果用户用程序恶意刷单,同一个token发起了多次请求怎么办?
想要实现这个功能,就需要借助分布式锁以及Lua脚本了,分布式锁可以保证同一个token不能有多个请求同时过来访问,lua脚本保证从redis中获取令牌->比对令牌->生成单号->删除令牌这一系列行为的原子性。


分布式锁+状态机(订单状态)


现在很多的业务服务都是分布式系统,所以就拿分布式锁来说,关于分布式锁,老猫在此不做赘述,之前老猫写过redis的分布式锁和实现,还有zk锁和实现,具体可见链接:



当然和上述的数据库悲观锁类似,咱们的分布式锁也只能保证同一个订单在同一时间的处理。其次也是要去校订单的状态,防止其重复支付的,也就是说,只要支付的订单进入后端,都要将原先的订单修改为支付中,防止后续支付中断之后的重复支付。


在上述小猫的流程中还没有涉及到现金补充,如果涉及到现金补充的话,例如对接了微信或者支付宝的情况,还需要根据最终的支付回调结果来最终将订单状态进行流转成支付完成或者是支付失败。


总结


在我们日常的开发中,一些重要的接口还是需要大家谨慎对待,即使是前任开发留下的接口,没有任何改动,当有人咨询的时候,其实就要好好去了解一下里面的实现,看看方案有没有问题,看看技术实现有没有问题,这应该也是每一个程序员的基本素养。


另外的,在一些重要的接口上,尤其是资金相关的接口上,幂等真的是相当的重要。小伙伴们,你们觉得呢?如果大家还有好的解决方案,或者有其他思考或者意见也欢迎大家的留言。


作者:程序员老猫
来源:juejin.cn/post/7324186292297482290
收起阅读 »

Java 中为什么要设计 throws 关键词,是故意的还是不小心

我们平时在写代码的时候经常会遇到这样的一种情况 提示说没有处理xxx异常 然后解决办法可以在外面加上try-catch,就像这样 所以我之前经常这样处理 //重新抛出 RuntimeException public class ThrowsDemo { ...
继续阅读 »

我们平时在写代码的时候经常会遇到这样的一种情况


throws.png


提示说没有处理xxx异常


然后解决办法可以在外面加上try-catch,就像这样


trycatch.png


所以我之前经常这样处理


//重新抛出 RuntimeException
public class ThrowsDemo {

public void demo4throws() {
try {
new ThrowsSample().sample4throws();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

//打印日志
@Slf4j
public class ThrowsDemo {

public void demo4throws() {
try {
new ThrowsSample().sample4throws();
} catch (IOException e) {
log.error("sample4throws", e);
}
}
}

//继续往外抛,但是需要每个方法都添加 throws
public class ThrowsDemo {

public void demo4throws() throws IOException {
new ThrowsSample().sample4throws();
}
}

但是我一直不明白


这个方法为什么不直接帮我做


反而要让我很多余的加上一步


我处理和它处理有什么区别吗?


而且变的好不美观


本来缩进就多,现在加个try-catch更是火上浇油


public class ThrowsDemo {

public void demo4throws() {
try {
if (xxx) {
try {
if (yyy) {

} else {

}
} catch (Throwable e) {
}
} else {

}
} catch (IOException e) {

}
}
}

上面的代码,就算里面没有业务,看起来也已经比较乱了,分不清哪个括号和哪个括号是一对


还有就是对Lambda很不友好


lambda.png


没有办法直接用::来优化代码,所以就变成了下面这样


lambdatry.png


本来看起来很简单很舒服的Lambda,现在又变得又臭又长


为什么会强制 try-catch


为什么我们平时写的方法不需要强制try-catch,而很多jdk中的方法却要呢


那是因为那些方法在方法的定义上添加了throws关键字,并且后面跟的异常不是RuntimeException


一旦你显式的添加了这个关键字在方法上,同时后面跟的异常不是RuntimeException,那么使用这个方法的时候就必须要显示的处理


比如使用try-catch或者是给调用这个方法的方法也添加throws以及对应的异常


throws 是用来干什么的


那么为什么要给方法添加throws关键字呢?


给方法添加throws关键字是为了表明这个方法可能会抛出哪些异常


就像一个风险告知


这样你在看到这个方法的定义的时候就一目了然了:这个方法可能会出现什么异常


为什么 RuntimeException 不强制 try-catch


那为什么RuntimeException不强制try-catch呢?


因为很多的RuntimeException都是因为程序的BUG而产生的


比如我们调用Integer.parseInt("A")会抛出NumberFormatException


当我们的代码中出现了这个异常,那么我们就需要修复这个异常


当我们修复了这个异常之后,就不会再抛出这个异常了,所以try-catch就没有必要了


当然像下面这种代码除外


public boolean isInteger(String s) {
try {
Integer.parseInt(s);
return true;
} catch (NumberFormatException e) {
return false;
}
}

这是我们利用这个异常来达成我们的需求,是有意为之的


而另外一些异常是属于没办法用代码解决的异常,比如IOException


我们在进行网络请求的时候就有可能抛出这类异常


因为网络可能会出现不稳定的情况,而我们对这个情况是无法干预的


所以我们需要提前考虑各种突发情况


强制try-catch相当于间接的保证了程序的健壮性


毕竟我们平时写代码,如果IDE没有提示异常处理,我们完全不会认为这个方法会抛出异常


我的代码怎么可能有问题.gif


我的代码怎么可能有问题!


不可能绝对不可能.gif


看来Java之父完全预判到了程序员的脑回路


throws 和 throw 的区别


java中还有一个关键词throw,和throws只有一个s的差别


throw是用来主动抛出一个异常


public class ThrowsDemo {

public void demo4throws() throws RuntimeException {
throw new RuntimeException();
}
}

两者完全是不同的功能,大家不要弄错了


什么场景用 throws


我们可以发现我们平时写代码的时候其实很少使用throws


因为当我们在开发业务的时候,所有的分支都已经确定了


比如网络请求出现异常的时候,我们常用的方式可能是打印日志,或是进行重试,把异常往外抛等等


所以我们没有那么有必要去使用throws这个关键字来说明异常信息


但是当我们没有办法确定异常要怎么处理的时候呢?


比如我在GitHub上维护了一个功能库,本身没有什么业务属性,主要就是对于一些复杂的功能做了相应的封装,提供给自己或别人使用(如果有兴趣可以看看我的库,顺便给Star,嘿嘿


对我来说,当我的方法中出现异常时,我是不清楚调用这个方法的人是想要怎么处理的


可能有的想要重试,有的想要打印日志,那么我干脆就往外抛,让调用方法的人自己去考虑,自己去处理


所以简单来说,如果方法主要是给别人用的最好用throws把异常往外抛,反之就是可加可不加


结束


很多时候你的不理解只是因为你还不够了解


作者:不够优雅
来源:juejin.cn/post/7204594495996100664
收起阅读 »

面试官:你之前的工作发布过npm包吗?

web
背景🌟 我们公司平时在开发的时候,总是会需要开发一些组件库,去提供给组内其他人通用,这样大大提高了复用性,当然大厂会有自己的组件库,不过学无止境嘛,大家可以根据本文学会如何发布npm包!现在一起来吧~ 01、步骤一注册 打开npm官网,如果没有账号就注册账号...
继续阅读 »

背景🌟


我们公司平时在开发的时候,总是会需要开发一些组件库,去提供给组内其他人通用,这样大大提高了复用性,当然大厂会有自己的组件库,不过学无止境嘛,大家可以根据本文学会如何发布npm包!现在一起来吧~


01、步骤一注册



打开npm官网,如果没有账号就注册账号,如果有就登陆。



02、步骤二创建文件夹



按需求创建一个文件夹,本文以test为例。



03、步骤三初始化package.json文件



进入test文件夹里面,使用cmd打开命令行窗口,在命令行窗口里面输入npm init初始化package.json文件。也可以在Visual Studio Coode的终端里面使用npm init命令初始化。



04、步骤四初始化package.json文件的过程



创建package.json的步骤


01、package name: 设置包名,也就是下载时所使用的的命令,设置需谨慎。


02、version: 设置版本号,如果不设置那就默认版本号。


03、description: 包描述,就是对这个包的概括。


04、entry point: 设置入口文件,如果不设置会默认为index.js文件。


05、test command: 设置测试指令,默认值就是一句不能执行的话,可不设置。


06、git repository: 设置或创建git管理库。


07、keywords: 设置关键字,也可以不设置。


08、author: 设置作者名称,可不设置。


09、license: 备案号,可以不设置。


10、回车即可生成package.json文件,然后还有一行需要输入yes命令就推出窗口。


11、测试package.json文件是否创建成功的命令npm install -g。



05、步骤五创建index.js文件



test文件夹根目录下创建index.js文件,接着就是编写index.js文件了,此处不作详细叙述。



06、步骤六初始化package-lock.json文件



test根目录下使用npm link命令创建package-lock.json文件。



07、步骤七登录npm账号



使用npm login链接npm官网账号,此过程需要输入Username、Password和Email,需要提前准备好。连接成功会输出Logged in as [Username] on registry.npmjs.org/ 这句话,账号不同,输出会有不同。



08、步骤八发布包到npm服务器



执行npm publish命令发布包即可。



09、步骤九下载安装



下载安装使用包,此例的下载命令是npm install mj-calculation --save



10、步骤十更新包



更新包的命npm version patch,更新成功会输出版本号,版本号会自动加一,此更新只针对本地而言。



11、步骤十一发布包到npm服务器



更新包至npm服务器的命令npm publish,成功会输出版本,npm服务器的版本也会更新。



12、步骤十二删除指定版本



删除指定版本npm unpublish mj-calculation@1.0.2,成功会输出删除的版本号,对应服务器也会删除。



13、步骤十三删除包



撤销已发布的包npm unpublish mj-calculation使用的命令。



14、步骤十四强制删除包



强制撤销已发布的包npm unpublish mj-calculation --force使用的命令。



作者:泽南Zn
来源:juejin.cn/post/7287425222365364259
收起阅读 »

面试被问到一个css属性,我却只会向面试官输出js解决方案。。。

web
事情是这样的,好不容易约到个面试,虽然是线下,还是开心得屁颠屁颠跑去面了。刚开始都很正常,面试官首先问一些关于css的问题,我都能对答如流,觉得还好,突然面试官说,有这么个场景:如果我现在有个 canvas 的区域,区域下方(重叠那种上下,不是二维的上下)是一...
继续阅读 »

事情是这样的,好不容易约到个面试,虽然是线下,还是开心得屁颠屁颠跑去面了。刚开始都很正常,面试官首先问一些关于css的问题,我都能对答如流,觉得还好,突然面试官说,有这么个场景:如果我现在有个 canvas 的区域,区域下方(重叠那种上下,不是二维的上下)是一些操作按钮,但是按钮在该区域下方,不能显示出来,怎么能够点击到按钮呢?


听到问题我一开始没反应过来,不是还在疯狂问css吗,怎么突然跳跃到canvas了?于是我就回:不好意思,canvas我不是特别熟。。。,面试官就说不关canvas的事,这样吧,你就把canvas想象成一张普通的图片,怎么能点击到图片下方的按钮呢?


思索片刻,难道面试官在考察我的JS基础了?我就很自信的回答:首先把按钮所在标签放到图片所在标签之下,再把图片的层级(z-index)设置比按钮高一点,这样按钮就被图片挡着不会显示出来,再给按钮和图片所在标签都加上点击事件,此时就可以通过事件冒泡处理按钮的事件了。说完我就非常有把握的看向面试官,以为稳了。但是面试官好像不太满意,于是添加条件说,那如果按钮和图片所在标签不是父子节点关系呢,没有这层关系,你就不能使用事件冒泡了,此时怎么处理?答曰:不知道。。。


回来后赶紧查资料,原来一个css属性就搞定了:pointer-events: none; 这才是面试官想要的答案。


pointer-events是一个CSS属性,它定义了在何种情况下元素可以成为鼠标事件(或触摸事件)的目标。这个属性可以控制元素是否可以被点击、是否可以触发鼠标事件,或者是否应该忽略鼠标事件,让事件传递给下面的元素


使用场景


pointer-events属性主要用于以下几种场景:



  • : 元素不会成为鼠标事件的目标。例如,如果想让一个元素透明对用户的点击,可以将其pointer-events设置为none

  • Auto: 默认值。元素正常响应鼠标事件。

  • VisiblePainted: 元素仅在可见部分响应鼠标事件。

  • 其他值: 还有一些其他值用于SVG元素,如visibleFillvisibleStrokepainted, 等。


示例




以下例子和上述试题很像,把mask当做一张图片,为了方便展示,为其设置了透明度,这样能看到具体按钮位置和展示层级关系。正常情况下点击按钮是不会触发click事件的,因为mask的层级更高,完全遮住了按钮,鼠标只会点击到mask,但若此时为其加上pointer-events: none属性,点击事件会“穿透”该元素并可触发下面元素的事件,即按钮点击事件就可以被触发了!!!


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<style>
.outer{
position: relative;
width: 200px;
height: 200px;
}
.mask{
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 1;
background: rgba(0, 0, 0, .7);
pointer-events: none; /* 重要 */
}
</style>
<body>
<div class="outer">
<div class="mask"></div>
<button id="btn">click</button>
</div>
<script>
const btn = document.getElementById('btn')
btn.addEventListener('click', function(e){
console.log('click');
})
</script>
</body>
</html>

image.png


作者:自驱
来源:juejin.cn/post/7320169906221826048
收起阅读 »

我是如何找到老婆的

本文不聊技术,聊聊我跟我老婆从认识到现在的过程。(现在已经领证了) 我们是2022年过年的时候在网上通过soul认识的,当时是大年初一,我爷爷跟我说,现在过年了,大家回家父母都在催找对象,你也去网上找。 听到这话,我懵了。说的轻巧,网购一个吗,但是我还是打开手...
继续阅读 »

本文不聊技术,聊聊我跟我老婆从认识到现在的过程。(现在已经领证了


我们是2022年过年的时候在网上通过soul认识的,当时是大年初一,我爷爷跟我说,现在过年了,大家回家父母都在催找对象,你也去网上找。


听到这话,我懵了。说的轻巧,网购一个吗,但是我还是打开手机,下载了软件。开始在里面看别人发的帖子,太多了,我也发个帖子,没人理我,哈哈。然后我就加了个湖北的群,我进去做了自我介绍,还是没人理我,我发现群里30多个人,只有几个女的。


好尴尬啊,我兴致勃勃发的一段自我介绍,赫然就出现在群里,就像一件华丽的衣服上面的一个补丁,那么显眼。算了,不管了,我去玩儿了。


过了好一会,我收到了一条消息,是一个小姑娘发来的。看到这里我是有点小意外的,也很惊喜,于是我就收起我在家里的粗犷,很有礼貌地跟她互相自我介绍。通过了解我们才知道,大家都是湖北的,我是十堰市,它是鄂州市,大家都在上海工作,不过因为疫情原因,她今年没有回家。这之后几天我们也互发消息,面对过年满桌的美食,我完全没有大快朵颐的心情,我只想等她的消息,我彷佛感觉她也殷切期盼我的消息。


就这样你一言我一语,殊不知一段姻缘悄咪咪的从这里就开始了,彷佛幂幂之中一切自有定数。。。


年后,我也要回上海工作了,去的第一件事就是去跟心里的这个姑娘见面。我那天特意穿了干净的衣服鞋子,洗了头发,整个人从头到脚都好好捯饬了一番。我们约的是中山公园站,那里是个大商场,下地铁后,我发现地铁口好多,这是个大站,约的是她在一个大的花门那里,其实是商场的入口,我对这里也不熟,急切而激动的我,不知所措,到处乱跑,诺大的地铁站我来回跑了两遍,哈哈。跑了个遍,总算找到了,我远远就看到她了。


大概一米七的个子,她穿着一件白色羽绒服,长长的头发乌黑浓密,像海草一样轻盈,又如瀑布一般美丽。


随着距离靠近,她也看到我了,向我走来,莲步轻移。。。


她的双眸清澈而明亮,宛如两泓清泉,楚楚动人,她没有化妆,却有着白皙透亮的肌肤,就像刚刚剥皮的鸡蛋一样,闪烁着,她没有涂口红,花瓣一样的嘴唇却呈现出粉嫩的淡红色,是那种很自然的颜色。


我们就这样看着,对视着,然后都笑了。


她拉起我的手,我说我们一起去吃饭吧。选的是外婆家,我记得点了个糖醋里脊,还有2个菜,我们边吃边聊,很是愉快。


饭后,我提出一起去坐了摩天轮,门票是200块,但我一点都不觉得贵,反而觉得跟她一起是最浪漫的事。在摩天轮缓缓升起到最高点的时候,我拉着她的手说:“我喜欢你”,她说:“我也喜欢你”,然后我们轻吻了彼此,此刻时间彷佛都静止了,我们觉得整个世界只剩下我们两个。


然后下午我们一起去看了电影,我小心翼翼地征求了她的意见,看的是一个爱情片,此刻的我们想看的就是这种类型。


微信图片_20240111223021


看完电影已经晚上了,她说自己晚上一般不吃饭,出于绅士,我主顶提出送她回家,她也没有拒绝。


接下来几天,我们上班都是一边工作,一边互发消息,我觉得心情愉悦,连空气都是甜的。


然后就到情人节了,我晚上下班直接去她的地方找她,我特意买了玫瑰花,第一次见面没有买,这让我觉得很亏欠。为了方便,我直接在美团上面定的,送到离她最近的一个地铁站,我直接坐地铁去那个地铁站,然后她直接来这里找我,外卖好慢,她来了我们俩等了会,外卖才把玫瑰花送来,是个大妈送的,不过我此时一点没有想责怪她,反而觉得好事多磨,美景常在。然后我们就一起去吃了寿喜烧。


image-20240111230008598


后来上海封城,我们就每天视频聊天,因为她有时候没有菜,我就每天早晨5点起来抢菜,这段时光真的令人难以忘怀。因为我住的是自如的合租房,厨房都满了,我也就懒得做饭,那几天本来按照官方的说法只屯了一个星期的粮食,后来延长封闭期限,我也就弹尽粮绝,每天靠点外卖度日,有时候外卖都点不到。她就花费高价从很远地方点外卖给我吃,我当时真的很感动,以后一定要对她好点,我心里这样想着。


后来上海解封,我们又重回每周约会的日子。中秋节我去她家见了父母,然后国庆节我带我爸去了他家,双方都聊的挺好的。


时间流逝,但我们的点点滴滴,都弥足珍贵。。。。


今年我们决定结婚了,1.5号我们领了结婚证,国庆节准备办婚礼,一切都在往好的地方发展。


世间繁华,唯有你我,相知相守,情深似海。


作者:大数据技术派
来源:juejin.cn/post/7322811509536194594
收起阅读 »

日志脱敏之后,无法根据信息快速定位怎么办?

日志脱敏之殇 小明同学在一家金融公司上班,为了满足安全监管要求,最近天天忙着做日志脱敏。 无意间看到了一篇文章金融用户敏感数据如何优雅地实现脱敏? 感觉写的不错,用起来也很方便。 不过日志脱敏之后,新的问题就诞生了:日志脱敏之后,很多问题无法定位。 比如身-份...
继续阅读 »

日志脱敏之殇


小明同学在一家金融公司上班,为了满足安全监管要求,最近天天忙着做日志脱敏。


无意间看到了一篇文章金融用户敏感数据如何优雅地实现脱敏? 感觉写的不错,用起来也很方便。


不过日志脱敏之后,新的问题就诞生了:日志脱敏之后,很多问题无法定位。


比如身-份-证号日志中看到的是 3****************8,业务方给一个身-份-证号也没法查日志。这可怎么办?


在这里插入图片描述


安全与数据唯一性


类似于数据库中敏感信息的存储,一般都会有一个哈希值,用来定位数据信息,同时保障安全。


那么日志中是否也可以使用类似的方式呢?


说干就干,小明在开源项目 sensitive 基础上,添加了对应的哈希实现。


使用入门


开源地址



github.com/houbb/sensi…



使用方式


1)maven 引入


<dependency>
<groupId>com.github.houbb</groupId>
<artifactId>sensitive-core</artifactId>
<version>1.1.0</version>
</dependency>

2)引导类指定


SensitiveBs.newInstance()
.hash(Hashes.md5())

将哈希策略指定为 md5


3)功能测试


final SensitiveBs sensitiveBs = SensitiveBs.newInstance()
.hash(Hashes.md5());

User sensitiveUser = sensitiveBs.desCopy(user);
String sensitiveJson = sensitiveBs.desJson(user);

Assert.assertEquals(sensitiveStr, sensitiveUser.toString());
Assert.assertEquals(originalStr, user.toString());
Assert.assertEquals(expectJson, sensitiveJson);

可以把如下的对象


User{username='脱敏君', idCard='123456190001011234', password='1234567', email='12345@qq.com', phone='18888888888'}

直接脱敏为:


User{username='脱**|00871641C1724BB717DD01E7E5F7D98A', idCard='123456**********34|1421E4C0F5BF57D3CC557CFC3D667C4E', password='null', email='12******.com|6EAA6A25C8D832B63429C1BEF149109C', phone='1888****888|5425DE6EC14A0722EC09A6C2E72AAE18'}

这样就可以通过明文,获取对应的哈希值,然后搜索日志了。


新的问题


不过小明还是觉得不是很满意,因为有很多系统是已经存在的。


如果全部用注解的方式实现,就会很麻烦,也很难推动。


应该怎么实现呢?


小伙伴们有什么好的思路?欢迎评论区留言


作者:老马啸西风
来源:juejin.cn/post/7239647672460705829
收起阅读 »

尊嘟假嘟?三行代码提升接口性能600倍

一、背景   业务在群里反馈编辑结算单时有些账单明细查不出来,但是新建结算单可以,我第一反应是去测试环境试试有没有该问题,结果发现没任何问题!!!   然后我登录生产环境编辑业务反馈有问题的结算单,发现查询接口直接504网关超时了,此时心里已经猜到是代码性能问...
继续阅读 »

一、背景


  业务在群里反馈编辑结算单时有些账单明细查不出来,但是新建结算单可以,我第一反应是去测试环境试试有没有该问题,结果发现没任何问题!!!
  然后我登录生产环境编辑业务反馈有问题的结算单,发现查询接口直接504网关超时了,此时心里已经猜到是代码性能问题导致的,接来下就把重点放到排查接口超时的问题上了。


二、问题排查


遇到生产问题先查日志是基本操作,登录阿里云的日志平台,可以查到接口耗时竟然高达469245毫秒


这个结算单关联的账单数量也就800多条,所以可以肯定这个接口存在性能问题。


image


但是日志除了接口耗时,并没有其他报错信息或异常信息,看不出哪里导致了接口慢。


接口慢一般是由如下几个原因导致:



  1. 依赖的外部系统慢,比如同步调用外部系统的接口耗时比较久

  2. 处理的数据过多导致

  3. sql性能有问题,存在慢sql

  4. 有大循环存在循环处理的逻辑,如循环读取exel并处理

  5. 网络问题或者依赖的中间件比较慢

  6. 如果使用了锁,也可能由于长时间获取不到锁导致接口超时


当然也可以使用arthas的trace命令分析哪一块比较耗时。


由于安装arthas有点麻烦,就先猜测可能慢sql导致的,然后就登录阿里云RDS查看了慢sql监控日志。
image
好家伙一看吓一跳,sql耗时竟然高达66秒,而且执行次数还挺多!


我赶紧把sql语句放到数据库用explain命令看下执行计划,分析这条sql为啥这么慢。


EXPLAIN SELECT DISTINCT(bill_code) FROM `t_bill_detail_2023_4` WHERE  
(settlement_order_code IS NULL OR settlement_order_code = 'JS23122600000001');

分析结果如下:


image


如果不知道explain结果每个字段的含义,可以看看这篇文章《长达1.7万字的explain关键字指南!》。


可以看到扫描行数达到了250多万行,ref已经是最高效的const,但是看最后的Extra列
Using temporary 表明这个sql用到了临时表,顿时心里清楚什么原因了。


因为sql有个去重关键字DISTINCT,所以mysql在需要建临时表来完成查询结果集的去重操作,如果结果集数据量比较小没有超过buffer,就可以直接在内存中去重,这种效率也是比较高的。


但是如果结果集数据量很大,buffer存不下,那就需要借助磁盘完成去重了,我们都知道操作磁盘相比内存是非常慢的,时间差几个数量级


虽然这个表里的settlement_order_code字段是有索引的,但是线上也有很多settlement_order_code为null的数据,这就导致查出来的结果集非常大,然后又用到临时表,所以sql耗时才这么久!


同时,这里也解释了为什么测试环境没有发现这个问题,因为测试环境的数据不多,直接在内存就完成去重了。


三、问题解决


知道了问题原因就很好解决了,首先根据SQL和接口地址很快就找到出现问题的代码是下图红框圈出来的地方


image


可以看到代码前面有个判断,只有当isThreeOrderQuery=true时才会执行这个查询,判断方法代码如下


image


然后因为这是个编辑场景,前端会把当前结算单号(usedSettlementOrderCode字段)传给后端,所以这个方法就返回了true。


同理,拼接出来的sql就带了条件(settlement_order_code IS NULL OR settlement_order_code = 'JS23122600000001')。
image


解决起来也很简单,把isThreeOrderQuery()方法圈出来的代码去掉就行了,这样就不会执行那个查询,同时也不会影响原有的代码逻辑,因为后面会根据筛选条件再查一次t_bill_detail表。


改代码发布后,再编辑结算单,优化后的效果如下图:


image


只改了三行代码,接口耗时就立马从469245ms缩短到700ms,性能提升了600多倍


四、总结


感觉压测环境还是有必要的,有些问题数据量小了或者请求并发不够都没法暴露出来,同时以后写代码可以提前把sql在数据库explain下看看性能如何,毕竟能跑就行不是我们的追求😏。


作者:2YSP
来源:juejin.cn/post/7322156759443144713
收起阅读 »

不要再滥用可选链运算符(?.)啦!

web
前言 之前整理过 整理下最近做的产品里 比较典型的代码规范问题,里面有一个关于可选链运算符(?.)的规范,当时只是提了一下,今天详细说下想法,欢迎大佬参与讨论。 可选链运算符(?.),大家都很熟悉了,直接看个例子: const result = obj?.a?...
继续阅读 »

前言


之前整理过 整理下最近做的产品里 比较典型的代码规范问题,里面有一个关于可选链运算符(?.)的规范,当时只是提了一下,今天详细说下想法,欢迎大佬参与讨论。


可选链运算符(?.),大家都很熟悉了,直接看个例子:


const result = obj?.a?.b?.c?.d

很简单例子,上面代码?前面的属性如果是空值(null或undefined),则result值是undefined,反之如果都不是空值,则会返回最后一个d属性值。


本文不是讲解这种语法的用法,主要是想分析下日常开发中,这种语法 滥用、乱用 的问题。


滥用、乱用


最近在code review一个公司项目代码,发现代码里用到的可选链运算符,很多滥用,用的很无脑,经常遇到这种代码:


const userName = data?.items?.[0]?.user?.name

↑ 不管对象以及属性有没有可能是空值,无脑加上?.就完了。


// react class component
const name = this.state?.name

// react hooks
const [items, setItems] = useState([])
items?.map(...)
setItems?.([]) // 真有这么写的

↑ React框架下,this.state 值不可能是空值,初始化以及set的值都是数组,都无脑加上?.


const item1 = obj?.item1
console.log(item1.name)

↑ 第一行代码说明obj或item1可能是空值,但第二行也明显说明不可能是空值,否则依然会抛错,第一行的?.也就没意义了。


if (obj?.item1?.item2) {
const item2 = obj?.item1?.item2
const name = obj?.item1?.item2?.name
}

↑ if 里已经判断了非空了,内部就没必要判断非空了。


问题、缺点


如果不考虑 ?. 使用的必要性,无脑滥用其实也没问题,不会影响功能,优点也很多:



  1. 不用考虑是不是非空,每个变量或属性后面加 ?. 就完了。

  2. 由于不用思考,开发效率高。

  3. 不会有空引用错误,不会有页面点点就没反应或弹错问题。


但是问题和缺点也很明显,而且也会很严重。分两点分析下:



  1. 可读性、维护性:给代码维护人员带来了很多分析代码的干扰,代码可读性和维护性都很差。

  2. 隐式过滤了异常:把异常给隐式过滤掉了,导致不能快速定位问题。

  3. 编译后代码冗余。

  4. 护眼:一串?.看着难受,特别是以一个code reviewer 角度看。


1. 可读性、维护性


可读性和维护性其实是一回事,都是指不是源代码作者的开发维护人员,在捋这块代码逻辑、修改bug等情况时,处理问题的效率,代码写的好处理就快,写的烂就处理慢,很简单道理。


const onClick = () => {
const user = props.data?.items?.[0]?.user
if (user) {
// use user to do something
}
}

已这行代码为例,有个bug现象是点击按钮没反应,维护开发看到这块代码,就会想这一串链式属性里,是不是有可能有空值,所以导致了user是空值,没走进if里导致没反应。然后就继续分析上层组件props传输代码,看data值从哪儿传来的,看是不是哪块代码导致data或items空值了。。。


其实呢?从外部传过来的这一串属性里不会有空值的情况,导致bug问题根本不在这儿。


const user = props.data.items[0].user

那把?.都去掉呢?维护开发追踪问题看到这行代码,data items 这些属性肯定不能是空值,不然console就抛错了,但是bug现象里并没有抛错,所以只需要检查user能不能是空值就行了,很容易就排除了很多情况。


总结就是:给代码维护人员带来了很多分析代码的干扰,代码可读性和维护性都很差。


2. 隐式过滤了异常


api.get(...).then(result => {
const id = result?.id
// use id to do something
})

比如有个需求,从后台api获取数据时,需要把结果里id属性获取到,然后进行数据处理,从业务流程上看,这个api返回的result以及id必须有值,如果没值的话后续的流程就会走不通。


然后后台逻辑由于写的有问题,导致个别情况返回的 result=null,但是由于前端这里加了?.,导致页面没有任何反应,js不抛错,console也没有log,后续流程出错了,这时候如果想找原因就会很困难,对代码熟悉还行,如果不是自己写的就只能看代码捋逻辑,如果是生产环境压缩混淆了就更难排查了。


api.get(...).then(result => {
const id = result.id
// use id to do something
})

?.去掉呢?如果api返回值有问题,这里会立即抛错,后面的流程也就不能进行下去了,无论开发还是生产环境都能在console里快速定位问题,即使是压缩混淆的也能从error看出一二,或者在一些前端监控程序里也能监听到。


其实这种现象跟 try catch 里不加 throw 类似,把隐式异常错误完全给过滤掉了,比如下面例子:


// 这个try本意是处理api请求异常
try {
const data = getSaveData() // 这段js逻辑也在try里,所以如果这个方法内部抛错了,页面上就没任何反应,很难追踪问题
const result = await api.post(url, data)
// result 逻辑处理
} catch (e) {
// 好点的给弹个框,打个log,甚至有的啥都不处理
}

总结就是:把异常给隐式过滤掉了,导致不能快速定位问题。


3. 编译后代码冗余


如果代码是ts,并且编译目标是ES2016,编译后代码会很长。可以看下 http://www.typescriptlang.org/play 效果。


image.png


Babel在个别stage下,编译效果一样。


image.png


但并不是说一点都不用,意思是尽量减少滥用,这样使用的频率会少很多,这种编译代码沉余也会少不少。


应该怎么用?


说了这么多,.? 应该怎么用呢?意思是不用吗?当然不是不能用,这个特性对于开发肯定好处很多的,但是得合理用,不能滥用。



  1. 避免盲目用,滥用,有个点儿就加问号,特别是在一个比较长的链式代码里每个属性后面都加。

  2. 只有可能是空值,而且业务逻辑中有空值的情况,就用;其它情况尽量不要用。


其实说白了就是:什么时候需要判断一个变量或属性非空,什么时候不需要。首先在使用的时候得想下,问号前面的变量或属性值,有没有可能是空值:



  1. 很明显不可能是空值,比如 React类组件里的 this.state this.props,不要用;

  2. 自己定义的变量或属性,而且没有赋值为空值情况,不要用;

  3. 某些方法或者组件里,参数和属性不允许是空值,那方法和组件里就不需要判断非空。(对于比较common的,推荐写断言,或者判断空值情况throw error)

  4. 后台api请求结果里,要求result或其内部属性必须有值,那这些值就不需要判断非空。

  5. 按正常流程走,某个数据不会有空值情况,如果是空值说明前面的流程出问题了,这种情况就不需要在逻辑里判断非空。


const userName = data?.items?.[0]?.user?.name // 不要滥用,如果某个属性有可能是空值,则需要?.
const userName = data.items[0].user?.name // 比如data.items数组肯定不是空数组

const items2 = items1.filter(item => item.checked)
if (items2?.length) { } // 不需要?.

// react class component
const name = this.state?.name // 不需要?.

// react hooks
const [items, setItems] = useState([])
items?.map(...) // 如果setItems没有赋值空值情况,则不需要?.
setItems?.([]) // 不需要?.

const item1 = obj?.item1 // 不需要?.
console.log(item1.name)

const id = obj?.id // 下面代码已经说明不能是空值了,不需要?.
const name = obj.name

if (obj?.item1?.item2) {
const item2 = obj?.item1?.item2 // 不需要?.
const name = obj?.item1?.item2?.name // 不需要?.
}

const id = obj?.item?.id // 不需要?.
api.get(id).then(...) // 这个api如果id是空值,则api会抛错

当然,写代码时还得多想一下属性是否可能是空值,会一定程度的影响开发效率,也一定有开发会觉得很烦,不理解,无脑写?.多容易啊,但是我从另外两个角度分析下:



  1. 我觉得一个合格的开发应该对自己的代码逻辑很熟悉,应该有责任知道哪些值可能是空值,哪些不可能是空值(并不是说所有,也有大部分了),否则就是对自己的代码了解很少,觉得代码能跑就行,代码质量自然就低。

  2. 想想在这个新特性出来之前大家是怎么写的,会对每个变量和属性都加if非空判断或者用逻辑与(&&)吗?不会吧。


总结


本文以一个 code reviewer 角度,分析了 可选链运算符(?.) 特性的滥用情况,以及“正确使用方式”,只是代表我本人的看法,欢迎大佬参与讨论,无条件接受任何反驳。


滥用的缺点:



  1. 可读性、维护性:给代码维护人员带来了很多分析代码的干扰,代码可读性和维护性都很差。

  2. 隐式过滤了异常:把异常给隐式过滤掉了,导致不能快速定位问题。

  3. 编译后代码冗余。

  4. 护眼:一串?.看着难受,特别是以一个code reviewer 角度看。


“正确用法”:



  1. 避免盲目用,滥用,有个点儿就加问号,特别是在一个比较长的链式代码里每个属性后面都加。

  2. 只有可能是空值,而且业务逻辑中有空值的情况,就用;其它情况尽量不要用。




后记(09月25日更新)


从评论上看,对于可选链的看法,大多声音是能加就加,多加总比少加好,原因就是不想背锅,不想上线后JS动不动就崩了,无论根本原因是不是前端开发没加判断导致的,第一责任人就会找到你,有的甚至会被上级追责,问题就更严重了,而且很难解释清楚;另一方面就是为了赶工期,可选链的其中一个优点就是简单,提高开发效率。


我再从几个方面浅浅的扩展下我的看法,欢迎参与讨论


总之。。。对对对,你们说的都对!


作者:Mark大熊
来源:juejin.cn/post/7280747572707999799
收起阅读 »

对于Android开发,Jetpack Compose真的要开始学起来了?

Jetpack Compose 是个啥?为啥要学它? 谷歌对 Jetpack Compose 的定义: Jetpack Compose 是推荐用于构建原生 Android 界面的新工具包。它可简化并加快 Android 上的界面开发,使用更少的代码、强大的工...
继续阅读 »

Jetpack Compose 是个啥?为啥要学它?


谷歌对 Jetpack Compose 的定义:



Jetpack Compose 是推荐用于构建原生 Android 界面的新工具包。它可简化并加快 Android 上的界面开发,使用更少的代码、强大的工具和直观的 Kotlin API,快速打造生动而精彩的应用。



提取关键词:界面开发新工具包、简化并加快界面开发、Kotlin API


对于大部分Android项目来说,如果基础库(如网络库、hybird、图片加载、热修复库等)已经搭好,那么平时大部分时间就是跟 UI界面、需求逻辑 打交道了,而谷歌提供的 Jetpack Compose 正好是加快界面开发的工具包
对比



就跟魂斗罗里的子弹类型似的,使用普通子弹(XML方式)也可以通关,但是相比之下耗时更长;而换成超级子弹(Jetpack Compose)体验就不一样了,耗时更少,而且游戏体验更爽!



命令式UI vs 声明式UI


长期以来,Android 视图层次结构一直可以表示为界面 widget 树。最常见的界面更新方式是使用 findViewById() 等函数遍历树,并通过调用 button.setText(String)、container.addChild(View) 或 img.setImageBitmap(Bitmap) 等方法更改节点。这些方法会改变 widget 的内部状态,这种手动更新UI的方式即是命令式UI


在过去的几年中,整个行业已开始转向声明性界面模型,该模型大大简化了与构建和更新界面关联的工程任务。该技术的工作原理是在概念上从头开始重新生成整个屏幕,然后仅执行必要的更改。此方法可避免手动更新有状态视图层次结构的复杂性。Compose 即是一个声明式UI框架。


Jetpack Compose要学起来了?


很遗憾,Jetpack Compose 确实要学起来了(快起来,你还能学!哈哈...),随着Jetpack Compose 版本的不断迭代,API 逐渐稳定了,性能也越来越好了。


优点



  • 更少的代码:编写代码只需要采用Kotlin,而不用拆分成 Kotlin + XML方式了。

  • 直观:只需描述界面,Compose会负责处理剩余工作。应用状态变化时,界面自动更新。

  • 加速开发View 与 Compose 之间可以相互调用,兼容现有的所有代码。借助AS可以实时预览界面,轻松执行界面检查。

  • 功能强大:直接访问Android API,内置对Material Design、主题、动画等的支持。


Jetpack Compose vs Flutter




  • Jetpack Compose的目的是为了提高 Android 原生的 UI 开发效率!声明式UI已经成为主流的开发方式了,就像当初谷歌将Kotlin定为Android主流语言时我们学习Kotlin一样,未来Jetpack Compose 一定会是Android UI开发的主流方式。

  • Flutter 的定位是多平台 UI 框架,优势在于跨平台。



大家很喜欢把Jetpack Compose 和 Flutter作对比,不知道该学哪一个?的确,某些场景下它们确实挺像的,而且还都是谷歌在推的。


个人理解是:如果你未来的主攻方向还是Android,那么无脑选择Jetpack Compose,虽然Compose目前也能实现跨端,但跨端目前看并不是它的主要工作;而如果你的方向是多平台开发,那么学习Flutter是首选吧


另外,与其一直纠结学哪一个,不如直接上手亲身感受下它们的不同,正所谓 “纸上得来终觉浅,绝知此事要躬行”。


Jetpack Compose入门


class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Text("Hello World!")
}
}
}

其中,setContent()传入一个@Composable作用域,其作用跟之前的setContentView()一样用来设置界面。Text()用来描述一个UI元素,里面有各种参数,这里我们只把文案填上去,执行结果:


hello world


一个最简单的功能就完成了。


1、@Composable 可组合函数


还是上述展示一个文本的功能,我们换一种写法:


class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MessageCard("Android")
}
}
}

@Composable
fun MessageCard(name: String) {
Text(text = "Hello $name!")
}

执行效果跟上面一样。唯一的区别就是把文本展示单独抽离到一个方法中了,并且该方法上面加了@Composable 注解



@Composable注解用于标记一个函数为可组合函数。可组合函数是一种特殊的函数,不需要返回任何UI元素,因为可组合函数描述的是所需的屏幕状态,而不是构造界面widget;而如果按我们以前的XML编程方式,必须在方法中返回UI元素才能使用它(如返回View类型)。



@Composable注解的函数之间可以相互调用,因为这样Compose框架才能正确处理依赖关系。另外,@Composable函数中也可以调用普通函数,而普通函数中却不能直接调用@Composable函数。 这里可以类比下kotlin中suspend挂起函数的用法,其用法是相似的


几个定义:



  • 组合:对 Jetpack Compose 在执行可组合项时所构建界面的描述。

  • 初始组合:通过首次运行可组合项创建组合。

  • 重组在数据发生变化时重新运行可组合项以更新组合


可组合函数的特点:



  • 使用同一参数多次调用此函数时,它的行为方式相同,并且它不使用其他值,如全局变量或对 random() 的调用。

  • 此函数描述界面而没有任何副作用,如修改属性或全局变量、点击事件的处理等。当需要执行附带效应时,应通过回调触发。如:


@Composable
private fun NamePickerItem(name: String, onClicked: (String) -> Unit) {
Text(name, Modifier.clickable(onClick = { onClicked(name) }))
}

可见通过回调,点击事件这个附带效应是在调用方触发的。


在编译时,Jetpack Compose会将标记为@Composable的函数编译成字节码,并生成一个专门的ComposeNode类来管理其状态和属性。这个类会自动处理依赖关系,并在需要时计算UI元素。这样,开发者就可以专注于编写UI逻辑,而不用担心状态管理和UI更新的细节


这里引出一个问题,Compose 是如何做 UI 更新的呢?总不能每次有一小部分数据的变化,整个UI都要跟着刷新一次吧,那性能肯定差的要死。其实,当有数据变化时,Compose实现的是增量更新,只会重新绘制数据有改动的UI(该过程称为重组),数据没有改动的则不会重新绘制了


2、布局基础知识


布局
Compose 通过元素组合、布局、绘制之后可以将状态转换为UI元素。


组合

在 Compose 中,可以通过从可组合函数中调用其他可组合函数来构建界面层次结构。
基础布局
如图所示:



  • Column :可以将多个项垂直地放置在屏幕上;

  • Row :可以将多个项水平地放置在屏幕上;

  • Box :可将元素放在其他元素上,还支持为其包含的元素配置特定的对齐方式。


排列及对齐方式:


/**
* @param verticalArrangement 竖直排列方式
* @param horizontalAlignment 水平对齐方式
*/

inline fun Column(
modifier: Modifier = Modifier,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
content: @Composable ColumnScope.() -> Unit
)
{...}

/**
* @param horizontalArrangement 水平排列方式
* @param verticalAlignment 竖直对齐方式
*/

@Composable
inline fun Row(
modifier: Modifier = Modifier,
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
verticalAlignment: Alignment.Vertical = Alignment.Top,
content: @Composable RowScope.() -> Unit
)
{...}

/**
* @param contentAlignment 内容对齐方式
*/

@Composable
inline fun Box(
modifier: Modifier = Modifier,
contentAlignment: Alignment = Alignment.TopStart,
propagateMinConstraints: Boolean = false,
content: @Composable BoxScope.() -> Unit
)
{...}

verticalArrangement、horizontalArrangement 排列方式及效果:
排列方式


布局

界面树布局通过单次传递即可完成。父节点会在其子节点之前进行测量,但会在其子节点的尺寸和放置位置确定之后再对自身进行调整
Layout流程
当界面树较深时,Compose 可以通过只测量一次子项来实现高性能。


3、Modifier修饰符


可以通过Modifier修饰符更改可组合项的大小、布局、外观,还可以添加高级互动,例如使元素可点击等。如:


  Image(
painter = painterResource(id = R.mipmap.icon_water_melon),
contentDescription = "",
modifier = Modifier
.size(50.dp)
.clip(CircleShape)
.border(width = 2.dp, Color.Red, CircleShape)
)

执行结果:Modifier,原图本身是长方形的,通过Modifier修饰符修饰后,很容易变成圆角图片。想一下如果用XML方式来写,是不是要写好多代码呢。


4、存储状态


可组合函数中可以使用 remember 将本地状态存储在内存中,并跟踪传递给 mutableStateOf 的值的变化。该值更新时,系统会自动重新绘制使用此状态的可组合项(及其子项),这也是上面所说的重组。如:


@Composable
fun MessageCard(msg: Message) {
// We keep track if the message is expanded or not in this variable
var isExpanded by remember { mutableStateOf(false) }
// We toggle the isExpanded variable when we click on this Column
Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) {...}
}

当点击Column 元素时,每次都会重新执行MessageCard()可组合函数进行刷新,而通过remember和mutableStateOf可以保存了上次的isExpanded状态;如果不使用它们,重新执行 MessageCard() 时 isExpanded 也会重新初始化。


除了remember之外,还有rememberSaveable、savedStateHandle.saveable等。。。


总结


这篇文章主要讲了Compose是什么以及我们要开始学习它的必要性。作为Compose 第一篇介绍文章,本文旨在初步感受一下 Compose的能力,后续再详细研究 Compose 的精彩用法!


资料


【1】谷歌Jetpack Compose 教程
https://developer.android.com/jetpack/compose/tutorial?hl=zh-cn


作者:_小马快跑_
来源:juejin.cn/post/7271832299340202036
收起阅读 »

面试官:能不能给 Promise 增加取消功能和进度通知功能... 我:???

web
扯皮 这段时间闲着没事就去翻翻红宝书,已经看到 Promise 篇了,今天又让我翻到两个陌生的知识点。 因为 Promise 业务场景太多了自我感觉掌握的也比较透彻,之前也跟着 Promise A+ 的规范手写过完整的 Promise,所以这部分内容基本上就大...
继续阅读 »

扯皮


这段时间闲着没事就去翻翻红宝书,已经看到 Promise 篇了,今天又让我翻到两个陌生的知识点。


因为 Promise 业务场景太多了自我感觉掌握的也比较透彻,之前也跟着 Promise A+ 的规范手写过完整的 Promise,所以这部分内容基本上就大致过一遍,直到看见关于 Promise 的取消以及监听进度...🤔


只能说以后要是我当上面试官一定让候选人来谈谈这两个点,然后顺势安利我这篇文章🤣


不过好像目前为止也没见哪个面试官出过...


正文


取消功能


我们都知道 Promise 的状态是不可逆的,也就是说只能从 pending -> fulfilled 或 pending -> rejected,这一点是毋庸置疑的。


但现在可能会有这样的需求,在状态转换过程当中我们可能不再想让它进行下去了,也就是说让它永远停留至 pending 状态


奇怪了,想要一直停留在 pending,那我不调用 resolve 和 reject 不就行了🤔


 const p = new Promise((resolve, reject) => {
setTimeout(() => {
// handler data, no resolve and reject
}, 1000);
});
console.log(p); // Promise {<pending>} 💡

但注意我们的需求条件,是在状态转换过程中,也就是说必须有调用 resolve 和 reject,只不过中间可能由于某种条件,阻止了这两个调用。


其实这个场景和超时中断有点类似但还是不太一样,我们先利用 Promise.race 来看看:模拟一个发送请求,如果超时则提示超时错误:


const getData = () =>
new Promise((resolve) => {
setTimeout(() => {
console.log("发送网络请求获取数据"); // ❗
resolve("success get Data");
}, 2500);
});

const timer = () =>
new Promise((_, reject) => {
setTimeout(() => {
reject("timeout");
}, 2000);
});

const p = Promise.race([getData(), timer()])
.then((res) => {
console.log("获取数据:", res);
})
.catch((err) => {
console.log("超时: ", err);
});

问题是现在确实能够确认超时了,但 race 的本质是内部会遍历传入的 promise 数组对它们的结果进行判断,那好像并没有实现网络请求的中断哎🤔,即使超时网络请求还会发出:


超时中断.png


而我们想要实现的取消功能是希望不借助 race 等其他方法并且不发送请求。


比如让用户进行控制,一个按钮用来表示发送请求,一个按钮表示取消,来中断 promise 的流程:



当然这里我们不讨论关于请求的取消操作,重点在 Promise 上



取消请求.png


其实按照我们的理解只用 Promise 是不可能实现这样的效果的,因为从一开始接触 Promise 就知道一旦调用了 resolve/reject 就代表着要进行状态转换。不过 取消 这两个字相信一定不会陌生,clearTimeoutclearInterval 嘛。


OK,如果你想到了这一点这个功能就出来了,我们直接先来看红宝书上给出的答案:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<button id="send">Send</button>
<button id="cancel">Cancel</button>

<script>
class CancelToken {
constructor(cancelFn) {
this.promise = new Promise((resolve, reject) => {
cancelFn(() => {
console.log("delay cancelled");
resolve();
});
});
}
}
const sendButton = document.querySelector("#send");
const cancelButton = document.querySelector("#cancel");

function cancellableDelayedResolve(delay) {
console.log("prepare send request");
return new Promise((resolve, reject) => {
const id = setTimeout(() => {
console.log("ajax get data");
resolve();
}, delay);

const cancelToken = new CancelToken((cancelCallback) =>
cancelButton.addEventListener("click", cancelCallback)
);
cancelToken.promise.then(() => clearTimeout(id));
});
}
sendButton.addEventListener("click", () => cancellableDelayedResolve(1000));
</script>
</body>
</html>

这段代码说实话是有一点绕的,而且个人觉得是有多余的地方,我们一点一点来看:


首先针对于 sendButton 的事件处理函数,这里传入了一个 delay,可以把它理解为取消功能期限,超过期限就要真的发送请求了。我们看该处理函数内部返回了一个 Promise,而 Promise 的 executor 中首先开启了定时器,并且实例化了一个 CancelToken,而在 CancelToken 中才给 cancelButton 添加点击事件。


这里的 CancelToken 就是我觉得最奇怪的地方,可能没有体会到这个封装的技巧,路过的大佬如果有理解的希望能帮忙解释一下。它的内部创建了一个 Promise,绕了一圈后相当于 cancelButton 的点击处理函数是调用这个 Promise 的 resolve,最终是在其 pending -> fuilfilled,即 then 方法里才去取消定时器,那为什么不直接在事件处理函数中取消呢?难道是为了不影响主执行栈的执行所以才将其推到微任务处理🤔?


介于自己没理解,我就按照自己的思路封装个不一样的🤣:


const sendButton = document.querySelector("#send");
const cancelButton = document.querySelector("#cancel");

class CancelPromise {

// delay: 取消功能期限 request:获取数据请求(必须返回 promise)
constructor(delay, request) {
this.req = request;
this.delay = delay;
this.timer = null;
}

delayResolve() {
return new Promise((resolve, reject) => {
console.log("prepare request");
this.timer = setTimeout(() => {
console.log("send request");
this.timer = null;
this.req().then(
(res) => resolve(res),
(err) => reject(err)
);
}, this.delay);
});
}

cancelResolve() {
console.log("cancel promise");
this.timer && clearTimeout(this.timer);
}
}

// 模拟网络请求
function getData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("this is data");
}, 2000);
});
}

const cp = new CancelPromise(1000, getData);

sendButton.addEventListener("click", () =>
cp.delayResolve().then((res) => {
console.log("拿到数据:", res);
})
);
cancelButton.addEventListener("click", () => cp.cancelResolve());

正常发送请求获取数据:


发送请求.gif


中断 promise:


取消请求.gif


没啥大毛病捏~


进度通知功能


进度通知?那不就是类似发布订阅嘛?还真是,我们来看红宝书针对这块的描述:



执行中的 Promise 可能会有不少离散的“阶段”,在最终解决之前必须依次经过。某些情况下,监控 Promise 的执行进度会很有用



这个需求就比较明确了,我们直接来看红宝书的实现吧,核心思想就是扩展之前的 Promise,为其添加 notify 方法作为监听,并且在 executor 中增加额外的参数来让用户进行通知操作:


class TrackablePromise extends Promise {
constructor(executor) {
const notifyHandlers = [];
super((resolve, reject) => {
return executor(resolve, reject, (status) => {
notifyHandlers.map((handler) => handler(status));
});
});
this.notifyHandlers = notifyHandlers;
}
notify(notifyHandler) {
this.notifyHandlers.push(notifyHandler);
return this;
}
}
let p = new TrackablePromise((resolve, reject, notify) => {
function countdown(x) {
if (x > 0) {
notify(`${20 * x}% remaining`);
setTimeout(() => countdown(x - 1), 1000);
} else {
resolve();
}
}
countdown(5);
});

p.notify((x) => setTimeout(console.log, 0, "progress:", x));
p.then(() => setTimeout(console.log, 0, "completed"));


emm 就是这个例子总感觉不太好,为了演示这种效果还用了递归,大伙们觉得呢?


不好就自己再写一个🤣!不过这次的实现就没有多大问题了,基本功能都具备也没有什么阅读障碍,我们再添加一个稍微带点实际场景的例子吧:



// 模拟数据请求
function getData(timer, value) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(value);
}, timer);
});
}

let p = new TrackablePromise(async (resolve, reject, notify) => {
try {
const res1 = await getData1();
notify("已获取到一阶段数据");
const res2 = await getData2();
notify("已获取到二阶段数据");
const res3 = await getData3();
notify("已获取到三阶段数据");
resolve([res1, res2, res3]);
} catch (error) {
notify("出错!");
reject(error);
}
});

p.notify((x) => console.log(x));
p.then((res) => console.log("Get All Data:", res));


notify获取数据.gif


对味儿了~😀


End


关于取消功能在红宝书上 TC39 委员会也曾准备增加这个特性,但相关提案最终被撤回了。结果 ES6 Promise 被认为是“激进的”:只要 Promise 的逻辑开始执行,就没有办法阻止它执行到完成。


实际上我们学了这么久的 Promise 也默认了这一点,因此这个取消功能反而就不太符合常理,而且十分鸡肋。比如说我们有使用 then 回调接收数据,但因为你点击了取消按钮造成 then 回调不执行,我们知道 Promise 支持链式调用,那如果还有后续操作都将会被中断,这种中断行为 debug 时也十分痛苦,更何况最麻烦的一点是你还需要传入一个 delay 来表示取消的期限,而这个期限到底要设置多少才合适呢...


至于说进度通知功能,仁者见仁智者见智吧...


但不管怎么样两个功能实现的思路都是比较有趣的,而且不太常见,不考虑实用性确实能够成为一道考题,只能说很符合面试官的口味😏


作者:討厭吃香菜
来源:juejin.cn/post/7312349904046735400
收起阅读 »

用脚本来写函数式弹窗,更快更爽

web
前言 在业务开发中,弹窗是我们常常能够遇到的开发需求,通常我们使用组件都是通过show变量来控制弹窗的开启或者隐藏。这样面对简单的业务需求的确可以满足了,但是面对深层嵌套,不同地方打开同一个弹窗便会让我们头疼。于是我想通过函数的方式来打开弹窗,通过函数参数的方...
继续阅读 »

前言


在业务开发中,弹窗是我们常常能够遇到的开发需求,通常我们使用组件都是通过show变量来控制弹窗的开启或者隐藏。这样面对简单的业务需求的确可以满足了,但是面对深层嵌套,不同地方打开同一个弹窗便会让我们头疼。于是我想通过函数的方式来打开弹窗,通过函数参数的方式来向弹窗内更新props和处理弹窗的emit事件。


iShot_2023-08-15_10.13.24.gif


<template>
<n-button @click="open">open</n-button>
</template>

<script setup lang="ts">
import { injectTestModal } from "./test-modal/use-test-modal"
const model = injectTestModal()
function open() {
model?.open({
testValue: "传给弹窗组件的值",
onConfirm({ formData }, close, endLoading) {
console.log({ ...formData })
endLoading()
close()
},
})
}
</script>


传统vue编写弹窗


通过变量来直接控制弹窗的开启和关闭。


<template>
<n-button @click="showModal = true">
来吧
</n-button>

<n-modal v-model:show="showModal" preset="dialog" title="Dialog">
<template #header>
<div>标题</div>
</template>
<div>内容</div>
<template #action>
<div>操作</div>
</template>
</n-modal>

</template>

<script lang="ts">
import { defineComponent, ref } from 'vue'

export default defineComponent({
setup () {
return {
showModal: ref(false)
}
}
})
</script>


痛点



  • 深层次的传props让人有很大的心理负担,污染组件props

  • 要关注弹窗show变量的true,false


函数式弹窗


在主页面用Provider包裹一下


// RootPage.vue
<ModalProvider>
<ChildPage></ChildPage>
</ModalProvider>

<script setup lang="ts">
import ModalProvider from "./ModalProvider.vue"
import ChildPage from "./ChildPage.vue"
</script>


在页面内的某个子组件中,直接通过oepn方法打开弹窗


// ChidPage.vue
<template>
<n-button @click="open">open</n-button>
</template>

<script setup lang="ts">
import { injectTestModal } from "./test-modal/use-test-modal"
const model = injectTestModal()
function open() {
model?.open({
testValue: "传给弹窗组件的值",
onConfirm({ formData }, close, endLoading) {
console.log({ ...formData })
endLoading()
close()
},
})
}
</script>


优势



  • 对于使用者来说简单,没有控制show的心理负担

  • 弹窗内容和其他业务代码分离,不会污染其他组件props和结构

  • 能够充分复用同一个弹窗,通过函数参数的方式来更新弹窗内容,在不同地方打开同一个弹窗


劣势



  • 对于一些简单需求的弹窗,用这种函数式的弹窗会有些臃肿

  • 在使用函数式弹窗前需要做一些操作(Provider和Inject),会在后面提到以及解决方案。


如何使用这种函数式的弹窗


原理


通过在根页面将我们的Modal组件挂载上去,然后通过使用hook去管理这个modal组件的状态值(props,ref),然后通过provide将props和event和我们的modal组件联系起来,再在我们需要使用弹窗的地方使用inject更新props来启动弹窗。


步骤1(❌):编写Modal


这里我使用的是Naive Ui的Modal组件,按喜好选择就行。按照你的需求编写弹窗的内容。定义好props和emits,写好他们的类型申明。


// TestModal.vue
<template>
<n-modal
v-model:show="isShowModal"
preset="dialog"
@after-leave="handleClose"
>

...你的弹窗内容
</n-modal>

</template>

<script setup lang="ts">
import { ref, reactive, computed, watch } from "vue"
import { useVModel } from "@vueuse/core"
import { FormRules } from "naive-ui"
interface ModalProps {
show: boolean
testValue: string
}

// 中间区域不要修改
const props = defineProps<ModalProps>()
const formRef = ref()
const loading = ref(false)
const isShowModal = useVModel(props, "show")
// 中间区域不要修改

const rules: FormRules = []

const formData = reactive({
testValue: props.testValue,
})

const callBackData = computed(() => {
return {
formData,
}
})

watch(
() => props.show,
() => {
if (props.show) {
formData.testValue = props.testValue
} else {
formData.testValue = ""
}
}
)

const emits = defineEmits<{
(e: "update:show", value: boolean): void
(e: "close", param: typeof callBackData.value): void
(
e: "confirm",
param: typeof callBackData.value,
close: () => void,
endLoading: () => void
): void
(e: "cancel", param: typeof callBackData.value): void
}>()
function handleCancel() {
// 中间区域不要修改
emits("cancel", callBackData.value)
isShowModal.value = false
// 中间区域不要修改
}

function handleClose() {
// 中间区域不要修改
emits("close", callBackData.value)
isShowModal.value = false
// 中间区域不要修改
}

function handleConfirm() {
// 中间区域不要修改
loading.value = true
emits(
"confirm",
callBackData.value,
() => {
loading.value = false
isShowModal.value = false
},
() => {
loading.value = false
}
)
// 中间区域不要修改
}
</script>

步骤2(❌):编写hook来管理弹窗的状态


在这个文件里面,使用hook管理 TestModal 弹窗需要的props和event事件,然后我们通过ts类型体操来获取TestModal的props类型和event类型,我们向外inject一个 open 函数,这个函数可以更新 TestModal 的props和event,同时打开弹窗,他的参数有完整的类型提示,可以让使用者更加明确的使用我们的弹窗。


// use-test-modal.ts
import {
ref,
provide,
InjectionKey,
inject,
VNodeProps,
AllowedComponentProps,
reactive,
} from "vue";
import Modal from "./TestModal.vue";

/**
* 通过引入弹窗组件来获取组件的除show,update:show以外的props和emits来作为open函数的
*/

type ModalInstance = InstanceType<
typeof Modal extends abstract new (...args: any) => any ? typeof Modal : any
>["$props"];
type OpenParam = Omit<
{
readonly [K in keyof Omit<
ModalInstance,
keyof VNodeProps | keyof AllowedComponentProps
>]: ModalInstance[K];
},
"show" | "onUpdate:show"
>;

interface AnyFileChangeModal {
open: (param?: OpenParam) => Promise<void>;
}

/**
* 通过弹窗实例来获取弹窗组件内需要哪些props
*/

type AllProps = Omit<
OpenParam,
"onClose" | "onCancel" | "onConfirm" | "onUpdate:show"
> & { show: boolean };
const anyModalKey: InjectionKey<AnyFileChangeModal> = Symbol("ModalKey");
export function provideTestModal() {
const allProps: AllProps = reactive({
show: false,
} as AllProps);
const closeCallback = ref();
const cancelCallback = ref();
const confirmCallback = ref();
const handleUpdateShow = (value: boolean) => {
allProps.show = value;
};

/**
* @param param 通过函数来更新props
*/

function updateAllProps(param: OpenParam) {
const excludeKey = ["show", "onClose", "onConfirm", "onCancel"];
for (const [key, value] of Object.entries(param)) {
if (!excludeKey.includes(key)) {
allProps[key] = value;
}
}
}
function clearAllProps() {
for (const [key] of Object.entries(allProps)) {
allProps[key] = undefined;
}
}

async function open(param: OpenParam) {
clearAllProps();
updateAllProps(param);
allProps.show = true;
param.onClose && (closeCallback.value = param.onClose);
param.onConfirm && (confirmCallback.value = param.onConfirm);
param.onCancel && (cancelCallback.value = param.onCancel);
}
provide(anyModalKey, { open });
return {
allProps,
closeCallback,
confirmCallback,
cancelCallback,
handleUpdateShow,
};
}

export function injectTestModal() {
return inject(anyModalKey)
}
Ï

步骤3(❌):提供Provider


在这个文件里,我将TestModal放在了根页面,然后将hook返回的props和event绑定给TestModal


// ModalProvider.vue
<template>
<slot />

<TestModal
v-bind="allTestModalProps"
@update:show="handleTestModalUpdateShow"
@close="closeTestModalCallback"
@confirm="confirmTestModalCallback"
@cancel="cancelTestModalCallback"
/>

<!-- 新增Modal -->
</template>

<script setup lang="ts">
import TestModal from "./test-modal/TestModal.vue";
import { provideTestModal } from "./test-modal/use-test-modal";
/** 新增import */

const {
allProps: allTestModalProps,
handleUpdateShow: handleTestModalUpdateShow,
closeCallback: closeTestModalCallback,
confirmCallback: confirmTestModalCallback,
cancelCallback: cancelTestModalCallback,
} = provideTestModal();
/** 新增provide */
</script>


步骤4(❌):通过函数打开弹窗


<template>
<n-button @click="open">open</n-button>
</template>

<script setup lang="ts">
import { injectTestModal } from "./test-modal/use-test-modal"
const model = injectTestModal()
function open() {
model?.open({
testValue: "传给弹窗组件的值",
onConfirm({ formData }, close, endLoading) {
console.log({ ...formData })
endLoading()
close()
},
})
}
</script>


到这里也就结束了。如果这样写一个弹窗,确实可以达到函数式的打开弹窗,但是他实在是太繁琐了,要写这么一堆东西,不如直接修改弹窗的show来的快。如果看过上面的use-test-modal.ts的话,会发现里面有AllProps,这是为了减少方便使用脚本工具来写通用化的代码,可以大大减少人工的重复代码编写,接下来我们使用一个工具来减少这些繁琐的操作。


步骤1(✅):初始化Provider


通过使用工具生成根页面ModalProvder组件。
具体步骤:在你想放置当前业务版本的弹窗文件夹路径下使用终端执行脚本,选择initProvider


iShot_2023-08-15_15.16.14.gif


步骤2(✅):生成弹窗模板


通过继续使用脚本工具,生成弹窗组件以及hook文件。
具体步骤:在你想放该弹窗的文件夹路径下使用终端,使用脚本工具,选择genModal,然后跟着指令操作就行。比如例子中,我们生产名为test的弹窗,然后告诉脚本我们的ModalProvider组件的绝对路径,脚本帮我生产test-modal的文件夹,里面放着testModal.vueuse-test-modal.ts,这里的use-test-modal.ts文件已经是成品了,不需要你去修改,ModalProvider.vue也不需要你去修改,里面的路径关系也帮你处理好了。


iShot_2023-08-15_15.17.33.gif


步骤3(✅):修改弹窗内容


上一步操作中,脚本帮我写好了TestModal.vue组件,我们可以在他的基础上完善我们的业务需求。


步骤4(✅):调用弹窗


我们找到生成的use-test-modal.ts,里面有一个injectXXXModal的方法,我们在哪个地方用到弹窗就引用执行他,返回的对象中有open方法,通过open方法去开启弹窗并且更新弹窗props。


Demo


预览
Demo地址
里面有完整的demo代码


iShot_2023-08-15_18.32.39.gif


脚本工具


仓库地址
这个是我写的脚本工具,用来减少一些重复代码的工作。用法可看readme


总结


本文先通过比较函数式的弹窗和直接传统编写弹窗的优劣,然后引出本文的主旨(如何编写函数式弹窗),再考虑我这一版函数式弹窗的问题,最后通过脚本工具来解决这一版函数式弹窗的劣势,完整的配合脚本工具,可以极大的加快我们的工作效率。
大家不要看这么写要好几个文件夹,实际使用的时候其实就是脚本生成代码后,我们只需要去修改弹窗主题的内容。有什么疑惑或者建议评论区聊。


作者:恐怖屋
来源:juejin.cn/post/7267418473401057321
收起阅读 »

离职后,前领导突然找你回去帮忙写代码解决问题,该怎么办?

题目中的这个问题,我相信有遇到过这种情况的同学的第一反应是:"诶,是要白嫖我还是说解决完问题给钱呀",且听我接下来慢慢分析。 首先要说的是,这种没头没尾的突发情况,一般大部分人都是很难遇到的。 原因也很简单,老板大部分也都是打过工,当过员工的,也是一路从职场老...
继续阅读 »

题目中的这个问题,我相信有遇到过这种情况的同学的第一反应是:"诶,是要白嫖我还是说解决完问题给钱呀",且听我接下来慢慢分析。


首先要说的是,这种没头没尾的突发情况,一般大部分人都是很难遇到的。


原因也很简单,老板大部分也都是打过工,当过员工的,也是一路从职场老油子混成的老板,很多人情世故,员工的小心思,老板其实都门儿清,甚至比很多员工都更熟。


如果公司里的一些工作是交接时不太能完全搞定的,可能还需要离职的员工继续帮忙的,一般在员工离职前的时候,就各种协商好了。


而像这种“突发情况”,大部分老板在联系离职的员工回去帮忙前,一般也都会把员工会想到的那些事儿,早就想了很多遍了,基本上相关问题都会在联系员工的时候说明白。


比如很多人都提到的报酬问题,这个基本上都是作为老板不可能回避,也不可能不知道的。


如果老板在联系员工的时候什么都提了,就是没聊这个。


那肯定是老板不想给报酬,还在做着让员工回来白干活的美梦。


不可否认,现实中确实有挺多这样的老板


所以,我的经验就是,如果老板在主动联系离职员工回来帮忙的时候,都没提报酬的事儿,那基本上就是不打算给,基本上你问了也是白问。


当然,大部分人都会遇到的情况是,本来跟老板领导关系也不错,老板领导也知道这一点,所以才会跟已经离职的员工开这个口。


这种时候,大部分人看在老板领导人还不错的份儿上,还是愿意回去帮忙的。


至于会和现在的工作造成的一些冲突,比如时间上走不开,现在住得离公司远,这些也都是可以直接明说的事儿,说了后,要么老板可以帮你解决,要么老板心里会知道你回来帮这次忙的成本有多高。


我以前工作过的公司,别说离职走的同事了,有一次是碰到了一个实习生经手的项目,上面很多东西没按照公司规范写,后来看到这些资料的员工整不明白是怎么回事。


但是,部门领导在知道了这件事后,在知道了这个实习生的同学就在本部门工作的情况下,并没有说让这位同学去搞定这个问题。


而是让这位同学联系好那位实习生后,领导亲自开车带着这位同学和要用到这个资料的人,专门在下班时间守在这位实习生的工作单位门口,接着他去一家还不错的餐厅,边吃饭边解决了这个问题。


至于很多人提的,跟老板没啥交情,甚至关系还不怎么好的,那还纠结什么,直接不理或拒绝就行了,但也没必要把话说得太绝。


毕竟,如果老板真的意识到你这边不好搞,同时也只有找你来帮忙是最划算的选择后,一般都会开出更高的加码,如果加码合适,你还是可以考虑一下的。


但是一定要就是论事,划定要解决问题的范围,要不然赖上你了有问题就找你可还行,同时也要注意不要留下太多痕迹。比如你回来帮忙,是不是属于违规行为,再比如请你回来帮忙的时候,装作无意间打听你现在公司的一些事儿,这个事儿很可能属于工作机密,毕竟大家都是同行,这些一定要注意。


综上我觉得,解决这个问题的公式是:上来先拖字诀、加各种不容易各种不行,这种能挡掉99%的需求,毕竟这么大个公司离了我这个小兵还不能转了咋地;实在不行了在谈什么样的条件你才能去帮忙解决问题,而且记住是单次解决问题的条件。


作者:kevinyan
来源:juejin.cn/post/7322344486159826996
收起阅读 »

环信服务端下载消息文件---菜鸟教程

前言在服务端,下载消息文件是一个重要的功能。它允许您从服务器端获取并保存聊天消息、文件等数据,以便在本地进行进一步的处理和分析。本指南将指导您完成环信服务端下载消息文件的步骤。环信服务端下载消息文件是指在环信服务端上,通过调用相应的API接口,从服务器端下载聊...
继续阅读 »

前言

在服务端,下载消息文件是一个重要的功能。它允许您从服务器端获取并保存聊天消息、文件等数据,以便在本地进行进一步的处理和分析。本指南将指导您完成环信服务端下载消息文件的步骤。
环信服务端下载消息文件是指在环信服务端上,通过调用相应的API接口,从服务器端下载聊天消息、文件等数据的过程。因环信服务端保存的消息漫游是有时间限制,有用户需要漫游全部的消息或者自己服务端做所有消息记录的备份。可以从环信服务端下载消息文件来进行解压,读取消息文件内容进行存储到自己的服务端。

前提条件

一、下载消息文件

以下将介绍如何通过环信接口获取到的URL来进行下载文件,解压文件,读取文件。
注:
time参数: 历史消息记录查询的起始时间。UTC 时间,使用 ISO8601 标准,格式为 yyyyMMddHH。例如 time 为 2018112717,则表示查询 2018 年 11 月 27 日 17 时至 2018 年 11 月 27 日 18 时期间的历史消息。若海外集群为 UTC 时区,需要根据自己所在的时区进行时间转换。

上图是环信官方文档中给出的获取历史消息记录响应示例。从示例中可以看出我们请求以后可以得到一个URL,这个URL为消息文件的下载URL。

1、下载消息文件环信rest 接口请求代码如下:

String url = "https://{{RestApi}}/{{org_name}}/{{app_name}}/chatmessages/2023122010";
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type","application/json");
headers.add("Authorization","Bearer Authorization");
Map<String, String> body = new HashMap<>();
HttpEntity<Map<String, String>> entity = new HttpEntity<>(body, headers);
ResponseEntity<Map> response;
try {
response = restTemplate.exchange(url, HttpMethod.GET, entity, Map.class);

System.out.print("消息文件下载成功---"+response.toString());
} catch (Exception e) {
System.out.print("消息文件下载失败---"+e.toString());
}

2、消息文件下载,通过请求环信下载历史消息文件接口获取到的URL 进行下载。

示例代码:

String url = "";
String targetUrl = "";
download(url,targetUrl);
/**
* 根据url下载文件,保存到filepath中
*
* @param url 文件的url
* @param diskUrl 本地存储路径
* @return
*/

public static String download(String url, String diskUrl) {
String filepath = "";
String filename = "";
try {
HttpClient client = HttpClients.createDefault();
HttpGet httpget = new HttpGet(url);
// 加入Referer,防止防盗链 httpget.setHeader("Referer", url);
HttpResponse response = client.execute(httpget);
HttpEntity entity = response.getEntity();
InputStream is = entity.getContent();
if (StringUtils.isBlank(filepath)){
Map<String,String> map = getFilePath(response,url,diskUrl);
filepath = map.get("filepath");
filename = map.get("filename");
}
File file = new File(filepath);
file.getParentFile().mkdirs();
FileOutputStream fileout = new FileOutputStream(file);
byte[] buffer = new byte[cache];
int ch = 0;
while ((ch = is.read(buffer)) != -1) {
fileout.write(buffer, 0, ch);
}
is.close();
fileout.flush();
fileout.close();

} catch (Exception e) {
e.printStackTrace();
}
return filename;
}


/**
* 获取response要下载的文件的默认路径
*
* @param response
* @return
*/

public static Map<String,String> getFilePath(HttpResponse response, String url, String diskUrl) {
Map<String,String> map = new HashMap<>();
String filepath = diskUrl;
String filename = getFileName(response, url);
String contentType = response.getEntity().getContentType().getValue();
if(StringUtils.isNotEmpty(contentType)){
// 获取后缀 String regEx = ".+(.+)$";
Pattern p = Pattern.compile(regEx);
Matcher m = p.matcher(filename);
if (!m.find()) {
// 如果正则匹配后没有后缀,则需要通过response中的ContentType的值进行匹配 filename = filename +".gz";

}else{
if(filename.length()>20){
filename = getRandomFileName() + ".gz";
}
}
}
if (filename != null) {
filepath += filename;
} else {
filepath += getRandomFileName();
}
map.put("filename", filename);
map.put("filepath", filepath);
return map;
}



/**
* 获取response header中Content-Disposition中的filename值
* @param response
* @param url
* @return
*/

public static String getFileName(HttpResponse response,String url) {
Header contentHeader = response.getFirstHeader("Content-Disposition");
String filename = null;
if (contentHeader != null) {
// 如果contentHeader存在 HeaderElement[] values = contentHeader.getElements();
if (values.length == 1) {
NameValuePair param = values[0].getParameterByName("filename");
if (param != null) {
try {
filename = param.getValue();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}else{
// 正则匹配后缀 filename = getSuffix(url);
}

return filename;
}

/**
* 获取随机文件名
*
* @return
*/

public static String getRandomFileName() {
return String.valueOf(System.currentTimeMillis());
}

/**
* 获取文件名后缀
* @param url
* @return
*/

public static String getSuffix(String url) {
// 正则表达式“.+/(.+)$”的含义就是:被匹配的字符串以任意字符序列开始,后边紧跟着字符“/”, // 最后以任意字符序列结尾,“()”代表分组操作,这里就是把文件名做为分组,匹配完毕我们就可以通过Matcher // 类的group方法取到我们所定义的分组了。需要注意的这里的分组的索引值是从1开始的,所以取第一个分组的方法是m.group(1)而不是m.group(0)。 String regEx = ".+/(.+)$";
Pattern p = Pattern.compile(regEx);
Matcher m = p.matcher(url);
if (!m.find()) {
// 格式错误,则随机生成个文件名 return String.valueOf(System.currentTimeMillis());
}
return m.group(1);

}
  • url为第一步中从环信下载历史消息文件接口中请求返回的url(消息文件下载地址)
  • targetUrl 为下载的本地存储路径

下载以后从对应的路径下就可以看到所下载的文件。

3、消息文件解压,下载完的文件是以.gz结尾的压缩文件,需要对压缩文件进行解压

 public static void unGzipFile(String gzFilePath,String directoryPath) {
String ouputfile = "";
try {
//建立gzip压缩文件输入流 FileInputStream fin = new FileInputStream(gzFilePath);
//建立gzip解压工作流 GZIPInputStream gzin = new GZIPInputStream(fin);
//建立解压文件输出流// ouputfile = sourcedir.substring(0,sourcedir.lastIndexOf('.'));// ouputfile = ouputfile.substring(0,ouputfile.lastIndexOf('.')); FileOutputStream fout = new FileOutputStream(directoryPath);
int num;
byte[] buf=new byte[1024];
while ((num = gzin.read(buf,0,buf.length)) != -1) {
fout.write(buf,0,num);
}
gzin.close();
fout.close();
fin.close();
} catch (Exception ex){
System.err.println(ex.toString());
}
return;
}

gzFilePath:压缩文件路径
directoryPath:加压到的文件目录路径
解压后的文件如下图所示:

4、文件读取,将解压后的文件读取出来

FileInputStream inputStream = null;
try {
inputStream = new FileInputStream("/Users/liupeng/Downloads/download/1234567890");
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String str = null;
long i = 0;
while(true){
try {
if (!((str = bufferedReader.readLine()) != null)) break;
} catch (IOException e) {
e.printStackTrace();
}
JSONObject jo = JSONObject.parseObject(str);
System.out.println("==========================================" + i);
System.out.println("消息id:" + jo.get("msg_id"));
System.out.println("发送id:" + jo.get("from"));
System.out.println("接收id:" + jo.get("to"));
System.out.println("服务器时间戳:" + jo.get("timestamp"));
System.out.println("会话类型:" + jo.get("chat_type"));
System.out.println("消息扩展:" + jo.getJSONObject("payload").get("ext"));
System.out.println("消息体:" + jo.getJSONObject("payload").getJSONArray("bodies").get(0));
i ++;
if (i > 100) break;
}
//close try {
inputStream.close();
bufferedReader.close();
} catch (IOException e) {
e.printStackTrace();
}

} catch (FileNotFoundException e) {
e.printStackTrace();
}

解析完以后日志打印如下:

至此,解析完以后可以将解析的数据进行存储。

相关文档:

注册环信即时通讯IM:https://console.easemob.com/user/register

IMGeek社区支持:https://www.imgeek.net/

收起阅读 »

漫画:成年人的社交潜台词

原则上可以=不可以原则上不可以=可以再说吧=没戏……花:学到了,看我活学活用!——————————————————————————————甜狗:hi花:?甜狗:最近过的怎么样花:还好吧甜狗:一起出去玩呀花:有空一定去甜狗:改天请你吃饭花:我比较相信缘分甜狗:我...
继续阅读 »




原则上可以=不可以
原则上不可以=可以
再说吧=没戏……


花:学到了,看我活学活用!
——————————————————————————————
甜狗:hi
花:?
甜狗:最近过的怎么样
花:还好吧
甜狗:一起出去玩呀
花:有空一定去
甜狗:改天请你吃饭
花:我比较相信缘分
甜狗:我们分手吧
花:我在考虑考虑
甜狗:考虑啥?你还爱我吗
花:哎呀、我不是这个意思
甜狗:晚安
——————————————————————————————
花:(朋友圈)最后还是自己默默承受

作者:灼见
来源:mp.weixin.qq.com/s/nvXTNj-GwNDW4zsvbBNTng

收起阅读 »

未来三年,请主动给生活降级

任正非曾在华为内部论坛发言时说:接下来3年,华为要将“活下去”作为主要纲领。企业寒意阵阵,其中的个人也在顶着寒气的侵袭,努力与生活周旋。未来的几年,普通人最好的应对方法,是主动给自己减负,给生活降级。01消费降级前阵子,话题#一件事说明你消费降级#登上微博热搜...
继续阅读 »

任正非曾在华为内部论坛发言时说:接下来3年,华为要将“活下去”作为主要纲领。


企业寒意阵阵,其中的个人也在顶着寒气的侵袭,努力与生活周旋。


未来的几年,普通人最好的应对方法,是主动给自己减负,给生活降级。


01

消费降级


前阵子,话题#一件事说明你消费降级#登上微博热搜。


以前人们把炫富当潮流,现在流行的是各种花式“抠门”。


深夜蹲在便利店等一份三折便当,作为第二天的早餐;


自己在家理发,全身上下没有超过100块的衣服;


洗面奶牙膏挤不动了,用剪刀剪开,接着再用个三四次……


谁也不想抠抠搜搜过日子,但感受过失业危机,承受过意外侵袭的我们,开始活得无比清醒。


相比起买买买的畅快,握在手里的存款,才是我们最大的安全感。


未来几年,各种考验依然在等着我们。


狠狠省钱,努力存钱,就是在不确定的未来,给自己攒一份确定性。


《极简生活》一书提供了一个购物标准,买东西前问自己3个问题:


我是否真的需要它?我使用它的频率是多少?我现有的物品中,是否有东西可以替代它?


想清楚这几个问题,就能帮你省下许多不必要的开支。


畅销书作家哈维·艾克说:你管理金钱的习惯,比你拥有的钱财数目更重要。


管理金钱的第一步,就是要养成记账的习惯。


你可以下载专门的记账APP,来记录你一天的开支。


然后每周或者每月进行复盘,看看什么地方该花,什么地方不该花。


当你清楚知道每一分钱的去处,自然能堵住出口,守住自己的钱包。


巴尔扎克说过:


对于浪费的人,金钱是圆的,可是对于节俭的人,金钱是扁的,是可以一块块堆积起来的。


学会省钱,你省的是风险;学会存钱,你存的是保障。


推荐几个我亲测有效的存钱方法:


百分比存钱法:每月把收入的10%存起来,强制储蓄,雷打不动;


365存钱法:画一个表格,每天挑1-365中的一个数字来存钱,一年后,能轻轻松松攒下66795元;



每周累计存钱法:一年52周,第一周存10块,第二周存20……


以此类推,一年下来,也能存住一万多。


消费有度,存钱上瘾,晴备雨伞,饱存饥粮,才是未来3年最好的金钱观。


02

投资降级


最近有个网络热词“中产不要命三件套”:投资商铺、辞职创业、全职炒股。


许多人对经济形势的判断太过乐观,盲目投资,最终连原本安稳的生活也赔了进去。


说两个我朋友的故事。


一位朋友是深圳一家贸易公司的高管,年薪百万。


妻子在家做全职太太,两人育有一双儿女。


去年,考虑到老大要上学了,他们想把手里的小三房卖了,置换一套好点的学区房。


可房子迟迟卖不出去,夫妻俩着急孩子上学的事,就和朋友借了150万,又贷款200万买下了学区房。


不曾想他们刚买完房子,朋友就被裁员了,此后投了上百份简历都杳无音讯。


可他还有一大家子要养活,每月还有近2万的房贷要还,离职的赔偿也很快被花光。


曾经的中产精英,只得选择断供,四处借钱度日。


另外一个朋友,是大型银行的技术专员,每月一万多的工资。


但某天他听一个朋友建议,投资了一个连锁餐饮项目。


他花光自己40多万的积蓄,还向3家银行借了贷。


但后来疫情来袭,朋友的投资也打了水漂,亏光所有本金不说,还欠了银行一屁股债。


作家连岳说:


投资的标准是,你要有本事先安置好家人的生活,此后还有闲钱,才能用来投资;


投资失败后,还要保证家人丰衣足食,不能遵循这个标准,就会被投资害死。


未来几年,各种不确定性依然存在,我建议你:


1. 清空负债,减少信用卡的使用频率;


2. 不要盲目投资,尽量选择稳健的投资策略;


3. 做任何事情都不要全押,卡上至少要有家庭储蓄一两年的生活资金。


03

就业降级


《凉子访谈录》中有位35岁的受访者,被大公司裁员后,收到一家资历尚浅的公司offer。


他直言自己还有一些行业自尊心,还是想去更大的平台,就拒绝了。


他认为凭自己的资历,找个跟之前差不多的公司不在话下,可现实却是他投的简历回应者寥寥无几。


大家应该都感觉到了,这几年工作越来越难找。


台湾劳动部《劳工失业后再就业情形》就有调查数据显示:


45岁以上职场人一旦失业,想要找到新工作平均得花6个月,还有33.6%的人找不到新工作或放弃不找了。


我身边有失业的朋友,找工作时也是一再降薪,不求岗位对口,只求尽快入职,因为房贷不等人。


未来3年,就业形势会更加残酷,你需要遵循以下三个法则。


1. 先活下去再说


一朝失业,才懂什么叫焦头烂额。


没有收入的日子,车贷房贷、孩子的教育支出、日常生活开支,样样都成了难题。


诚如俞敏洪所讲:当一个人面临生存问题,先活下去再说。


“只要这份工作不玷污你的人格,你再劳累再不喜欢,只要可以给你带来一份收入,你可以先做。”


世道艰难,暂时苟着,并不丢人。


2. 珍惜现在的单位


去年1月,Google突然宣布裁员12000人,紧接着,IBM加入了裁员大军,裁员3900人,3月底,微软也宣布裁员1万名员工……


在这个瞬息万变的时代,如果你还有班可上,其实就已经跑赢了大多数人。


所以,要珍惜现有的工作,善待你所在的单位。


一份按时到来的工资,能让你维持生计,一份不错的工作,可以为你遮风挡雨。


寒意尚未褪去之际,和现有单位一起抗住压力,和同事抱团取暖,才不至于被冻僵。


3. 保持归零心态


在找工作的心态上,你要抛弃走到今天为止你所有的成就、地位和光环。


大家总有“35岁焦虑”,是因为大家总认为自己应该越挣越多。


但是,年龄与成就并非线性关系。


前面跑得快,后面跑得慢,你会被后面的人超越,这是必然会发生的。


未来3年,什么样的人会活得很好?


热爱变化,主动拥抱不确定性,勇于走出舒适圈的人。


愿你在反复归零的状态下,依然能充满勇气,义无反顾迎接下一个变化。


04

社交降级


英国作家普利斯特利说:


社交性聚会就是去不去你都会感到后悔的一种活动,不去也没人注意你的缺席,去了就是参加一种虚情假意的游戏。


一场疫情,更是让许多人在自我隔离中,逐渐发现了社交非必要性。


事实上,过度的社交不仅无法排解情感上的孤独,无法带来所谓的人脉,反而是一种自我消耗。


以后,请给自己的社交降级。


1. 不去无意义的饭局


当我们参加热热闹闹的饭局,以为在吃喝玩乐中就把人脉搞定了。


却不明白,酒桌上的交情无法延伸到酒桌外。


热衷于这种聚会,浪费精力不说,还会有一种“朋友遍天下”的错觉。


等到现实的一个浪头打过来,你才会明白,逢场作戏的友谊,根本不堪一击。


2. 走出虚假的名利场


苏芒在《芭莎》杂志任主编时,每次与一众名媛合照都站在C位。


可当她在时尚圈地位不保之后,别人发合照都会把她裁掉。


经历过动荡起伏的人,更能懂什么叫人走茶凉。


成年人的世界向来现实,自己没有价值,所有的社交都是浮云。


与其在名利场上费心攀关系,不如好好修炼自己的本事。


3. 远离低层次圈子


周国平曾讲:


“为了尽兴而聚在一起的人,要么债台高筑,要么百病缠身,最终往往不能尽兴;


反倒是那些聚在一起吃苦的人,身体和心灵都得到锻炼,最终过得幸福圆满。”


低层次的圈子,会不断消耗你、腐蚀你,直到你沉沦其中;


融入更优秀的群体,才是成长的最佳路径。



诗人里尔克说:哪有什么胜利可言,挺住意味着一切。


生活的海域从不平静,你要以稳健的姿态迎接风浪,挺过风浪。


未来3年,请捂紧钱包,低配欲望。


请相信,你对生活的每一次低头,都是为了以后更好地昂首。


你当下的每一份积累,终将换来命运的厚待。


作者:每晚安娜贝苏
来源:每晚一卷书(JYXZ89896)

收起阅读 »

Java切换到Kotlin,Crash率上升了?

前言 最近对一个Java写的老项目进行了部分重构,测试过程中波澜不惊,顺利上线后几天通过APM平台查看发现Crash率上升了,查看堆栈定位到NPE类型的Crash,大部分发生在Java调用Kotlin的函数里,本篇将会分析具体的场景以及规避方式。 通过本篇文章...
继续阅读 »

前言


最近对一个Java写的老项目进行了部分重构,测试过程中波澜不惊,顺利上线后几天通过APM平台查看发现Crash率上升了,查看堆栈定位到NPE类型的Crash,大部分发生在Java调用Kotlin的函数里,本篇将会分析具体的场景以及规避方式。

通过本篇文章,你将了解到:




  1. NPE(空指针 NullPointerException)的本质

  2. Java 如何预防NPE?

  3. Kotlin NPE检测

  4. Java/Kotlin 混合调用

  5. 常见的Java/Kotlin互调场景



1. NPE(空指针 NullPointerException)的本质


变量的本质


    val name: String = "fish"

name是什么?

对此问题你可能嗤之以鼻:



不就是变量吗?更进一步说如果是在对象里声明,那就是成员变量(属性),如果在方法(函数)里声明,那就是局部变量,如果是静态声明的,那就是全局变量。



回答没问题很稳当。

那再问为什么通过变量就能找到对应的值呢?



答案:变量就是地址,通过该地址即可寻址到内存里真正的值



无法访问的地址



在这里插入图片描述


如上图,若是name="fish",表示的是name所指向的内存地址里存放着"fish"的字符串。

若是name=null,则说明name没有指向任何地址,当然无法通过它访问任何有用的信息了。


无论C/C++亦或是Java/Kotlin,如果一个引用=null,那么这个引用将毫无意义,无法通过它访问任何内存信息,因此这些语言在设计的过程中会将通过null访问变量/方法的行为都会显式(抛出异常)提醒开发者。


2. Java 如何预防NPE?


运行时规避


先看Demo:


public class TestJava {
public static void main(String args[]) {
(new TestJava()).test();
}

void test() {
String str = getString();
System.out.println(str.length());
}

String getString() {
return null;
}
}

执行上述代码将会抛出异常,导致程序Crash:



在这里插入图片描述


我们有两种解决方式:




  1. try...catch

  2. 对象判空



try...catch 方式


public class TestJava {
public static void main(String args[]) {
(new TestJava()).testTryCatch();
}

void testTryCatch() {
try {
String str = getString();
System.out.println(str.length());
} catch (Exception e) {
}
}

String getString() {
return null;
}
}

NPE被捕获,程序没有Crash。


对象判空


public class TestJava {
public static void main(String args[]) {
(new TestJava()).testJudgeNull();
}

void testJudgeNull() {
String str = getString();
if (str != null) {
System.out.println(str.length());
}
}

String getString() {
return null;
}
}

因为提前判空,所以程序没有Crash。


编译时检测


在运行时再去做判断的缺点:



无法提前发现NPE问题,想要覆盖大部分的场景需要随时try...catch或是判空
总有忘记遗漏的时候,发布到线上就是个生产事故



那能否在编译时进行检测呢?

答案是使用注解。


public class TestJava {
public static void main(String args[]) {
(new TestJava()).test();
}

void test() {
String str = getString();
System.out.println(str.length());
}

@Nullable String getString() {
return null;
}
}

在编写getString()方法时发现其可能为空,于是给方法加上一个"可能为空"的注解:@Nullable


当调用getString()方法时,编译器给出如下提示:



在这里插入图片描述


意思是访问的getString()可能为空,最后访问String.length()时可能会抛出NPE。

看到编译器的提示我们就知道此处有NPE的隐患,因此可以针对性的进行处理(try...catch或是判空)。


当然此处的注解仅仅只是个"弱提示",你即使没有进行针对性的处理也能编译通过,只是问题最后都流转到运行时更难挽回了。


有"可空"的注解,当然也有"非空"的注解:



在这里插入图片描述


@Nonnull 注解修饰了方法后,若是检测到方法返回null,则会提示修改,当然也是"弱提示"。


3. Kotlin NPE检测


编译时检测


Kotlin 核心优势之一:



空安全检测,变量分为可空型/非空型,能够在编译期检测潜在的NPE,并强制开发者确保类型一致,将问题在编译期暴露并解决



先看非空类型的变量声明:


class TestKotlin {

fun test() {
val str = getString()
println("${str.length}")
}

private fun getString():String {
return "fish"
}
}

fun main() {
TestKotlin().test()
}


此种场景下,我们能确保getString()函数的返回一定非空,因此在调用该函数时无需进行判空也无需try...catch。


你可能会说,你这里写死了"fish",那我写成null如何?



在这里插入图片描述


编译期直接提示不能这么写,因为我们声明getString()的返回值为String,是非空的String类型,既然声明了非空,那么就需要言行一致,返回的也是非空的。


有非空场景,那也得有空的场景啊:


class TestKotlin {

fun test() {
val str = getString()
println("${str.length}")
}

private fun getString():String? {
return null
}
}

fun main() {
TestKotlin().test()
}

此时将getString()声明为非空,因此可以在函数里返回null。

然而调用之处就无法编译通过了:



在这里插入图片描述


意思是既然getString()可能返回null,那么就不能直接通过String.length访问,需要改为可空方式的访问:


class TestKotlin {

fun test() {
val str = getString()
println("${str?.length}")
}

private fun getString():String? {
return null
}
}

str?.length 意思是:如果str==null,就不去访问其成员变量/函数,若不为空则可以访问,于是就避免了NPE问题。


由此可以看出:



Kotlin 通过检测声明与实现,确保了函数一定要言行一致(声明与实现),也确保了调用者与被调用者的言行一致



因此,若是用Kotlin编写代码,我们无需花太多时间去预防和排查NPE问题,在编译期都会有强提示。


4. Java/Kotlin 混合调用


回到最初的问题:既然Kotlin都能在编译期避免了NPE,那为啥使用Kotlin重构后的代码反而导致Crash率上升呢?


原因是:项目里同时存在了Java和Kotlin代码,由上可知两者在NPE的检测上有所差异导致了一些兼容问题。


Kotlin 调用 Java


调用无返回值的函数


Kotlin虽然有空安全检测,但是Java并没有,因此对于Java方法来说,不论你传入空还是非空,在编译期我都没法检测出来。


public class TestJava {
void invokeFromKotlin(String str) {
System.out.println(str.length());
}
}

class TestKotlin {

fun test() {
TestJava().invokeFromKotlin(null)
}
}

fun main() {
TestKotlin().test()
}

如上无论是Kotlin调用Java还是Java之间互调,都没法确保空安全,只能由被调用者(Java)自己处理可能的异常情况。


调用有返回值的函数


public class TestJava {
public String getStr() {
return null;
}
}

class TestKotlin {
fun testReturn() {
println(TestJava().str.length)
}
}

fun main() {
TestKotlin().testReturn()
}

如上,Kotlin调用Java的方法获取返回值,由于在编译期Kotlin无法确定返回值,因此默认把它当做非空处理,若是Java返回了null,那么将会Crash。


Java 调用 Kotlin


调用无返回值的函数


先定义Kotlin类:


class TestKotlin {

fun testWithoutNull(str: String) {
println("len:${str.length}")
}

fun testWithNull(str: String?) {
println("len:${str?.length}")
}
}

有两个函数,分别接收可空/非空参数。


在Java里调用,先调用可空函数:


public class TestJava {
public static void main(String args[]) {
(new TestKotlin()).testWithNull(null);
}
}

因为被调用方是Kotlin的可空函数,因此即使Java传入了null,也不会有Crash。


再换个方式,在Java里调用非空函数:


public class TestJava {
public static void main(String args[]) {
(new TestKotlin()).testWithoutNull(null);
}
}

却发现Crash了!



在这里插入图片描述


为什么会Crash呢?反编译查看Kotlin代码:


public final class TestKotlin {
public final void testWithoutNull(@NotNull String str) {
Intrinsics.checkNotNullParameter(str, "str");
String var2 = "len:" + str.length();
System.out.println(var2);
}

public final void testWithNull(@Nullable String str) {
String var2 = "len:" + (str != null ? str.length() : null);
System.out.println(var2);
}
}

对于非空的函数来说,会有检测代码:

Intrinsics.checkNotNullParameter(str, "str"):


    public static void checkNotNullParameter(Object value, String paramName) {
if (value == null) {
throwParameterIsNullNPE(paramName);
}
}
private static void throwParameterIsNullNPE(String paramName) {
throw sanitizeStackTrace(new NullPointerException(createParameterIsNullExceptionMessage(paramName)));
}

可以看出:




  1. Kotlin对于非空的函数参数,先判断其是否为空,若是为空则直接抛出NPE

  2. Kotlin对于可空的函数参数,没有强制检测是否为空



调用有返回值的函数


Java 本身就没有空安全,只能在运行时进行处理。


小结


很容看出来:




  1. Java 调用Kotlin的非空函数有Crash的风险,编译器无法检查到传入的参数是否为空

  2. Java 调用Kotlin的可空函数没有Crash风险,Kotlin编译期检查空安全

  3. Kotlin 调用Java的函数有Crash风险,由Java代码规避风险

  4. Kotlin 调用Java有返回值的函数有Crash风险,编译器无法检查到返回值是否为空



回到文章的标题,我们已经大致知道了Java切换到Kotlin,为啥Crash就升上了的原因了,接下来再详细分析。


5. 常见的Java/Kotlin互调场景


Android里的Java代码分布



在这里插入图片描述


在Kotlin出现之前,Java就是Android开发的唯一语言,Android Framework、Androidx很多是Java代码编写的,因此现在依然有很多API是Java编写的。


而不少的第三方SDK因为稳定性、迁移代价的考虑依然使用的是Java代码。


我们自身项目里也因为一些历史原因存在Java代码。


以下讨论的前提是假设现有Java代码我们都无法更改。


Kotlin 调用Java获取返回值


由于编译期无法判定Java返回的值是空还是非空,因此若是确认Java函数可能返回空,则可以通过在Kotlin里使用可空的变量接收Java的返回值。


class TestKotlin {
fun testReturn() {
val str: String? = TestJava().str
println(str?.length)
}
}

fun main() {
TestKotlin().testReturn()
}

Java 调用Kotlin函数


LiveData Crash的原因与预防


之前已经假设过我们无法改动Java代码,那么Java调用Kotlin函数的场景只有一个了:回调。

上面的有返回值场景还是比较容易防备,回调的场景就比较难发现,尤其是层层封装之后的代码。

这也是特别常见的场景,典型的例子如LiveData。


Crash原因


class TestKotlin(val lifecycleOwner: LifecycleOwner) {
val liveData: MutableLiveData = MutableLiveData()
fun testLiveData() {
liveData.observe(lifecycleOwner) {
println(it.length)
}
}

init {
testLiveData()
}
}

如上,使用Kotlin声明LiveData,其类型是非空的,并监听LiveData的变化。


在另一个地方给LiveData赋值:


TestKotlin(this@MainActivity).liveData.value = null

虽然LiveData的监听和赋值的都是使用Kotlin编写的,但不幸的是还是Crash了。

发送和接收都是用Kotlin编写的,为啥还会Crash呢?

看看打印:



在这里插入图片描述


意思是接收到的字符串是空值(null),看看编译器提示:



在这里插入图片描述


原来此处的回调传入的值被认为是非空的,因此当使用it.length访问的时候编译器不会有空安全提示。


再看看调用的地方:



在这里插入图片描述


可以看出,这回调是Java触发的。


Crash 预防


第一种方式:

我们看到LiveData的数据类型是泛型,因此可以考虑在声明数据的时候定为非空:


class TestKotlin(val lifecycleOwner: LifecycleOwner) {
val liveData = MutableLiveData()
fun testLiveData() {
liveData.observe(lifecycleOwner) {
println(it?.length)
}
}

init {
testLiveData()
}
}

如此一来,当访问it.length时编译器就会提示可空调用。


第二种方式:

不修改数据类型,但在接收的地方使用可空类型接收:


class TestKotlin(val lifecycleOwner: LifecycleOwner) {
val liveData = MutableLiveData()
fun testLiveData() {
liveData.observe(lifecycleOwner) {
val dataStr:String? = it
println(dataStr?.length)
}
}

init {
testLiveData()
}
}

第三种方式:

使用Flow替换LiveData。


LiveData 修改建议:




  1. 若是新写的API,建议使用第三种方式

  2. 若是修改老的代码,建议使用第一种方式,因为可能有多个地方监听LiveData值的变化,如果第一种方式的话需要写好几个地方。



其它场景的Crash预防:


与后端交互的数据结构
比如与后端交互声明的类,后端有可能返回null,此时在客户端接收时若是使用了非空类型的字段去接收,那么会发生Crash。

通常来说,我们会使用网络框架(如retrofit)接收数据,数据的转换并不是由我们控制,因此无法使用针对LivedData的第二种方式。

有两种方式解决:




  1. 与后端约定,不能返回null(等于白说)

  2. 客户端声明的类的字段声明为可空(类似针对LivedData的第一种方式)



Json序列化/反序列化

Json字符串转换为对象时,有些字段可能为空,也需要声明为可空字段。


小结



在这里插入图片描述


作者:小鱼人爱编程
来源:juejin.cn/post/7274163003158511616
收起阅读 »

当遇到需要在Activity间传递大量的数据怎么办?

在Activity间传递的数据一般比较简单,但是有时候实际开发中也会传一些比较复杂的数据,尤其是面试问道当遇到需要在Activity间传递大量的数据怎么办? Intent 传递数据的大小是有限制的,它大概能传的数据是1M-8K,原因是Binder锁映射的内存大...
继续阅读 »

在Activity间传递的数据一般比较简单,但是有时候实际开发中也会传一些比较复杂的数据,尤其是面试问道当遇到需要在Activity间传递大量的数据怎么办?


Intent 传递数据的大小是有限制的,它大概能传的数据是1M-8K,原因是Binder锁映射的内存大小就是1M-8K.一般activity间传递数据会要使用到binder,因此这个就成为了数据传递的大小的限制。那么当activity间要传递大数据采用什么方式呢?其实方式很多,我们就举几个例子给大家说明一下,但是无非就是使用数据持久化,或者内存共享方案。一般大数据的存储不适宜使用SP, MMKV,DataStore。


Activity之间传递大量数据主要有如下几种方式实现:


  • LruCache

  • 持久化(sqlite、file等)

  • 匿名共享内存


使用LruCache

LruCache是一种缓存策略,可以帮助我们管理缓存,想具体了解的同学可以去Glide章节中具体先了解下。在当前的问题下,我们可以利用LruCache存储我们数据作为一个中转,好比我们需要Activity A向Activity B传递大量数据,我们可以Activity A先向LruCache先写入数据,之后Activity B从LruCache读取。


首先我们定义好写入读出规则:


public interface IOHandler {
   //保存数据
   void put(String key, String value);
   void put(String key, int value);
   void put(String key, double value);
   void put(String key, float value);
   void put(String key, boolean value);
   void put(String key, Object value);

   //读取数据
   String getString(String key);
   double getDouble(String key);
   boolean getBoolean(String key);
   float getFloat(String key);
   int getInt(String key);
   Object getObject(String key);
}

我们可以根据规则也就是接口,写出具体的实现类。实现类中我们保存数据使用到LruCache,这里面我们一定要设置一个大小,因为内存中数据的最大值是确定,我们保存数据的大小最好不要超过最大值的1/8.


LruCache<String, Object> mCache = new LruCache<>( 10 * 1024*1024);

写入数据我们使用比较简单:


@Override
public void put(String key, String value) {
   mCache.put(key, value);
}

好比上面写入String类型的数据,只需要接收到的数据全部put到mCache中去。


读取数据也是比较简单方便:


@Override
public String getString(String key) {
   return String.valueOf(mCache.get(key));
}

持久化数据

那就是sqlite、file等方式。将需要传递的数据写在临时文件或者数据库中,再跳转到另外一个组件的时候再去读取这些数据信息,这种处理方式会由于读写文件较为耗时导致程序运行效率较低。这种方式特点如下:


优势:


(1)应用中全部地方均可以访问


(2)即便应用被强杀也不是问题了


缺点:


(1)操做麻烦


(2)效率低下


匿名共享内存

在跨进程传递大数据的时候,我们一般会采用binder传递数据,但是Binder只能传递1M一下的数据,所以我们需要采用其他方式完成数据的传递,这个方式就是匿名共享内存。


Anonymous Shared Memory 匿名共享内存」是 Android 特有的内存共享机制,它可以将指定的物理内存分别映射到各个进程自己的虚拟地址空间中,从而便捷的实现进程间内存共享。


Android 上层提供了一些内存共享工具类,就是基于 Ashmem 来实现的,比如 MemoryFile、 SharedMemory。



今日分享到此结束,对你有帮助的话,点个赞再走呗,每日一个面试小技巧




关注公众号:Android老皮

解锁  《Android十大板块文档》 ,让学习更贴近未来实战。已形成PDF版



内容如下



1.Android车载应用开发系统学习指南(附项目实战)

2.Android Framework学习指南,助力成为系统级开发高手

3.2023最新Android中高级面试题汇总+解析,告别零offer

4.企业级Android音视频开发学习路线+项目实战(附源码)

5.Android Jetpack从入门到精通,构建高质量UI界面

6.Flutter技术解析与实战,跨平台首要之选

7.Kotlin从入门到实战,全方面提升架构基础

8.高级Android插件化与组件化(含实战教程和源码)

9.Android 性能优化实战+360°全方面性能调优

10.Android零基础入门到精通,高手进阶之路



作者:派大星不吃蟹
来源:juejin.cn/post/7264503091116965940
收起阅读 »

如何让 Android 网络请求像诗一样优雅

在 Android 应用开发中,网络请求必不可少,如何去封装才能使自己的请求代码显得更加简洁优雅,更加方便于以后的开发呢?这里利用 Kotlin 的函数式编程和 Retrofit 来从零开始封装一个网络请求框架,下面就一起来瞧瞧吧! 首先,引入网络请求框架的依...
继续阅读 »

在 Android 应用开发中,网络请求必不可少,如何去封装才能使自己的请求代码显得更加简洁优雅,更加方便于以后的开发呢?这里利用 Kotlin 的函数式编程和 Retrofit 来从零开始封装一个网络请求框架,下面就一起来瞧瞧吧!


首先,引入网络请求框架的依赖。


implementation 'com.squareup.okhttp3:okhttp:4.9.1'
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.1'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

定义拦截器


我们可以先自定义一些拦截器,对一些公共提交的字段做封装,比如 token。在服务器注册成功或者登录成功之后获取 token,过期之后便无法正常请求接口,所以需要在请求接口时判断 token 是否过期,由于接口众多,不可能每个接口都进行判断,所以需要全局设置一个拦截器判断 token。


class TokenInterceptor : Interceptor {

override fun intercept(chain: Interceptor.Chain): Response {
// 当前拦截器中收到的请求对象
val request = chain.request()
// 执行请求
var response = chain.proceed(request)
if (response.body == null) {
return response
}
val mediaType = response.body!!.contentType() ?: return response
val type = mediaType.toString()
if (!type.contains("application/json")) {
return response
}
val result = response.body!!.string()
var code = ""
try {
val jsonObject = JSONObject(result)
code = jsonObject.getString("code")
} catch (e: Exception) {
e.printStackTrace()
}
// 重新构建 response
response = response.newBuilder().body(result.toResponseBody(null)).code(200).build()
if (isTokenExpired(code)) {
// token 过期,需要获取新的 token
val newToken = getNewToken() ?: return response
// 重新构建新的 token 请求
val builder = request.url.newBuilder().setEncodedQueryParameter("token", newToken)
val newRequest = request.newBuilder().method(request.method, request.body)
.url(builder.build()).build()
return chain.proceed(newRequest)
}
return response
}

// 判断 token 是否过期
private fun isTokenExpired(code: String) =
TextUtils.equals(code, "401") || TextUtils.equals(code, "402")

// 刷新 token
private fun getNewToken() = ServiceManager.instance.refreshToken()

}

这里是 token 过期之后直接重新请求接口获取新的 token,这需要根据具体业务需求来,有些可能是过期之后跳转到登录页面,让用户重新登录等等。


我们还可以再定义一个拦截器,全局添加 token。


class TokenHeaderInterceptor : Interceptor {

override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
val headers = request.headers
var token = headers["token"]
if (TextUtils.isEmpty(token)) {
token = ServiceManager.instance.getToken()
request = request.newBuilder().addHeader("token", token).build()
}
return chain.proceed(request)
}

}

创建 retrofit


class RetrofitUtil {

companion object {

private const val TIME_OUT = 20L

private fun createRetrofit(): Retrofit {

// OkHttp 提供的一个拦截器,用于记录和查看网络请求和响应的日志信息。
val interceptor = HttpLoggingInterceptor()
// 打印请求和响应的所有内容,响应状态码和执行时间等等。
interceptor.level = HttpLoggingInterceptor.Level.BODY

val okHttpClient = OkHttpClient().newBuilder().apply {
addInterceptor(interceptor)
addInterceptor(TokenInterceptor())
addInterceptor(TokenHeaderInterceptor())
retryOnConnectionFailure(true)
connectTimeout(TIME_OUT, TimeUnit.SECONDS)
writeTimeout(TIME_OUT, TimeUnit.SECONDS)
readTimeout(TIME_OUT, TimeUnit.SECONDS)
}.build()

return Retrofit.Builder().apply {
addConverterFactory(GsonConverterFactory.create())
baseUrl(ServiceManager.instance.baseHttpUrl)
client(okHttpClient)
}.build()

}

fun <T> getAPI(clazz: Class<T>): T {
return createRetrofit().create(clazz)
}

}
}

网络请求封装


定义通用基础请求返回的数据结构


private const val SERVER_SUCCESS = "200"

data class BaseResp<T>(val code: String, val message: String, val data: T)

fun <T> BaseResp<T>?.isSuccess() = this?.code == SERVER_SUCCESS

请求状态流程封装,可以根据具体业务流程实现方法。


class RequestAction<T> {

// 开始请求
var start: (() -> Unit)? = null
private set

// 发起请求
var request: (suspend () -> BaseResp<T>)? = null
private set

// 请求成功
var success: ((T?) -> Unit)? = null
private set

// 请求失败
var error: ((String) -> Unit)? = null
private set

// 请求结束
var finish: (() -> Unit)? = null
private set

fun request(block: suspend () -> BaseResp<T>) {
request = block
}

fun start(block: () -> Unit) {
start = block
}

fun success(block: (T?) -> Unit) {
success = block
}

fun error(block: (String) -> Unit) {
error = block
}

fun finish(block: () -> Unit) {
finish = block
}

}

因为网络请求都是在 ViewModel 中进行的,我们可以定义一个 ViewModel 的扩展函数,用来处理网络请求。


fun <T> ViewModel.netRequest(block: RequestAction<T>.() -> Unit) {

val action = RequestAction<T>().apply(block)

viewModelScope.launch {
try {
action.start?.invoke()
val result = action.request?.invoke()
if (result.isSuccess()) {
action.success?.invoke(result!!.data)
} else {
action.error?.invoke(result!!.message)
}
} catch (ex: Exception) {
// 可以做一些定制化的返回错误提示
action.error?.invoke(getErrorTipContent(ex))
} finally {
action.finish?.invoke()
}
}

}

private const val SERVER_ERROR = "HTTP 500 Internal Server Error"
private const val HTTP_ERROR_TIP = "服务器或者网络连接错误"

fun getErrorTipContent(ex: Throwable) = if (ex is ConnectException || ex is UnknownHostException
|| ex is SocketTimeoutException || SERVER_ERROR == ex.message.toString()
) HTTP_ERROR_TIP else ex.message.toString()

使用案例


定义网络请求接口


interface HttpApi {

@GET("/exampleA/exampleP/exampleI/exampleApi/getNetData")
suspend fun getNetData(@QueryMap params: HashMap<String, String>): BaseResp<NetDataBean>

@GET("/exampleA/exampleP/exampleI/exampleApi/getTestData")
suspend fun getTestData(
@Query("param1") param1: String,
@Query("param2") param2: String
)
: BaseResp<NetDataBean>

@GET("/exampleA/exampleP/exampleI/exampleApi/{id}")
fun getNetTask(
@Path("id") id: String,
@QueryMap params: HashMap<String, String>,
)
: Call<BaseResp<TaskBean>>

@FormUrlEncoded
@POST("/exampleA/exampleP/exampleI/exampleApi/confirm")
suspend fun confirm(@Field("id") id: String, @Field("token") token: String): BaseResp<String>

@FormUrlEncoded
@POST("/exampleA/exampleP/exampleI/exampleApi/upload")
suspend fun upload(@FieldMap params: Map<String, String>): BaseResp<String>

}

我们可以写一个网络请求帮助类,用于请求的创建。


class RequestHelper {

private val httpApi = RetrofitUtil.getAPI(HttpApi::class.java)

companion object {
val instance: RequestHelper by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
RequestHelper()
}
}

suspend fun getNetData(params: HashMap<String, String>) = httpApi.getNetData(params)

suspend fun getTestData(branchCode: String, token: String) =
httpApi.getTestData(branchCode, token)

suspend fun getNetTask(id: String, params: HashMap<String, String>) =
httpApi.getNetTask(id, params)

suspend fun confirm(id: String, token: String) = httpApi.confirm(id, token)

suspend fun upload(params: HashMap<String, String>) = httpApi.upload(params)

}

定义用户的意图和 UI 状态


// 定义用户意图
sealed class MainIntent {
object FetchData : MainIntent()
}

// 定义 UI 状态
sealed class MainUIState {
object Loading : MainUIState()
data class NetData(val data: NetDataBean?) : MainUIState()
data class Error(val error: String?) : MainUIState()
}

ViewModel 中做意图的处理和 UI 状态的变更,根据网络请求结果传递不同的状态,使用定义的扩展方法去执行网络请求,封装过后的网络请求就很简洁方便了,下面演示下具体使用。


class MainViewModel : ViewModel() {

val mainIntent = Channel<MainIntent>(Channel.UNLIMITED)

private val _mainUIState = MutableStateFlow<MainUIState>(MainUIState.Loading)
val mainUIState: StateFlow<MainUIState>
get() = _mainUIState

init {
viewModelScope.launch {
mainIntent.consumeAsFlow().collect {
if (it is MainIntent.FetchData) {
getNetDataResult()
}
}
}
}
// 使用
private fun getNetDataResult() = netRequest {
start { _mainUIState.value = MainUIState.Loading }
request {
val paramMap = hashMapOf<String, String>()
paramMap["param1"] = "param1"
paramMap["param2"] = "param2"
RequestHelper.instance.getNetData(paramMap)
}
success { _mainUIState.value = MainUIState.NetData(it) }
error { _mainUIState.value = MainUIState.Error(it) }
}

}

这样是不是看起来很简洁呢?接下来,Activity 负责发送意图和接收 UI 状态进行相关的处理就行啦!


class MainActivity : AppCompatActivity() {

private val viewModel by viewModels<MainViewModel>()
private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
initData()
observeViewModel()
}

private fun initData() {
lifecycleScope.launch {
// 发送意图
viewModel.mainIntent.send(MainIntent.FetchData)
}
}

private fun observeViewModel() {
lifecycleScope.launch {
viewModel.mainUIState.collect {
when (it) {
is MainUIState.Loading -> showLoading()
// 这里拿到网络请求返回的数据,根据业务自行操作,这里只做简单的显示。
is MainUIState.NetData -> showText(it.data.toString())
is MainUIState.Error -> showText(it.error)
}
}
}
}

private fun showLoading() {
binding.progressBar.visibility = View.VISIBLE
binding.netText.visibility = View.GONE
}

private fun showText(result: String?) {
binding.progressBar.visibility = View.GONE
binding.netText.visibility = View.VISIBLE
binding.netText.text = result
}

}

文件的上传与下载


如果是文件的上传和下载呢?其实文件还不太一样,这涉及到上传进度,文件的处理等方面,所以,为了方便开发使用,我们可以针对文件单独再做一下封装。


定义文件上传对象


data class UpLoadFileBean(val file: File, val fileKey: String)

自定义 RequestBody,从中获取上传进度。


class ProgressRequestBody(
private var requestBody: RequestBody,
var onProgress: ((Int) -> Unit)?,
) : RequestBody() {

private var bufferedSink: BufferedSink? = null

override fun contentType(): MediaType? = requestBody.contentType()

override fun contentLength(): Long {
return requestBody.contentLength()
}

override fun writeTo(sink: BufferedSink) {
if (bufferedSink == null) bufferedSink = createSink(sink).buffer()
bufferedSink?.let {
requestBody.writeTo(it)
it.flush()
}
}

private fun createSink(sink: Sink): Sink = object : ForwardingSink(sink) {
// 当前写入字节数
var bytesWritten = 0L

// 总字节长度
var contentLength = 0L

override fun write(source: Buffer, byteCount: Long) {
super.write(source, byteCount)

if (contentLength == 0L) {
contentLength = contentLength()
}

// 增加当前写入的字节数
bytesWritten += byteCount

CoroutineScope(Dispatchers.Main).launch {
// 进度回调
onProgress?.invoke((bytesWritten * 100 / contentLength).toInt())
}
}
}

}

创建 MultipartBody.Part


fun <T> createPartList(action: UpLoadFileAction<T>): List<MultipartBody.Part> =
MultipartBody.Builder().apply {
// 公共参数 token
addFormDataPart("token", ServiceManager.instance.getToken())

// 其他基本参数
action.params?.forEach {
if (it.key.isNotBlank() && it.value.isNotBlank()) {
addFormDataPart(it.key, it.value)
}
}

// 文件校验
action.fileData?.let {
addFormDataPart(
it.fileKey, it.file.name, ProgressRequestBody(
requestBody = it.file
.asRequestBody("application/octet-stream".toMediaTypeOrNull()),
onProgress = action.progress
)
)
}
}.build().parts

定义文件上传行为


class UpLoadFileAction<T> {

// 请求体
lateinit var request: (suspend () -> BaseResp<T>)
private set

lateinit var parts: List<MultipartBody.Part>

// 其他普通参数
var params: HashMap<String, String>? = null
private set

// 文件参数
var fileData: UpLoadFileBean? = null
private set

// 初始化参数
fun init(params: HashMap<String, String>?, fileData: UpLoadFileBean?) {
this.params = params
this.fileData = fileData
parts = createPartList(this)
}

var start: (() -> Unit)? = null
private set

var success: (() -> Unit)? = null
private set

var error: ((String) -> Unit)? = null
private set

var progress: ((Int) -> Unit)? = null
private set

var finish: (() -> Unit)? = null
private set

fun start(block: () -> Unit) {
start = block
}

fun success(block: () -> Unit) {
success = block
}

fun error(block: (String) -> Unit) {
error = block
}

fun progress(block: (Int) -> Unit) {
progress = block
}

fun finish(block: () -> Unit) {
finish = block
}

fun request(block: suspend () -> BaseResp<T>) {
request = block
}

}

同样,定义 ViewModel 的扩展函数,用来执行文件上传。


fun <T> ViewModel.upLoadFile(
block: UpLoadFileAction<T>.() -> Unit,
params: HashMap<String, String>?,
fileData: UpLoadFileBean?,
)
= viewModelScope.launch {
val action = UpLoadFileAction<T>().apply(block)
try {
action.init(params, fileData)
action.start?.invoke()
val result = action.request.invoke()
if (result.isSuccess()) {
action.success?.invoke()
} else {
action.error?.invoke(result.message)
}
} catch (ex: Exception) {
action.error?.invoke(getErrorTipContent(ex))
} finally {
action.finish?.invoke()
}
}

定义文件上传接口


interface HttpApi {
//...

@Multipart
@POST("/exampleA/exampleP/exampleI/exampleApi/uploadFile")
suspend fun uploadFile(@Part partLis: List<MultipartBody.Part>): BaseResp<String>

}

在 RequestHelper 中定义上传文件方法


class RequestHelper {

private val httpApi = RetrofitUtil.getAPI(HttpApi::class.java)

companion object {
val instance: RequestHelper by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
RequestHelper()
}
}

//...

suspend fun uploadFile(partList: List<MultipartBody.Part>) = httpApi.uploadFile(partList)

}

封装过后的文件上传就很简洁方便了,下面演示下具体使用。


private fun uploadMyFile() = upLoadFile(
params = hashMapOf("param1" to "param1", "param2" to "param2"),
fileData = UpLoadFileBean(File(absoluteFilePath), "file"),
) {
start {
// TODO: 开始上传,此处可以显示加载动画
}
request { RequestHelper.instance.uploadFile(parts) }
success {
// TODO: 上传成功
}
error {
// TODO: 上传失败
}
finish {
// TODO: 上传结束,此处可以关闭加载动画
}
}

既然上传文件都有了,那怎么少得了下载呢?其实,下载比上传更简单,下面就来写一下,同样利用了 kotlin 的函数式编程,我们添加 ViewModel 的扩展函数,需要注意的是,由于这边是直接使用 OkHttp 的同步请求,所以把这部分代码放在了 IO 线程中。


fun ViewModel.downLoadFile(
downLoadUrl: String,
dirPath: String,
fileName: String,
progress: ((Int) -> Unit)?,
success: (File) -> Unit,
failed: (String) -> Unit,
)
= viewModelScope.launch(Dispatchers.IO) {
try {
val fileDir = File(dirPath)
if (!fileDir.exists()) {
fileDir.mkdirs()
}
val downLoadFile = File(fileDir, fileName)
val request = Request.Builder().url(downLoadUrl).get().build()
val response = OkHttpClient.Builder().build().newCall(request).execute()
if (response.isSuccessful) {
response.body?.let {
val totalLength = it.contentLength().toDouble()
val stream = it.byteStream()
stream.copyTo(downLoadFile.outputStream()) { currentLength ->
// 当前下载进度
val process = currentLength / totalLength * 100
progress?.invoke(process.toInt())
}
success.invoke(downLoadFile)
} ?: failed.invoke("response body is null")
} else failed.invoke("download failed:$response")
} catch (ex: Exception) {
failed.invoke("download failed:${getErrorTipContent(ex)}")
}
}


// InputStream 添加扩展函数,实现字节拷贝。
private fun InputStream.copyTo(
out: OutputStream,
bufferSize: Int = DEFAULT_BUFFER_SIZE,
progress: (Long) -> Unit,
)
: Long {
var bytesCopied: Long = 0
val buffer = ByteArray(bufferSize)
var bytes = read(buffer)
while (bytes >= 0) {
out.write(buffer, 0, bytes)
bytesCopied += bytes
bytes = read(buffer)
progress(bytesCopied)
}
return bytesCopied
}

然后,使用就会变得很简洁了,如下所示:


fun downloadMyFile(downLoadUrl: String, dirPath: String, fileName: String) =
downLoadFile(
downLoadUrl = downLoadUrl,
dirPath = dirPath,
fileName = fileName,
progress = {
// TODO: 这里可以拿到进度
},
success = {
// TODO: 下载成功,拿到下载的文件对象 File
},
failed = {
// TODO: 下载失败,返回原因
}

)

作者:阿健君
来源:juejin.cn/post/7266768708139434045
收起阅读 »

Service 层异常抛到 Controller 层处理还是直接处理?

0 前言 一般初学者学习编码和[错误处理]时,先知道[编程语言]有一种处理错误的形式或约定(如Java就抛异常),然后就开始用这些工具。但却忽视这问题本质:处理错误是为了写正确程序。可是 1 啥叫“正确”? 由解决的问题决定的。问题不同,解决方案不同。 如一个...
继续阅读 »

0 前言


一般初学者学习编码和[错误处理]时,先知道[编程语言]有一种处理错误的形式或约定(如Java就抛异常),然后就开始用这些工具。但却忽视这问题本质:处理错误是为了写正确程序。可是


1 啥叫“正确”?


由解决的问题决定的。问题不同,解决方案不同。


如一个web接口接受用户请求,参数age,也许业务要求字段是0~150之间整数。如输入字符串或负数就肯定不接受。一般在后端某地做输入合法性检查,不过就抛异常。


但归根到底这问题“正确”解决方法总是要以某种形式提示用户。而提示用户是某种前端工作,就要看界面是app,H5+AJAX还是类似于[jsp]的服务器产生界面。不管啥,你要根据需求去”设计一个修复错误“的流程。如一个常见的流程要后端抛异常,然后一路到某个集中处理错误的代码,将其转换为某个HTTP的错误(业务错误码)提供给前端,前端再映射做”提示“。如用户输入非法请求,从逻辑上后端都没法自己修复,这是个“正确”的策略。


2 报500了嘞!


如用户上传一个头像,后端将图片发给[云存储],结果云存储报500,咋办?你可能想重试,因为也许仅是[网络抖动],重试就能正常执行。但若重试多次无效,若设计了某种热备方案,可能改为发到另一个服务器。“重试”和“使用备份的依赖”都是“立刻处理“。


但若重试无效,所有的[备份服务]也无效,也许就能像上面那样把错误抛给前端,提示用户“服务器开小差”。从这方案易看出,你想把错误抛到哪里是因为那个catch的地方是处理问题最方便的地方。一个问题的解决方案可能要几个不同的错误处理组合起来才能办到。


3 NPE了!


你的程序抛个NPE。这一般就是程序员的bug:



  • 要不就是程序员想表达一个东西”没有“,结果在后续处理中忘判断是否为null

  • 要不就是在写代码时觉得100%不可能为null的地方出现了一个null


不管哪种,这错误用户总会看到一个很含糊的报错信息,这远远不够。“正确”办法是程序员自己能尽快发现它,并尽快修复。要做到这点,需要[监控系统]不断爬log,把问题报警出来。而非等用户找客服投诉。


4 OOM了!


比如你的[后端程序]突然OOM挂了。挂的程序没法恢复自己。要做到“正确”,须在服务之外的容器考虑这问题。


如你的服务跑在[k8s],他们会监控你程序状态,然后重启新的服务实例弥补挂掉的服务,还得调整流量,把去往宕机服务的流量切换到新实例。这的恢复因为跨系统所以不能仅用异常实现,但道理一样。


但光靠重启就“正确”了?若服务是完全无状态,问题不大。但若有状态,部分用户数据可能被执行一半的请求搞乱。因此重启要留意先“恢复数据到合法状态”。这又回到你要知道咋样才是“正确”的做法。只依靠简单的语法功能不能无脑解决这事。


5 提升维度



  • 一个工作线程的“外部容器“是管理工作线程的“master”

  • 一个网络请求的“外部容器”是一个Web Server

  • 一个用户进程的“外部容器”是[操作系统]

  • Erlang把这种supervisor-worker的机制融入到语言的设计


Web程序很大程度能把异常抛给顶层,是因为:



  • 请求来自前端,对因为用户请求有误(数据合法性、权限、用户上下文状态)造成的问题,最终基本只能告诉用户。因此抛异常到一个集中处理错误的地方,把异常转换为某个业务错误码的方法,合理

  • 后端服务一般无状态。这也是软件系统设计的一般原则。无状态才意味着可随时随地安心重启。用户数据不会因为因为下一条而会出问题

  • 后端对数据的修改依赖DB的事务。因此一个改一半的、没提交的事务不会造成副作用。


但这3条件并非总成立。总能遇到:



  • 一些处理逻辑并非无状态

  • 也并非所有的数据修改都能用一个事务保护


尤其要注意对[微服务]的调用,对内存状态的修改是没有事务保护的,一不留神就会搞乱用户数据。比如下面代码段


6 难以排查的代码段


 try {
int res1 = doStep1();
this.status1 += res1;
int res2 = doStep2();
this.status2 += res2;
// 抛个异常
int res3 = doStep3();
this.status3 = status1 + status2 + res3;
} catch ( ...) {
// ...
}

先假设status1、status2、status3之间需维护某种不变的约束(invariant)。然后执行这段代码时,如在doStep3抛异常,下面对status3的赋值就不会执行。这时如不能将status1、status2的修改rollback,就会造成数据违反约束的问题。


而程序员很难发现这个数据被改坏了。坏数据还可能导致其他依赖这数据的代码逻辑出错(如原本应该给积分的,却没给)。而这种错误一般很难排查,从大量数据里找到不正确的那一小段何其困难。


7 更难搞定的代码段


// controller
void controllerMethod(/* 参数 */) {
try {
return svc.doWorkAndGetResult(/* 参数 */);
} catch (Exception e) {
return ErrorJsonObject.of(e);
}
}

// svc
void doWorkAndGetResult(/* some params*/) {
int res1 = otherSvc1.doStep1(/* some params */);
this.status1 += res1;
int res2 = otherSvc2.doStep2(/* some params */);
this.status2 += res2;
int res3 = otherSvc3.doStep3(/* some params */);
this.status3 = status1 + status2 + res3;
return SomeResult.of(this.status1, this.status2, this.status3);
}

难搞在于你写的时候可能以为doStep1~3这种东西即使抛异常也能被Controller里的catch。


在svc这层是不用处理任何异常,因此不写[try……catch]天经地义。但实际上doStep1、doStep2、doStep3任何一个抛异常都会造成svc的数据状态不一致。甚至你一开始都可以通过文档或其他沟通确定doStep1、doStep2、doStep3一开始都是必然可成功,不会抛错的,因此你写的代码一开始是对的。


但你可能无法控制他们的实现(如他们是另外一个团队开发的[jar]提供的),而他们的实现可能会改成抛错。你的代码可能在完全不自知情况下从“不会出问题”变成“可能出问题”…… 更可怕的类似代码不能正确工作:


void doWorkAndGetResult(/* some params*/) {
try {
int res1 = otherSvc1.doStep1(/* some params */);
this.status1 += res1;
int res2 = otherSvc2.doStep2(/* some params */);
this.status2 += res2;
int res3 = otherSvc3.doStep3(/* some params */);
this.status3 = status1 + status2 + res3;
return SomeResult.of(this.status1, this.status2, this.status3);
} catch (Exception e) {
// do rollback
}
}

你以为这样就会处理好数据rollback,甚至觉得这种代码优雅。但实际上doStep1~3每一个地方抛错,rollback的代码都不一样。


得这么写


void doWorkAndGetResult(/* some params*/) {
int res1, res2, res3;
try {
res1 = otherSvc1.doStep1(/* some params */);
this.status1 += res1;
} catch (Exception e) {
throw e;
}

try {
res2 = otherSvc2.doStep2(/* some params */);
this.status2 += res2;
} catch (Exception e) {
// rollback status1
this.status1 -= res1;
throw e;
}

try {
res3 = otherSvc3.doStep3(/* some params */);
this.status3 = status1 + status2 + res3;
} catch (Exception e) {
// rollback status1 & status2
this.status1 -= res1;
this.status2 -= res2;
throw e;
}
}

这才是得到正确结果的代码,在任何地方出错都能维护数据一致性。优雅吗?


看起来很丑。比go的if err != nil还丑。但要在正确性和优雅性取舍,肯定毫不犹豫选前者。作为程序员不能直接认为抛异常可解决任何问题,须学会写出有正确逻辑的程序,哪怕很难且看起来丑。


为达成高正确性,你不能总将自己大部分注意力放在“一切都OK的流程“,而把错误看作是可随便应付了事的工作或简单的相信exception可自动搞定一切。


8 总结


对错误处理要有敬畏之心:



  • Java因为Checked Exception设计问题不得不避免使用

  • 而Uncaughted Exception实在弱鸡,不能给程序员提供更好帮助


因此,程序员在每次抛错或者处理错误的时候都要三省吾身:



  • 这个错误的处理是正确吗?

  • 会让用户看到啥?

  • 会不会搞乱数据?


不要以为自己抛个异常就完事了。在[编译器]不能帮上太多忙时,好好写UT来保护代码可怜的正确性。


请多写正确的代码


作者:JavaEdge在掘金
来源:juejin.cn/post/7280050832949968954
收起阅读 »

当别人因为React、Vue吵起来时,我们应该做什么

web
大家好,我卡颂。 最近尤大的一个推文引起了不小热议,大概经过是: 有人在推上夸React文档写的好,把可能的坑点都列出来 尤看到后批评道:框架应该自己处理这些坑点,而不是把他们暴露给用户 尤大在推上的发言一直比较耿直,这次又涉及到React这个老对手,关...
继续阅读 »

大家好,我卡颂。


最近尤大的一个推文引起了不小热议,大概经过是:



  1. 有人在推上夸React文档写的好,把可能的坑点都列出来

  2. 尤看到后批评道:框架应该自己处理这些坑点,而不是把他们暴露给用户



尤大在推上的发言一直比较耿直,这次又涉及到React这个老对手,关注度自然不低。


再加上国内前端自媒体的一波引导发酵,比如知乎下这个话题相关的问题中的措辞是怒喷,懂得都懂。



在这样氛围与二手信源的影响下,会给人一种大佬都亲手下场撕了的感觉,自然会引来ReactVue各自拥趸的一番激烈讨论。


年年都是一样的套路,毫无新意......


面对这样的争吵,我们应该做什么呢?


首先,回到源头本身,尤大diss的有道理么?有。


React的心智负担重么?确实重。比如useEffec这个API,你能想象文档中一个章节居然有6篇文章是教你如何正确使用useEffec的么?



造成这一现象的原因有很多,比如:



  1. Hooks的实现原理使得必须显式声明依赖

  2. 显式声明依赖无法覆盖useEffect所有场景,为此专门提出一个叫Effect Event的概念,以及一个对应的新hook —— useEffectEvent

  3. useEffect承载了太多功能,比如未来Offscreen的显隐回调(类似Vue中的Keep-Alive)也是通过useEffect实现


当我们继续往前回溯,Hooks必须显式声明依赖React更新机制决定的,而React更新机制又是React实现原理的核心。


本质来说,还是React既往的成功、庞大的社区生态让他积重难返,无法从底层重写。


这是历史必然的进程,如果Vue所有新特性都在Vue2基础上迭代(而不是完全重写的Vue3),我相信也是同样的局面。


所以,当前React的迭代方向是 —— 支持上层框架(比如Next.jsRemix),寄希望于靠这些框架的封装能力弥补React自身心智负担重的缺点。这个策略显然也是成功的。


回到这次争吵本身,尤大不知道React文档为什么要花大篇幅帮开发者避坑(以及背后反映的积重难返)么?他显然是知道的。


他如此回复是因为他所处的位置是框架作者React是他的竞争对手。设想一下,如果你的竞争对手在一些方面确实不如你,但他的用户对此的反应不是“太难用了,我要换个好用的”,而是“一定是我用的姿势不对,你快出个文档好好教教我”


面对这样的用户,换谁都得有一肚子牢骚吧~



让我们再把视角转到React的用户(也就是我们这些普通开发者)上。我们为什么选择React呢?


可能有些人是处于喜好。但大部分开发者之所以用React,完全是因为公司要求用React


React的公司多,招React的岗位多,自然选择React的开发者就多了。


那么为什么用React的公司多呢?这显然是多年前React在先发优势、社区生态两场战役取胜后得到的结果。


总结


所以,我们需要尊重两个事实:



  1. React心智负担重是事实

  2. React的公司多也是事实


两者并不矛盾,他们都是历史进程的产物。


VueReact之间的讨论,即使是从技术层面出发,最后也容易陷入“React心智负担这么重,你们还甘之如饴,你们React党是不是傻”这样的争吵中。


这显然就是忽略了历史的进程。


正确的应对方式是多关心关心自己未来的发展:



  • 如果我的重心在海外,那应该给Next.js更多关注。海外远程团队不是Next就是Nest

  • 如果我的重心在国内,国内流量都被小程序分割了。一个长远的增长点应该是鸿蒙


作者:魔术师卡颂
来源:juejin.cn/post/7321589055883427855
收起阅读 »

线程数突增!领导说再这么写就gc掉我

线程数突增!领导说再这么写就gc掉我 前言 大家好,我是魔性的茶叶,今天给大家分享一个线上问题引出的一次思考,过程比较长,但是挺有意思。 今天上班把需求写完,出于学习(摸鱼)的心理上skywalking看看,突然发现我们的一个应用,应用内线程数超过900条,接...
继续阅读 »

线程数突增!领导说再这么写就gc掉我


前言


大家好,我是魔性的茶叶,今天给大家分享一个线上问题引出的一次思考,过程比较长,但是挺有意思。


今天上班把需求写完,出于学习(摸鱼)的心理上skywalking看看,突然发现我们的一个应用,应用内线程数超过900条,接近1000条,但是cpu并没有高涨,内存也不算高峰。但是敏锐的我还是立刻意识到这个应用有不妥,因为线程数太多了,不符合我们一个正常健康的应用数量。熟练的打出cpu dump观察,首先看线程组名的概览。


image-20230112200957387


从线程分组看,pool名开头线程占616条,而且waiting状态也是616条,这个点就非常可疑了,我断定就是这个pool开头线程池导致的问题。我们先排查为何这个线程池中会有600+的线程处于waiting状态并且无法释放,记接下来我们找几条线程的堆栈观察具体堆栈:


image-20230112201456234


这个堆栈看上去很合理,线程在线程池中不断的循环获取任务,因为获取不到任务所以进入了waiting状态,等待着有任务后被唤醒。


看上去不只一个线程池,并且这些线程池的名字居然是一样的,我大胆的猜测一下,是不断的创建同样的线程池,但是线程池无法被回收导致的线程数,所以接下来我们要分析两个问题,首先这个线程池在代码里是哪个线程池,第二这个线程池是怎么被创建的?为啥释放不了?


我在idea搜索new ThreadPoolExecutor()得到的结果是这样的:


image-20230112202915219


于是我陷入懵逼的状态,难道还有其他骚操作?


正在这时,一位不知名的郑网友发来一张截图:


image-20230112203527173


好家伙!竟然是用new FixedTreadPool()整出来的。难怪我完全搜不到,因为用的new FixedTreadPool(),所以线程池中的线程名是默认的pool(又多了一个不使用Executors来创建线程池的理由)。


然后我迫不及die的打开代码,试图找到罪魁祸首,结果发现作者居然是我自己。这是另一个惊喜,惊吓的惊。


冷静下来后我梳理一遍代码,这个接口是我两年前写的,主要是功能是统计用户的钱包每个月的流水,因为担心统计比较慢,所以使用了线程池,做了批量的处理,没想到居然导致了线程数过高,虽然没有导致事故,但是确实是潜在的隐患,现在没出事不代表以后不会出事。


去掉多余业务逻辑,我简单的还原一个代码给大家看,还原现场:


private static void threadDontGcDemo(){
      ExecutorService executorService = Executors.newFixedThreadPool(10);
      executorService.submit(() -> {
           System.out.println("111");
       });
   }

那么为啥线程池里面的线程和线程池都没释放呢


难道是因为没有调用shutdown?我大概能理解我两年前当时为啥不调用shutdown,是因为当初我觉得接口跑完,方法走到结束,理论上栈帧出栈,局部变量应该都销毁了,按理说executorService这个变量应该直接GG了,那么按理说我是不用调用shutdown方法的。


我简单的跑了个demo,循环的去new线程池,不调用shutdown方法,看看线程池能不能被回收


image-20230113142322106


打开java visual vm查看实时线程:


image-20230113142304644


可以看到线程数和线程池都一直在增加,但是一直没有被回收,确实符合发生的问题状况,那么假如我在方法结束前调用shutdown方法呢,会不会回收线程池和线程呢?


简单写个demo结合jvisualvm验证下:


image-20230113142902514


image-20230113142915722


结果是线程和线程池都被回收了。也就是说,执行了shutdown的线程池最后会回收线程池和线程对象


我们知道,一个对象能不能回收,是看它到gc root之间有没有可达路径,线程池不能回收说明到达线程池的gc root还是有可达路径的。这里讲个冷知识,这里的线程池的gc root是线程,具体的gc路径是thread->workers->线程池。线程对象是线程池的gc root,假如线程对象能被gc,那么线程池对象肯定也能被gc掉(因为线程池对象已经没有到gc root的可达路径了)。


那么现在问题就转为线程对象是在什么时候gc


郑网友给了一个粗浅但是合理的解释,线程对象肯定不是在运行中的时候被回收的,因为jvm肯定不可能去回收一条在运行中的线程,至少runnalbe状态的线程jvm不可能去回收。


在stackoverflow上我找到了更准确的答案:stackoverflow.com/questions/2…


image-20230113152802164


A running thread is considered a so called garbage collection root and is one of those things keeping stuff from being garbage collected。


这句话的意思是,一条正在运行的线程是gc root,注意,是正在运行,这个正在运行我先透露下,即使是waiting状态,也算正在运行。这个回答的整体的意思是,运行的线程是gc root,但是非运行的线程不是gc root(可以被回收)。


现在比较清楚了,线程池和线程被回收的关键就在于线程能不能被回收,那么回到原来的起点,为何调用线程池的shutdown方法能够导致线程和线程池被回收呢?难道是shutdown方法把线程变成了非运行状态吗


talk is cheap,show me the code


我们直接看看线程池的shutdown方法的源码


public void shutdown() {
       final ReentrantLock mainLock = this.mainLock;
       mainLock.lock();
       try {
           checkShutdownAccess();
           advanceRunState(SHUTDOWN);
           interruptIdleWorkers();
           onShutdown(); // hook for ScheduledThreadPoolExecutor
      } finally {
           mainLock.unlock();
      }
       tryTerminate();
}

private void interruptIdleWorkers() {
       interruptIdleWorkers(false);
}

private void interruptIdleWorkers(boolean onlyOne) {
       final ReentrantLock mainLock = this.mainLock;
       mainLock.lock();
       try {
           for (Worker w : workers) {
               Thread t = w.thread;
               if (!t.isInterrupted() && w.tryLock()) {
                   try {
                       t.interrupt();
                  } catch (SecurityException ignore) {
                  } finally {
                       w.unlock();
                  }
              }
               if (onlyOne)
                   break;
          }
      } finally {
           mainLock.unlock();
      }
}

我们从interruptIdleWorkers方法入手,这方法看上去最可疑,看到interruptIdleWorkers方法,这个方法里面主要就做了一件事,遍历当前线程池中的线程,并且调用线程的interrupt()方法,通知线程中断,也就是说shutdown方法只是去遍历所有线程池中的线程,然后通知线程中断。所以我们需要了解线程池里的线程是怎么处理中断的通知的。


我们点开worker对象,这个worker对象是线程池中实际运行的线程,所以我们直接看worker的run方法,中断通知肯定是在里面被处理了


//WOrker的run方法里面直接调用的是这个方法
final void runWorker(Worker w) {
       Thread wt = Thread.currentThread();
       Runnable task = w.firstTask;
       w.firstTask = null;
       w.unlock(); // allow interrupts
       boolean completedAbruptly = true;
       try {
           while (task != null || (task = getTask()) != null) {
               w.lock();
               // If pool is stopping, ensure thread is interrupted;
               // if not, ensure thread is not interrupted. This
               // requires a recheck in second case to deal with
               // shutdownNow race while clearing interrupt
               if ((runStateAtLeast(ctl.get(), STOP) ||
                    (Thread.interrupted() &&
                     runStateAtLeast(ctl.get(), STOP))) &&
                   !wt.isInterrupted())
                   wt.interrupt();
               try {
                   beforeExecute(wt, task);
                   Throwable thrown = null;
                   try {
                       task.run();
                  } catch (RuntimeException x) {
                       thrown = x; throw x;
                  } catch (Error x) {
                       thrown = x; throw x;
                  } catch (Throwable x) {
                       thrown = x; throw new Error(x);
                  } finally {
                       afterExecute(task, thrown);
                  }
              } finally {
                   task = null;
                   w.completedTasks++;
                   w.unlock();
              }
          }
           completedAbruptly = false;
      } finally {
           processWorkerExit(w, completedAbruptly);
      }
}



这个runwoker属于是线程池的核心方法了,相当的有意思,线程池能不断运作的原理就是这里,我们一点点看。


首先最外层用一个while循环套住,然后不断的调用gettask()方法不断从队列中取任务,假如拿不到任务或者任务执行发生异常(抛出异常了)那就属于异常情况,直接将completedAbruptly 设置为true,并且进入异常的processWorkerExit流程。


我们看看gettask()方法,了解下啥时候可能会抛出异常:


private Runnable getTask() {
       boolean timedOut = false; // Did the last poll() time out?

       for (;;) {
           int c = ctl.get();
           int rs = runStateOf(c);

           // Check if queue empty only if necessary.
           if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
               decrementWorkerCount();
               return null;
          }

           int wc = workerCountOf(c);

           // Are workers subject to culling?
           boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

           if ((wc > maximumPoolSize || (timed && timedOut))
               && (wc > 1 || workQueue.isEmpty())) {
               if (compareAndDecrementWorkerCount(c))
                   return null;
               continue;
          }

           try {
               Runnable r = timed ?
                   workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                   workQueue.take();
               if (r != null)
                   return r;
               timedOut = true;
          } catch (InterruptedException retry) {
               timedOut = false;
          }
      }
  }

这样很清楚了,抛去前面的大部分代码不看,这句代码解释了gettask的作用:


Runnable r = timed ?
  workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
  workQueue.take()

gettask就是从工作队列中取任务,但是前面还有个timed,这个timed的语义是这样的:如果allowCoreThreadTimeOut参数为true(一般为false)或者当前工作线程数超过核心线程数,那么使用队列的poll方法取任务,反之使用take方法。这两个方法不是重点,重点是poll方法和take方法都会让当前线程进入time_waiting或者waiting状态。而当线程处于在等待状态的时候,我们调用线程的interrupt方法,毫无疑问会使线程当场抛出异常


也就是说线程池的shutdownnow方法调用interruptIdleWorkers去对线程对象interrupt是为了让处于waiting或者是time_waiting的线程抛出异常


那么线程池是在哪里处理这个异常的呢?我们看runwoker中的调用的processWorkerExit方法,说实话这个方法看着就像处理抛出异常的方法:


private void processWorkerExit(Worker w, boolean completedAbruptly) {
       if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
           decrementWorkerCount();

       final ReentrantLock mainLock = this.mainLock;
       mainLock.lock();
       try {
           completedTaskCount += w.completedTasks;
           workers.remove(w);
      } finally {
           mainLock.unlock();
      }

       tryTerminate();

       int c = ctl.get();
       if (runStateLessThan(c, STOP)) {
           if (!completedAbruptly) {
               int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
               if (min == 0 && ! workQueue.isEmpty())
                   min = 1;
               if (workerCountOf(c) >= min)
                   return; // replacement not needed
          }
           addWorker(null, false);
      }
}

我们可以看到,在这个方法里有一个很明显的 workers.remove(w)方法,也就是在这里,这个w的变量,被移出了workers这个集合,导致worker对象不能到达gc root,于是workder对象顺理成章的变成了一个垃圾对象,被回收掉了。然后等到worker中所有的worker都被移出works后,并且当前请求线程也完成后,线程池对象也成为了一个孤儿对象,没办法到达gc root,于是线程池对象也被gc掉了。


写了挺长的篇幅,我小结一下:



  1. 线程池调用shutdownnow方法是为了调用worker对象的interrupt方法,来打断那些沉睡中的线程(waiting或者time_waiting状态),使其抛出异常

  2. 线程池会把抛出异常的worker对象从workers集合中移除引用,此时被移除的worker对象因为没有到达gc root的路径已经可以被gc掉了

  3. 等到workers对象空了,并且当前tomcat线程也结束,此时线程池对象也可以被gc掉,整个线程池对象成功释放


最后总结:


如果只是在局部方法中使用线程池,线程池对象不是bean的情况时,记得要合理的使用shutdown或者shutdownnow方法来释放线程和线程池对象,如果不使用,会造成线程池和线程对象的堆积。


作者:魔性的茶叶
来源:juejin.cn/post/7197424371991855159
收起阅读 »

面试官:手写一个“发布-订阅模式”

web
发布-订阅模式又被称为观察者模式,它是定义在对象之间一对多的关系中,当一个对象发生变化,其他依赖于它的对象收到通知。在javascript的开发中,我们一般用事件模型替代发布-订阅模式。 DOM事件 document.body.addEventListener...
继续阅读 »

发布-订阅模式又被称为观察者模式,它是定义在对象之间一对多的关系中,当一个对象发生变化,其他依赖于它的对象收到通知。在javascript的开发中,我们一般用事件模型替代发布-订阅模式。


DOM事件


document.body.addEventListener('click',function(){

alert(绑定1);

},false);

document.body.click(); //模拟点击

document.body.addEventListener('click',function(){

alert(绑定2);

},false);

document.body.addEventListener('click',function(){

alert(绑定3);

},false);

document.body.click(); //模拟点击

我们可以增加更多订阅者,不会对发布者的代码造成影响。注意,标准浏览器下用dispatchEvent实现。


自定义事件


① 确定发布者。(例如售票处)


② 添加缓存列表,便于通知订阅者。(预订车票列表)


③ 发布消息。遍历缓存列表。依次触发里面存放的订阅者回调函数(遍历列表,逐个发送短信)。


另外,我们还可以在回调函数填入一些参数,例如车票的价格之类信息。


let ticketOffice = {};    //售票处

ticketOffice.clientList = []; //缓存列表,存放订阅者的回调函数

ticketOffice.on = function (fn) { //增加订阅者
this.clientList.push(fn); //订阅的消息添加进缓存列表
};

ticketOffice.emit = function () { //发布消息
for(var i = 0, fn; fn = this.clientList[i++];){
fn.apply(this, arguments); //arguments 是发布消息时带上的参数
}
}

// 下面进行简单测试:

ticketOffice.on(function(time, path){ //小刚订阅消息
console.log('时间:' + time);
console.log('路线:' + path);
});


ticketOffice.on(function(time, path){ //小强订阅消息
console.log('时间:' + time);
console.log('路线:' + path);
});

ticketOffice.emit('晚上8:00','深圳-上海');
ticketOffice.emit('晚上8:10','上海-深圳');

至此,我们实现了一个最简单发布-订阅模式。不过这里存在一些问题,我们运行代码可以看到订阅者接收到了所有发布的消息。


// 时间:晚上8:00
// 路线:深圳-上海
// 时间:晚上8:00
// 路线:深圳-上海
// 时间:晚上8:10
// 路线:上海-深圳
// 时间:晚上8:10
// 路线:上海-深圳

我们有必要加个key让订阅者只订阅 自己感兴趣的消息。改写后的代码如下:


let ticketOffice = {};    //售票处

ticketOffice.clientList = []; //缓存列表,存放订阅者的回调函数

ticketOffice.on = function (key, fn) { //增加订阅者
if (!this.clientList[key]){
this.clientList[key] = [];
}
this.clientList[key].push(fn); //订阅的消息添加进缓存列表
};

ticketOffice.emit = function () { //发布消息
let key = Array.prototype.shift.call(arguments), //取出消息类型
fns = this.clientList[key]; //取出该消息对应的回调函数集合
if (!fns || fns.length === 0) {
return false;
}

fns.forEach(fn => {
fn.apply(this, arguments) //arguments 是发布消息时带上的参数
})
}

// --------- 测试数据 --------------
ticketOffice.on('上海-深圳', function(time){ //小刚订阅消息
console.log('小刚时间:' + time);
});

ticketOffice.on('深圳-上海', function(time){ //小强订阅消息
console.log('小强时间:' + time);
});

ticketOffice.emit('深圳-上海', '晚上8:00');
ticketOffice.emit('上海-深圳', '晚上8:10');

// 小强时间:晚上8:00
// 小刚时间:晚上8:10

这样子,订阅者就可以只订阅自己感兴趣的事件了。


小强临时行程有变,不想订阅对应的消息了,我们还需要再新增一个移除订阅的方法


let ticketOffice = {};    //售票处

ticketOffice.clientList = []; //缓存列表,存放订阅者的回调函数

ticketOffice.on = function (key, fn) { //增加订阅者
if (!this.clientList[key]){
this.clientList[key] = [];
}
this.clientList[key].push(fn); //订阅的消息添加进缓存列表
};

ticketOffice.emit = function () { //发布消息
let key = Array.prototype.shift.call(arguments), //取出消息类型
fns = this.clientList[key]; //取出该消息对应的回调函数集合
if (!fns || fns.length === 0) {
return false;
}

fns.forEach(fn => {
fn.apply(this, arguments) //arguments 是发布消息时带上的参数
})
}

ticketOffice.remove = function (key, fn) {
let fns = this.clientList[key]
if (!fns) return false

if (!fn) {
fns && (fns.length = 0)
} else {
fns.forEach((cb, i) => {
if (cb === fn) {
fns.splice(i, 1)
}
})
}
}

// --------- 测试数据 --------------
const xiaoGangOn = function(time){ //小刚订阅消息
console.log('小刚时间:' + time);
}
const xiaoQiangOn = function(time){ //小强订阅消息
console.log('小强时间:' + time);
}

ticketOffice.on('上海-深圳', xiaoGangOn);
ticketOffice.on('深圳-上海', xiaoQiangOn);

ticketOffice.remove('深圳-上海', xiaoQiangOn);

ticketOffice.emit('深圳-上海', '晚上8:00');
ticketOffice.emit('上海-深圳', '晚上8:10');

// 小刚时间:晚上8:10

至此,我们实现了一个相对完善的发布-订阅模式


但是可以看到使用数组进行时间的push和remove可能绑定相同的事件,且事件remove的效率低,我们可以用Set来替换Array


let ticketOffice = {};    //售票处

ticketOffice.clientList = []; //缓存列表,存放订阅者的回调函数

ticketOffice.on = function (key, fn) { //增加订阅者
if (!this.clientList[key]){
this.clientList[key] = new Set();
}
this.clientList[key].add(fn); //订阅的消息添加进缓存列表
};

ticketOffice.emit = function () { //发布消息
let key = Array.prototype.shift.call(arguments), //取出消息类型
fns = this.clientList[key]; //取出该消息对应的回调函数集合
if (!fns || fns.length === 0) {
return false;
}

fns.forEach(fn => {
fn.apply(this, arguments) //arguments 是发布消息时带上的参数
})
}

// 移除路线的单个订阅
ticketOffice.remove = function (key, fn) {
this.clientList[key]?.delete(fn)
}
// 移除路线的所有订阅
ticketOffice.removeAll = function (key) {
delete this.clientList[key]
}

// --------- 测试数据 --------------
const xiaoGangOn = function(time){ //小刚订阅消息
console.log('小刚时间:' + time);
}
const xiaoQiangOn = function(time){ //小强订阅消息
console.log('小强时间:' + time);
}

ticketOffice.on('上海-深圳', xiaoGangOn);
ticketOffice.on('深圳-上海', xiaoQiangOn);

ticketOffice.remove('深圳-上海', xiaoQiangOn);

ticketOffice.emit('深圳-上海', '晚上8:00');
ticketOffice.emit('上海-深圳', '晚上8:10');

// 小刚时间:晚上8:10

参考资料


《JavaScript 设计模式与开发实践》


作者:dudulala
来源:juejin.cn/post/7320075000702533671
收起阅读 »

思辨:移动开发的未来在哪?

前段时间在知乎看到关于移动开发未来的问题,就尝试回答了一下,也触发了我对移动开发未来的思考。移动开发未来怎么样? - 知乎 http://www.zhihu.com/question/61…什么是移动开发?我们口中说的移动开发是什么,从广义和狭义的角...
继续阅读 »

前段时间在知乎看到关于移动开发未来的问题,就尝试回答了一下,也触发了我对移动开发未来的思考。

image.png

移动开发未来怎么样? - 知乎 http://www.zhihu.com/question/61…

什么是移动开发?

我们口中说的移动开发是什么,从广义和狭义的角度分别来看下:

从广义角度来看,移动开发是指为移动设备(如智能手机、平板电脑等)创建软件、应用程序和服务的过程。这包括了为各种移动操作系统(如 iOS、Android 和 Windows Phone)设计、开发、测试和发布应用程序。移动开发旨在为用户提供高质量的、功能丰富的移动体验,以满足其日常需求和娱乐需求。广义上的移动开发可以包括原生应用程序开发、跨平台应用程序开发、移动网页应用程序开发,以及相关的后端服务和API开发等。

从狭义角度来看,移动开发通常指开发针对特定移动操作系统的应用程序,如 iOS 和 Android。这些应用程序通常使用特定于平台的编程语言(如 Swift 或 Kotlin)开发,并利用该平台的特性和功能。狭义的移动开发关注于为特定平台提供最佳的性能、用户体验和原生功能集成。这种开发方法需要对目标平台的技术细节和设计原则有深入了解,以便充分发挥其潜力。

这段内容是我问GPT4的生成的,针对移动开发的定义基本准确。移动开发涉及的的细分领域有非常多,比如:

  • 混合开发和跨平台框架
  • Framework和Kernel
  • 逆向安全
  • 音视频
  • 移动Web
  • 嵌入式

大家可以对照着自己的岗位要求,给自己所涉及的技术领域归个类,分析下市场的需求如何。

简单回顾一下

移动开发辉煌的十年也是移动互联网快速发展的十年,我还记得2015当年o2o百“团”大战的时候,各种创业公司,各行各业,只要你懂点移动开发就能找到不错的开发工作,那时候移动开发的培训机构也如雨后春笋一般诞生,培训个几个月可能就能获得offer。现在的滴滴、美团都是当年烧钱大户,通过庞大的资本,持续打补贴战,最后才活下来,也是寥寥无几的几家独角兽创业公司。通过烧钱的方式毕竟是不可持续的,经营一家公司必须有足够竞争力的产品和可持续的商业模式,当年的泡沫被刺破之后,你才知道什么公司在裸奔,回过头想想现在还有多少家公司能幸存至今呢。

回到今年2023年,疫情三年让整个中国经济都是千疮百孔,不知道大家是否发现这些年基本没有什么新的独角兽出现了,基本上10年前的成立的公司,跑出来成为新的大厂的我们手指头能数得过来,比如我们熟知字节跳动,因为抖音短视频,直接在短视频领域突围成为了打破了老牌大厂腾讯在社交垄断下的新的巨头,成为新的BAT中的B。

另外附上一张2022年中国互联网综合实力企业排名:

image.png

大家是否发现自己手机上常用的App基本集中在这些我们耳熟能详的企业里面。其他的App要么访问量很少,要么永远消失在你的应用列表当中,可叹可惜。所以App的消亡带来的就是移动端的夕阳西下,除了大厂和中厂还有移动客户端的需求,但也是一坑难求,对求职者的要求基本上是要中高级别的,初级的刚毕业的基本上很难拿到offer。

这里我从自己的理解分析了移动开发目前的情况,从历史进程和供需关系,我们可以看到移动开发的求职环境已经大不如前,所以如果还想进入互联网从事移动开发就要结合自身情况去考虑,或许你需要积累得更多才能在残酷的求职环境中脱颖而出拿到心仪的offer。

个人的一些思考

先说说我个人的情况,自从14年毕业之后一直从事移动开发,岗位是Android工程师,基本也算是赶上了移动互联网发展的快车道,求职路上基本上也没遇到什么坎坷,当时也算是比较幸运毕业一年半左右,以社招的身份面试进入到了腾讯,然后就一直待到现在。期间做过研发工具,比如Bugly Crash上报,应用更新和热更新;做过教育产品,比如腾讯课堂;目前投身于金融科技领域,做创新硬件上层应用相关的开发。主要的技术栈还是Android、Java/Kotlin,目前因为业务的需要,技术栈就开始涉足Linux嵌入式和C/C++。其实我个人也一直求变,不管是业务方向还是技术,危机感也在驱使着我去在专业领域获得更多的成长。作为技术人只能保持饥饿感,不停的更新自己的知识体系。

针对移动开发的未来,我个人还是保持谨慎乐观的态度的,虽然当下的求职环境发生了变化,但存量市场需求依然有很多机会,以下是我认为值得我们去关注的技术方向,但不作为任何求职建议:

  1. AIGC+移动端

2023年的AIGC的火热空前绝后,它带来的影响是非常深远的,甚至能够变革整个互联网行业,很多产品可能将会以新的思路去重构和延伸,这里面就会产生相应的在移动端和AIGC结合相关产品和业务,公司层面也会有相应的投入意愿,这也许会给我们带来新的机会。

  1. 元宇宙:VR/AR/XR

元宇宙虽然被炒概念,一直不温不火的,但这里面涉及的技术是比较前沿的,在游戏领域跟元宇宙的结合,如果能找到愿意投入企业,未尝不是一个不错的方向。

  1. IoT物联网

万物互联方向,比如智能家居,智能创新硬件产品,类似小米IoT相关的产品,智能手环、扫地机器人等等。这里面也有庞大的市场需求,另外软硬件结合对开发人员要求更高,更接近底层。

  1. 新能源车载系统

新能源车的其中一个核心就是智能中控,比如特斯拉的中控系统是Linux,比亚迪还有蔚小理和大多数造车新势力用的是Android系统,这里面也有很多车载系统应用的需求,也是很多人都求职热门方向。

  1. 音视频技术领域

当下流行的短视频,涉及到的核心就是音视频技术,有这方面的技术积累的同学应该也能获得不错的发展机会,而且这方面的人才相对而言比较稀缺。

  1. 跨平台技术

从企业降本的角度,未来可能会更倾向招聘懂跨平台开发的,希望能统一技术栈能够实现多端发布的能力。比如Flutter、React Native、UniApp等。

  1. 鸿蒙OS应用开发

国产替代是个很深远的话题,卡脖子问题现在越演越烈,从软件产业我们跟漂亮国还存在很多差距,我们能够正视这些差距并且迎头突围是一个非常值得敬佩和骄傲的事情。鸿蒙OS有望成为第一个完全去Android化的操作系统,Mate60系列手机产品我认为是一个标志性里程碑,我们不谈什么遥遥领先,我相信华为一定会越来越好,鸿蒙OS应用开发也是我觉得有较好前景的方向。

当然还有很多其他技术方向无法一一列举,我个人觉得一专多能可能是未来我们更应追求的目标,仅靠写几个UI页面就能打天下的时代已经不再适用了,想让自己有足够的竞争力,就必须要多涉猎各种技术,打通任督二脉,很多时候单一视角很难获得创新,只有多维度思考才有可能让自己突围。

最后

作为互联网从业人员,保持一定的危机感是必要的,另外多扩展自己的视野,除了专注于本身的专业领域,也要多关注技术趋势的变化,很多时候技术的价值是需要匹配业务的。移动开发有没有未来这个问题可以转化为:我们自己当前要做哪些选择,才能让自己拥有更多的未来。最后跟大家分享一句话作为结尾:

个人努力固然重要,也要考虑历史进程。


作者:巫山老妖
来源:juejin.cn/post/7292347319431790607
收起阅读 »

勇闯体制内00后:丢自己的脸,要领导的命

最近刷到00后在体制内上班,差点没给我人笑没。别的00后忙着整顿职场,而体制内的00后都在出尽洋相。众所周知,体制内工作跟普通的职场不大一样。工作内容比较接地气,工作能力比较看交际。这批进了体制内的00后,看着是端起了铁饭碗,实际上端碗的手,没有一天不在抖。三...
继续阅读 »


最近刷到00后在体制内上班,差点没给我人笑没。


别的00后忙着整顿职场,而体制内的00后都在出尽洋相。


众所周知,体制内工作跟普通的职场不大一样。


工作内容比较接地气,工作能力比较看交际。


这批进了体制内的00后,看着是端起了铁饭碗,实际上端碗的手,没有一天不在抖。


三天一大错两天一小错,每天都在勇闯体制的边缘嘚瑟。

00后前脚上岸,后脚怀疑自己该不会是个原装的傻子。


基本特征是“沉默寡言、体弱多病、孤僻内向且不善交际。”


干活主打的就是一个迷茫,说话前不着村后不着店儿。


领导上一秒说完,他下一秒就忘。



开会的时候他人五人六,把小本摆出来咔咔往上写;


完事说看看你整的会议纪要,他开始阿巴阿巴不敢吱声。



表面奋笔疾书,实际上00后的小本打开是这样的:



还有这样的:



主打一个领导说前门楼子,他在那扯胯骨轴子。


初进了体制内的00后,觉得自己像个傻子,又不只是个傻子;


还有点像腼腆的哑巴,和想努力但就是做不好的笨蛋。



周一上班碰到领导,迎面忘了领导叫啥;


轻则直接摆摆手一个“嗨”,重则四目相对毫无反应,原地飘过去了。


隔天见面敢打招呼,但又记错了领导的职称,张嘴直接给人降了半个级别。


刚进单位时头像还是这种不被重用的傻大姐人设:



后经领导点拨,改成静待花开风格,就算是当笨蛋,不如当个看起来沉稳点的笨蛋:



但头像的玄学作用,在体制内明显受限。


由于太没有眼力价,还不会跟人打交道,部分00后的愚蠢人设还是焊死在身上了。



典型的就是让众多00后显眼包,又爱又恨的酒局修罗场。


爱的是可以把酒局当搂席,恨的是自己酒量真不咋地。


凡开席必须把好吃的端自己跟前,不爱吃的放在领导前面。


前不久有个新闻,某国企会议结束有个晚宴,领导让新来的00后安排;


00后风风火火把晚宴安排到了自己爱的重庆火锅饭店,领导的心情跟怨种特效完美搭配上。


说到酒局,体制里的00后,酒量也不是不行,而是压根就没有,吃饭基本就得坐小孩那桌。



周围人花式敬酒,他低头扒拉米饭。


周围人换上白酒,他拿白水伪装白酒,还拿的热水,满桌子就他一个酒杯里冒热气。


不会喝酒也没事,问题是打进门他一屁股坐到主位上;


别人敬酒寒暄讨好领导,中间还隔着个他这个怨种。



还有的朋友更离谱,领导敬酒他端起饮料,领导低头他把酒往领导鞋上倒。


领导端酒杯致辞,他端个空杯还来回晃荡。


老员工以为这莫不是传说中的00后来整顿职场了?


00后听完把心一沉,想着自己哪有那个心眼子,不过是没有眼力价罢了。


喝酒他不行,但干饭他第一名;


别人吃完半天了,起身前还拍拍他问问吃饱了吗?不行咱就打包。



有的饭局结束了当事人还纳闷,为啥整个晚上自己的饮料杯从来没空过?


后来破案了,副局长全程给他倒了五次豆奶,同事直呼还得是00后牛逼。


不论e人还是i人,进了体制内一律按i人处理。


00后睡前都在给自己洗脑,告诉自己明天会更好;



隔天见了人还是想躲,结果不是去食堂碰到主任,就是去洗澡碰到书记,命运就是如此眷顾,想逃都逃不掉。


为了避免跟领导有眼神接触,有的路过局长办公室,浑身僵硬眼神失焦不敢歪头;


有的在乡镇工作,地方不大还研究躲避路线,真是外向不了一点。


还有的被点名参加合唱比赛,主任问她“你想参加吗?”


她直接用问题回答问题,打了主任一个措手不及“我想参加吗?”


心里其实想的是让我登台献艺,比杀了我还难受。


你永远想象不到00后在体制内是怎么活下来的,毕竟他们这新脑子完全不够用。



不是走廊里走路给老领导一杵子的,就是抬手倒水把领导茶杯盖给碰掉。


职场的打工人上班如上坟,最多就是钱难挣屎难吃;


而体制内的00后,月薪1800拿命往里搭;


每天都觉得脑子有点痒痒的,期待着赶紧长个新脑子吧。

为啥体制内的00后总担心自己闯大祸?


上岸来之不易,两眼一睁,担心竞争。


在某书上搜索体制内的00后,个个都像热锅上的蚂蚁,整天琢磨如何快速适应工作环境。


有人担心单位不让染发,也不能美甲;


有人上网寻求穿搭秘籍,准备放弃穿衣自由,走向局里局气;



有人担心听不明白话,转而研究领导语言习惯和工作中的花式暗语;


也有人按时按点写自己的闯祸日记,有的按天写,有的是周记;

重点记录每日上班遭遇,研究今天丢脸有没有比昨天少那么一点。



偶尔发现隔壁同事姐姐也会把茶水浇到副书记身上,顿时就变得很安心了,看到大家都和自己一样呆呆的,真好啊。


从小科员要掌握的办公字体,到对付老油条停止自我内耗。


再到上传下达“文经我手必熟悉”,硬着头皮记住各种可以提高效率的铁律。


日常给自己加油打气,隔天出了问题立马又泄气。



想起白天犯蠢想到失眠,打开手机又不小心点开高情商问题:


“和领导打羽毛球你赢了,领导说,我老了不中用了,你如何回答。”


看到坐标山东的网友油腻且不失风趣的回答,00后默默赞叹不愧是命里带编,下一秒赶紧把模版熟记于心。


这届进了体制内的00后,一边担心闯祸丢脸,一边又害怕自己过于被边缘;


上班前还以为体制内工作会很清闲,做好了泡壶茶水坐一天的心理预期,结果真上了班发现并不是这么简单。


基层工作跑断腿,总结汇报想流泪,更别说复杂的人际关系,直接让人身心俱疲。


甭管是体制内还是职场里,对刚工作的新人来说,总是最胆战心惊的那个。


不过话又说回来,涉世未深才有资格闯祸;


也只有清澈愚蠢的年轻人,对待人生第一份工作还肯花心思,瞎琢磨。


总有一天,爱闯祸的笨蛋,会变成真正的“大人”。


把头发梳成帅气的模样,在各种场合里游刃有余。


做着曾经最不擅长的事儿,也是最不喜欢的事儿。


或早或晚,都会长大。


眼下不如放轻松,“无论你多早迎接这清晨,在路上,都会有人在。”



作者:英才校园招聘
来源:mp.weixin.qq.com/s/UIKucQDDD5CAglfuTIzqlA

收起阅读 »

史上最全的2024罗振宇跨年演讲思维导图

作者:PMO前沿来源:mp.weixin.qq.com/s/hgB7g_F6ArrPgnAkg0tuyA















作者:PMO前沿
来源:mp.weixin.qq.com/s/hgB7g_F6ArrPgnAkg0tuyA

普通的文本输入框无法实现文字高亮?试试这个highlightInput吧!

web
背景 前几天在需求评审的时候,产品问我能不能把输入框也做成富文本那样,在输入一些敏感词、禁用词的时候,给它标红。我听完心里一颤,心想:好家伙,又来给我整活。本着能砍就砍的求生法则,我和产品说:输入框输入的文字都会被转成字符串,这没办法去标红呀!产品很硬气的回到...
继续阅读 »

背景


前几天在需求评审的时候,产品问我能不能把输入框也做成富文本那样,在输入一些敏感词、禁用词的时候,给它标红。我听完心里一颤,心想:好家伙,又来给我整活。本着能砍就砍的求生法则,我和产品说:输入框输入的文字都会被转成字符串,这没办法去标红呀!产品很硬气的回到:没办法,这是老板提的需求,你下去研究研究吧。行吧,老板发话说啥也没用,开干吧!


实现思路


实现标红就需要给文字加上html标签和样式,但是输入框会将html都转为字符串,既然输入框无法实现,那么我们换一种思路,通过div代替输入框来显示输入的文本,那我们是不是就可以实现文本标红了?话不多说,直接上代码(文章结尾会附上demo):


<div class="main">
<div id="shadowInput" class="highlight-shadow-input"></div>
<textarea
id="textarea"
cols="30"
rows="10"
class="highlight-input"
>
</textarea>
</div>

.main {
position: relative;
}
.highlight-shadow-input {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
padding: 8px;
border: 1px;
box-sizing: border-box;
font-size: 12px;
font-family: monospace;
overflow-y: auto;
word-break: break-all;
white-space: pre-wrap;
}
.highlight-input {
position: relative;
width: 100%;
padding: 8px;
box-sizing: border-box;
font-size: 12px;
background: rgba(0, 0, 0, 0);
-webkit-text-fill-color: transparent;
z-index: 999;
word-break: break-all;
}

实现这个功能的精髓就在于将输入框的背景和输入的文字设置为透明,然后将其层级设置在div之上,这样用户既可以在输入框中输入,而输入的文字又不会展示出来,然后将输入的文本处理后渲染到div上。


const textarea = document.getElementById("textarea");
const shadowInput = document.getElementById("shadowInput");
const sensitive = ["敏感词", "禁用词"];
textarea.oninput = (e) => {
let value = e.target.value;
sensitive.forEach((word) => {
value = value.replaceAll(
word,
`<span style="color:#e52e2e">${word}</span>`
).replaceAll("\n", "<br>");;
});
shadowInput.innerHTML = value;
};

监听输入框oninput事件,用replaceAll匹配到敏感词并转为html后渲染到shadowInput上。此外,我们还需要对输入框的滚动进行监听,因为shadowInput是固定高度的,如果用户输入的文本出现滚动条,则需要让shadowInput也滚动到对应的位置


<div><div id="shadowInput" class="highlight-shadow-input"></div></div>

textarea.onscroll = (e) => {
shadowInput.scrollTop = e.target.scrollTop;
};
// 此处输入时也需要同步是因为输入触底换行时,div的高度不会自动滚动
textarea.onkeydown = (e) => {
shadowInput.scrollTop = e.target.scrollTop;
};

最终实现效果:


至此一个简单的文本输入框实现文字高亮的功能就完成了,上述代码只是简单示例,在实际业务场景中还需要考虑xss注入、特殊字符处理、特殊字符高亮等等复杂问题。


总结


这篇文章主要给遇到有类似业务需求的同学一个参考,以及激发大家的灵感,用这种方法是不是还可以实现一些简单的富文本功能呢?例如文字加删除线、文字斜体加粗等等。有想法或有问题的小伙伴可以在评论区留言一起探讨哦!


demo



作者:宇智波一打七
来源:juejin.cn/post/7295169886177918985
收起阅读 »

多行标签超出展开折叠功能

web
前言  记录分享每一个日常开发项目中的实用小知识,不整那些虚头巴脑的框架理论与原理,之前分享过抽奖功能、签字功能等,有兴趣的可以看看本人以前的分享。  今天要分享的实用小知识是最近项目中遇到的标签相关的功能,我不知道叫啥,姑且称之为【多行标签展开隐藏】功能吧,...
继续阅读 »

前言


 记录分享每一个日常开发项目中的实用小知识,不整那些虚头巴脑的框架理论与原理,之前分享过抽奖功能、签字功能等,有兴趣的可以看看本人以前的分享。
 今天要分享的实用小知识是最近项目中遇到的标签相关的功能,我不知道叫啥,姑且称之为【多行标签展开隐藏】功能吧,类似于多行文本展开折叠功能,如果超过最大行数则显示展开隐藏按钮,如果不超过则不显示按钮。多行文本展开与折叠功能在网上有相当多的文章了,也有许多开源的封装组件,而多行标签展开隐藏的文章却比较少,刚好最近我也遇到了这个功能,所以就单独拿出来与大家分享如何实现。


出处


 【多行标签展开与隐藏】该功能我们平时可能没注意一般在哪里会有,其实最常见的就是各种APP的搜索页面的历史记录这里,下面是我从拼多多(左)和腾讯学堂小程序(右)截下来的功能样式:


多行标签案列图(pdd/txxt)


其它APP一般搜索的历史记录这里都有这个小功能,比如京东、支付宝、淘宝、抖音、快手等,可能稍有点儿不一样,有的是按钮样式,有的是只有展开没有收起功能,可能我们用过了很多年平时都没有注意到这个小功能,有想了解的可以去看一看哈。如果有一天你们需要开发一个搜索页面的话产品就很有可能出这样的一个功能,接下来我们就来看看这种功能我们该如何实现。


功能实现


我们先看实现的效果图,然后再分析如何实现,效果图如下:



【样式一】:标签容器和展开隐藏按钮分开(效果图样式一)


 标签容器和按钮分开的这种样式功能实现起来的话我个人觉得难度稍微简单一些,下面我们看看如何实现这种分开的功能。


第一种方法:通过与第一个标签左偏移值对比实现

原理:遍历每个标签然后通过与第一个标签左偏移值对比,如果有几个相同偏移值则说明有几个换行


具体实现上代码:


<div class="list-con list-con-1">
<div class="label">人工智能div>
<div class="label">人工智能与应用div>
<div class="label">行业分析与市场数据div>
<div class="label">标签标签标签标签标签标签标签标签div>
<div class="label">标签div>
<div class="label">啊啊啊div>
<div class="label">宝宝贝贝div>
<div class="label">微信div>
<div class="label">吧啊啊div>
<div class="label">哦哦哦哦哦哦哦哦div>
div>
<div class="expand expand-1">展开 ∨div>



解析:HTML布局就不用多说了,是个前端都知道该怎么搞,如果不知道趁早送外卖去吧,多说无益,把机会留给其他人。其次CSS应该也是比较简单的,注意的是有个前提需要先规定容器的最大高度,然后使用overflow超出隐藏,这样展开就直接去掉该属性,让标签自己撑开即可。JavaScript部分我这里没有使用啥框架,因为这块实现就是个简单的Demo所以就用纯原生写比较方便,这里我们先获取容器,然后获取容器的孩子节点(这里我们也可以直接通过className查询出所有标签元素),返回的是一个可遍历的变签对象,然后我们记录第一个标签的offsetLeft左偏移值,接下来遍历所有的标签元素,如果有与第一个标签相同的值则累加,最终line表示有几行,如果超过我们最大行数(demo超出2行隐藏)则显示展开隐藏按钮。


第二种方法:通过计算容器高度对比

原理:通过容器底部与标签top比较,如果有top值大于容器底部bottom则表示超出容器隐藏。


具体上代码:




解析:HTMLCSS同方法一同,不同点在于这里是通过getBoundingClientRect()方法来判断,还是遍历所有标签,不同的是如果有标签的top值大于等于了容器的bottom值,则说明了标签已超出容器,则要显示展开隐藏按钮,展开隐藏还是通过容器overflow属性来实现比较简单。


【样式二】:展开隐藏按钮和标签同级(效果图样式二)


 这种样式也是绝大部分APP产品使用的风格,不信你可以打开抖音商城或汽车之家的搜索历史,十个产品九个是这样设计的,不是这样的我倒立洗头。
 这种放在同级的就相对稍微难一点,因为要把展开隐藏按钮塞到标签的最后,如果是隐藏的话就要切割标签展示数量,那下面我就带大家看看我是是如何实现的。


方法一:通过遍历高度判断

原理:同样式一的高度判断一样,通过容器底部bottom与标签top比较,如果有top值大于容器顶部bottom则表示超出容器隐藏,不同的是如何计算标签展示的长度。有个前提是按钮和标签的的宽度要做限制,最好是一行能放一个标签和按钮。


具体实现上代码:


<div id="app3">
<div class="list-con list-con-3" :class="{'list-expand': isExpand}">
<div class="label" v-for="item in labelArr.slice(0, labelLength)">{{ item }}div>
<div class="label expand-btn" v-if="showExpandBtn" @click="changeExpand">{{ !isExpand ? '展开 ▼' : '隐藏 ▲' }}div>
div>
div>


<script>
const { createApp, nextTick } = Vue
createApp({
props: {
maxLine: {
type: Number,
default: 2
}
},
data () {
return {
labelArr: [],
isExpand: false,
showExpandBtn: false,
labelLength: 0,
hideLength: 0
}
},
mounted () {
const labels = ['人工智能', '人工智能与应用', '行业分析与市场数据', '标签标签标签标签标签标签标签', '标签A', '啊啊啊', '宝宝贝贝', '微信', '吧啊啊', '哦哦哦哦哦哦哦哦', '人工智能', '人工智能与应用']

this.labelArr = labels
this.labelLength = labels.length
nextTick(() => {
this.init()
})
},
methods: {
init () {
const listCon = document.querySelector('.list-con-3')
const labels = listCon.querySelectorAll('.label:not(.expand-btn)')
const expandBtn = listCon.querySelector('.expand-btn')

let labelIndex = 0 // 渲染到第几个
const listConBottom = listCon.getBoundingClientRect().bottom // 容器底部距视口顶部距离
for(let i = 0; i < labels.length; i++) {
const _top = labels[i].getBoundingClientRect().top
if (_top >= listConBottom ) { // 如果有标签顶部距离超过容器底部则表示超出容器隐藏
this.showExpandBtn = true
console.log('第几个索引标签停止', i)
labelIndex = i
break
} else {
this.showExpandBtn = false
}
}
if (!this.showExpandBtn) {
return
}
nextTick(() => {
const listConRect = listCon.getBoundingClientRect()
const expandBtn = listCon.querySelector('.expand-btn')
const expandBtnWidth = expandBtn.getBoundingClientRect().width
const labelMaringRight = parseInt(window.getComputedStyle(labels[0]).marginRight)
for (let i = labelIndex -1; i >= 0; i--) {
const labelRight = labels[i].getBoundingClientRect().right - listConRect.left
if (labelRight + labelMaringRight + expandBtnWidth <= listConRect.width) {
this.hideLength = i + 1
this.labelLength = this.hideLength
break
}
}
})
},
changeExpand () {
this.isExpand = !this.isExpand
console.log(this.labelLength)
if (this.isExpand) {
this.labelLength = this.labelArr.length
} else {
this.labelLength = this.hideLength
}
}
}
}).mount('#app3')
script>


解析:同级样式Demo我们使用vue来实现,HTML布局和CSS样式没有啥可说的,还是那就话,不行真就送外卖去比较合适,这里我们主要分析一下Javascript部分,还是先通过getBoundingClientRect()方法来获取容器的bottom和标签的top,通过遍历每个标签来对比是否超出容器,然后我们拿到第一个超出容器的标签序号,就是我们要截断的长度,这里是通过数组的slice()方法来截取标签长度,接下来最关建的如何把按钮拼接上去,因为标签的宽度是不定的,我们要把按钮显示在最后,我们并不确定按钮拼接到最后是不是会导致宽度不够超出,所以我们倒叙遍历标签,如果(最后一个标签的右边到容器的距离right值+标签的margin值+按钮的width)和小于容器宽度,则说明展示隐藏按钮可以直接拼接在后面,否则标签数组长度就要再减一位来判断是否满足。然后展开隐藏功能就通过切换原标签长度和截取的标签长度来完成即可。


方法二:通过与第一个标签左偏移值对比实现

原理:同样式一的方法原理,遍历每个标签然后通过与第一个标签左偏移值对比判断是否超出行数,然后长度截取同方法一一致。


直接上代码:




这里也无需多做解释了,直接看代码即可。


结尾


上面就是【多行标签展开隐藏】功能的基本实现原理,网上相关实现比较少,我也是只用了Javascript来实现,如果可以纯靠CSS实现,有更简单或更好的方法实现可以留言相互交流学。代码没有封装成组件,但是具有一些参考意义,用于生产可以自己去封装成组件使用,完整的代码在我的GitHub仓库。




作者:Liben
来源:juejin.cn/post/7251394142683742269
收起阅读 »

村超,淄博烧烤,哈尔滨,本质都在做情绪价值这门生意

最近哈尔滨火了,人们对它的称呼也从之前的哈尔滨变成了尔滨,一个字就把南北的距离拉近了。 还有什么南方小土豆,在我看来也是挺讲究的,虽然网络人会有一部分人会反感这个称呼,但是大多数人还是比较吃这一套的。 如果叫南方小豆角,南方小冬瓜,我相信大多数人就会急了,因为...
继续阅读 »

最近哈尔滨火了,人们对它的称呼也从之前的哈尔滨变成了尔滨,一个字就把南北的距离拉近了。


还有什么南方小土豆,在我看来也是挺讲究的,虽然网络人会有一部分人会反感这个称呼,但是大多数人还是比较吃这一套的。


如果叫南方小豆角南方小冬瓜,我相信大多数人就会急了,因为这两个词听起来都不可爱,还会有地域歧视的意思。


虽然都是蔬菜,但是南方小土豆就不一样,虽然大家都知道里面有南方人矮的意思,但是大多数人不反感,因为现在很多家长都会叫自己的孩子小土豆,养一个宠物也有叫土豆,所以说土豆,其实带有可爱的意思。


所以就南方小土豆这个称呼,就带动了很大的流量,就打破了这么多年来对东北的刻板映像。


但是就因为一个称呼就能带动那么多的人赶往东北吗?就为了去听一句南方小土豆吗?


我想显然不是,也并不是谁都愿意听的,也有一些游客去以后说反感这个称呼。


那么究其原因,其实本质还是在做情绪价值这门生意。


而情绪价值的背后是什么?


是长久积攒的急需释放的情绪和看透生活后的无能为力。


怎么理解呢?


我发现一个现象,包括我本人也是这样。


在疫情之前,我在很多地方看到有人卖唱,下面的人基本都在听,跟着唱的人不多。


但是近年来,只要有卖唱歌手的地方,基本上大家都会蜂拥上去吼上几句,有甚者直接流着眼泪大声歌唱。


因为大家都从之前的内卷中失望了,生活很大程度上并不会因为努力而发生变化,就不太和自己较真了,从而将重心移到了生活中来。


而市井,热闹就是生活的最真实写照,不需要花多少钱就能释放情绪,收获快乐。


贵州村超淄博烧烤,再到哈尔滨,都能得到很好的体现。


我们还发现一个问题,这几个城市都是比较落后的,其实并没有什么吸引人的地方,景区,经济,文化其实都没有什么突出的地方。


但是有一个特点,那就是消费便宜


你想一下,如果要在香港,澳门,上海这些城市打造这样的活动,做这样的城市IP,现实吗?


我想不现实,因为消费太高,大多数人承受不起。


你想,开一个好一点的酒店都要不少钱,还有吃和也是很贵,加上处于经济高速发展的地区,本地人比较少。


所以情感并不浓,消费并不低。


可能会像村超那样直接免费接游客去自己家里住宿,游客离开后还深情相拥吗?


可能会像哈尔滨这样一到位就一口一个南方小土豆,然后排着队接送吗?


我想基本上不会。


因为多数人的消费能力是有限的,肯定会选择热闹,便宜且好玩的地方。


所以这样的火热IP,大概只会出现在消费相对来说比较低的城市。


所以,现在的生意大多都围绕着提供情绪价值这个方向出发。


前段时间火爆全网的海底捞科目三,虽然海底捞的价格高了一点,但是在你累了,失落了的时候,突然在你面前响起了生日快乐歌,随后又跳起了科目三。


在冰冷的建筑下瞬间热泪盈眶,脑海中蹦出一句:人间值得


要知道,在外面花几百块钱是买不到这种服务的。


而这些服务本质就是提供情绪价值。


特别是在今天这样的现状下,大家兜里都没几个子,生活也都不太如意,所以这时候情绪价值对于一个人来说尤为重要。


所以以后这样火爆的城市IP还会持续出现,这是毋庸置疑的!


作者:苏格拉的底牌
来源:juejin.cn/post/7321943946309124136
收起阅读 »

IT外传:老郑和老钱

正式声明:以下内容完全为道听途说,肆意杜撰。请勿对号入座,自寻烦恼。 老郑出门去厕所,瞥了一眼旁边测试工程师的屏幕。 他还在整理Excel表格,里面是老郑参与的项目,上面列满了红红的风险点,像是堵车时的尾灯一般。 老郑刚做了一个智能识别的AI项目。这个项目快...
继续阅读 »

正式声明:以下内容完全为道听途说,肆意杜撰。请勿对号入座,自寻烦恼。



老郑出门去厕所,瞥了一眼旁边测试工程师的屏幕。


他还在整理Excel表格,里面是老郑参与的项目,上面列满了红红的风险点,像是堵车时的尾灯一般。


老郑刚做了一个智能识别的AI项目。这个项目快提交测试时,老郑还在别的项目组干活。这个智能识别干了没几周,又被调走干别的事情了。


即便如此,老郑开发的智能识别项目,在识别率和识别能力上,在整个业内也是领先。这得益于他长久以来的经验积累以及巧妙的算法设计。


但是,大家却不这么看。即便业内识别率只能到50%,老郑做出了85%,但是大家也会盯着那无法识别的15%。


刚刚测试工程师就在整理那15%,他们要把这15%里的100%全部汇报给领导:你看,这些一塌糊涂,这种情况能不能用?请领导定夺。


言外之意:上线后有问题跟我们无关,风险已经全部抛出来了。


其实……老郑也习惯了。


上次另一个识别项目,老郑把准确率从刚提交测试时的50%提高到97%。而测试工程师在给领导汇报时,开头说识别率很差,只有50%。领导很忙,听完这个结论就走了。剩下一些中层,又听了40分钟他是如何通过围追堵截的测试方法一步步将识别率提高的。


大家都没错,也都很辛苦,这些老郑并不关心。老郑的心情很差,因为有一个同事离职了。


老郑的这个同事,技术能力很强,强到一个人可以顶一个团队。


在体力劳动上,一个人顶一队人可能很难。比如普通的劳工一次扛3袋水泥,有个大力士可以一次扛30袋,而且速度还很快。这很罕见。


但是在科技或者软件行业,这种情况很普遍,但是很少有人被认可。


老郑的这个同事老钱,就是这样一个人。


老钱设计的代码,简洁纯净,他擅长使用中间件和编程语言的特性,代替大量的代码逻辑。其代码格式规范、文档注释清晰。也正是得益于简洁和巧妙,他的效率还很高。同样的功能,其他同事需要3个人写两周,老钱1个人一周就能搞定。


对于速度,这顶多算是多扛几袋水泥,在软件行业,这点贡献算不了什么巨大改善。


关键是老钱写的代码很少出问题。程序员写的代码出了问题(bug),会引发后续一堆人的投入。测试同事会测试,前后端要排查、修改,产品经理要做决策,市场要应对用户的投诉。这bug就像是喂鸽子时的粮食,往东边撒一把儿,一群鸽子蜂拥而至,往西边撒一把儿,西边又密密麻麻。


老钱设计的代码,很少有bug,这一点就避免了三四个部门、10多个人白忙活几周的情况。这个隐形成本的节省是巨大的。


除了代码的设计,老钱还有个优势,那就是有远见和守原则。


项目开发中,会面临很多的技术选型和方案选定。大到使用什么框架,小到一个参数选用何种数据类型。


很多的时候,在进行技术讨论时,老钱会对其他人的方案提出建议。比如一个参数不要传来传去,就要以一方为准,否则会出问题。


其他人一般会有自己的理由,比如,传来传去不用给数据库增加额外字段。但是,往往过不了多久,问题就出现了,传着传着就传乱了。于是大家又聚到一起调试:你传给我啥,我收到啥,又传给了他啥……在广场的空地撒了一把粮食,远处的鸽群放弃了旧粮,急忙朝这里飞奔而来。


老郑和老钱也合作过一个项目。老钱曾经建议老郑不要那么搞,否则会出问题。老郑没听,结果后面确实走不通了,最终老郑还是改了回去,那一个星期白干了。


然而,老钱却离职了。


他的离职半含被迫,半含自愿。首先,经济形势不好,导致公司出现了拖延工资的情况。


其次,在拖延工资的背景下,不同员工的发放情况参差不齐。有的人拖延5个月,有的人拖延3个月,有的人正常发放。而老钱的工资,拖得最久,向领导反馈也没有结果。


领导说,公司现在的回款出现延迟,前年该给的钱,去年才刚刚给。不过,每个月也都是有回款的。这点回款,首先要保证公司的日常运作,其次保证新员工,再次保证有贡献的员工。其他人,只能克服一下了。


好像意图比较明显。老钱也是个智商和情商都在线的人。


老钱提出了离职,领导立马批准,限两周内办好手续。老钱说,我原本打算能有1个月缓冲期的。


其实,老钱和老郑早就被投诉多次了。


甚至连人事都看不惯他们:凭什么这俩人工资比我们高,还不拼命加班?我不平衡……不是,他们没有大局意识!


而同为技术人员,兄弟部门的意见就更多了:再复制一份接口,随便改个字段都不配合!群里半夜@你的消息,为什么没有及时回复?我们换个对接人问你问题,你不培训他,让他看接口文档是什么意思!


老钱和老郑有个观念:用工作时间的高效率工作,换取下班后的安心休息。但是,似乎大家并不都想这样,往往是白天静悄悄,只要一下班,工作群里立刻变得活跃起来。


老郑和老钱有时候就讨论,你说领导是否知道一个员工的真实水平或者价值。


比如,A员工干的活能顶B、C、D,3个人。或者,他手下有个员工的水平在整个行业中处于上层还是下层。


“好像不知道!”


老钱说,交接工作期间,有个问题找来,领导还问他:你也参与这个项目了?


其实就上个月,老钱还在这个项目上干了半个月,日报、周报、早会、周会地定期汇报。显然领导没有关注过,因为没有发生过大的问题。一贯零失误的工作,让老钱变成了一个小透明,而且还是经常提意见的那种问题员工。


一线的员工常常辗转于项目代码之中。领导们则开会,看书,制定考核KPI指标。长期脱离一线阵地,会让领导从业务管理上浮到任务管理(从如何带领人解决问题,变成安排人去解决问题)。


软件其实是一门工程学,而非玄学。


软件工程的最佳的实践是多进行工程管理,而非思想管理。


现实很多情况都是反过来的,大家都很重视思想管理。


如果把完成一个软件项目比作攻下一座城池。那么,策略要比士气更重要。


讲策略的将军会规划好完整的攻城计划。首先,他会盘点自己有多少人员和器械,会分析对方城池有几个薄弱点。然后,部署几个分队:哪个队伍扛着云梯往城墙上驾,哪个队伍推着木车从东门撞击。其实,队伍主力要从北门水路强攻。等到把敌方守卫都引到东门时,以山坡黑烟为号,北门发起进攻。最终全面进攻,一举取得胜利。


类比到项目开发中,其实就是各个工种的配合,结构的定义和数据的流转。安排好整个项目每个端口,从上游拿到什么数据,做怎样的处理,然后给下游如何提供。最终,定好流程和时间节点,一气呵成。肯定没法想得完全周到,不过即便有问题,也都是局部问题,整体还是丝滑的。


4cb98f20-a6d4-4872-98a5-51113d85858a.jpeg


讲士气的将军则不然,他不考虑每个环节,或者技术更新太快,他已经不擅长每个环节了。他的主要精力是给士兵做思想工作。他告诫士兵们,我们又要攻打一座城池了,大家要有大局意识,不想当将军的士兵不是好士兵,士兵就是要解决问题的,不是提出问题的。他不关注粮草,不关注器械,不关注目标城池的特征,主要强调大家一定要攻下城池,这是所有人的目标和责任。然后,一声令下,众士兵蜂拥上前,去哪儿的都有。


最后没有攻下城池。将军要求手下将领做复盘,开会讨论为什么没有成功。然后,再次鼓舞大家要有建功立业的雄心。而手下的将领回去也纷纷效仿,告诉士兵们,一定要有建功立业的雄心壮志,遇到问题要解决问题而非吐槽问题,人人都是主人翁,没有粮草你要想办法去搞些粮草。第二次,士兵们又向敌方发起总攻……


这不仅仅体现在软件行业,其他行业也一样,正如一些专家、教授频频发出雷人的言论。


在国内,大家都有上级崇拜。针对如上言论,一般会有人怼你:能当领导的人,必然有过人之处,否则为什么不是你当领导?


其实这句话也没错,还真不是一个把产品做得越好就能生存得越好的环境。


老郑不知道老钱以后会不会改变,正如他不知道自己还能坚持多久。


老钱离职前,曾经问过老郑:老郑,你说那帮“埋头苦干”的年轻人,是以前的我们呢?还是我们的以后呢?


作者:TF男孩
来源:juejin.cn/post/7322356470253731859
收起阅读 »

MyBatis实战指南(二):工作原理与基础使用详解

MyBatis是一个优秀的持久层框架,它支持定制化SQL、存储过程以及高级映射。那么,它是如何工作的呢?又如何进行基础的使用呢?本文将带你了解MyBatis的工作原理及基础使用。一、MyBatis的工作原理1.1 MyBatis的工作原理工作原理图示:1、读取...
继续阅读 »

MyBatis是一个优秀的持久层框架,它支持定制化SQL、存储过程以及高级映射。那么,它是如何工作的呢?又如何进行基础的使用呢?本文将带你了解MyBatis的工作原理及基础使用。

一、MyBatis的工作原理

1.1 MyBatis的工作原理

工作原理图示:
Description

1、读取MyBatis配置文件

mybatis-config.xml为MyBatis的全局配置文件,配置了MyBatis的运行环境等信息,例如数据库连接信息。

2、加载映射文件(SQL映射文件,一般是XXXMapper.xml)

该文件中配置了操作数据库的SQL语句,需要在MyBatis配置文件mybatis-config.xml中加载。

XXXMapper.xml可以在mybatis-config.xml文件可以加载多个映射文件,每个文件对应数据库中的一张表。

3、构造会话工厂

通过MyBatis的环境等配置信息构建会话工厂SqlSessionFactory。

4、创建会话对象

由会话工厂创建SqlSession对象,该对象中包含了执行SQL语句的所有方法。

5、Executor执行器

MyBatis底层定义了一个Executor接口来操作数据库,它将根据SqlSession传递的参数动态地生成需要执行的SQL语句,同时负责查询缓存的维护。

6、MappedStatement对象

在 Executor接口的执行方法中有一个MappedStatement类型的参数,该参数是对映射信息的封装,用于存储要映射的SQL语句的id、参数等信息。

7、输入参数映射

输入参数类型可以是Map、List等集合类型,也可以是基本数据类型和POJO类型。输入参数映射过程类似于JDBC对preparedStatement对象设置参数的过程。

8、输出结果映射

输出结果类型可以是Map、List等集合类型,也可以是基本数据类型和POJO类型。输出结果映射过程类似于JDBC对结果集的解析过程。

1.2 MyBatis架构

Description

API接口层

提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。

MyBatis和数据库的交互有两种方式:使用传统的MyBatis提供的API、使用Mapper接口。

1)使用传统的MyBatis提供的API

这是传统的传递Statement Id和查询参数给SqlSession对象,使用SqlSession对象完成和数据库的交互;
Description
MyBatis提供了非常方便和简单的API,供用户实现对数据库的增删改查数据操作,以及对数据库连接信息和MyBatis自身配置信息的维护操作。
示例:

SqlSession session = sqlSessionFactory.openSession();
Category c = new Category();
c.setName("新增加的Category");
session.insert("addCategory",c);

上述使用MyBatis的方法,是创建一个和数据库打交道的SqlSession对象,然后根据Statement Id和参数来操作数据库,这种方式固然很简单和实用,但是它不符合面向对象语言的概念和面向接口编程的编程习惯。

2)使用Mapper接口

MyBatis将配置文件中的每一个<mapper>节点抽象为一个Mapper接口,而这个接口中声明的方法和跟<mapper>节点中的<select|update|delete|insert>节点项对应,

即<select|update|delete|insert>节点的id值为Mapper接口中的方法名称,parameterType值表示Mapper对应方法的入参类型,而resultMap值则对应了Mapper接口表示的返回值类型或者返回结果集的元素类型。

示例:

SqlSession session = sqlSessionFactory.openSession();
CategoryMapper mapper = session.getMapper(CategoryMapper.class);
List<Category> cs = mapper.list();
for (Category c : cs) {
System.out.println(c.getName());
}

根据MyBatis的配置规范配置后,通过SqlSession.getMapper(XXXMapper.class)方法,MyBatis会根据相应的接口声明的方法信息,通过动态代理机制生成一个Mapper实例。
Description

使用Mapper接口的某一个方法时,MyBatis会根据这个方法的方法名和参数类型,确定Statement Id,底层还是通过SqlSession.select(“statementId”,parameterObject)或者SqlSession.update(“statementId”,parameterObject)等等来实现对数据库的操作。

MyBatis引用Mapper接口这种调用方式,纯粹是为了满足面向接口编程的需要。

数据处理层

负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。

1)参数映射和动态SQL语句生成

动态语句生成可以说是MyBatis框架非常优雅的一个设计,MyBatis通过传入的参数值,使用OGNL表达式来动态地构造SQL语句,使得MyBatis有很强的灵活性和扩展性。
Description
参数映射指的是对于Java数据类型和JDBC数据类型之间的转换,这里包括两个过程:

  • 查询阶段,我们要将java类型的数据,转换成JDBC类型的数据,通过preparedStatement.setXXX()来设值;

  • 另一个就是对ResultSet查询结果集的JdbcType 数据转换成Java数据类型。

2)SQL语句的执行以及封装查询结果集成List< E>

动态SQL语句生成之后,MyBatis将执行SQL语句并将可能返回的结果集转换成List<E> 。

MyBatis 在对结果集的处理中,支持结果集关系一对多和多对一的转换,并且有两种支持方式,一种为嵌套查询语句的查询,还有一种是嵌套结果集的查询。

基础支撑层

负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。

MyBatis层次结构

Description

1.3 Executor执行器

Executor的类别

Mybatis有三种基本的Executor执行器:SimpleExecutor、ReuseExecutor和BatchExecutor。

Description

1、SimpleExecutor

每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。

2、ReuseExecutor

执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map<String, Statement>内,供下一次使用。简言之,就是重复使用Statement对象。

3、BatchExecutor

执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同。

作用范围:Executor的这些特点,都严格限制在SqlSession生命周期范围内。

你还在苦恼找不到真正免费的编程学习平台吗?可以试试云端源想!课程视频、在线书籍、在线编程、实验场景模拟、一对一咨询……你想要的全部学习资源这里都有,重点是统统免费!点这里即可查看

Executor的配置

指定Executor方式有两种:

1、在配置文件中指定

<settings>
<setting name="defaultExecutorType" value="BATCH" />
</settings>

2、在代码中指定

在获取SqlSession时设置,需要注意的时是,如果选择的是批量执行器时,需要手工提交事务(默认不传参就是SimpleExecutor)。

示例:

// 获取指定执行器的sqlSession
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
// 获取批量执行器时, 需要手动提交事务
sqlSession.commit();

1.4 Mybatis是否支持延迟加载

延迟加载是什么

MyBatis中的延迟加载,也称为懒加载,是指在进行表的关联查询时,按照设置延迟规则推迟对关联对象的select查询。

例如在进行一对多查询的时候,只查询出一方,当程序中需要多方的数据时,mybatis再发出sql语句进行查询,这样子延迟加载就可以的减少数据库压力。

MyBatis 的延迟加载只是对关联对象的查询有迟延设置,对于主加载对象都是直接执行查询语句的。

假如Clazz 类中有子对象HeadTeacher。两者的关系:

public class Clazz {
private Set<HeadTeacher> headTeacher;
//...
}

是否查出关联对象的示例:

@Test
public void testClazz() {
ClazzDao clazzDao = sqlSession.getMapper(ClazzDao.class);
Clazz clazz = clazzDao.queryClazzById(1);
//只查出主对象
System.out.println(clazz.getClassName());
//需要查出关联对象
System.out.println(clazz.getHeadTeacher().size());
}

延迟加载的设置

在Mybatis中,延迟加载可以分为两种:延迟加载属性和延迟加载集合,association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。

在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false

1)延迟加载的全局设置

延迟加载默认是关闭的。如果需要打开,需要在mybatis-config.xml中修改:

<settings>

<setting name="lazyLoadingEnabled" value="true" />

<setting name="aggressiveLazyLoading" value="false"/>
</settings>

比如class班级与student学生之间是一对多关系。在加载时,可以先加载class数据,当需要使用到student数据时,我们再加载 student 的相关数据。

  • 侵入式延迟加载:指的是只要主表的任一属性加载,就会触发延迟加载,比如:class的name被加载,student信息就会被触发加载。

  • 深度延迟加载: 指的是只有关联的从表信息被加载,延迟加载才会被触发。通常,更倾向使用深度延迟加载。

2)延迟加载的局部设置

如果设置了全局加载,但是希望在某一个sql语句查询的时候不适用延时策略,可以配置局部的加载策略。

示例:

 <association
property="dept" select="com.test.dao.DeptDao.getDeptAndEmpsBySimple"
column="deptno" fetchType="eager"/>

etchType值有2种,

  • eager:立即加载;

  • lazy:延迟加载。

由于局部的加载策略的优先级高于全局的加载策略。指定属性后,将在映射中忽略全局配置参数lazyLoadingEnabled,使用属性的值。

延迟加载的原理

MyBatis使用Java动态代理来为查询对象生成一个代理对象。当访问代理对象的属性时,MyBatis会检查该属性是否需要进行延迟加载。

如果需要延迟加载,则MyBatis将再次执行SQL查询,并将查询结果填充到代理对象中。

二、MyBatis基础使用示例

1、添加MyBatis依赖

首先,我们需要在项目中添加MyBatis的依赖。如果你使用的是Maven项目,可以在pom.xml文件中添加以下依赖:

<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>

2、创建实体类

假设我们有一个用户表(user),我们可以创建一个对应的实体类User:

public class User {
private int id;
private String name;
private int age;
// getter和setter方法省略
}

3、创建映射文件UserMapper.xml

在MyBatis的映射文件中,我们需要定义一个与实体类对应的接口。例如,我们可以创建一个名为UserMapper的接口:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.UserMapper">
<select id="getUserById" parameterType="int" resultType="com.example.User">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>

4、创建接口UserMapper.java

接下来,我们需要创建一个与映射文件对应的接口。例如,我们可以创建一个名为UserMapper的接口:

package com.example;

import com.example.User;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

public interface UserMapper {
@Select("SELECT * FROM user WHERE id = #{id}")
User getUserById(@Param("id") int id);
}

5、使用MyBatis进行数据库操作

最后,我们可以在业务代码中使用MyBatis进行数据库操作。例如,我们可以在一个名为UserService的类中调用UserMapper接口的方法:

public class UserService {
public User getUserById(int id) {
SqlSession sqlSession = MyBatisUtil.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.getUserById(id);
sqlSession.close();
return user;
}
}

总结:

MyBatis是一个非常强大的持久层框架,它可以帮助我们简化数据库操作,提高开发效率。在实际开发中,我们还可以使用MyBatis进行更复杂的数据库操作,如插入、更新、删除等。希望这篇文章能帮助你更好地理解和使用MyBatis。

收起阅读 »

2024律师课程推荐:iCourt律师执行实务集训营(赠《执行实务大礼包》)

律师行业竞争激烈,想要突破困境,就一定要把握蓝海机遇,实现提早布局。如今,还有哪些业务是尚未被“卷起来”的“蓝海业务”?从数据来看,执行业务一定是其中之一。在 Alpha 系统中,以“执行”为关键词检索最近三年的案例,显示有 1,729,5317 条结果;根据...
继续阅读 »

律师行业竞争激烈,想要突破困境,就一定要把握蓝海机遇,实现提早布局。


如今,还有哪些业务是尚未被“卷起来”的“蓝海业务”?


从数据来看,执行业务一定是其中之一。


在 Alpha 系统中,以“执行”为关键词检索最近三年的案例,显示有 1,729,5317 条结果;


根据中国执行信息公开网数据,截止到 12 月 28 日,公布的失信被执行人高达 8,618,870 位;


根据最高法公布的数据显示,得益于全国法院持续推进执行难综合治理、源头治理,仅 2023 年上半年,我国执行到位金额高达 1.2 万亿元,同比增长 23.03% ......



在业内,执行案件是公认的数量多、案件标的大、个案收费高。同时,作为诉讼案件的“最后一公里”,执行能够将司法裁判真正的“落到实处”,维护客户合法利益,实现律师职业价值。因此,一直以来,都有大量律师对执行业务“跃跃欲试”。


但,为什么现实中并没有那么多律师真正从事执行业务?


蓝海是真,“执行难”也是真:线索难找、到位率低、执行周期长、执行程序复杂多变......执行背后的难题让人望而却步。


啃下执行这块“硬骨头”,就能开拓业务范围,拓宽职业路径,在广阔的蓝海中寻找发展新机遇。


基于此,3 月 2 日 - 3 月 3 日,「执行实务集训营 07 期」将在古都西安与各位校友相见。2 天一夜的课程,将对执行实务的疑难问题进行思路点拨和深入解析,以“两个拳头” + “四大利刃”全方位切入执行案件,系统梳理相关法规案例,详细剖析执行专业知识和实务要点。


老赖隐匿财产,当事人无计可施,有哪些途径可以挖掘被执行人财产线索?

处置财产,如何形成更专业化、标准化、流程化的执行案件办理流程?

“借名买房”、“股权代持”等疑难案件该如何拆解破局、一击即中?


经过了 6 期课程的升级迭代和精心打磨,「执行实务集训营 07 期」将直击痛点,打通诉讼最后一公里。


同时,该集训营也将结合刚刚审议通过、即将在 2024 年 7 月 1 日起施行的最新修订的《公司法》内容,对执行相关的新变化、新趋势、新动向进行深入解读和思路点拨。


课程大纲


(一)查找执行财产的三大视角与方法


本次课程将会详细剖析十多种常见与非常见财产类型,并从执行法院、执行律师和当事人三个视角出发,分享查询被执行人财产的途径和方法。


(二)处置被执行人财产的标准化流程


本次课程将通过细致讲解,帮助大家形成自己、团队的执行案件办理流程,以便更好地展现代理执行业务的专业化、标准化、流程化。


(三)执行异议之诉案件的破局之道


本次课程会对案外人执行异议之诉、申请执行人执行异议之诉、追加股东为被执行人异议之诉、执行分配方案异议之诉四个板块进行逐一详细解读。此外,本次课程还将对“借名买房”能否排除强制执行、“股权代持”的隐名股东能否排除强制执行等关联问题进行拆解式分享。


(四)破解执行难的“两个拳头”与“四大利刃”


推进执行案件,常规做法是“两个拳头”:一拳打向的是被执行人财产;另一拳则打向的是被执行人。对于执行案件,除了以上两种常规打法,本次课程还提炼出了破解执行难的“四大利刃”。这些“利刃”并非会用在每一个执行案件中,但却可以成为某个具体执行案件中的“大杀器”,在关键时刻真正做到执行无阻,使命必达。



课程安排




课程讲师




往期现场:


(课程现场)


在两天一夜的课程中,除了干货满满、深入浅出的内容讲解外,校友们还将通过紧张刺激的小组比赛将课程内容串联起来,达到“融会贯通”的效果。


为了团队的荣誉,各个小组直接“卷起来了”,最终的作业展示环节简直“神仙打架”、惊喜连连。经过系统的学习和模拟实践,校友们也对破解执行难题,办理执行案件有了全新的思路和理解。


(比赛环节现场)


北京、武汉、广州...... 2023 年,校友们与「执行实务集训营」共同度过了 6 期的时光。每一期校友们都满载而归、直言不虚此行。该课程也是 2023 年 iCourt 线下集训营参与人数最多、最火爆的课程之一。

(学员合影)


课程资料


除精彩内容外,现在报名集训营,更有「执行实务干货大礼包」全部送送送!


• 1 节线上课程,业务品牌双管齐下

校友报名后均可获得《执行律师如何打造个人品牌》录播课程,方便大家随时随地进行学习提升。课程围绕写作与讲课,这两个法律人必备的技能,结合实务案例,帮助执行律师打造专业品牌,赋能业务开拓。


• 2 套项目模板,标准化办案新思路

随课程赠送《执行与执行异议》、《执行》两套项目模板,涵盖丰富的任务与任务附件,帮助校友规范办案流程,建立团队知识宝库。


• 4 本实务指引,配套学习事半功倍

《执行实务操作指引》( 4.0 版)、《执行实务 108 问》、《执行一本通法律法规汇编》( 2023 版)、《律师代理执行案件 168 个执行步骤》。4 本执行办案实务操作指引,从法律规范、常见问题出发,帮助校友解决服务中的文本困扰。


• 多份文书模板,高效应对执行难题

我们汇总了办案过程中常用的变更申请执行人、查封申请书、到期债务通知书模板等等,希望能够帮助校友提升执行办案效率,高效破解执行难题。

执行虽难,但抽丝剥茧,便能破解难题,实现职业突破。在「执行实务集训营」07 期,拥有丰富执行实务经验的韩锦超老师将带领大家探讨疑难案例,学习方法,启发思路。在前六期的基础上,iCourt 课程中心也对课程内容、课程环节、课程资料、课程体验等环节进行了全面系统的升级,结合最新法律动态,带给大家焕然一新的体验。

机遇往往与挑战并存,机遇也往往留给做足了准备的人。

眼前,是一片可待征服的蓝海。跨过这座大山,是属于我们的广阔的征途。2024 年春节“节后第一课”,「执行实务集训营」将带领大家破解执行难题,挑战业务蓝海。

突破执业困境,把握发展机遇。3 月 2 日 - 3 月 3 日,我们西安见~

了解课程详情:iCourt集训营

原文链接:https://www.icourt.cc/prac-article/728.html

收起阅读 »

探索发展,融合共生|惠州OpenHarmony城市大会圆满举行

1月9日,以“探索发展,融合共生”为主题的OpenHarmony城市大会在惠州隆重举行。本次大会由惠州市工业和信息化局、惠州市政务服务数据管理局、惠州仲恺高新技术产业开发区管理委员会主办,惠州仲恺民营投资集团有限公司、OpenAtomOpenHarmony(简...
继续阅读 »

1月9日,以“探索发展,融合共生”为主题的OpenHarmony城市大会在惠州隆重举行。本次大会由惠州市工业和信息化局、惠州市政务服务数据管理局、惠州仲恺高新技术产业开发区管理委员会主办,惠州仲恺民营投资集团有限公司、OpenAtomOpenHarmony(简称“OpenHarmony”)超高清专委会承办,惠州市电子信息产业协会协办。

(图片:惠州OpenHarmony城市大会现场)

广东省政务数据管理局局领导、一级调研员姚进,广东省工业和信息化厅信息化与软件服务业处副处长陈古典,惠州市政府副秘书长程坤,惠州市工业和信息化局局长廖巍,惠州市政务服务数据管理局局长杨伟斌,仲恺高新区管委会副主任、潼湖生态智慧区党工委副书记、管委会主任汤俊,广东九联科技股份有限公司董事长詹启军,OpenHarmony社区工作委员会执行主席、华为终端BG软件部副总裁柳晓见,鸿蒙生态服务有限公司总经理杜金彪,中国信通院泰尔终端实验室副主任果敢,开放原子开源基金会教育培训部部长王岩广,京东方高级副总裁荆林峰,OpenHarmony生态伙伴企业代表,惠州市OpenHarmony潜在政企客户,超高清领域企业代表,专家学者,高校代表及研究机构代表等300余人出席了本次大会。大会旨在分享及探索OpenHarmony生态发展路径,着力搭建地方产业及OpenHarmony生态领域的交流合作平台,助推OpenHarmony技术创新和生态繁荣。

广东省政务服务数据管理局局领导姚进为大会在开场致辞中强调,广东作为OpenHarmony的发源地,相关工作起步早、优势大,其在开源软件贡献、人才培养、生态应用和政策支持等方面的OpenHarmony生态体系建设上已经位居全国前列。特别指出,惠州在OpenHarmony建设方面走在全省前列,本次大会在惠州市的召开,不仅为广东省乃至全国的OpenHarmony系统发展提供了宝贵的交流平台,也为其他城市发展OpenHarmony提供了典范。未来,广东省政务服务数据管理局将继续通过成立产业协会、制定标准规范以及开展应用示范等措施,全力推动OpenHarmony产业的繁荣壮大。

(图:广东省政务服务数据管理局局领导姚进)

惠州市人民政府副秘书长程坤在致辞中强调,OpenHarmony作为构建智能终端操作系统的重要基础能力平台和安全底座,对于打造自主可控的国产操作系统和构建新的智能终端产业生态具有深远意义。惠州凭借其坚实的电子信息产业基础,积极把握OpenHarmony发展的战略机遇,并已取得了一系列实践成果。例如,支持和指导华为终端、九联科技等企业牵头成立了“OpenHarmony超高清专委会”;印发实施了加快OpenHarmony生态产业发展行动计划,成效初显。程坤表示,惠州将继续强化对OpenHarmony产业的宣传引导、主体培育、示范应用、环境优化和政策谋划等工作,为OpenHarmony生态在惠落地发展提供有力支持。

(图:惠州市人民政府副秘书长程坤)

OpenHarmony打造下一代智能终端操作系统根社区

OpenHarmony社区工作委员会执行主席、华为终端BG软件部副总裁柳晓见分享了OpenHarmony项目及生态进展。目前已有超过220家伙伴加入OpenHarmony生态共建,累计落地超过460款软硬件产品通过OpenHarmony兼容性测评,覆盖金融、教育、交通、医疗、公共安全、智慧城市等多个行业。随着行业标准规范的推进,OpenHarmony已成为各行各业的优选。

同时,深圳、福州、惠州、北京、重庆、南京等城市率先出台相关产业政策支持OpenHarmony发展,从供给侧和需求侧推动生态建设。为培育与产业发展契合的创新型人才,壮大OpenHarmony生态新兴力量,生态伙伴联合高校共同打造人才培养闭环生态链。柳晓见表示,希望OpenHarmony生态伙伴们聚力前行,期待更多伙伴加入OpenHarmony共建中,共筑下一代智能终端操作系统根社区。

(图:OpenHarmony社区工作委员会执行主席、华为终端BG软件部副总裁柳晓见)

OpenHarmony生态服务助力生态商业成功

鸿蒙生态服务公司总经理杜金彪分享了在各地政府大力支持下出台的相关OpenHarmony产业扶持政策,并介绍鸿蒙生态服务公司围绕“行业集采、政府集采、政府奖补、联盟标准”四大商业机遇,开展测评认证、市场拓展、商机对接、活动承办等服务,协助生态伙伴实现降本增效。通过搭建相关联盟平台推动产业标准建设,促进良性竞争。同时也期待更多伙伴加入,共同加速OpenHarmony生态产业健康发展。

(图:鸿蒙生态服务公司总经理杜金彪)

两位特邀嘉宾专题分享完毕后,在参会嘉宾的共同见证下,举办了OpenHarmony超高清专委会揭牌仪式、OpenHarmony人才培养战略行动启动仪式。本地政府、院校、企业等多方力量表示,将全力以赴支持OpenHarmony的发展,共同推动其在更多领域的应用和普及。此次相关仪式的成功举行,为惠州市OpenHarmony生态的发展开启了新的篇章。

OpenHarmony超高清专委会揭牌仪式

2022年,在开放原子开源基金会的指导下,在惠州市政府、惠州仲恺高新区管委会的支持下,由华为终端有限公司、广东九联科技股份有限公司、惠州开鸿数字产业发展有限公司牵头推动成立了“OpenHarmony超高清专委会”(OpenHarmony生态委员会下属负责推进OpenHarmony在超高清领域生态发展的唯一主体)。专委会将面向超高清全行业,推动 OpenHarmony的研发、装机、应用等工作,打造OpenHarmony超高清生态圈。

(图:OpenHarmony超高清专委会揭牌仪式合影)

OpenHarmony人才培养战略行动启动仪式

由开放原子开源基金会、惠州学院、惠州市技师学院、惠州城市职业学院、惠州经济职业技术学院、惠州工程职业学院、深圳技术大学、广东九联科技股份有限公司、惠州开鸿数字产业发展有限公司(OpenHarmony超高清专委会秘书处单位)共同开启OpenHarmony人才培养战略行动启动仪式,承诺合力开展OpenHarmony新型操作系统的产业及技术培训等相应的活动,为企业培养更多的实用性、复合型的软件人才。仪式结束后,大会邀请了OpenHarmony生态专家及伙伴进行主题演讲。

(图:OpenHarmony人才培养战略行动启动仪式合影)

OpenHarmony的基本设计理念和关键技术最新进展

OpenHarmony项目管理委员会(PMC)主席任革林分享了OpenHarmony的基本设计理念和关键技术最新进展,并介绍了基于OpenHarmony使能的金融、智慧教室、智能化公路建设等一系列行业场景创新解决方案。他表示,OpenHarmony技术底座能力越来越成熟,一个面向全场景、全连接、全智能时代OS的阶段目标已经达成,既可以满足生态伙伴开发丰富多彩的创新设备,也可以满足应用开发者开发复杂大型应用和极致高性能应用。

(图:OpenHarmony项目管理委员会主席任革林)

深开鸿基于OpenHarmony高校人才培养实践

深圳开鸿数字产业发展有限公司(简称“深开鸿”)OpenHarmony社区开发部总经理、OpenHarmony项目管理委员会(PMC)成员巴延兴分享了深开鸿基于OpenHarmony高校人才培养实践。作为一家立足于OpenHarmony生态,为行业数字化、智慧化提供基础软件的生态平台型企业,深开鸿积极响应国家切实推动“深化产教融合、校企人才共育”的号召,与北京理工大学、哈尔滨工业大学、东南大学、深圳信息职业技术学院、深圳技术大学等众多高校开展合作,计划在未来几年内培养大量优秀的OpenHarmony技术人才。

(图:深开鸿OpenHarmony社区开发部总经理、OpenHarmony项目管理委员会成员巴延兴)

基于OpenHarmony的全场景解决方案实践

江苏润开鸿数字科技有限公司(简称“润开鸿”)生态技术总监、OpenHarmony社区龙芯架构SIG组长连志安分享了润开鸿基于OpenHarmony面向行业的HiHopeOS发行版使能千行百业的场景创新解决方案及商业落地实践,并重点介绍了基于龙芯+OpenHarmony的适配进展及工业场景探索。

(图:润开鸿生态技术总监、OpenHarmony社区龙芯架构SIG组长连志安)

新功能,新形态,新场景

京东方科技集团股份有限公司视觉艺术事业部总经理吴坚围绕京东方集团业务新功能、新形态、新场景展开讨论,探讨京东方屏之物联与OpenHarmony结合所带来的创新与变革。

(图:京东方视觉艺术事业部总经理吴坚)

基于OpenHarmony的Nearlink(星闪)赋能智能空间实践探索

广东九联科技股份有限公司产品研究院院长、广东九联开鸿科技发展有限公司CEO钟义秀分享了基于OpenHarmony的Nearlink(星闪)赋能智能空间实践探索。围绕OpenHarmony和星闪的特性在智慧空间场景中应用以及场景中特性闭环,低时延、近场联接联动让分布式空间场景无处不在。

(图:广东九联科技股份有限公司产品研究院院长、广东九联开鸿科技发展有限公司CEO钟义秀)

在盛大的OpenHarmony城市大会上,我们见证了科技与生活的深度融合,感受到了开源文化与创新精神的激情碰撞。通过深入的探讨与交流,我们更加坚信,OpenHarmony所倡导的开放、共享、互联的理念,将引领我们迈向一个更加智能、高效、和谐的美好未来,共同开创一个万物智联、万物互融的新时代。让我们携手并进,以“探索发展,融合共生”的精神,为开源生态的繁荣与辉煌而努力!

(图:惠州OpenHarmony城市大会现场)

OpenHarmony是由开放原子开源基金会(OpenAtomFoundation)孵化及运营的开源项目,目标是面向全场景、全连接、全智能时代、基于开源的方式,搭建一个智能终端设备操作系统的框架和平台,促进万物互联产业的繁荣发展。OpenHarmony开源三年多来,社区快速成长,版本已迭代到OpenHarmony 4.1beta1 Release,超过 6700 名共建者、70家共建单位,贡献代码行数超过1亿行。截至2023年底,OpenHarmony开源社区已有250多家生态伙伴加入,OpenHarmony项目捐赠人达35家,通过OpenHarmony兼容性评测的伙伴达170余个,累计落地230余款商用设备,涵盖金融、教育、智能家居、交通、数字政府业、医疗等各个领域,OpenHarmony已成为下一代智能终端操作系统根社区。

收起阅读 »

2024年,现在去开发一款App需要投入多少资金?

前言 本文主要探讨跨平台应用的开发成本,原生与小程序不在探讨范围之内,为什么呢?请接着往下看~ 选择大于努力 原生开发的现状 先来看下目前原生开发存在的问题以及国内的现状。 开发人员的人力成本相对高于跨平台开发人员,对于纯原生的项目,企业通常需要招两个端的开...
继续阅读 »

1.jpg


前言


本文主要探讨跨平台应用的开发成本,原生与小程序不在探讨范围之内,为什么呢?请接着往下看~


选择大于努力


原生开发的现状


先来看下目前原生开发存在的问题以及国内的现状。



  1. 开发人员的人力成本相对高于跨平台开发人员,对于纯原生的项目,企业通常需要招两个端的开发人员。这也是导致很多企业不愿意选择原生开发的重要原因之一(Android、iOS)。

  2. 原生应用开发成本高,开发周期慢,如果不招人(提高用人成本)较难跟上市场节奏。

  3. 原生应用推广成本也高(与小程序相对比)。

  4. 对于我们开发人员来说,需要掌握一种语言Java或者kotlin,ios开发需要oc或者swift,难度相对于跨平台学习成本较高。


企业对于技术上的选择,目前需要的就是能节省成本、同时开发效率高的,跨平台已经是大势所趋


国内特有的小程序


小程序的优势很大,自从小程序出来后,蚕食了很大一部分手机应用的市场份额。


小程序相较于原生应用具有显著的优势,其中最大的优势在于成本的降低。相比于开发原生应用,小程序的开发成本更低,同时也更加省时省力。此外,小程序还能够充分利用微信等大型平台的庞大用户流量入口,从而降低企业在推广方面的成本。这种降低成本的好处不仅体现在企业推广方面,也使得用户在使用小程序时所需投入的成本降低(不用去下载app,也不用再走一遍注册流程)。


正因为如此,许多中小企业不愿再开发原生应用,或者说,“没能力”开发原生应用。更倾向于选择小程序。小程序的低成本开发和推广,使得中小企业能够以更少的投入获得更大的回报。此外,小程序还可以借助微信等平台的用户基础,更容易吸引和留住用户。


如何计算一款App开发的成本


本文选择跨平台技术作为开发成本的参考。那在跨平台中,从Statista(一家全球领先的统计数据平台和市场研究公司)收集的数据来看,很明显 Flutter 继续脱颖而出,成为跨平台框架中的首选。截止2023年6月,Flutter占跨平台份额的46%,在跨平台中占比第一,React Native占32%,居第二。



长话短说,开发 Flutter 应用程序的相关费用基本在 10,000 到 450,000 人民币之间,甚至更高。在这本文中,我们将分解各种成本因素,去计算 Flutter 应用程序开发成本。


那如何去计算一款应用的开发成本呢,开发一款应用一共分一下几个阶段,每个阶段都会影响总成本。



  • 第一阶段:需求分析与规划

  • 第二阶段:原型设计(UI/UX)

  • 第三阶段:正式编码(此时应用已经基本成型)

  • 第四阶段:测试

  • 第五阶段:部署与维护


Flutter技术在国内多用于外包项目,所以通常三四五(有些项目会包含二)的几个阶段都由开发者全权负责完成,应用的总体成本通常是通过将总工时乘以开发者的小时费来估算的。


影响一款应用开发成本的因素


不同的应用开发的成本可能会因多种因素而有很大差异,每个因素都会直接影响项目的预算和时间表。最终价格可能受到一系列因素的影响,例如应用程序的复杂程度、要纳入的功能总数、开发人员的每小时费以及许多其他方面。


主要的因素也是对应到开发阶段中,主要是以下这些:



  • 在需求分析时,应用程序的范围和复杂性

  • 在UI设计时,UI的动画、复杂的布局、对设计风格的要求

  • 在开发时,选择的开发方式(1.外包给自由职业者。2.外包给专业软件公司。3.自己招人干)。选择外包开发者(开发商)的地理位置,假设你在美国,找一个中国开发者,成本就会降低许多

  • 在测试时,跨平台的设备成本,功能测试的范围

  • 部署维护时,服务器的成本,bug的修复,添加新的功能


那让我们再来详细聊聊每个阶段具体要花多少费用。


需求分析设计阶段


项目的需求和范围是开发成本的主要决定因素,例如,开发一个基本的笔记应用程序比开发一个功能齐全的电商平台便宜得多。因此,在App开发的初始阶段定义项目需求和应用程序复杂性对于估算总体成本至关重要。App在刚开始需要舍弃掉一些不重要的功能。


UI设计阶段


如果有一个高质量的 UI/UX 设计,那对于App的成功是很有帮助的。但它也会影响成本,一款简单、简约的设计比具有独特图形、复杂动画动画的定制成本更低。如果需要高度定制的设计或想要实现特定的品牌元素,这将极大增加的应用程序开发成本。根据应用程序的复杂程度,设计一款完整的App平均需要 40 到 90 多个小时。设计一款App的UI,价格平均在5000-25000左右,让我们对应到每项工作中去。



  1. 前期的需求交流和沟通。此阶段涉及创建草图和线框图。所需的时间和成本取决于设计的复杂程度。创建草图和线框图可能需要 200 至 1000 的预算分配

  2. UI/UX 设计视觉效果的创建。 此阶段为整个App的内容设计,例如登录界面、注册界面等。同样,实际所需时间取决于App的复杂性。此阶段的预算范围从 5,000 到 15,000或更多

  3. logo设计。在这个阶段阶段,设计师根据之前设计的App内容和、我们的品牌配色和其他设计元素。这项共工作需要相当大的预算,大约需要 5,000 到 10,000 的预算甚至更多。当然,为了节省成本也可以放弃这一阶段,由我们自己设计


代码开发阶段


选择不同的开发人员或开发团队也会影响成本。如果选择经验丰富的专业人员团队会花费更多的前期成本,但可以带来更高的效率和更高质量的产品。如果,雇用经验不足的开发人员刚开始可能会省钱,但可能会导致开发时间更长或日后出现潜在问题。目前主流的方式为以下三种:


自由职业者(外包给程序员做私活)


这种方式可以很好的降低成本,身边也有很多朋友会接私活,确实是一个很不错的选择。但是,这种方式可能会遇到许多不确定性,例如没法按时交付。此外,如果这个项目后期需要进行维护、更新,那这个方案可能就不是最可靠选择了,因为他们可能会转移到其他项目(或者跑路),从而使持续协作变得具有挑战性。如果选择这个方案,建议是朋友推荐,或者是网上具有一定知名度的开发者。在国内,跨平台应用开发者(Flutter开发)的时薪通常在每小时150到350人民币不等。如果选择这个方式,开发成本在10000到50000之间。


外包公司


这种方法是节省开发资金而又不影响产品质量的绝佳方法,通常开发成本在50000到150000之间。如果项目需要后期的维护,迭代,那么可以优先选择这样的方式。(现在的外包公司也比较卷)


自己组团队


如果是想要真的以一种创业的方式,那么开发成本的范围是0到无上限。如果自身就是一个技术人员,那么只需要一台笔记本就可以完成对应用的开发,所花的只是时间成本。如果要招人组团队,那成本就不可估计了。


测试阶段


这部分在大多数App开发过程中,已经由开发者自己测试解决的。稍微正规些的应用可以将测试的工作外包给测试公司。成本在0~20000人民币之间。


维护与迭代


开发一款App不是短跑,而更像是一场马拉松。即使在App第一版上线后,这个旅程仍在继续。定期更新、bug修复和UI修改只是维护App的冰山一角。最好预留总成本的 15-20% 的额外费用,用来进行维护。


其他因素


——每个项目都是独特的,具体要求将决定最终成本。因此,在规划App开发预算时,必须彻底了解这些因素并加以考虑。


第三方API集成


如果项目中需要集成即时通讯等功能模块,那么第三方API集成的这部分的花销也是不可忽略。


软著申请、应用商店发布


软著申请是免费的,自行准备材料申请即可,但是通常会有2~3个月的时间,才能申请成功。如果想快速申请,可以找专门的三方申请机构,价格在500-2000左右。如果App需要上架Google Play和App Store,那么,开通Google Play 开发者账户一次性收取 25 美元费用,Apple Store 个人开发者账号每年收取 99 美元费用。此外,还会从应用内购买或订阅中扣除部分费用。申请软著和App上架的材料准备工作,通常需要10-20小时的工作。按每小时50元,此部分工作需要500-1000元的费用。


后端开发和服务器的费用


如果App只会进行一些本地操作,那么这部分的费用基本为0。如果需要后端提供服务,则需要在拿出一大笔钱进行后端的开发和服务器的购买费用。


如何降低开发成本


外包项目


这种模式允许利用全球人才库,通常以比雇用本地人才更具竞争力的价格获得服务。这点如果你在美国等发达国家可以考虑。如果在大陆,可以看看三哥他们。此外,这种方式还减少了对办公空间和设备的需求,并减少了与员工福利和津贴相关的管理费用。


明确项目要求


还是那句话,最后的成本一定与开始的需求有着很大关联。所以一定要精简需求,明确App到底要做什么。


专注于敏捷方法


如果你是个人开发者或者要带领团队开发,那一定要注重敏捷开发,确定任务优先级、经常重新评估和调整项目目标。


结论 — 关于开发一款App的成本


关于开发一款App的成本,为了让大家能更直观的感受,让我们具体数字来说明这一点。(采用Flutter跨平台)



  1. 对于简单功能的App(例如提供膳食计划App、日记App、记账App等),估计开发成本约为 10,000 — 50,000人民币之间,根据项目的复杂度来决定。

  2. 对于中等复杂度的App(例如具有即时通讯、语音通话等功能)预计成本约为 50,000 — 150,000人民币之间。

  3. 对于开发高复杂度的应用,例如抖音(简化版,真抖音现在哪个团队能从0开始做一个...),起价基本在150,000,上不封顶。


那这就是当前开发一款App的成本,以及对应的工作。


免责声明:本文中提供的数字是大致的、调研来的,可能会根据具体项目要求而有所不同!!!


作者:编程的平行世界
来源:juejin.cn/post/7312353213348347916
收起阅读 »

ThreadLocal:你不知道的优化技巧,Android开发者都在用

引言 在Android开发中,多线程是一个常见的话题。为了有效地处理多线程的并发问题,Android提供了一些工具和机制。其中,ThreadLocal是一个强大的工具,它可以使得每个线程都拥有自己独立的变量副本,从而避免了线程安全问题。 本文将深入探讨Andr...
继续阅读 »

引言


Android开发中,多线程是一个常见的话题。为了有效地处理多线程的并发问题,Android提供了一些工具和机制。其中,ThreadLocal是一个强大的工具,它可以使得每个线程都拥有自己独立的变量副本,从而避免了线程安全问题。


本文将深入探讨Android中的ThreadLocal原理及其使用技巧, 帮助你更好的理解和使用ThreadLocal


ThreadLocal的原理


public class Thread implements Runnable {

/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */

ThreadLocal.ThreadLocalMap threadLocals = null;
}

ThreadLocal的原理是基于每个线程都有一个独立的ThreadLocalMap对象。ThreadLocalMap对象是一个Map,它的键是ThreadLocal对象,值是ThreadLocal对象保存的值。


public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}

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

当我们调用ThreadLocalset()方法时,会将值存储到当前线程的ThreadLocalMap对象中。当我们调用ThreadLocalget()方法时,会从当前线程的ThreadLocalMap对象中获取值。


ThreadLocal的使用


使用ThreadLocal非常简单,首先需要创建一个ThreadLocal对象,然后通过setget方法来设置和获取线程的局部变量。以下是一个简单的例子:


val threadLocal = ThreadLocal<String>()

fun setThreadName(name: String) {
threadLocal.set(name)
}

fun getThreadName(): String {
return threadLocal.get() ?: "DefaultThreadName"
}

Android开发中,ThreadLocal的使用场景非常多,比如:



  • Activity中存储Fragment的状态

  • Handler中存储消息的上下文

  • RecyclerView中存储滚动位置


实际应用场景


// 在 Activity 中存储 Fragment 的状态
class MyActivity : AppCompatActivity() {

private val mFragmentState = ThreadLocal<FragmentState>()

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

// 获取 Fragment 的状态
val fragmentState = mFragmentState.get()
if (fragmentState == null) {
// 初始化 Fragment 的状态
fragmentState = FragmentState()
}

// 设置 Fragment 的状态
mFragmentState.set(fragmentState)

// 创建 Fragment
val fragment = MyFragment()
fragment.arguments = fragmentState.toBundle()
supportFragmentManager.beginTransaction().add(R.id.container, fragment).commit()
}

}

class FragmentState {

var name: String? = null
var age: Int? = null

fun toBundle(): Bundle {
val bundle = Bundle()
bundle.putString("name", name)
bundle.putInt("age", age)
return bundle
}

}

这段代码在Activity中使用ThreadLocal来存储Fragment的状态。当Activity第一次启动时,会初始化Fragment的状态。当Activity重新启动时,会从ThreadLocal中获取Fragment的状态,并将其传递给Fragment


注意事项



  • 内存泄漏风险:


ThreadLocal变量的生命周期与线程的生命周期是一致的。这意味着,如果一个线程一直不结束,那么它所持有的ThreadLocal变量也不会被释放。这可能会导致内存泄漏。


为了避免内存泄漏,我们应该在不再需要ThreadLocal变量时,显式地将其移除。


threadLocal.remove()


  • 不适合全局变量: ThreadLocal适用于需要在线程间传递的局部变量,但不适合作为全局变量的替代品。


优化技巧



  • 合理使用默认值: 在获取ThreadLocal值时,可以通过提供默认值来避免返回null,确保代码的健壮性。


fun getThreadName(): String {
return threadLocal.get() ?: "DefaultThreadName"
}


  • 懒加载初始化: 避免在声明ThreadLocal时就初始化,可以使用initialValue方法进行懒加载,提高性能。


val threadLocal = object : ThreadLocal<String>() {
override fun initialValue(): String {
return "DefaultValue"
}
}


  • 尽量避免在ThreadLocal中保存大对象


结论


在本文中,我们介绍了ThreadLocal的原理和使用技巧,希望这些知识能够帮助你更好地理解和使用它。


推荐


android_startup: 提供一种在应用启动时能够更加简单、高效的方式来初始化组件,优化启动速度。不仅支持Jetpack App Startup的全部功能,还提供额外的同步与异步等待、线程控制与多进程支持等功能。


AwesomeGithub: 基于Github的客户端,纯练习项目,支持组件化开发,支持账户密码与认证登陆。使用Kotlin语言进行开发,项目架构是基于JetPack&DataBinding的MVVM;项目中使用了Arouter、Retrofit、Coroutine、Glide、Dagger与Hilt等流行开源技术。


flutter_github: 基于Flutter的跨平台版本Github客户端,与AwesomeGithub相对应。


android-api-analysis: 结合详细的Demo来全面解析Android相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点。


daily_algorithm: 每日一算法,由浅入深,欢迎加入一起共勉。


作者:午后一小憩
来源:juejin.cn/post/7317859658285858842
收起阅读 »

Android 通知文本颜色获取

前言 Android Notification 几乎每个版本都有改动,因此有很多兼容性问题,摆在开发者面前的难题是每个版本的展示效果不同,再加app保活能力的日渐式微和google大力推进WorkManager、JobScheduler、前台进程的行为,即便A...
继续阅读 »

前言


Android Notification 几乎每个版本都有改动,因此有很多兼容性问题,摆在开发者面前的难题是每个版本的展示效果不同,再加app保活能力的日渐式微和google大力推进WorkManager、JobScheduler、前台进程的行为,即便AlarmManager#setAlarmClock这种可以解除Dozen模式的超级工具,也无法对抗进程死亡的问题,通知到达率和及时性的效果已经大幅减弱。


Screenshot_20190403-131551.png


自定义通知是否仍有必要?

实际上,目前大多推送通知都被系统厂商代理展示了,导致实现效果雷同且没有新意。众多一样的效果,站在用户角度也产生了很多厌恶情绪,对用户的吸引点也是逐渐减弱,这其实和自定义通知的初衷是相背离的,因为自定义通知首要解决的是特色功能的展示,而通用通知却很难做到这一点。因此,在一些app中,自定义通知仍然是有必要的,但必要性没有那么强了。


当前的使用场景:



  • 前台进程常驻类型app,比如直播、音乐类等

  • 类似QQ的app浮动弹窗提醒 (这类不算通知,但是可以使用统一的方法适配)

  • 系统白名单中的app


现状


通知首要解决的是功能问题,其次是主题问题。当前,大部分app已经习惯使用系统通知栏而不使用自定义的通知,主要原因是适配难度问题。


对于自定义通知的适配,目前有两条路线:



  • 统一样式:

    是定义一套深色模式和浅色模式都能通用的色彩搭配,一些音视频app也是这么做的,巧妙的避免了因系统主题不一致造成的现实效果不同的问题,但仍然在部分手机上展示的比较突兀。

  • 读取系统通知颜色进行适配:

    遗憾的是,在Android 7.0之后,正常的通知是拿不到notification.contentView,但似乎没有看到相关的文章来解决这个问题。


两种方案可以搭配使用,但方案二目前存在无法提取颜色的问题,关键是怎么解决contentView拿不到的问题呢?接下来我们重点解决方案二的这个问题。


问题点


我们在无论使用NotificationBuilder或者NotificationCompatBuilder,其内部的build方法存在targetSdkVersion的判断,而在大于Android 7.0 的版本中,不会立即创建ContentView


protected Notification buildInternal() {
if (Build.VERSION.SDK_INT >= 26) {
return mBuilder.build();
} else if (Build.VERSION.SDK_INT >= 24) {
Notification notification = mBuilder.build();

if (mGr0upAlertBehavior != GR0UP_ALERT_ALL) {
// if is summary and only children should alert
if (notification.getGr0up() != null
&& (notification.flags & FLAG_GR0UP_SUMMARY) != 0
&& mGr0upAlertBehavior == GR0UP_ALERT_CHILDREN) {
removeSoundAndVibration(notification);
}
// if is group child and only summary should alert
if (notification.getGr0up() != null
&& (notification.flags & FLAG_GR0UP_SUMMARY) == 0
&& mGr0upAlertBehavior == GR0UP_ALERT_SUMMARY) {
removeSoundAndVibration(notification);
}
}

return notification;
} else if (Build.VERSION.SDK_INT >= 21) {
mBuilder.setExtras(mExtras);
Notification notification = mBuilder.build();
if (mContentView != null) {
notification.contentView = mContentView;
}
if (mBigContentView != null) {
notification.bigContentView = mBigContentView;
}
if (mHeadsUpContentView != null) {
notification.headsUpContentView = mHeadsUpContentView;
}

if (mGr0upAlertBehavior != GR0UP_ALERT_ALL) {
// if is summary and only children should alert
if (notification.getGr0up() != null
&& (notification.flags & FLAG_GR0UP_SUMMARY) != 0
&& mGr0upAlertBehavior == GR0UP_ALERT_CHILDREN) {
removeSoundAndVibration(notification);
}
// if is group child and only summary should alert
if (notification.getGr0up() != null
&& (notification.flags & FLAG_GR0UP_SUMMARY) == 0
&& mGr0upAlertBehavior == GR0UP_ALERT_SUMMARY) {
removeSoundAndVibration(notification);
}
}
return notification;
} else if (Build.VERSION.SDK_INT >= 20) {
mBuilder.setExtras(mExtras);
Notification notification = mBuilder.build();
if (mContentView != null) {
notification.contentView = mContentView;
}
if (mBigContentView != null) {
notification.bigContentView = mBigContentView;
}

if (mGr0upAlertBehavior != GR0UP_ALERT_ALL) {
// if is summary and only children should alert
if (notification.getGr0up() != null
&& (notification.flags & FLAG_GR0UP_SUMMARY) != 0
&& mGr0upAlertBehavior == GR0UP_ALERT_CHILDREN) {
removeSoundAndVibration(notification);
}
// if is group child and only summary should alert
if (notification.getGr0up() != null
&& (notification.flags & FLAG_GR0UP_SUMMARY) == 0
&& mGr0upAlertBehavior == GR0UP_ALERT_SUMMARY) {
removeSoundAndVibration(notification);
}
}

return notification;
} else if (Build.VERSION.SDK_INT >= 19) {
SparseArray<Bundle> actionExtrasMap =
NotificationCompatJellybean.buildActionExtrasMap(mActionExtrasList);
if (actionExtrasMap != null) {
// Add the action extras sparse array if any action was added with extras.
mExtras.putSparseParcelableArray(
NotificationCompatExtras.EXTRA_ACTION_EXTRAS, actionExtrasMap);
}
mBuilder.setExtras(mExtras);
Notification notification = mBuilder.build();
if (mContentView != null) {
notification.contentView = mContentView;
}
if (mBigContentView != null) {
notification.bigContentView = mBigContentView;
}
return notification;
} else if (Build.VERSION.SDK_INT >= 16) {
Notification notification = mBuilder.build();
// Merge in developer provided extras, but let the values already set
// for keys take precedence.
Bundle extras = NotificationCompat.getExtras(notification);
Bundle mergeBundle = new Bundle(mExtras);
for (String key : mExtras.keySet()) {
if (extras.containsKey(key)) {
mergeBundle.remove(key);
}
}
extras.putAll(mergeBundle);
SparseArray<Bundle> actionExtrasMap =
NotificationCompatJellybean.buildActionExtrasMap(mActionExtrasList);
if (actionExtrasMap != null) {
// Add the action extras sparse array if any action was added with extras.
NotificationCompat.getExtras(notification).putSparseParcelableArray(
NotificationCompatExtras.EXTRA_ACTION_EXTRAS, actionExtrasMap);
}
if (mContentView != null) {
notification.contentView = mContentView;
}
if (mBigContentView != null) {
notification.bigContentView = mBigContentView;
}
return notification;
} else {
return mBuilder.getNotification();
}
}

那么我们怎么解决这个问题呢?


Context Wrapper


在App 开发中,Context Wrapper是常见的事情,比如用在预加载Layout、模拟Service运行、插件加载等方面有大量使用。


本文思路是要hack targetSdkVersion,但targetSdkVersion是保存在ApplicationInfo中的,不过没关系,它是通过Context获取的,因此我们在它获取前将其修改为android 5.0的不就行了?


为什么可以修改ApplicationInfo,因为其事Parcelable的子类,看到Parcleable的子类你就能明白,该类的修改是不会触发系统服务的调度,但会影响部分功能,安全起见,我们可以拷贝一下。


public class NotificationContext extends ContextWrapper {
private Context mContextBase;
private ApplicationInfo mApplicationInfo;
private NotificationContext(Context base) {
super(base);
this.mContextBase = base;
}

@Override
public ApplicationInfo getApplicationInfo() {
if(mApplicationInfo!=null) return mApplicationInfo;
ApplicationInfo applicationInfo = super.getApplicationInfo();
mApplicationInfo = new ApplicationInfo(applicationInfo);
return mApplicationInfo;
}

public static NotificationContext from(Context context) {
return new NotificationContext(context);
}
}

targetSdkVersion hack


下一步,修改targetSdkVersion 为android 5.0版本


NotificationContext notificationContext = NotificationContext.from(context);
ApplicationInfo applicationInfo = notificationContext.getApplicationInfo();
int targetSdkVersion = applicationInfo.targetSdkVersion;

applicationInfo.targetSdkVersion = Math.min(21, targetSdkVersion);

完整的代码


要获取的属性


class NotificationResourceInfo {
String titleResourceName;
int titleColor;
float titleTextSize;
ViewGr0up.LayoutParams titleLayoutParams;
String descResourceName;
int descColor;
float descTextSize;
ViewGr0up.LayoutParams descLayoutParams;
long updateTime;

}

获取颜色,用于判断是不是深色模式,这里其实利用的是标记查找方法,先给标题和内容设置Text,然后查找具备此Text的TextView


private static String TITLE_TEXT = "APP_TITLE_TEXT";
private static String CONTENT_TEXT = "APP_CONTENT_TEXT";

下面是核心查找逻辑


  //遍历布局找到字体最大的两个textView,视其为主副标题
private <T extends View> T findView(ViewGr0up viewGr0upSource, CharSequence locatorTextId) {

Queue<ViewGr0up> queue = new ArrayDeque<>();
queue.add(viewGr0upSource);
while (!queue.isEmpty()) {
ViewGr0up parentGr0up = queue.poll();
if (parentGr0up == null) {
continue;
}
int childViewCount = parentGr0up.getChildCount();
for (int num = 0; num < childViewCount; ++num) {
View childView = parentGr0up.getChildAt(num);
String resourceIdName = getResourceIdName(childView.getContext(), childView.getId());
Log.d("NotificationManager", "--" + resourceIdName);
if (TextUtils.equals(resourceIdName, locatorTextId)) {
Log.d("NotificationManager", "findView");
return (T) childView;
}
if (childView instanceof ViewGr0up) {
queue.add((ViewGr0up) childView);
}

}
}
return null;

}

NotificationThemeHelper 实现


public class NotificationThemeHelper {
private static String TITLE_TEXT = "APP_TITLE_TEXT";
private static String CONTENT_TEXT = "APP_CONTENT_TEXT";

final static String TAG = "NotificationThemeHelper";
static SoftReference<NotificationResourceInfo> notificationInfoReference = null;
private static final String CHANNEL_NOTIFICATION_ID = "CHANNEL_NOTIFICATION_ID";

public NotificationResourceInfo parseNotificationInfo(Context context) {
String channelId = createNotificationChannel(context, CHANNEL_NOTIFICATION_ID, CHANNEL_NOTIFICATION_ID);
NotificationResourceInfo notificationInfo = null;
NotificationContext notificationContext = NotificationContext.from(context);
ApplicationInfo applicationInfo = notificationContext.getApplicationInfo();
int targetSdkVersion = applicationInfo.targetSdkVersion;

try {
applicationInfo.targetSdkVersion = Math.min(21, targetSdkVersion);
//更改版本号,这样可以让builder自行创建contentview
NotificationCompat.Builder builder = new NotificationCompat.Builder(notificationContext, channelId);
builder.setContentTitle(TITLE_TEXT);
builder.setContentText(CONTENT_TEXT);
int icon = context.getApplicationInfo().icon;
builder.setSmallIcon(icon);
Notification notification = builder.build();
if (notification.contentView == null) {
return null;
}
int layoutId = notification.contentView.getLayoutId();
ViewGr0up root = (ViewGr0up) LayoutInflater.from(context).inflate(layoutId, null);
notificationInfo = getNotificationInfo(notificationContext, root);

} catch (Exception e) {
Log.d(TAG, "更新失败");
} finally {
applicationInfo.targetSdkVersion = targetSdkVersion;
}
return notificationInfo;
}

private NotificationResourceInfo getNotificationInfo(Context Context, ViewGr0up root) {
NotificationResourceInfo resourceInfo = new NotificationResourceInfo();

root.measure(0,0);
root.layout(0,0,root.getMeasuredWidth(),root.getMeasuredHeight());

Log.i(TAG,"bitmap ok");

TextView titleTextView = (TextView) root.findViewById(android.R.id.title);
if (titleTextView == null) {
titleTextView = findView(root, "android:id/title");
}
if (titleTextView != null) {
resourceInfo.titleColor = titleTextView.getCurrentTextColor();
resourceInfo.titleResourceName = getResourceIdName(Context, titleTextView.getId());
resourceInfo.titleTextSize = titleTextView.getTextSize();
resourceInfo.titleLayoutParams = titleTextView.getLayoutParams();
}

TextView contentTextView = findView(root, "android:id/text");
if (contentTextView != null) {
resourceInfo.descColor = contentTextView.getCurrentTextColor();
resourceInfo.descResourceName = getResourceIdName(Context, contentTextView.getId());
resourceInfo.descTextSize = contentTextView.getTextSize();
resourceInfo.descLayoutParams = contentTextView.getLayoutParams();
}
return resourceInfo;
}

//遍历布局找到字体最大的两个textView,视其为主副标题
private <T extends View> T findView(ViewGr0up viewGr0upSource, CharSequence locatorTextId) {

Queue<ViewGr0up> queue = new ArrayDeque<>();
queue.add(viewGr0upSource);
while (!queue.isEmpty()) {
ViewGr0up parentGr0up = queue.poll();
if (parentGr0up == null) {
continue;
}
int childViewCount = parentGr0up.getChildCount();
for (int num = 0; num < childViewCount; ++num) {
View childView = parentGr0up.getChildAt(num);
String resourceIdName = getResourceIdName(childView.getContext(), childView.getId());
Log.d("NotificationManager", "--" + resourceIdName);
if (TextUtils.equals(resourceIdName, locatorTextId)) {
Log.d("NotificationManager", "findView");
return (T) childView;
}
if (childView instanceof ViewGr0up) {
queue.add((ViewGr0up) childView);
}

}
}
return null;

}

public boolean isDarkNotificationTheme(Context context) {
NotificationResourceInfo notificationInfo = getNotificationInfoFromReference();
if (notificationInfo == null) {
notificationInfo = parseNotificationInfo(context);
saveNotificationInfoToReference(notificationInfo);
}
if (notificationInfo == null) {
return isLightColor(Color.TRANSPARENT);
}
return !isLightColor(notificationInfo.titleColor);
}

private void saveNotificationInfoToReference(NotificationResourceInfo notificationInfo) {
if (notificationInfoReference != null) {
notificationInfoReference.clear();
}

if (notificationInfo == null) return;
notificationInfo.updateTime = SystemClock.elapsedRealtime();
notificationInfoReference = new SoftReference<NotificationResourceInfo>(notificationInfo);
}

private boolean isLightColor(int color) {
int simpleColor = color | 0xff000000;
int baseRed = Color.red(simpleColor);
int baseGreen = Color.green(simpleColor);
int baseBlue = Color.blue(simpleColor);
double value = (baseRed * 0.299 + baseGreen * 0.587 + baseBlue * 0.114);
if (value < 192.0) {
Log.d("ColorInfo", "亮色");
return true;
}
Log.d("ColorInfo", "深色");
return false;
}

public NotificationResourceInfo getNotificationInfoFromReference() {
if (notificationInfoReference == null) {
return null;
}
NotificationResourceInfo resourceInfo = notificationInfoReference.get();
if (resourceInfo == null) {
return null;
}
long dx = SystemClock.elapsedRealtime() - resourceInfo.updateTime;
if (dx > 10 * 1000) {
return null;
}
return resourceInfo;
}

public static String getResourceIdName(Context context, int id) {

Resources r = context.getResources();
StringBuilder out = new StringBuilder();
if (id > 0 && resourceHasPackage(id) && r != null) {
try {
String pkgName;
switch (id & 0xff000000) {
case 0x7f000000:
pkgName = "app";
break;
case 0x01000000:
pkgName = "android";
break;
default:
pkgName = r.getResourcePackageName(id);
break;
}
String typeName = r.getResourceTypeName(id);
String entryName = r.getResourceEntryName(id);
out.append(pkgName);
out.append(":");
out.append(typeName);
out.append("/");
out.append(entryName);
} catch (Resources.NotFoundException e) {
}
}
return out.toString();
}

private String createNotificationChannel (Context context,String channelID, String channelNAME){
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
NotificationManager manager = (NotificationManager)context. getSystemService(NOTIFICATION_SERVICE);
NotificationChannel channel = new NotificationChannel(channelID, channelNAME, NotificationManager.IMPORTANCE_LOW);
manager.createNotificationChannel(channel);
return channelID;
} else {
return null;
}
}
public static boolean resourceHasPackage(int resid) {
return (resid >>> 24) != 0;
}
}

深浅色判断其实有两种方法,第一种是305911公式,第二种是相似度。


下面是305911公式的,其实就是利用视频亮度算法YUV中的Y分量计算,Y分量表示明亮度。


private boolean isLightColor(int color) {
int simpleColor = color | 0xff000000;
int baseRed = Color.red(simpleColor);
int baseGreen = Color.green(simpleColor);
int baseBlue = Color.blue(simpleColor);
double value = (baseRed * 0.299 + baseGreen * 0.587 + baseBlue * 0.114);
if (value < 192.0) {
Log.d("ColorInfo", "亮色");
return true;
}
Log.d("ColorInfo", "深色");
return false;
}

第二种是相似度算法,一般用于检索相似照片,一般用于优化汉明距离算法,不过这里可以用来判断是否接近黑色。
blog.csdn.net/zz_dd_yy/ar…


private boolean isSimilarColor(int colorL, int colorR) {
int red = Color.red(colorL);
int green = Color.green(colorL);
int blue = Color.blue(colorL);

int red2 = Color.red(colorR);
int green2 = Color.green(colorR);
int blue2 = Color.blue(colorR);

float vertor = red * red2 + green * green2 + blue * blue2;
// 向量1的模
double vectorMold1 = Math.sqrt(Math.pow(red, 2) + Math.pow(green, 2) + Math.pow(blue, 2));
// 向量2的模
double vectorMold2 = Math.sqrt(Math.pow(red2, 2) + Math.pow(green2, 2) + Math.pow(blue2, 2));

// 向量的夹角[0, PI],当夹角为锐角时,cosθ>0;当夹角为钝角时,cosθ<0
float cosAngle = (float) (vertor / (vectorMold1 * vectorMold2));
float radian = (float) Math.acos(cosAngle);

float degrees = (float) Math.toDegrees(radian);
if(degrees>= 0 && degrees < 30) {
return true;
}
return false;
}


用法


这种适配其实无法拿到背景色,只能拿到文字的颜色,如果文字偏亮则背景必须的是深色,反之区亮色,那么核心方法是下面的实现


public boolean isDarkNotificationTheme(Context context) {
NotificationResourceInfo notificationInfo = getNotificationInfoFromReference();
if (notificationInfo == null) {
notificationInfo = parseNotificationInfo(context);
saveNotificationInfoToReference(notificationInfo);
}
if (notificationInfo == null) {
return isLightColor(Color.TRANSPARENT);
}
return !isLightColor(notificationInfo.titleColor);
}

遗留问题


正常情况下,只能取深色和暗色,但是如果存在系统UI Mode的变化时,已经展示出来的通知,显然适配颜色无法动态变化,这也是无法避免的,解决办法是删除通知后重新发送。


总结


本篇到这里就结束了,说实在的,Android的通知的重要性大不如从前,但是必要的适配还是需要的。


作者:时光少年
来源:juejin.cn/post/7320146668476645387
收起阅读 »

PageHelper引发的“幽灵数据”,怎么回事?

前言 最近测试反馈一个问题,某个查询全量信息的接口,有时候返回全量数据,符合预期,但是偶尔又只返回1条数据,简直就是“见鬼”了,究竟是为什么出现这样的“幽灵数据”呢? 大胆猜测 首先我们看了下这对代码的业务逻辑,非常的简单,总共没有几行代码,也没有分页逻辑,代...
继续阅读 »

前言


最近测试反馈一个问题,某个查询全量信息的接口,有时候返回全量数据,符合预期,但是偶尔又只返回1条数据,简直就是“见鬼”了,究竟是为什么出现这样的“幽灵数据”呢?


大胆猜测


首先我们看了下这对代码的业务逻辑,非常的简单,总共没有几行代码,也没有分页逻辑,代码如下:


public  List<SdSubscription> findAll() {
return sdSubscriptionMapper.selectAll();
}

那么究竟是咋回事呢?讲道理不可能出现这种情况的啊,不要慌,我们加点日志,将日志级别调整为DEBUG,让日志飞一段时间。


public  List<SdSubscription> findAll() {
log.info("find the sub start .....");
List<SdSubscription> subs = sdSubscriptionMapper.selectAll();
log.info("find the sub end .....");
return subs;
}

果不其然,日志中出现了奇奇怪怪的分页参数,如下图所示:



果然是PageHelper这个开源框架搞的鬼,我想大家都用过吧,分页非常方便,那么究竟为什么别人都没问题,单单就我会出现问题呢?


PageHelper工作原理


为了回答上面的疑问,我们先看看PageHelper框架的工作原理吧。


PageHelper 是一个开源的 MyBatis 分页插件,它可以帮助开发者在查询数据时,快速的实现分页功能。


PageHelper 的工作原理可以简单概括为以下几个步骤:



  1. 在需要进行分页的查询方法前,调用 PageHelper 的静态方法 startPage(),设置当前页码和每页显示的记录数。它会将分页信息放到线程的ThreadLocal中,那么在线程的任何地方都可以访问了。

  2. 当查询方法执行时,PageHelper 会自动拦截查询语句,如果发现线程的ThreadLocal中有分页信息,那么就会在其前后添加分页语句,例如 MySQL 中的 LIMIT 语句。

  3. 查询结果将被包装在 Page 对象中返回,该对象包含分页信息和查询结果列表。

  4. 在查询方法执行完毕后,会在finally中清除线程ThreadLocal中的分页信息,避免分页设置对其他查询方法的影响。


PageHelper 的实现原理主要依赖于拦截器技术和反射机制,通过拦截查询语句并动态生成分页语句,实现了简单、高效、通用的分页功能。具体源码在下图的类中,非常容易看懂。



明白了PageHelper的工作原理后,反复检查代码,都没有调用过startPagedebug查看ThreadLocal中也没有分页信息啊,懵逼中。那我看看别人写的添加分页参数的代码吧,不看不知道,一看吓一跳。



原来有位“可爱”的同事竟然在查询后,加了一个分页,就是把分页信息放到线程的ThreadLocal中。


那大家是不是有疑问,丁是丁,矛是矛,你的线程关我何事?这就要说到我们的tomcat了。


Tomcat请求流程


其实这就涉及到我们的tomcat相关知识了,我们一个浏览器发一个接口请求,经过我们的tomcat的,究竟是一个什么样的流程呢?



  1. 客户端发送HTTP请求到Tomcat服务器。

  2. TomcatHTTP连接器(Connector)接收到请求,将连接请求交给线程池Executor处理,解析它,然后将请求转发给对应的Web应用程序。

  3. Tomcat的Web应用程序容器(Container)接收到请求,根据请求的URL找到对应的Servlet


关于tomcat中使用线程池提交浏览器的连接请求的源码如下:



从而得知,你的连接请求是从线程池从拿的,而拿到的这个线程恰好是一个“脏线程”,在ThreadLocal中放了分页信息,导致你这边出现问题。


总结


后来追问了同事具体原因,才发现是粗心导致的。有些bug总是出现的莫名其妙,就像生活一样。所以关键的是我们在使用一些开源框架的时候一定要掌握底层实现的原理、核心的机制,这样才能够在解决一些问题的时候有据可循。



欢迎关注个人公众号【JAVA旭阳】交流学习!



作者:JAVA旭阳
来源:juejin.cn/post/7223590232730370108
收起阅读 »

该死,这次一定要弄懂什么是时间复杂度和空间复杂度!

开始首先,相信大家在看一些技术文章或者刷算法题的时候,总是能看到要求某某某程序(算法)的时间复杂度为O(n)或者O(1)等字样,就像这样: Q:那么到底这个O(n)、O(1)是什么意思呢?A:时间复杂度和空间复杂度其实是对算法执行期间的性能进行衡量的...
继续阅读 »

开始

首先,相信大家在看一些技术文章或者刷算法题的时候,总是能看到要求某某某程序(算法)的时间复杂度为O(n)或者O(1)等字样,就像这样:

image.png Q:那么到底这个O(n)、O(1)是什么意思呢?

A:时间复杂度空间复杂度其实是对算法执行期间的性能进行衡量的依据。

Talk is cheap, show me the code!

下面从代码入手,来直观的理解一下这两个概念:

时间复杂度

先来看看copilot如何解释的

image.png

  • 举个🌰
function fn (arr) {
let length = arr.length
for (let i = 0; i < length; i++) {
console.log(arr[i])
}
}

首先来分析一下这段代码,这是一个函数,接收一个数组,然后对这个数组进行了一个遍历

  1. 第一段代码,在函数执行的时候,这段代码只会被执行1次,这里记为 1 次
let length = arr.length
  1. 循环体中的代码,循环多少次就会执行多少次,这里记为 n 次
console.log(arr[i])
  1. 循环条件部分,首先是 let i = 0,只会执行一次,记为 1 次
  2. 然后是i < length这个判断,想要退出循环,这里最后肯定要比循环次数多判断一次,所以记为 n + 1 次
  3. 最后是 i++,会执行 n 次

我们把总的执行次数记为T(n)

T(n) = 1 + n + 1 (n + 1) + n = 3n + 3
  • 再来一个🌰
// arr 是一个二维数组
function fn2(arr) {
let lenOne = arr.length
for(let i = 0; i < lenOne; i++) {
let lenTwo = arr[i].length
for(let j = 0; j < lenTwo; j++) {
console.log(arr[i][j])
}
}
}

来分析一下这段代码,这是一个针对二维数组进行遍历的操作,我们再来分析一下这段代码的执行次数

  1. 第一行赋值代码,只会执行1次
let lenOne = arr.length
  1. 第一层循环,let i = 0 1次,i < lenOne n + 1 次,i++ n 次,let len_two = arr[i].length n 次
  2. 第二层循环,let j = 0 n 次,j < lenTwo n * (n + 1) 次,j++ n * n 次
  3. console n*n 次
T(n) = 1 + n + 1 + n + n + n + n * (n + 1) + n * n + n * n = 3n^2 + 5n + 3

代码的执行次数,可以反映出代码的执行时间。但是如果每次我们都逐行去计算 T(n),事情会变得非常麻烦。算法的时间复杂度,它反映的不是算法的逻辑代码到底被执行了多少次,而是随着输入规模的增大,算法对应的执行总次数的一个变化趋势。我们可以尝试对 T(n) 做如下处理:

  • 若 T(n) 是常数,那么无脑简化为1
  • 若 T(n) 是多项式,比如 3n^2 + 5n + 3,我们只保留次数最高那一项,并且将其常数系数无脑改为1。

那么上面两个算法的时间复杂度可以简化为:

T(n) = 3n + 3
O(n) = n

T(n) = 3n^2 + 5n + 3
O(n) = n^2

实际推算时间复杂度时不用这么麻烦,像上面的两个函数,第一个是规模为n的数组的遍历,循环会执行n次,所以对应的时间幅度是O(n),第二个函数是 n*n的二维数组的遍历,对应的时间复杂度就是O(n^2) 依次类推,规模为n*m的二维数组的遍历,时间复杂度就是O(n*m)

常见的时间复杂度按照从小到大的顺序排列,有以下几种:

常数时间对数时间线性时间线性对数时间二次时间三次时间指数时间
O(1)O(logn)O(n)O(nlogn)O(n^2)O(n^3)O(2^n)

空间复杂度

先看看copilot的解释:

image.png

  • 来一个🌰看看吧:
function fn (arr) {
let length = arr.length
for (let i = 0; i < length; i++) {
console.log(arr[i])
}
}

在函数fn中,我们创建了变量 length arr i,函数 fn 对内存的占用量是固定的,无论,arr的length如何,所以这个函数对应的空间复杂度就是 O(1)

  • 再来一个🌰:
function fn2(n) {
let arr = []
for(let i = 0; i < n; i++) {
arr[i] = i
}
}

在这个函数中,我们创建了一个数组 arr,并在循环中向 arr 中添加了 n 个元素。因此,arr 的大小与输入 n 成正比。所以,我们说这个函数的空间复杂度是 O(n)。

  • 再再来一个🌰:
function createMatrix(n) {
let matrix = [];
for (let i = 0; i < n; i++) {
matrix[i] = [];
for (let j = 0; j < n; j++) {
matrix[i][j] = 0;
}
}
return matrix;
}

在这个函数中,我们创建了一个二维数组 matrix,并在两层循环中向 matrix 中添加了 n*n 个元素。因此,matrix 的大小与输入 n 的平方成正比。所以,我们说这个函数的空间复杂度是 O(n^2)。

  • 再再再来一个🌰:
// 二分查找算法
function binarySearch(arr, target, low, high) {
if (low > high) {
return -1;
}
let mid = Math.floor((low + high) / 2);
if (arr[mid] === target) {
return mid;
} else if (arr[mid] > target) {
return binarySearch(arr, target, low, mid - 1);
} else {
return binarySearch(arr, target, mid + 1, high);
}
}

在二分查找中,我们每次都将问题规模减半,因此需要的额外空间与输入数据的对数成正比,我们开始时有一个大小为 n 的数组。然后,我们在每一步都将数组划分为两半,并只在其中一半中继续查找。因此,每一步都将问题的规模减半

所以,最多要划分多少次才能找到目标数据呢?答案是log2n次,但是在计算机科学中,当我们说 log n 时,底数通常默认为 2,因为许多算法(如二分查找)都涉及到将问题规模减半的操作。

2^x = n

x = log2n

常见的时间复杂度按照从小到大的顺序排列,有以下几种:

常数空间线性空间平方空间对数空间
O(1)O(n)O(n^2)O(logn)

你学废了吗?


作者:爱吃零食的猫
来源:juejin.cn/post/7320288222529536038

收起阅读 »

10个让你爱不释手的一行Javascript代码

web
在这篇博客中,我们将分享 10+ 个实用的一行 JavaScript 代码,这些代码可以帮助你提高编码效率和代码简洁度。这些代码片段将涵盖各种用途,从操作数组和字符串,到更高级的概念,如异步编程和面向对象编程。 获取数组中的随机元素 使用 Math.rand...
继续阅读 »

freysteinn-g-jonsson-s94zCnADcUs-unsplash.jpg
在这篇博客中,我们将分享 10+ 个实用的一行 JavaScript 代码,这些代码可以帮助你提高编码效率和代码简洁度。这些代码片段将涵盖各种用途,从操作数组和字符串,到更高级的概念,如异步编程和面向对象编程。


获取数组中的随机元素


使用 Math.random() 函数和数组长度可以轻松获取数组中的随机元素:


const arr = [1, 2, 3, 4, 5];
const randomElement = arr[Math.floor(Math.random() * arr.length)];
console.log(randomElement);

数组扁平化


使用 reduce() 函数和 concat() 函数可以轻松实现数组扁平化:


const arr = [[1, 2], [3, 4], [5, 6]];
const flattenedArr = arr.reduce((acc, cur) => acc.concat(cur), []);
console.log(flattenedArr); // [1, 2, 3, 4, 5, 6]

对象数组根据某个属性值进行排序


const sortedArray = array.sort((a, b) => (a.property > b.property ? 1 : -1));

从数组中删除特定元素


const removedArray = array.filter((item) => item !== elementToRemove);

检查数组中是否存在重复项


const hasDuplicates = (array) => new Set(array).size !== array.length;

判断数组是否包含某个值


const hasValue = arr.includes(value);

首字母大写


const capitalized = str.charAt(0).toUpperCase() + str.slice(1);

获取随机整数


const randomInt = Math.floor(Math.random() * (max - min + 1)) + min;

获取随机字符串


const randomStr = Math.random().toString(36).substring(2, length);

使用解构和 rest 运算符交换变量的值:


let a = 1, b = 2
[b, a] = [a, b]
console.log(a, b) // 2, 1

将字符串转换为小驼峰式命名:


const str = 'hello world'
const camelCase = str.replace(/\s(.)/g, ($1) => $1.toUpperCase()).replace(/\s/g, '').replace(/^(.)/, ($1) => $1.toLowerCase())
console.log(camelCase) // "helloWorld"

计算两个日期之间的间隔


const diffInDays = (dateA, dateB) => Math.floor((dateB - dateA) / (1000 * 60 * 60 * 24));

查找日期位于一年中的第几天


const dayOfYear = (date) => Math.floor((date - new Date(date.getFullYear(), 0, 0)) / 1000 / 60 / 60 / 24);

复制内容到剪切板


const copyToClipboard = (text) => navigator.clipboard.writeText(text);

copyToClipboard("Hello World");

获取变量的类型


const getType = (variable) => Object.prototype.toString.call(variable).slice(8, -1).toLowerCase();

getType(''); // string
getType(0); // number
getType(); // undefined
getType(null); // null
getType({}); // object
getType([]); // array
getType(0); // number
getType(() => {}); // function

检测对象是否为空


const isEmptyObject = (obj) => Object.keys(obj).length === 0 && obj.constructor === Object;

系列文章



我的更多前端资讯


欢迎大家技术交流 资料分享 摸鱼 求助皆可 —链接


作者:shichuan
来源:juejin.cn/post/7230810119122190397
收起阅读 »

刷了四百道算法题,我在项目里用过哪几道呢?

大家好,我是老三,今天和大家聊一个话题:项目中用到的力扣算法。 不知道从什么时候起,算法已经成为了互联网面试的标配,在十年前,哪怕如日中天的百度,面试也最多考个冒泡排序。后来,互联网越来越热,涌进来的人越来越多,整个行业越来越内卷的,算法也慢慢成了大小互联网公...
继续阅读 »

大家好,我是老三,今天和大家聊一个话题:项目中用到的力扣算法。


不知道从什么时候起,算法已经成为了互联网面试的标配,在十年前,哪怕如日中天的百度,面试也最多考个冒泡排序。后来,互联网越来越热,涌进来的人越来越多,整个行业越来越内卷的,算法也慢慢成了大小互联网公司面试的标配,力扣现在已经超过3000题了,那么这些题目有多少进入了面试的考察呢?


以最爱考算法的字节跳动为例,看看力扣的企业题库,发现考过的题目已经有1850道——按照平均每道题花20分钟来算,刷完字节题库的算法题需要37000分钟,616.66小时,按每天刷满8小时算,需要77.08天,一周刷五天,需要15.41周,按一个月四周,需要3.85个月。也就是说,在脱产,最理想的状态下,刷完力扣的字节题库,需要差不多4个月时间。


字节题库


那么,我在项目里用过,包括在项目中见过哪些力扣上的算法呢?我目前刷了400多道题,翻来覆去盘点了一下,发现,也就这么几道。


刷题数量


1.版本比较:比较客户端版本


场景


在日常的开发中,我们很多时候可能面临这样的情况,兼容客户端的版本,尤其是Android和iPhone,有些功能是低版本不支持的,或者说有些功能到了高版本就废弃掉。


这时候就需要进行客户端的版本比较,客户端版本号通常是这种格式6.3.40,这是一个字符串,那就肯定不能用数字类型的比较方法,需要自己定义一个比较的工具方法。


某app版本


题目


165. 比较版本号


这个场景对应LeetCode: 165. 比较版本号



  • 题目:165. 比较版本号 (leetcode.cn/problems/co…)

  • 难度:中等

  • 标签:双指针 字符串

  • 描述:


    给你两个版本号 version1version2 ,请你比较它们。


    版本号由一个或多个修订号组成,各修订号由一个 '.' 连接。每个修订号由 多位数字 组成,可能包含 前导零 。每个版本号至少包含一个字符。修订号从左到右编号,下标从 0 开始,最左边的修订号下标为 0 ,下一个修订号下标为 1 ,以此类推。例如,2.5.330.1 都是有效的版本号。


    比较版本号时,请按从左到右的顺序依次比较它们的修订号。比较修订号时,只需比较 忽略任何前导零后的整数值 。也就是说,修订号 1 和修订号 001 相等 。如果版本号没有指定某个下标处的修订号,则该修订号视为 0 。例如,版本 1.0 小于版本 1.1 ,因为它们下标为 0 的修订号相同,而下标为 1 的修订号分别为 010 < 1


    返回规则如下:



    • 如果 *version1* > *version2* 返回 1

    • 如果 *version1* < *version2* 返回 -1

    • 除此之外返回 0


    示例 1:


    输入:version1 = "1.01", version2 = "1.001"
    输出:0
    解释:忽略前导零,"01""001" 都表示相同的整数 "1"

    示例 2:


    输入:version1 = "1.0", version2 = "1.0.0"
    输出:0
    解释:version1 没有指定下标为 2 的修订号,即视为 "0"

    示例 3:


    输入:version1 = "0.1", version2 = "1.1"
    输出:-1
    解释:version1 中下标为 0 的修订号是 "0",version2 中下标为 0 的修订号是 "1"0 < 1,所以 version1 < version2

    提示:



    • 1 <= version1.length, version2.length <= 500

    • version1version2 仅包含数字和 '.'

    • version1version2 都是 有效版本号

    • version1version2 的所有修订号都可以存储在 32 位整数




解法


那么这道题怎么解呢?这道题其实是一道字符串模拟题,就像标签里给出了了双指针,这道题我们可以用双指针+累加来解决。


在这里插入图片描述



  • 两个指针遍历version1version2

  • . 作为分隔符,通过累加获取每个区间代表的数字

  • 比较数字的大小,这种方式正好可以忽略前导0


来看看代码:


class Solution {
   public int compareVersion(String version1, String version2) {
       int m = version1.length();
       int n = version2.length();

       //两个指针
       int p = 0, q = 0;

       while (p < m || q < n) {
           //累加version1区间的数字
           int x = 0;
           while (p < m && version1.charAt(p) != '.') {
               x += x * 10 + (version1.charAt(p) - '0');
               p++;
          }

           //累加version2区间的数字
           int y = 0;
           while (q < n && version2.charAt(q) != '.') {
               y += y * 10 + (version2.charAt(q) - '0');
               q++;
          }

           //判断
           if (x > y) {
               return 1;
          }
           if (x < y) {
               return -1;
          }

           //跳过.
           p++;
           q++;
      }
       //version1等于version2
       return 0;
  }
}


应用


这段代码,直接CV过来,就可以直接当做一个工具类的工具方法来使用:


public class VersionUtil {

   public static Integer compareVersion(String version1, String version2) {
       int m = version1.length();
       int n = version2.length();

       //两个指针
       int p = 0, q = 0;

       while (p < m || q < n) {
           //累加version1区间的数字
           int x = 0;
           while (p < m && version1.charAt(p) != '.') {
               x += x * 10 + (version1.charAt(p) - '0');
               p++;
          }

           //累加version2区间的数字
           int y = 0;
           while (q < n && version2.charAt(q) != '.') {
               y += y * 10 + (version2.charAt(q) - '0');
               q++;
          }

           //判断
           if (x > y) {
               return 1;
          }
           if (x < y) {
               return -1;
          }

           //跳过.
           p++;
           q++;
      }
       //version1等于version2
       return 0;
  }
}


前面老三分享过一个规则引擎:这款轻量级规则引擎,真香!


比较版本号的方法,还可以结合规则引擎来使用:



  • 自定义函数:利用AviatorScript的自定义函数特性,自定义一个版本比较函数


        /**
        * 自定义版本比较函数
        */

       class VersionFunction extends AbstractFunction {
           @Override
           public String getName() {
               return "compareVersion";
          }

           @Override
           public AviatorObject call(Map<String, Object> env, AviatorObject arg1, AviatorObject arg2) {
               // 获取版本
               String version1 = FunctionUtils.getStringValue(arg1, env);
               String version2 = FunctionUtils.getStringValue(arg2, env);
               return new AviatorBigInt(VersionUtil.compareVersion(version1, version2));
          }
      }


  • 注册函数:将自定义的函数注册到AviatorEvaluatorInstance


        /**
        * 注册自定义函数
        */

       @Bean
       public AviatorEvaluatorInstance aviatorEvaluatorInstance() {
           AviatorEvaluatorInstance instance = AviatorEvaluator.getInstance();
           // 默认开启缓存
           instance.setCachedExpressionByDefault(true);
           // 使用LRU缓存,最大值为100个。
           instance.useLRUExpressionCache(100);
           // 注册内置函数,版本比较函数。
           instance.addFunction(new VersionFunction());
           return instance;
      }


  • 代码传递上下文:在业务代码里传入客户端、客户端版本的上下文


        /**
        * @param device 设备
        * @param version 版本
        * @param rule   规则脚本
        * @return 是否过滤
        */

       public boolean filter(String device, String version, String rule) {
           // 执行参数
           Map<String, Object> env = new HashMap<>();
           env.put("device", device);
           env.put("version", version);
           //编译脚本
           Expression expression = aviatorEvaluatorInstance.compile(DigestUtils.md5DigestAsHex(rule.getBytes()), rule, true);
           //执行脚本
           boolean isMatch = (boolean) expression.execute(env);
           return isMatch;
      }


  • 编写脚本:接下来我们就可以编写规则脚本,规则脚本可以放在数据库,也可以放在配置中心,这样就可以灵活改动客户端的版本控制规则


    if(device==bil){
    return false;
    }

    ## 控制Android的版本
    if (device=="Android" && compareVersion(version,"1.38.1")<0){
    return false;
    }

    return true;



2.N叉数层序遍历:翻译商品类型


场景


一个跨境电商网站,现在有这么一个需求:把商品的类型进行国际化翻译。


某电商网站商品类型国际化


商品的类型是什么结构呢?一级类型下面还有子类型,字类型下面还有子类型,我们把结构一画,发现这就是一个N叉树的结构嘛。


商品树


翻译商品类型,要做的事情,就是遍历这棵树,翻译节点上的类型,这不妥妥的BFS或者DFS!


题目


429. N 叉树的层序遍历


这个场景对应LeetCode:429. N 叉树的层序遍历



  • 题目:429. N 叉树的层序遍历(leetcode.cn/problems/n-…)

  • 难度:中等

  • 标签: 广度优先搜索

  • 描述:


    给定一个 N 叉树,返回其节点值的层序遍历。(即从左到右,逐层遍历)。


    树的序列化输入是用层序遍历,每组子节点都由 null 值分隔(参见示例)。


    示例 1:


    img


    输入:root = [1,null,3,2,4,null,5,6]
    输出:[[1],[3,2,4],[5,6]]

    示例 2:


    img


    输入:root = [1,null,2,3,4,5,null,null,6,7,null,8,null,9,10,null,null,11,null,12,null,13,null,null,14]
    输出:[[1],[2,3,4,5],[6,7,8,9,10],[11,12,13],[14]]

    提示:



    • 树的高度不会超过 1000

    • 树的节点总数在 [0, 10^4] 之间




解法


BFS想必很多同学都很熟悉了,DFS的秘诀是,BFS的秘诀是队列


层序遍历的思路是什么呢?


使用队列,把每一层的节点存储进去,一层存储结束之后,我们把队列中的节点再取出来,孩子节点不为空,就把孩子节点放进去队列里,循环往复。


N叉树层序遍历示意图


代码如下:


class Solution {
public List<List<Integer>> levelOrder(Node root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) {
return result;
}

//创建队列并存储根节点
Deque<Node> queue = new LinkedList<>();
queue.offer(root);

while (!queue.isEmpty()) {
//存储每层结果
List<Integer> level = new ArrayList<>();
int size = queue.size();
for (int i = 0; i < size; i++) {
Node current = queue.poll();
level.add(current.val);
//添加孩子
if (current.children != null) {
for (Node child : current.children) {
queue.offer(child);
}
}
}
//每层遍历结束,添加结果
result.add(level);
}
return result;
}
}

应用


商品类型翻译这个场景下,基本上和这道题目大差不差,不过是两点小区别:



  • 商品类型是一个属性多一些的树节点

  • 翻译过程直接替换类型名称即可,不需要返回值


来看下代码:



  • ProductCategory:商品分类实体


    public class ProductCategory {
    /**
    * 分类id
    */

    private String id;
    /**
    * 分类名称
    */

    private String name;
    /**
    * 分类描述
    */

    private String description;
    /**
    * 子分类
    */

    private List<ProductCategory> children;

    //省略getter、setter

    }




  • translateProductCategory:翻译商品类型方法


       public void translateProductCategory(ProductCategory root) {
    if (root == null) {
    return;
    }

    Deque<ProductCategory> queue = new LinkedList<>();
    queue.offer(root);

    //遍历商品类型,翻译
    while (!queue.isEmpty()) {
    int size = queue.size();
    //遍历当前层
    for (int i = 0; i < size; i++) {
    ProductCategory current = queue.poll();
    //翻译
    String translation = translate(current.getName());
    current.setName(translation);
    //添加孩子
    if (current.getChildren() != null && !current.getChildren().isEmpty()) {
    for (ProductCategory child : current.getChildren()) {
    queue.offer(child);
    }
    }
    }
    }
    }



3.前缀和+二分查找:渠道选择


场景


在电商的交易支付中,我们可以选择一些支付方式,来进行支付,当然,这只是交易的表象。


某电商支付界面


在支付的背后,一种支付方式,可能会有很多种支付渠道,比如Stripe、Adyen、Alipay,涉及到多个渠道,那么就涉及到决策,用户的这笔交易,到底交给哪个渠道呢?


这其实是个路由问题,答案是加权随机,每个渠道有一定的权重,随机落到某个渠道,加权随机有很多种实现方式,其中一种就是前缀和+二分查找。简单说,就是先累积所有元素权重,再使用二分查找来快速查找。


题目


先来看看对应的LeetCode的题目,这里用到了两个算法:前缀和二分查找


704. 二分查找



  • 题目:704. 二分查找(leetcode.cn/problems/bi…)

  • 难度:简单

  • 标签:数组 二分查找

  • 描述:


    给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1


    示例 1:


    输入: nums = [-1,0,3,5,9,12], target = 9
    输出: 4
    解释: 9 出现在 nums 中并且下标为 4

    示例 2:


    输入: nums = [-1,0,3,5,9,12], target = 2
    输出: -1
    解释: 2 不存在 nums 中因此返回 -1

    提示:



    1. 你可以假设 nums 中的所有元素是不重复的。

    2. n 将在 [1, 10000]之间。

    3. nums 的每个元素都将在 [-9999, 9999]之间。




解法


二分查找可以说我们都很熟了。


数组是有序的,定义三个指针,leftrightmid,其中midleftright的中间指针,每次中间指针指向的元素nums[mid]比较和target比较:


二分查找示意图



  • 如果nums[mid]等于target,找到目标

  • 如果nums[mid]小于target,目标元素在(mid,right]区间;

  • 如果nums[mid]大于target,目标元素在[left,mid)区间


代码:


class Solution {
public int search(int[] nums, int target) {
int left=0;
int right=nums.length-1;

while(left<=right){
int mid=left+((right-left)>>1);
if(nums[mid]==target){
return mid;
}else if(nums[mid]<target){
//target在(mid,right]区间,右移
left=mid+1;
}else{
//target在[left,mid)区间,左移
right=mid-1;
}
}
return -1;
}
}

二分查找,有一个需要注意的细节,计算mid的时候:int mid = left + ((right - left) >> 1);,为什么要这么写呢?


因为这种写法int mid = (left + right) / 2;,可能会因为left和right数值太大导致内存溢出。同时,使用位运算,也是除以2最高效的写法。


——这里有个彩蛋,后面再说。


303. 区域和检索 - 数组不可变


不像二分查找,在LeetCode上,前缀和没有直接的题目,因为本身前缀和更多是一种思路,一种工具,其中303. 区域和检索 - 数组不可变 是一道典型的前缀和题目。



  • 题目:303. 区域和检索 - 数组不可变(leetcode.cn/problems/ra…)

  • 难度:简单

  • 标签:设计 数组 前缀和

  • 描述:


    给定一个整数数组 nums,处理以下类型的多个查询:



    1. 计算索引 leftright (包含 leftright)之间的 nums 元素的 ,其中 left <= right


    实现 NumArray 类:



    • NumArray(int[] nums) 使用数组 nums 初始化对象

    • int sumRange(int i, int j) 返回数组 nums 中索引 leftright 之间的元素的 总和 ,包含 leftright 两点(也就是 nums[left] + nums[left + 1] + ... + nums[right] )


    示例 1:


    输入:
    ["NumArray", "sumRange", "sumRange", "sumRange"]
    [[[-2, 0, 3, -5, 2, -1]], [0, 2], [2, 5], [0, 5]]
    输出:
    [null, 1, -1, -3]

    解释:
    NumArray numArray = new NumArray([-2, 0, 3, -5, 2, -1]);
    numArray.sumRange(0, 2); // return 1 ((-2) + 0 + 3)
    numArray.sumRange(2, 5); // return -1 (3 + (-5) + 2 + (-1))
    numArray.sumRange(0, 5); // return -3 ((-2) + 0 + 3 + (-5) + 2 + (-1))

    提示:



    • 1 <= nums.length <= 104

    • -105 <= nums[i] <= 105

    • 0 <= i <= j < nums.length

    • 最多调用 104sumRange 方法




解法


这道题,我们如果不用前缀和的话,写起来也很简单:


class NumArray {
private int[] nums;

public NumArray(int[] nums) {
this.nums=nums;
}

public int sumRange(int left, int right) {
int res=0;
for(int i=left;i<=right;i++){
res+=nums[i];
}
return res;
}
}

当然时间复杂度偏高,O(n),那么怎么使用前缀和呢?



  • 构建一个前缀和数组,用来累积 (0……i-1)的和,这样一来,我们就可以直接计算[left,right]之间的累加和


前缀和数组示意图


代码如下:


class NumArray {
private int[] preSum;

public NumArray(int[] nums) {
int n=nums.length;
preSum=new int[n+1];
//计算nums的前缀和
for(int i=0;i<n;i++){
preSum[i+1]=preSum[i]+nums[i];
}
}

//直接算出区间[left,right]的累加和
public int sumRange(int left, int right) {
return preSum[right+1]-preSum[left];
}
}

可以看到,通过前缀和数组,可以直接算出区间[left,right]的累加和,时间复杂度O(1),可以说非常高效了。


应用


了解了前缀和和二分查找之后,回归我们之前的场景,使用前缀和+二分查找来实现加权随机,从而实现对渠道的分流选择。


渠道分流选择



  • 需要根据渠道和权重的配置,生成一个前缀和数组,来累积权重的值,渠道也通过一个数组进行分配映射

  • 用户的支付请求进来的时候,生成一个随机数,二分查找找到随机数载前缀和数组的位置,映射到渠道数组

  • 最后通过渠道数组的映射,找到选中的渠道


代码如下:


/**
* 支付渠道分配器
*/
public class PaymentChannelAllocator {
//渠道数组
private String[] channels;
//前缀和数组
private int[] preSum;
private ThreadLocalRandom random;

/**
* 构造方法
*
* @param channelWeights 渠道分流权重
*/
public PaymentChannelAllocator(HashMap<String, Integer> channelWeights) {
this.random = ThreadLocalRandom.current();
// 初始化channels和preSum数组
channels = new String[channelWeights.size()];
preSum = new int[channelWeights.size()];

// 计算前缀和
int index = 0;
int sum = 0;
for (String channel : channelWeights.keySet()) {
sum += channelWeights.get(channel);
channels[index] = channel;
preSum[index++] = sum;
}
}

/**
* 渠道选择
*/
public String allocate() {
// 生成一个随机数
int rand = random.nextInt(preSum[preSum.length - 1]) + 1;

// 通过二分查找在前缀和数组查找随机数所在的区间
int channelIndex = binarySearch(preSum, rand);
return channels[channelIndex];
}

/**
* 二分查找
*/
private int binarySearch(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;

while (left <= right) {
int mid = left + ((right - left) >> 2);
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
// 当找不到确切匹配时返回大于随机数的最小前缀和的索引
return left;
}
}

测试一下:


    @Test
void allocate() {
HashMap<String, Integer> channels = new HashMap<>();
channels.put("Adyen", 50);
channels.put("Stripe", 30);
channels.put("Alipay", 20);

PaymentChannelAllocator allocator = new PaymentChannelAllocator(channels);

// 模拟100次交易分配
for (int i = 0; i < 100; i++) {
String allocatedChannel = allocator.allocate();
System.out.println("Transaction " + (i + 1) + " allocated to: " + allocatedChannel);
}
}

彩蛋


在这个渠道选择的场景里,还有两个小彩蛋。


二分查找翻车


我前面提到了一个二分查找求mid的写法:


int mid=left+((right-left)>>1);

这个写法机能防止内存溢出,用了位移运算也很高效,但是,这个简单的二分查找写出过问题,直接导致线上cpu飙升,差点给老三吓尿了。


吓惨了


int mid = (right - left) >> 2 + left;

就是这行代码,看出什么问题来了吗?


——它会导致循环结束不了!


为什么呢?因为>>运算的优先级是要低于+的,所以这个运算实际上等于:


int mid = (right - left) >> (2 + left);

在只有两个渠道的时候没有问题,三个的时候就寄了。


当然,最主要原因还是没有充分测试,所以大家知道我在上面为什么特意写了单测吧。


加权随机其它写法


这里用了前缀和+二分查找来实现加权随机,其实加权随机还有一些其它的实现方法,包括别名方法、树状数组、线段树 随机列表扩展 权重累积等等方法,大家感兴趣可以了解一下。


加权随机的实现


印象比较深刻的是,有场面试被问到了怎么实现加权随机,我回答了权重累积前缀和+二分查找,面试官还是不太满意,最后面试官给出了他的答案——随机列表扩展


什么是随机列表扩展呢?简单说,就是创建一个足够大的列表,根据权重,在相应的区间,放入对应的渠道,生成随机数的时候,就可以直接获取对应位置的渠道。


public class WeightedRandomList {
private final List<String> expandedList = new ArrayList<>();
private final Random random = new Random();

public WeightedRandomList(HashMap<String, Integer> weightMap) {
// 填充 expandedList,根据权重重复元素
for (String item : weightMap.keySet()) {
int weight = weightMap.get(item);
for (int i = 0; i < weight; i++) {
expandedList.add(item);
}
}
}

public String getRandomItem() {
// 生成随机索引并返回对应元素
int index = random.nextInt(expandedList.size());
return expandedList.get(index);
}

public static void main(String[] args) {
HashMap<String, Integer> items = new HashMap<>();
items.put("Alipay", 60);
items.put("Adyen", 20);
items.put("Stripe", 10);

WeightedRandomList wrl = new WeightedRandomList(items);

// 演示随机选择
for (int i = 0; i < 10; i++) {
System.out.println(wrl.getRandomItem());
}
}
}

这种实现方式就是典型的空间换时间,空间复杂度O(n),时间复杂度O(1)。优点是时间复杂度低,缺点是空间复杂度高,如果权重总和特别大的时候,就需要一个特别大的列表来存储元素。


当然这种写法还是很巧妙的,适合元素少、权重总和小的场景。


刷题随想


上面就是我在项目里用到过或者见到过的LeetCode算法应用,416:4,不足1%的使用率,还搞出过严重的线上问题。


……


在力扣社区里关于算法有什么的贴子里,有这样的回复:


“最好的结构是数组,最好的算法是遍历”。


“最好的算法思路是暴力。”


……


坦白说,如果不是为了面试,我是绝对不会去刷算法的,上百个小时,用在其他地方,绝对收益会高很多。


从实际应用去找刷LeetCode算法的意义,本身没有太大意义,算法题的最大意义就是面试。


刷了能过,不刷就挂,仅此而已。


这些年互联网行业红利消失,越来越多的算法题,只是内卷的产物而已。


当然,从另外一个角度来看,考察算法,对于普通的打工人,可能是个更公平的方式——学历、背景都很难卷出来,但是算法可以。


我去年面试的真实感受,“没机会”比“面试难”更令人绝望。


写到这,有点难受,刷几道题缓一下!






参考:


[1].leetcode.cn/circle/disc…


[2].36kr.com/p/121243626…


[3].leetcode.cn/circle/disc…


[4].leetcode.cn/circle/disc…







备注:涉及敏感信息,文中的代码都不是真实的投产代码,作者进行了一定的脱敏和演绎。





作者:三分恶
来源:juejin.cn/post/7321271017429712948
收起阅读 »

年终被砍、降薪、被拒,用我今年的经历给你几个忠告| 2023年终总结

2023年我的经历可以说是和大A一样,用今年的经历给大家几个忠告,希望我的经历让各位乐呵一下,学习到一些职场的小知识。 本人现任职某产业互联网独角兽公司交易部门后端开发,会点前端已经在这里躺了2年多。 春节前 第一次大跌从1月20号开始,也就是春节放假前一...
继续阅读 »

2023年我的经历可以说是和大A一样,用今年的经历给大家几个忠告,希望我的经历让各位乐呵一下,学习到一些职场的小知识。


本人现任职某产业互联网独角兽公司交易部门后端开发,会点前端已经在这里躺了2年多。



春节前



第一次大跌从1月20号开始,也就是春节放假前一天按照以往的经历是20号会发年终然而公司一波顶级操作一纸公告下来只有ABC绩效有年终而且与之前相比还打折,打开手机一看1000块过节费。后来才知道只给了部门几个可以拿年终的绩效名额,其他80%都是D。就这样拿着过节费过了一个年。


image.png



春节后



过完年回来后3月底要给我降薪,从组长那里得知原因是绩效评估是E,开完会后连忙去OA查询发现绩效评估为D,后来组长知道后开始和HR沟通。20号左右HR开始找我谈话开头先是道歉又说降薪不是以绩效为标准而是22年的几次线上事故影响过大原因。


一会是绩效组长沟通后又不是绩效,让我感觉是恶意降薪,就这样一直扯皮到快4月份。由于那段时间需要处理的事情太多不想和她扯皮所以选择同意降薪。


后来的小道消息得知系统录入的绩效和HR那里是两份,而系统里面高是因为有项目的加分,真不懂他们的绩效评估是怎么做的,那段时间真是可以说掉到了谷底身心俱疲。到今天想起来如果没有和别人说我系统中D绩效 HR没准也不会有其他理由降薪。


给打工人的第一个忠告:在公司里面谁也不要相信,定期收集考勤、加班证据,把证据握在自己手里,至于代码事故问题就写单元测试,留好评审会议记录,测试记录证据至少这样可以不被认定为主要责任。





9月、10月、11月裁员



之前一直听组长说23年业绩一直不好公司想要裁员到9月还是等到了,好像定了10个人将近部门人数的三分之一。因为公司砍掉了年终而且加班严重有几个小伙伴也有走的意愿,定了5个开发,还有几个转岗。10月又裁了几个开发,和被裁的小伙伴交流公司裁掉的全是年轻人30往上的一个没动,11月测试部门述职定不下名额直接两个测试全部裁掉


谈补偿HR又是神级操作先是套路员工灌输是自己想走,不是公司裁员不想给补偿金,后来又想按照实习期工资补偿,被部门几个人骂了后妥协了,年假还是不想给最后按照一倍补偿。到了发薪日又是一波操作最后几天的工资不给,听说要起诉公司又拖了一个月才发。都把人家裁了最后一天还在让别人加班太顶了。


给大家的第二个忠告:裁员的话不要慌也不要怕,一定要强硬,不要随便签字属于自己的赔偿一定要争取:赔偿金、代通知金、加班费、年假都算上,确定好最后的上班时间、社保、发薪时间。


给大家的第三个忠告:在公司不要和招惹或者和那些老员工、领导身边的红人翻脸,他们这些人就是能决定领导的想法,一边添油加醋一边对你笑嘻嘻





小插曲



8月底的一天HR突然找我说工时不够要扣工资,正常应该出勤23天184个小时,我其中一天请了假22天出勤了188个小时。按照之前公司要求加班的工时可以抵请假时长我用22天出勤了23天应出勤的时间是没问题的。HR的顶级算法是即使请假也要够应出勤工时然后多出来的才可以使用抵扣。真是这公司HR就是个大聪明数学不会算,最后还是没扣。


IMG_2392.jpg



年底



今年公司严格控制了部门支出,打车报销严查、加班也不管饭了。裁员后能干活的走了一半,现在的项目开发流程真是一言难尽,产品不设计原型、不写需求文档、不在OA提需求还说没有时间,需求没确定、没宣讲已经开始让开发这边开始了,开发按照做完初稿原型做完推倒重来。
三季度公司偷偷把社保调整到了80%,年底大言不惭的说在国家允许的情况下公积金调整到了5%,每次开会就是PUA让我们看看别的企业都在裁员应该把公司当成自己家一样。小道消息今年也没有年终。又沉闷的过了一年





出京



年终没了、也降薪了,放假后不想待在北京了端午直接去了杭州,由于接近亚运会的时间所以杭州氛围非常好,这个时候有点小梅雨,西湖边上拍的环境和氛围真的好。


IMG_1915.heic

周末和朋友们还去了承德,这个阳光和草原真绝了


trim.3A5310B6-27AB-4748-BA47-83A853A4C647.gif


11月去了南京,去南京是也为了自己的执念吧她还是没同意,这么久了也是时候放下了,第一次为了一个人跨越千里去了一个陌生的城市,鸡鸣寺的小猫都是两只。


IMG_2115.HEIC

年底和朋友几个去了威海,认识了一个辽宁的大哥开车带我们玩了一整天


2307e0a157b2162a6e595f689ed66b83.jpg

给大家的第四个忠告:工作不是你的全部,甚至不是你的生活,你要按照自己想过的方式去活着,有些事和东西得到了当然很好,你要知道得不到也不是你的问题尽人事听天命,降低期待。



2024计划



今年的计划是



  1. 继续走走到处去看看,西安、成都、武汉具体的到时候在看吧

  2. 在网上输出一些技术文章,之前的开发经历一直都没有沉淀

  3. 如果有机会可以继续搞搞副业,去年给朋友公司开发了一个APP,还有帮朋友做了一些需求

  4. 读书、读书、读书,继续学习,先试试中级软考吧,人还是不能停下来,一停下来就容易拖延

  5. 周末运动拒绝躺平,618全款拿下的公路车锻炼起来,身体才是革命的本钱

  6. 看机会,今年春节前有可能还会有一轮裁员,闲下来的时候看看机会。毕竟我们组的高级开发已经快3年没涨薪了,公司还不让人家走。


作者:旧梦呀
来源:juejin.cn/post/7320435287296032820
收起阅读 »

Android跳转系统界面_总结

1、跳转Setting应用列表(所有应用) Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_APPLICATIONS_SETTINGS); this.startActivity(intent); ...
继续阅读 »

1、跳转Setting应用列表(所有应用)


Intent intent =  new Intent(Settings.ACTION_MANAGE_ALL_APPLICATIONS_SETTINGS);
this.startActivity(intent);


2、跳转Setting应用列表(安装应用)


Intent intent =  new Intent(Settings.ACTION_MANAGE_APPLICATIONS_SETTINGS);


3、跳转Setting应用列表


Intent intent =  new Intent(Settings.ACTION_APPLICATION_SETTINGS);


4、开发者选项


Intent intent =  new Intent(Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS);


5、允许在其它应用上层显示的应用


Intent intent =  new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);


6、无障碍设置


Intent intent =  new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);


7、添加账户


Intent intent =  new Intent(Settings.ACTION_ADD_ACCOUNT);


8、WIFI设置


Intent intent =  new Intent(Settings.ACTION_WIFI_SETTINGS);


9、蓝牙设置


Intent intent =  new Intent(Settings.ACTION_BLUETOOTH_SETTINGS);


10、移动网络设置


Intent intent =  new Intent(Settings.ACTION_DATA_ROAMING_SETTINGS);


11、日期时间设置


Intent intent =  new Intent(Settings.ACTION_DATE_SETTINGS);


12、关于手机界面


Intent intent =  new Intent(Settings.ACTION_DEVICE_INFO_SETTINGS);


13、显示设置界面


Intent intent =  new Intent(Settings.ACTION_DISPLAY_SETTINGS);


14、声音设置


Intent intent =  new Intent(Settings.ACTION_SOUND_SETTINGS);


15、互动屏保


Intent intent =  new Intent(Settings.ACTION_DREAM_SETTINGS);


16、输入法


Intent intent =  new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS);


17、输入法_SubType


Intent intent =  new Intent(Settings.ACTION_INPUT_METHOD_SUBTYPE_SETTINGS);


18、内部存储设置界面


Intent intent =  new Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS);


19、存储卡设置界面


Intent intent =  new Intent(Settings.ACTION_MEMORY_CARD_SETTINGS);


20、语言选择界面


Intent intent =  new Intent(Settings.ACTION_LOCALE_SETTINGS);


21、位置服务界面


Intent intent =  new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);


22、运营商


Intent intent =  new Intent(Settings.ACTION_NETWORK_OPERATOR_SETTINGS);


23、NFC共享界面


Intent intent =  new Intent(Settings.ACTION_NFCSHARING_SETTINGS);


24、NFC设置


Intent intent =  new Intent(Settings.ACTION_NFC_SETTINGS);


25、备份和重置


<Intent intent =  new Intent(Settings.ACTION_PRIVACY_SETTINGS);


26、快速启动


Intent intent =  new Intent(Settings.ACTION_QUICK_LAUNCH_SETTINGS);


27、搜索设置


Intent intent =  new Intent(Settings.ACTION_SEARCH_SETTINGS);


28、安全设置


Intent intent =  new Intent(Settings.ACTION_SECURITY_SETTINGS);


29、设置的主页


Intent intent =  new Intent(Settings.ACTION_SETTINGS);


30、用户同步界面


Intent intent =  new Intent(Settings.ACTION_SYNC_SETTINGS);


31、用户字典


Intent intent =  new Intent(Settings.ACTION_USER_DICTIONARY_SETTINGS);


32、IP设置


Intent intent =  new Intent(Settings.ACTION_WIFI_IP_SETTINGS);


33、App设置详情界面


public void startAppSettingDetail() {
String packageName = getPackageName();
Uri packageURI = Uri.parse("package:" + packageName);
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(packageURI);
startActivity(intent);
}


34、跳转应用市场


public void startMarket() {
Intent intent = new Intent(Intent.ACTION_VIEW);
// intent.setData(Uri.parse("market://details?id=" + "com.xxx.xxx"));
intent.setData(Uri.parse("market://search?q=App Name"));
startActivity(intent);
}


35、获取Launcherbaoming


public void getLauncherPackageName() {
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_HOME);
final ResolveInfo res = this.getPackageManager().resolveActivity(intent, 0);
if (res.activityInfo == null) {
Log.e("TAG", "没有获取到");
return;
}

if (res.activityInfo.packageName.equals("android")) {
Log.e("TAG", "有多个Launcher,且未指定默认");
} else {
Log.e("TAG", res.activityInfo.packageName);
}
}


36、跳转图库获取图片


public void startGallery() {
Intent intent = new Intent(Intent.ACTION_PICK,
android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
intent.setType("image/*");
this.startActivityForResult(intent, 1);
}


37、跳转相机,拍照并保存


public void startCamera() {
String dir = Environment.getExternalStorageDirectory().getAbsolutePath() + "/test.jpg";
Uri headCacheUri = Uri.fromFile(new File(dir));
Intent takePicIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
takePicIntent.putExtra(MediaStore.EXTRA_OUTPUT, headCacheUri);
startActivityForResult(takePicIntent, 2);
}


38、跳转文件管理器


public void startFileManager() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT,
android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
intent.setType("file/*");
this.startActivityForResult(intent, 3);
}


39、直接拨打电话


 public void startCall() {
Intent callIntent = new Intent(Intent.ACTION_CALL);
callIntent.setData(Uri.parse("tel:" + "13843894038"));
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
return;
}
startActivity(callIntent);
}


40、跳转电话应用


public void startPhone() {
Intent intent = new Intent(Intent.ACTION_DIAL,Uri.parse("tel:" + "13843894038"));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}


41、发送短信


public void startSMS() {
Uri smsToUri = Uri.parse("smsto://10086");
Intent mIntent = new Intent( android.content.Intent.ACTION_SENDTO, smsToUri );
startActivity(mIntent);
}


42、发送彩信


public void startMMS() {
Uri uri = Uri.parse("content://media/external/images/media/11");
Intent it = new Intent(Intent.ACTION_SEND);
it.putExtra("sms_body", "some text");
it.putExtra(Intent.EXTRA_STREAM, uri);
it.setType("image/png");
startActivity(it);
}


43、发送邮件


public void startEmail() {
Uri uri = Uri.parse("mailto:6666666@qq.com");
String[] email = {"12345678@qq.com"};
Intent intent = new Intent(Intent.ACTION_SENDTO, uri);
intent.putExtra(Intent.EXTRA_CC, email); // 抄送人
intent.putExtra(Intent.EXTRA_SUBJECT, "这是邮件的主题部分"); // 主题
intent.putExtra(Intent.EXTRA_TEXT, "这是邮件的正文部分"); // 正文
startActivity(Intent.createChooser(intent, "请选择邮件类应用"));
}


44、跳转联系人


public void startContact() {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setData(Contacts.People.CONTENT_URI);
startActivity(intent);

/*Intent intent = new Intent();
intent.setAction(Intent.ACTION_PICK);
intent.setData(Uri.parse("content://contacts/people"));
startActivityForResult(intent, 5);*/

}


45、插入联系人


public void insertContact() {
Intent intent = new Intent(Intent.ACTION_INSERT);
intent.setData(ContactsContract.Contacts.CONTENT_URI);
intent.putExtra(ContactsContract.Intents.Insert.PHONE, "18688888888");
startActivityForResult(intent, 1);
}


46、插入日历事件


public void startCalender() {
Intent intent = new Intent(Intent.ACTION_INSERT);
intent.setData(CalendarContract.Events.CONTENT_URI);
intent.putExtra(CalendarContract.Events.TITLE, "开会");
startActivityForResult(intent, 1);
}


47、跳转浏览器


public void startBrowser() {
Uri uri = Uri.parse("http://www.baidu.com");
Intent intent = new Intent(Intent.ACTION_VIEW,uri);
startActivity(intent);
}


48、安装应用


public void startInstall() {
String filePath="/xx/xx/abc.apk";
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse("file://" + filePath),
"application/vnd.android.package-archive");
startActivity(intent);
}>



49、卸载应用


public void startUnInstall() {
String packageName="cn.memedai.mas.debug";
Uri packageUri = Uri.parse("package:"+packageName);//包名,指定该应用
Intent uninstallIntent = new Intent(Intent.ACTION_DELETE, packageUri);
startActivity(uninstallIntent);
}


50、回到桌面


public void startLauncherHome() {
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_HOME);
startActivity(intent);
}


51、打开任意文件(根据其MIME TYPE自动选择打开的应用)


  private void openFile(File f) {
Intent intent = new Intent();
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction(android.content.Intent.ACTION_VIEW);
String type = getMIMEType(f);
intent.setDataAndType(Uri.fromFile(f), type);
startActivity(intent);
}

private String getMIMEType(File f) {
String end = f.getName().substring(f.getName().lastIndexOf(".") + 1,
f.getName().length()).toLowerCase();
String type = "";
if (end.equalsIgnoreCase("mp3")
|| end.equalsIgnoreCase("aac")
|| end.equalsIgnoreCase("amr")
|| end.equalsIgnoreCase("mpeg")
|| end.equalsIgnoreCase("mp4")) {
type = "audio";
} else if(end.equalsIgnoreCase("mp4")
|| end.equalsIgnoreCase("3gp")
|| end.equalsIgnoreCase("mpeg4")
|| end.equalsIgnoreCase("3gpp")
|| end.equalsIgnoreCase("3gpp2")
|| end.equalsIgnoreCase("flv")
|| end.equalsIgnoreCase("avi")) {
type = "video";
} else if (end.equalsIgnoreCase("jpg")
|| end.equalsIgnoreCase("gif")
|| end.equalsIgnoreCase("bmp")
|| end.equalsIgnoreCase("png")
|| end.equalsIgnoreCase("jpeg")) {
type = "image";
} else {
type = "*";
}
type += "/*";
return type;
}


52、跳转录音


public void startRecord() {
Intent intent = new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION);
startActivity(intent);
}



👀关注公众号:Android老皮!!!欢迎大家来找我探讨交流👀



作者:派大星不吃蟹
来源:juejin.cn/post/7321551188092403764
收起阅读 »

检测自己网站是否被嵌套在iframe下并从中跳出

web
iframe被用于将一个网页嵌套在另一个网页中,有的时候这会带来一些安全问题,这时我们就需要一些防嵌套操作了。 本文分为俩部分,一部分讲解如何检测或者禁止嵌套操作,另一部分讲解如何从嵌套中跳出。 末尾放了正在使用的完整代码,想直接用的可以拉到最后。 效果 当存...
继续阅读 »

iframe被用于将一个网页嵌套在另一个网页中,有的时候这会带来一些安全问题,这时我们就需要一些防嵌套操作了。

本文分为俩部分,一部分讲解如何检测或者禁止嵌套操作,另一部分讲解如何从嵌套中跳出。


末尾放了正在使用的完整代码,想直接用的可以拉到最后。


效果


当存在嵌套时会出现一个蒙版和窗口,提示用户点击。

点击后会在新窗口打开网站页面。


嵌套展示


嵌套检测


设置响应头


响应头中有一个名为X-Frame-Options的键,可以针对嵌套操作做限制。

它有3个可选值:


DENY:拒绝所有


SAMEORIGIN:只允许同源


ALLOW-FROM origin:指定可用的嵌套域名,新浏览器已弃用


后端检测(以PHP为例)


通过获取$_SERVER中的HTTP_REFERERHTTP_SEC_FETCH_DEST值,可以判断是否正在被iframe嵌套


// 如果不是iframe,就为空的字符串
$REFERER_URL = $_SERVER['HTTP_REFERER'];

// 资源类型,如果是iframe引用的,会是iframe
$SEC_FETCH_DEST = $_SERVER['HTTP_SEC_FETCH_DEST'];

// 默认没有被嵌套
$isInIframe = false;

if (isset($_SERVER['HTTP_REFERER'])) {
$refererUrl = parse_url($_SERVER['HTTP_REFERER']);
$refererHost = isset($refererUrl['host']) ? $refererUrl['host'] : '';

if (!empty($refererHost) && $refererHost !== $_SERVER['HTTP_HOST']) {
$isInIframe = true;
}
}

// 这里通过判断$isInIframe是否为真,来处理嵌套和未嵌套执行的动作。
if($isInIframe){
....
}

前端检测(使用JavaScript)


通过比较window.self(当前窗口对象)和window.top(顶层窗口对象)可以判断是否正在被iframe嵌套


if (window.self !== window.top) {
// 检测到嵌套时该干的事
}

从嵌套中跳出


跳出只能是前端处理,如果使用了PHP等后端检测,可以直接返回前端JavaScript代码,或者HTML的A标签设置转跳。


JavaScript直接转跳(不推荐)


不推荐是因为现在大多浏览器为了防止滥用,会阻止自动弹出新窗口。


window.open(window.location.href, '_blank');

A标签点击转跳(较为推荐)


当发生了用户交互事件,浏览器就不会阻止转跳了,所以这是个不错的方法。


href="https://www.9kr.cc" target="_blank">点击进入博客

JavaScript+A标签(最佳方法)


原理是先使用JavaScript检测是否存在嵌套,

如果存在嵌套,再使用JavaScript加载蒙版和A标签,引导用户点击。


这个方法直接查看最后一节。


正在使用的方法


也就是上一节说的JavaScript+A标签。


先给待会要显示的蒙版和A标签窗口设置样式


/* 蒙版样式 */
.overlay1 {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5); /* 半透明背景颜色 */
z-index: 9999; /* 确保蒙版位于其他元素之上 */
display: flex;
align-items: center;
justify-content: center;
}

/* 窗口样式 */
.modal1 {
background-color: #fff;
padding: 20px;
border-radius: 5px;
}

然后是检测和加载蒙版+A标签的JavaScript代码


if (window.self !== window.top) {
// 创建蒙版元素
var overlay = document.createElement('div');
overlay.className = 'overlay1';

// 创建窗口元素
var modal = document.createElement('div');
modal.className = 'modal1';

// 创建A标签元素
var link = document.createElement('a');
link.href = 'https://www.9kr.cc';
link.target = '_blank'; // 在新窗口中打开链接
link.innerText = '点击进入博客';
//link.addEventListener('click', function(event) {
// event.preventDefault(); // 阻止默认链接行为
// alert('Test');
//});

// 将A标签添加到窗口元素中
modal.appendChild(link);

// 将窗口元素添加到蒙版元素中
overlay.appendChild(modal);

// 将蒙版元素添加到body中
document.body.appendChild(overlay);
}

博客的话,只需要在主题上设置自定义CSS自定义JavaScript即可


博客后台设置




作者:Edit
来源:juejin.cn/post/7272742720841252901
收起阅读 »

2024年突如其来的危机感和反思总结

前言 说来也讽刺,我刚刚在23年12月写了一篇走出迷茫,还给自己定了个目标,新的一年刚开始就遇到危机了。 因为我负责一个项目迁移了两次都失败了,领导说虽然去年没扣你绩效,但你连续失败可能会被领导扣绩效。原因是上面只看结果,过程他们看不到。如果严重的话,你可能会...
继续阅读 »

前言


说来也讽刺,我刚刚在23年12月写了一篇走出迷茫,还给自己定了个目标,新的一年刚开始就遇到危机了。


因为我负责一个项目迁移了两次都失败了,领导说虽然去年没扣你绩效,但你连续失败可能会被领导扣绩效。原因是上面只看结果,过程他们看不到。如果严重的话,你可能会被列入 优化名单。


刚刚7天内通宵两个晚上的我,听到这个消息后脑子真的嗡嗡的。因为我本能的认为失败的原因不在我,网络问题结合使用的nextJS插件,导致我们无法线下测试,所以有问题只会在生产上暴露。


就这样通宵后的我三天没有睡好,有一天晚上我梦见我在和领导解释为什么会出现这些问题,但是他们不听。我不是失落,而是害怕。有房贷和孩子没有坚强的家庭支援的人,大概会懂我这几天的无助。


起因


事情是这样的,我负责的一个项目要从公有云迁移到私有云,而私有云中有部分中间要求使用国产化,这些问题都已经解决了之前也简单记录了一下。


但是这个项目最复杂的是网络,有十几二十个防火墙要申请,还有一些白名单要配置,而且我们没有域名,使用的是别人的域名http://www.aaa.cn/path进行转发到我们web代理服务器上。就因为这个/path的原因,我们的前端后端都在代码中做了修改。关键他还不止一个域名,还有一个aaa.cn/path这个地址也可以访问。但该死的http://www.aaa.cn/path一开始只有外网可访问内网访问不了,aaa.cn/path一开始内网都可以访问。然而,今天测试的时候发现这两个https地址都可以访问了,但是我事先没有收到任何通知。这里说一下为什么要https,因为微信必须要求https域名,当然还有一些其他场景。


第一次割接


因为迁移后的环境没有割接前没有域名,更没有https的域名供我们使用测试。我们申请了公网负载IP进行测试一切顺利,于是我开始第一次割接。然后失败了,因为迁移前的obs是自带公网可访问域名的(我们的资源公网客户端可以直接访问),但是迁移后我们的obs是私有云,他们虽然提供了域名但只能内网访问,于是我们使用了nginx做了反向代理。反向代理后使用公网负载IP到访问这个私有云的obs资源是没有问题的,但上了服务器使用域名访问这些资源,就不能访问。因为当晚除了这个域名问题,还有一个程序问题,所以我在凌晨5点放弃了割接,发邮件说明失败原因。


第二次割接


第一次的使用域名无法方私有云obs问题,我领导去修改了nginx代理配置,增加了header头,将host改成了可以正常访问的公网负载IP,然后使用浏览器测试直接打开了私有云obs的图片。另外程序问题是开发忘记刷脚本了,我没有骂他,因为我觉得我骂了影响后面的工作。外包是一个团队,因为他工作的原因导致其他人无效通宵,其他人会给他压力的。当然,提还是要提的。


解决上述两个问题后,我准备了第二次割接,然后还是失败了。原因nextJS打包是需要访问后端服务器,同时nextJS中有个图片模糊加载的插件访问图片的域名和打包需要访问后端服务器域名是同一个,共用同一个参数配置。而不巧的是我们部署打包的服务器无法访问http://www.aaa.cn这个域名,而aaa.cn虽然可以访问,但是他的证书不安全nextJS的模糊加载插件直接提示安全问题,不予与加载展示。


我们蹭着线上域名割接后,做了几轮测试得出一下结论。


方案一:http://www.aaa.cn需要打包服务器能访问,运维说配置hosts就可以,但这个要提工单,无法直接协调;


方案二:aaa.cn配置上SSL安全证书,使其https合法;


方案三:如果方案一和方案二尝试后都不行,在http://www.aaa.cn打包服务器可访问的情况下或aaa.cn配置安全证书的情况,去掉nextJS的模糊图片加载问题;


再次放弃割接计划,发邮件说明原因。




然后6点睡,10点起,和领导沟通问题的时候,领导说了上面的话。我给领导回复是,主要还是网络太复杂了,但是我会尽全力的,结果怎么样我也没得选,听天命吧。


过程


领导和我聊完后,我的心情是不能平复的。


我想的最多的是,如果我失业了,我那每月1.3w的房贷怎么办?


每月的家庭支出怎么办?


我老婆一个人能不能扛得住?


现在这个环境我能快速找到工作吗?


就算找到了,我能找到心仪的工作吗?


找到新工作后,我能不能待多久?


我现在是不是该去复习一些技术了?


我应该先学哪些东西呢?


我是不是应该找个副业?


搞短视频?写小说?滴滴?外卖?


自己做几个益智的微信小程序游戏,然后靠广告赚点饭钱?


回老家问问我爷爷或者我父辈的那些山和地是否能给我种果树或者粮食?


...


第二天是个周六,我开始冷静了一点。我开始拿起手机看着一串延期的计划表发呆,我完全提不起一点兴趣,也许自己不行去做的一种借口吧。但结果是我真的没有去做,因为我不想做。


看着计划,我越看越不对劲。


第三天是个周日,快到晚上的时候,我老婆问我吃完饭不。我说不吃了,刚好适应一下失业后饿肚子的感觉,以后说不准要经常饿肚子。


第四天早上,起来把掘金、华住、学习强国签到完,学了一节多领国,然后就去完成运动计划1000跳绳+10组其他健身运动。运动完后去洗澡,然后就萌生了鼓励自己的念头。


“想想这两次失败是否完全不可测试的?”


“还有哪些我能做的?”


“领导只是说我有危险,那何不在努力试试留下来,毕竟你自己希望能在这里呆满3年+的!”


“第二次割接的问题是不是可以通过自己购买域名模拟?”


“做自己该做的,船到桥头自然直,况且你一直觉得自己能力还可以,至少是中等水品?”


反思


反思第一次失败


1、虽然自己整理了checklist清单,让项目确认了他们也确认了,但自己并没有让他们把每个环节需要执行细节落入书面;


2、自己在整个上线过程中,确实没有针对具体问题做深度的剖析,只是站在方向的引导上,过度依赖团队中的开发;


3、网络知识和nginx虽然一直在用,但自己不熟悉却没有放到学习项中,自己一直在学习其他玩意,重要紧急没有分清楚;


4、出现问题,具体的问题没有自己剖析过,觉得是网络问题自己肯定不会;


反思第二次失败


1、和第一次一样,没有亲自分析问题日志和原因,基本都是团队反馈,然后自己总结的归纳;


2、没有深思熟虑,既然上次有域名访问图片的问题,但却没有考虑https的问题和nextJS打包需要访问后端的问题;


反思个人计划


1、强化工作的部分有,但太少需要针对性增加学习工作中遇到的薄弱的技术问题;


2、整个计划中,基本除了健康就是学习,没有增加实施后可以增加收入或者增加收入机会的内容,即使列了也没有执行到位;


3、计划中应该有侧重,计划中内容太多时间太分散,应该每个阶段增加一个侧重;


调整


关于本次迁移的工作的总结:


1、上线前整理checklist,并且核对每个人负责的内容,包括细节操作和操作所需材料,并收集材料;


2、以前是团队负责人,现在是技术经理,需要下沉,表现在现场分析解决问题和增加技术知识面;


3、增对工作汇总遇到的薄弱技术知识点,针对性的寻找资料学习;


4、遇到问题,冲在一线,现在是技术经理需要关系技术细节,并且需要从细节上帮助团队解决问题;


5、没有解决不了的问题,没有复现不了的环境,无法是成本问题,不要一分不掏,因为没了工作损失的不止这点钱;


关于自身工作状态的总结:


1、这家公司自从自己将责任划分清楚后,开始有点安逸,但所有需求自己要过一遍,每个技术方案自己要把持;


2、还是要以工作为主,有一半的学习要和当下的工作相关;


3、不要过分信任团队,特别是外包团队,要将核心掌握在自己手里;


4、防御性上班,关键核心的要素信息要记笔记,但点到为止自己明白就行,不然对你下黑手时,你无力反抗和无法维护自己的权益;


结合上述总结调整2024年执行计划:


原计划


一、工作:
1)2024年保住当前工作,做好项目技术管理,保持向上汇报,平级保持责任分明适当帮忙,识别风险提前向干系人预警;

二、学习:
1)每天保持至少3天的coding或技术学习,将自己花了万元的VIP培训视频一点点消化,每天就算看10分钟也行;
2)每周一篇技术博客,将解决技术问题和技术学习的内容,分享到微信公众号或掘金等博客上;
3)学习英语,多领国每天只是少一节,时间多可以多练习几个,拓宽后续就业面,避免被需要英语的外企或国际公司限制;
4)通过五月份的软考高项,去年上半年没有过,下半年放弃了,每天背知识点、练习和看教学视频;

三、健康:
1)每天保持运动,常规每天1000个跳绳+10组其他运动,如俯卧撑,最次每天200个跳绳,争取将结石排除提完;
2)控制饮食,多吃粗纤维果蔬少油少盐,争取大多时候半碗饭和两素一荤,至少每周一个晚上不吃晚饭,晚上19点后不食;
3)体重减到170以下,除了坚持以上两项,多出去走走;
4)排出肾结石,中度脂肪肝转轻度或无,降血液中的胆固醇,治好咽喉炎和鼻窦炎,以上四样至少完成两项;
5)平均睡眠提升到6小时+;
6)作为兴趣学学中医,看看倪海厦的中医视频,聊胜于无;
四、创作:
1)持续创作短视频或者小说,小说24年争取实现100w字,短视频每周一篇,不做硬性要求业余时间够就走;

以上所有目标,均坚持非强制原则,如果昨天没有完成,把今天的完成即可,有时间再补昨天的。

分解原计划


一、工作:
12024年保住当前工作,做好项目技术管理,保持向上汇报,平级保持责任分明适当帮忙,识别风险提前向干系人预警;
1.运行并阅读分析当前项目代码、分析数据库设计和分析中间件的使用,发现问题提出改进计划 -- 提高领导力的影响,专家权利;
2.对所有新增需求进行阅读,参与并制定需求所使用的技术方案 -- 掌握项目技术栈和架构变化,增加项目经验和能力;
3.对nginx、http协议、kafka、mysql等进行系统的学习,并将学习的内容用自己的语言总结描文章供后续自己翻阅;
4.不定期向领导汇报工作进展,包括工作中的问题、好消息等,特别是风险要提前预警;

二、学习:
1)每周保持至少3天的coding或技术学习,将自己花了万元的VIP培训视频一点点消化,每天就算看10分钟也行;
2)每周一篇技术博客,将解决技术问题和技术学习的内容,分享到微信公众号或掘金等博客上;
3)多领国学习英语每天只是少一节,尽可能多读多听重点练习听读,拓宽后续就业面,避免被需要英语的外企或国际公司限制;
4)按照提供的学习方看回放、复习讲义、做练习、对照题找书本原话,争取通过五月份的软考高项;

三、健康:
1)每天保持运动,每天200个跳绳,最佳常规每周三次 1000个跳绳+10组其他运动,争取体重减到170以下;
2)控制饮食,多吃粗纤维果蔬少油少盐,至少每周一个晚上不吃晚饭,晚上19点后不食;
3)每天200跳争取排出肾结石;少吃油腻增加运动争取中度脂肪肝转轻度或无和降血液中的胆固醇;少吃辛辣争取治好咽喉炎和鼻窦炎;
4)平均睡眠提升到6小时+;
5)每天拍胆经肝经心经;

四、创作:
1)每周至少发布一个短视频,主要发布自学中医相关内容或者郑强、罗翔、温铁军、艾跃进等爱国思想的演讲相关的内容,主打传播正能量和价值;
2)每天500字小说,争取24年完成30w字的小说;

五、拓展:
1)每周至少看书2小时;
2)每周学习中医至少1小时;
以上所有目标,均坚持非强制原则,如果昨天没有完成,把今天的完成即可,有时间再补昨天的。

新计划


因为之前的计划使用iphone自带的提醒事项做的,但是这东西在统计上手机和电脑不同步,而且手机电脑一起用还会重复计数。因此准备自己搞个计划清单列表小程序,至于app后续再研究,使用微信消息推送。


一、工作:
1)运行并阅读分析当前项目代码、分析数据库设计和分析中间件的使用,发现问题提出改进计划;
-- 本月每天2小时,将程序打包编译先搞定,独立完成UAT环境的部署和安装(侧重);
2)对所有新增需求进行阅读,参与并制定需求所使用的技术方案;
-- 有就阅读,并分析需求中是否需要使用新的技术方案;
3)对nginx、http协议、kafka、mysql等进行系统的学习,并将学习的内容用自己的语言总结描文章供后续自己翻阅;
-- 每2周学习nginx一个功能点,整理成技术文章;
4)不定期向领导汇报工作进展,包括工作中的问题、好消息等,特别是风险要提前预警;
-- 一句项目情况汇报;

二、学习:
1)每周保持至少3天的coding或技术学习,将自己花了万元的VIP培训视频一点点消化,每天就算看10分钟也行;
2)每周一篇技术博客,将解决技术问题和技术学习的内容,分享到微信公众号或掘金等博客上;
3)多领国学习英语每天只是少一节,尽可能多读多听重点练习听读,拓宽后续就业面,避免被需要英语的外企或国际公司限制;
4)按照提供的学习方看回放、复习讲义、做练习、对照题找书本原话,争取通过五月份的软考高项(侧重);

三、健康:
1)每天保持运动,每天200个跳绳,最佳常规每周三次 1000个跳绳+10组其他运动,争取体重减到170以下(侧重);
2)控制饮食,多吃粗纤维果蔬少油少盐,至少每周一个晚上不吃晚饭,晚上19点后不食;
3)每天200跳争取排出肾结石;少吃油腻增加运动争取中度脂肪肝转轻度或无和降血液中的胆固醇;少吃辛辣争取治好咽喉炎和鼻窦炎;
4)平均睡眠提升到6小时+;
5)每天拍胆经肝经心经;

四、创作:
1)每周至少发布一个短视频,主要发布自学中医相关内容或者郑强、罗翔、温铁军、艾跃进等爱国思想的演讲相关的内容,主打传播正能量和价值;
2)每天500字小说,争取24年完成30w字的小说;
33月底前,开发一个小程序用于记录计划清单,并使用微信提醒,后续看情况加上短信提醒(侧重);
4)模仿一个微信小游戏,

五、拓展:
1)每周至少看书2小时;
2)每周学习中医至少1小时;
以上所有目标,均坚持非强制原则,如果昨天没有完成,把今天的完成即可,有时间再补昨天的。

总结回顾


这些年我做了很多选择,但是我并没有因为我的选择变得更好。早先时候我一路走上坡的时候,我确实觉得是因为自己能力变强了我才有这样的成就,我也很自信我确实有这样的能力。但最近这4年一路下坡,让我重新认识了自己。早期我的能力可能确实在中上游,加上环境好很容易上去,而最终无论什么原因自己下来了说明自己总归有些问题的。


什么问题?自己认为比较严重的问题有如下:


1、过早且长期脱离一线,虽然有心想要重回一线,但是内心是抗拒那种艰苦的日子,虽然我不会把所有功绩揽给自己,但确实沾沾自喜;这就导致很多技术上的问题,我虽然了解但浮于表面,带着团队能解决,自己不一定能解决,最多只有思路。


2、没有认清打工人的本质,我曾在几家高端职位的公司任职,因为觉得高层领导或者直属领导太煞笔、不听劝、独断专行,而愤然离职;说到底还是太年轻,打工人就和上钟的技师一样,你要让领导爽,然后才能谈条件;他的煞笔不应该由你自己来买单,当然也和个人性格有关,城府和隐忍在职场上相当重要。


3、方向问题,我虽然做了11年,我之前的求职一直是以工资和职位头衔为目标,我基本没有规划过我的职业领域方向;等到要进入高端职位的圈子时,发现自己竟然什么都会一些,但别人要的是某个领域至少5年以上的工作经验,而我其中一个领域最多只有3.5年。


4、重心和当前迫切的问题自己没有刻意的把我,就比如很多计划看着挺好,但做起来也挺好,但是没有沉淀或者和当前的工作没有关系,就这样失去了很多巩固和提升能力的机会。


5、心里一直想要给自己留条后路,却发现前路没有走好,后路也没有留上,终日惶惶不安日。


有时候我在想,每一次的成功是不是老天给我的机会或者上辈子积德所致,每一次的失败或者落魄是不是老天觉得我朽木不可雕也。


但实际上自己也知道问题在哪?


不想做一线工作 -- 懒;


没有城府和隐忍 -- 蠢;


没有规划和防线 -- 笨;


没有重心和侧重 -- 懒;


前路没好后路成 -- 贪;


虽然明知道自己有这么多缺点,但是我还是想扛着氧气罐自救一下,说不定哪天让我踩上了风口飞起来了呢?放下氧气罐,也许我再也起不来了,但扛着虽然累,好歹我还活着。


-- 来自于35岁的自白!


作者:暗黑腐竹
来源:juejin.cn/post/7321531849850945570
收起阅读 »