注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

从9G到0.3G,腾讯会议对他们的git库做了什么?

web
导读 过去三年在线会议需求井喷,腾讯会议用户量骤增到3亿。快速迭代的背后,腾讯会议团队发现:业务保留了长达5年的历史数据,大量未进行 lfs 转换,新 clone 仓库本地空间占17.7G+。本地磁盘面临严重告急,强烈影响团队 clone 效率。当务之急是将仓...
继续阅读 »

导读


过去三年在线会议需求井喷,腾讯会议用户量骤增到3亿。快速迭代的背后,腾讯会议团队发现:业务保留了长达5年的历史数据,大量未进行 lfs 转换,新 clone 仓库本地空间占17.7G+。本地磁盘面临严重告急,强烈影响团队 clone 效率。当务之急是将仓库进行瘦身。本栏目特邀腾讯会议的智子研发团队成员李双君,回顾腾讯会议客户端的瘦身历程和经验,欢迎阅读。


目录


1 瘦身成效


2 瘦身前事项


3 瘦身整体方案


4 瘦身具体命令执行


5 新代码库验证


6 解决其它设备本地老分支 push 问题


7 其他平台适配


8 最后的验证


9 瘦身完毕后的知会


10 兜底回滚方案


11 踩坑记录及应对


12 写在最后


*作者所在的腾讯会议智子研发团队是腾讯会议的终端团队,负责腾讯会议 Win、Mac、Linux、Android、iOS、小程序、Web 等全栈开发,致力于打造一流的端产品体验。


01、瘦身成效


腾讯会议瘦身完毕后整体收益:




  • Git 仓库大小,9G 到350M。




  • 新 clone 仓库占用空间,从17.7G 到12.2G。




  • 平常拉代码速度(北京地区测试):macbook m1 pro:提升45%;devcloud win:提升56%。




  • 包构建流水线全量拉代码耗时,从16分钟减少到5分钟以内。





02、瘦身前事项


2.1 环境准备


使用有线网,看看能否通过其他办法给机器的上传和下载速度提速?不建议在家中开代理来瘦身,因为家里网速一般都没有公司快;如果在家操作,提前配置好远程桌面,远程公司电脑来瘦身。


使用性能较好的机器,硬盘空间至少要有 xxxG 剩余 (可以提前演练,看看究竟要多大磁盘空间?会议最起码得要求有600G 空余)。会议本次瘦身使用的设备是 MAC Book M1 Pro(16寸)笔记本电脑。


2.2 周知


工作开发群或者邮件等通知瘦身时间和注意事项:


瘦身时间: 选一个大家基本上都不会提交代码的时间,比如十一国庆或者春节;会议选的是春节期间。


注意事项: (开发重点关注)


瘦身期间会锁库,必须提前推送代码到远端,否则需要手动同步; 锁库期间无法进行 MR,且已创建 MR 会失效; 因删除历史记录,会导致本地仓库与远端冲突,请恢复后重新 clone 代码; 需要查询或处理更老的代码,需要去备份仓库查看。

2.3 代码库锁定


禁止代码库写操作,一般公司的代码管理平台可以提供这个功能,Git 项目的 owner 有权限。


2.4 第三方 Git 平台禁用


如果 Git 项目被第三方 Git 平台使用了,要保证瘦身前仓库的同步任务禁用。


比如,会议使用了 Ugit(UGit 是腾讯内部的一款自研 Git 客户端,主要是为腾讯内部研发环境特点而定制),就要如下禁用项目同步:



03、瘦身整体方案


原仓库继续保留作为备份仓库,另外新建仓库,新仓库沿用原仓库的项目名称、版本库路径和 id,并同步原项目数据。


之所以这么做,是为了保证其他平台无缝对接新的 Git 仓库,不用再更换 Git 地址,另外有些通过 api 调用的系统和工具也不受到影响。


瘦身内容:




  • 历史记录删除,只保留最近半年的历史记录。




  • 将历史大文件以及未加入 lfs 的大文件进行 lfs 处理。




04、瘦身具体命令执行


4.1 clone 项目,拉取所有文件版本到本地


git clone example.com/test.git


为了后面的比对验证,可以拷贝一份 test 文件夹放到和 test 同级目录下面的新建的 copyForCompare 文件夹中。


ulimit -n 9999999 # 解决可能出现的报错too many open files的问题
ulimit -n # 查看改成9999999了没

遍历拉取所有分支的 lfs 最新文件,并追踪远端分支到本地


以下这段 shell 脚本可以直接拷贝到终端运行,也可以创建一个.sh 文件放到根目录执行


cur_index=1
j=1
git branch -r | grep -v '->' |
while read remote
do
echo ”deal $cur_index th branch“
cur_index=$[cur_index+1]
git branch --track "${remote#origin/}" "$remote"
echo "begin to lfs fetch branch $remote"
git lfs fetch origin $remote
if [ $? -eq 0 ]; then
echo "fetch branch $remote success"
else
echo "fetch branch $remote failed"
lfs_fetch_fail_array[$j]=$remote
j=$[j+1]
fi
done
if [ ${#lfs_fetch_fail_array[*]} -gt 0 ]; then
echo "git lfs fetch error branches are: ${lfs_fetch_fail_array[*]}"
else
echo "fetch all branches success. done."
fi

获取所有分支的文件和 lfs 文件版本


git fetch --all
git lfs fetch --all

4.2 使用 git filter-branch 截断历史记录


这次瘦身只保留最近半年的历史记录,2022.6.1之前的提交记录都删除,所以截断的 commit 节点按如下所述来找:


提前用 sourceTree(或者别的 Git 界面工具)找出来需要截断的那个 commit,以主干 master 为例,找到 master 分支上提交的并且只有一个父的提交节点(如果提交节点有多个父,那么所有父节点都要处理),该节点必须是所有分支的父节点,否则需要考虑其他分支特殊处理的情况,该情况后面的【特殊分支处理】会有说明。



可以看到选中的截断 commit id 是 ff75cc5cdbf0423a24b4f5438e52683210813ba0



  • 根据上面的 commit id,带入下面的命令,找出其父


git cat-file -p ff75cc5cdbf0423a24b4f5438e52683210813ba0



可以看到只有一个父,其父是7ffe6782272879056ca9618f1d85a5f9716f8e90 ,所以该提交 id 就是要置为空的。如果有多个父都需要处理。



  • 执行命令


注意:对于普通提交节点,下面命令的 parent 值是"-p parentId";对于合并提交节点,下面命令的 parent 值是"-p parentId1 -p parentId2 -p parentId3 ..."


git filter-branch --force --parent-filter '
read parent
if [ "$parent" = "-p 7ffe6782272879056ca9618f1d85a5f9716f8e90" ]
then
echo
else
echo "$parent"
fi' --tag-name-filter cat -- --all


  • 重点验证:上述命令执行完毕后,一定要用如下命令检查是否修改成功


注意:因为执行完了命令已经修改了历史记录,此时 Git log 命令执行会慢点,大概5分钟可以出结果,另外可以用这个在线时间戳转换工具来转换时间戳。


工具链接:http://www.beijing-time.org/shijianchuo…


如果执行成功会把之前的文件版本取最新的 add 到这个截断的提交节点里面,如下图:


git log --all-match --author="xxxx" --grep="auto update .code.yml by robot" --name-status --before="1654043400" --after="1654012800" --all


4.3 使用 git-filter-repo 清理截断日期前的所有历史记录,并将截断节点的提交信息修改


注意此步骤要谨慎处理,因为这步会真正地删除提交记录。


提前安装好 git-filter-repo,执行下面的 python 代码。


import os
try:
import git_filter_repo as fr
except ImportError:
raise SystemExit("Error: Couldn't find git_filter_repo.py. Did you forget to make a symlink to git-filter-repo named git_filter_repo.py or did you forget to put the latter in your PYTHONPATH?")

k_work_dir = "/Volumes/SolidCompany/S_Shoushen/test"
# 2022.6.1 00:00:00
k_clean_history_deadline = b"1654012800"
# 2022.6.1 07:05:07
k_clean_deadline_commit_date = b"1654038307"
k_clean_deadline_commit_author_name = b"xxxxx"
k_new_root_commit_message = "仓库瘦身历史记录裁剪,截断提交记录后新根结点新增历史文件;如果想查看更多历史记录,请去备份仓库:https://example.com/test_backup.git"

def commitCallBackFun(commit, metadata):
[time_stamp, timezone] = commit.committer_date.split()
if time_stamp == k_clean_deadline_commit_date and commit.author_name == k_clean_deadline_commit_author_name:
commit.message = k_new_root_commit_message.encode("utf-8")
if time_stamp >= k_clean_history_deadline:
return
commit.file_changes = []

def main():
os.chdir(k_work_dir)
print("git work dir is", os.getcwd())
args = fr.FilteringOptions.parse_args(['--force', '--debug'])
filter = fr.RepoFilter(args, commit_callback = commitCallBackFun)
filter.run()

if __name__ == '__main__':
main()

验证下截断提交结点的提交信息更改成功了没?


git log --all-match --author="xxx" --grep="仓库瘦身历史记录裁剪" --name-status --before="1654043400" --after="1654012800"

如下就对了:



以上执行完后做个简单验证:


用 BeyondCompare 工具跟刚开始备份的 copyForCompare 目录下的 test 仓库对比,看看有没有增删改文件,期望应该没有任何变化才对。



  • 特殊分支处理


说明:以上历史记录裁剪并删除历史提交记录执行完后,对于基于截断提交节点前的提交节点创建出来的分支或者其子分支会出现文件被删除或者整个分支被删除的情况。



所以要提前弄清楚有没有在截断节点之前早就创建出来一直在用的分支, 如果有就得特殊处理上面的2和3步骤了:


第2步中截断历史记录的时候,要类似分析 master 分支那样分析其它需要保留的特殊分支,找出各自的截断节点的父提交 id;然后执行的 shell 脚本里面条件判断改成判断所有的父提交 id;类似这样:


git filter-branch --force --parent-filter '
read parent
if [ "$parent" = "-p 85f5ee6314f4f46cc47eb02c6af93bd3020a1053 -p cd207e9b3372f68a6d1ffe06fcf189d952e3bf9f" ] || [ "$parent" = "-p 7ffe6782272879056ca9618f1d85a5f9716f8e90" ]
then
echo
else
echo "$parent"
fi' --tag-name-filter cat -- --all

第3步中删除截断节点前提交记录的 python 脚本里面,按照分支名字和自己分支的截断日期来做比对逻辑进行删除提交记录的操作。类似如下:


#!/usr/bin/env python3
import os
try:
import git_filter_repo as fr
except ImportError:
raise SystemExit("Error: Couldn't find git_filter_repo.py. Did you forget to make a symlink to git-filter-repo named git_filter_repo.py or did you forget to put the latter in your PYTHONPATH?")

k_work_dir = "/Users/jevon/Disk/work/appShoushen/shoushen/test"

# 2022.6.1 07:05:07
k_master_cut_date = b"1654038307"

# 2022.3.25 19:32:00
k_private_new_saas_sdk_master_cut_date = b"1648207920"

k_new_root_commit_message = "仓库瘦身历史记录裁剪,截断提交记录后新根结点新增历史文件;如果想查看更多历史记录,请去备份仓库:https://example.com/test_backup.git"

def commitCallBackFun(commit, metadata):
[time_stamp, timezone] = commit.committer_date.split()
# 每个特殊分支的截断提交点的提交信息修改
if (time_stamp == k_master_cut_date and commit.author_name == b"xxx_author1") or \
(time_stamp == k_private_new_saas_sdk_master_cut_date and commit.author_name == b"xxx_author2"):
commit.message = k_new_root_commit_message.encode("utf-8")

# 每个特殊分支的截断提交点前的提交记录,需要根据各自截止日期来做比对删除日期前的历史记录
strBranch = commit.branch.decode("utf-8")
if strBranch.endswith("refs/heads/master"):
if time_stamp < k_master_cut_date:
commit.file_changes = []
elif strBranch.endswith("refs/heads/private/feature/3.12.1/new-saas-sdk-master"):
if time_stamp < k_private_new_saas_sdk_master_cut_date:
commit.file_changes = []
def main():
os.chdir(k_work_dir)
print("git work dir is", os.getcwd())
args = fr.FilteringOptions.parse_args(['--force', '--debug'])
filter = fr.RepoFilter(args, commit_callback = commitCallBackFun)
filter.run()

if __name__ == '__main__':
main()

以上[特殊分支处理]没有实验过,但是个解决思路,具体实践结果待补充,也欢迎实验过的同学交流。


4.4 进行 lfs 转换


rm -Rf .git/refs/original
rm -Rf .git/logs
git branch | wc -l # 看一下本地分支总数
# 拷贝原来的仓库到新目录下面
git clone file:///Users/jevon/Disk/work/appShoushen/shoushen/test /Users/jevon/Disk/work/appShoushen/shoushen/test_new
cd test_new
git branch -r | grep -v '->' |
while read remote
do
git branch --track "${remote#origin/}" "$remote"
done
git fetch --all git branch | wc -l # 看一下本地分支总数,和拷贝之前是否一样
# 分析仓库中占用空间较大的文件类型(演练的时候可以提前分析出来,节省瘦身时间)
git lfs migrate info --top=100 --everything

命令结果如下,是按照文件所有的历史版本累加统计的,只有未加入 lfs 的才会统计。



git rev-list --objects --all | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | sed -n 's/^blob //p' | sort --numeric-sort --key=2 | cut -c 1-12,41- | $(command -v gnumfmt || echo numfmt) --field=2 --to=iec-i --suffix=B --padding=7 --round=nearest | grep MiB

该命令执行结果如下,是把所有大于 1Mb 的文件版本都列出来了,不进行累加,从小到大排序,已经加入 lfs 的不会统计。



# lfs转换
# --include=之后填入根据实际分析的大文件列表
git lfs migrate import --everything --include="*.jar,tool/ATG/index.js,xxx"
# 上面lfs转换执行完后,看一下根目录的.gitattribute文件里面是不是加入了新的lfs文件了

4.5 新建新仓库,推送所有历史记录修改


新创建目标仓库 test_backup.git ,然后运行下面代码:


git remote remove origin
git remote add origin https://example.com/test_backup.git
git remote set-url origin https://example.com/test_backup.git
git remote -v # 确保设置成功新仓库地址

此时可以用下面的命令看看还有没有大文件了(可选)。


git rev-list --objects --all | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | sed -n 's/^blob //p' | sort --numeric-sort --key=2 | cut -c 1-12,41- | $(command -v gnumfmt || echo numfmt) --field=2 --to=iec-i --suffix=B --padding=7 --round=nearest | grep MiB

用以下命令看看还有没有未转换的大的 lfs 文件了(可选)。


git lfs migrate info --top=100 --everything
# 推送历史记录修改到目标新仓库:
git push origin --no-verify --all
git push origin --no-verify --tags

4.6 回到原来的 test 目录,推送 lfs 文件


cd ../test
git config lfs. https://example.com/test_backup.git /info/lfs.access basic
git lfs env # 看一下改成了basic了吗
# 设置成远端目标仓库test_backup.git
git remote remove origin
git remote add origin https://example.com/test_backup.git
git remote set-url origin https://example.com/test_backup.git
git remote -v # 确保设置成功新仓库地址


将 upload_objects.sh 拷贝到 test 目录,然后执行 sh ./upload_objects.sh


upload_objects.sh 内容如下:


#!/bin/bash

set -e

count=$(find .git/lfs/objects -type f | wc -l)
echo "-- total objects count is $count"

index=0
concurrency=25

find .git/lfs/objects -type f | awk -F '/' '{print $NF}' | while read -r obj; do
echo "-- $(date) -- uploading ($index/$count) $obj"
git lfs push origin --object-id "$obj" &
index=$((index+1))
if [[ $index%$concurrency -eq 0 ]]; then
echo -e "\r\n-- $(date) -- waiting --------------------------\r\n"
wait
fi
done

注意脚本里面的并发值不能设置太高,不然会报错./upload_objects.sh: line 12: echo: write error: Interrupted system call,测试发现设置25是比较合适的。



确保像上图这样,最后一个也上传成功。


05、新代码库验证


git clone https://example.com/test_backup.git

使用 git lfs pull 先拉取主干分支所有的历史文件进行测试,保留瘦身的本地仓库;  后续如果发现有其他分支的 lfs 文件没有上传成功,再单独上传即可。


上传命令:


git lfs push --object-id origin "$objId"

对比新旧代码库主干最新代码是否一致,可使用 beyond compare 工具进行验证。四端编译不同代表性的分支运行验证。


06、解决其它设备本地老分支 push 问题


在公司的代码管理平台上设置瘦身后的 test_backup 仓库单文件大小上限为1.5M。


一般公司自己的代码管理平台都会提供设置单个 git 文件上传大小上限的功能,得管理员才有权限设置;腾讯的代码管理平台是像下图这样设置的:



解释:之后的步骤将会把新老仓库互换,新旧仓库互换后,其它机器本地的老仓库分支还是有 push 上去的风险,这样就会把瘦身前的历史记录又推送到瘦身后的 Git 仓库,造成瘦身白费。


07、其他平台适配


7.1 代码管理平台


找代码管理平台协助完成下面的操作:(需要提前预约沟通好)


会议用的代码管理平台是工蜂:


项目名称、版本库路径互换:test_backup 重命名为 test,test 重命名为 test_backup。 将两个项目项目 id 进行调换:新项目沿用旧项目的项目 id,以此保证通过 api 调用的系统和工具不受到影响。 项目数据同步:同步项目成员和权限相关的数据、保护分支规则组到新仓库。

自己工蜂适配(可以提前进行)。对照老工蜂的所有配置,在新工蜂上手动同步修改。


7.2 第三方 Git 工具


如果使用了第三方 Git 工具平台做过瘦身仓库与其他项目仓库的同步,需要处理下(会议使用了 UGit 第三方工具):


通知 UGit 相关负责人把旧的工作区移除一下,重新 clone test 仓库。 把 Ugit 里面 test 仓库的同步任务恢复(如有需要)。

7.3 出包流水线构建平台


因为执行完瘦身后,Git 的 commit id 都变了,历史记录也变了,而 coding 的构建机如果不清理缓存删掉老仓库的话,会导致构建机本地仓库历史与远端冲突,执行 Git 命令会异常,所以 coding 必须要清理掉老仓库,重新 clone。


08、最后的验证


代码管理平台以及出包构建平台都处理完成后,进行最后的验证。


本地验证:


本地是否能正常 clone 代码。 本地对比新旧仓库主干最新代码是否一致。 本地随机抽取分支对比新旧仓库文件个数以及最新代码是否一致。 本地编译验证,程序启动主流程验证。

出包构建平台验证:


主干分支、发布分支、个人需求分支、个人分支等的构建验证。

代码管理平台验证:


代码库基础、高级配置是否正确 保护分支规则配置是否正确,是否有效 项目成员是否和原仓库一致 MR 是否可正常发起、合并,能否正常调起检测流水线

代码库写权限恢复:


保证瘦身后的 Git 仓库恢复写权限;备份仓库禁用写权限。

09、瘦身完毕后的知会


知会参考模板:


xxx 仓库瘦身完成了!接下来需要开发重点关注:本地旧仓库已经失效,必须删掉重新 clone 代码【最最重要】未提前push到远端的本地新增代码需要手动同步旧的未完成的MR已经失效,需要关闭后重新发起需要查询或处理更老的代码,需要去备份仓库查看(xxxx/xxxx.git)开发过程中有任何疑问,欢迎请随时联系 xxx

10、兜底回滚方案


因为使用了备份仓库,所以不会修改原始仓库,但只有代码管理平台(工蜂)在第七步的时候修改了原始仓库,对于这个工蜂的协助修改,需要提前确认好工蜂那边做好了回滚的方案。


11、踩坑记录及应对


11.1 上传 lfs 的时候报错 User is null or anonymous user



LFS: Git:User is null or anonymous user.


解决:git config lfs.example.com/test_backup… basic


输入 git lfs env 看一下输出结果改成了 basic 了吗?



11.2 git push 的时候报错



把远程链接改成 https 的:


git remote set-url origin https://example.com/test_backup.git
git remote -v

如果~/.gitconfig 中有如下的内容要先注释掉。


url.git@example.com:.insteadof=http://example.com/ url.git@example.com:.insteadof=https://example.com/

最后再 push 即可。


如果上述还不行,那么在命令行中执行:


git config --global https.postbuffer 1572864000git config --global https.lowSpeedLimit 0git config --global https.lowSpeedTime 999999

如仍然无法解决,可能是用户的客户端默认有设默认值限制 git 传输包的大小,可执行指令:


git config --global core.packedGitLimit 2048m
git config --global core.packedGitWindowSize 2048m

11.3 window 如何在 git batch 里面运行 git-filter-repo?


安装 python:打开 cmd 窗口,运行 python -m pip install git-filter-repo,安装 git-filter-repo;


用 everything 查找 git-filter-repo.exe;


cmd 窗口,运行 git --exec-path,得到路径类似:C:\Program Files\Git\mingw64\libexec\git-core;


把上面找到的 git-filter-repo.exe 拷贝到 git-core 文件夹里面;


此时在 git batch 窗口中,输入命令 git filter-repo(注意输入的git后面没有-),会提示 No arguments specified.证明 ok 了。


11.4 如果想让 git-filter-repo 作为一个 python 库来使用,实现更复杂的功能该怎么办?


比如,不想这么用了 git-filter-repo --force --commit-callback "xxxx python code...",因为这么用只能写回调的 python 代码,太弱了。


解决:python3 -m pip install --user git-filter-repo,不行就 python3 -m pip install git-filter-repo,安装这个 git-filter-repo包,然后就可以在 python 代码中作为库使用:import git_filter_repo as fr。


11.5 瘦身后发现 coding 的 win 构建机器在 clone 代码时出问题,怎么办?


卡在 git lfs pull:



卡在 git checkout --force xxxxx 提交 id:



卡在 checking out files:



调查发现,是 lfs 进程卡住,不知道什么样的场景触发的,官方有个类似 issue,以上问题均是因为 git 或者 git lfs 版本过低导致的,升级到高版本即可解决。


据当时出错 case 总结得出结论,以下 git 和 git lfs 的版本号可以保证稳定运行不出问题,如果版本号低于以下所示,最好升级。



11.6 执行 git lfs fetch 的时候报错 too many open files 的问题


解决办法:ulimit -n 9999999


12、写在最后


仓库瘦身是个细致耗时的工作,需要谨慎认真地完成。最后腾讯会议客户端仓库的大小也从 9G 瘦身到 350M ,实现的效果还是不错的。


本次我们分享了仓库瘦身的全历程,把执行命令也公示给各位读者。希望可以帮助到为类似困境而头疼的开发者们。这篇文章对您有帮助的话,欢迎转发分享。


-End-


原创作者|李双君


技术责编|陈从贵、郭浩伟



作者:腾讯云开发者
来源:juejin.cn/post/7261814990843265061
收起阅读 »

浏览器渲染15M文本导致崩溃怎么办

web
最近,我刚刚完成了一个阅读器的txt文件阅读功能,但在处理大文件时,遇到了文本内容过多导致浏览器崩溃的问题。 一般情况下,没有任何样式渲染时不会出现什么问题,15MB的文件大约会有3秒的空白时间。 <div id="content"></di...
继续阅读 »

最近,我刚刚完成了一个阅读器的txt文件阅读功能,但在处理大文件时,遇到了文本内容过多导致浏览器崩溃的问题。


一般情况下,没有任何样式渲染时不会出现什么问题,15MB的文件大约会有3秒的空白时间。


<div id="content"></div>

fetch('./dp.txt').then(resp => resp.text()).then(text => {
document.getElementById('content').innerText = text
})

尽管目前还没有严重的问题,但随着文件继续增大,肯定会超过浏览器内存限制而导致崩溃。


在开发阅读器的过程中,我添加了下面的样式,结果导致浏览器直接崩溃:


* {
margin: 0;
padding: 0;
}

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

body {
column-fill: auto;
column-width: 375px;
overflow-x: auto;
}

预期结果应该是像下面这样分段显示:



然而,实际出现了下面的问题:


unnamed.png


因此,文件内容太多会导致浏览器崩溃。即使进行普通的渲染,我们也要考虑这个问题。


如何解决


解决这个问题的方法有点经验的前端开发工程师应该都知道可以使用虚拟滚动,重点是怎么对文本分段分段,最容易的可能就是按照一定数量字符划分,但是这个导致文本衔接不整齐出现文本跳动。如图,橙色和蓝色表示两端文本的衔接,虚拟滚动必然会提前移除橙色那块内容,那么就会导致蓝色文本位置发生改变。



要解决这个问题,我们需要想办法用某个元素替代原来的位置。当前页橙色部分删除并计算位置,问题会变得复杂并且误差比较大,因此这一部分直接保留,把这部分前面的内容移除,然后用相同长度的元素占据,接下来重点就是怎么获取到橙色部分与前面内容的分界点。


获取分界点可以使用document.createRange()document.createRange()是 JavaScript 中用于创建Range对象的方法。Range对象表示一个包含节点与文本节点之间一定范围的文档片段。这个范围可以横跨单个节点、部分节点或者多个节点。


// 创建 Range 对象
const range = document.createRange();

range.setStart(textNode, 0); // 第一个参数可以是文本节点,第二个参数表示偏移量
range.setEnd(textNode, 1);
const rect = range.getBoundingClientRect(); // 获取第一个字符的位置信息

利用Range对象的特性,我们可以从橙色部分的最后一个字符开始计算,直到找到分界点的位置。


阅读器如果仅仅只是从左往右阅读,按照上面的方法已经可以实现,但是阅读器可能会出现页面直接跳转,跳转之后的文本起点你并不知道,并且页面总页码你也无法知道。因此从一开始就要知道每一页的分界点,也就是需要做预渲染。以下是一个简单的示例:


let text = '...'
const step = 300
let end = Math.min(step, value.length) // 获取结束点

while (text.length > 0) {
node.innerText = value.substring(0, end) // 取部分插入节点
const range = document.createRange()
range.selectNodeContents(node)
const rect = range.getBoundingClientRect() // 获取当前内容的位置信息

if (rect.height > boxHeight) {
// 判断当前内容高度是否会超出显示区域的高度
// 如果超出,从 end 最后一个字符开始计算,直到不超出范围
while (bottom > boxHeight) {
// node.childNodes[0] 表示文本节点
range.setStart(node.childNodes[0], end - 1)
range.setEnd(node.childNodes[0], end)
bottom = range.getBoundingClientRect().bottom
end--
}
} else {
// 如果没有超出,end 继续增加
// ...
}
}

上面只是简单的实现原理,可以达到精确区分每一页的字符,但是计算量有点太大,15MB文本大约500多万字,循环次数估计也在几十万到上百万。在本人的电脑上测试大约需要20秒,每个人设备的性能不同,所需时间也会有所不同。很明显,这种实现方式并不太理想。


后来我对这个方案进行了优化,实际上我们不需要计算每一页的分界点,可以计算出多页的分界点,例如10页、20页、50页等。优化后的代码是将step增大,比如设为10000,然后将不能组成一页的尾部内容去掉。优化后,15MB的文本大约需要4秒左右。需要注意的是,step并不是越大越好,太大会导致渲染页面占用时间过长。


这就是我目前用来解决页面渲染大量文本的方法。如果你有更

作者:60岁咯
来源:juejin.cn/post/7261231729523965989
好的方案,欢迎留言。

收起阅读 »

古茗前端到底搞什么飞机

在前几期文章的评论中,我发现不少人有类似“古茗前端到底搞什么飞机”的疑问: 其实在入职古茗前我也有这种观点,不就是做做下单小程序,做做简单的内部管理系统吗,甚至在面试过程中我也问了面试官这个问题,在听完面试官的解答之后,我也同样地忍不住发出了“居然要做这么牛...
继续阅读 »


在前几期文章的评论中,我发现不少人有类似“古茗前端到底搞什么飞机”的疑问:



其实在入职古茗前我也有这种观点,不就是做做下单小程序,做做简单的内部管理系统吗,甚至在面试过程中我也问了面试官这个问题,在听完面试官的解答之后,我也同样地忍不住发出了“居然要做这么牛逼的事情,这还是一个奶茶公司吗”的感慨!


于是我毫不犹豫地选择加入了古茗(有免费奶茶喝!畅饮的那种!)。


到现在已经入职快一年了,是时候跟大家讲讲“古茗前端到底搞什么飞机”了。


做一杯奶茶,总共分几步?


其实古茗的业务真的很多,很多,很多!具体有哪些我不方便透露,只能说光是我们前端团队就服务了4个业务域,18条业务线。那我们是怎么服务这些业务域和业务线的呢?我就拿我所在的“机料”举例吧。


那什么是机料呢?在这里我先卖个关子,相信看完下面的介绍,你就会知道是什么意思了,以后去古茗点奶茶就可以跟别人吹牛了(狗头)


首先问大家一个问题:做一杯奶茶,总共分几步?


就和把大象塞进冰箱一样,第一步:倒上茶,第二步:加上料,第三步:吨吨吨!


是不是很简单?步骤看着是简单,但是衍生出来的问题还是很多的


第一步涉及到的问题:



  1. 泡茶汤时,不同的茶,分别用多少度的水泡?泡多长时间?泡多少量?

  2. 怎么保证全国门店的店员按要求执行了泡茶方法?

  3. 怎么灵活控制不同地区的茶保持相同/不同口感?

  4. ...


再来看看第二步涉及到的问题:



  1. 有些物料是冷冻运输的,什么时候拆封解冻?解冻多久?保质期多久?

  2. 有些物料是原材料,什么时候要制备成半成本?保质期多久?

  3. 怎么保证加料时物料是在最佳赏味期内的?

  4. 怎么保证全国门店的店员遵守食品安全规范?

  5. 怎么提前告知门店高峰期的预估物料种类以及用量?

  6. ...


最后第三步里的问题:



  1. 怎么保证门店能尽可能还原研发室里研发出来的口味?

  2. 怎么保证更快地出杯?

  3. ...


虽然看着很麻烦很多,但是我们稍微捋一下还是能捋明白的:



  • 对于“怎么保证”这类问题,其实是一种功能性问题,我们需要让我们的功能代码在门店运行

  • 对于“多少”、“多久”、“什么时候”这些问题,可以归类为配置性问题,可以通过后台配置并进行下发


那基于这两大类问题,“机料”业务就浮出水面了。


机-机器设备


机,就是机器设备(下文统一叫设备),奶茶店里有各种各样的设备,这些机器都是用来辅助店员去标准化地制作奶茶的,设备更多地在解决一些“功能性”的问题,有了这些功能,我们就可以更好地保证一系列流程的规范性。


解决了什么问题


比如上面提到的问题:泡茶汤时,不同的茶,分别用多少度的水泡?泡多长时间?泡多少量?这些问题都是直接影响茶汤的口感的,茶汤是一杯奶茶的基底,要是口感不佳,那么整杯奶茶就毁了。


虽然古茗有很严格的培训体系,每一个店员都需要来总部进行培训学习和考试,但是哪怕是老虎也有打盹的时候,我们不能完全寄希望于店员时时刻刻严格遵守不同类别的茶的制作流程,人不是机器,对不同茶汤的温度、水量、时长等等因素进行人为控制,这些都是难度极大的。


既然人不是机器,那就直接造!于是乎我们的设备部做了泡茶机、制冷设备等等设备。


有泡茶机前,我们的店员需要记下每一款茶的调制过程,然后人工去泡,这就导致同一个人,同一家店,不同时间,泡出来的茶口感会不一样。


有了泡茶机以后,店员只需要把茶包包装上的茶包码往泡茶机上扫一下,泡茶机就可以检索对应的茶汤配方自动按照标准流程、按照标准参数进行泡茶,这样就解决了人为带来的茶汤口感不稳定的问题。


并且无形中还解决了另一个问题,就是“灵活控制不同地区的茶保持相同/不同口感”,因为刚才有提到,泡茶机会根据对应的茶汤配方进行泡茶流程,那么我们就可以根据不同地区下发不同的配方,来保证泡出预期口感的茶汤。


怎么实现


那这一套我们是怎么做的呢?前端在其中扮演了什么角色呢?考虑到保密的原因,我这里就简单画几个图,详细的技术细节就不透露了。



设备侧的产品经理需求评审之后:



  1. 嵌入式开发:与“网关”开发(一般是前端开发)共同制定数据通信协议,并按这份协议进行嵌入式开发,通过这份协议可以实现硬件设备与“网关”上USB或者蓝牙的数据传输方式

  2. 后端开发:与前端制定接口格式,通过接口可以将设备的数据上传至后端,用做设备信息展示、设备异常告警等用处

  3. 前端开发:根据数据通信协议进行“网关”功能的开发,通过协议解析设备的数据,并按照接口格式上传至后端,在后台大盘上显示数据,或者在“网关”上显示相关设备的异常告警


可以看到前端不光要开发后台的功能,还需要开发“网关”的功能,这对传统前端开发来说是一种新的挑战,因为不光要写前端代码,还要掌握硬件的通信协议,了解客户端的相关开发技能,目前的“网关”其实是运行在搭载了Android系统平板的APP中,协议的实现都需要在这个APP中完成。



用这种“伪网关”的方式存在一个很明显的弊端,就是我们的门店存在多个种类的设备,光是制冷设备就分为平冷、冷冻、冷藏三种类型,每种类型分别会有1-2台设备下店,加上一些研发中的设备,这样一来网关平板的蓝牙连接数量很容易达到上限,就会造成有部分设备无法连接蓝牙的问题,为了解决这个问题,我们出了一个临时方案,一个长久方案。


临时方案就是保证核心设备是保持连接的,但是给一些相对来说不是那么重要的设备做定时断开、轮流连接的处理,这样能一定程度上解决这个问题。


长久方案就是开发一个真正意义上的“边缘网关”,边缘网关能接入更多的设备,且能更聚焦于设备的通信、信息的处理分析等功能。


边缘网关是一种用于连接边缘设备和云平台之间的网络设备。 其主要作用是在边缘设备和云平台之间构建一个灵活、高效和安全的网络,将数据从边缘设备收集并处理后,再传输到云平台上进行存储和分析。 在这个过程中,边缘网关需要具备多种功能,包括数据的采集、处理、分析、存储和传输等。


小结


我经常感叹,我们组不是单纯的前端开发工程师了,因为组里的成员一直在和设备、各种协议打交道,桌上也放着各种各样的电路板,年前甚至把一台要下店的净水器直接放在工位边联调,也写过“220V,勿碰”的牌子放在工位上。


之前和TL聊物料网的应用场景,我们都认为这就是物联网一个很好的落地场景,并且我们的目标远不止于此,业务上还有很多的问题要依赖于设备去解决,很多降本增效的事情也要依赖于设备去实现,我相信在不久的将来,古茗的设备一定会给门店带来更多的收益,古茗的IoT方案会成为茶饮界IoT方案中值得参考的那个。


料-物料


说完“机”,再来看看“料”,就是制作一杯奶茶所需要用到的物料。


餐饮行业最重视的一个问题就是食品安全问题,这首先关系到每个消费者的身体健康,其次涉及门店的经营情况,最后会影响品牌在公众心里的印象,所以食安是每一个餐饮企业的底线。


古茗为了保证食安,搞了自己的种植基地,搭建了自己的供应链系统、智能报货系统,研发室的研发员们针对每一款物料制定了“有效期”...


除了种植基地,其他业务都和我们组有关系,这里我就介绍一下物料“有效期”这件事。


解决了什么问题


在开头的经典三步的第二步,都是物料相关的内容,首先我先科普一下物料有效期相关的几个概念:解冻时间、备料时间、最佳赏味期。


解冻时间:冷冻品需要解冻的时长,比如家里的冻肉,做菜时得先拿出来解冻一下;


备料时间:解冻完成后就需要对原料进行备料加工了,还是拿冻肉举例,冻肉解冻完了就得切成肉片开始炒了;


最佳赏味期:顾名思义,就是在这个期间内食用是最好的,好比你妈妈把肉炒完了,喊你吃饭,结果你一直在玩游戏就错过了最佳赏味期了。



为什么是定这三个时间,而不是其他的四个时间,五个时间呢?


其实也是根据奶茶的制作方式来定的,因为门店要做一杯奶茶是需要用到半成品物料的,半成品物料又需要由原料制备得到,而那些冷冻原料又需要解冻(解冻其实也是制备的一个环节,只不过解冻需要的时间会很长,且不是每个物料都需要解冻,比如茶就不需要解冻),所以结合实际情况,就定了这三个时间。


按我个人理解的话,其实就是为了保证任何一家门店、任何一个店员做出来的奶茶,都是符合食安的、最新鲜的、口感最好的。


怎么实现


那这三个时间需要怎么在门店里直观地展现给店员呢?


聪明的你可能想到了,对,就是利用设备!这台设备的名字就叫“效期机”,我再画个图给大家看看效期机是怎么在门店发挥作用的。



假设店员发现XX冷冻原浆快不够用了,那他就会从冷冻柜中取出XX冷冻原浆,然后去效期机的物料列表上去找XX冷冻原浆这个物料,找到后在使用效期机的打印功能进行打印,这时打印机就会打印XX冷冻原浆的效期贴(记载了物料相关信息的一个小贴纸),店员撕下小贴纸后贴到XX冷冻原浆的容器上,最后放到解冻区进行解冻。



店员做完上面这些步骤之后就可以做自己的事了,等到了解冻时间、备料时间、超过最佳赏味期前2分钟这些节点,效期机就会进行语音播报,提醒店员XX冷冻原浆需要进行XX操作了。


那效期机是怎么知道物料的这三个时间并在对应时间给出语音提醒的呢?


其实这个链路的流程蛮简单的,效期机种记录了每个物料的三种时间,在店员打印效期贴的那一刻,这个物料的生命周期就开始了,物料的生命周期每个阶段的时长是按照物料配置平台配置的时间决定的,同时效期机内部维护了三个有序队列,分别是解冻提醒队列,备料提醒队列,最佳赏味期超期提醒队列,物料的生命周期就在这三个队列之间流转,驱动生命周期的是一个10s一次的轮询,每次轮询都会去判断三个队列的头数据是否达到触发条件,即是否到了提醒时间。


比如XX冷冻原浆的配置是解冻时间30分钟,备料时间10分钟,最佳赏味期60分钟,店员打印效期贴的时间是12:00,那么这个物料的生命周期就是这么流转的:



这里有个小细节:我们会在最佳赏味期前2分钟就进行提醒,这个的目的就是为了更进一步保证食安以及最后奶茶的口感。


小结


其实对于物料的管理,上文提到的内容只是冰山一角,我们已经做的、正在做的、未来计划要做的事还多的很。


在食安问题上,针对私自篡改效期时间的门店,会进行高额的罚款,每天也会定期进行后厨的打扫,在培训时也会针对食安部分进行严格的培训和考试;


在物料提醒的优化上,我们发现目前的提醒交互还是会存在店员理解不到位的情况,这里也在不断地进行优化和迭代;


在保证更快的出杯速度上,我们现在正在做的一件事是基于门店的实际出杯情况去生成预测物料用量,并通过效期机下发给到门店,辅助门店更好准确地进行物料的提前制备,以及提升出杯速度。


感悟


最后聊聊入职古茗后的感悟吧,在这不到一年的时间感觉成长了很多。


首先是技术吧,前面也提到了,我所在的组要和设备打交道,加上我本来是个安卓开发,现在加入古茗前端部门后,也在针对性学习前端的内容、跨端的技术,其实学习一门语言并不是难题,难的是要培养前端的编程思维,这和客户端思维还是有差别的。


其次是业务,古茗应该是我离业务最近的公司了,我给门店打过上百通电话,线下跑过十来家门店,跟产品经理去实地调研......这些经历让我更了解了店员需要什么,我们需要提供什么样的功能给他们。


然后是工程师能力,这是在古茗前端部门经常被提起的一个概念,我们给工程师的定位是能解决一类复杂问题的人,在此基础上我们就应该具备更完善的能力,不仅仅局限于写某一种或几种代码,为了搭建运维平台,我特地去学习了产品知识,按照一个完整的流程进行了原型图的评审,需求文档的评审,并参与到平台的开发中。不仅是我,古茗的前端都在朝着“工程师”而在努力着。


最后的最后,就是人长胖了,古茗真的是无限畅饮,我可真喝不动了......


最后


关注公众号「Goodme前端团队」,获取更多干货

作者:古茗前端团队
来源:juejin.cn/post/7261628991055183930
实践,欢迎交流分享~

收起阅读 »

浅谈软件质量与度量

我正在参加「掘金·启航计划」本文从研发角度探讨下高质量软件应具备哪些特点,以及如何度量软件质量。软件质量的分类软件质量通常可以分为:内部质量和外部质量。内部质量内部质量是指软件的结构和代码质量,以及其是否适合维护、扩展和重构。它关注的是软件本身的特性和属性,包...
继续阅读 »

我正在参加「掘金·启航计划」

本文从研发角度探讨下高质量软件应具备哪些特点,以及如何度量软件质量。

软件质量的分类

软件质量通常可以分为:内部质量和外部质量。

内部质量

内部质量是指软件的结构和代码质量,以及其是否适合维护、扩展和重构。它关注的是软件本身的特性和属性,包括:

  • 可读性:代码易于阅读和理解;
  • 易维护性:代码易于修改和维护;
  • 可测试性:代码易于编写单元测试并进行自动化测试;
  • 可靠性:代码稳定、不容易崩溃或出现错误;
  • 可扩展性:代码能够方便地进行扩展;
  • 可重用性:代码可被复用于其他项目中。

内部质量直接影响软件的可维护性和开发效率。如果软件的内部质量很差,那么开发人员可能需要花费更多的时间修复问题,而不是开发新功能。

外部质量

外部质量是指软件的用户体验和其符合用户需求的程度。它关注的是软件的功能和表现形式,包括:

  • 功能性:软件是否具有所需的功能,并且这些功能是否能够正常工作;
  • 易用性:软件是否易于使用,是否符合用户的期望;
  • 性能:软件是否运行快速并响应迅速;
  • 兼容性:软件是否能够在不同的操作系统和设备上正常工作。

外部质量如果很差,那么用户在使用软件过程中可能会遇到问题,而这些问题可能会影响用户体验,导致用户流失。

为什么内部质量更重要

内部质量高的核心降低了未来变更的成本

可以参考下图的时间-功能累计关系图。

对于内部质量比较差的软件,虽然初期进展迅速,但是随着时间的流逝,添加新功能变得越来越困难。甚至一个小改动也需要程序员理解大量代码。当开发做代码变更时,还可能产生意想不到的缺陷,因此导致测试时间长,需要更高成本来做缺陷修复和验证。

对于内部质量高的软件,则与其相反,可以参考下图的比较。

内部质量高的软件更容易被实现。

内部质量高的软件特点之一就是易读性。 这样利于开发者更快弄清楚应用程序是如何运行的,这样就可以知道如何添加新功能。如果将软件很好地划分为不同的实现模块,则开发者没必要阅读所有代码,只需要快速找到涉及功能变动模块的代码就行。

如何衡量软件质量

Cyclomatic Complexity(圈复杂度)

Cyclomatic Complexity通过计算代码中不同路径的数量来衡量代码的复杂程度。圈复杂度越高,表示代码的控制流程越复杂,可能存在更多的错误和缺陷。

下面举例说明Cyclomatic Complexity如何计算。

public int calculate(x, y) {
if (x >= 20) {
if (y >= 20) {
return y;
}

return x;
}

return x + y;
}

这段代码的流程图如下:

圈复杂度的公式如下:

E - N + 2

其中 E 表示图中的边数(上图中的所有形状),N 表示节点数(上图中的所有箭头)。因此,在我们的例子中,6 - 5 + 2 = 3,的确这段代码包含三条路径。

Maintainability Index(可维护性指数)

Maintainability Index(可维护性指数)是一种用于评估软件代码可维护性的指标。它通常考虑代码的复杂度、长度和注释等因素,并将这些因素整合成一个分数来衡量代码的可读性、可维护性和可重构性。

通常情况下,可维护性指数的分数范围是 [0,100],分数越高表示代码的可维护性越好。可维护性指数可以帮助开发人员识别哪些代码需要改进,以提高代码的可维护性和可读性,从而减少维护成本、降低缺陷率,提高代码的质量。

Dependencies(依赖)

软件的开发过程势必会依赖外部框架和库,这些框架和库自身也会经常更新(维护者会添加和删除功能、修复错误、改进性能,并修补安全漏洞)。

旧版本库和框架通常会对依赖它的软件质量产生负面影响。例如安全漏洞是明显的风险(例如22年8月份的

Apachelog4j漏洞)。

SQALE评估法

SQALE评估法主要关注四个指标:

  1. 技术债:即未来要花费的时间和资源去修复当前存在的问题
  2. 可维护性:即代码的易读性、可理解性和可扩展性,从代码的模块化程度、命名规范、注释等因素,并对这些因素进行打分。
  3. 可靠性:即软件的稳定性和可靠性,评估代码中存在的错误、漏洞和异常处理情况。如果存在较多的问题,他们就需要考虑重新设计代码或增加更多的测试用例。
  4. 性能:即软件的响应速度和处理能力

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

for和range性能大比拼!

能GET到的知识点什么场景使用for和range1. 从一个遍历开始万能的range遍历遍历array/slice/stringsarraypackage main import "fmt" func main() { var ...
继续阅读 »

能GET到的知识点

  • 什么场景使用for和range

1. 从一个遍历开始

万能的range遍历

  1. 遍历array/slice/strings

array

package main  

import "fmt"

func main() {
var UserIDList = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

for i, v := range UserIDList {
fmt.Println(i, v)
}
}

0 1
1 2
2 3
3 4
4 5
5 6
6 7
7 8
8 9
9 10

slice

package main  

import "fmt"

func main() {
var UserIDList = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
var UerSlice = UserIDList[:]

for i, v := range UerSlice {
fmt.Println(i, v)
}
}
0 1
1 2
2 3
3 4
4 5
5 6
6 7
7 8
8 9
9 10

字符串

func main(){
var Username = "斑斑砖abc"
for i, v := range Username {
fmt.Println(i, v)
}
}


0 26001
3 26001
6 30742
9 97
10 98
11 99


range进行对array、slice类型遍历一切都正常,但是到了对字符串进行遍历时这里就出问题了,出问题主要在索引这一块。可以看出索引是每个字节的位置,在go语言中的字符串是UTF-8编码的字节序列。而不是单个的Unicode字符。遇到中文字符时需要使用多个字节表示,英文字符一个字节进行表示,索引0-3表示了一个字符及以此完后。

  1. 遍历map
func ByMap() {  
m := map[string]int{
"one": 1,
"two": 2,
"three": 3,
}
for k, v := range m {
delete(m, "two")
m["four"] = 4
fmt.Printf("%v: %v\n", k, v)
}
}

one: 1
four: 4
three: 3


  • 和切片不同的是,迭代过程中,删除还未迭代到的键值对,则该键值对不会被迭代。
  • 在迭代过程中,如果创建新的键值对,那么新增键值对,可能被迭代,也可能不会被迭代。个人认为应该是hash的无序性问题
  • 针对 nil 字典,迭代次数为 0
  1. 遍历channel
func ByChannel() {  
ch := make(chan string)
go func() {
ch <- "a"
ch <- "b"
ch <- "c"
ch <- "d"
close(ch)
}()
time.Sleep(time.Second)
for n := range ch {
fmt.Println(n)
}
}
  • 针对于range对关闭channel的遍历,会直到把元素都读取完成。
  • 但是在for遍历会造成阻塞,因为for变量读取一个关闭的管道并不会进行退出,而是一直进行等待,但是如果关闭了会返回一个状态值可以根据该状态值判断是否需要操作

2.for和range之间奇怪的问题

2.1 无限遍历现象

for

c := []int{1, 2, 3}  
for i := 0; i < len(c); i++ {
c = append(c, i)
fmt.Println(i)
}

1
2
3
.
.
.
15096
15097
15098
15099
15100
15101
15102
15103
15104

range

c := []int{1, 2, 3}  
for _, v := range c {
c = append(c, v)
fmt.Println(v)
}

1
2
3

可以看出for循环一直在永无止境的进行追加元素。 range循环正常。原因:for循环的i < len(c)-1都会进行重新计算一次,造成了永远都不成立。range循环遍历在开始前只会计算一次,如果在循环进行修改也不会影响正常变量。

2.2 在for和range进行修改操作

for

type UserInfo struct {  
Name string
Age int
}
var UserInfoList = [3]UserInfo{
{Name: "John", Age: 25},
{Name: "Jane", Age: 30},
{Name: "Mike", Age: 28},
}
for i := 0; i < len(UserInfoList); i++ {

UserInfoList[i].Age += i
}
fmt.Println(UserInfoList)

0
1
2
[{John 25} {Jane 31} {Mike 30}]

range

var UserInfoList = [3]UserInfo{  
{Name: "John", Age: 25},
{Name: "Jane", Age: 30},
{Name: "Mike", Age: 28},
}


for i, info := range UserInfoList {
info.Age += i
}
fmt.Println(UserInfoList)

[{John 25} {Jane 30} {Mike 28}]

可以看出for循环进行修改了成功,但是在range循环修改失效,为什么呢?因为range循环返回的是对该值的拷贝,所以修改失效。for循环修相当于进行原地修改了。但如果在for循环里面进行赋值修改操作,那么修改也会进行失效 具体如下

var UserInfoList = [3]UserInfo{  
{Name: "John", Age: 25},
{Name: "Jane", Age: 30},
{Name: "Mike", Age: 28},
}
for i := 0; i < len(UserInfoList); i++ {
fmt.Println(i)
item := UserInfoList[i]
item.Age += i

}


fmt.Println(UserInfoList)
> [{John 25} {Jane 30} {Mike 28}]

3. Benchmark大比拼

主要是针对大类型结构体

type Item struct {  
id int
val [4096]byte
}

for_test.go

func BenchmarkForStruct(b *testing.B) {  
var items [1024]Item
for i := 0; i < b.N; i++ {
length := len(items)
var tmp int
for k := 0; k < length; k++ {
tmp = items[k].id
}
_ = tmp
}
}
func BenchmarkRangeStruct(b *testing.B) {
var items [1024]Item
for i := 0; i < b.N; i++ {
var tmp int
for _, item := range items {
tmp = item.id
}
_ = tmp
}
}
goos: windows
goarch: amd64
pkg: article/02fortest
cpu: AMD Ryzen 5 5600G with Radeon Graphics
BenchmarkForStruct-12 2503378 474.8 ns/op 0 B/op 0 allocs/op
BenchmarkRangeStruct-12 4983 232744 ns/op 0 B/op 0 allocs/op
PASS
ok article/02fortest 3.268s

可以看出 for 的性能大约是 range 的 600 倍。

为什么会产生这么大呢?

上述也说过,range遍历会对迭代的值创建一个拷贝。在占据占用较大的结构时每次都需要进行做一次拷贝,取申请大约4kb的内存,显然是大可不必的。所以在对于占据较大的结构时,应该使用for进行变量操作。

总结

如何选择合适的遍历,在针对与测试场景的情况下,图便捷可以使用range,毕竟for循环需要写一堆的条件,初始值等。但是如果遍历的元素是个占用大个内存的结构的话,避免使用range进行遍历。且如果需要进行修改操作的话只能用for遍历来修改,其实range也可以进行索引遍历的,在本文为写,读者可以去尝试一下。


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

告别StringUtil:使用Java 全新String API优化你的代码

前言  Java 编程语言的每一次重要更新,都引入了许多新功能和改进。 并且在String 类中引入了一些新的方法,能够更好地满足开发的需求,提高编程效率。repeat(int count):返回一个新的字符串,该字符串是由原字符串重复指定次数形成的。isBl...
继续阅读 »

前言

  Java 编程语言的每一次重要更新,都引入了许多新功能和改进。 并且在String 类中引入了一些新的方法,能够更好地满足开发的需求,提高编程效率。

  1. repeat(int count):返回一个新的字符串,该字符串是由原字符串重复指定次数形成的。
  2. isBlank():检查字符串是否为空白字符序列,即长度为 0 或仅包含空格字符的字符串。
  3. lines():返回一个流,该流由字符串按行分隔而成。
  4. strip():返回一个新的字符串,该字符串是原字符串去除前导空格和尾随空格后形成的。
  5. stripLeading():返回一个新的字符串,该字符串是原字符串去除前导空格后形成的。
  6. stripTrailing():返回一个新的字符串,该字符串是原字符串去除尾随空格后形成的。
  7. formatted(Object... args):使用指定的参数格式化字符串,并返回格式化后的字符串。
  8. translateEscapes():将 Java 转义序列转换为相应的字符,并返回转换后的字符串。
  9. transform() 方法:该方法可以将一个函数应用于字符串,并返回函数的结果。

示例

1. repeat(int count)

public class StringRepeatExample {
public static void main(String[] args) {
String str = "abc";
String repeatedStr = str.repeat(3);
System.out.println(repeatedStr);
}
}

输出结果:

abcabcabc

2. isBlank()

public class StringIsBlankExample {
public static void main(String[] args) {
String str1 = "";
String str2 = " ";
String str3 = " \t ";

System.out.println(str1.isBlank());
System.out.println(str2.isBlank());
System.out.println(str3.isBlank());
}
}

输出结果:

true
true
true

3. lines()

import java.util.stream.Stream;

public class StringLinesExample {
public static void main(String[] args) {
String str = "Hello\nWorld\nJava";
Stream<String> lines = str.lines();
lines.forEach(System.out::println);
}
}

输出结果:

Hello
World
Java

4. strip()

public class StringStripExample {
public static void main(String[] args) {
String str1 = " abc ";
String str2 = "\t def \n";
System.out.println(str1.strip());
System.out.println(str2.strip());
}
}

输出结果:

abc
def

5. stripLeading()

public class StringStripLeadingExample {
public static void main(String[] args) {
String str1 = " abc ";
String str2 = "\t def \n";
System.out.println(str1.stripLeading());
System.out.println(str2.stripLeading());
}
}

输出结果:

abc
def

6. stripTrailing()

public class StringStripTrailingExample {
public static void main(String[] args) {
String str1 = " abc ";
String str2 = "\t def \n";
System.out.println(str1.stripTrailing());
System.out.println(str2.stripTrailing());
}
}

输出结果:

abc
def

7. formatted(Object... args)

public class StringFormattedExample {
public static void main(String[] args) {
String str = "My name is %s, I'm %d years old.";
String formattedStr = str.formatted( "John", 25);
System.out.println(formattedStr);
}
}

输出结果:

My name is John, I'm 25 years old.

8. translateEscapes()

public class StringTranslateEscapesExample {
public static void main(String[] args) {
String str = "Hello\\nWorld\\tJava";
String translatedStr = str.translateEscapes();
System.out.println(translatedStr);
}
}

输出结果:

Hello
World Java

9. transform()

public class StringTransformExample {
public static void main(String[] args) {
String str = "hello world";
String result = str.transform(i -> i + "!");
System.out.println(result);
}
}

输出结果:

hello world!

结尾

  如果觉得对你有帮助,可以多多评论,多多点赞哦,也可以到我的主页看看,说不定有你喜欢的文章,也可以随手点个关注哦,谢谢。

  我是不一样的科技宅,每天进步一点点,体验不一样的生活。我们下


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

俺,25岁,踌躇一下?

人比山高大家好,我是寒草,一只工作两年的程序猿。农历七月初五,阳历八月七日是我的生日,小时候总是期盼生日的到来,而且总有一种我过生日的那天我就是山大王的错觉,毕竟可以吃香的,喝辣的。但是我也早就度过了无忧无虑的年纪,而生日这一天的意义也早就有所不同。长大后,每...
继续阅读 »

人比山高

大家好,我是寒草,一只工作两年的程序猿。农历七月初五,阳历八月七日是我的生日,小时候总是期盼生日的到来,而且总有一种我过生日的那天我就是山大王的错觉,毕竟可以吃香的,喝辣的。但是我也早就度过了无忧无虑的年纪,而生日这一天的意义也早就有所不同。

长大后,每当生日临近,总是思绪万千,或是思考过去,或是展望未来,年少时的悠哉不再,而思绪确是一年比一年多,有一位著名工程师曾经说过:

思绪和发量是负相关的。

虽然上面这个定律在我身上还没有印证,但是我不妨借生日这个难得的机会整理一下,收拾行囊,以便整装待发。

脚比路长

写文的当下,我正在思考,去年写文章的时候是什么样的心理:

  • 持续亢奋?
  • 充满希望?
  • 活力四射?

反正在我印象里,去年的我心中是充满希望和活力的,但是现在的我肯定和去年大不相同,可能是因为这一年出现了很多的变化:

  • 停不下来的裁员潮
  • 一座座楼(公司)塌了

听了很多悲伤的事,也经历了很多令人消沉的事,使得我并不会如去年那般纯粹的充满活力,也以更加理性和辩证的去看待问题,世界也不再非黑即白,这大概就是“成长”吧,在此推荐:《少有人走的路

在夜晚一个人走在回家的小路,我会思考很多很多事,但多数像泡沫一样飞散了,但是有一件事是我不会放弃的,也算是我长久以来的梦。

见下文

白山旭日

前一段时间,我开了一个新坑:程序猿之梦!星辰大海的前端建站之路「第一周」,没错,现在的我有一个理想,想创造一个自己的产品,我对她寄托了很多很多情感:

  • 我希望她可以承载更多的美好
  • 我希望她可以创造一股清流在网络社会流淌
  • 我希望她可以为社会提供向上的价值
  • ...

但是这个事情已经停摆了一个月了,毕竟我上个月大概上了 250 个小时的班,工作饱和度也基本来到了 150% ,整个人特别特别疲惫,掘金技术圈的群也是会经常 cue 到我的加班。

不要说我卷,我是很期望刘慈欣摸鱼写《三体》的那种生活的,但是生活所迫。

即使工作比较辛苦让我疲惫至极,我也会想把我的产品搞下去,我总是做不切实际的梦,我总是有一堆幻想,我总希望我从事的事业可以让世界更加美好,我总是焦虑,我总是烦躁,我总是有一种奇奇怪怪的理想主义,还有那么一丝丝的浪漫情怀

所以,无论如何,我欣赏我,我会是我。

黑水金光

前几天农历生日的时候,同事们陪我过了一个生日:

我作为一个东北人,也是第一次吃到铁锅炖大鹅(我怕不是个假东北人),还是很开心的,我从小到大还是第一次这么“正式”的过生日,近几天也陆陆续续的收到一些礼物:

  • 一把工学椅:这样我在家就可以更舒适的创作了
  • 一个木制密码箱:最关键还需要我自己拼(其实我没有什么特别值得放在密码箱里的东西)
  • 特利迦奥特曼:这肯定是很了解我的人才会送的礼物~
  • ...

羁绊越来越多了呢🌟还是要充满阳光的好好生活呀~

旅程

如果大家有耐心读到这里,一定有这样的一个疑问:“你这前四个标题是什么意思?”,其实只是我摘取了我母校校歌中的几句词,同时我也想到很多关于母校的指引我前进的故事:

  • 黄大年教授:振兴中华,乃我辈之责

已故,纪念文:「振兴中华,乃我辈之责」于75周年校庆使用 canvas 纪念黄大年老师

  • 张希校长:群居不倚,独立不惧
  • ...

最后,祝我自己 25 岁生日快乐 🎂

✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨

江山如此多娇,引无数英雄竞折腰。
惜秦皇汉武,略输文采;唐宗宋祖,稍逊风骚。
一代天骄,成吉思汗,只识弯弓射大雕。
俱往矣,数风流人物,还看今朝。

— 《沁园春.雪》

✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨

各位,感谢阅读,一起加油!也欢迎各位加我微信:hancao97 和我交流。

-寒草写于2022.08.06 🔥 To Be Continued-


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

深入理解与运用Android Jetpack ViewModel

在Android开发中,数据与界面的分离一直是一项重要的挑战。为了解决这个问题,Google推出了Android Jetpack组件之一的ViewModel。ViewModel是一种用于管理UI相关数据的架构组件,它能够帮助开发者实现优雅的数据驱动和生命周期管...
继续阅读 »

在Android开发中,数据与界面的分离一直是一项重要的挑战。为了解决这个问题,Google推出了Android Jetpack组件之一的ViewModel。ViewModel是一种用于管理UI相关数据的架构组件,它能够帮助开发者实现优雅的数据驱动和生命周期管理。本文将深入浅出地介绍ViewModel的使用和原理,带你一步步掌握这个强大的组件。

什么是ViewModel

ViewModel是Android Jetpack组件之一,它的主要目的是将UI控制器(如Activity和Fragment)与数据相关的业务逻辑分开,使得UI控制器能够专注于展示数据和响应用户交互,而数据的获取和处理则交由ViewModel来管理。这种分离能够使代码更加清晰、易于测试和维护。

ViewModel的原理

ViewModel的原理其实并不复杂。在设备配置发生变化(如屏幕旋转)导致Activity或Fragment重建时,ViewModel不会被销毁,而是保留在内存中。这样,UI控制器可以在重建后重新获取之前的ViewModel实例,并继续使用其中的数据,从而避免数据丢失和重复加载。

ViewModelStore和ViewModelStoreOwner

ViewModel的原理涉及两个核心概念:ViewModelStore和ViewModelStoreOwner。

ViewModelStore是一个存储ViewModel实例的容器,它的生命周期与UI控制器的生命周期关联。在UI控制器(Activity或Fragment)被销毁时,ViewModelStore会清理其中的ViewModel实例,避免内存泄漏。

ViewModelStoreOwner是拥有ViewModelStore的对象,通常是Activity或Fragment。ViewModelProvider通过ViewModelStoreOwner来获取ViewModelStore,并通过ViewModelStore来管理ViewModel的生命周期。

ViewModelProvider

ViewModelProvider是用于创建和获取ViewModel实例的工具类。它负责将ViewModel与ViewModelStoreOwner关联,并确保ViewModel在合适的时机被销毁。

在Activity中获取ViewModel实例:

viewModel = new ViewModelProvider(this).get(MyViewModel.class);

在Fragment中获取ViewModel实例:

viewModel = new ViewModelProvider(this).get(MyViewModel.class);

使用ViewModel

添加ViewModel依赖

首先,确保你的项目已经使用了AndroidX,并在build.gradle中添加ViewModel依赖:

dependencies {
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"
}

创建ViewModel

创建ViewModel非常简单,只需继承ViewModel类并在其中定义数据和相关操作。

public class MyViewModel extends ViewModel {
private MutableLiveData<String> data = new MutableLiveData<>();

public LiveData<String> getData() {
return data;
}

public void fetchData() {
// 模拟异步数据获取
new Handler().postDelayed(() -> {
data.setValue("Hello, ViewModel!");
}, 2000);
}
}

在UI控制器中使用ViewModel

在Activity或Fragment中获取ViewModel的实例,并观察数据变化:

viewModel = new ViewModelProvider(this).get(MyViewModel.class);
viewModel.getData().observe(this, data -> {
// 更新UI
textView.setText(data);
});

viewModel.fetchData(); // 触发数据获取操作

ViewModel与跨组件通信

ViewModel不仅仅用于在单个UI控制器内部共享数据,它还可以用于在不同UI控制器之间共享数据,实现跨组件通信。例如,一个Fragment中的数据可以通过ViewModel传递给Activity。

在Activity中共享数据:

sharedViewModel = new ViewModelProvider(this).get(SharedViewModel.class);
sharedViewModel.getData().observe(this, data -> {
// 更新UI
textView.setText(data);
});

在Fragment中共享数据:

sharedViewModel = new ViewModelProvider(requireActivity()).get(SharedViewModel.class);

注意:在跨组件通信时,需要使用同一个ViewModelProvider获取相同类型的ViewModel实例。在Activity中,使用this作为ViewModelProvider的参数,在Fragment中,使用requireActivity()作为参数。

ViewModel与SavedState

有时,我们可能希望在ViewModel中保存一些与UI控制器生命周期无关的数据,以便在重建时恢复状态。ViewModel提供了SavedState功能,它可以让我们在ViewModel中持久化保存数据。

示例代码:

public class MyViewModel extends ViewModel {
private SavedStateHandle savedStateHandle;

public MyViewModel(SavedStateHandle savedStateHandle) {
this.savedStateHandle = savedStateHandle;
}

public LiveData<String> getData() {
return savedStateHandle.getLiveData("data");
}

public void setData(String data) {
savedStateHandle.set("data", data);
}
}

使用SavedStateViewModelFactory创建带有SavedState功能的ViewModel:

public class MyActivity extends AppCompatActivity {
private MyViewModel viewModel;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

ViewModelProvider.Factory factory = new SavedStateViewModelFactory(getApplication(), this);
viewModel = new ViewModelProvider(this, factory).get(MyViewModel.class);

viewModel.getData().observe(this, data -> {
// 更新UI
textView.setText(data);
});

if (savedInstanceState == null) {
// 第一次创建时,触发数据获取操作
viewModel.fetchData();
}
}
}

ViewModel使用过程中的注意点

  • 不要在ViewModel中持有Context的引用,避免引发内存泄漏。
  • ViewModel应该只关注数据和业务逻辑,不应处理UI相关的操作。
  • 不要在ViewModel中保存大量数据,避免占用过多内存。
  • 当数据量较大或需要跨进程共享数据时,应该考虑使用其他解决方案,如Room数据库或SharedPreferences。

结论

通过本文的介绍,你已经了解了Android Jetpack ViewModel的使用与原理。ViewModel的出现极大地简化了Android开发中的数据管理和生命周期处理,使得应用更加健壮和高效。在实际开发中,合理使用ViewModel能够帮助你构建优雅、易维护的Android应用。


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

Android TextView中那些冷门好用的用法

介绍TextView 是 Android 开发中最常用的小部件之一。它用于在屏幕上显示文本。但是,TextView 有几个较少为人知的功能,对开发人员非常有用。在本博客文章中,我们将探讨其中的一些功能。自定义字体默认情况下,TextView 使用系统字体显示文...
继续阅读 »

介绍

TextView 是 Android 开发中最常用的小部件之一。它用于在屏幕上显示文本。但是,TextView 有几个较少为人知的功能,对开发人员非常有用。在本博客文章中,我们将探讨其中的一些功能。

自定义字体

默认情况下,TextView 使用系统字体显示文本。但其实我们也可以导入我们自己的字体文件在 TextView 中使用自定义字体。这可以通过将字体文件添加到资源文件夹(res/font 或者 assets)并在 TextView 上以编程方式设置来实现。

要使用自定义字体,我们需要下载字体文件(或者自己生成)并将其添加到资源文件夹中。然后,我们可以使用setTypeface()方法在TextView上以编程方式设置字体。我们还可以在XML中使用android:fontFamily属性设置字体。需要注意的是,fontFamily方式只能使用系统预设的字体并且仅对英文字符有效,如果TextView的文本内容是中文的话这个属性设置后将不会有任何效果。

以下是 Android TextView 自定义字体的代码示例:

  1. 将字体文件添加到 assets 或 res/font 文件夹中。
  2. 通过以下代码设置字体:
// 字体文件放到 assets 文件夹的情况
Typeface tf = Typeface.createFromAsset(getAssets(), "fonts/myfont.ttf");
TextView tv = findViewById(R.id.tv);
tv.setTypeface(tf);
// 字体文件放到 res/font 文件夹的情况, 需注意的是此方式在部分低于 Android 8.0 的设备上可能会存在兼容性问题
val tv = findViewById<TextView>(R.id.tv)
val typeface = ResourcesCompat.getFont(this, R.font.myfont)
tv.typeface = typeface

在上面的示例中,我们首先从 assets 文件夹中创建了一个新的 Typeface 对象。然后,我们使用 setTypeface() 方法将该对象设置为 TextView 的字体。

在上面的示例中,我们将字体文件命名为 “myfont.ttf”。我们可以将其替换为要使用的任何字体文件的名称。

自定义字体是 TextView 的强大功能之一,它可以帮助我们创建具有独特外观和感觉的应用程序。另外,我们也可以通过这种方法实现自定义图标的绘制。

AutoLink

AutoLink 可以自动检测文本中的模式并将其转换为可点击的链接。例如,如果 TextView 包含电子邮件地址或 URL,则 AutoLink 将识别它并使其可点击。此功能使开发人员无需手动创建文本中的可点击链接。

要在 TextView 上启用 AutoLink,您需要将autoLink属性设置为emailphoneweball。您还可以使用Linkify类设置自定义链接模式。

以下是一个Android TextView AutoLink代码使用示例:

<TextView
android:id="@+id/tv3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autoLink="web"
android:textColorLink="@android:color/holo_red_dark"
android:text="这是我的个人博客地址: http://www.geektang.cn" />

在上面的示例中,我们将 autoLink 属性设置为 web ,这意味着 TextView 将自动检测文本中的 URL 并将其转换为可点击的链接。我们还将 text 属性将文本设置为 这是我的个人博客地址: http://www.geektang.cn 。当用户单击链接时,它们将被带到 http://www.geektang.cn 网站。另外,我们也可以通过 textColorLink 属性将 Link 颜色为我们喜欢的颜色。

AutoLink是一个非常有用的功能,它可以帮助您更轻松地创建可交互的文本。

对齐模式

对齐模式允许您通过在单词之间添加空格将文本对齐到左右边距,这使得文本更易读且视觉上更具吸引力。您可以将对齐模式属性设置为 inter_word 或 inter_character

要使用对齐模式功能,您需要在 TextView 上设置 justificationMode 属性。但是,此功能仅适用于运行 Android 8.0(API 级别 26)或更高版本的设备。

以下是对齐模式功能的代码示例:

<TextView
android:id="@+id/text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="This is some sample text that will be justified."
android:justificationMode="inter_word"/>

在上面的示例中,我们将 justificationMode 属性设置为 inter_word 。这意味着 TextView 将在单词之间添加空格,以便将文本对齐到左右边距。

以下是对齐模式功能的显示效果示例:

同样一段文本,上面的设置 justificationMode 为 inter_word ,是不是看起来会比下面的好看一些呢?这个属性一般用于多行英文文本,如果只有一行文本或者文本内容是纯中文字符的话,不会有任何效果。


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

六种常见的排序算法

排序算法数组任意两值交换创建临时变量进行交换private void swap(int[] nums, int idx1, int idx2) { int temp = nums[idx1]; nums[idx1] = nums[idx2]; ...
继续阅读 »

排序算法

数组任意两值交换

创建临时变量进行交换

private void swap(int[] nums, int idx1, int idx2) {
int temp = nums[idx1];
nums[idx1] = nums[idx2];
nums[idx2] = temp;
}

冒泡排序

思路:每次对 [0, j] 进行排序,把该区间中最大的值放到这个区间的最右边

时间复杂度:O(n2)

空间复杂度:O(1)

/**
* 冒泡排序
*
* @param nums 数组
*/
public void bubbleSort(int[] nums) {
for (int i = 0; i < nums.length - 1; i++) {
for (int j = 0; j < nums.length - 1 - i; j++) {
if (nums[j] > nums[j + 1]) {
swap(nums, j, j + 1);
}
}
}
}

选择排序

思路:对于区间 [j, nums.length] (i <= j <= nums.length),每次在这个区间中选择最小的值,插入到 nums[i] 中,即每次选择一个最小的值插入到 nums[i] 中;

时间复杂度:O(n2)

空间复杂度:O(1)

/**
* 插入排序
*
* @param nums 数组
*/
public void insertSort(int[] nums) {
for (int i = 0; i < nums.length; i++) {
int idx = 0;
int min = Integer.MAX_VALUE;
for (int j = i; j < nums.length; j++) {
if (nums[j] < min) {
min = nums[j];
idx = j;
}
}
swap(nums, i, idx);
}
}

插入排序

思路:对于区间 [0, j] ,在 [i, length-1] 的区间中每次使用下标的 i 的数( j <= i ),插入到区间 [0, j] 中,保证 [0, j] 是有序的

时间复杂度:O(n2)

空间复杂度:O(1)

/**
* 插入排序
*
* @param nums 数组
*/
public void insertSort(int[] nums) {
for (int i = 1; i < nums.length; i++) {
int temp = nums[i];
int j = i;
for (; j > 0; j--) {
if (temp < nums[j - 1]) {
nums[j] = nums[j - 1];
} else {
break;
}
}
nums[j] = temp;
}
}

快速排序

思路:

  1. 对于单次的排序 partition() ,定义一个标志 part ,凡是小于该值的都放左边,大于该值的都放右边,最后把该值放到中间,并返回中间的下标 partition ,这里实现的关键是:存在一个指针 j 始终指向左边区间的最靠右的值,若 j + 1,则去到了右区间;
  2. 将数组以 partition 为中点,将数组分成两份,每一份继续进行 partition()

时间复杂度:O(nlogn)

空间复杂度:O(logn)

/**
* 递归函数
*
* @param nums 数组
* @param left 左
* @param right 右
*/
public void quickSort(int[] nums, int left, int right) {

if (left >= right) {
return;
}

int partition = partition(nums, left, right);

quickSort(nums, left, partition - 1);
quickSort(nums, partition + 1, right);
}

/**
* 将小于某个元素的值放到左边,大于某个元素的值放到右边
*
* @param nums 数组
* @param left 左
* @param right 右
* @return 结果
*/
public int partition(int[] nums, int left, int right) {
// 以数组的左边的值作为标记
int part = nums[left];
int i = left + 1;
// j 始终指向左边区间小于或等于 part 的最靠右的值
int j = left;

for (; i < nums.length; i++) {
if (nums[i] < part) {
j++;
swap(nums, i, j);
}
}

swap(nums, j, left);
return j;
}

三向切分快速排序

适用于有重复内容的排序

思路:

  1. 分成三个区间,小于 pivot左区间),等于 pivot中区间),大于 pivot右区间);
  2. 左区间的 lt 指针永远指向该区间的最右的位置,右区间的指针永远指向该区间的最左的位置;
  3. 对于中区间,不断移动游标 i 的位置即可;

时间复杂度:O(nlogn)

空间复杂度:O(logn)

public void threeQuickSort(int[] nums, int left, int right) {
if (left >= right) {
return;
}
int pivot = nums[left];

// [left + 1, lt] 小于 pivot
// [lt + 1, i) 等于 pivot
// [gt, right] 大于 pivot
int lt = left; // 左区间的指针
int gt = right + 1; // 右区间的指针
int i = left + 1;

while (i < gt) {
if (nums[i] < pivot) {
lt++;
swap(nums, i, lt);
i++;
} else if (nums[i] == pivot) {
i++;
} else {
gt++;
swap(nums, gt, i);
}
}
swap(nums, left, lt);
threeQuickSort(nums, left, lt - 1);
threeQuickSort(nums, gt, right);
}

归并排序

思路:

  1. 分隔:先将数组不断分割,直到分割到区间 [left, right] 内只有一个值
  2. 合并:将分隔后的数组不断向上合并,利用临时数组 temp[] 存储 原来 nums 数组 [left,right] 区间的值,然后分别从 temp 数组中 [left, mid] 和 [mid + 1, right] 区间分别取出最小的值,放入 nums 数组对应的位置即可;
  3. 代码的主要难点是 nums 数组 和 temp 数组的下标对应关系
    1. 对应 left,即 [left, mid] 的起点,i 在 temp 数组起始值为 0
    2. 对应 mid + 1,即 [mid + 1, right] 的起点,j 在 temp 数组起始值为 mid - left + 1

时间复杂度:O(nlogn)

空间复杂度:O(n + logn) => O(n):临时的数组和递归时压入栈的数据占用的空间

public void mergeSort(int[] nums, int left, int right) {

if (left >= right) {
return;
}
int mid = left + (right - left) / 2;

mergeSort(nums, left, mid);
mergeSort(nums, mid + 1, right);

merge(nums, left, mid, right);
}

/**
* 合并数组
*
* @param nums 数组
* @param left 左端点
* @param mid 中点
* @param right 右端点
*/
private void merge(int[] nums, int left, int mid, int right) {
int length = right - left + 1;
int[] temp = new int[length];

for (int i = 0; i < length; i++) {
temp[i] = nums[left + i];
}

// i j 为 temp 数组的下标
// 关键是找到 i j 与 原数组 nums 下标的对应关系
int i = 0;
int j = mid - left + 1;
for (int k = 0; k < length; k++) {
if (i == mid - left + 1) {
nums[k + left] = temp[j];
j++;
} else if (j == right - left + 1) {
nums[k + left] = temp[i];
i++;
} else if (temp[i] <= temp[j]) {
nums[k + left] = temp[i];
i++;
} else {
nums[k + left] = temp[j];
j++;
}
}
}

堆排序

思路:

  1. 读者首先搞懂什么是  ,代码示例中介绍的 大顶堆,这里不作过多介绍;
  2. 首先初始化一个大顶堆,每个大顶堆的根节点是最大值;
  3. 不断把根节点的值与数组最后一个值交换,然后长度减 1 再次进行大顶堆的整理操作;

时间复杂度:O(nlogn),每次整理的时间复杂度是 logn,要进行 n 次

空间复杂度:O(1)

/**
* 堆排序
*
* @param nums 数组
*/
public void heapSort(int[] nums) {
initMaxHeap(nums);
int len = nums.length - 1;
while (len > 0) {
swap(nums, 0, len);
len--;
siftDown(nums, 0, len);
}
}

/**
* 初始化为大顶堆
*
* @param nums 数组
*/
public void initMaxHeap(int[] nums) {
int len = nums.length;
for (int i = (len - 1) / 2; i >= 0; i--) {
siftDown(nums, i, len - 1);
}
}

/**
* 向下整理
* @param nums 数组
* @param k 某个节点
* @param len 数组长度
*/
public void siftDown(int[] nums, int k, int len) {

while (k * 2 + 1 <= len) {
int j = k * 2 + 1;
if (j + 1 <= len && nums[j] < nums[j + 1]) {
j++;
}
if (nums[k] > nums[j]) {
break;
}
swap(nums, k, j);
k = j;
}
}

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

Java 理论知识整理

过滤器数据准备DAO 层 UserDao、AccountDao、BookDao、EquipmentDaopublic interface UserDao { public void save(); }@Component("userDao") public ...
继续阅读 »

过滤器

数据准备
  • DAO 层 UserDao、AccountDao、BookDao、EquipmentDao

    public interface UserDao {
    public void save();
    }
    @Component("userDao")
    public class UserDaoImpl implements UserDao {
    public void save() {
    System.out.println("user dao running...");
    }

    }
  • Service 业务层

    public interface UserService {
    public void save();
    }
    @Service("userService")
    public class UserServiceImpl implements UserService {
    @Autowired
    private UserDao userDao;//...........BookDao等

    public void save() {
    System.out.println("user service running...");
    userDao.save();
    }
    }

过滤器

名称:TypeFilter

类型:接口

作用:自定义类型过滤器

示例:

  • config / filter / MyTypeFilter

    public class MyTypeFilter implements TypeFilter {
    @Override
    /**
    * metadataReader:读取到的当前正在扫描的类的信息
    * metadataReaderFactory:可以获取到任何其他类的信息
    */
    //加载的类满足要求,匹配成功
    public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
    //获取当前类注解的信息
    AnnotationMetadata am = metadataReader.getAnnotationMetadata();
    //获取当前正在扫描的类的类信息
    ClassMetadata classMetadata = metadataReader.getClassMetadata();
    //获取当前类资源(类的路径)
    Resource resource = metadataReader.getResource();


    //通过类的元数据获取类的名称
    String className = classMetadata.getClassName();
    //如果加载的类名满足过滤器要求,返回匹配成功
    if(className.equals("service.impl.UserServiceImpl")){
    //返回true表示匹配成功,返回false表示匹配失败。此处仅确认匹配结果,不会确认是排除还是加入,排除/加入由配置项决定,与此处无关
    return true;
    }
    return false;
    }
    }
  • SpringConfig

    @Configuration
    //设置排除bean,排除的规则是自定义规则(FilterType.CUSTOM),具体的规则定义为MyTypeFilter
    @ComponentScan(
    value = {"dao","service"},
    excludeFilters = @ComponentScan.Filter(
    type= FilterType.CUSTOM,
    classes = MyTypeFilter.class
    )
    )
    public class SpringConfig {
    }

导入器

bean 只有通过配置才可以进入 Spring 容器,被 Spring 加载并控制

  • 配置 bean 的方式如下:

    • XML 文件中使用 标签配置
    • 使用 @Component 及衍生注解配置

导入器可以快速高效导入大量 bean,替代 @Import({a.class,b.class}),无需在每个类上添加 @Bean

名称: ImportSelector

类型:接口

作用:自定义bean导入器

  • selector / MyImportSelector

    public class MyImportSelector implements ImportSelector{
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
    // 1.编程形式加载一个类
    // return new String[]{"dao.impl.BookDaoImpl"};

    // 2.加载import.properties文件中的单个类名
    // ResourceBundle bundle = ResourceBundle.getBundle("import");
    // String className = bundle.getString("className");

    // 3.加载import.properties文件中的多个类名
    ResourceBundle bundle = ResourceBundle.getBundle("import");
    String className = bundle.getString("className");
    return className.split(",");
    }
    }
  • import.properties

    #2.加载import.properties文件中的单个类名
    #className=dao.impl.BookDaoImpl

    #3.加载import.properties文件中的多个类名
    #className=dao.impl.BookDaoImpl,dao.impl.AccountDaoImpl

    #4.导入包中的所有类
    path=dao.impl.*
  • SpringConfig

    @Configuration
    @ComponentScan({"dao","service"})
    @Import(MyImportSelector.class)
    public class SpringConfig {
    }

注册器

可以取代 ComponentScan 扫描器

名称:ImportBeanDefinitionRegistrar

类型:接口

作用:自定义 bean 定义注册器

  • registrar / MyImportBeanDefinitionRegistrar

    public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    /**
    * AnnotationMetadata:当前类的注解信息
    * BeanDefinitionRegistry:BeanDefinition注册类,把所有需要添加到容器中的bean调用registerBeanDefinition手工注册进来
    */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    //自定义注册器
    //1.开启类路径bean定义扫描器,需要参数bean定义注册器BeanDefinitionRegistry,需要制定是否使用默认类型过滤器
    ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(registry,false);
    //2.添加包含性加载类型过滤器(可选,也可以设置为排除性加载类型过滤器)
    scanner.addIncludeFilter(new TypeFilter() {
    @Override
    public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
    //所有匹配全部成功,此处应该添加实际的业务判定条件
    return true;
    }
    });
    //设置扫描路径
    scanner.addExcludeFilter(tf);//排除
    scanner.scan("dao","service");
    }
    }
  • SpringConfig

    @Configuration
    @Import(MyImportBeanDefinitionRegistrar.class)
    public class SpringConfig {
    }

处理器

通过创建类继承相应的处理器的接口,重写后置处理的方法,来实现拦截 Bean 的生命周期来实现自己自定义的逻辑

BeanPostProcessor:bean 后置处理器,bean 创建对象初始化前后进行拦截工作的

BeanFactoryPostProcessor:beanFactory 的后置处理器

  •  加载时机:在 BeanFactory 初始化之后调用,来定制和修改 BeanFactory 的内容;所有的 bean 定义已经保存加载到 beanFactory,但是 bean 的实例还未创建
  •   执行流程:
    • ioc 容器创建对象

    • invokeBeanFactoryPostProcessors(beanFactory):执行 BeanFactoryPostProcessor

      • 在 BeanFactory 中找到所有类型是 BeanFactoryPostProcessor 的组件,并执行它们的方法
      • 在初始化创建其他组件前面执行

BeanDefinitionRegistryPostProcessor:

  • 加载时机:在所有 bean 定义信息将要被加载,但是 bean 实例还未创建,优先于 BeanFactoryPostProcessor 执行;利用 BeanDefinitionRegistryPostProcessor 给容器中再额外添加一些组件

  • 执行流程:

    • ioc 容器创建对象

    • refresh() → invokeBeanFactoryPostProcessors(beanFactory)

    • 从容器中获取到所有的 BeanDefinitionRegistryPostProcessor 组件

      • 依次触发所有的 postProcessBeanDefinitionRegistry() 方法
      • 再来触发 postProcessBeanFactory() 方法

监听器

基本概述

ApplicationListener:监听容器中发布的事件,完成事件驱动模型开发

public interface ApplicationListener<E extends ApplicationEvent>

所以监听 ApplicationEvent 及其下面的子事件

应用监听器步骤:

  • 写一个监听器(ApplicationListener实现类)来监听某个事件(ApplicationEvent及其子类)

  • 把监听器加入到容器 @Component

  • 只要容器中有相关事件的发布,就能监听到这个事件;

    •  ContextRefreshedEvent:容器刷新完成(所有 bean 都完全创建)会发布这个事件
    •  ContextClosedEvent:关闭容器会发布这个事件
  • 发布一个事件:applicationContext.publishEvent()

@Component
public class MyApplicationListener implements ApplicationListener<ApplicationEvent> {
//当容器中发布此事件以后,方法触发
@Override
public void onApplicationEvent(ApplicationEvent event) {
System.out.println("收到事件:" + event);
}
}

实现原理

ContextRefreshedEvent 事件:

  • 容器初始化过程中执行 initApplicationEventMulticaster():初始化事件多播器

    • 先去容器中查询 id = applicationEventMulticaster 的组件,有直接返回
    • 没有就执行 this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory) 并且加入到容器中
    • 以后在其他组件要派发事件,自动注入这个 applicationEventMulticaster
  • 容器初始化过程执行 registerListeners() 注册监听器

    • 从容器中获取所有监听器:getBeanNamesForType(ApplicationListener.class, true, false)
    • 将 listener 注册到 ApplicationEventMulticaster
  • 容器刷新完成:finishRefresh() → publishEvent(new ContextRefreshedEvent(this))

    发布 ContextRefreshedEvent 事件:

    • 获取事件的多播器(派发器):getApplicationEventMulticaster()

    • multicastEvent 派发事件

      • 获取到所有的 ApplicationListener

      • 遍历 ApplicationListener

        • 如果有 Executor,可以使用 Executor 异步派发 Executor executor = getTaskExecutor()
        • 没有就同步执行 listener 方法 invokeListener(listener, event),拿到 listener 回调 onApplicationEvent

容器关闭会发布 ContextClosedEvent


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

frp内网穿透

frp
Frp是什么简单地说,frp就是一个反向代理软件,它体积轻量但功能很强大,可以使处于内网或防火墙后的设备对外界提供服务,它支持HTTP、TCP、UDP等众多协议。服务端配置SSH连接到VPS之后运行如下命令查看处理器架构,根据架构下载不同版本的frp运行如下命...
继续阅读 »

Frp是什么

简单地说,frp就是一个反向代理软件,它体积轻量但功能很强大,可以使处于内网或防火墙后的设备对外界提供服务,它支持HTTP、TCP、UDP等众多协议。

服务端配置

SSH连接到VPS之后运行如下命令查看处理器架构,根据架构下载不同版本的frp
运行如下命令,根据架构不同,选择相应版本并进行下载

wget https://github.com/fatedier/frp/releases/download/v0.22.0/frp_0.22.0_linux_amd64.tar.gz

然后解压缩

tar -zxvf frp_0.22.0_linux_amd64.tar.gz

服务端的配置我们只需要关注如下几个文件

  • frps
  • frps.ini

这两个文件(s结尾代表server)分别是服务端程序和服务端配置文件
然后修改frps.ini文件

[common]
bind_port = 49273
vhost_http_port = 9001
token = Er3@SGTwHtPl+jMRD0/f3QH/A
  • “bind_port”表示用于客户端和服务端连接的端口,这个端口号我们之后在配置客户端的时候要用到。
  • “vhost_http_port”和“vhost_https_port”用于反向代理HTTP主机时使用。
  • “token”是用于客户端和服务端连接的口令,请自行设置并记录,稍后会用到。

编辑完成后保存(vim保存如果不会请自行搜索)

客户端配置

frp的客户端就是我们想要真正进行访问的那台设备。
同样地,根据客户端设备的情况选择相应的frp程序进行下载,将“frp_0.22.0_windows_amd64.zip”解压
客户端的配置我们只需要关注如下几个文件

  • frpc

  • frpc.ini

    这两个文件(c结尾代表client)分别是客户端程序和客户端配置文件。
    然后修改frpc.ini文件

[common]
server_addr = 52.80.184.170
server_port = 49273
token = Er3@SGTwHtPl+jMRD0/f3QH/A

[sentry]
type = http
local_ip = 10.10.75.137
local_port = 9001
custom_domains = 172.31.20.248

其中common字段下的三项即为服务端的设置。

  • server_addr”为服务端IP地址,填入即可。
  • server_port”为服务器端口,填入你设置的端口号即可。
  • token”是你在服务器上设置的连接口令,原样填入即可。

自定义规则

上面frpc.ini的sentry字段是自己定义的规则,自定义端口对应时格式如下。

  • [xxx]”表示一个规则名称,自己定义,便于查询即可。
  • type”表示转发的协议类型,有TCP和UDP等选项可以选择,如有需要请自行查询frp手册。
  • local_ip”是本地应用的IP地址,按照实际应用工作在本机的IP地址填写即可。
  • local_port”是本地应用的端口号,按照实际应用工作在本机的端口号填写即可。
  • custom_domains”服务端IP地址或域名,可以直接使用服务端ip或者生成一个内网ip。

后台运行脚本

运行服务端

./frpc -c frps.ini

运行客户端

./frpc -c frpc.ini

至此,我们的frp仅运行在前台,如果Ctrl+C停止或者关闭SSH窗口后,frp均会停止运行,因而我们使用 nohup命令将其运行在后台。
服务端创建start.sh脚本文件以及frps.log日志文件 编辑start.sh

nohup ./frps -c frps.ini &> frps.log &

客户端创建start.sh脚本文件以及frpc.log日志文件 编辑start.sh

nohup ./frpc -c frpc.ini &> frpc.log &

客户端和服务端执行start.sh脚本

./stash

查看log日志

tail -f frps.log
tail -f frpc.log

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

一篇文章学会正则表达式(Kotlin举例)

一篇文章学会正则表达式(Kotlin举例)正则表达式是一种用来匹配字符串的工具,它可以在文本中查找特定的模式,从而实现对文本的处理和分析。在很多编程语言中,正则表达式都是非常重要的一部分。了解正则表达式在学习正则表达式之前,我们需要先了解一些基本概念。正则表达...
继续阅读 »

一篇文章学会正则表达式(Kotlin举例)

正则表达式是一种用来匹配字符串的工具,它可以在文本中查找特定的模式,从而实现对文本的处理和分析。在很多编程语言中,正则表达式都是非常重要的一部分。

了解正则表达式

在学习正则表达式之前,我们需要先了解一些基本概念。正则表达式由一系列字符和特殊字符组成,用来匹配字符串中的模式。例如,我们可以使用正则表达式来匹配一个电话号码、一个电子邮件地址或者一个网址。正则表达式中的一些常用特殊字符包括:

  • .:匹配任意一个字符。
  • *:匹配前面的字符零次或多次。
  • +:匹配前面的字符一次或多次。
  • ?:匹配前面的字符零次或一次。
  • |:表示或的关系,匹配两边任意一边的内容。
  • ():表示分组,可以将多个字符组合成一个整体。
  • ^:匹配字符串的开头。
  • $:匹配字符串的结尾。
  • {n}:匹配前面的字符恰好出现 n 次。
  • {n,}:匹配前面的字符

正则表达式的基本语法

正则表达式的基本语法包括两个部分:模式和修饰符。模式是用来匹配字符串的规则,而修饰符则用来控制匹配的方式。

在 Kotlin 中,我们可以使用 Regex 类来表示一个正则表达式。例如,下面的代码定义了一个简单的正则表达式,用来匹配一个由数字组成的字符串:

val pattern = Regex("\\d+")

在这个正则表达式中,\d 表示匹配一个数字,+ 表示匹配前面的字符一次或多次。注意,在 Kotlin 中,我们需要使用 \\ 来表示 \ 字符,因为 \ 在字符串中有特殊的含义。

接下来,我们可以使用 matchEntire 函数来检查一个字符串是否符合这个正则表达式:

val input = "12345"
if (pattern.matchEntire(input) != null) {
println("Match!")
} else {
println("No match.")
}

这个代码会输出 Match!,因为输入的字符串符合正则表达式的规则。

常见的正则表达式的高级用法

除了基本的语法之外,正则表达式还有很多高级用法,可以实现更加复杂的匹配和替换操作。下面是一些常用的高级用法:

1. 捕获组

捕获组是指用 () 包围起来的一部分正则表达式,可以将匹配到的内容单独提取出来。例如,下面的代码定义了一个正则表达式,用来匹配一个由姓和名组成的字符串:

val pattern = Regex("(\\w+)\\s+(\\w+)")
val input = "John Smith"
val matchResult = pattern.matchEntire(input)
if (matchResult != null) {
val firstName = matchResult.groupValues[1]
val lastName = matchResult.groupValues[2]
println("First name: $firstName")
println("Last name: $lastName")
}

在这个代码中,\\w+ 表示匹配一个或多个字母、数字或下划线,\\s+ 表示匹配一个或多个空格。groupValues属性可以返回一个列表,其中包含了所有捕获组的内容。在这个例子中,groupValues[1] 表示第一个捕获组的内容,即姓,groupValues[2] 表示第二个捕获组的内容,即名。

2. 非捕获组

非捕获组是指用 (?:) 包围起来的一部分正则表达式,它和普通的捕获组的区别在于,非捕获组匹配到的内容不会单独提取出来。例如,下面的代码定义了一个正则表达式,用来匹配一个由单词和空格组成的字符串:

val pattern = Regex("(?:\\w+\\s+)+\\w+")
val input = "one two three four"
val matchResult = pattern.matchEntire(input)
if (matchResult != null) {
println("Match!")
} else {
println("No match.")
}

在这个代码中,(?:\\w+\\s+)+ 表示匹配一个或多个单词和空格组成的片段,\\w+ 表示匹配一个或多个字母、数字或下划线,\\s+ 表示匹配一个或多个空格。注意,这个正则表达式并没有使用捕获组,因此 matchResult.groupValues 的结果是一个空列表。

3. 零宽断言

零宽断言是指用 (?=) 或 (?!) 包围起来的一部分正则表达式,它可以在匹配的时候不消耗任何字符。例如,下面的代码定义了一个正则表达式,用来匹配一个以 http 或 https 开头的 URL:

val pattern = Regex("(?=http|https)\\w+")
val input = "https://www.google.com"
val matchResult = pattern.find(input)
if (matchResult != null) {
println("Match: ${matchResult.value}")
} else {
println("No match.")
}

在这个代码中,(?=http|https) 表示匹配一个以 http 或 https 开头的字符串,但是不消耗任何字符。find 函数可以在输入字符串中查找第一个匹配的子串,返回一个 MatchResult? 类型的结果。

总结

本文介绍了正则表达式的基本概念和语法,以及一些常用的高级用法。在实际的编程中,正则表达式是一种非常有用的工具,可以帮助我们快速地处理和分析文本数据。在 Kotlin 中,我们可以使用 Regex 类来表示和操作正则表达式,同时还可以使用一些高级用法来实现更加复杂的匹配和替换操作。


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

ThreadLocal的实现原理,ThreadLocal为什么使用弱引用

前言本文将讲述ThreadLocal的实现原理,还有## ThreadLocal为什么使用弱引用。ThreadLocalThreadLocal 是 Java 中的一个类,用于在多线程环境下为每个线程提供独立的变量副本。它通常用于解决多线程并发访问共享变量时的线...
继续阅读 »

前言

本文将讲述ThreadLocal的实现原理,还有## ThreadLocal为什么使用弱引用。

ThreadLocal

ThreadLocal 是 Java 中的一个类,用于在多线程环境下为每个线程提供独立的变量副本。它通常用于解决多线程并发访问共享变量时的线程安全性问题。

ThreadLocal 的工作原理是每个线程内部维护一个 ThreadLocalMap 对象,该对象用于存储每个线程的变量副本。当通过 ThreadLocal 对象获取变量时,它会首先检查当前线程是否已经创建了该变量的副本,如果有,则直接返回副本;如果没有,则通过初始化方法创建一个新的副本,并将其保存在当前线程的 ThreadLocalMap 中

使用 ThreadLocal 时,每个线程都可以独立地访问和修改自己的变量副本,而不会影响其他线程的副本。这使得在多线程环境中共享变量变得更加安全和可靠。

需要注意的是,使用 ThreadLocal 时要注意及时清理不再使用的变量副本,以避免内存泄漏问题。可以通过调用 remove() 方法来清除当前线程的变量副本。

源码解释

set方法源码

// ThreadLocal的set方法,value是要保存的值
public void set(T value) {
   // 得到当前线程对象
   Thread t = Thread.currentThread();
   // 得到当前线程对象关联的ThreadLocalMap对象
   ThreadLocalMap map = getMap(t);
  // 得到map对象就保存值,键为当前ThreadLocal对象
   // 如果没有map对象就创建一个map对象,保存值
   if (map != null)
       map.set(this, value);
   else
       createMap(t, value);
}
// 得到当前线程关联的ThreadLocalMap对象
ThreadLocalMap getMap(Thread t) {
      return t.threadLocals;
}
// 创建一个ThreadLocalMap对象,赋给当前线程的threadLocals属性,并且存入值
void createMap(Thread t, T firstValue) {
      t.threadLocals = new ThreadLocalMap(this, firstValue);
}
private void set(ThreadLocal<?> key, Object value) {
   Entry[] tab = table;
   int len = tab.length;
   // 通过key计算在tab数组中的槽位i
   int i = key.threadLocalHashCode & (len-1);
// 拿到槽位上的Entry对象,如果不为null,则进入循环,如果为null则表示可以直接加入该槽位
   // e = tab[i = nextIndex(i, len)])取出下一个槽位的Entry实体,如果为null,则表示可以直接添加进该槽位
   for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
       // 拿到与当前Entry有关联的ThreadLocal对象
       ThreadLocal<?> k = e.get();
   // 如果k与当前要保存值的key相等,则替换掉value,相当于修改key的值
       if (k == key) {
           e.value = value;
           return;
      }
// 检查当前节点的ThreadLocal如果为null,表示ThreadLocal已经被gc回收,则调用 replaceStaleEntry() 方法来替换陈旧的 Entry,将新的 ThreadLocal 和值插入到数组中的索引位置 i 处,并返回。
       if (k == null) {
           replaceStaleEntry(key, value, i);
           return;
      }
  }
// 创建一个Entry对象,加入i槽位
   tab[i] = new Entry(key, value);
   // 记录Entry对象个数
   int sz = ++size;
   // cleanSomeSlots清理陈旧的Entry,清理完后如果大于阈值,则调用rehash扩容数组
   if (!cleanSomeSlots(i, sz) && sz >= threshold)
       rehash();
}

get方法源码

public T get() {
   // 获取当前线程对象
   Thread t = Thread.currentThread();
   // 得到当前线程关联的ThreadLocalMap对象
   ThreadLocalMap map = getMap(t);
   if (map != null) {
       // 通过key获取到Entry对象
       ThreadLocalMap.Entry e = map.getEntry(this);
       // Entry不为空,则直接获取值返回结果
       if (e != null) {
           @SuppressWarnings("unchecked")
           T result = (T)e.value;
           return result;
      }
  }
   // 如果map为null,或者Entry为null,则返回一个初始化值
   return setInitialValue();
}
private T setInitialValue() {
  // 如果是在调用构造器初始化的ThreadLocal对象,该方法直接返回null
  // 如果是调用的静态方法withInitial,则返回你指定的一个初始化则
  // 并且还会把该初始化的值保存进ThreadLocalMap
       T value = initialValue();
       Thread t = Thread.currentThread();
       ThreadLocalMap map = getMap(t);
       if (map != null)
           map.set(this, value);
       else
           createMap(t, value);
       return value;
}
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
  // SuppliedThreadLocal是ThreadLocal的子类,重写了initialValue方法,通过传入一个Supplier,指定初始化值
       return new SuppliedThreadLocal<>(supplier);
}
private Entry getEntry(ThreadLocal<?> key) {
   // 计算当前key的落脚点
   int i = key.threadLocalHashCode & (table.length - 1);
   // 取出落脚点的Entry对象
   Entry e = table[i];
   // 如果e不为空,并且跟e关联的ThreadLocal对象等于当前的key,则返回当前e对象
   if (e != null && e.get() == key)
       return e;
   // 否则进入getEntryAfterMiss
   else
       return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
   Entry[] tab = table;
   int len = tab.length;
   // 如果e为null,则直接返回null,表示当前key并没有数据
   while (e != null) {
  // 取出与e关联的ThreadLocal对象
  ThreadLocal<?> k = e.get();
       // 判断k是否等于当前的ThreadLocal对象
       if (k == key)
           return e;
       // 当前k是否等于null,为null表示被gc垃圾回收,就清理旧的Entry对象
       if (k == null)
           expungeStaleEntry(i);
       else
           // 否则k不为null,取出下一个槽位,接着循环
           i = nextIndex(i, len);
       e = tab[i];
  }
   return null;
}

总结

可以看出实际保存线程局部变量的是ThreadLocalMap对象,每个线程都有一个这样的对象,保存的是键值对,键为当前的ThreadLocal对象,ThreadLocal对象一般设置为静态,非静态只会造成对象的冗余,因为ThreadLocalMap的键只能是当前ThreadLocal对象,所以只能保存一个键值对,如果要保存多个键值对,可以定义多个ThreadLocal对象作为不同的键,这样获取到的还是与线程有关联的ThreadLocalMap对象,而ThreadLocalMap的键是当前的ThreadLocal对象,多少个该对象,那就可以保存多少个值

强软弱虚四大引用

在Java中,引用是用于引用对象的一个机制,它允许我们通过引用变量来操作和访问对象。在Java中,主要有以下几种引用类型:

  1. 强引用(Strong Reference):这是最常见的引用类型。当我们使用 new 关键字创建对象时,默认就是使用强引用。如果一个对象具有强引用,即存在一个强引用变量引用它,那么垃圾回收器就不会回收该对象。只有当对象没有任何强引用时,才会被认为是不再需要的,可以被垃圾回收
  2. 软引用(Soft Reference):软引用用于描述还有用但非必需的对象。在内存不足时,垃圾回收器可能会选择回收软引用对象。使用软引用可以实现一些缓存功能,在内存不足时释放缓存中的对象,从而避免 OutOfMemoryError。可以使用 SoftReference 类来创建软引用。
  3. 弱引用(Weak Reference):弱引用的生命周期更短暂,只要垃圾回收器发现一个对象只有弱引用与之关联,就会立即回收该对象。弱引用通常用于实现一些特定的缓存或关联数据结构,当对象的强引用被释放后,关联的弱引用对象也会被自动清除。可以使用 WeakReference 类来创建弱引用。
  4. 虚引用(Phantom Reference):虚引用是最弱的引用类型,几乎没有实际的使用场景。虚引用的主要作用是跟踪对象被垃圾回收的状态。当垃圾回收器决定回收一个对象时,如果该对象有虚引用,将会在对象被回收之前,将虚引用加入到与之关联的引用队列中,供应用程序获取对象回收的状态信息。

在内存管理方面,软引用和弱引用都可以用于解决一些特定的内存问题,例如缓存管理或对象关联。它们对于临时性或可替代性对象的管理非常有用,可以在内存紧张时进行垃圾回收,从而提高系统的性能和可用性。然而,需要注意的是,对于软引用和弱引用对象,程序应该在使用时进行必要的判空和恢复处理,以避免 NullPointerException 和其他相关问题。

ThreadLocal为什么使用弱引用

ThreadLocal 使用弱引用的主要原因是为了避免内存泄漏问题。

当使用强引用持有 ThreadLocal 对象时,只有线程销毁或显式地调用 remove() 方法时,Entry 才会被释放。这可能导致在多线程环境下使用线程池时,即使线程已经使用结束处于空闲状态,对应的 Entry 仍然会存在于 ThreadLocalMap 中,导致无法回收相关资源,从而造成内存泄漏。

使用弱引用作为 ThreadLocal 的键(key),可以解决这个问题。弱引用在垃圾回收时只要发现只有弱引用指向,则会被直接回收。因此,当线程结束且对应的 ThreadLocal 对象只有弱引用存在时,垃圾回收器会自动清理该弱引用,进而清理 ThreadLocalMap 中对应的 Entry。这样可以避免内存泄漏问题。

需要注意的是,尽管 ThreadLocalMap 使用了弱引用来避免内存泄漏问题,但仍然需要在使用 ThreadLocal 后调用 remove() 方法,以确保及时清理 ThreadLocal 对象和对应的值。这是因为弱引用的回收时机不确定,不能完全依赖垃圾回收器的工作。

当我们应该请求进来分配一个线程处理请求,此时ThreadLocal对象就会被创建,并且是一个强引用,当第一次把值存入时ThreadLocal时,就会通过Thread拿到或者创建一个ThreadLocalMap对象,并且存入我们的数据,此时ThreadLocal作为键就会被放入弱引用中,此时就算发送垃圾回收也不会回收ThreadLocal因为有一个强引用指向,但是一旦我们的请求执行完毕返回,线程处于空闲状态时,这个强引用就没了,此时就剩下一个弱引用,这个时候发生垃圾回收就ThreadLocal就会被收回。


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

Kotlin | 高阶函数reduce()、fold()详解

在 Kotlin 中,reduce() 和 fold() 是函数式编程中常用的高阶函数。它们都是对集合中的元素进行聚合操作的函数,将一个集合中的元素缩减成一个单独的值。它们的使用方式非常相似,但是返回值略有不同...
继续阅读 »

在 Kotlin 中,reduce() 和 fold() 是函数式编程中常用的高阶函数。它们都是对集合中的元素进行聚合操作的函数,将一个集合中的元素缩减成一个单独的值。它们的使用方式非常相似,但是返回值略有不同。下面是它们的区别:

  • reduce() 函数是对集合中的所有元素进行聚合处理,并返回最后一个合并处理值。
  • fold() 函数除了合并所有元素之外,还可以接受一个初始值,并将其与聚合结果一起返回。注:如果集合为空的话,只会返回初始值。

reduce示例

1、使用 reduce() 函数计算列表中所有数字的总和:

fun reduceAdd() {
val list = listOf(1, 2, 3, 4, 5)
val sum = list.reduce { acc, i ->
println("acc:$acc, i:$i")
acc + i
}
println("sum is $sum") // 15
}

执行结果:

acc:1, i:2
acc:3, i:3
acc:6, i:4
acc:10, i:5
sum is 15

2、使用 reduce() 函数计算字符串列表中所有字符串的拼接结果:

val strings = listOf("apple", "banana", "orange", "pear")
val result = strings.reduce { acc, s -> "$acc, $s" }
println(result) // apple, banana, orange, pear

执行结果:

apple, banana, orange, pear

fold示例

1、使用 fold() 函数计算列表中所有数字的总和,并在其基础上加上一个初始值:

val numbers = listOf(1, 2, 3, 4, 5)
val sum = numbers.fold(10) { acc, i -> acc + i }
println(sum) // 25

执行结果为:

acc:10, i:1
acc:11, i:2
acc:13, i:3
acc:16, i:4
acc:20, i:5
sum is 25

2、使用 fold() 函数将列表中的所有字符串连接起来,并在其基础上加上一个初始值:

val strings = listOf("apple", "banana", "orange", "pear")
val result = strings.fold("Fruits:") { acc, s -> "$acc $s" }
println(result) // Fruits: apple banana orange pear

执行结果:

Fruits: apple banana orange pear

源码解析

  • reduce() 在Kotlin标准库的实现如下:
public inline fun <S, T : S> Iterable<T>.reduce(operation: (acc: S, T) -> S): S {
val iterator = this.iterator()
if (!iterator.hasNext()) throw UnsupportedOperationException("Empty collection can't be reduced.")
var accumulator: S = iterator.next()
while (iterator.hasNext()) {
accumulator = operation(accumulator, iterator.next())
}
return accumulator
}

从代码中可以看出,reduce函数接收一个operation参数,它是一个lambda表达式,用于聚合计算。reduce函数首先获取集合的迭代器,并判断集合是否为空,若为空则抛出异常。然后通过迭代器对集合中的每个元素进行遍历操作,对元素进行聚合计算,将计算结果作为累加器,传递给下一个元素,直至聚合所有元素。最后返回聚合计算的结果。

  • fold() 在Kotlin标准库的实现如下:
public inline fun <T, R> Iterable<T>.fold(
initial: R,
operation: (acc: R, T) -> R
): R {
var accumulator: R = initial
for (element in this) {
accumulator = operation(accumulator, element)
}
return accumulator
}

从代码中可以看出,fold函数接收两个参数,initial参数是累加器的初始值,operation参数是一个lambda表达式,用于聚合计算。

fold函数首先将初始值赋值给累加器,然后对集合中的每个元素进行遍历操作,对元素进行聚合计算,将计算结果作为累加器,传递给下一个元素,直至聚合所有元素。最后返回聚合计算的结果。

总结

  • reduce()适用于不需要初始值的聚合操作,fold()适用于需要初始值的聚合操作。
  • reduce()操作可以直接返回聚合后的结果,而fold()操作需要通过lambda表达式的返回值来更新累加器的值。

在使用时,需要根据具体场景来选择使用哪个函数。


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

Kotlin 字符串常用的操作符

字符串常用的操作符commonPrefixWith返回两个字符串中最长的相同前缀,如果它们没有共同的前缀,则返回空字符串,可以定义 ignoreCase 为 true忽略大小写val action = "蔡徐坤唱跳rap" val...
继续阅读 »

字符串常用的操作符

commonPrefixWith

返回两个字符串中最长的相同前缀,如果它们没有共同的前缀,则返回空字符串,可以定义 ignoreCase 为 true忽略大小写

val action = "蔡徐坤唱跳rap"
val time = "蔡徐坤两年半"
val introduce = "个人练习生蔡徐坤喜欢唱跳rap"
println(action.commonPrefixWith(time)) // 蔡徐坤
println(action.commonPrefixWith(introduce)) // ""

源码实现

// 通过while获取两个字符串同个索引下的字符是否相等
// 最后通过subSequence切割字符串
public fun CharSequence.commonPrefixWith(other: CharSequence, ignoreCase: Boolean = false): String {
val shortestLength = minOf(this.length, other.length)

var i = 0
while (i < shortestLength && this[i].equals(other[i], ignoreCase = ignoreCase)) {
i++
}
if (this.hasSurrogatePairAt(i - 1) || other.hasSurrogatePairAt(i - 1)) {
i--
}
return subSequence(0, i).toString()
}

commonSuffixWith

返回两个字符串中最长的相同后缀,如果它们没有共同的后缀,则返回空字符串,可以定义 ignoreCase 为 true忽略大小写

val action = "蔡徐坤唱跳rap"
val time = "蔡徐坤两年半"
val introduce = "个人练习生蔡徐坤喜欢唱跳rap"
println(action.commonSuffixWith(time))
println(action.commonSuffixWith(introduce))

源码实现

// 与commonPrefixWith的实现差不多,只是commonSuffixWith是倒序循环
public fun CharSequence.commonSuffixWith(other: CharSequence, ignoreCase: Boolean = false): String {
val thisLength = this.length
val otherLength = other.length
val shortestLength = minOf(thisLength, otherLength)

var i = 0
while (i < shortestLength && this[thisLength - i - 1].equals(other[otherLength - i - 1], ignoreCase = ignoreCase)) {
i++
}
if (this.hasSurrogatePairAt(thisLength - i - 1) || other.hasSurrogatePairAt(otherLength - i - 1)) {
i--
}
return subSequence(thisLength - i, thisLength).toString()
}

contains

判断字符串是否包含某字符或某字符串,可以定义 ignoreCase 为 true 忽略大小写

val introduce = "个人练习生蔡徐坤喜欢唱跳rap"
println(introduce.contains('唱')) // true
println(introduce.contains("蔡徐坤")) // true
println("蔡徐坤" in introduce) // // 同上,contains是重载操作符,可以使用该表达式
println(introduce.contains("Rap", ignoreCase = true)) // true
println("Rap" !in introduce) // !in表示不包含的意思,与!introduce.contains("Rap")是同个意思

源码实现

// 通过indexOf判断字符是否存在
public operator fun CharSequence.contains(char: Char, ignoreCase: Boolean = false): Boolean =
indexOf(char, ignoreCase = ignoreCase) >= 0

// 通过indexOf判断字符串是否存在
public operator fun CharSequence.contains(other: CharSequence, ignoreCase: Boolean = false): Boolean =
if (other is String)
indexOf(other, ignoreCase = ignoreCase) >= 0
else
indexOf(other, 0, length, ignoreCase) >= 0

endsWith

判断字符串是否以某字符或某字符串作为后缀,可以定义 ignoreCase 为 true 忽略大小写

val introduce = "个人练习生蔡徐坤喜欢唱跳rap"
println(introduce.endsWith("蔡徐坤")) // false
println(introduce.endsWith("唱跳rap")) // true

源码实现

// 字符直接判断最末尾的字符
public fun CharSequence.endsWith(char: Char, ignoreCase: Boolean = false): Boolean =
this.length > 0 && this[lastIndex].equals(char, ignoreCase)
// 如果都是String,返回String.endsWith,否则返回regionMatchesImpl
public fun CharSequence.endsWith(suffix: CharSequence, ignoreCase: Boolean = false): Boolean {
if (!ignoreCase && this is String && suffix is String)
return this.endsWith(suffix)
else
return regionMatchesImpl(length - suffix.length, suffix, 0, suffix.length, ignoreCase)
}

// 不忽略大小写,返回java.lang.String.endsWith
public actual fun String.endsWith(suffix: String, ignoreCase: Boolean = false): Boolean {
if (!ignoreCase)
return (this as java.lang.String).endsWith(suffix)
else
return regionMatches(length - suffix.length, suffix, 0, suffix.length, ignoreCase = true)
}

equals

判断两个字符串的值是否相等,可以定义 ignoreCase 为 true 忽略大小写

val introduce = "蔡徐坤rap"
println(introduce.equals("蔡徐坤Rap")) // false
println(introduce == "蔡徐坤Rap") // 同上,因为equals是重载操作符,通常使用 == 表示即可
println(introduce.equals("蔡徐坤Rap", false)) // true

源码实现

// 通过java.lang.String的equals和equalsIgnoreCase判断
public actual fun String?.equals(other: String?, ignoreCase: Boolean = false): Boolean {
if (this === null)
return other === null
return if (!ignoreCase)
(this as java.lang.String).equals(other)
else
(this as java.lang.String).equalsIgnoreCase(other)
}

ifBlank

如果字符串都是空格,将字符串转成默认值。这个操作符非常有用

val whitespace = "    ".ifBlank { "default" }
val introduce = "蔡徐坤rap".ifBlank { "default" }
println(whitespace) // default
println(introduce) // 蔡徐坤rap

源码实现

public inline fun <C, R> C.ifBlank(defaultValue: () -> R): R where C : CharSequence, C : R =
if (isBlank()) defaultValue() else this

ifEmpty

如果字符串都是空字符串,将字符串转成默认值。这个操作符非常有用,省去了你去判断空字符串然后再次赋值的操作

val whitespace = "    ".ifEmpty { "default" }
val empty = "".ifEmpty { "default" }
val introduce = "蔡徐坤rap".ifEmpty { "default" }
println(whitespace) // " "
println(empty) // default
println(introduce) // 蔡徐坤rap

判断空字符串、null 和空格字符串

  • isEmpty 判断空字符串
  • isBlank 判断字符串都是空格
  • isNotBlank 与 isBlank 相反,判断字符串不是空格
  • isNotEmpty 与 isEmpty 相反,判断字符串不是空格
  • isNullOrBlank 判断字符串不是 null 和 空格
  • isNullOrEmpty 判断字符串不是 null 和 空字符串

lines

将字符串以换行符或者回车符进行分割,返回每一个分割的子字符串 List<String>

val article = "大家好我是练习时长两年半的个人练习生\n蔡徐坤\r喜欢唱跳rop"
println(article.lines()) // [大家好我是练习时长两年半的个人练习生, 蔡徐坤, 喜欢唱跳rop]

源码实现

// 大概就是通过Sequence去切割字符串
public fun CharSequence.lines(): List<String> = lineSequence().toList()
public fun CharSequence.lineSequence(): Sequence<String> = splitToSequence("\r\n", "\n", "\r")

public fun <T> Sequence<T>.toList(): List<T> {
return this.toMutableList().optimizeReadOnlyList()
}

lowercase

将字符串都转换成小写

val introduce = "蔡徐坤RaP"
println(introduce.lowercase()) // 蔡徐坤rap

源码实现

// 通过java.lang.String的toLowerCase方法实现,其实很多kotlin的方法都是调用java的啦
public actual inline fun String.lowercase(): String = (this as java.lang.String).toLowerCase(Locale.ROOT)

replace

将字符串内的某一部分替换为新的值,可以定义 ignoreCase 为 true 忽略大小写

val introduce = "蔡徐坤rap"
println(introduce.replace("rap", "RAP"))
println(introduce.replace("raP", "RAP", ignoreCase = true))

源码实现

// 首先通过indexOf判断是否存在要被替换的子字符串
// do while循环添加被替换之后的字符串,因为字符串有可能是有多个地方需要替换,所有通过occurrenceIndex判断是否还有需要被替换的部分
public actual fun String.replace(oldValue: String, newValue: String, ignoreCase: Boolean = false): String {
run {
var occurrenceIndex: Int = indexOf(oldValue, 0, ignoreCase)
// FAST PATH: no match
if (occurrenceIndex < 0) return this

val oldValueLength = oldValue.length
val searchStep = oldValueLength.coerceAtLeast(1)
val newLengthHint = length - oldValueLength + newValue.length
if (newLengthHint < 0) throw OutOfMemoryError()
val stringBuilder = StringBuilder(newLengthHint)

var i = 0
do {
stringBuilder.append(this, i, occurrenceIndex).append(newValue)
i = occurrenceIndex + oldValueLength
if (occurrenceIndex >= length) break
occurrenceIndex = indexOf(oldValue, occurrenceIndex + searchStep, ignoreCase)
} while (occurrenceIndex > 0)
return stringBuilder.append(this, i, length).toString()
}
}

startsWith

判断字符串是否以某字符或某字符串作为前缀,可以定义 ignoreCase 为 true 忽略大小写

val introduce = "rap"
println(introduce.startsWith("Rap"))
println(introduce.startsWith("Rap", ignoreCase = true))

源码实现

// 还是调用的java.lang.String的startsWith
public actual fun String.startsWith(prefix: String, ignoreCase: Boolean = false): Boolean {
if (!ignoreCase)
return (this as java.lang.String).startsWith(prefix)
else
return regionMatches(0, prefix, 0, prefix.length, ignoreCase)
}

substringAfter

获取分割符之后的子字符串,如果不存在该分隔符默认返回原字符串,当然你可以自定义返回

例如在截取 ip:port 格式的时候,分隔符就是 :

val ipAddress = "192.168.1.1:8080"
println(ipAddress.substringAfter(":")) // 8080
println(ipAddress.substringAfter("?")) // 192.168.1.1:8080
println(ipAddress.substringAfter("?", missingDelimiterValue = "没有?这个子字符串")) // 没有?这个子字符串

源码实现

// 还是通过substring来截取字符串的
public fun String.substringAfter(delimiter: String, missingDelimiterValue: String = this): String {
val index = indexOf(delimiter)
return if (index == -1) missingDelimiterValue else substring(index + delimiter.length, length)
}

substringAfterLast

与 substringAfter 是同一个意思,不同的是如果一个字符串中有多个分隔符,substringAfter 是从第一个开始截取字符串,substringAfterLast 是从最后一个分隔符开始截取字符串

val network = "255.255.255.0:192.168.1.1:8080"
println(network.substringAfter(":")) // 192.168.1.1:8080
println(network.substringAfterLast(":")) // 8080

源码实现

// 源码和substringAfter差不多,只是substringAfterLast获取的是最后一个分割符的索引
public fun String.substringAfterLast(delimiter: String, missingDelimiterValue: String = this): String {
val index = lastIndexOf(delimiter)
return if (index == -1) missingDelimiterValue else substring(index + delimiter.length, length)
}

substringBefore

获取分割符之前的子字符串,如果不存在该分隔符默认返回原字符串,当然你可以自定义返回,与 substringAfter 刚好相反

val ipAddress = "192.168.1.1:8080"
println(ipAddress.substringBefore(":")) // 192.168.1.1
println(ipAddress.substringBefore("?")) // 192.168.1.1:8080
println(ipAddress.substringBefore("?", missingDelimiterValue = "没有?这个子字符串")) // 没有?这个子字符串

源码实现

// 还是通过substring来截取字符串的,只是是从索引0开始截取子字符串
public fun String.substringBefore(delimiter: String, missingDelimiterValue: String = this): String {
val index = indexOf(delimiter)
return if (index == -1) missingDelimiterValue else substring(0, index)
}

substringBeforeLast

与 substringBefore 是同一个意思,不同的是如果一个字符串中有多个分隔符,substringBefore 是从第一个开始截取字符串,substringBeforeLast 是从最后一个分隔符开始截取字符串

val network = "255.255.255.0:192.168.1.1:8080"
println(network.substringBefore(":")) // 255.255.255.0
println(network.substringBeforeLast(":")) // 255.255.255.0:192.168.1.1

源码实现

// 源码和substringBefore差不多,只是substringBeforeLast获取的是最后一个分割符的索引
public fun String.substringBeforeLast(delimiter: String, missingDelimiterValue: String = this): String {
val index = lastIndexOf(delimiter)
return if (index == -1) missingDelimiterValue else substring(0, index)
}

trim

去掉字符串首尾的空格符,如果要去掉字符串中间的空格符请用 replace

val introduce = "  个人练习生蔡徐坤  喜欢唱跳rap  "
println(introduce.trim()) // 个人练习生蔡徐坤 喜欢唱跳rap

uppercase

将字符串都转换成大写

源码实现

// java.lang.String.toUpperCase
public actual inline fun String.uppercase(): String = (this as java.lang.String).toUpperCase(Locale.ROOT)

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

简单教你Intent如何传大数据

前言最近想不出什么比较好的内容,但是碰到一个没毕业的小老弟问的问题,那就借机说说这个事。Intent如何传大数据?为什么是简单的说,因为这背后深入的话,有很多底层的细节包括设计思想,我也不敢说完全懂,但我知道当你用Intent传大数据报错的时候应该怎么解决,并...
继续阅读 »

前言

最近想不出什么比较好的内容,但是碰到一个没毕业的小老弟问的问题,那就借机说说这个事。Intent如何传大数据?为什么是简单的说,因为这背后深入的话,有很多底层的细节包括设计思想,我也不敢说完全懂,但我知道当你用Intent传大数据报错的时候应该怎么解决,并且简单聊聊这背后所涉及到的东西。

Intent传大数据

平时可能不会发生这种问题,但比如我之前是做终端设备的,我的设备每秒都会生成一些数据,而长时间的话数据量自然大,这时当我跳到另外一个页面使用intent把数据传过去的时候,就会报错

我们调用

intent.putExtra("key", value) // value超过1M

会报错

android.os.TransactionTooLargeException: data parcel size xxx bytes

这里的xxx就是1M左右,告诉你传输的数据大小不能超过1M,有些话咱也不敢乱说,有点怕误人子弟。我这里是凭印象说的,如果有大佬看到我说错,请狠狠的纠正我。

这个错误描述是这么描述,但真的是限死1M吗,说到这个,就不得不提一样东西,Binder机制,先不要跑,这里不会详细讲Binder,只是提一嘴。

说到Binder那就会联系到mmap内存映射,你可以先简单理解成内存映射是分配一块空间给内核空间和用户空间共用,如果还是不好理解,就简单想成分配一块空间通信用,那在android中mmap分配的空间是多少呢?1M-4K。

那是不是说Intent传输的数据超过1M-4K就会报错,理论上是这样,但实际没到这个值,比如0.8M也可能会报错。所以你不能去走极限操作,比如你的数据到了1M,你觉得只要减少点数据,减到8K,应该就能过了,也许你自己测试是正常的,但是这很危险。

所以能不传大数据就不要传大数据,它的设计初衷也不是为了传大数据用的。如果真要传大数据,也不要走极限操作。

那怎么办,切莫着急,请听我慢慢讲。就这个Binder它是什么玩意,它是Android中独特的进程通信的方式,而Linux中进程通信的方式,在Android中同样也适用。进程间通信有很多方式,Binder、管道、共享内存等。为什么会有这么多种通信方式,因为每种通信方式都有自己的特点,要在不同的场合使用不同的通信方式。

为什么要提这个?因为要看懂这个问题,你需要知道Binder这种通信方式它有什么特点,它适合大量的数据传输吗?那你Binder又与我Intent何干,你抓周树人找我鲁迅干嘛~~所以这时候你就要知道Android四大组件之间是用什么方式通信的。

有点扯远了,现在可以来说说结论了,Binder没办法传大数据,我就1M不到你想怎样?当然它不止1M,只是Android在使用时限制了它只能最多用1M,内核的最大限制是4M。又有点扯远了,你不要想着怎么把限制扩大到4M,不要往这方面想。前面说了,不同的进程通信方式,有自己的特点,适用于某些特定的场景。那Binder不适用于传输大数据,我共享内存行不行?

所以就有了解决办法

bundle.putBinder()

有人可能一看觉得,这有什么不同,这在表面上看差别不大,实则内部大大的不同,bundle.putBinder()用了共享内存,所以能传大数据,那为什么这里会用共享内存,而putExtra不是呢?想搞清楚这个问题,就要看源码了。 这里就不深入去分析了,我怕劝退,不是劝退你们,是劝退我自己。有些东西是这样的,你要自己去看懂,看个大概就差不多,但是你要讲出来,那就要看得细致,而有些细节确实会劝退人。所以想了解为什么的,可以自己去看源码,不想看的,就知道这是怎么一回事就行。

那还有没有其它方式呢?当然有,你不懂共享内存,你写到本地缓存中,再从本地缓存中读取行不行?

办法有很多,如果你不知道这个问题怎么解决,你找不到你觉得可行的解决方案,甚至可以通过逻辑通过流程的方式去绕开这个问题。但是你要知道为什么会出现这样的问题,如果你没接触过进程通信,没接触过Binder,让你看一篇文章就能看懂我觉得不切实际,但是至少得知道是怎么一回事。

比如我只说bundle.putBinder()能解决这个问题,你一试,确实能解决,但是不知道为什么,你又怕会不会有其它问题。虽然这篇文章我一直在打擦边球,没有提任何的原理,但我觉得还是能大概让人知道为什么bundle.putBinder()能解决Intent传大数据,你也就能放心去用了。


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

记一次反编译并重新打包的过程

反编译部分的介绍在文章末尾排查原因根据现象来看,这程序要嘛崩溃了,要嘛该App不适配此款盒子(比如ABI不支持、Target SDK Version等问题)minSdkVersion系统版本不支持?但是同事告诉我这款盒子是Android9,在其他的Androi...
继续阅读 »

反编译部分的介绍在文章末尾

排查原因

根据现象来看,这程序要嘛崩溃了,要嘛该App不适配此款盒子(比如ABI不支持、Target SDK Version等问题)

minSdkVersion系统版本不支持?

但是同事告诉我这款盒子是Android9,在其他的Android9和Android 4.4盒子上都跑过,没问题,排除了minSdkVersion的问题

ABI不支持?

不太可能,先不想这个

ADB才是王道

但凡遇到问题,只要设备能够adb,起码问题就解决了一半,但一问,说这盒子似乎不能Adb,,鹅鹅鹅饿~~ 后来借助adbhelper发现,此款盒子还是能adb,只是常规情况下adb的端口是60001,连上之后执行adb root的话,又会换回默认端口,这个情况我也是活久见。。

说正题,连上adb之后,通过抓日志,发现了如下问题:Permission denial: writing to settings requires:android.permission.WRITE_SECURE_SETTINGS,根据错误堆栈信息,大致是app调用了wifimanager.setWifiEnabled(true)这个方法引起的

解决

权限思路

因为自己对android.permission.WRITE_SECURE_SETTINGS这个权限并不太了解,所以从异常的字面意思理解,我以为是权限不够,所以我尝试让app拥有权限来确保其正常运行。

方法1:adb shell pm grant {包名} {权限内容}

执行命令,赋予该程序权限adb shell pm grant {packagename} android.permission.WRITE_SECURE_SETTINGS,执行命令之后,提示java.lang.SecurityException: Package xxxx has not requested permission android.permission.WRITE_SECURE_SETTINGS意思说该程序不需要这个权限,这个问题的原因是因为app并没有在清单文件中申明这个权限,这就有意思了,这个app操作需要这个权限却没有申请权限,可能主要原因是因为以前的低版本不需要,Android9需要吧,所以我们得先给他增加这个申明,然后再赋予这个权限。

通过apktool反编译,然后修改清单文件添加这个权限,再重新打包(后面再说具体得反编译重打包的步骤),一切妥当之后再次执行上面命令,果然老天是不会让我舒坦的,执行后出现异常:java.lang.SecurityException: Package android does not belong to 10034,触发问题的调用堆栈还是之前那个方法引起的。(⊙﹏⊙),思考半天,看了下它的清单文件,并没有申明targetSdkVersion,这有点怪哦,也是活久见,难道游戏apk就可以这么无视规则?那我要不给他增加上,,,嗯可以一试,还是相同的配方,给清单文件增加如下代码:

<uses-sdk
android:minSdkVersion="17"
android:targetSdkVersion="22" />

然后重新打包,再来,没错,还是同样的味道,同样的问题,我以为修改来低于23,权限能够就自动允许了,现在想起来真是too young to simple。。

东搜搜西搜搜,想尝试下是不是因为这个程序不是系统app,后来将程序放在system/app下作为系统程序,还是同样问题,所以很显然,权限这条路行不通。

所以这个问题的解决办法应该参考如下内容:

这个错误是因为你的应用试图调用setWifiEnabled方法,这个方法在Android 9(API级别28)及以上版本已经被弃用。在这些版本中,只有系统应用才能调用setWifiEnabled方法。  
即使你的应用已经被安装为系统应用,并且已经获得了WRITE_SECURE_SETTINGS权限,它仍然不能调用setWifiEnabled方法。这是因为这个方法现在只能被系统UI调用,其他应用,包括系统应用,都不能调用这个方法。
你可以考虑使用WifiNetworkSuggestion API来提示用户连接到特定的Wi-Fi网络,或者使用Settings.Panel.ACTION_WIFI来引导用户到Wi-Fi设置页面。
以下是如何使用Settings.Panel.ACTION_WIFI的示例:
val intent = Intent(Settings.Panel.ACTION_WIFI)
startActivity(intent)
这段代码会打开Wi-Fi设置页面,让用户自己开启或关闭Wi-Fi。

修改程序源代码

权限的路行不通,那我们只能想办法修复app这段逻辑代码了,但是别人的apk,显然不是那么容易让人想改就改撒,提出是这里的问题,别人也不一定信啊,所以我打算自己改这个apk的编译后的源代码,让他不要触发引起异常的那个逻辑,绕过看程序能不能正常跑起来,这一步就需要懂得起smali,还好这个问题比较简单,定位到代码改了之后,重新打包,这下消停了,程序很好正常的运行。

反编译

反编译这一块涉及很多概念,以前大概接触过,都只是看,没有实际操作,有些时候操作也只是简单的反编译看下源码,但总体工具和涉及的概念主要有ApkTool、dex2jar-2.1、jarsigner、jd-gui,还有些文章提到SignApk.jar、jax-gui等;

说下自己的理解,假如我们需要重新打包一个apk,那么我们肯定要从这个apk得到我们可以编辑的文件进行修改,修改后再重新打包,这是我们需要的核心流程;

反编译APK

ApkTool,具体的作用自己查,大致意思是如果我们想看清单文件内容,资源文件之类的,我们就可以通过这个工具进行反编译,由于前面我需要给源程序在清单文件中新增权限申明,所以我们就需要先得到反编译的工程,然后直接修改清单文件即可(把AndroidManifest拖动到Android Studio或者其他文本编辑工具中直接修改然后保存即可)

  • 反编译apk :先在终端将当前位置定位到ApkTool的目录,然后执行命令apktool d {xxx.apk:你的apk名称},该命令会将apk反编译后保存在apktool所在目录下。也可以使用如下命令指定反编译后工程的存储路径apktool d -o {反编译后的存储目录} {xxx.apk},其实只需要知道反编译apk是使用的apktool d即可,查一下文档了解更详细的用法。

  • 重新编译:修改之后我们需要重新打包成apk,使用命令apk b {反编译后的存储目录},编译成功之后,会保存在指定目录下的dist文件夹中。也可以用-o 指定存储的目录。

重签名

apk反编译修改了,也重新编译成了新的apk,但此时这个apk是没有签名的,直接拿到设备上安装是不行的,所以我们需要签名。 这里就需要用到jarsign,这个应该是jdk内自带的jarsigner -verbose -keystore {签名文件路径:也就是keystore、jks文件} -signedjar {签名后的apk路径} {没有签名的apk路径} {使用的签名文件别名,也就是keyalias}如果没出错,则我们就拥有了已经签名的被修改过的apk了,可以去运行验证了。但是由于我们是用的自己签名文件签名的,是不能覆盖安装原来的apk的,两者签名不一致。

使用系统签名

这块我没尝试过,需要的自行查看搜索查看,附带个链接Android应用程序签名系统的签名(SignApk.jar)_新根的博客-CSDN博客

修改源码

跟直接修改清单文件的原理差不多,都是直接修改,但是由于是smali,就需要做一定的语法了解才能改了。也有工具可以将smali转换成java的,比如使用skylot/jadx: Dex to Java decompiler (github.com)工具,不过这个方法只是将smali转换成java来查阅,如果我们想重新打包,那还是必须修改smali文件才行的。

关于这一节建议参考:Android App 逆向入門之二:修改 smali 程式碼 (cymetrics.io)

额外说的:其实上面整个过程我们没用到dex2jar-2.1、jd-gui之类的,其实作用不同,dex2jar的作用是将apk的后缀改成zip解压出来会得到很多dex文件,我们通过dex2jar可以让这些dex转换成jar文件,jar文件就是常规生成的java class文件了,但是jar文件没法直接打开查看,就需要借助jd-gui之类的工具。。所以的目的是说我们想看看别人程序的代码的时候用的吧。

就到这吧,做个记录。。


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

何时使用Kafka而不是RabbitMQ

Kafka 和 RabbitMQ 都是流行的开源消息系统,它们可以在分布式系统中实现数据的可靠传输和处理。Kafka 和 RabbitMQ 有各自的优势和特点,它们适用于不同的场景和需求。本文将比较 Kafka 和 RabbitMQ 的主要区别,并分析何时使用...
继续阅读 »

Kafka 和 RabbitMQ 都是流行的开源消息系统,它们可以在分布式系统中实现数据的可靠传输和处理。Kafka 和 RabbitMQ 有各自的优势和特点,它们适用于不同的场景和需求。本文将比较 Kafka 和 RabbitMQ 的主要区别,并分析何时使用 Kafka 而不是 RabbitMQ。

推荐博主开源的H5商城项目waynboot-mall,这是一套全部开源的微商城项目,包含一个运营后台、h5商城和后台接口。 实现了一个商城所需的首页展示、商品分类、商品详情、sku详情、商品搜索、加入购物车、结算下单、订单状态流转、商品评论等一系列功能。 技术上基于最新得Springboot3.0、jdk17,整合了Redis、RabbitMQ、ElasticSearch等常用中间件, 贴近生产环境实际经验开发而来不断完善、优化、改进中。

github地址:github.com/wayn111/way…

影响因素

  1. 可扩展性:Kafka 旨在处理大容量、高吞吐量和实时数据流。它每秒能够处理数百万个事件,并且可以处理大量数据。另一方面,RabbitMQ 的设计更加灵活,可以处理广泛的用例,但可能不太适合大容量、实时数据流。
  2. 耐用性:Kafka 通过将所有数据写入磁盘来提供高度的耐用性,这对于任务关键型应用程序非常重要。 RabbitMQ 还提供基于磁盘的持久性,但这可能不如 Kafka 提供的那么强大。
  3. 延迟:RabbitMQ 设计为低延迟,这对于实时数据处理和分析非常重要。Kafka 延迟相比 RabbitMQ 会高一点。
  4. 数据流:Kafka 使用无界的数据流,即数据持续地流入到指定的主题(topic)中,不会被删除或过期,除非达到了预设的保留期限或容量限制。RabbitMQ 使用有界的数据流,即数据被生产者(producer)创建并发送到消费者(consumer),一旦被消费或者达到了过期时间,就会从队列(queue)中删除。
  5. 数据使用:Kafka 支持多个消费者同时订阅同一个主题,并且可以根据自己的进度来消费数据,不会影响其他消费者。这意味着 Kafka 可以支持多种用途和场景,比如实时分析、日志聚合、事件驱动等。RabbitMQ 的消费者从一个队列中消费数据,一旦被消费,就不会再被该队列其他消费者看到。这意味着 RabbitMQ 更适合一对一的通信或任务分发。
  6. 数据顺序:Kafka 保证了同一个分区(partition)内的数据是有序的,即按照生产者发送的顺序来存储和消费。但是不同分区之间的数据是无序的,即不能保证跨分区的数据按照全局顺序来处理。 RabbitMQ 保证了同一个队列内的数据是有序的,即按照先进先出(FIFO)的原则来存储和消费。但是不同队列之间的数据是无序的,即不能保证跨队列的数据按照全局顺序来处理。
  7. 数据可靠性:Kafka 通过副本(replica)机制来保证数据的可靠性,即每个主题可以有多个副本分布在不同的节点(broker)上,如果某个节点发生故障,可以自动切换到其他节点继续提供服务。 RabbitMQ 通过镜像(mirror)机制来保证数据的可靠性,即每个队列可以有多个镜像分布在不同的节点上,如果某个节点发生故障,可以自动切换到其他节点继续提供服务。
  8. 数据持久性:Kafka 将数据持久化到磁盘中,并且支持数据压缩和批量传输,以提高性能和节省空间。Kafka 可以支持TB级别甚至PB级别的数据存储,并且可以快速地重放历史数据。RabbitMQ 将数据缓存在内存中,并且支持消息确认和事务机制,以提高可靠性和一致性。RabbitMQ 也可以将数据持久化到磁盘中,但是会降低性能和吞吐量。RabbitMQ 更适合处理小规模且实时性较高的数据。
  9. 数据扩展性:Kafka 通过分区机制来实现水平扩展,即每个主题可以划分为多个分区,并且可以动态地增加或减少分区数量
  10. 复杂性:与 RabbitMQ 相比,Apache Kafka 具有更复杂的架构,并且可能需要更多的设置和配置,因此它的复杂性也允许更高级的功能和定制。另一方面,RabbitMQ 更容易设置和使用。

应用场景

Kafka 适用场景和需求

  • 跟踪高吞吐量的活动,如网站点击、应用日志、传感器数据等。
  • 事件溯源,Kafka 保存着所有历史消息,可以用于事件回溯和审计。
  • 流式处理,如实时分析、实时推荐、实时报警等。
  • 日志聚合,如收集不同来源的日志并统一存储和分析。

RabbitMQ 适用场景和需求

  • 中小项目,项目消息量小、吞吐量不高、对延时敏感。
  • 遗留应用,如需要与旧系统或第三方系统进行集成或通信。
  • 复杂路由,如需要根据不同的规则或条件来分发或过滤消息。
  • 任务分发,如需要将任务均匀地分配给多个工作进程或消费者。

总结

在公司项目中,一般消息量都不大的情况下,博主推荐大家可以使用 RabbitMQ。消息量起来了可以考虑切换到 Kafka,但是也要根据公司内部对两种 MQ 的熟悉程度来进行选择,避免 MQ 出现问题时无法及时处理。


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

实战:工作中对并发问题的处理

大家好,我是 方圆。最近在接口联调时发生了数据并发修改问题,我想把这个问题讲解一下,并把当时提出的解决方案进行实现,希望它能在大家以后在遇到同样的问题时提供一些借鉴和思考的方向。原文还是收录在我的 Github: enthusiasm&nb...
继续阅读 »

大家好,我是 方圆。最近在接口联调时发生了数据并发修改问题,我想把这个问题讲解一下,并把当时提出的解决方案进行实现,希望它能在大家以后在遇到同样的问题时提供一些借鉴和思考的方向。原文还是收录在我的 Github: enthusiasm 中,欢迎Star和获取原文。

1. 问题背景

问题发生在快递分拣的流程中,我尽可能将业务背景简化,让大家只关注并发问题本身。

分拣业务针对每个快递包裹都会生成一个任务,我们称它为 task。task 中有两个字段需要关注,一个是分拣中发生的 异常(exp_type),另一个是分拣任务的 状态(status)。另外,需要关注 分拣状态上报接口,通过它来记录分拣过程中的异常和状态变更。

一般情况下,分拣机在分拣异常发生时会及时调用接口上报,在分拣完成时调用接口来标记为完成状态,两次接口调用的时间间隔较长,不会发生并发问题。

但是有一种特殊的分拣机,它不会在异常发生时及时上报,而是在分拣完成时将分拣过程中发生的异常和分拣结果一起上报,那么此时分拣状态上报接口在同一时间内就会有两次调用,这时便发生了预期外的并发问题。

我们先看下分拣状态上报接口的执行流程:

  1. 先查询到该分拣任务 task,默认情况下 exp_type 和 status 均为默认值0

  2. 分拣异常修改 task 中的 exp_type,分拣完成修改 status 字段信息

  3. 修改完成将 task 写入

数据库初始值为 1, 0, 0,分拣异常和分拣完成几乎同时上报,它们都读取到该值。分拣异常动作将 exp_type 修改为9,写入数据库,此时数据库值为 1, 9, 0;分拣完成动作将 status 修改为1,写入数据库,使得数据库最终值为 1, 0, 1,它将异常字段的值覆盖掉了。正常情况下,最终值应该为 1, 9, 1,分拣完成动作应该读取到分拣异常完成后的值 1, 9, 0 后再进行修改才对。

2. 解决方案

发生这个问题的原因很容易就能发现:两个事务同时执行 读取-修改-写入 序列,其中一个写操作在没有合并另一个写操作变更的情况下,直接覆盖了另一个写操作的结果,所以导致了数据的丢失。

这种问题是比较典型的 丢失更新 问题,可以通过对数据库读操作加锁或者改变数据库的隔离级别为可串行化使事务串行执行的方式进行避免。下面我会将大家在讨论避免丢失更新问题时提出的方案进行介绍,并尽可能的用代码来表现它们。

2.1 数据库读操作加锁和可串行化隔离级别

我们可以考虑:如果对每条Task数据修改的事务都是在当前事务完成之后才允许后续事务进行修改,使事务串行执行,那么我们就能够避免这种情况。比较直接的实现是通过显式加锁来实现,如下

select exp_type, status
from task
where id = 1
for update;

先查询该行数据的事务会获取到该行数据的 排他锁,后续针对该数据的所有读写请求都会被阻塞,直到先前事务执行完将锁释放。

这样通过加锁的方式实现了事务的串行执行。但是,在为SQL添加加锁语句时,需要确定是不是为该行数据加锁而不是锁住了整个表,如果是后者,那么可能会造成系统性能严重下降,而且还需要关注有哪些业务场景使用到了该SQL,是否存在长时间执行的只读事务使用,如果存在的话可能会出现因加锁导致延迟和系统性能下降,所以需要谨慎的评估。

此外,可串行化的数据库隔离级别也能保证事务的串行执行,不过它针对的是所有事务。一般情况下为了保证性能,我们不会采用这种方案(默认使用MySQL可重复读隔离级别)。

MySQL的InnoDB引擎实现可串行化隔离级别采用的是2PL机制:在第一阶段事务执行时获取锁,第二阶段事务执行完成释放锁。

2.2 针对业务只修改必要字段

如果异常状态请求仅修改 exp_type 字段,分拣完成仅修改 status 字段的话,那么我们可以梳理一下业务逻辑,仅将必要修改的字段写入数据库,这样就不会发生丢失更新的异常,如下代码所示:

// 处理异常状态请求,封装修改数据的对象
Task task = new Task();
tast.setId(id);
task.setExpType(expType);

// 更改数据
taskService.updateById(task);

在执行修改数据前,创建一个新的修改对象,并只为其必要修改字段赋值。但是还需要考虑的是:如果这个业务流程处理已经很复杂了,很可能不清楚该为哪些字段赋值而导致再发生新的异常,所以采用这种方法需要对业务足够熟悉,并且在修改完后进行充分的测试。

2.3 分布式锁

分布式锁的方法与方法一类似,都是通过加锁的方式来保证同时只有一个事务执行,区别是方法一的锁加在了数据库层,而分布式锁是借助Redis来实现。

这种实现方式的好处是锁的粒度小,发生锁争抢仅限于单个包裹,无需像数据库加锁一样去考虑锁的粒度和对相关业务的影响。伪代码如下所示:

// 分布式锁KEY
String distributedKey = String.format(DISTRIBUTED_KEY_PREFIX, packageNo);
try {
// 分布式锁阻塞同一包裹号的修改
lock(distributedKey);
// 处理业务逻辑
handler();
} finally {
// 执行完解锁
redissonDistributedLocker.unlock(distributedKey);
}

需要注意,lock() 加锁方法要保证加锁失败或发生其他异常情况不影响业务逻辑的执行,并设定好锁持有时间和等待锁的阻塞时间,此外解锁方法务必添加到 finally 代码块中保证锁的释放。

2.4 CAS

CAS是乐观的解决方案,它一般通过在数据库中增加时间戳列来记录上次数据更改的时间,当新的事务执行时,需要比对读取时该行数据的时间戳和数据库中保存的时间戳是否一致,以此来判断事务执行期间是否有其他事务修改过该行数据,只有在没有发生改变的情况下才允许更新,否则需要重试这个事务。样例SQL如下所示:

update task 
set exp_type = #{expType}, status = #{status}, ts = #{currentTs}
where id = #{id} and ts = #{readTs}

它的原理不难理解,但是实现起来可能会存在困难,因为需要考虑在执行失败后该如何重试,重试的方式和重试的次数需要根据业务去判断。


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

技术主管是否需要什么段位的技术

今天来跟大家讨论一下技术主管需要什么样段位的技术?首先我要说明的一点,技术主管前提一定是技术出身。对于那些完全不懂技术,但是又身兼技术主管或者总监的同学,我这里就不再赘述,毕竟这个已经超出我目前理解力的范围。比如阿里云的王坚博士,基本上不懂技术细节,但是依然是...
继续阅读 »

今天来跟大家讨论一下技术主管需要什么样段位的技术?

首先我要说明的一点,技术主管前提一定是技术出身。对于那些完全不懂技术,但是又身兼技术主管或者总监的同学,我这里就不再赘述,毕竟这个已经超出我目前理解力的范围。比如阿里云的王坚博士,基本上不懂技术细节,但是依然是阿里云的CTO,一手缔造了阿里云。

那我们这里再详细讨论一下,作为一名技术主管,到底应该有什么样的一个技术的段位?或者换句话来说,你的主管的技术水平需要到达什么样的一个水位?

先说结论,作为一名技术主管,一定是整个团队的技术架构师。像其他的一些大家所讨论的条件我觉得都是次要的,比如说写代码的多少,对于技术深度的钻研多少,带的团队人数多少等等,最核心的是技术主管一定要把控整个团队整个业务技术发展的骨架。

为什么说掌控团队技术架构是最重要的?因为对于一个团队来说无非就两点,第一点就是业务价值,第二点就是技术价值。

对于业务价值来说,有各种各样的同学都可以去负责业务上面的一些导向和推进,比如说产品经理,比如说运营同学。技术主管可以在一定程度上去帮助业务成功,甚至是助力业务成功,但是一定要明白技术同学一定要有自己的主轴,就是你对于整个技术的把握。因为业务上的决策说到底技术主管是只能去影响而非去决策,否则就是你们整体业务同学太过拉胯,无法形成战术合力的目的。

对于一线开发同学来说,你只要完成一个接一个的技术项目即可。但是对于技术主管来说,你就要把握整体的技术发展脉络。要清晰的明白什么样的技术架构是和当前的业务匹配的,同时又具备未来业务发展的可扩展性。

那为什么不能把整个技术架构的设计交给某一个核心的骨干研发同学呢?

所以这里就要明白,对于名技术主管来说,未必一定要深刻的钻研技术本身,一定要把技术在业务上的价值发挥到最大。所以在一定程度上来说,可以让适当的同学参与或者主导整个技术架构的设计,但是作为主管必须要了解到所谓的技术投入的产出比是什么。但是如果不对技术架构有一个彻底的理解,如何能决定ROI?

也就是在技术方案的选型里面一定要有一个平衡,能够用最小的技术投入获取到最大的技术利益,而非深究于技术本身的实习方式。如果一名技术主管不了解技术的框架或者某一些主干流程,那么就根本谈不上怎么样去评估这投入的技术产出比。一旦一名技术主管无法衡量整个技术团队的投入产出比,那就意味着整个团队的管理都是在抓虾和浑水摸鱼的状态,这时候就看你团队同学是否自觉了。

出现了这种情况下的团队,可能换一头猪在主管的位置上,业务依然运行良好。如果在业务发展好的时候,可能一直能够顺利推动,你只要坐享其成就可以了,但是一旦到了要突破困难的时期,或者在业务走下行的时候,这个时候你技术上面的优势就一点就没有了。而且在这种情况下,如果你跳槽到其他公司,作为一名技术主管,对方的公司对你的要求也是非常高的,所以这个时候你如果都说不出来你的技术价值对于业务上面的贡献是什么那想当然,你可能大概率就凉凉了。

那问题又回到了什么样的水平才能到达架构师这个话题,可以出来另一篇文章来描述,但是整体上来说,架构的本质首先一定要明白,为的就是业务的增长。

其次,架构的设计其实就是建造一个软件体系的结构,使得具备清晰度,可维护性和可扩展性。另外要想做好架构,基本的基础知识也必不可少,比如说数据库选型、分布式缓存、分库分表、幂等、分布式锁、消息架构、异步架构等等。所以本身来说做好架构师本身难度就非常大,需要长期的积累,实现厚积而薄发。如何成为一名优秀的架构师可以看我的公众号的其他文章,这里就不再详细的介绍了。

第二点是技术主管需要对于技术细节有敏感度。很多人在问一名主管到底应该具备什么样的综合能力,能不能用一种更加形象的方式来概括,我认为就有一句话就可以概括了。技术主管应该是向战略轰炸机在平常的时候一直遨游在大气的最上层能够掌控整个全局,当到了必须要战斗的时候,可以快速的补充下去,定点打击。

我参加过一次TL培训课程,讲师是阿里云智能交付技术部总经理张瑞,他说他最喜欢的一句管理概括,就是“心有猛虎,细嗅蔷薇”,也就是技术主管在平常的时候会关注于更大的宏观战略或策略,也就是注重思考全局,但是在关键的时候一定要关注和落地实际的细节。

换句更加通俗的话来说,就是管理要像战略轰炸机,平常的时候飞在万丈高空巡视,当发生了战斗的时候,立即能够实现定点轰炸。

所以如果说架构上面的设计就是对于整个团队业务和技术骨架的把握,那么对于细节的敏感度就是对于解决问题的落地能力。

那怎么样能够保证你自己有一个技术细节的敏感度?

我认为必要的代码量是需要的,也就是说对于一个主管来说,不必要写太多低代码,但一定要保证一定的代码量,让自己能够最好的,最快的,最贴近实际的理解实际的业务项目。自己写一些代码,其实好处非常多,一方面能够去巩固和加深自己对技术的理解,另外一方面也能够通过代码去更加理解业务。

当然贴近技术的方式有很多种,不一定要全部靠写代码来完成,比如说做code review的方式来完成,做技术方案的评审来完成,这都是可以的。对我来说,我就会强迫自己在每一个迭代会写上一个需求,需求会涉及到各方各面的业务点。有前端的,有后端的,也有数据库设计的。

自己亲自参与写代码或者code review,会让自己更加贴近同学,能够感知到同学的痛点,而不至于只是在空谈说教。

总结

所以对于一个技术主管来说,我认为首要的就是具备架构设计的能力,其次就是要有代码细节的敏感度,对全局和对细节都要有很强大的把控能力。

当然再总结一下,这一套理论只是适用于基础的管理者,而非高层的CTO等,毕竟不同的层级要求的能力和影响力都是不一样的。


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

前端发展:走进行业迷茫的迷雾中

引言2023年,前端开发作为IT行业中备受关注的领域之一,正在经历着巨大的挑战和变革。然而,在当前行业不景气、失业率居高不下以及裁员潮席卷而来的情况下,许多人开始质疑前端开发的未来前景以及学习它是否依然有意义。本文将探讨这个问题并试图给出一些启示。第一部分:前...
继续阅读 »

引言

2023年,前端开发作为IT行业中备受关注的领域之一,正在经历着巨大的挑战和变革。然而,在当前行业不景气、失业率居高不下以及裁员潮席卷而来的情况下,许多人开始质疑前端开发的未来前景以及学习它是否依然有意义。本文将探讨这个问题并试图给出一些启示。

第一部分:前端的价值

前端开发作为网页和移动应用程序开发的重要组成部分,扮演着连接用户与产品的桥梁。前端技术的发展不仅推动了用户体验的提升,也对整个互联网行业产生了深远的影响。随着移动互联网的普及和技术的进步,前端在用户与产品之间的交互变得越来越重要。

对于企业而言,拥有优秀的前端开发团队意味着能够提供更好的用户体验、增强品牌形象、吸引更多用户和扩大市场份额。因此,前端开发的技能依然是企业争相追求的核心能力之一。

第二部分:行业不景气的背后

然而,正如每个行业都经历高低起伏一样,前端开发也面临着行业不景气带来的挑战。2023年,全球经济增长乏力、市场竞争激烈以及萧条的就业市场等因素,使得许多公司紧缩预算、停止招聘,并导致了失业率的上升和裁员的潮水。

在这种情况下,前端开发者需要重新审视自己的技能和市场需求。他们需要具备综合能力,包括对最新前端技术的深入了解、与其他团队成员的良好沟通合作能力以及持续学习和适应变化的能力。

第三部分:自我调整与进阶

面对市场变化和就业压力,前端开发者需要主动调整自己的发展路径。以下是一些建议:

  1. 多元化技能:学习并精通多种前端框架和库,如React、Vue.js和Angular等。同时,了解后端开发和数据库知识,拥有全栈开发的能力,将会让你在就业市场上更具竞争力。
  2. 学习与实践并重:不仅仅是学习新知识,还要将所学应用于实际项目中。积累项目经验,并在GitHub等平台分享你的作品,以展示自己的能力和潜力。同时,参加行业内的比赛、活动和社区,与他人交流并学习他们的经验。
  3. 持续学习:前端技术发展日新月异,不断学习是必需的。关注行业的最新趋势和技术,参加培训、研讨会或在线课程,保持对新知识的敏感度和学习能力。

第四部分:面对就业市场的挑战

在面对行业不景气和裁员的情况下,重新进入就业市场变得更加具有挑战性。以下是一些建议:

  1. 提升个人竞争力:通过获得认证、实习或自主开发项目等方式,提升自己在简历中的竞争力。扩展自己的专业网络,与其他开发者和雇主建立联系。
  2. 寻找新兴领域:探索新兴的技术领域,如大数据、人工智能和物联网等,这些领域对前端开发者的需求逐渐增加,可能为你提供新的机会。
  3. 转型或深耕细分领域:如果市场需求不断减少,可以考虑转型到与前端相关的领域,如UI设计、交互设计或用户体验设计等。或者在前端领域深耕细分领域,在特定行业或特定技术方向上寻找就业机会。

结论

 虽然当前的行业环境确实严峻,但前端开发作为连接用户与产品的重要纽带,在未来依然有着广阔的发展空间。关键在于前端开发者要不断自我调整与进阶,持续学习并适应市场需求。通过多元化技能、学习实践、提升个人竞争力以及面对市场挑战,前端开发者依然可以在这个变革时代中谋得一席之地。


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

一代枭雄曹操也需要借力,何况我们

前言1、人情世故如果做得好就会说是情商高,做不好会说是世故,这是冯仑老师一段话,然后怎么做不世故呢,也很难评判。借着这个聊聊人情世故,在我看来它也是做事规则的一部分,我们发展很长一段历史,从不同的立场、不同的利益分出了派别,又从血缘关系分出了宗族,这些都是为了...
继续阅读 »

前言


1、人情世故

如果做得好就会说是情商高,做不好会说是世故,这是冯仑老师一段话,然后怎么做不世故呢,也很难评判。

借着这个聊聊人情世故,在我看来它也是做事规则的一部分,我们发展很长一段历史,从不同的立场、不同的利益分出了派别,又从血缘关系分出了宗族,这些都是为了利益最大化的一个产物。

反观博主本人,典型理工男,执着技术研究,所以这块一直是弱项,不太会讲话,但是我人缘一直比较好的。当然有利也有弊,弊端的话比较明显的,当一个人说话很厉害的时候,会给人自信,给人觉得靠谱,当一个人说话不咋样的时候,其实也有好处,就是藏锋,你不说出来个人想法大家是不知道你心里的小九九的,所以保全了你自身。(当一个人份量足的时候,说话会引发很大的影响,所以你可以发现如果一个人在公开场合大发演讲,要么是初出茅庐要么就是有靠山)

2、人生的发展需要平台

王立群老师:人生发展往往需要平台,秦国李斯这么一个故事,他发现仓鼠跟厕鼠待遇很不一样,同样是一个物种,但是一个光明正大的吃着粮食,一个过街老鼠人人喊打,所以他悟到了一个道理,人生好的发展需要借助平台的。

我们今天讲的人物:曹操,我们还是从几个学习角度去看,一个是做事的方法,另一个我们从他的事迹里面看出成事的借力的这么一回事。

曹操


出身

他祖父是一个大太监,伺候皇后还有皇上,古代有三股力量,两股都是因为比较亲近产生的,一个是外戚,另一个太监,还有一股力量是文官,这个是人数最多的。那么他祖父权利很大的,然后收了一个义子也就是曹操的父亲,然后他本身属于夏侯家族,所以他带的资源是曹家还有夏侯家非常有实力。

他并没有说直接躺平,而是想着有所作为,接下来我们再看看他的做事方面

做事手段

1、许劭风评

古代有个一个规则,靠着这些有能力、有品德的人来进行推荐人才,曹操想出来做事,他找到许劭,一开始是不肯的,因为前面讲过三股力量,文官是很鄙视太监的,后面曹操使了点手段最终让许劭给他做了风评,然后他听完大笑而去。

idea:从这件事看做什么事都是有个窍门,这个方式是别人建议曹操这么干,所以做事要恰到好处。另外里面提到曹操使了点手段,哈哈透出了一个狠,有点东西。

2、傍大腿

曹操曾经在袁绍下面干活,然后好几次都把自己的精锐干没了,袁绍作为盟主,慷慨的给予兵马才得以恢复元气。

idea:我们看曹操的出身,这么牛逼的背景,他也需要大腿的支持,更何况普普通通的我们。

3、挟天子以令诸侯

这个是非常著名的历史典故,也是因为这个跟袁绍闹掰了,当汉献帝去了洛阳的时候,他马上去迎接,然后用这个发号施令讨伐别人。

idea:曹操的眼光十分毒辣,他看出潜在的价值,不愧是曹老板。

4、善用人才

像官渡之战,像迎接汉献帝,都是底下这批谋士给的主意,曹操手下文官是人才济济的,另外这个老板是善于听从这些好的计谋,这是非常重要的。

官渡之战,袁绍没有听从谋士的重兵把守粮草,导致给了曹操抓住了机会,乌巢一把火烧光了粮草。

个人看法

a、平台是重要的,借力也是需要的

从曹操的发迹来看,他站在一个大平台上面,不像刘备四处投奔。人并不是说能力很强就能表现出来,需要有平台,有这么伯乐去发现你,然后有这么一股力量在你困难的时候拉你一把,这是重要的。

b、曹操做事狠

这里的狠,不是残暴,而是毒辣,眼光毒辣、做事方式到位,我们从善用人才,许劭风评,挟天子以令诸侯,这些做的都很到位。举个例子,比如说我们要煮开一壶水,需要火柴、木头、可能需要鼓风工具,这都是关键那些点。

这个我们前面也提到了,做事一定要有所研究,事情的关键点是什么,当然有这么一群得力助手也很重要,发现关键突破点。所以古代对英雄标准是:腹有良策,有大气概。

c、驾驭人

司马家起来是在曹操去世后几代的事情,可以说在曹操在的时候,这些有心机的人没有动作的,侧面看出曹操的厉害之处,懂人心。在资治通鉴里面也有一个例子,就是桓温,他也是古代一个权臣,后面几代就不行了压不住这批人。

学历史,学读懂人心


历史里面基本都是那个朝代的精英,他们的事迹,做事方法,当然我们看到很多东西,包括抱负、无奈、遗憾;我们学的不仅仅是做事方法,避开权谋的陷阱,还有就是学习读懂人心、人性。当我们谈到这个,大家第一印象就是坏的人性,其实它是一种自然的表现,就像饿了就要吃饭。

《百家讲坛》里面讲了这么一个故事,曹操的下邳之战生擒了吕布,原本曹操很爱惜人才的,后面刘备的一句话:吕布对以往老板不好,而曹操生性多疑,最终嘎了吕布。王立群老师:人们往往看重结果,以结果说话,而不是问你这么做的原因。

是啊,我们在故事背后,看到整件事情人心的博弈,刘备被人称为仁义之君,但是他在那会落进下石了,因为他之前跟吕布有些矛盾的,吕布把他从原来的根据地赶走了,当然他说的也是事实。所以我们除了学习历史,还需要去洞察人心,往往这些能决定事情的走向。


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

独立开发前100天真正重要的事

我从4月开始离职开始全职做独立开发,算是真正踏进入了这条河流。在过去半年多了我也观察了很多独立开发者。自己目前算是过了新手村(有正常的开发节奏,有3万用户)。看到很多刚起步的独立开发者还是有很多疑问,所以分享一下我在独立开发最初期的一些经验。因为我也不是很成功...
继续阅读 »

我从4月开始离职开始全职做独立开发,算是真正踏进入了这条河流。在过去半年多了我也观察了很多独立开发者。自己目前算是过了新手村(有正常的开发节奏,有3万用户)。看到很多刚起步的独立开发者还是有很多疑问,所以分享一下我在独立开发最初期的一些经验。因为我也不是很成功(没有走的很远),所以只能分享独立开发前100天的经验。

先说一下我认为独立开发起步阶段面临的主要困难:

第一:没有公司的孤独感。如果是一个人全职开发就更寂寞了。即使有一两个合作伙伴,但是大概率也是异地,因此也算是网友性质的社交。人说到底是群居的,所以需要找到一种社交平衡。我想可能这也是很多独立开发白天要在外面地方待着的原因,也许一个人一直在家待着有点闷。

第二:无法建立产品的健康开发节奏。以前在公司的时候自己是流程里的一环,只关心自己分工的完成情况。做了独立开发以后,所有事情都需要自己决策。太多自由的结果就是没了方向。什么都想做,好像什么都可以做,又感觉什么都不好做。

第三:没有收入。产品从开始建造到有足够健康收入中间有一段过程。这还有一个前提要是一个真正有用户价值的产品。如果你起步的时候自己没有一个优势产品方向,又没有个人社区号召力,就算你的产品是好的,也需要一段时间(可长可短)才能获得有效收入。一上线就火的概率太小了。高强度投入一件事情,如果长期没有收入,家人会有很多质疑,可能最后自己也很怀疑自己。

我把这三点结合起来,编一个故事大家可能比较有画面了:

一个人做独立开发已经半年多了,产品设计都是自己做,也没有什么人可以讨论,不知道下一步该做什么。目前每天只有零星的新增。做了这么久,总共只有两三千的收入。看来做产品还得会营销**,打算最近开始学习一下运营**。最近也打算做一下AI产品,感觉这个赛道很火。老婆说如果不行就早点回去上班好了,总不能一直这样。家人们,你们说我应该坚持吗。

家人们会说你的产品很棒,会说你做的比他们强,会说下次一定,会说你未来会成功。但是家人们不会为你掏一分钱。

也许我们不知道如何成功,但是我们可以知道什么是失败。你知道的失败方式越多,你成功的概率就越大。总的来说,产品的成功就两个要点:有用户价值,能赚钱。注意,这两点是或的关系,不是且的关系。一个产品可以能赚钱,但是没有用。一个产品也可以有用,但是不赚钱。失败就是你做的产品:既没用,又不赚钱

基于前面提到的三个困难,我得出的前100天最重要的事是:找到一个可行的产品迭代方向。和团队经过磨合,互相能有有效、信任的协作。找到一百个种子用户。你越早解决这三个困难,你越快走上轨道

确认产品方向

如果你真的做过产品,你就知道最终正确的产品路径不是通过脑中的某刻灵光乍现得到的。所以不是那种大脑飞速运算解题的方式。这里有两件事情需要确认:大的产品方向,产品的路径。

比如阿里巴巴,马云不是一开始就做的淘宝网。他只是觉得互联网普及以后,电子商务会有需求。最开始做的是黄页,并不是淘宝网。但是他没有在第一个项目失败以后,去做门户网站。产品路径的例子是特斯拉。特斯拉很早就确定了先出高性能的跑车,高性能轿车(model s),有了前面的技术积累以后,最后通过推出平价的轿车赢得市场(model 3)。特斯拉在 model 3 大规模量产前都是亏损的。

所以最重要的是确认产品方向。这个方向要结合自身的情况进行设定,就是我在前面帖子里提到的要是你想做的,能做的。也许想达到的产品方向有很多工作量,这个时候就要有同步的产品路径。比如小米手机的创业,他们一开始就想造手机。但是直接启动手机的制造市场、技术都有很大的困难。于是他们先通过做 MIUI 入局。

这里面首先要有个大的方向判断,对于独立开发来说,我觉得张宁在《创作者》里提到的两个维度的方向挺有意思:大众、小众;高频、低频。这里面两两结合各有什么特点我这里就不展开了,大家可以自行体会。

但是可以明确的是,独立开发者做不了又大众又高频的应用。大众又高频,就不可能小而美。大众又高频,最后赢家除了产品能力,要有运营优势,要有资源优势。独立开发者通常没有运营优势和资源优势。另外一点,如果是小众低频,就一定要高忠诚,高付费转化。可以往大众低频或者小众高频的方向多想想

产品方向选择还有一个建议就是要有秘密。成功的业务后面一定有秘密。秘密也回答了一个问题:如果这个需求真的存在,为什么用户选择了你的产品。

最初级的秘密就是信息差,你知道别人不知道,所以你可以,更早做,可以更低的成本,更高效,有更高的获客率。

更高级的秘密就是大家都能看到,但是大家知道了,但是大家不信(脑中想到了拼多多的砍一刀)。

最高级的秘密就是所有人都知道,但是他们做不到。

总结起来,你应该找到一个你有优势的细分方向。信息优势,洞察优势也是优势。

没有一发即中的银弹,最平凡的方式想很多方向,用最低成本进行最快速的验证。在反馈中渐渐明晰产品路径。如果你三个月不管反馈闷头做,只做出了一个产品方向。你失败的概率是很大的。所以我看到很多产品1.0 的时候就做会员,做社区,做跨平台我是很不理解的。其实这些功能在早期性价比很低。

我的方式是脑海中有10个想法,挑出3个想法做初步设计,选出一个或者两个想法做产品验证。可能是原型,数据是模拟的,没有设计,如果产品真的解决了痛点的话,用户会愿意用,然后他会给你反馈他想要更好的体验,他愿意付钱得到这些改进。这里的效率优势是,你能在更短的时间验证产品方向是不是对的。总比走了3个月才发现是一条死胡同要好。

开发者很容易因为想到一个想法很兴奋,觉得这个很有用,就闷头做了一个月。有可能的问题是,这个想法虽然是个痛点,但是这个痛点频次很低,场景很少,所以虽然有用,但是没人会愿意买单。所以尽量跳出自己的思维,从用户的角度来进行验证是很必要的。

团队协作

独立开发的开发方式和传统公司不同。需要建立一个全新的工作流程。在初期大家都是空白,所以需要通过产品迭代中,形成高效的开发默契。大家松散做东西,工作习惯,工作职责都需要有共识才行。

比如我合作的设计师早期喜欢一次做一大板块的整体设计,大概一周的工作量。初期我觉得我们对产品有激情,大家都应该有自由的发挥空间。但是做了一周的设计图和产品脑海中的产品行进方向不一致怎么办。在工作时间上,我合作的设计师因为目前还是兼职,他只能在下班后设计。然而我全职只在6点前工作。这又是一个要协调的地方。

如果你是一个产品,需要协调研发和设计,三个人协调就又更复杂了。要找到一个大家都舒服,高效的协作方式。

100个种子用户

独立开发最核心的一环就是找到一个健康的商业模式。产品方向和团队协作的目标都是为了未来可以达成一个健康的商业模式。我觉得太多独立开发者上来就把目标(野心)定的太高。一口吃不成胖子。独立开发早期的商业目标只有一个:尽快达成团队最低维持标准。一鸟在手胜过二鸟在林。不要在团队只有几个人的时候用几十个人的方式管理。

初期就要估算出产品(团队)能够持续运转的最低收入。这个成本越低,团队就越容易跑起来。当收入足够覆盖团队的成本后,你的心态就会得到极大的自由,可以尝试很多奇奇怪怪有趣的想法。所以早期不要想有多高的天花板,如何建立壁垒,就关心如何达成产品的及格生命线。谁会想做一个注定失败的产品呢。

早期在没有运营优势的情况下,最重要的指标就是用户满意度了。用户满意度,就暗示了这个产品有没有解决切实的用户问题,用户愿不愿意为你宣传。其实很多人都搞错了重点,在产品没有让100个种子用户满意前,新增的流量是没有意义的。因为再多的用户都会流失。竹篮打水一场空。如果你把产品的用户目标定在100个种子用户,你也就没了运营压力,可以关注在如何打造正确的产品上。在产品基本盘没有问题后,再思考后面的才有意义。

总结

总结起来三点就是:做什么(产品方向),怎么做(团队协作),为谁做(验证用户)。以上就是我全职独立开发3个多月以来肤浅的经验分享,希望对你有帮助。


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

《程序员职场工具库》从行动开始 —— MORS 法则

你是否曾经有过类似的疑惑?我尝试过好几次健身,但都坚持不下来,我是不是一个没有耐心,没有毅力的人?领导反馈说我需要提升沟通能力(开发效率、主动性),可是我要怎么提升呢?我知道该怎么做,但就是做不到呀。在我们的个人成长中,出现这些疑惑是很正常的,它们可以归纳为以...
继续阅读 »

你是否曾经有过类似的疑惑?

  • 我尝试过好几次健身,但都坚持不下来,我是不是一个没有耐心,没有毅力的人?
  • 领导反馈说我需要提升沟通能力(开发效率、主动性),可是我要怎么提升呢?
  • 我知道该怎么做,但就是做不到呀。

在我们的个人成长中,出现这些疑惑是很正常的,它们可以归纳为以下两个原因:

  • 不知道方法
  • 知道方法但不知道该怎样坚持

如果你经常被这两个问题困扰,长此以往,就会陷入自我怀疑的状态。这时,MORS 法则可以帮到你。

MORS 法则,又叫做具体性原则。MORS 法则认为,我们的行为只有符合Measured(可测评)、Observable(可观察)、Reliable(可信赖)、Specific(明确化) 这 4 个要素,才是一个具体的、可执行的行为。

针对上面说到的问题,我们不要光喊口号,而需要给出具体的、可执行的方案,然后行动起来!

也就是说,当你碰到一个很复杂的目标时,要尽量拆解目标,制定一系列符合 MORS 法则的行动计划,然后开始行动,这样就能达成目标

让我用一个栗子来解释 MORS 法则,假想一下,你今年给自己立了一个 flag:“今年要跑半马(21公里)”。要是只有这样一个目标,你能做到吗?大概率是不能的,即使你知道要每天坚持跑步,也很难坚持下来。你有可能会被临时的一些事情耽搁了几天就放弃了;你有可能在坚持了一段时间之后发现自己“根本做不到”就放弃了。

想要达成这个大目标,你可以尝试先拆解目标。假设第一次开始跑步,你可以跑 1 公里,希望 10 个月后能跑 21 公里,那就是每个月要多跑 2 公里;然后再拆到每周要多跑 0.5 公里;然后再拆解到每 2 天多跑 0.07 公里左右。这样分拆之后,就可以罗列出每天的跑步行动计划,比如:xx月xx日,跑 1.14 公里;xx月xx+1日,跑 1.21 公里... 这些行动是符合 MORS 法则的:

  • Measured:这些行动是可以被测量和评估的,你可以明确知道今天计划跑多少公里,最终有没有完成这个行动也可以很清楚地衡量和评估。
  • Observable:这些行动是可见的,你要跑起来,可能需要一些跑步装备和资源(比如准备水),这些都是实实在在的行动,不是 YY。
  • Reliable:这些行动是可以完成的,它不会太难,每天多跑 0.07 公里远远比多跑 20 公里要简单太多了,肯定能做到吧?
  • Specific:这些行动是非常明确的,就是跑步,不会有什么歧义。

当然,肯定有更好的、更加符合运动理论的跑步计划,比如是不是每天跑,还是一周休息 2 天、是应该每天突破,还是隔一天突破等等。这里只是举例,就简单一点了。不过不管是什么跑步计划,原理是不变的。

当你制定了符合 MORS 法则的行动计划之后,就能够更加容易地达成目标,也更加容易坚持下来了。因为:

  • 行动计划是具体的,明确的,可行的。相比原来的假大空的目标,它更容易做到!而且,按照这个行动计划来行动,就能达成目标。
  • 能够不断地感受到成果,这会帮助你更容易坚持下来。在这个栗子中,就是每天都多跑了一些,感受到自己每天都在进步,这是强大的正反馈,会给予你力量。

注意不要跟 SMART 原则搞混了哈。SMART 原则是制定目标时使用的,MORS 法则是制定行动计划的时候使用的。它们有一些相同的点,比如 Measured 和 Specific,那是因为每一个行动本身也是带有目的性的,所以 MORS 法则和 SMART 原则会有重叠的地方。

好了,MORS 法则的介绍就到这里了,它的内容非常简单,但是 MORS 法则对于我们的个人成长来说,作用是非常大的,希望你们可以多加实践啦。

除了在个人成长上面有帮助之外,MORS 法则对于管理者指导下属也非常有用,后面有机会我再介绍吧。

加油,让我们从行动开始!


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

我的日常开发收获

passive event listenerspassive event listeners 是一种新兴的web标准,Chrome 51中提供的新功能为滚动性能提供了巨大的潜在提升。Chrome Release Notes.背景:所有的现代浏览器都有一个滚动功...
继续阅读 »
  1. passive event listeners

passive event listeners 是一种新兴的web标准,Chrome 51中提供的新功能为滚动性能提供了巨大的潜在提升。Chrome Release Notes.

背景:所有的现代浏览器都有一个滚动功能的线程,可以保证即使在运行耗时的js代码时滚动也能够平滑进行,但这种优化部分因需要等待任何touchstart 和 touchmove处理程序的结果而失败,因为这些交互可能会通过调用preventDefault() 事件来完全阻止滚动。

于是有了 {passive: true}

通过将touchwheel事件监听标记为 passive,开发人员承诺处理程序不会调用 preventDefault来禁用滚动。这使浏览器可以立即响应滚动,而无需等待js的执行,从而确保为用户提供可靠流畅的滚动体验。

  1. 关于系统设计

    • 关于分层

分层一般是基于模块功能来分层,有时候分层不清晰可能是有哪些模块,各模块间的功能,整个功能流程不是很清楚。

有时候两个模块间的交互复杂度增加,可以考虑构建一个中间层。 这样可以保持两个模块不会杂糅相关度不是很高的处理逻辑,功能逻辑更纯粹,保持边界清晰,降低模块本身的复杂度。

  1. 关于编程思维

仔细想来,虽然从事开发工作很久了,但是编程上还是很没有章法,架构设计能力较弱,多年来都是凭借以前热血和几分小聪明存活。

今日份反思:

拿到一个需求,分析该需求需要支持那些场景,为了支持这些场景它需要具备哪些功能,思考怎样实现这些功能,根据功能做模块划分,对这些模块进行分析,做逻辑抽象(也就是分层),然后整个需求实现的大致框架就心中有数了,开始产出技术方案。 技术方案产出后,按照技术方案的设想去实施,实施过程中可能会遇到没考虑到的场景,或者发现之前的设计不能很好的cover,调整设计,增加分层或者调整已有的分层,然后修改技术方案。 不断的经历上述过程,会慢慢的沉淀出一些业务通用的设计思路,这样下次再做技术方案的时候就不会很迷茫。脑子理清楚,而后出设计,实践后总结。

  1. ShadowRealm API

一个进入 statge3 的新的 JavaScript 提案,用于创建一个独立的JavaScript运行环境,里面有独立的变量作用域。

数据结构:

declare class ShadowRealm {
  constructor();
// 同步执行字符串,类似eval()
  evaluate(sourceText: string): PrimitiveValueOrCallable;
// 返回一个Promise对象,异步执行代码字符串
  importValue(specifier: string, bindingName: string): Promise<PrimitiveValueOrCallable>;
}

使用场景:

  • 在Web IDE 或 Web绘图应用程序中运行插件等第三方代码; 这种方式比iframe的实现更简单、灵活度更高,占用内存更少、代码的安全性更高。
  • 用 ShadowRealms 中创建编程环境,运行用户代码,如codepen,codesandbox;
  • 服务器可以在 ShadowRealms 运行第三方代码,防止第三方代码出错打挂主环境;
  • 网页抓取和网页应用测试可以在ShadowRealms中运行;

补充:Node.jsvm模块与ShadowRealm API类似,但具有更多功能,缓存Javascript 引擎,拦截import() 等等。

  1. 关于状态管理

做状态管理的核心就是监听数据的变化,监听数据的变化有两种方式:

  • 提供api来修改,内部做联动处理(React的setState)
  • 对对象做一层代理,set的时候做联动处理,同时get时收集所有依赖。(vue,mobx的响应式数据)
  1. 需求/调研

关于需求调研,我还是很急躁,急急忙忙开始技术方案评审、开发、排期的话,就会导致整个开发过程很被动。

比较好的方式,前期对于自己做的需求,以及需求的各个功能依赖有比较充分的了解(不过这在很多功能依赖方都没有文档的情况下很难做到),然后写一个符合需求的简版的demo,对可能出现的阻塞点和解决方案心里有一个预期,把图纸画好,照着图纸开发,从而更好的掌控整个开发进度。

  1. 功能/需求拆解

对功能需求的有效拆解,功能模块详细具体,粒度合适,不会太过细化,也不会忽略一些关键点,才能够更好的把控整个开发进度,评估可能出现的风险点。

  1. 工具方法收敛思路

收敛-内部集中分发给各个具体的util处理,保证对外暴露的接口的统一,降低该方法使用的心智负担。

  1. 关于「自顶向下」和「自底向上」

自顶向下:

程序设计时,先考虑整体,后考虑细节。先考虑全局目标,后考虑局部目标。不要一开始就过多追求总多的细节,先从最上层总目标开始设计,逐步使问题具体化。

模块化设计:一个复杂的问题,肯定是有若干稍简单的问题构成。模块化是把程序要解决的总目标拆解为子目标,再进一步细化分解为具体的小目标,把每一个小目标称为一个模块。

自底向上:

自底向上的设计简单来说就是先完成细节功能,每个细节功能抽象成一个运算符,然后将这些完成的细节功能组装到整体的架构中。

自动化的设计是不是就应该采用自底向上的设计思路,把每个需要的细节功能做抽象,使得配置规则的人可以任意组装,对于不支持的功能制造新的抽象?

  1. eval 和 new Function的区别

eval 和 new Function都可以解析执行一段传入的字符串。但有以下不同的地方:

  • eval中的代码是当前作用域,它可以访问当前函数中的局部变量和全局变量。new Function中的代码执行的作用域是全局作用域,不论它在哪个地方被调用,可访问的都是全局变量;
  • eval接收函数作为字符串时需要“(”和“)”作为前缀和后缀,new Function不需要,new Function可以接收N个参数,最后一个参数作为函数体;
  • eval不容易调试,用chromeDev等调试工具无法打断点调试;
  • 性能问题,eval通常比其他替代方法更慢,因为他必须调用js解释器,而其他结构则可被现代js引擎优化;
  • eval存在安全问题,因为可访问局部作用域的变量,其内部逻辑不可预测性很强,可能导致XSS攻击;
  1. 乐观更新与保守更新
  • 乐观更新(Optimistic Update): 乐观更新:如果有编辑等改动,先更新前端页面,再像服务端发送请求,如果请求成功则结束操作,无需额外处理,若请求失败,则页面回滚到先前状态; 这样更新方式的优点是响应及时,缺点就是低概率的请求失败回滚的体验不太好。
  • 保守更新(Perssimistic Update): 保守更新:如果有编辑等改动,向服务端发送请求,等收到回复请求后再响应用户操作,在此之前用户都需要处于等待状态。 这样做的缺点是会使页面有比较大的延时感,优点是最终呈现的结果是可信赖、稳定可靠的。
  1. 正交的概念

编程上的正交,从数学上引进这个词,用于表示相互独立,相互间不可替代,并且可以组合起来实现其他功能。比如if和for语言是正交的,但for和while与句的功能是有重叠的。逻辑运算not、and也是正交的,其他复杂的逻辑运算都可以用这三种基本运算叠加起来。 编程语言经常定义一组正交语法特性,相互间不可替代,组合起来可以实现其他功能。为了更方便使用,在基础特性之上,再添加一些额外特性。这些非基本的额外特性,成为语法糖。语法糖对语言的功能没有太大影响,只是有了,代码写起来更方便些。

  1. 引入外部字体,因等待字体文件的加载而产生文字不可见问题的一些解决方案
  • 临时显示系统字体:添加font-display: swap;到自定义字体的style中,在自定义字体加载好之前显示系统字体;
  • 预加载网页字体:用<link rel="preload" as="font" >更早的获取字体文件。
  1. 总结写技术文章的几个步骤,引导自己学习并践行:
  • 学习优秀的人写的东西,看看他们的理解;
  • 带着自己的疑问,和目前接收到的理解去读源码;
  • 读完之后,按照自己最终的理解绘制相关逻辑的流程图;
  • 针对关键功能模块做拆解和源码解读;
  • 总结相关功能的实现机制,以及给我带来的启发和思考;
  1. 关于团队中被高频讨论的去底座;

去底座不是完全失去对底座(数据)的访问能力,而是设计一个标准化的API来支持按需访问底座(数据)的能力。


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

用小明的故事随便谈谈kotlin中的apply等函数

前言本文仅简单描述一下kotlin中常用到的scope function,如apply,let,run,with,also等函数的常用方法和选取。即使很多情况下选择不同函数,也同样都能达到最终效果,具体选择哪个函数我们不会严格约束,但如果你是对代码规范要求比较...
继续阅读 »

前言

本文仅简单描述一下kotlin中常用到的scope function,如apply,let,run,with,also等函数的常用方法和选取。即使很多情况下选择不同函数,也同样都能达到最终效果,具体选择哪个函数我们不会严格约束,但如果你是对代码规范要求比较高的,最好建立良好的代码习惯。

一般对比

函数一般使用场景函数定义上下文对象可用作返回值
apply在需要对对象进行初始化或配置的时候使用public inline fun <T> T.apply(block: T.() -> Unit): T接收器this返回值是对象本身
also在需要对对象执行额外操作并返回原对象的时候使用public inline fun <T> T.also(block: (T) -> Unit): T变量it返回值是对象本身
let在需要对对象进行非空判断并执行特定操作的时候使用public inline fun <T, R> T.let(block: (T) -> R): R变量it返回值是 lambda 结果
run在需要对对象进行多个操作,并返回一个结果的时候使用,通常是一个新的对象或其他public inline fun <T, R> T.run(block: () -> R): R 接收器this返回值是 lambda 结果
with在不拥有对象的上下文的时候使用public inline fun <T, R> with(receiver: T, block: T.() -> R): R接收器this返回值是 lambda 结果
  1. apply 函数接收一个 lambda 表达式作为参数,并返回被调用对象本身。通过 apply,可以在对象创建后立即对其进行链式操作,设置属性值、调用方法等。适合用于链式初始化或配置一些属性。
val person = Person().apply { 
name = "John"
age = 30
}
  1. also 函数接收一个 lambda 表达式作为参数,lambda 表达式中的 it 引用指向调用 also 的对象。通过 also,可以对对象进行额外的操作,而原对象仍然是函数调用的结果。适合用于在对象操作过程中执行额外的副作用操作。
val modifiedObject = myObject.also {
// 额外操作 it
}
  1. let 函数接收一个 lambda 表达式作为参数,lambda 表达式中的 it 引用指向调用 let 的对象。如果对象不为空,则执行 lambda 表达式内的操作,并返回 lambda 表达式的结果。适合用于安全地操作对象,避免空指针异常。
val result = nullableValue?.let {
// 操作非空对象 it
}
  1. run 函数接收一个 lambda 表达式作为参数,lambda 表达式中的 this 引用指向调用 run 的对象。通过 run,可以便捷地对对象进行多次操作,并返回最后一个表达式的结果。适合用于执行一系列操作并返回最终结果。
val result = myObject.run {
// 对象操作1
// 对象操作2
// ...
// 返回结果
}

  1. with 函数接收一个对象和一个 lambda 表达式作为参数,lambda 表达式中的 this 引用指向传入的对象。通过 with,可以在没有对象接收者的情况下操作对象,并返回最后一个表达式的结果。适合用于对对象进行一系列操作,而无需在乎返回值。
val result = with(myObject) {
// 对象操作1
// 对象操作2
// ...
// 返回结果
}

小明的故事

故事是这样的

  1. 小明今年上一年级
  2. 但是家长跟学校说,小明是个天才,现在可以直接跳级到二年级
  3. 学校给二年级分配的老师是王老师,是个女教师
  4. 半学期后,王老师怀孕了需要休息,于是学校给王老师放假
  5. 学校给二年级分配了新的李老师,小明有了新老师

下面是故事的代码:


data class Student(var name: String = "", var grade: String = "", var teacher: Teacher? = null) {
//插班跳级
fun needSkippingGrade(insertGrade: String) {
this.grade = insertGrade
}
}

data class Teacher(var name: String = "") {
fun relax() {
println("$name 休假了!")
}
}

fun main() {

//1. **小明**今年上一年级
val xiaoming = Student()
.apply {
name = "小明"
grade = "一年级"
println("小明开始前: $this")
}
.also {
//2.现在可以直接跳级到二年级
it.needSkippingGrade("二年级")
println("小明插班后: $it")
}

//3. 学校给二年级分配的老师是**王老师**,是个女教师
val ownTeacher = xiaoming.teacher?.let {
println("小明当前的老师不为NULL,是${it}")
} ?: Teacher("王老师").also {
xiaoming.teacher = it
println("小明有了老师: $xiaoming")
}

fun changeStudentCurrentTeacher(student: Student): Teacher? {
return student.run {
teacher?.relax()
Teacher("李老师")
}
}

//4. 半学期后,王老师怀孕了需要休息,于是学校给王老师放假
//5. 学校给二年级分配了新的**李老师**,小明有了新老师
with(xiaoming) {
println("开学了!")
println("半学期后,王老师怀孕了!...")
val newTeacher = changeStudentCurrentTeacher(this)
println("新老师是$newTeacher")
teacher = newTeacher
println("小明有了新老师$this")
}

}

输出结果:

小明开始前: Student(name=小明, grade=一年级, teacher=null)
小明插班后: Student(name=小明, grade=二年级, teacher=null)
小明有了老师: Student(name=小明, grade=二年级, teacher=Teacher(name=王老师))
开学了!
半学期后,王老师怀孕了!...
王老师 休假了!
新老师是Teacher(name=李老师)
小明有了新老师Student(name=小明, grade=二年级, teacher=Teacher(name=李老师))

最后

实际过程中,需要根据具体的场景和需求来选择适合的函数。前面这些函数在 Kotlin 中提供了更简洁、可读性更高的方式来处理对象,根据不同的使用场景,你可以选择最适合和更易读的函数来操作对象


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

当面试官问你BroadcastReciver的静态注册与动态注册的区别,你又该作何应对?

什么是广播,简单点广播就是安卓系统本身发出的声音,我们可以通过安卓提供给我们的一系列内容来接收和发出广播,以此来简单快捷地实现一些功能。在实际开发中也常常用到,而是否熟悉使用,这成为面试官最常问的问题。当面试官问你:1.请问BroadcastReciver的静...
继续阅读 »

什么是广播,简单点广播就是安卓系统本身发出的声音,我们可以通过安卓提供给我们的一系列内容来接收和发出广播,以此来简单快捷地实现一些功能。

在实际开发中也常常用到,而是否熟悉使用,这成为面试官最常问的问题。

当面试官问你:

1.请问BroadcastReciver的静态注册与动态注册的区别?,你在开发中用过吗?

答:

其实广播分为两种基本类型:

在一个程序中,可以发送广播供当前程序的广播接收器收到。首先我们来看下两种方式的发送广播。 在Android系统中,主要有两种基本的广播类型: - 标准广播(Normal Broadcasts) - 有序广播(Ordered Broadcasts)

标准广播:

是一种完全异步执行的广播,在广播发出之后,所有的广播接收器会在同一时刻接收到这条广播,广播无法被中断。

发送广播的方式十分容易的,只需要实例化一个Intent对象,然后调用context的** sendBroadcast() **方法。这样就完成了广播的发送。

        //intent中的参数为action
       Intent intent=new Intent("com.example.dimple.BROADCAST_TEST");
       sendBroadcast(intent);

有序广播:

是一种同步执行的广播,在广播发出之后,优先级高的广播接收器会先接收到这条广播,并可以在优先级较低的广播接收器之前终止发送这条广播。

        //intent中的参数为action
       Intent intent=new Intent("com.example.dimple.BROADCAST_TEST");
       sendOrderBroadcast(intent,null);//第二个参数是与权限相关的字符串。

到此时,如果你的程序中只有一个广播接收器的话,是体现不出有序广播的特点的, 右击包名——New——Other——BroadcastReceiver多创建几个广播接收器。

此时你还是会发现,所有的广播接收器是同时接收到广播消息的。注意上面介绍的时候说到优先级,这个时候我们需要设置优先级,在AndroidManifest文件中的Receiver标签中设置广播接收器的优先级。

        <receiver
           android:name=".MyReceiver"
           android:enabled="true"
           android:exported="true">
           <!--注意此时有一个Priority属性-->
           <intent-filter android:priority="100">
               <action android:name="android.intent.action.BROADCAST_TEST"></action>
           </intent-filter>
       </receiver>

优先级越高的广播接收器优先收到广播,也可以在收到广播的时候调用abortBroadcast() 方法截断广播。优先级低的广播接收器就无法接收到广播了。

面试官,假设我有一个接收者如下:

在Android的广播接收机制中,如果接收到广播,就需要创建广播接收器。而创建广播接收器的方法就是新建一个类(可以是单独新建类,也可以是内部类(public)) 继承自BroadcastReceiver

   class myBroadcastReceiver extends BroadcastReceiver{

       @Override
       public void onReceive(Context context, Intent intent) {
           //接收到广播的处理,注意不能有耗时操作,当此方法长时间未结束,会报错。
           //同时,广播接收器中不能开线程。
      }
  }

面试官,接下来就是面临注册的问题了,有两种注册方式,一种是动态注册,一种是静态注册

所谓动态注册是指在代码中注册。步骤如下 :

  • 实例化自定义的广播接收器。
  • 创建IntentFilter实例。
  • 调用IntentFilter实例的addAction()方法添加监听的广播类型。
  • 最后调用Context的registerReceiver(BroadcastReceiver,IntentFilter)动态的注册广播。

这个时候,已经为我们自定义的广播接收器关联了广播,当收到和绑定的广播一直的广播的时候,就会调用广播接收器中的onReceiver方法。

        MyBroadcastReceiver myBroadcastReceiver=new MyBroadcastReceiver();
       IntentFilter intentFilter=new IntentFilter();
       intentFilter.addAction("com.example.dimple.MY_BROADCAST");
       registerReceiver(myBroadcastReceiver,intentFilter);

这里需要注意的是,如果需要接收系统的广播(比如电量变化,网络变化等等),别忘记在AndroidManifest配置文件中加上权限。另外,动态注册的广播在活动结束的时候需要取消注册:

    @Override
   protected void onDestroy() {
       super.onDestroy();
       unregisterReceiver(myBroadcastReceiver);
  }  

静态注册:

在创建好的广播接收器中添加一个Toast提示。代码如下:

public class MyReceiver extends BroadcastReceiver {
   @Override
   public void onReceive(Context context, Intent intent) {
       Toast.makeText(context,"开机启动!",Toast.LENGTH_LONG).show();
  }  
}

然后在AndroidManifest文件中添加:

  • 权限 <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"></uses-permission>

  • Intent-filter

            <receiver
               android:name=".MyReceiver"
               android:enabled="true"
               android:exported="true">
               <!--添加以下3行-->
               <intent-filter>
                   <action android:name="android.intent.action.BOOT_COMPLETED"></action>
               </intent-filter>
           </receiver>

    此时重启Android系统就可以收到开机提示了。

总结:

动态注册静态注册的不同:

动态注册的广播接收器可以自由的实现注册和取消,有很大的灵活性。但是只有在程序启动之后才能收到广播,此外,不知道你注意到了没,广播接收器的注销是在onDestroy()方法中的。所以广播接收器的生命周期是和当前Activity的生命周期一样。

静态注册的广播不受程序是否启动的约束,当应用程序关闭之后,还是可以接收到广播。

标准广播和有序广播的接收和发送都是全局性的,这样会使得其他程序有几率接收到广播,会造成一定的安全问题。为了解决这个问题,Android系统中有一套本地广播的机制。这个机制是让所有的广播事件(接收与发送)都在程序内部完成。主要是采用的一个localBroadcastReceiver对广播进行管理。


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

终于搞明白了什么是同步屏障

背景今天突然听到隔壁在讨论同步屏障,听到这个名字,我依稀记得 Handler 里面是有同步屏障机制的,但是具体的原理怎么有点模糊不清呢?就像一个明星,你明明看着面熟,就是想不起来他叫啥,让我这样的强迫症患者无比难受,所以抽时间来扒一扒同步屏...
继续阅读 »

背景

今天突然听到隔壁在讨论同步屏障,听到这个名字,我依稀记得 Handler 里面是有同步屏障机制的,但是具体的原理怎么有点模糊不清呢?就像一个明星,你明明看着面熟,就是想不起来他叫啥,让我这样的强迫症患者无比难受,所以抽时间来扒一扒同步屏障。

同步屏障机制

1. 直奔主题,同步屏障机制这几个字听起来很牛逼,能浅显的解释一下,先让大家明白它的作用是啥不?

同步屏障实际上就是字面意思,可以理解为建立一道屏障,隔离同步消息,优先处理消息队列中的异步消息进行处理,所以才叫同步屏障。

2. 第二个问题,同步消息又是啥呢?异步消息和同步消息有啥不一样呢?

要回答这个问题,我们就得了解一下 MessageMessage 的消息种类分为三种:

  • 普通消息(同步消息)
  • 异步消息
  • 同步屏障消息

我们平时使用 Handler 发送的消息基本都是普通消息,中规中矩的排到消息队列中,轮到它了再乖乖地出来执行。

考虑一个场景,我现在往 UI 线程发送了一个消息,想要绘制一个关键的 View,但是现在 UI 线程的消息队列里面消息已经爆满了,我的这条消息迟迟都没有办法得到处理,导致这个关键 View 绘制不出来,用户使用的时候很恼怒,一气之下给出差评这是什么垃圾 app,卡的要死。

此时,同步屏障就派上用场了。如果消息队列里面存在了同步屏障消息,那么它就会优先寻找我们想要先处理的消息,把它从队列里面取出来,可以理解为加急处理。那同步屏障机制怎么知道我们想优先处理的是哪条消息呢?如果一条消息如果是异步消息,那同步屏障机制就会优先对它处理。

3.那要如何设置异步消息呢?怎样的消息才算一条异步消息呢?

Message 已经提供了现成的标记位 isAsynchronous 用来标志这条消息是不是异步消息。

4.能看看源码了解下官方到底怎么实现的吗?

看看怎么往消息队列 MessageQueue 中插入同步屏障消息吧。

private int postSyncBarrier(long when) {
synchronized (this) {
final int token = mNextBarrierToken++;
final Message msg = Message.obtain();
msg.markInUse();
msg.when = when;
msg.arg1 = token;

Message prev = null;
// 当前消息队列
Message p = mMessages;
if (when != 0) {
// 根据when找到同步屏障消息插入的位置
while (p != null && p.when <= when) {
prev = p;
p = p.next;
}
}
// 插入同步屏障消息
if (prev != null) {
msg.next = p;
prev.next = msg;
} else {
msg.next = p;
// 前面没有消息的话,同步屏障消息变成队首了
mMessages = msg;
}
return token;
}
}

在代码关键位置我都做了注释,简单来说呢,其实就像是遍历一个链表,根据 when 来找到同步屏障消息应该插入的位置。

5.同步屏障消息好像只设置了when,没有target呢?

这个问题发现了华点,熟悉 Handler 的朋友都知道,插入消息到消息队列的时候,系统会判断当前的消息有没有 targettarget 的作用就是标记了这个消息最终要由哪个 Handler 进行处理,没有 target 会抛异常。

boolean enqueueMessage(Message msg, long when) {
// target不能为空
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
...
}

问题 4 的源码分析中,同步屏障消息没有设置过 target,所以它肯定不是通过 enqueueMessage() 添加到消息队列里面的啦。很明显就是通过 postSyncBarrier() 方法,把一个没有 target 的消息插入到消息队列里面的。

6.上面我都明白了,下面该说说同步屏障到底是怎么优先处理异步消息的吧?

OK,插入了同步屏障消息之后,消息队列也还是正常出队的,显然在队列获取下一个消息的时候,可能对同步屏障消息有什么特殊的判断逻辑。看看 MessageQueue 的 next 方法:

Message next() {
...
// msg.target == null,很明显是一个同步屏障消息
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
...
}

方法代码很长,看源码最主要还是看关键逻辑,也没必要一行一行的啃源码。这个方法中相信你一眼就发现了msg.target == null,前面刚说过同步屏障消息的 target 就是空的,很显然这里就是对同步屏障消息的特殊处理逻辑。用了一个 do...while 循环,消息如果不是异步的,就遍历下一个消息,直到找到异步消息,也就是 msg.isAsynchronous() == true

7.原来如此,那如果消息队列中没有异步消息咋办?

如果队列中没有异步消息,就会休眠等待被唤醒。所以 postSyncBarrier() 和 removeSyncBarrier() 必须成对出现,否则会导致消息队列中的同步消息不会被执行,出现假死情况。

8.系统的 postSyncBarrier() 貌似也没提供给外部访问啊?这我们要怎么使用?

确实我们没办法直接访问 postSyncBarrier() 方法创建同步屏障消息。你可能会想到不让访问我就反射调用呗,也不是不可以。

但我们也可以另辟蹊径,虽然没办法创建同步屏障消息,但是我们可以创建异步消息啊!只要系统创建了同步屏障消息,不就能找到我们自己创建的异步消息啦。

系统提供了两个方法创建异步 Handler

public static Handler createAsync(@NonNull Looper looper) {
if (looper == null) throw new NullPointerException("looper must not be null");
// 这个true就是代表是异步的
return new Handler(looper, null, true);
}

public static Handler createAsync(@NonNull Looper looper, @NonNull Callback callback) {
if (looper == null) throw new NullPointerException("looper must not be null");
if (callback == null) throw new NullPointerException("callback must not be null");
return new Handler(looper, callback, true);
}

异步 Handler 发送的就是异步消息。

9.那系统什么时候会去添加同步屏障呢?

有对 View 的工作流程比较了解的朋友想必已经知道了,在 ViewRootImpl 的 requestLayout 方法中,系统就会添加一个同步屏障。

不了解也没关系,这里我简单说一下。

(1)创建 DecorView

当我们启动了 Activity 后,系统最终会执行到 ActivityThread 的 handleLaunchActivity 方法中:

final Activity a = performLaunchActivity(r, customIntent);

这里我们只截取了重要的一行代码,在 performLaunchActivity 中执行的就是 Activity 的创建逻辑,因此也会进行 DecorView 的创建,此时的 DecorView 只是进行了初始化,添加了布局文件,对用户来说,依然是不可见的。

(2)加载 DecorView 到 Window

onCreate 结束后,我们来看下 onResume 对应的 handleResumeActivity 方法:

@Override
public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest,
boolean isForward, String reason) {
...
// 1.performResumeActivity 回调用 Activity 的 onResume
if (!performResumeActivity(r, finalStateRequest, reason)) {
return;
}
...
final Activity a = r.activity;
...
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
// 2.获取 decorview
View decor = r.window.getDecorView();
// 3.decor 现在还不可见
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
...
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
// 4.decor 添加到 WindowManger中
wm.addView(decor, l);
} else {
a.onWindowAttributesChanged(l);
}
}
}
...
}

注释 4 处,DecorView 会通过 WindowManager 执行了 addView() 方法后加载到 Window 中,而该方法实际上是会最终调用到 WindowManagerGlobal 的 addView() 中。

(3)创建 ViewRootImpl 对象,调用 setView() 方法

// WindowManagerGlobal.ddView()
root = new ViewRootImpl(view.getContext(), display);
root.setView(view, wparams, panelParentView);

WindowManagerGlobal 的 addView() 会先创建一个 ViewRootImpl 实例,然后将 DecorView 作为参数传给 ViewRootImpl,通过 setView() 方法进行 View 的处理。setView() 的内部主要就是通过 requestLayout 方法来请求开始测量、布局和绘制流程

(4)requestLayout() 和 scheduleTraversals()

@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
// 主要方法
scheduleTraversals();
}
}

void scheduleTraversals() {
if (!mTraversalScheduled) {
// 1.将mTraversalScheduled标记为true,表示View的测量、布局和绘制过程已经被请求。
mTraversalScheduled = true;
// 2.往主线程发送一个同步屏障消息
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// 3.注册回调,当监听到VSYNC信号到达时,执行该异步消息
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}

看到了吧,注释 2 的代码熟悉的很,系统调用了 postSyncBarrier() 来创建同步屏障了。那注释 3 是啥意思呢?mChoreographer 是一个 Choreographer 对象。

要理解 Choreographer 的话,还要明白 VSYNC

我们的手机屏幕刷新频率是 1s 内屏幕刷新的次数,比如 60Hz、120Hz 等。60Hz表示屏幕在一秒内刷新 60 次,也就是每隔 16.6ms 刷新一次。屏幕会在每次刷新的时候发出一个 VSYNC 信号,通知CPU进行绘制计算,每收到 VSYNC,CPU 就开始处理各帧数据。这时 Choreographer 就上场啦,当有 VSYNC 信号到来时,会唤醒 Choreographer,触发指定的工作。它提供了一个回调功能,让业务知道 VSYNC 信号来了,可以进行下一帧的绘制了,也就是注释 3 使用的 postCallback 方法。

当监听到 VSYNC 信号后,会回调来执行 mTraversalRunnable 这个 Runnable 对象。

final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}

void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
// 移除同步屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
// View的绘制入口方法
performTraversals();

if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}

在这个 Runnable 里面,会移除同步屏障。然后调用 performTraversals 这个View 的工作流程的入口方法完成对 View 的绘制。

这回明白了吧,系统会在调用 requestLayout() 的时候创建同步屏障,等到下一个 VSYNC 信号到来时才会执行相应的绘制任务并移除同步屏障。所以在等待 VSYNC 信号到来的期间,就可以执行我们自己的异步消息了。


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

该怎么放弃你,我的内卷

各位,两个月没写文章了,这两个月发生了很多事,也让我产生了很多不一样的感悟。从上次发完《阅阿里大裁员有感》,我的手机里就推了越来越多的“裁员”、“经济下行”、“焦虑”等信息。我这边现在这家公司,虽然不裁员,但是执行了“SABC”绩效分布考核,什么内容大家应该也...
继续阅读 »

各位,两个月没写文章了,这两个月发生了很多事,也让我产生了很多不一样的感悟。从上次发完《阅阿里大裁员有感》,我的手机里就推了越来越多的“裁员”、“经济下行”、“焦虑”等信息。我这边现在这家公司,虽然不裁员,但是执行了“SABC”绩效分布考核,什么内容大家应该也都清楚,最后给我打了个 B-。呵呵,扣工资 20%,变成所谓的绩效工资,下次考核看情况发放。

很多兄弟看到这可能会替我打抱不平,狗资本家,快去发起劳动仲裁。可是他们马上又下上另一剂猛药,那就是不停的 PUA 你,告诉你现在有家庭,要多努力,要一心扑在工作上,放心,下次一定给你打回来。

我承认,他们这些话术我都看过,基本相当于明牌。可依然我还是被影响到了,情绪十分低落,也没心思去劳动局跟他们 PK。对未来的预期瞬间变得很悲观,人要是一悲观了,真的干什么也提不起兴趣。我一门心思都扑在以后能干什么上,其它的啥也不想管,疯狂的在国内外门户上刷信息,希望能找到一条“赚钱之路”。我研究了 Web3、AI 绘画、搞自媒体、网赚攻略(什么视频搬运、抄书、小说转漫画等等),基本上信息流推给我的,我都研究了一遍。这些玩意越研究越让人焦虑,因为那些标题都起的特别的有煽动性,动不动就日入几万,而我发的那些,浏览量都破不了百。于是我就想研究更多的路子去赚钱,老实说,东南亚那边的情况,我也了解过一些。

后面我对家人的态度也越来越坏,经常不耐烦,看着我小孩我经常叹气,我想这他妈可能就是中年危机提前爆发了,总之那段时间人会越来越焦虑。

后来还是我一兄弟,邀请我一家人去平潭自驾游,我们其实也没玩几天,属于特种兵式旅游,两天两晚(晚上熬夜开车去)。回来之后心情就好多了,也没那么焦虑了。其实本来也没什么,君子不立于危墙之下,这里不行那就走。找不到就先干自己的项目(我有开源项目)。我其实对干这行还是蛮有兴趣的,应该持续坚持的干下去,半途而废干别的是下策。

回想下我那时候焦虑的经历,我以前根本看都不看那种赚钱文章的,因为我知道这些大部分是在卖课,可为什么那时候我着了魔一样呢?其实很大部分与网络有关系,你着急干什么,你就愿意看点什么,你看点什么,网络就给你推什么。这种消极循环人一旦深陷其中,光凭自己是很难走出来的。其实这种时候应该主动去接收一些积极乐观的情绪,有助于自己调整心态,网络给不了你,只有身边人能给你。

更深一步的想,所谓内卷是不是也是通过网络在传播着,深刻的影响到每一个人。所谓的“智能推荐算法”,真的智能吗?大家想看的一定就是适合每个人的吗?你不停的点击去看的信息,真的能帮助到你吗?网络是我们的工具,还是我们是网络的工具?

我想我们真的应该停下来,想想我们到底在多大程度上需要抖音、需要 BiliBili、需要知乎,也许它们真的没这么重要。

人生在世,我们到底应该追逐什么?或者说,追逐什么其实不重要,重要的是我们去追逐的过程。在这个过程中,没有内卷,没有与别人的竞争,只有对自我的审视和成长。

换句话说,我有多久没有好好了解自己了,那些独属于自己的东西,永远不会背叛的资源。我们常说的:能力、人脉、技术、视野。其实除此之外还有很多很多,我刷视频看到的有趣视频点的赞,我 Chrome 里收藏的网页,我百度网盘里躺着的分享资料等等等等,还有最重要的一项,就是我的身体和组成我身体的每一个部分:大脑、心脏、肺...... 有多久没有关注和了解它们了?在这个内卷的时代,每个人都在比拼都在竞争,都怕落于人后,都想快点挣更多的钱,这些,时常让我们忽视了对我们最重要的东西。

有趣的是,每个平台都在疯狂的更新自己的算法,期望能更精准的描述一个人,给人打上各种各样的标签。但在这场竞赛中,没有平台能竞争的过你自己,在这个世界上,只有自己更了解自己。所以我真的感觉它们在做无用功,浪费资源,最好的平台,不是给打各种标签,而是引导每个人发现自己的标签是什么。

这里我想分享给各位几个我思考的点,以供探讨。

原则一:相比与到处去找信息差,更重要的是建立自己的“资源池”

我那时候不停的刷信息,不停的找信息,本质上,我是在幻想着找到一个信息差,从而获利。这也是网上铺天盖地的文章所推崇的,所谓在风口上猪都能飞。但它们总是在掩盖一个逻辑错误,那就是找到信息差和获利之间的因果关系。实际上,找到信息差只是获利的条件之一,你有多大的能力利用这个信息差,这个信息差的时效性,方方面面的因素都会互相交织和影响。

更进一步的想,信息差就像风一样,它存在于冷热空气的交换之时,它存在于各行各业、每时每刻。让我们去追逐风,这现实吗?

我们更应该静下来,好好数数自己手头的东西,整理自己的大脑。找到自己“资源池”有哪些资源,哪些可以为我们所用,哪些可以继续扩充。思路可以打开一点,任何在当前时刻属于你的东西,都是你“资源池”的一部分。

原则二:出卖自己时间和体力的不做

这个不做,不是指不去做,而是指不长期的做。一般入门一个行业或者技术,肯定要付出时间和体力的。但你要说十年如一日的付出相同的东西,那所谓“35 岁”危机就只能找到你了。这点其实各行各业都一样,只是互联网行业处在发声的前沿罢了。

包括所谓网赚、搬运都是一个道理,毫无技术含量的事做几年就好。要时常审视自己现在在干什么,手头有哪些资源,未来的目标是什么。这跟程序运行是一个道理,运行了一段时间,停下来让自己 GC 一下。不然很容易 StackOverflow。

原则三:自己抓住的资源,千万不要轻易放手

如果不经常审视自己的“资源池”,给所有资源估估价值,就很容易被人带坑里。

原先我就做过一个项目,这是个跨部门项目,我那个领导一直告诉我说这个项目没前途、没卵用,绩效也给我打的不好,问我还要不要继续做。我说那就算了吧,做的我都不想做了。

我一放弃,马上就有新人接手,连交接也不用做,代码直接拿走,吃相可见一斑。

也就是从这里我才理解到,我其实没有了解自己,没了解过我手里的项目,被人潜移默化的影响了。影响一个人的思想真的不难,不停的重复就好了。所以还是那句话,多把自己手里的“资源池”拿出来晒一晒,整理一下。

其实 996 也是一样,拿出了你最重要的资源---身体,到底换来了什么,值得好好评估一下。

原则四:做自己喜欢的赛道,更要积累自己的资源

这几个月的经历给我的最大感觉是,这世界上真的有太多太多的行业,也有很多人赚到了钱(至少网络上宣传他们赚了钱)。网络能让这些信息病毒式的传播,导致很多人错觉的以为自己照着做也能挣到钱。但他们忽视的是,网络能把世界各地的人汇聚起来,让信息流通。其实也提供了一个更大的平台,在这个平台里,只有更卷的人才能挣到钱。

有时候真的应该抛开网络。比如,你会写代码,这是你“资源池”里的一项技能,你把这个技能公开到网络售卖。只有两种情况,要么你非常的卷,打拼出一番事业;要么你根本竞争不过别人,这是普遍情况,这世界那么大,比你优秀的人有太多太多了。

但是抛开网络,回到你身边的小小社交圈子,你的技能可能就没那么普遍了。可能你会说,那我做程序员,我身边朋友认识的大部分也是做程序员啊。那么可以这么想,假如你会做菜,你身边的程序员朋友都会做菜吗?假如你会画画,你身边的程序员朋友都会画画吗?人和人总有差异点,你觉得找不到优势,那是因为你尚未建立自己的“资源库”。

先认识自己,再让身边的人认识自己,当他们会给你打标签时,他们就成了你“资源库”中的一员,这就是人脉。这才是是独属于你自己的标签,而不是抖音、B 站为你打的冷冰冷的标签。

总结

以上我感悟的四个原则,我称之为“资源池思维”,一个比较程序员化的名词。

这篇文章发完后,我后续可能就继续更新一下具体的技术文章了,继续深耕技术。

最后,推荐看到最后的各位看一部冷门电影:《神迹》,讲述的是医生维维安托马斯的故事。看完可以来一起交流交流感悟。


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

摸鱼时间打造一款产品

辞职我辞职了拿上水杯,挎起背包,工位被我丢在了身后,一阵清风过后,我便离开了这度过一年半载的地方辞职的原因很简单,公司快没钱了,要么同公司共进退,要么离开,于是我选择了离开公司的待遇不算好也不算差,工资不算满意,但至少双休不加班。平时开发阶段末尾还比较闲,大把...
继续阅读 »

辞职

我辞职了

拿上水杯,挎起背包,工位被我丢在了身后,一阵清风过后,我便离开了这度过一年半载的地方

辞职的原因很简单,公司快没钱了,要么同公司共进退,要么离开,于是我选择了离开

公司的待遇不算好也不算差,工资不算满意,但至少双休不加班。平时开发阶段末尾还比较闲,大把摸鱼时间,逛逛各种论坛,掘金、知乎、github不亦乐乎,现在看来公司倒闭和我不无关系

久而久之,不免有些无聊。论坛里充斥着灌水文章,看多了属实是食之无味。于是为了打发时间,只能写一写自己的项目。一想到老板在为我打工,敲打键盘的双手便愈发轻盈了

未曾设想的道路

大半年前为了记录学习一项技能到底要花多少时间,我开了个新坑,做一款计时软件,记录某个时间段发生的事情

和以往一样,最初只是打算随便写写,写个基础功能完事。但在使用的过程中,越来越多的需求在脑海中构建。编程最有趣的便是创造感,你能感受到自己在创建一个新的世界,在创造的过程中时间飞速流逝,一转眼便度过了无聊的一天

激情是短暂的,生活是漫长的。和往常一样,功能在逐步完成,但我的兴趣在逐渐减少。没有添加柴薪,火便只能渐渐势弱

此时两个选择摆在面前,一个便是不再更新下去,毕竟要做的已经完成了,再去寻找下一个打发时间的事情就好了。另一个则是保持兴趣继续做下去

兴趣?利益?

做一个开源软件,如果能收获社区的掌声想必是件自豪的事情。但如果只有掌声,久而久之开源作者可能会陷入自己到底为了什么才做这件事的思维泥潭。有的人失去了兴趣便离开了,有的人发出了声音希望得到一些回馈

兴趣可以支撑人前行,但又有多少人能不求回报去做一件事?不可否认,曾经幻想过做出爆红的软件,然后不用打工,财富自由这样的白日梦。虽然不能一步登天,但我想借助它向前一步

审视一下目前的状况,如果要供用户使用,一个简单的计时功能加上记录,未免太过单薄。这么简单的功能实在谈不上什么竞争力,实现成本过低,而且我相信人们更愿意使用移动app,而不是在pc上去使用这个功能。我需要一个特定于pc且有实在价值的功能,很快我便找到了,它既满足前面的要求,又契合软件的主题

广告恐怕是最理想的获利方式,不会影响用户使用,也不用去考虑升级版之类的的东西。虽然不知道具体能有多少收入,但希望起码能够抵消掉域名的费用

有了继续前进的目标,这艘小船便能扬帆远航

但眼下的问题很严重,我在技术选型上摔了个大跟头

重头再来

好的开始是成功的一半,但没有人能预料到未来会发生什么

使用vue3为前端,我直接选择了webview方向的跨端框架

在以go为后端的wails和rust为后端tauri中,我选择了go。之前学习过一段时间的rust,深知学习的难度。而且在最初的预想中,我只是打算做个简单的计时软件,使用go也只是做一下数据库操作。不久后就完成了最初的一版,但在后续的尝试中,发现wails的生态还是太小了,很多基础的功能都需要自己实现。这时再看看tauri就显得很香了,各种插件和前端的绑定,再加上go并没有用得多么称手,于是只能长痛不如短痛了

ui框架的选择上我也犯了同样的问题。开始是偏向于material design这种风格,选择了vuetify,这个框架当时我看了很久,做的时候已经要到v3正式版本了。本来以为没问题,但后续使用时过于难受,此时文档基本没怎么更新,issue也被各种bug塞满了。只能快刀斩乱麻,换了习惯的ant-design-vue,风格区别很大,但改改样式也能用。quasar同样在我的考虑范围内,但更加小众,目前是不打算换了,在tauri v2移动端正式版后,再做尝试

为什么最开始没有选择做移动端?功能契合,使用起来也更方便。一方面是我的主要技能栈是js,另一方面重新学移动端过于不切实际,为一个八字没一撇的项目去学实在没有必要。flutter我之前也学过,试着写了一点,但还是不如js来得舒服

回过头来,发现走了很多弯路,但不去尝试只站在远处观望,永远也不会有结果。颠颠撞撞重头再来

编程之外

我一直把时间花在了代码之上,但想要做一款产品还远远不够,它迫使我不得不将视角转向那些我不曾关注的角落

UI可谓是产品的脸面,用户的第一印象便停留在了logo和界面上,虽然使用了风格统一的组件库,但将他们组合在一起的时候未必能将它们严丝合缝。目前只能说是勉强能看,日后再做修改

说明文档带领用户快速理解程序的运作,由于用户没有设计者的前提条件,很多理所当然也就需要一一记录

想要完善功能,bug和feature的反馈也要做指引,方便接收用户意见,确定前进路线

说明文档

参考vite的官网,使用vitepress,写markdown就可以了,还可以配上vue组件,还算方便

部署上选择了netlify,可以换自己的域名,还可以自动更新ssl证书

本来以为部署很麻烦的,结果一个小时左右就全部搞定,包括在namesilo上买域名,然后在netlify部署、配置

拥抱AI

在完成这些工作的过程中,有不少地方借助了AI,可以说很大程度加快了进程

编码上,由于我完全不懂windows编程和勉强会点rust语法,想要完成监听系统上的应用状态这项功能,根本就无从谈起。要花大量时间去学习的话,反而和我利用碎片时间进行编程相冲突了。况且在new bing的帮助下,我完成一个简单的函数就要花费数个小时的尝试。new bing根据我的需求返回了相关的api参考,但很多时候返回的代码并不能直接运行,有着这样那样的问题,需要去修正。很难想象仅凭我一人去翻找资料何时才能完成这冰山一角

在这个过程中,new bing最大的帮助就是提供了关键词。很多时候,你知道一个事物,想用自己的语言需要一长串词语去描述,但过去的搜索引擎并不能理解这些,而且就算把描述输入进去,也会因为过多的关键字导致答案被淹没在茫茫的网页之中。这就造成了一个困境,我不知道它叫什么,所以我要去搜索,但搜索的时候要知道它叫什么

在netlify配置域名,我输入了如何去配置,new bing给出了关键的name servers,省去了花时间去到处去找教程

为应用绘制一个logo,很显然我并没有这个能力,使用Bing Image Creator,一段描述就能生成

这些都是一些无关紧要,琐碎的事情,我只想获取结果,把精力留在我擅长的事情上。试想一下,我一个人去实现要花掉多少时间?最终能实现吗?部分功能交给其他人,又要用什么去换取?

计划

讲到这里如果有兴趣了解一下的话,可以移步仓库地址,但目前的功能我只能说很少,而且还可能出现问题,我提前声明一下。说明文档见此处,需要看清警告提示

为什么这个时候来写这篇文章来介绍呢,主要是辞职了也没事干,已经做了大半年就整理了一下。原本是半年后再辞职的,但计划赶不上变化,只能提前放出来看看情况

还有一项没能赶上的便是广告,使用的是google adsense,但提交申请后便石沉大海。尽管提前做了申请,但已经过去了几个星期。可能是开始建站时随意申请被驳回的缘故,久久没有反应

最后

辞职其实还有一个原因,就是累了,光是待在公司什么都不干,也能感觉到劳累。工作小憩之余,在过道眺望远方时,我一直想问自己究竟在干些什么。我想做出改变,想去尝试新的东西,体验另外一种生活

想过很多次,以后也许不会从事编程的工作,但又有什么选择呢。我希望是创作,而不是枯燥的重复劳作,但我很清楚这不是换一个职业就能改变的问题,终究是实力的问题。编程很有趣,但在公司并不是如此

现在我已经度过了一周的悠闲时光了,白天在家看看书,傍晚下楼走走,看着面向我驶过的匆忙下班的人流,感叹这也是自己前不久的模样。我背朝着喧闹,走上凉爽的林荫道,晚风吹过,天边挂着一轮淡淡的月牙


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

工作三年后的胡思乱想

一眨眼工作已经三年了,前两年的总结 工作第一年、工作第二年 基本上把在公司做的事情都介绍了,今年站在「前端已死」、互联网大裁员的环境下,想想未来的路可能更为应景。经常说这是最好的时代,也是最坏的时代,互联网便是如此。通过互联网将人与人之间的...
继续阅读 »

一眨眼工作已经三年了,前两年的总结 工作第一年工作第二年 基本上把在公司做的事情都介绍了,今年站在「前端已死」、互联网大裁员的环境下,想想未来的路可能更为应景。

经常说这是最好的时代,也是最坏的时代,互联网便是如此。通过互联网将人与人之间的各种链接都成为了可能,在互联网诞生之前,人与人之间的交流就是现实生活中的圈子,而现在本来这一辈子都不会在现实中产生交集的人在互联网却会相遇。

各种写书的大佬、开源的大佬,以往可能只是从文字、代码中了解他们,但现在通过社交媒体、微信竟然就产生了互动。当然不好一面就是也会遇到和自己不相投的人,也许会影响自己的心情。

通过互联网极大的扩宽了我们的视野,看到了别人在怎么生活,也放大了自己的焦虑和欲望。我们需要认清自己的边界,知道自己想要什么,自己能做什么,不需要对本来不可能发生在自己身上的事情而焦虑。

当迷茫焦虑时,看看宇宙的纪录片,从宇宙的视角去看自己,无论从空间大小还是时间维度,其实自己什么都不是,想那么多干啥。

再想想其他动物,吃饭睡觉喵喵叫,也挺好的。

前端已死

互联网已经结束了快速扩张的时期,这是个客观事实,因此招聘的人数相对于之前减少了很多,但远没到一个已死的状态,相对于其他行业,选择互联网依旧是一个不错的选择。

前端会不会死不知道,互联网肯定会一直存在下去,现在整个社会都是基于互联网,已经变成了像电、水一样的基础设施,没有人可以离开它。因此互联网的相关的岗位一定会一直一直存在。

至于互联网中具体的职业划分,前端、后端、算法、数据库等,它们各自使用的语言、技术一定会发生变化的,当选择互联网技术行业的时候,就应该抱有持续学习的态度。

塞班操作系统被安卓、iOS 取代、.Net 岗位的减少、客户端大量岗位转前端,这些也就发生在近十几二十年。当某一个岗位减少的时候,一定又会出现新的岗位,保持开放的心态去学就可以,变化再多肯定也有不变的东西。当掌握一门技术再学习另一门技术的时候,肯定会比小白学习一门新技术快很多很多,很多经验也会迁移过去。

去年 12 月出来的 chatGPT 为代表的大模型,到现在也就半年多的时间,很多以前完全不敢想的事情就这样发生了。可以预见的是一部分岗位数量肯定也会减少,目前影响最大的应该是 UI 岗,其次一定程度上可以提高程序员的开发以及学习效率,但还没有到取代的程度,但未来会再怎么发展就不得而知了。

相对于其他行业,虽然互联网相关技术迭代确实很快,但如果是因为热爱而选择这个行业,我觉得去做一辈子是没问题的。

技术

底层技术服务于上层技术,上层技术服务于应用,真正赚钱的是应用,它可能提升了用户的效率、也可能提升了用户的生活体验,这样用户才愿意付费。上层技术的人收到了钱,进一步也愿意为底层技术的人付费。

但对于一个应用,技术并不是最重要的,更多需要的是产品和运营,一个应用在 chatGPT 和各种框架、云服务的加持下做出来变得太简单了,更多的是我们需要思考如何设计产品和如何推广运营产品,和用户产生更亲密的连接,用户才愿意付费。

极端一点,即使现在所有的应用都停止更新了,其实也并不会产生多大的影响。

在公司中亦是如此,对于技术开发,没有谁是不可取代的,公司更期望的是那些可以发现问题、分析问题、定义问题的人,至于怎么解决,问题定义清楚以后,解决方案自然可以出来,谁去解决并不重要了。

但也不用太过悲观,虽然技术不是最重要的,但一定是不可或缺的,在解决问题的过程中也会区分出能力强和能力差的:方案的设定、代码编写的好坏、线上的 bug 数、代码的扩展性等。

赚钱

赚钱很大程度又是需要运气的,比如同一个人十年前进入互联网和现在进入互联网差别就会很大,再比如开发一个应用突然爆火,例如「羊了个羊」,这些我们是很难控制的,我们只能「尽人事,听天命」。

最近几年,除了在公司工作,对于有技术的同学赚钱有下边的方式:

  • 付费课程、出书

    最近几年越来越多的人在极客时间、掘金小册写课程或者直接出书。

    对于写课的人赚到了钱,对于买课的人只要跟着看完了,多多少少都会有很多收获。付费课程会比较系统, 如果没有这些课程,去学东西肯定也是可以学的,但需要花很多时间去网上搜一些零碎的资料,由于没有经验甚至可能走很多弯路。

  • 付费社群

    市面上也会有一些付费训练的社群或者知识星球

    对于组织付费社群的人会花费很大的精力,需要持续运营并且照顾到每一个人,不然就等着挨骂吧。因此这类收益也会很高,一些人会辞去工作专职来搞。

  • 开源

    大部分开源基本上是用爱发电,更多是收获一些朋友、流量、提升技术。

    比如 core-js 作者的经历,一个 22.6k star 的项目,几乎各个网站都在用的一个项目,作者却因为钱的问题被很多人谩骂。因此如果是个人专职开源一个项目靠 GitHub Sponsor 会很难很难。

    当然,开源也是能赚到钱的,比如 Vue 开源就赚到了很多钱,但毕竟是很少很少数了。

    依赖纯开源项目赚到钱,还是需要背靠公司。比如阿里云谦的 Umi、通过开源加入 NuxtLab 的 Anthony Fu、在 AFFiNE 的雪碧等等。

  • 应用

    身为一个程序员,尤其是前端程序员,当然可以自己维护一个应用来赚钱。

    做得很成功的比如 Livid 的 V2ex 社区,Abner Lee 的 Typora(后来知道作者竟然是国内开发者)。

    也有一些没有那么出名的,比如大鹏的 mdnice,秋风的 木及简历

    当然如果要做一个很大的项目,背靠公司也是一个很好的选择,比如之前阿里玉伯的语雀、之前极客邦池建强的极客时间。

    还有一些小的创业公司会做的,冯大辉的「抽奖助手」、吴鲁加的「知识星球」等。

    做出这些应用不需要很多时间,需要我们善于发现生活中的痛点以及强大的执行力,当然想成功的话需要再加一点运气,在成功前需要不断尝试不同的东西。

  • 流量变现

    有流量就会赚钱,不管是接广告、还是带货。互联网上也会有部分人专注于怎么搞流量,知乎怎么获得更多曝光、视频号怎么获得更多流量、怎么批量注册号,各个平台规则可能是什么,怎么对抗规则,这类有技术加持也会更加顺利,很多人也在专职做。

赚钱的方式有很多,对于我来说,我会尽量选择复利的事情,这样才能产生更大的价值。比如一对一咨询,一份时间换一份收入。但如果把东西写成课程,只需要花一份的时间就能获得 N 份的收入。

另外就是需要保持分享,分享除了能帮助其他人,对自己也会有很大的帮助,写文章的过程中也会不断的有新的认知得到。虽然当下可能没有金钱方面的收入,但时间放宽到几十年,相信一定会有很大的回报。

人的欲望是无穷的,也不能陷入赚钱的极端,目标应该是关注此刻,体验生活,享受生活,而不是不停的赚钱。之前听播客,有一个恰当的比喻,钱就好比汽油,不停的赚钱相当于不停的加油,但如果汽车停着一直不动,再多的汽油也是无意义的。

健康

最近几年总是爆出程序员突然离世的新闻,前段时间耗子叔突然离世的消息听到之后真的很震惊。twitter 经常刷到耗子叔的动态,然后突然一天竟然就戛然而止了,毫无征兆。

意外是无法避免的,只能尽可能的从饮食、作息、锻炼三方面降低生病的风险。

饮食

我是工作第一年体检的时候检查出了中度脂肪肝、尿酸高,当时因为是刚毕业,体重是我的巅峰,140 多斤,脂肪都堆在了肚子上。那段时间就开始跑步加吃沙拉,少吃米饭、面条。降的也快,几个月就回到了 130 斤以下,甚至到 120 多点。

第二年体检的时候,脂肪肝基本没有了,尿酸也降了许多。

image-20230702141922024

后来就保持少吃米饭,多吃蛋白质、蔬菜的饮食了。

作息

有一次得了带状疱疹,那种非常痛的类似于痘痘的东西,后来了解了一下是因为免疫力低导致病毒入侵的。猜测因为晚上坐在电脑前,气温降低了没注意,从而导致了生病。

病好之后就决心养成早睡早起的习惯。

之前作息基本上是 1 点到 2 点睡觉,9 点前后起床。现在基本上保持在 11 点前后睡觉,6 点到 7 点间起床了。

早起的好处就是早上会有大把的时间,而且这段时间是专属于自己的,并且因为大脑刚苏醒,效率也会很高。但如果是工作一天,晚上回家再做自己的事情,此时大脑已经很疲惫了,效率会比较低。

运动

最开始是跑步,但确实很难坚持下去,跑步需要换衣服、出门,还依赖于外边的天气,成本很高。后来陆续尝试过 keep、一些付费课程,都做了但没有完全养成习惯。

后来知道了 switch 的健身环大冒险,然后就一路坚持到了现在,前段时间已经通关了。

目前也一直在坚持,基本上一周会运动三到四次,一次大概花费 50 分钟左右。

投资

大学的时候开始接触到理财,知道了基金的概念,看了银行螺丝钉的「指数基金定投指南」,也看了「穷爸爸富爸爸」、「小狗钱钱」这类理财入门的书。当时赚到的一些钱,就跟着银行螺丝钉投了,主要是一些宽基和中概、医疗。

一直到工作的第一年,基金收入确实不错,甚至赚了百分之四五十。当时想着原来股市这么简单,这咋还能亏钱了。

接着疫情不断发展,还有外部经济的变化,中概、医疗都大跌,当时发了年终奖还不停的补仓中概,到现在亏损也有百分之三四十了。

但我心态是可以的,一切都是浮亏和浮盈,只要不卖一切都是浮云。

经历了大起大落后吸取了一些教训,那就是一定要严格执行计划,现金流多不一定要立刻全部投入,而是按计划定投,因为没人知道会跌多久,只有有充足的现金流,才能够把亏损逐步拉平。

现在国家规定互联网基金这些必须走「投顾」,也就是主理人帮我们买入、卖出,我们只需要交一定的投顾费即可。目前我都是在雪球上投,跟投的有孟岩的「长钱账户」、alex 的「全球精选」、螺丝钉的指数增强和主动优选。

能设置自动跟投的就自动跟投了,我相信专业的事交给专业的人肯定是没问题的。

投资肯定是财富自由不了的,但一定比把钱放余额宝强一些,只要耐心持有,尤其是目前这样的熊市投入,相信到下一个牛市会有不错的回报。

(以上仅个人看法,股市有风险,入市需谨慎)

保险

如果开始接触理财,除了投资,一个绕不过去的点就是保险。

对于保险是什么的比喻,之前听薛兆丰的课时候印象深刻。

我现在还年轻力壮,将来年纪大了可能会生病,为了防止以后生病要花一大笔医药费,今天就开始存钱,每个月拿出 10% 的收入存起来,未雨绸缪。这是一种做法。

另外一种做法,是我每个月也拿出 10% 的收入去买保险。

这两种做法有什么区别呢?

区别在于,如果我是用储蓄来未雨绸缪,那么未来可能就会发生两种不同的情形。

如果我将来年纪大了也没生病,我存的钱就还是我的钱,我不需要花出去,这时候我还是很幸运的,能够保有我原来的收入,这份储蓄没有被花掉,我赚了。

但是如果我运气不好,生病了,这份储蓄就会被用掉,甚至需要借很多钱去治病,生活会发生巨大的变化。

所以通过储蓄来未雨绸缪,它的特点是未来的结局是可变的,是变动的、是带有风险的。要么高、要么低,要么能够保有原来的这份储蓄,要么这份储蓄就被用掉了甚至借更多的钱。

而对于保险来说,如果你没病,那你的生活该怎么样还是怎么样。如果你病了,那会有保险公司给你支付一大笔钱,你也不用和别人借钱,病好后继续该干啥干啥。

因此存钱去防止生病就有赌的成分了,如果没病就白赚了很多钱,如果病了生活质量可能会发生很大的变化。

而保险就可以降低风险,未来即使生病了,由于看病不需要花钱了,病好后生活质量也尽可能的维持在原来轨道 。

我期望未来肯定是尽量稳定的,所以在不影响当前生活质量的条件下我愿意拿出一部分钱来买保险。原计划我可能会 30 岁以后开始买重疾险,之前女朋友的朋友有推荐保险的,然后就跟女朋友一起配置了重疾险。

选保险一定要慎重,一些看起来很划算的保险, 到理赔的时候可能会推三阻四,甚至理赔前公司破产了,尽量要选择大公司。

当然生活没有标准答案,每个人看到世界也都是不同的,我也一直在成长,一直在认识新的东西,上边的所想的也不能保证说未来不会再变。

未来能做的就是多看看书,不限制自己,看看经济学的、哲学的、心理学的、人文的,多出去走走看看,尽可能多的增加人生体验,去认识世界,认识自己,做自己想做的事,爱自己所爱的人,走下去就好了。


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

谈软件项目中的外行管理内行

掘友的一个评论,让我又想跟大家唠会儿嗑了。我之前写过一篇文章叫《该写好代码吗?我也迷茫了》。于是,这位掘友评论说,他在上家公司的时候,他们组代码质量要求高,前期设计考虑全面,所以上线后问题少。但是在领导眼里就像没事做一样。其他组经常出事故,反而很忙,像是一直在...
继续阅读 »

掘友的一个评论,让我又想跟大家唠会儿嗑了。

我之前写过一篇文章叫《该写好代码吗?我也迷茫了》。于是,这位掘友评论说,他在上家公司的时候,他们组代码质量要求高,前期设计考虑全面,所以上线后问题少。但是在领导眼里就像没事做一样。其他组经常出事故,反而很忙,像是一直在攻克难题。

我回复说:那可能是领导不够专业,没有深入到项目里

回复完了,我仍然意犹未尽。于是,才有了这篇文章。

正常情况下,对于一个软件项目或者一次版本迭代,负责人应该是心中有数的。这个项目的难度如何?手里的人员能力如何?大约做多长时间?最终会达到什么效果?这些应该是负责人的基本功。我不自夸,反正我带项目时,我们几个负责人都是能做到的。

据此,你才能更好地把握一个项目的始终。有时,项目中间出现难题,要干预。有时,大家超额完成,要嘉奖。有时,很简单的功能,他开发出了很多bug,要扣奖金。甚至有时候,一个月的工期,老板硬要半个月完成,这时对结果不能要求太高,你不能又要做完又要做好,那是找打的节奏,最终会两败俱伤。

但是,存在一种叫“外行管理内行”的情况。这类领导呢,他不知道你干了啥,也不知道对你来说这活好不好干,甚至你干没干完他也分辨不出,全靠读你的周报和计划。但他却是你的分管领导

我原本以为,在技术领域不会存在这类情况。后来发现,不但有,居然还很普遍。而且在国内,这还是一道别致的景观。

听过一个报道,说国内有一位教授,他从国外买来高端芯片,先把标志磨掉,再印上自己的Logo。然后,他对外宣传是自主研发的。出人意料的是,他还开发布会,还拿到了上亿的科研资金。随后几年,他甚至还推出了第二代、第三代、第四代,都是通过自己印标签的手段完成的。

整个过程,但凡有内行审一下图纸,看一下生产车间,都会很容易识破骗局

当然,咱们普通人一般到不了研究芯片,那么高精尖的层次。但是,我觉得作为一个普通企业的小领导,参与到项目中,这总是可以的吧。

从实际情况来看,其实很少有领导能躬身入局。哪怕这个领导只管六、七个人,他也会再分成三四个组。遇到一项任务,领导会安排给员工,让它们去协商完成。而他,要去规划团队未来的发展。

我有种很明显的感觉,但现在还找不到一个合适的名词来描述它。只能从一些例子上去体会其中的差异。

我有一个任领导,开周会是他做总结。他说这一周,后端完成了什么,前端没有完成什么。前端还反驳他,说自己完成了。领导说,别糊弄我,你只是把接口地址敲上了,根本没调用过,你跑跑试试,数据结构对不上。他是最了解整个项目开发进度的人

还有些领导与他形成鲜明的对比。他们了解事情靠开会听汇报,就算只有三个下属,也要他们说说这周干了啥。他做一下汇总,然后再上报给他的领导。功能交付后,项目出现了问题,他会说是下属欺骗他,居然谎报工期,没测说测了,没做说做了。

至于形成这种区别的原因,可能跟文化和制度有关。不能说哪种绝对好或者不好。

前面那种“严管型”领导所在的公司,企业文化中,负责人制,不讲理由。出了问题不要推脱是张三、李四没做好,他们担不起这个责任,就是你负责人的问题。所以,这才导致了领导会落实每一项流程。

后者那类“汇报型”的企业,从上到下都注重文书的格式、措辞,讲究高瞻远瞩,着眼未来,允许试错,探索创新。因此,他们便将更多的精力投入到了汇报上。

作为一名老程序员,我感觉,软件开发是一种很工程化的工作,一层层拆分好,安排到基层人员手里,告诉他们如何执行就可以了。但是新一代领导说,管理的最高境界是让团队具备自驱力,硅谷就是这样的。自驱力就是你不用安排,他们自己就能主动克服困难去完成。即便领导不在,团队也能照常运转,这才是健康的团队。

抱歉,有点儿跑题了。

本文的初衷是“外行管理内行”与“躬身入局”。

我觉得“外行管理内行”在国内是必然现象,尤其在技术领域

在国内的中小企业,作为团队的领导,“行政类”的事情要比“专业类”多。你看看你的领导,他经常开会和写材料。这不是它自己要干的,也不是老板要求的,是受行业和环境的影响。你想要申报科技企业,申请政策扶持,参加高新评级,你就得去准备。

除此之外,团队的日常管理,年计划、月计划,人员流动、评优、优化等等,都会耗费日常的精力。而这些活,专业的程序员并不想干。

另外,就算一个内行被推上管理岗位,程序员工种复杂,知识更新也快,那他干上几年,也会慢慢被磨成“外行”。

因此,内行外行只是相对的,并不重要。我则更强调“躬身入局”

躬身入局,就是参与到项目中。就算不写代码,起码也了解下大家遇到的问题。在这个过程中,作为管理者,可以制定一些规则和标准,来保证良性运作。好的流程制度,是可以实现员工自驱动的。

举一个身边的例子。现在夏天了,天气很热,办公室都会开一天的空调,温度调到23度。

这天下过大暴雨,空气清爽,气温很低。这时,23度的空调就很冷了。正对着空调口的瘦同事,就想去关掉空调。结果被一个静止都会喘粗气的大胖子给制止了。

胖子问:为啥关空调?

瘦子说:外面下雨,不热了!我冷。

胖子说:你冷你穿衣服。你关了我热。

瘦子说:你热,你打开小风扇。

说完,瘦子关了空调。 随后,胖子又打开了空调。

这类事情就是公说公有理,婆说婆有理。搞不好还需要领导出面调停,或者搞一个群体性的投票。如果没人理你,那就看谁的底线更低,或者说谁更狠。

但是,如果说团队制定这么一个小规则,就是室温超过26度可以开空调制冷,制冷底线是23度。那么,即便没有领导在场,大家也能很好处理此类事情。甚至,这类情况根本都不会发生。

这只是为了体现规则重要性的例子。到软件开发中,可能就是比如这类情形:一个接口如果多个端都调用,那么就由后端组织特定的数据格式。

如果你躬身入局,你就会经历类似的事情。否则的话,你可能会说,吵什么吵,大家要以大局为重(只是要求停止争吵,没有决定开不开空调)。

可能说这么多,有人朝我笑了:你以为我傻?我这么做,老板给我涨工资,多拿钱!

抱歉,抱歉!我只想着如何提高项目质量了。关于挣钱方面,我外行了!

唉,大家一定要重视环境的重要性。


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

假如互联网人都很懂冒犯

大家好,我是老三,最近沉迷于听脱口秀,并且疯狂安利同事。脱口秀演员常常说的一句话是:“脱口秀是冒犯的艺术”。最近我发现,同事们好像有点不一样了。阳光灿烂的早上,趿拉着我的宝马拖鞋,跨上包浆的小黄车,屁股感受着阳光积累的炙热,往公司飞驰而去。一步跨进电梯间,我擦...
继续阅读 »

大家好,我是老三,最近沉迷于听脱口秀,并且疯狂安利同事。

脱口秀演员常常说的一句话是:“脱口秀是冒犯的艺术”。最近我发现,同事们好像有点不一样了。


阳光灿烂的早上,趿拉着我的宝马拖鞋,跨上包浆的小黄车,屁股感受着阳光积累的炙热,往公司飞驰而去。

一步跨进电梯间,我擦汗的动作凝固住了,挂上了矜持的微笑:“老板,早上好。”

老板:“早,你还在呢?又来带薪划水了?”

我:“嗨,我这再努力,最后不也就让你给我们多换几个嫂子嘛。”

老板:“没有哈哈,我开玩笑。”

我:“我也是,哈哈哈。”

今天的电梯似乎比往常慢了很多。

我:“老板最近在忙什么?”

老板:“昨天参加了一个峰会,马xx知道吧?他就坐我前边。”

我:“卧槽,真能装。没有,哈哈。”

老板:“哈哈哈”。

电梯到了,我俩都步履匆匆地进了公司。

小组内每天早上都有一个晨会,汇报工作进度和计划。

开了一会,转着椅子,划着朋友圈的我停了下来——到我了。

我:“昨天主要……今天计划……”

Leader:“你这不能说没有一点产出,也可以说一点产出都没有。其实,我对你是有一些失望的,原本今年绩效考评给你一个……”

我:“影响你合周报了是吗?不是哈哈。”

Leader、小组同事:“哈哈哈“。

Leader:“好了,我们这次顺便来对齐一下双月OKR,你们OKR都写的太保守了,一看就是能完成的,往大里吹啊。开玩笑哈哈。”。

我:”我以前就耕一亩田,现在把整个河北平原都给犁了。不是,哈哈。”

同事:“我要带公司打上月球,把你踢下来,我来当话事人。唉,哈哈”

Leader、同事、我:“哈哈哈“。

晨会开完,开始工作,产品经理拉我和和前端对需求。

产品经理:“你们程序员懂Java语言、Python语言、Go语言,就是不懂汉语言,真不想跟你们对需求。开个玩笑,哈哈。”

我:“没啥,你吹牛皮像狼,催进度像狗,做需求像羊,就这需求文档,还没擦屁股纸字多,没啥好对的。不是哈哈。”

产品经理、前端、我:“哈哈哈”。

产品经理:“那我们就对到这了,你们接着聊技术实现。”

前端:“没啥好聊的,后端大哥看着写吧,反正你们那破接口,套的比裹脚布还厚,没事还老出BUG。没有哈哈。”

我:“还不是为了兼容你们,一点动脑子的逻辑都不写,天天切图当然不出错。不是哈哈。”

前端、我:“哈哈哈”。

经过一番拉扯之后,我终于开始写代码了。

看到一段代码,我皱起了眉头,同事写的,我顺手写下了这样一段注释:

/**
* 写这段代码的人,建议在脑袋开个口,把水倒掉。不是哈哈,开个玩笑。
**/

代码写完了,准备上线,找同事给我Review,同事看了一会,给出了评论。

又在背着我们偷偷写烂代码了,建议改行。没有哈哈。

同事、我:“哈哈哈”。

终于下班了,路过门口,HR小姐姐还在加班。

我:“小姐姐怎么还没下班?别装了,老板都走了。开玩笑哈哈。”

HR小姐姐:“这不是看看怎么优化你们嘛,任务比较重。不是,哈哈。”

HR小姐姐、我:“哈哈哈”。

我感觉到一种不一样的氛围在公司慢慢弥散开来,我不知道怎么形容,但我想到了一句话——

“既分高下,也决生死”。


写这篇的时候,想到两年前,有个叫码农小说家的作者横空出世,写了一些生动活泼、灵气十足的段子,我也跟风写了两篇,这就是“荒腔走板”系列的来源。

后来,他结婚了。

看(抄)不到的我只能自己想,想破头也写不不来像样的段子,这个系列就不了了之,今天又偶尔来了灵感,写下一篇,也顺带缅怀一下光哥带来的快乐。


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

人性的弱点之如何让别人信服你

前言 早些年花时间拜读了《人性的弱点》一书,不可否认其中的一些观点和方法确实很有用,在后来的工作与生活中我有意无意的使用其中的一些语言上的技巧,确实化解了一些无意义的争端,在生活中与人相处也顺利、平和了许多。 当然了,里面的一些观点与方法并不适合所有人,也...
继续阅读 »

前言


早些年花时间拜读了《人性的弱点》一书,不可否认其中的一些观点和方法确实很有用,在后来的工作与生活中我有意无意的使用其中的一些语言上的技巧,确实化解了一些无意义的争端,在生活中与人相处也顺利、平和了许多。


当然了,里面的一些观点与方法并不适合所有人,也许有人会觉得这样很累,也会有人觉得大可不必,因人而异吧, 如果有些朋友明显感觉到在生活、工作中因为说话频频损失了一些机会,那么我建议可以阅读一下这本书。


本文是我对其中一部分章节的提炼与总结,也是后来时长翻阅警醒自己的摘要。


一、避免陷入争论

  1. 丢掉先入为主的观念
  2. 控制好自己的情绪
  3. 耐心听对方说完
  4. 努力找认同点
  5. (语言上)能让步就让步
  6. 真心感谢别人对你的重视

二、永远别说:“你错了。”


永远不要对别人说:“让我来告诉你,你哪里错了!”


这是一种挑衅,会造成对方的反感,会让聆听者想要和你争论,即使你是好心提醒,并没有想要引起争端。


如果你直接了当的告诉他:你错了。人们的第一反应是会反击你,但不会让他们改变主意。


因为你的直接否定了对方的判断力、智商、打击了他们的自尊和骄傲。


三、学会认错

  • 靠争夺,你永远都不能让自己满足;
  • 懂得谦让,你才会收获出乎意料的受益。

四、温柔友爱比狂躁和武力更强大

  1. 一滴蜂蜜比一加仑胆汁更吸引苍蝇
  2. 如果你是充满物理倾向的来谈论问题,别人也会气势汹汹的和你争论
  3. 你用温和友好的态度,别人也会不由自主的认同你的观点
  4. 善良友好的交谈方式更容易让别人接受

五、让对方说“是”

  1. 再开始与别人交流的时候,先认可对方,在刚开始得到“是”也多,越容易被接受
  2. 不要着急把自己的见解说出来,先强调你认同对方的那些观点
  3. 如果一开始就让对方说“是”,那么就会忘记争议,听从我的建议
  4. 温和提出问题,一个可以让对方说“是”的问题
  5. 轻履者行远

六、对待抱怨的安全方式

  1. 人们通常会不停的谈论自己的观点,借此获得他人的信服
  2. 不要打断别人的讲话,即便是你不认同的观点
  3. 耐心听完对方的话,一定要诚恳,还要鼓励他们说完想法和意见
  4. 让对方成为你们交谈的主要人物
  5. 如果想交友,就让你的朋友胜过你

七、让对方觉得自己聪明

  1. 和别人的叙述相比,人们更愿意相信自己努力的出来的结论
  2. 让他人觉得这是依照他的想法所为
  3. 我们不喜欢别人强行把东西卖给我们,也不喜欢别人逼迫我们做某事
  4. 我们希望可以购买自己喜欢的东西,可以做自己喜欢的事
  5. 我们希望别人关心我们的想法和愿望

八、学会换位思考

  1. 一定要站在对方的立场想问题
  2. 一个人即使真的错了,也不会认为自己有错
  3. 人们的一切想法和行为都是有依据的
  4. 想要他人和你倾心交谈,请像重视你自己的感受一向重视他人
  5. 出于自愿的改过,不会有不满的情绪
  6. 想让对方认可你钱,先问问自己:“他为什么想做这种事?”

九、与他人充分共情

  1. 你的观点我一点都不否认,如果换做是我,我也会和你有相同的感受
  2. 你所遇到的人中,百分之九十的人都渴望得到怜悯
  3. 对于那些不愉快的情绪,同情能起到关键性的调节作用
  4. 所有人都希望得到同情

十、激发他人高尚的情操

  1. 人们做一件事的原因有两个,一个是高尚的借口,另一个才是真正的理由
  2. 人们总是把自己理想化,更愿意去相信那些高尚的借口
  3. 当你想让他们是发生改变的时候,就需要把他的高尚动机激发出法
  4. 大部分人都是诚实的, 他们一旦认为自己行为是正确的,就会非常想维护这种正确性。
  5. 我相信即是一个村新欺骗的人,当你嘉定他是真诚正直的时候,他也不想辜负你的信任。

十一、戏剧化表达你的想法

  1. 单凭陈述事实不能解决问题,需要以生动有趣的方式展现出来
  2. 在任何常场所,都可以把你想的想法戏剧化的展现出来
  3. 戏剧化表达方式就是利用某件东西,环境来衬托下面要说的事

十二、发起挑战的激励性

  1. 竞争可以产生效率,所以要激发他的竞争意识
  2. 所有人的心中都有恐惧,只有勇士会忘记恐惧,勇往直前。他们可能一败涂地,但通常都会取得胜利
  3. 战胜恐惧是人世间最大的挑战
  4. 每个成功人士都热爱竞争
  5. 激发人对胜利的渴望,对“被重视的感觉”的渴望


作者:子洋
来源:mdnice.com/writing/2b351a9007a249e19c9d3757876c19e5
收起阅读 »

图解项目管理抓手,如何轻松管理项目?

项目管理抓手.png 一、项目进度控制 1.  制定周报、月报、季报等进度报告,实时监控项目进度,及时跟进。 2. 设置里程碑,定期审核各项任务的完成情况和项目整体进度。 3. 使用项目管理软件或工具,制作项目进度计划,动态分配资源和任...
继续阅读 »
项目管理抓手.png

项目管理抓手.png


一、项目进度控制


1.  制定周报、月报、季报等进度报告,实时监控项目进度,及时跟进。


2. 设置里程碑,定期审核各项任务的完成情况和项目整体进度。


3. 使用项目管理软件或工具,制作项目进度计划,动态分配资源和任务。


4. 加强对关键任务的跟踪和监督,确保按时完成。


5. 积极处理进度偏差,采取调整计划、增加资源等措施弥补延误。


二、项目成本控制


1. 制定具体的项目预算,并下达到每个任务和人员。


2. 实施成本监控,分析阶段成本与实际情况间的差异。


3. 评估项目影响要素,制定成本风险预案。


4. 优化资源配置,最大程度提高投入产出比。


5. 严格审核支出,防范不必要的支出。


三、项目变更管理


1. 根据明确的变更标准和流程管理项目变更事项。


2. 通过项目变更委员会或会议,审议和批准变更。


3. 评估变更对其他事项的影响,并做出相应调整。


4. 详细记录变更内容及其历史,方便查询和参考。


5. 定期分析项目变更趋势,降低未来变更的可能性。


四、质量控制


1. 对项目产出物在功能、性能、兼容性和可靠性等方面进行测试和复核。


2. 设置质量标准和控制措施,并对团队成员进行培训和监督。


3. 针对测试或使用过程中发现的问题进行跟踪处理和改进。


4. 详细记录质量数据和信息,分析质量事件的影响因素。


5. 不断完善质量管理流程和标准 ,以提高整体质量水平。


六、风险管理


1. 开展项目风险识别,穷尽可能出现的风险因素。


2. 对风险因素进行定量分析,评估影响程度和可能性。


3. 制定风险应对计划并统筹资源,积极化解风险。


4. 持续监测项目进展中的不确定性,及时更新风险分析与应对。


5. 分析风险事件的教训与启示。为今后提供借鉴。


七、项目团队管理


1.  明确团队各成员的工作任务和职责。


2. 建立沟通机制和交流渠道。


3. 通过奖励机制来激励团队工作积极性。


4. 建立团队协同工作能力,提高效率和效果。


5. 不断完善团队组建和管理流程与机制。


四、其他方面


1. 统筹资源配置,有效考核绩效,激励执行。


2. 保存项目相关文件,创建项目知识库。


3. 分析项目经验教训,为今后提供借鉴。


4. 加强项目状态信息和数据的共享。


5. 不断优化项目管理流程,以提高效率。


作者:极客技术之路
来源:mdnice.com/writing/a3b44581c0f5475a9def01b2c762b3fa
收起阅读 »

8个方面快速提高项目交付速度

项目完成和项目成功地完成是两个不同的概念。作为一个专业的项目管理人员,需要确保项目尽可能地输出好的成果,这个成果要满足以下两个基本要求,才能算是真正成功的项目交付,其中 达到内外部客户满意的要求 是最重要的。 达到基本的质量要求 ...
继续阅读 »

项目完成和项目成功地完成是两个不同的概念。作为一个专业的项目管理人员,需要确保项目尽可能地输出好的成果,这个成果要满足以下两个基本要求,才能算是真正成功的项目交付,其中 达到内外部客户满意的要求 是最重要的。





  • 达到基本的质量要求



  • 达到内外部客户满意的要求


以下是整理的8个方面提高项目交付速度,供大家参考。





  • 提前开始



  • 停止多任务



  • 设定预期



  • 并行增加工作量



  • 减少需求



  • 优化依赖关系



  • 尝试增加风险



  • 优先排序


一、提前开始



如果在项目还没有正式开始之前,项目经理就已经知道项目的时间限制,那么可以提前做好项目的准备工作。特别是对于项目需要交付的内容做好提前规划,对于确定性的工作形成合适的规划,对于不确定的部分提前做好变化应对。一旦不确定性的工作确定下来,可以节省项目前期规划的时间,加快整个项目的交付进度。






  1. 提前组建项目团队,尽早进入工作状态





  2. 与客户进行需求交流,尽早确定需求范围和优先级





  3. 对确定的需求提前进行设计和规划





  4. 将确定的设计转换为项目任务和工作包,形成初步的项目计划。





  5. 对不确定需求,提前制定假设和方案,待确定后快速推进





  6. 提前准备技术方案,评估不同方案的可行性。





  7. 预先确定供应商,签订合同,准备材料





  8. 制定项目进度表、资源计划,确定项目范围。





  9. 提前测试相关工具、环境,确保可用性。





  10. 制定风险管理策略,识别可能的风险因素。




二、停止多任务



许多报告和研究表明,在项目中不断从一个任务转向另一个任务是一种不切实际的工作方式,而且这种方式反而会让工作效率变得更低。项目经理需要确保项目团队成员每天留出专用的时间专注于某一个任务,这样他们才能更好地完成工作,更少地分心,更有可能在截止期限前完成任务。






  1. 制定项目工作规范,建立不间断工作时间段,减少随机任务切换





  2. 对团队成员的工作时间进行合理规划,留出至少2-3小时的专注工作区间。





  3. 在专注区间内,关闭通讯工具,减少外部干扰。





  4. 制定个人工作计划表,按优先级专注于一项任务,避免跳跃作业。





  5. 优化工作环境,减少外界干扰源。





  6. 建立时间管理意识,记录每个工作区间的产出。





  7. 跟踪工作进度,衡量多专注工作的效果提升。





  8. 培养团队自律习惯,鼓励单项任务深度完成。





  9. 合理安排休息,保证工作效率。





  10. 不断优化多任务问题,提升团队效率。




三、设定预期


> 不要总是以为项目的发起人对项目的进度、成本和质量都关注,他们可能只会关注其中的某一个方面。项目经理要管理项目关键利益相关者的期望,一定要找出对方最为关注的点。这是非常重要的一件事,这样一旦项目中出现冲突的时候,项目经理可以做出一定的取舍。如果重点关注质量,并想要提前交付完成,那就可以在保证质量的前提下要求增加资源。





  1. 与项目发起人和关键利益相关方进行充分沟通,理解各方的需求和期望。





  2. 制定项目三角管理框架,评估客户对成本、进度、质量、范围各方面的关注点。





  3. 对客户进行需求调研和访谈,确认关键的期望指标。





  4. 构建项目目标体系,确定客户的主要关注点,如质量或交付时间。





  5. 根据主要关注点设定项目进度和质量目标,形成项目章程。





  6. 进行风险分析,识别可能影响关键关注点的风险因素。





  7. 制定风险缓解策略,保证达成客户关键预期。





  8. 与客户持续沟通,明确范围变更对预期的影响。





  9. 在项目执行中优先保证客户关键关注点,做出必要权衡和取舍。





  10. 不断确认客户的关注点,及时调整项目目标。




四、并行增加工作量



项目经理可以对项目中的各项任务进行审核和分析,看看哪些工作可以早点启动,哪些工作需要和其他任务一并执行。项目经理需要同时注意资源管理,因为你无法安排同一个人同时执行两项任务,如果确实增加了工作量,那就需要引进更多的人来帮助你进行任务管理。






  1. 分析任务依赖关系,识别可以并行执行的任务





  2. 对独立任务进行资源评估,确定可以增加的并行工作量。





  3. 根据关键路径,优先使关键任务尽早开始





  4. 对冗余资源较充裕的任务适当提前启动。





  5. 重新制定资源计划,扩充项目团队,提供并行任务所需资源。





  6. 制定多团队协作机制,加强沟通和协调。





  7. 监控各并行任务的进展,必要时进行资源调配。





  8. 合理安排任务优先级,平衡资源使用。





  9. 加强项目整体计划和风险管理。





  10. 避免过度并行增加项目管理复杂度。




五、减少需求



项目经理可以与项目发起人一起商量这个项目究竟能砍掉些什么,哪些可选的要求能够取消,将范围缩小到实际可以实现的交付工作量。通过这种方式可以去掉不能为实现项目主要目标增值的任务,也就是意味着缩小项目的范围,从而减少交付工时,加快项目完成进度。






  1. 与客户沟通确定核心需求和次要需求。





  2. 对次要需求进行价值分析,评估其实施的优先级。





  3. 制定不同的交付方案,包含次要需求的全量和简化版本。





  4. 估算各方案的交付进度,与客户讨论不同方案的业务影响。





  5. 根据客户的意见,确定移除或简化的次要需求。





  6. 更新需求文档,移除或标记出简化处理的需求。





  7. 重新评估工作量和交付时间表,更新项目计划。





  8. 调整资源分配,将精力集中在核心需求的交付上。





  9. 加强对范围变更的管理,避免需求不断膨胀





  10. 提高客户沟通频率,及时获取客户反馈




六、优化依赖关系



项目中各项任务之间的依赖关系在调度中非常重要,而且往往是项目计划中的关键组成部分,也意味着项目任务必须按照顺序进行。但是,实际的项目交付并非一成不变地按照项目经理制订的计划来进行。这个时候,项目经理要检查所有的依赖关系,看看哪些是必需的,哪些是需要去除的。大多数情况下都能发现一些可以改变和优化的地方。






  1. 清理任务之间不必要的依赖关系,识别可以同时进行的任务。





  2. 对依赖关系进行质疑和验证,确定必须依赖的关键任务链。





  3. 采用方法设计、敏捷开发等,使任务模块更加独立。





  4. 明确依赖关系中的关键路径任务,缩短其时长。





  5. 对冗余依赖任务进行重构、优化或外包。





  6. 增加资源投入,使依赖任务可以并行推进。





  7. 引入新的技术或方法,简化任务流程,减少依赖。





  8. 加强沟通协作,使信息流通,及时消除依赖。





  9. 监控依赖关系变化,及时更新项目计划。





  10. 继续寻找简化依赖的新思路。




七、尝试增加风险


很多时候,过于保守地执行计划会导致项目进度缓慢。如果出现这种情况,为了更快地交付,项目经理可以尝试做出一些改变而不是墨守成规。项目本身的存在即是一种风险,项目经理不应该简单地避免风险,而是要学会管理风险,需要查看项目规划阶段时的假设,看看能否通过一些假设来推动项目进度。





  1. 识别项目进展缓慢的领域,寻找可采取的风险。





  2. 对增加进行成本效益分析,评估收益和代价。





  3. 优先选择对项目进度影响显著、风险较可控的方案。





  4. 制定风险缓解和应急策略,做好风险管理。





  5. 留出时间和资源增加风险缓冲。





  6. 与团队和利益相关方沟通,获取支持。





  7. 监控风险状况,根据需要及时调整策略。





  8. 加强数据统计分析,衡量风险的成效。





  9. 记录风险管理过程,总结经验教训。





  10. 不断提高风险意识和管理能力。




八、优先排序


项目经理可以对工作量和要求进行优先级排序,将低优先级的工作或项目延后,提前完成项目中的高优先级部分。





  1. 明确项目关键路径和里程碑节点





  2. 根据客户需求与项目目标,确定各项任务的优先级。





  3. 制定优先级评估矩阵,评分确定任务顺序。





  4. 优先安排关键路径和高优先级任务所需资源。





  5. 优化资源计划,将非关键任务适当推迟。





  6. 对低优先任务简化要求,减少工作量。





  7. 提前完成高优先级任务,为后续任务创造缓冲。





  8. 与客户沟通,获取对优先级的确认。





  9. 监控进展情况,必要时调整任务优先级。





  10. 持续总结和改进优先排序的决策机制。




作者:极客技术之路
来源:mdnice.com/writing/42e1d4e8e00843e7a5dcdfa6d5db9781
收起阅读 »

看完这篇,SpringBoot再也不用写try/catch了

前言 使用 SpringBoot 开发 Web 应用时,异常处理是必不可少的一部分。在应用中,异常可能会出现在任何地方,例如在控制器、服务层、数据访问层等等。如果不对异常进行处理,可能会导致应用崩溃或者出现未知的错误。因此,对于异常的处理是非常重要的。 ...
继续阅读 »

前言


使用 SpringBoot 开发 Web 应用时,异常处理是必不可少的一部分。在应用中,异常可能会出现在任何地方,例如在控制器、服务层、数据访问层等等。如果不对异常进行处理,可能会导致应用崩溃或者出现未知的错误。因此,对于异常的处理是非常重要的。


本篇主要讲述在SpringBoot 中,如何用全局异常处理优雅的处理异常。


为什么要优雅的处理异常


如果我们不统一的处理异常,开发人员经常会在代码中东一块的西一块的写上 try catch代码块,长久以往容易堆积成屎山。


@Slf4j
@Api(value = "User Interfaces", tags = "User Interfaces")
@RestController
@RequestMapping("/user")
public class UserController {
    /**
     * @param userParam user param
     * @return user
     */

    @ApiOperation("Add User")
    @ApiImplicitParam(name = "userParam", type = "body", dataTypeClass = UserParam.classrequired true)
    @PostMapping("add")
    public ResponseEntity add(@Valid @RequestBody UserParam userParam) {
        // 每个接口都需要手动try catch
        try {
            // do something
        } catch(Exception e) {
            return ResponseEntity.fail("error");
        }
        return ResponseEntity.ok("success");
    }
}

那我们应该如何实现统一的异常处理呢?


使用 @ControllerAdvice + @ExceptionHandler注解



@ControllerAdvice 定义该类为全局异常处理类


@ExceptionHandler 定义该方法为异常处理方法。value 的值为需要处理的异常类的 class 文件。



首先自定义异常类 BusinessException :


/**
 * 业务异常类
 * @author rango
 */

@Data
public class BusinessException extends RuntimeException {
    private String code;
    private String msg;
 
    public BusinessException(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}

然后编写全局异常类,用 @ControllerAdvice 注解:


/**
 * 全局异常处理器
 * @author rango
 */

@ControllerAdvice
public class GlobalExceptionHandler {
    private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
 
    /**
     * 处理 Exception 异常
     * @param httpServletRequest httpServletRequest
     * @param e 捕获异常
     * @return
     */

    @ResponseBody
    @ExceptionHandler(value = Exception.class)
    public ResponseEntity exceptionHandler(HttpServletRequest httpServletRequestException e
{
        logger.error("服务错误:", e);
        return new ResponseEntity("******""服务出错");
    }
 
    /**
     * 处理 BusinessException 异常
     * @param httpServletRequest httpServletRequest
     * @param e 捕获异常
     * @return
     */

    @ResponseBody
    @ExceptionHandler(value = BusinessException.class)
    public ResponseEntity businessExceptionHandler(HttpServletRequest httpServletRequestBusinessException e
{
        logger.info("业务异常报错!code:" + e.getCode() + "msg:" + e.getMsg());
        return new ResponseEntity(e.getCode(), e.getMsg());
    }
}


定义了全局异常处理器,项目就可以对不同的异常进行统一处理了。通常,为了使 controller 中不再使用任何 try/catch,会在 GlobalExceptionHandler 中对 Exception 做统一的拦截处理。这样其他没有用 @ExceptionHandler 配置的异常就都会统一被处理。


遇到异常时主动抛出异常


在业务中,遇到业务异常的地方,我们直接 throw 抛出对应的业务异常即可。如下所示


throw new BusinessException(ERROR_CODE, "用户账号/密码有误");

在 Controller 中的写法


Controller 中,不需要再写 try/catch,除非特殊场景。


@RequestMapping(value = "/test")
public ResponseEntity test() {
    ResponseEntity re = new ResponseEntity();
    // 业务处理
    return re;
}

结果展示


异常抛出后,返回如下结果。


{
    "code""E0014",
    "msg""用户账号/密码有误",
    "data"null
}

注意!!!



  • 抛出的异常如果被代码内的 try/catch 捕获了,就不会被 GlobalExceptionHandler 处理



  • 异步方法中的异常不会被全局异常处理(多线程)



  • 不是 controller 层抛出的异常才能被 GlobalExceptionHandler 处理,只要异常最后是从 contoller 层抛出去的都可以被捕获并处理

总结


本文介绍了使用 SpringBoot 时,如何通过配置全局异常处理器统一处理项目中的一些通用的异常,避免程序员不断的写try/catch导致的代码冗余,有利于代码的维护。


作者:程序员典籍
来源:mdnice.com/writing/103055f00ba04cf4b06f0195f839a449
收起阅读 »

pnpm 是凭什么对 npm 和 yarn 降维打击的

web
大家最近是不是经常听到 pnpm,我也一样。今天研究了一下它的机制,确实厉害,对 yarn 和 npm 可以说是降维打击。 那具体好在哪里呢? 我们一起来看一下。 我们按照包管理工具的发展历史,从 npm2 开始讲起: npm2 用 node 版本管理工具把...
继续阅读 »

大家最近是不是经常听到 pnpm,我也一样。今天研究了一下它的机制,确实厉害,对 yarn 和 npm 可以说是降维打击。
那具体好在哪里呢? 我们一起来看一下。



我们按照包管理工具的发展历史,从 npm2 开始讲起:


npm2


用 node 版本管理工具把 node 版本降到 4,那 npm 版本就是 2.x 了。


768C1B00093D82D19D2CC333F3221670.jpg


然后找个目录,执行下 npm init -y,快速创建个 package.json。


然后执行 npm install express,那么 express 包和它的依赖都会被下载下来:


FB54F396F6A73093CE052A6881AF7C50.jpg
展开 express,它也有 node_modules:


E652FB00C06BA36FA8861E3B785981BF.jpg
再展开几层,每个依赖都有自己的 node_modules:


75AC9B15A99383C9EA9E5E4EF8302588.jpg
也就是说 npm2 的 node_modules 是嵌套的。


这很正常呀?有什么不对么?


这样其实是有问题的,多个包之间难免会有公共的依赖,这样嵌套的话,同样的依赖会复制很多次,会占据比较大的磁盘空间。


这个还不是最大的问题,致命问题是 windows 的文件路径最长是 260 多个字符,这样嵌套是会超过 windows 路径的长度限制的。


当时 npm 还没解决,社区就出来新的解决方案了,就是 yarn:


yarn


yarn 是怎么解决依赖重复很多次,嵌套路径过长的问题的呢?


铺平。所有的依赖不再一层层嵌套了,而是全部在同一层,这样也就没有依赖重复多次的问题了,也就没有路径过长的问题了。


我们把 node_modules 删了,用 yarn 再重新安装下,执行 yarn add express:


这时候 node_modules 就是这样了:


7AF387F155588612B92C329F91D30BFA.jpg


全部铺平在了一层,展开下面的包大部分是没有二层 node_modules 的:


BBBA3B5F68AB691541FD51569E5B1316.jpg


当然也有的包还是有 node_modules 的,比如这样:


B5E0DBF3C7E6FBEEDC2CC90D350A278C.jpg
为什么还有嵌套呢?


因为一个包是可能有多个版本的,提升只能提升一个,所以后面再遇到相同包的不同版本,依然还是用嵌套的方式。


npm 后来升级到 3 之后,也是采用这种铺平的方案了,和 yarn 很类似:


67B0E1280BAD542944AB08A35CCE88C3.jpg
当然,yarn 还实现了 yarn.lock 来锁定依赖版本的功能,不过这个 npm 也实现了。


yarn 和 npm 都采用了铺平的方案,这种方案就没有问题了么?


并不是,扁平化的方案也有相应的问题。


最主要的一个问题是幽灵依赖,也就是你明明没有声明在 dependencies 里的依赖,但在代码里却可以 require 进来。


这个也很容易理解,因为都铺平了嘛,那依赖的依赖也是可以找到的。


但是这样是有隐患的,因为没有显式依赖,万一有一天别的包不依赖这个包了,那你的代码也就不能跑了,因为你依赖这个包,但是现在不会被安装了。


这就是幽灵依赖的问题。


而且还有一个问题,就是上面提到的依赖包有多个版本的时候,只会提升一个,那其余版本的包不还是复制了很多次么,依然有浪费磁盘空间的问题。


那社区有没有解决这俩问题的思路呢?


当然有,这不是 pnpm 就出来了嘛。


那 pnpm 是怎么解决这俩问题的呢?


pnpm


回想下 npm3 和 yarn 为什么要做 node_modules 扁平化?不就是因为同样的依赖会复制多次,并且路径过长在 windows 下有问题么?


那如果不复制呢,比如通过 link。


首先介绍下 link,也就是软硬连接,这是操作系统提供的机制,硬连接就是同一个文件的不同引用,而软链接是新建一个文件,文件内容指向另一个路径。当然,这俩链接使用起来是差不多的。


如果不复制文件,只在全局仓库保存一份 npm 包的内容,其余的地方都 link 过去呢?


这样不会有复制多次的磁盘空间浪费,而且也不会有路径过长的问题。因为路径过长的限制本质上是不能有太深的目录层级,现在都是各个位置的目录的 link,并不是同一个目录,所以也不会有长度限制。


没错,pnpm 就是通过这种思路来实现的。


再把 node_modules 删掉,然后用 pnpm 重新装一遍,执行 pnpm install。


你会发现它打印了这样一句话:


FA450CB6BE37F7AEDADDD7AF8CB5EBF9.jpg


包是从全局 store 硬连接到虚拟 store 的,这里的虚拟 store 就是 node_modules/.pnpm。


我们打开 node_modules 看一下:


DD21BA4ABF8516795C6BC205C18793E3.jpg
确实不是扁平化的了,依赖了 express,那 node_modules 下就只有 express,没有幽灵依赖。


展开 .pnpm 看一下:


25BF2AA593655F0A20232371A43AB81A.jpg
所有的依赖都在这里铺平了,都是从全局 store 硬连接过来的,然后包和包之间的依赖关系是通过软链接组织的。


比如 .pnpm 下的 expresss,这些都是软链接:


6F84C353D1CFE72E2F820B62C9A3B96E.jpg
也就是说,所有的依赖都是从全局 store 硬连接到了 node_modules/.pnpm 下,然后之间通过软链接来相互依赖。


官方给了一张原理图,配合着看一下就明白了:


0E694CA43CC1E52ED6AF8BCD50882004.jpg
这就是 pnpm 的实现原理。


那么回过头来看一下,pnpm 为什么优秀呢?


首先,最大的优点是节省磁盘空间呀,一个包全局只保存一份,剩下的都是软硬连接,这得节省多少磁盘空间呀。


其次就是快,因为通过链接的方式而不是复制,自然会快。


这也是它所标榜的优点:


image.png


相比 npm2 的优点就是不会进行同样依赖的多次复制。


相比 yarn 和 npm3+ 呢,那就是没有幽灵依赖,也不会有没有被提升的依赖依然复制多份的问题。


这就已经足够优秀了,对 yarn 和 npm 可以说是降维打击。


总结


pnpm 最近经常会听到,可以说是爆火。本文我们梳理了下它爆火的原因:


npm2 是通过嵌套的方式管理 node_modules 的,会有同样的依赖复制多次的问题。


npm3+ 和 yarn 是通过铺平的扁平化的方式来管理 node_modules,解决了嵌套方式的部分问题,但是引入了幽灵依赖的问题,并且同名的包只会提升一个版本的,其余的版本依然会复制多次。


pnpm 则是用了另一种方式,不再是复制了,而是都从全局 store 硬连接到 node_modules/.pnpm,然后之间通过软链接来组织依赖关系。


这样不但节省磁盘空间,也没有幽灵依赖问题,安装速度还快,从机制上来说完胜 npm 和 yarn。


pnpm 就是凭借这个对 npm 和 yarn 降维打击的。


作者:JEECG官方
来源:juejin.cn/post/7260283292754919484
收起阅读 »

前端发展:走进行业迷茫的迷雾中

web
引言 2023年,前端开发作为IT行业中备受关注的领域之一,正在经历着巨大的挑战和变革。然而,在当前行业不景气、失业率居高不下以及裁员潮席卷而来的情况下,许多人开始质疑前端开发的未来前景以及学习它是否依然有意义。本文将探讨这个问题并试图给出一些启示。 第一部...
继续阅读 »

引言


image.png
2023年,前端开发作为IT行业中备受关注的领域之一,正在经历着巨大的挑战和变革。然而,在当前行业不景气、失业率居高不下以及裁员潮席卷而来的情况下,许多人开始质疑前端开发的未来前景以及学习它是否依然有意义。本文将探讨这个问题并试图给出一些启示。


第一部分:前端的价值


image.png
前端开发作为网页和移动应用程序开发的重要组成部分,扮演着连接用户与产品的桥梁。前端技术的发展不仅推动了用户体验的提升,也对整个互联网行业产生了深远的影响。随着移动互联网的普及和技术的进步,前端在用户与产品之间的交互变得越来越重要。


对于企业而言,拥有优秀的前端开发团队意味着能够提供更好的用户体验、增强品牌形象、吸引更多用户和扩大市场份额。因此,前端开发的技能依然是企业争相追求的核心能力之一。


第二部分:行业不景气的背后


image.png
然而,正如每个行业都经历高低起伏一样,前端开发也面临着行业不景气带来的挑战。2023年,全球经济增长乏力、市场竞争激烈以及萧条的就业市场等因素,使得许多公司紧缩预算、停止招聘,并导致了失业率的上升和裁员的潮水。


在这种情况下,前端开发者需要重新审视自己的技能和市场需求。他们需要具备综合能力,包括对最新前端技术的深入了解、与其他团队成员的良好沟通合作能力以及持续学习和适应变化的能力。


第三部分:自我调整与进阶


image.png
面对市场变化和就业压力,前端开发者需要主动调整自己的发展路径。以下是一些建议:



  1. 多元化技能:学习并精通多种前端框架和库,如React、Vue.js和Angular等。同时,了解后端开发和数据库知识,拥有全栈开发的能力,将会让你在就业市场上更具竞争力。

  2. 学习与实践并重:不仅仅是学习新知识,还要将所学应用于实际项目中。积累项目经验,并在GitHub等平台分享你的作品,以展示自己的能力和潜力。同时,参加行业内的比赛、活动和社区,与他人交流并学习他们的经验。

  3. 持续学习:前端技术发展日新月异,不断学习是必需的。关注行业的最新趋势和技术,参加培训、研讨会或在线课程,保持对新知识的敏感度和学习能力。


第四部分:面对就业市场的挑战


image.png
在面对行业不景气和裁员的情况下,重新进入就业市场变得更加具有挑战性。以下是一些建议:



  1. 提升个人竞争力:通过获得认证、实习或自主开发项目等方式,提升自己在简历中的竞争力。扩展自己的专业网络,与其他开发者和雇主建立联系。

  2. 寻找新兴领域:探索新兴的技术领域,如大数据、人工智能和物联网等,这些领域对前端开发者的需求逐渐增加,可能为你提供新的机会。

  3. 转型或深耕细分领域:如果市场需求不断减少,可以考虑转型到与前端相关的领域,如UI设计、交互设计或用户体验设计等。或者在前端领域深耕细分领域,在特定行业或特定技术方向上寻找就业机会。


结论


image.png
虽然当前的行业环境确实严峻,但前端开发作为连接用户与产品的重要纽带,在未来依然有着广阔的发展空间。关键在于前端开发者要不断自我调整与进阶,持续学习并适应市场需求。通过多元化技能、学习实践、提升个人竞争力以及面对市场挑战,前端开发者依然可以在这个变革

作者:Jony_men
来源:juejin.cn/post/7260330862289371173
时代中谋得一席之地。

收起阅读 »

树结构的数据扁平化

web
function flattenTree(data) { data = JSON.parse(JSON.stringify(data)); var res = []; while(data.length) { var n...
继续阅读 »

function flattenTree(data) {
data = JSON.parse(JSON.stringify(data));
var res = [];
while(data.length) {
var node = data.shift();
if (node.children && node.children.length) {
data = data.concat(node.children);
}
delete node.children;
res.push(node);
}
return res;
}


我们用一个数据来测试:



var tree = [{
id: 1,
name: '1',
children: [{
id: 2,
name: '2',
children: [{
id: 3,
name: '3',
children: [{
id: 4,
name: '4'
}]
}, {
id: 6,
name: '6'
}]
}]
}, {
id: 5,
name: '5'
}]


使用:



console.log(flattenTree(tree));


打印结果:


image.png


作者:tntxia
来源:juejin.cn/post/7260500913848090661
收起阅读 »

千万级高可用分布式对账系统设计实践

背景         目前线上业务量与日俱增,每日的订单量超过千万,资金流动大,资金安全成为了重点关注的问题。为了确保每一笔交易的正确性,提高资金的正确性和保障业务的利益,除了RD代码逻辑严格以外,还需要对每日甚至每小时订单的流水进行核对,对异常情况能及时处理...
继续阅读 »

背景


        目前线上业务量与日俱增,每日的订单量超过千万,资金流动大,资金安全成为了重点关注的问题。为了确保每一笔交易的正确性,提高资金的正确性和保障业务的利益,除了RD代码逻辑严格以外,还需要对每日甚至每小时订单的流水进行核对,对异常情况能及时处理。面对千万级的订单量,人工对账肯定是不可行的,所以,实现一套对账系统成为了必然的事,不仅为资金安全提供依据,也节省公司运维人力,数据更加可视化。目前这套系统已覆盖聚合渠道网关与外部渠道100%的对账业务,完成春晚期间支付宝亿级订单量对账,完成日常AC项目千万级订单量对账,对账准确率实现6个9,为公司节省2~3个人力。


介绍


        对账模块是支付系统的核心功能之一,不同业务设计的对账模型不同,但是都会遇到以下几个问题:



  • 海量的数据,就目前聚合支付的订单量来看,设计的对账系统需要应对千万级的数据量;

  • 面对日切、多账、少账等异常差异订单应该如何处理;

  • 账单格式、下载账单时间、下载方式等不一致问题。


        针对以上问题,并结合财经聚合支付系统的特点,本文将设计一套可以应对千万级数据量、分布式和高可用的对账系统,利用消息队列Kafka的解耦性解决对账系统各模块之间的强依赖性。文章从三个方面介绍对账系统,第一方面,总体介绍对账系统的设计,依次介绍各个模块的实现及其过程中使用到的设计模式;第二方面,介绍对账系统版本迭代的过程,为什么需要进行版本迭代,以及版本迭代过程中踩过的“坑”;第三方面,总结现有版本的特点并提出下一步的优化思路。


系统设计


系统结构图


        图1为对账系统总结构图,分为六个模块,分别是文件下载模块、文件解析并推送模块、平台数据获取并推送模块、执行对账模块、对账结果统计模块和中间态模块,每个模块负责自己的职能。
对账系统总结构图


图1 对账系统总结构图


        图2为对账系统利用Kafka实现的状态转换图。每个模块独立存在,彼此之间通过消息中间件Kafka实现系统状态转换,通过中间态UpdateReconStatus类实现状态更新和message发送。这种设计不仅实现流水线对账,也利用消息中间件的特点,实现重试和模块之间的解耦。

对账系统状态转换图.png


图2 对账系统状态转换图


        为了更好的了解每个模块的实现过程,下面将依次对各个模块进行说明。

文件下载模块


设计

        文件下载模块主要完成各个外部渠道账单的下载功能。众所周知,聚合支付是聚众家三方机构能力为一体的支付方式,其中三方机构包括支付宝、微信等支付界的领头羊,多样性的支付渠道导致账单下载存在多样性,如何实现多模式、可拔插的文件下载能力成为该模块设计的重点。分析Java设计模式的特点,本模块采用接口模式,符合面向对象的设计理念,可实现快速接入。具体实现类图如图3所示(只展示部分类图)。


图3 文件下载实现类图


        下面就以支付宝对账文件下载方式为例,具体阐述一下实现过程。


实现

        分析支付宝接口文档,目前采用的下载方式为HTTPS,文件格式为.csv的压缩包。根据这些条件,本系统的实现方式如下(只摘取了部分代码)。由于消息中间件Kafka和中间态模块的机制,已经从系统层面考虑了重试的能力,因此不需要考虑重试机制,后续模块也如此。


public interface BillFetcher {
// ReconTaskMessage 为kafka消息,
// FetcherConsumer为自定义账单下载后的处理方式
String[] fetch(ReconTaskMessage message,FetcherConsumer consumer) throws IOException;
}

@Component
public class AlipayFetcher implements BillFetcher {

public AlipayFetcher(@Autowired BillDownloadService billDownloadService) {
Security.addProvider(new BouncyCastleProvider());
billDownloadService.register(BillFetchWay.ALIPAY, this);
}
...
@Override
public String[] fetch(ReconTaskMessage message, FetcherConsumer consumer) throws IOException {
String appId = map.getString("appId");
String privateKey = getConvertedPrivateKey(map.getString("privateKey"));
String alipayPublicKey = getPublicKey(map.getString("publicKey"), appId);
String signType = map.getString("signType");
String url = "https://openapi.alipay.com/gateway.do";
String format = "json";
String charset = "utf-8";
String billDate = DateFormatUtils.format(message.getBillDate(), DateTimeConstants.BILL_DATE_PATTERN);
String notExists = "isp.bill_not_exist";
String fileContentType = "application/oct-stream";
String contentTypeAttr = "Content-Type";
//实例化客户端
AlipayClient alipayClient = new DefaultAlipayClient(url, appId, privateKey, format, charset, alipayPublicKey, signType);
//实例化具体API对应的request类,类名称和接口名称对应,当前调用接口名称
AlipayDataDataserviceBillDownloadurlQueryRequest request = new AlipayDataDataserviceBillDownloadurlQueryRequest();
// trade指商户基于支付宝交易收单的业务账单
// signcustomer是指基于商户支付宝余额收入及支出等资金变动的帐务账单
request.setBizContent("{" +
""bill_type":"trade"," +
""bill_date":"" + billDate + """ +
" }");
AlipayDataDataserviceBillDownloadurlQueryResponse response = alipayClient.execute(request);
if(response.isSuccess()){
//do 根据下载地址获取对账文件,通过流式方式将文件放到指定的目录下
...
System.out.println("调用成功");
} else {
System.out.println("调用失败");
}
}
}

具体步骤:



  1. 重写构造方法,将实现类注入到一个map中,根据对应的下载方式获取实现类;

  2. 实现fetch接口,包括构造请求参数、请求支付宝、解析响应结果、采用流式将文件放入对应的目录下,以及这个过程中的异常处理。


文件解析并推送模块


设计

        前面提到,聚合支付是面对不同的外部渠道,对账文件的多样性不言而喻。比如微信是采用txt格式,支付宝采用csv格式等等,而且各个渠道的账单内容也是不一致的。如何解决渠道之间账单的差异性成为该模板需要重点考虑的问题。通过调研和现有对账系统的分析,本系统采用接口模式+RDF(结构化文本文件)的实现方式,其中接口模式解决账单多模式的问题,同时也实现可拔插的机制,RDF工具组件实现账单的快速标准化,操作简单易会。具体实现类图如图4所示(只展示部分类图)。


图4 文件标准化实现类图


        下面就以支付宝对账文件解析为例,具体阐述一下实现过程。
实现

        根据支付宝的账单格式,提前定义RDF标准模板,后续账单解析将根据模板将每一行对账文件解析为对应的一个实体类,其中需要注意标准模板的字段必须要和账单数据一一对应,实体类的字段可以多于账单字段,但必须包括所有的账单字段。接口定义如下:


public interface BillConverter<T> {
//账单是否可以使用匹配器
boolean match(String channelType, String name);
//转换原始对账文件到Hive
void convertBill(InputStream sourceFile, ConverterConsumer<T> consumer) throws IOException;
//转换原始对账文件到Hive
void convertBill(String localPath, ConverterConsumer<T> consumer) throws IOException;
}

具体实现步骤如图5所示:


流程图.png


图5 文件解析流程图



  1. 定义RDF标准模板,如下为支付宝业务流水明细模板,其中body结构内字段名必须和实体类名保持一致。


{
"head": [
"title|支付宝业务明细查询|Required",
"merchantId|账号|Required",
"billDate|起始日期|Required",
"line|业务明细列表|Required",
"header|header|Required"
],
"body": [
"channelNo|支付宝交易号",
"merchantNo|商户订单号",
"businessType|业务类型",
"production|商品名称",
"createTime|创建时间|Date:yyyy-MM-dd HH:mm:ss",
"finishTime|完成时间|Date:yyyy-MM-dd HH:mm:ss",
"storeNo|门店编号",
"storeName|门店名称",
"operator|操作员",
"terminalNo|终端号",
"account|对方账户",
"orderAmount|订单金额|BigDecimal",
"actualReceipt|商家实收|BigDecimal",
"alipayRedPacket|支付宝红包|BigDecimal",
"jiFenBao|集分宝|BigDecimal",
"alipayPreferential|支付宝优惠|BigDecimal",
"merchantPreferential|商家优惠|BigDecimal",
"cancelAfterVerificationAmount|券核销金额|BigDecimal",
"ticketName|券名称",
"merchantRedPacket|商家红包消费金额|BigDecimal",
"cardAmount|卡消费金额|BigDecimal",
"refundOrRequestNo|退款批次号/请求号",
"fee|服务费|BigDecimal",
"feeSplitting|分润|BigDecimal",
"remark|备注",
"merchantIdNo|商户识别号"
],
"tail": [
"line|业务明细列表结束|Required",
"tradeSummary|交易合计|Required",
"refundSummary|退款合计|Required",
"exportTime|导出时间|Required"
],
"protocol": "alib",
"columnSplit":","
}


  1. 实现接口的getChannelType、match方法,这两个方法用于匹配具体使用哪一个Convert类。如匹配支付宝账单,实现方式为:


@Override
public String getChannelType() {
return ChannelType.ALI.name();
}
@Override
public boolean match(String channelType, String name) {
return name.endsWith(".csv.zip");
}


  1. 实现接口的convertBill方法,完成账单标准化;


@Override
public void convertBill(String path, ConverterConsumer<ChannelBillPojo> consumer) throws IOException
{
FileConfig config = new FileConfig(path, "rdf/alipay-business.json", new StorageConfig("nas"));
config.setFileEncoding("UTF-8");
FileReader fileReader = FileFactory.createReader(config);
AlipayBusinessConverter.AlipayBusinessPojo row;
try {
while (null != (row = fileReader.readRow(AlipayBusinessConverter.AlipayBusinessPojo.class))) {
convert(row, consumer);
}
...
}


  1. 将标准化账单推送至Hive


平台数据获取并推送模块


        平台数据获取一般都是从数据库中获取,数据量小的时候,查询时数据库的压力不会很大,但是数据量很大时,如电商交易,每天成交量在100万以上,通过数据库查询是不可取的,不仅效率低,而且容易导致数据库崩溃,影响线上交易,这点会在后续的版本迭代中体现。因此,平台数据的抽取是从Hive上获取,只需要提前将交易数据同步到Hive表中即可,这样做不仅效率高,而且更加安全。考虑到抽取的Hive表不同、数据的表结构,数据收集器Collector类也采用了接口模式。Collector接口定义如下:


public interface DataCollector {
void collect(OutputStream os) throws IOException;
}

        根据目前平台数据收集器实现情况,可以得到类图如图6所示。


图6 平台数据收集器实现类图


执行对账模块


        该模块主要完成Hive命令的执行,在平台账单和渠道账单已全部推送至Hive的前提下,利用Hive处理大数据效率高的特点,执行全连接sql,并将结果存入指定的Hive表中,用于对账结果统计。执行对账sql可以根据业务需求而定,如需要了解本系统的全连接sql,欢迎与我交流。


对账结果统计模块


        对账任务执行成功之后,需要统计全连接后的数据,重点统计金额不一致、状态不一致、日切、少账(平台无账,渠道有账)和多账(平台有账,渠道无账)等差异。针对不同的情况,本系统分别采用如下的解决方案:



  1. 金额不一致:前端页面展示差异原因,人工进行核对;

  2. 状态不一致:针对退款订单,查询平台退款表,存在且金额一致认为已对平,不展示差异,其他情况,需要在前端页面展示差异原因,人工进行核对;

  3. 日切:当平台订单为成功,渠道无单时,根据平台订单创建时间判断是否可能存在日切,如果判断是日切订单,会将这笔订单存入buffer文件中,待统计结束后,将buffer文件上传至Hive日切表中,等第二天重新加载这部分数据实现跨日对账。对于平台无订单,渠道有单的情况,通过查询平台数据库判断是否存在差异,如果存在差异,需要在前端页面展示差异,人工进行核对。

  4. 少账:目前主要通过查询平台数据库判断是否存在差异,确认确实存在差异时,需要在前端页面展示差异,人工进行核对。

  5. 多账:目前这种有可能是日切,会先考虑日切,如果不在日切范围内,需要在前端页面展示差异,人工进行核对。


中间态模块


        中间态模块是用于各模块之间状态转换的模块,利用Kafka和状态是否更新的机制,实现消息的重发和对账状态的更新。从一个状态到下一个状态,必须满足当前状态为成功,对账流程才会往下一步执行。中间态的设计不仅解决了重试问题,而且将数据库的操作进行了收敛,更符合模块化的设计,各个模块各司其职。重试次数也不是无限的,目前设置的重试次数为3次,如果3次重试后依然没有成功,会发lark通知,人工介入解决。


        总之,对账工作,既复杂也不复杂,需要我们细心,对业务要有深入的了解,并选择合适的处理方式,针对不同的业务,不断迭代优化系统。


版本迭代


        系统的设计很大程度受业务规模的影响,对于财经聚合支付而言,订单量发生了几个数量级的变化,这个过程中不断暴露出对账系统存在的问题,优化改进对账系统是必然的事。从系统设计到目前大致可以分为三个阶段:初始阶段、过渡阶段和当前阶段。


初始版(v1.0)

        初始版上线后实现了聚合渠道对账的自动化,尤其在2018年的春节活动中,资金安全提供了重要的保障,实现了聚合和老合众、支付宝、微信等渠道的对账。随着财经业务的发展,抖音电商的快速崛起,对账系统逐渐暴露出不足,比如对账任务失败增多,尤其是数据量大的对账、非正常差异结果展示、对账效率低等问题。通过不断分析,发现存在以下几个问题:



  1. 系统的文件都是放在临时目录tmp下的,TCE平台会对这个目录下的文件定时清理,导致推送文件到Hive时会报找不到文件的情况,尤其是大数据量的对账任务;

  2. Kafka消息积累多,导致对账流程中断,主要是新增渠道,对账任务增加,同时Hive执行队列是共享队列,大部分的对账流程因为没有资源而卡住;

  3. 非正常差异结果展示,主要是查单没有增加重试机制,当查询过程中出现超时等异常,会出现非正常差异结果,还有部分原因是日切跨度小而导致的非正常差异结果。


过渡版(v2.0)

        考虑到初始版对账系统存在的不足和对账功能的急迫性,对初始版进行过渡性的优化,初步实现大数据量的对账功能,同时也提高了差异结果的准确率。相比初始版,该版本主要进行了以下几点优化:



  1. 文件存放目录由临时目前改为服务下的某一个目录,防止大文件被回收,文件上传到Hive后删除文件;

  2. 重新申请独占的执行队列,解决资源不足导致对账流程卡住的问题;

  3. 查单新增重试机制,日切跨度增大,解决非正常差异结果展示,提供差异结果的准确率。


        过渡版集中解决初始版明显存在的问题,对于一些潜在的问题并没有彻底解决,如代码容错率低、对账任务异常后人工响应慢、对账效率低、数据库安全性低等问题。


当前版(v3.0)

        当前版优化的宗旨是实现对账系统的"三高",分别为高效率、高准确率(6个9)和高稳定性。


        对于高效率,主要体现在平台数据获取慢,而且存在数据库安全问题,针对这块逻辑进行了优化,改变数据获取途径,由原来的数据库获取改为从高效率的Hive中获取,只需要提前将数据同步到Hive表中即可。


        对于高准确率,主要优化对账差异处理逻辑,进一步细化差异处理方式,新增差异结果报警,细化前端页面差异原因。


        对于高稳定性,主要优化RDF处理对账文件发生异常时新增兜底逻辑,提高系统的容错性;对账任务失败或超过指定重试阈值时增加报警,加快人工响应速率;对查单等操作数据库逻辑增加限流,防止数据库崩溃。


        版本迭代过程可以总结如下,希望读者别重复入坑,尤其是大文件处理方面。


业务情况优点存在的问题目标
初始版(v1.0)财经部门初期,订单量少,业务结构简单实现少量交易量对账;支持分布式效率低;对账任务容易卡住;非异常case普遍;大数据基本不能完成对账保障资金安全问题,实现聚合渠道网关与外部渠道的对账功能
过渡版(v2.0)电商业务崛起,订单量增加,业务种类增多实现海量数据对账;查单新增重试机制;降低非异常case数量影响数据库安全性;代码容错率低;对账效率低;对账任务异常时人工响应慢支持千万级订单量对账
当前版(v3.0)优化过渡版遗漏问题,改变数据获取路径效率大大提升;实现千万级数据量对账;实现高稳定性,高准确率,高效率全连接效率低;不支持订单状态推进实现对账系统的高效率,准确率实现6个9;功能全面

总结


        对账系统模型与业务息息相关,业务不同,对账系统模型也会不同,但是大部分对账系统的整体架构变化不大,主要区别是各个模块的实现方式不同。希望本文介绍的对账系统能为各位读者提供设计思路,避免重复入坑。对对账系统感兴趣的同学可以找财经支付团队同学详聊,一起深入探讨,提出优化建议,比如优化全连接策略,也欢迎各种简历推荐。


参考文章


信息流对账与平台化实现-曾佳


混合编程在财经对账中的应用-王亚宁


内推链接


image.png

收起阅读 »

【镜·映】《烂》:没有反转的生活

增村保造的这部《烂》(Tadare,1962,tt0310199),改编自日本自然主义大家德田秋声的同名小说。 益子(Masuko,若尾文子)在东京与汽车销售员浅井(Asai,田宫二郎)同居一段时间之后,才发现这个男人原有妻子。益子不情愿破坏浅井的婚姻,提议...
继续阅读 »


增村保造的这部《烂》(Tadare,1962,tt0310199),改编自日本自然主义大家德田秋声的同名小说。


益子(Masuko,若尾文子)在东京与汽车销售员浅井(Asai,田宫二郎)同居一段时间之后,才发现这个男人原有妻子。益子不情愿破坏浅井的婚姻,提议分手,但浅井早已经不能忍受神经质的妻子了,只是因为早年接受过妻家的资助,一直未能下决心离婚。被益子发现后,浅井终于离了婚,而前妻也因承受不住打击而精神崩溃。此时益子的侄女英子(Eiko,水谷八重子)不愿与家里安排的对象相亲,从乡下跑到东京,寄宿在姑姑家中。英子向往大城市的生活,却又看不起给人做情妇的姑姑,结果却是自己与浅井勾搭在一起,被益子捉奸在床,怒不可遏的益子将英子逐出家门。而后则强行安排英子与她原本的相亲对象——一个西装革履的农民结了婚。


小三上位,然后又被自己的侄女绿了,好在她“奋起反击”,终于捍卫了所谓“爱情”——看起来,这是一段有些夸张却俗套的故事,然而,故事的结尾,却拍得令人震撼。


一切依传统进行。姑姑拉扯着身着新娘盛装的侄女来到一众亲友面前,“看,她是个好新娘!”





浅井站起来,走到英子面前,面对着这个前两天还和自己享受着最后疯狂的性爱的女子,他挤出一些不多的笑容:“你真美,你太棒了!”,然后转头离开。





在众人面带微笑的审视中,一脸漠然的新人被送上花车,她的姑姑益子就坐在她身边,紧盯着她,像极了押送犯人走向牢笼。





镜头切换,开往名古屋的列车就要出发了。





已经换了便装的英子坐在车厢中,带着幽怨凝视着窗外的浅井——他和益子站在窗外,眉头微蹙,益子则面无表情。





英子的丈夫在模糊的前景中微笑着和他人告别。车站的广播,正一遍遍播放着:“请站在白线以外,请站在白线以外......”





列车徐徐开动,浅井缓缓抬起右手,犹犹豫豫地做了一个告别的手势,似乎还未完成,就缓缓放下了。





益子抬眼凝视着他,轻叹一口气,默然转头,独自离开,送行人群纷纷挥动手臂,背景渐渐模糊,益子面色苍白疲惫,仿佛仍旧难以释怀,而明明一切已然结束,除了当事人,没有人知道姑侄两人曾是情敌,为了争夺一个男人,到了以死相拼的地步。





本来,电影到这里其实可以结束了。然而,接下来的3分多钟,才是见证一位大导演真实功力的时刻。


送走英子,益子和浅井 “像往常一样” 回到“家”,开始了 “像往常一样”“日常时刻”


益子问:“要吃点东西吗?”“要我给你准备洗澡水吗?”“要睡觉了吗?”“我给你泡点热茶吧?”浅井一概说不。





然而他回到卧室,看着曾经和两个女人翻滚过的床,却又觉得空虚。





回到客厅,益子已经泡好了茶。两人开始 “像往常一样”“日常闲谈”





益子说,“她会是一位好妻子”,浅井说,“也许吧”;益子说,“举办婚礼真好”“我们要不要也举办一场婚礼?”,浅井笑笑说,“那也挺好,我们准备一下吧”。然后,就独自回房了。





益子漠然坐着,低下头将茶杯顶在额头。自己的提议并未遭到拒绝,然而,浅井那种怎么都无所谓的回答,却比拒绝还令人难受。





尽管如此,当听到浅井那一句“你怎么还不来?”,她还是反射般地应道 “哈依” 。不想表达内心的苦楚,因为表达了也无意义。她开始一件一件脱去衣服,像往常一样搭在椅子背上,只留下半透的薄纱内衣。





然后,益子慢慢踱向内室,带上了门,屏幕转暗,左下角显示出一个 “终” 字。





还记得《毕业生》(The Graduate,1967)最后的反转吗?曾被女友母亲诱惑的男主最后的时刻鼓起勇气冲入婚礼现场,劫走了即将成为别人新娘的女友,宣告一切错误终结与新生活的开始。


然而,《烂》的结尾,没有反转


一切如常。


一切都过去了,一切都被无形的力量压制在生活的 “日常和谐” 之下,再无声息,只有三个当事人吞咽下那些无法言说的苦楚,不出意外的话,他们将会把这些意难平带进坟墓。而那些不明缘由的关系人与看客,只会知道这是一场完美的婚礼,并为此或真情或假意地抚掌相庆。错误已被终结,但新的生活并未开始。


村上春树说:“我不想找一个搭伙过日子的人,我要找一个一见我就笑,我一见就笑,喝了酒满眼光给我讲浪漫和爱的人。”也许,这是个讽刺。


也许,浅井和益子也曾期待过那样的生活。但经过这小小的插曲,他们的生活似乎又回到了常态,并且很可能那就是他们可能期待的、唯一的生活。


益子看似是这场“宫斗”戏中的胜利者,她挤走了浅井的原配,又逼退了自家的侄女,然而,除了肉体,她不知道还有什么可以留住这个男人——现在连这一点,她都不那么确定了。更为悲哀的是,她似乎只能接受这种生活的“安排”,她可以战胜情敌,却无力摆脱一个社会系统将她锁定的位置。某种程度上,她甚至不如英子这个“失败者”,至少英子曾经痛快地享受过Stolen Pleasure(这是影片的另一个名字)。


如果仅从女性主义的角度解读这部电影,就会忽略一个事实:浅井也不是胜利者。这个男人因为接受过前妻家的资助,娶了他并不爱的女人,觉得自己处处受制于神经质的妻子,好不容易摆脱了,却发现益子的善妒与疯狂,比前妻更甚。在这场荒唐的情爱纠缠中,的确女性受到的损害更甚,但浅井也没办法为所欲为,当益子和英子两位女性疯狂地撕打在一起时,他的惶恐无措说明了一切。他依靠益子摆脱了前妻,现在他必须接受这个可以为了保住自己的位置而试图掐死自己侄女的疯狂的女人。这里没有胜利者,也没有自由人。


并非是男人操纵女人,或者女人操纵男人那么简单,所有人都在被一只看不见的手操弄着。


人一般很难超越对于自己身处其中的社会的既定秩序的理解。 一般人所能做的,就是无意识地压抑,然后再无意识地合理化这种压抑:事实如此,历来如此,所有人都如此。


故事中所有的当事人,都不是脸谱化的坏人,浅井可以为情妇的兄弟慷慨解囊(当时英子还没出现);益子曾经不愿拆散浅井的婚姻,在愤怒地将英子逐出家门之后,又不忍她流落街头。


普通人的普通,也许正在于此,无法摆脱甚至完全意识不到自己就生活在社会话语的规训中,不能知行合一地依照本心行事,却又无法让良知彻底沉默。


这当然不是说人就应当违背公序良俗、像野蛮人一样生活,而是说每个人都应该意识到这些话语权力与生存困境,这种问题意识的觉醒也许会带来痛苦,但却是通向自由意志选择的必经之路。像《烂》中的男男女女一样,本能的情欲化反抗,彼此扯着头发的撕打,保卫虚假爱情的算计,始终都不可能在没有反转的生活里掀起一点点波澜。


作者:wingsay
来源:mdnice.com/writing/df16952233da49c1816ddf3746d1fa84
收起阅读 »

构建写作世界的元素:概念库、词汇库与风格感觉的塑造

对于那些渴望在文字的广袤世界中留下个人思想和观点的人来说,写作无疑是一项必备的技能。噢,老板曾言我写的文字仿若小学生。或许,老板高估了我,我自觉可能还不及个别小孩儿的水平。 为了能够写出简单、清晰、真实的文章,我构想了一个写作工具箱,从下到上约4层内容,从基...
继续阅读 »


对于那些渴望在文字的广袤世界中留下个人思想和观点的人来说,写作无疑是一项必备的技能。噢,老板曾言我写的文字仿若小学生。或许,老板高估了我,我自觉可能还不及个别小孩儿的水平。


为了能够写出简单、清晰、真实的文章,我构想了一个写作工具箱,从下到上约4层内容,从基本素材到风格感觉,从写作方法到写作SOP(Standard Operating Procedure),力求全方位地提升我的写作能力。目前只完成了第1层基本素材,其他层次怎在探索中,哈哈。



第1层 基本素材

第1层 基本素材


第1层,基本素材,正如Stephen King所说,包括概念、词汇和语法,它们是文章的基石,是构建文字世界的灵感火花。这些元素将成为写作中常常使用的工具。


1、概念库


什么是概念呢?概念是对一个事物的清晰定义。我的理解就是概念在说清楚什么是什么。概念之所以重要,就好比你修房子时所用的砖块,虽然微小不起眼,却是构成了我们构建知识和认知大厦的基本单位。


为什么要掌握清晰的、准确的、必要的概念?它们为我们的思考提供了基本框架。明辨式思维,理性决策,都依赖于对概念的准确掌握。


最近学到了一个有趣的例子,让我费曼一下,哈哈。你是否知道「给予」和「付出」之间的区别?


在亲密关系中,「给予」意味着我愿意给你这些东西,做这件事情本身让我很快乐。例如你有喜欢的东西,而我正好能送你,我会因为你的喜欢而高兴;而「付出」则不同,它带有一种期望回报的心态。就是说我给你这个东西,是希望你下一次也要回报给我,并不是因为这个行为本身让我快乐。例如我请你吃顿饭,下次你得回请我才行。


这两个概念反应的是爱的动力机制问题。弗洛姆说:爱是给予,这种给予是抛开计算的,不存在我送你一份东西,你就欠我们一份人情之说。


掌握清晰而准确的概念,可以帮助我们在行动决策时,做出符合自己价值观的事情,认清什么重要,什么更重要。


概念库的构建并不是独自进行的,就像人是社会性动物一样,我们需要与他人互动、交流,形成社区、小团体,与同频的小伙伴一起搭伴前行,才能走的更好一样。


当建立起概念库之后,这些概念与概念之间,有多少清晰的、必要的联系?是接下里要做的事情。不要让它们孤立存在,而是相互联系、相互补充,形成一个有机的整体。


2.词汇库


什么是词汇?词汇≠单词,它是由单词与短语组成的丰富集合体。在构建词汇库时,我们可以放入各种素材,不仅仅局限于单个词汇,也可以是短语、句子甚至一段话。


词汇库是文章的基石,它决定了文章的架构与内涵。虽然名言金句能为文章增色不少,但并不能形成文章的骨架和血肉。因此,在词汇库中,应该注重收集更多事实性知识、概念性知识、作者的重要理论、对话、案例以及那些支持批判性思考的素材。这些资料能够确保你的论证有据可依,避免将个人轶事或经历当作普遍规律,从而在写作时避免陷入无病呻吟和猛灌鸡汤的误区。


如何扩大词汇库?仅仅拿个本子摘抄是不够的。还记得小时候死命摘抄的「金句本」吗?我现在还有一大摞,抄的时候激情澎湃,都是名言佳句;用的时候抓耳挠腮,为啥?不是书到用时方恨少,而是书到用时找不到!这就是纸质版的缺点,无法实现快速检索和有效提取内容,翻半天都找不到想用的那句话写在哪里,气哭自己系列。


因此,扩大词汇库时,不仅要考虑放在在哪里,还要要考虑怎么提取方便。怎么放?放的目的是为了将来能够快速的提取。要实现这个目标,需要做到2点。


首先,将工具电子化,如Obsidian、 iA writer,它们的优势在于能够实现关键词检索、概念互链、工具之间互相转换(例如Obsidian->Anki)。将收集的素材打上标签或拟定标题,这样一键搜索即可轻松找到需要的内容。同时,电子工具的容量也较大,方便随时收藏和携带,让词汇库始终伴随你左右。


其次,建立一个素材收集体系并定期整理。虽然关键词检索能够实现快速查找,但随着时间推移,收集的词汇会不断增多,简单地收集已经无法满足我们的需求。因此,根据主题或兴趣进行分类整理,有助于进行更有系统性的提取。



词汇库-写作主题

词汇库-写作主题


3. 语法库


写作是一门既关乎内容又关乎表达的艺术。语法作为语言的基本规则,通过简洁精炼的表达方式,赋予文字更强大的力量和自信。虽然英文和中文的语法略有不同,但本质上有许多共通之处。


真正的写作应当摒弃造作和恐惧。只有放下恐惧和造作才能写出好东西。要使表达简洁精炼,通常使用「名词+动词」形式,减少不必要的副词修饰和被动语态,这是掌握语言节奏好的方法。过多的副词会使文章显得啰嗦,而被动语态则会让表达显得不够自信。


我写文章老板常说像个小学生写的。有段时间写着写着就不想写了,后来一琢磨吧。嗨,有啥不敢写的呀,遇到问题了就琢磨琢磨。我的问题在于,输入的太少、概念太少、词汇太少,嗯嗯啊啊咿咿呀呀,来来回回就是那几句,而且就那几句还写的非常稚嫩、生涩。怎么办呢?那就继续写吧,不写怎么积累,怎么改进呀,哈哈。


4. 好作品库


史蒂芬·平克在《风格感觉》中说过,成为一个好作者的起点是成为一个好读者。好作者都酷爱阅读,就像Savage一样,他们家没有电视,而满眼都是书,客厅有一排长长的书架、卧室也有一排长长的书架,书很多大概有几千本。王小波也是喜欢读书,文学、哲学、历史、科学什么都有。一个好的读者能够阅读中能够发现、欣赏,并逆向解构好作品,从而获得自己写作所需的技巧。通过阅读好作品,他们逐渐掌握了丰富的概念、词汇和语法,培养出自己独特的风格感觉。分辨和欣赏优秀作品有时比单纯遵循写作技巧更加有效,能从根本上提升写作能力。


什么样的作品是好作品?大师经典。举个例子,如果你想了解关于霸道总裁小说的套路和框架,直接阅读《呼啸山庄》就可以,因为它是该类型小说的原型和经典之作。通过阅读这本书,你可以获取第一手的信息和资料,不需要去翻阅其他杂乱的书籍。这种方式可以让你更加深入地理解和把握这一类型的小说。


有时候,一说到阅读经典,脑子里反射性的就是晦涩、难懂、冗长......我小时候也被“经典”二字吓到了,觉得玩意儿是我能读的,不想读!后来,我的写作水平一直停留在,嗯,小学。翻开几页,多翻几页,会发现阅读⼤师经典能让人更清楚地认识到什么是好,感受到什么是好。以后遇到好的作品,尝试流连不去,沉思它好在哪⾥吧。


如何阅读好作品?对于我们这些基础知识相对欠缺的人来说(没有读过1000本书很难有太多基础知识,参考我老板,读完1000本之后,无论是阅读速度还是阅读质量远远超过我们),如果自己不懂如何阅读经典,最好的方式就是找个老师带着读。你不懂如何提问,那就看好老师如何提问;你不懂如何欣赏,那就看好老师如何解读好作品;你不懂如何提炼技巧,那就看好老师如何拆解作品。跟随老师的步伐,模仿老师,学习老师的方法,你会不断进步,完善自己的技艺是终⽣的事情,将错误看作是进步过程中的一部分,把它们当作游戏的一部分。




小狐狸专区





“本质的东西眼睛是看不见的”,需要用心去感受和理解。小王子喜欢玫瑰,我爱狐狸。


你相信永恒吗?我相信它是存在的,它需要两件法宝才能得以存在:信任和时间。不是一时的,也不是长期如此,而是终生如是。

  • 标题:《小狐狸在讲话》
  • 作者:陈小羊
  • AI copilot:Midjourney
  • Prompt:A cute white fox giving a speech, sunrise, wide view, full body, side view, wearing a blue dressin,the style of riso printing --style raw --ar 137:58
  • 话说:有人说Midjourney生成的图没有灵魂,我咂摸了一下,Prompt是为图注入灵魂的关键呐。


作者:happy_logos
来源:mdnice.com/writing/31230914cb804a91897c3ce3b8bf98cb
收起阅读 »

Flutter-数字切换动画

效果 需求 数字切换时新数字从上往下进入,上个数字从上往下出 新数字进入时下落到位置并带有回弹效果 上个数字及新输入切换时带有透明度和缩放动画 实现 主要采用Animat...
继续阅读 »

效果





需求





  • 数字切换时新数字从上往下进入,上个数字从上往下出



  • 新数字进入时下落到位置并带有回弹效果



  • 上个数字及新输入切换时带有透明度和缩放动画


实现


主要采用AnimatedSwitcher实现需求,代码比较简单,直接撸


import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter_xy/widgets/xy_app_bar.dart';

class NumAnimPage extends StatefulWidget {
  const NumAnimPage({super.key});

  @override
  State<NumAnimPage> createState() => _NumAnimPageState();
}

class _NumAnimPageState extends State<NumAnimPage> {
  int _currentNum = 0;

  // 数字文本随机颜色
  Color get _numColor {
    Random random = Random();
    int red = random.nextInt(256);
    int green = random.nextInt(256);
    int blue = random.nextInt(256);
    return Color.fromARGB(255, red, green, blue);
  }

  // 数字累加
  void _addNumber() {
    setState(() {
      _currentNum++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: XYAppBar(
        title: "数字动画",
      ),
      body: Center(
        child: _bodyWidget(),
      ),
    );
  }

  Widget _bodyWidget() {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        AnimatedSwitcher(
          duration: const Duration(milliseconds: 500),
          transitionBuilder: (Widget child, Animation<double> animation) {
            Offset startOffset = animation.status == AnimationStatus.completed
                ? const Offset(0.0, 1.0)
                : const Offset(0.0, -1.0);
            Offset endOffset = const Offset(0.0, 0.0);
            return SlideTransition(
              position: Tween(begin: startOffset, end: endOffset).animate(
                CurvedAnimation(parent: animation, curve: Curves.bounceOut),
              ),
              child: FadeTransition(
                opacity: Tween(begin: 0.0, end: 1.0).animate(
                  CurvedAnimation(parent: animation, curve: Curves.linear),
                ),
                child: ScaleTransition(
                  scale: Tween(begin: 0.5, end: 1.0).animate(
                    CurvedAnimation(parent: animation, curve: Curves.linear),
                  ),
                  child: child,
                ),
              ),
            );
          },
          child: Text(
            '$_currentNum',
            key: ValueKey<int>(_currentNum),
            style: TextStyle(fontSize: 100, color: _numColor),
          ),
        ),
        const SizedBox(height: 80),
        ElevatedButton(
          onPressed: _addNumber,
          child: const Text(
            '数字动画',
            style: TextStyle(fontSize: 25, color: Colors.white),
          ),
        ),
      ],
    );
  }
}


具体见github:https://github.com/yixiaolunhui/flutter_xy


作者:移动小样
来源:mdnice.com/writing/9645b22a9a54493f9f2e3f74e60d17c7
收起阅读 »

第三方认证中心跳转

一、业务需求 由第三方认证中心将 token 放在 header 中跳转系统,前端获取到第三方系统携带 header 中的 token。 二、 业务流程 模拟第三方应用 CUSTOM-USERTOKEN 是第三方的 tok...
继续阅读 »

一、业务需求


由第三方认证中心将 token 放在 header 中跳转系统,前端获取到第三方系统携带 header 中的 token。


二、 业务流程





模拟第三方应用





  • CUSTOM-USERTOKEN 是第三方的 token



  • proxy_pass 是我们的前端地址


  server {
listen 12345;
server_name localhost;

location / {
proxy_set_header Host $host:$server_port;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-Port $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header CUSTOM-USERTOKEN 'MY-TOKEN'
proxy_pass http://127.0.0.1;
}
}

前端静态代理





  • backend 是后端服务地址



  • 80 是前端代理端口


  server {
listen 80;
server_name localhost;

location / {
root /vuepress/docs;
index index.html;
try_files $uri $uri/ /index.html;
}
error_page 405 =200 $uri;
}

三、处理方式


由于放在 header 中的内容,前端只有从 XHR 请求中才能拿到,所以直接打开页面时,肯定是无法拿到 header 中的 token 的,又因为这个 token 只有从第三方系统中跳转才能携带,所以也无法通过请求当前页面去获取 header 中的内容。


一、通过后端重定向


在 nginx 代理中,第三方请求从原本跳转访问前端的地址==改为==后端地址, 因为后端是可以从请求总直接拿到 header,所以这时由后端去处理 token ,在重定向到前端。





  • 后端可以设置 cookie,前端从 cookie 中获取



  • 后端可以拼接 URL, 前端从 url 中获取



  • 后端可以通过缓存 cookie, 重定向到前端后发请求获取 token


模拟第三方应用





  • 第三方应用由跳转前端改为跳转后端接口


  server {
listen 12345;
server_name localhost;

location / {
proxy_set_header Host $host:$server_port;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-Port $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header CUSTOM-USERTOKEN 'MY-TOKEN'
proxy_pass http://backend/token;
}
}

前端静态代理





  • 前端代理不需要做任何处理


  server {
listen 80;
server_name localhost;

location / {
root /vuepress/docs;
index index.html;
try_files $uri $uri/ /index.html;
}
error_page 405 =200 $uri;
}

二、通过 nginx 重定向 URL


在 nginx 代理中,新增一个 /token 的代理地址,用于转发地址,第三方请求从原本跳转访问前端的地址,改为 /token 代理地址 因为 nginx 中是可以获取 header 中的内容的,所以这时由 /token 处理拼接好 url ,在重定向到前端。





模拟第三方应用



  server {
listen 12345;
server_name localhost;

location / {
proxy_set_header Host $host:$server_port;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-Port $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header CUSTOM-USERTOKEN 'MY-TOKEN'
proxy_pass http://127.0.0.1/token;
}
}

前端静态代理





  • 新增 /token 代理,进行拼接 URL 后跳转


  server {
listen 80;
server_name localhost;

location / {
root /vuepress/docs;
index index.html;
try_files $uri $uri/ /index.html;
}
location /token {
# 将 $http_custom_usertoken 拼接在 URL 中,同时重定向到前端
# 前端通过 location.search 处理 token
rewrite (.+) http://127.0.0.1?token=$http_custom_usertoken;
}
error_page 405 =200 $uri;
}

三、通过 nginx 设置 Cookie


由于通过响应头中设置 Set-Cookie 可以直接存储到浏览器中,所以我们也可以通过直接设置 cookie 的方式处理。





模拟第三方应用





  • 此时第三方应用直接访问前端即可


  server {
listen 12345;
server_name localhost;

location / {
proxy_set_header Host $host:$server_port;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-Port $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header CUSTOM-USERTOKEN 'MY-TOKEN'
proxy_pass http://127.0.0.1;
}
}

前端静态代理





  • token 设置在 cookie


  server {
listen 80;
server_name localhost;

location / {
add_header Set-Cookie "token=$http_custom_usertoken;HttpOnly;Secure";
root /vuepress/docs;
index index.html;
try_files $uri $uri/ /index.html;
}
error_page 405 =200 $uri;
}

四、nginx 代理转发设置 Cookie


方法 三、通过 nginx 设置 Cookie 中,存在一个问题,由于此时在前端静态代理上添加 cookie,这就会导致所有静态资源都会携带 cookie, 这就会造成 cookie 中因为 path 不同而重复添加, 所以我们还可以通过造一层代理的方式处理这个问题





模拟第三方应用





  • 代理地址再次修改为 token


  server {
listen 12345;
server_name localhost;

location / {
proxy_set_header Host $host:$server_port;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-Port $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header CUSTOM-USERTOKEN 'MY-TOKEN'
proxy_pass http://127.0.0.1/token;
}
}

前端静态代理





  • token 设置在 /token 代理地址的 cookie



  • /token 重定向到前端地址


  server {
listen 80;
server_name localhost;

location / {
root /vuepress/docs;
index index.html;
try_files $uri $uri/ /index.html;
}

location /token {
add_header Set-Cookie "token=$http_custom_usertoken;HttpOnly;Secure";
rewrite (.+) http://127.0.0.1;
}
error_page 405 =200 $uri;
}

作者:子洋
来源:mdnice.com/writing/d92f346cc96a43b49fc36c9894add729
收起阅读 »

用Vue.js构建一个Web3应用像,像开发 Web2 一样熟悉

web
作为一名涉足去中心化网络的前端 JavaScript 开发人员,您可能遇到过许多 Web3 开发解决方案。但是,这些解决方案通常侧重于钱包集成和交易执行,这就造成了学习曲线,偏离了熟悉的 Web2 开发体验。 但不用担心!有一种解决方案可以无缝衔接 Web2...
继续阅读 »

作为一名涉足去中心化网络的前端 JavaScript 开发人员,您可能遇到过许多 Web3 开发解决方案。但是,这些解决方案通常侧重于钱包集成和交易执行,这就造成了学习曲线,偏离了熟悉的 Web2 开发体验。


但不用担心!有一种解决方案可以无缝衔接 Web2 和 Web3,它就是 Juno



网址:https://juno.build/



在本篇博文中,我们将探讨如何利用 Vue 和 Juno 的强大功能来开发去中心化应用程序(dApps)。加入我们的旅程,揭开 Juno 的神秘面纱,让您轻松创建非凡的去中心化体验!





导言


在我之前的博文中,我讨论了 React[1]Angular[2] 这两个流行的 JavaScript 前端框架的类似解决方案。如果这两个框架中的任何一个是您的首选,我建议您浏览这些具体的文章,以获得量身定制的见解。


Juno如何工作


如果你还不了解 Juno,它是一个功能强大的开源区块链即服务平台,旨在让去中心化应用程序开发变得更加容易。可以把它想象成一个无服务器平台,类似于谷歌Firebase或AWS Amplify等流行服务,但增加了区块链技术的优势。Juno 完全在区块链上运行您的应用程序,确保完全去中心化和安全的基础设施。


通过利用Internet Computer[3]区块链网络和基础设施,Juno 为您创建的每个应用程序引入了一个名为 “Satellites” 的独特概念。这些 Satellites 作为强大的智能合约,封装了您的整个应用程序,包括 JavaScript、HTML 和图像文件等网络资产,以及简单的数据库、文件存储和身份验证机制。通过 Juno,您可以完全控制应用程序的功能和数据。


构建一个 Dapp


让我们开始构建我们的第一个去中心化应用程序,简称“dapp”。在这个例子中,我们将创建一个笔记应用程序,允许用户存储和检索数据条目,以及上传文件。


本教程和代码示例使用了 Vue Composition API。


初始化


在将 Juno 集成到应用程序之前,需要创建一个 satellite。该过程在文档[4]中有详细的解释。


此外,还需要安装SDK。


npm i @junobuild/core

完成这两个步骤后,您可以在 Vue 应用程序的根目录(例如 App.vue)中使用 satellite ID 初始化 Juno。这将配置库与您的智能合约进行通信。


<script setup lang="ts">
import { onMounted } from 'vue'
import { initJuno } from '@junobuild/core'

onMounted(
  async () =>
    await initJuno({
      satelliteId'pycrs-xiaaa-aaaal-ab6la-cai'
    })
)
</script>

<template>
<h1>Hello World</h1>
</template>

配置完成!现在,您的应用程序已经可以用于 Web3 了!😎


身份验证


为了确保用户身份的安全性和匿名性,需要对用户进行登录和注销。要做到这一点,可以将相关函数绑定到应用程序中任何位置的 call-to-action 按钮。


<script setup lang="ts">
import { signIn, signOut} from '@junobuild/core'
</script>

<button @click="signIn">Sign-in</button>
<button @click="signOut">Sign-out</button>

为了与其他服务建立无缝集成,库和 satellite 组件在用户成功登录后自动在您的智能合约中生成新条目。此功能使库能够在任何数据交换期间有效地验证权限。


为了监视并深入了解该条目,从而访问有关用户状态的信息,Juno提供了一个名为authSubscribe() 的可观察函数。您可以根据需要灵活地多次使用此函数。然而,你也可以创建一个在整个应用中有效传播用户信息的 store。


import { ref, type Ref } from 'vue'
import { defineStore } from 'pinia'
import { authSubscribe, type User } from '@junobuild/core'

export const useAuthStore = defineStore('auth', () => {
  const user: Ref<User | null | undefined> = ref(undefined)

  const unsubscribe = authSubscribe((u) => (user.value = u))

  return { user, unsubscribe }
})

这样,在应用程序的顶层订阅它就变得非常方便。


<script setup lang="ts">
import { useAuthStore } from '../stores/auth.store'
import { storeToRefs } from 'pinia'

const store = useAuthStore()
const { user } = storeToRefs(store)
</script>

<template>
  <template v-if="user !== undefined && user !== null">
    <slot /
>
  </template>

  <template v-else>
    <p>Not signed in.</
p>
  </template>
</
template>

存储文档


Juno提供了一个名为“Datastore”的功能,旨在将数据直接存储在区块链上。Datastore 由一组集合组成,其中每个集合保存文档,这些文档由您选择的键唯一标识。


在本教程中,我们的目标是存储笔记,因此必须按照文档中提供的说明创建一个集合。为集合选择合适的名称,例如“notes”。


一旦设置好应用程序并创建了必要的集合,就可以利用库的 setDoc 函数将数据持久化到区块链上。此功能使您能够安全且不变地存储笔记。


import { setDoc } from "@junobuild/core";

// TypeScript example from the documentation
await setDoc<Example>({
  collection"my_collection_key",
  doc: {
    key"my_document_key",
    data: myExample,
  },
});

由于集合中的文档是通过唯一的密钥来标识的,因此我们使用 nanoid[5] 来创建密钥--这是一种用于 JavaScript 的微型字符串 ID 生成器。


<script lang="ts" setup>
import { ref } from 'vue'
import { setDoc } from '@junobuild/core'
import { nanoid } from 'nanoid'

const inputText = ref('')

const add = async () => {
  const key = nanoid()

  await setDoc({
    collection'notes',
    doc: {
      key,
      data: {
        text: inputText.value
      }
    }
  })
}
</script>

<template>
  <textarea rows="5" placeholder="Your diary entry" 
            v-model="inputText">
</textarea>

  <button @click="add">Add</button>
</template>


请注意,为简单起见,本教程提供的代码片段不包括适当的错误处理,也不包括复杂的表单处理。



检索文档列表


为了检索存储在区块链上的文档集合,我们可以使用库的 listDocs 函数。这个多功能函数允许加入各种参数,以方便数据过滤、排序或分页。


出于本教程的目的,我们将保持示例的简约性。我们的目标是在挂载组件时简单地列出所有用户数据。


<script lang="ts" setup>
import { listDocs } from '@junobuild/core'
import { onMounted, ref } from 'vue'

const items = ref([])

const list = async () => {
  const { items: data } = await listDocs({
    collection'notes'
  })

  items.value = data
}

onMounted(async () => await list())
</script>

<template>
  <p v-for="(item, index) in items">
    <span>
      {{ index + 1 }}
    </span>
    <span>{{ item.data.text }}</span>
  </p>
</template>

文件上传


在去中心化网络上存储数据是一项复杂的任务。然而,Juno 的设计旨在为需要轻松存储和检索用户生成内容(如照片或文件)的应用程序开发人员简化这一过程。


在处理文档时,第一步是按照文档[6]中提供的说明创建一个集合。在本教程中,我们将重点实施图片上传,因此该集合可以恰当地命名为 “images”。


为确保存储数据的唯一性和正确识别,每个文件都有唯一的文件名和路径。这一点非常重要,因为数据是在网络上提供的,每条数据都应该有一个独特的 URL。


要实现这一点,我们可以使用用户唯一ID的文本表示形式和每个上传文件的时间戳的组合来创建一个键。通过访问我们之前在存储中声明的属性,我们可以检索相应的用户键。


<script lang="ts" setup>
import { ref } from 'vue'
import { useAuthStore } from '@/stores/auth.store'
import { storeToRefs } from 'pinia'
import { uploadFile } from '@junobuild/core'

const file = ref(undefined)

const store = useAuthStore()
const { user } = storeToRefs(store)

const setFile = (f) => (file.value = f)

const upload = async () => {
  // Demo purpose therefore edge case not properly handled
  if ([nullundefined].includes(user.value)) {
    return
  }

  const filename = `${user.value.key}-${file.value.name}`

  const { downloadUrl } = await uploadFile({
    collection'images',
    data: file.value,
    filename
  })

  console.log('Uploaded', downloadUrl)
}
</script>

<template>
  <input type="file" @change="(event) => setFile(event.target.files?.[0])" />

  <button @click="upload">Upload</button>
</template>

一旦一个资源被上传,一个 downloadUrl 返回,它提供了一个直接的 HTTPS 链接,可以在web上访问上传的资源。


列出资源


为了检索存储在区块链上的资产集合,我们可以利用库提供的 listAssets 函数。这个函数在参数方面提供了灵活性,允许我们根据需要对文件进行过滤、排序或分页。


与前面的文档示例类似,我们将保持这个示例的简约性。


<script lang="ts" setup>
import { listAssets } from '@junobuild/core'
import { onMounted, ref } from 'vue'

const assets = ref([])

const list = async () => {
  const { assets: images } = await listAssets({
    collection'images'
  })

  assets.value = images
}

onMounted(async () => await list())
</script>

<template>
  <img loading="lazy" :src="asset.downloadUrl" v-for="asset in assets" />
</template>

部署 🚀


在开发和构建应用程序之后,下一步是将其部署到区块链上。为此,您需要在终端中执行以下命令来安装 Juno 命令行接口(CLI):


npm i -g @junobuild/cli

安装过程完成后,您可以按照文档[7]中的说明并从终端登录来访问您的 satellite。这将使你的机器能够控制你的 satellite。


juno login

最后,您可以使用以下命令部署项目:


juno deploy

恭喜你!您的 Vue dapp 现在已经上线,并完全由区块链提供支持。🎉


资源





原文:https://betterprogramming.pub/build-a-web3-app-with-vuejs-db1503ca20d2


是哒是哒说


参考资料


[1]

React: https://betterprogramming.pub/build-a-web3-app-with-react-js-6353825baf9a

[2]

Angular: https://levelup.gitconnected.com/develop-an-angular-app-on-blockchain-9cde44ae00b7

[3]

Internet Computer: https://internetcomputer.org/

[4]

文档: https://juno.build/docs/add-juno-to-an-app/create-a-satellite

[5]

nanoid: https://github.com/ai/nanoid

[6]

文档: https://juno.build/docs/build/storage#collections-and-rules

[7]

文档: https://juno.build/docs/miscellaneous/cli#login

[8]

https://juno.build/docs/intro: https://juno.build/docs/intro

[9]

GitHub 代码库: https://github.com/buildwithjuno/examples/tree/main/vue/diary



作者:程序员张张
来源:mdnice.com/writing/26615feb73924bb4821f543e0f041fa4
收起阅读 »

前端开发如何给自己定位?初级?中级?高级!

web
引言 在快速发展的互联网时代,前端开发一直处于高速增长的趋势中。作为构建用户界面和实现交互功能的关键角色,前端开发人员需要不断提升自己的技能和能力,以适应变化的行业需求。本文将为前端开发人员提供一个能力定位指南,帮助他们了解自己在前端领域的定位,内容参考阿里前...
继续阅读 »

引言


在快速发展的互联网时代,前端开发一直处于高速增长的趋势中。作为构建用户界面和实现交互功能的关键角色,前端开发人员需要不断提升自己的技能和能力,以适应变化的行业需求。本文将为前端开发人员提供一个能力定位指南,帮助他们了解自己在前端领域的定位,内容参考阿里前端面试指南,P6/P6+/P7的能力标准。


目录



0.掌握图形学,webgl或熟练使用threejs框架,熟练canvas相关渲染及动画操作的优先。

1.熟练掌握JavaScript。

2.熟悉常用工程化工具,掌握模块化思想和技术实现方案。

3.熟练掌握React前端框架,了解技术底层。同时了解vue以及angular等其他框架者优先。

4.熟练掌握react生态常用工具,redux/react-router等。

5.熟悉各种Web前端技术,包括HTML/XML/CSS等,有基于Ajax的前端应用开发经验。

6.有良好的编码习惯,对前端技术有持续的热情,个性乐观开朗,逻辑性强,善于和各种背景的人合作。

7.具有TS/移动设备上前端开发/NodeJS/服务端开发等经验者优先。



0.掌握图形学,webgl或熟练使用threejs框架,熟练canvas相关渲染及动画操作的优先。


初级:



  • 学习过图形学相关知识,知道矩阵等数学原理在动画中的作用,知道三维场景需要的最基础的构成,能用threejs搭3d场景,知道webgl和threejs的关系。

  • 知道canvas是干嘛的,聊到旋转能说出canvas的api。

  • 知道css动画,css动画属性知道关键字和用法(换句话说,电话面试会当场出题要求口喷css动画,至少能说对大概,而不是回答百度一下就会用)。

  • 知道js动画,能说出1~2个社区js动画库,知道js动画和css动画优缺点以及适用场景。

  • 知道raf和其他达到60fps的方法。


中级:



  • 如果没有threejs,你也能基于webgl自己封装一个简单的threejs出来。

  • 聊到原理能说出四元数,聊到鼠标操作能提到节流,聊到性能能提到restore,聊到帧说出raf和timeout的区别,以及各自在优化时候的作用。

  • 知道怎样在移动端处理加载问题,渲染性能问题。

  • 知道如何结合native能力优化性能。

  • 知道如何排查性能问题。对chrome动画、3d、传感器调试十分了解。


高级:



  • 搭建过整套资源加载优化方案,能说明白整体方案的各个细节,包括前端、客户端、服务端分别需要实现哪些功能点、依赖哪些基础能力,以及如何配合。

  • 设计并实现过前端动画引擎,能说明白一个复杂互动项目的技术架构,知道需要哪些核心模块,以及这些模块间如何配合。

  • 有自己实现的动画相关技术方案产出,这套技术方案必须是解决明确的业务或技术难点问题的。为了业务快速落地而封装一个库,不算这里的技术方案。如果有类似社区方案,必须能从原理上说明白和竞品的差异,各自优劣,以及技术选型的原因。


1.熟练掌握JavaScript。


初级:



  • JavaScript各种概念都得了解,《JavaScript语言精粹》这本书的目录都得有概念,并且这些核心点都能脱口而出是什么。这里列举一些做参考:

  • 知道组合寄生继承,知道class继承。

  • 知道怎么创建类function + class。

  • 知道闭包在实际场景中怎么用,常见的坑。

  • 知道模块是什么,怎么用。

  • 知道event loop是什么,能举例说明event loop怎么影响平时的编码。

  • 掌握基础数据结构,比如堆、栈、树,并了解这些数据结构计算机基础中的作用。

  • 知道ES6数组相关方法,比如forEach,map,reduce。


中级:



  • 知道class继承与组合寄生继承的差别,并能举例说明。

  • 知道event loop原理,知道宏微任务,并且能从个人理解层面说出为什么要区分。知道node和浏览器在实现loop时候的差别。

  • 能将继承、作用域、闭包、模块这些概念融汇贯通,并且结合实际例子说明这几个概念怎样结合在一起。

  • 能脱口而出2种以上设计模式的核心思想,并结合js语言特性举例或口喷基础实现。

  • 掌握一些基础算法核心思想或简单算法问题,比如排序,大数相加。


2.熟悉常用工程化工具,掌握模块化思想和技术实现方案。


初级:



  • 知道webpack,rollup以及他们适用的场景。

  • 知道webpack v4和v3的区别。

  • 脱口而出webpack基础配置。

  • 知道webpack打包结果的代码结构和执行流程,知道index.js,runtime.js是干嘛的。

  • 知道amd,cmd,commonjs,es module分别是什么。

  • 知道所有模块化标准定义一个模块怎么写。给出2个文件,能口喷一段代码完成模块打包和执行的核心逻辑。


中级:



  • 知道webpack打包链路,知道plugin生命周期,知道怎么写一个plugin和loader。

  • 知道常见loader做了什么事情,能几句话说明白,比如babel-loader,vue-loader。

  • 能结合性能优化聊webpack配置怎么做,能清楚说明白核心要点有哪些,并说明解决什么问题,需要哪些外部依赖,比如cdn,接入层等。

  • 了解异步模块加载的实现原理,能口喷代码实现核心逻辑。


高级:



  • 能设计出或具体说明白团队研发基础设施。具体包括但不限于:

  • 项目脚手架搭建,及如何以工具形态共享。

  • 团队eslint规范如何设计,及如何统一更新。

  • 工具化打包发布流程,包括本地调试、云构建、线上发布体系、一键部署能力。同时,方案不仅限于前端工程部分,包含相关服务端基础设施,比如cdn服务搭建,接入层缓存方案设计,域名管控等。

  • 客户端缓存及预加载方案。


3.熟练掌握React前端框架,了解技术底层。同时了解vue以及angular等其他框架者优先。


初级:



  • 知道react常见优化方案,脱口而出常用生命周期,知道他们是干什么的。

  • 知道react大致实现思路,能对比react和js控制原生dom的差异,能口喷一个简化版的react。

  • 知道diff算法大致实现思路。

  • 对state和props有自己的使用心得,结合受控组件、hoc等特性描述,需要说明各种方案的适用场景。

  • 以上几点react替换为vue或angular同样适用。


中级:



  • 能说明白为什么要实现fiber,以及可能带来的坑。

  • 能说明白为什么要实现hook。

  • 能说明白为什么要用immutable,以及用或者不用的考虑。

  • 知道react不常用的特性,比如context,portal。

  • 能用自己的理解说明白react like框架的本质,能说明白如何让这些框架共存。


高级:



  • 能设计出框架无关的技术架构。包括但不限于:

  • 说明如何解决可能存在的冲突问题,需要结合实际案例。

  • 能说明架构分层逻辑、各层的核心模块,以及核心模块要解决的问题。能结合实际场景例举一些坑或者优雅的处理方案则更佳。


4.熟练掌握react生态常用工具,redux/react-router等。


初级:



  • 知道react-router,redux,redux-thunk,react-redux,immutable,antd或同级别社区组件库。

  • 知道vue和angular对应全家桶分别有哪些。

  • 知道浏览器react相关插件有什么,怎么用。

  • 知道react-router v3/v4的差异。

  • 知道antd组件化设计思路。

  • 知道thunk干嘛用的,怎么实现的。


中级:



  • 看过全家桶源码,不要求每行都看,但是知道核心实现原理和底层依赖。能口喷几行关键代码把对应类库实现即达标。

  • 能从数据驱动角度透彻的说明白redux,能够口喷原生js和redux结合要怎么做。

  • 能结合redux,vuex,mobx等数据流谈谈自己对vue和react的异同。


高级:



  • 有基于全家桶构建复杂应用的经验,比如最近很火的微前端和这些类库结合的时候要注意什么,会有什么坑,怎么解决


5.熟悉各种Web前端技术,包括HTML/XML/CSS等,有基于Ajax的前端应用开发经验。


初级:



  • HTML方面包括但不限于:语义化标签,history api,storage,ajax2.0等。

  • CSS方面包括但不限于:文档流,重绘重排,flex,BFC,IFC,before/after,动画,keyframe,画三角,优先级矩阵等。

  • 知道axios或同级别网络请求库,知道axios的核心功能。

  • 能口喷xhr用法,知道网络请求相关技术和技术底层,包括但不限于:content-type,不同type的作用;restful设计理念;cors处理方案,以及浏览器和服务端执行流程;口喷文件上传实现;

  • 知道如何完成登陆模块,包括但不限于:登陆表单如何实现;cookie登录态维护方案;token base登录态方案;session概念;


中级:



  • HTML方面能够结合各个浏览器api描述常用类库的实现。

  • css方面能够结合各个概念,说明白网上那些hack方案或优化方案的原理。

  • 能说明白接口请求的前后端整体架构和流程,包括:业务代码,浏览器原理,http协议,服务端接入层,rpc服务调用,负载均衡。

  • 知道websocket用法,包括但不限于:鉴权,房间分配,心跳机制,重连方案等。

  • 知道pc端与移动端登录态维护方案,知道token base登录态实现细节,知道服务端session控制实现,关键字:refresh token。

  • 知道oauth2.0轻量与完整实现原理。

  • 知道移动端api请求与socket如何通过native发送,知道如何与native进行数据交互,知道ios与安卓jsbridge实现原理。


高级:



  • 知道移动端webview和基础能力,包括但不限于:iOS端uiwebview与wkwebview差异;webview资源加载优化方案;webview池管理方案;native路由等。

  • 登陆抽象层,能够给出完整的前后端对用户体系的整体技术架构设计,满足多业务形态用户体系统一。考虑跨域名、多组织架构、跨端、用户态开放等场景。

  • mock方案,能够设计出满足各种场景需要的mock数据方案,同时能说出对前后端分离的理解。考虑mock方案的通用性、场景覆盖度,以及代码或工程侵入程度。

  • 埋点方案,能够说明白前端埋点方案技术底层实现,以及技术选型原理。能够设计出基于埋点的数据采集和分析方案,关键字包括:分桶策略,采样率,时序性,数据仓库,数据清洗等。


6.有良好的编码习惯,对前端技术有持续的热情,个性乐观开朗,逻辑性强,善于和各种背景的人合作。


初级:



  • 知道eslint,以及如何与工程配合使用。

  • 了解近3年前端较重要的更新事件。

  • 面试过程中遇到答不出来的问题,能从逻辑分析上给出大致的思考路径。

  • 知道几个热门的国内外前端技术网站,同时能例举几个面试过程中的核心点是从哪里看到的。


高级:



  • 在团队内推行eslint,并给出工程化解决方案。

  • 面试过程思路清晰,面试官给出关键字,能够快速反应出相关的技术要点,但是也要避免滔滔不绝,说一堆无关紧要的东西。举例来说,当时勾股老师面试我的时候,问了我一个左图右文的布局做法,我的回答是:我自己总结过7种方案,其中比较好用的是基于BFC的,float的以及flex的三种。之后把关键css口喷了一下,然后css就面完了。


7.具有TS/移动设备上前端开发/NodeJS/服务端开发等经验者优先。



  • 根据了解的深度分初/中/高级。

  • 知道TS是什么,为什么要用TS,有TS工程化实践经验。

  • 知道移动端前端常见问题,包括但不限于:rem + 1px方案;预加载;jsbridge原理等。

  • 能说出大概的服务端技术,包括但不限于:docker;k8s;rpc原理;中后台架构分层;缓存处理;分布式;响应式编程等。


5. 结论与进一步学习


本文为前端开发人员提供了一个能力定位指南,帮助他们了解自己在前端领域的定位,并提供了具体的代码示例来巩固学习成果。通过不断学习和实践,前端开发人员可以逐步提升自己的能力,从初级到中级再到高级。但请注意,在实际工作中,不同公司和项目对于各个级别的要求可能会有所不同。


为了进一步提高自己的水平,前端开发人员可以考虑以下学习路径和资源:



  • 阅读官方文档和教程,如MDN、React官方文档等;

  • 参与开源项目,并与其他开发人员进行交流和合作;

  • 关注前端开发的博客和社区,如Medium、Stack Overflow等;

  • 参加在线或线下的前端开发培训课程;

  • 阅读经典的前端开发书籍,如《JavaScript高级程序设计》、《CSS权威指南》等。


通过持续学习和实践,相信每个前端开发人员都可以不断成长,并在前端领域中取得更好的成就。祝愿大家在前端开

作者:Jony_men
来源:juejin.cn/post/7259961208794628151
发的道路上越走越远!

收起阅读 »

该写好代码吗?我也迷茫了

我在抖音上看到当当网创始人李国庆发了一条视频,感觉他说的挺有意思。 他说,作为企业中层管理者,该不该有自己的山头,用于自我保护,这让人很迷茫。 其实我也迷茫过。我猜测,我们迷茫的可能是同一件事。 程序员内部,曾经流传着这样几句圣经: 代码写的好,写得快,会像...
继续阅读 »

2023-04-21_172756.png


我在抖音上看到当当网创始人李国庆发了一条视频,感觉他说的挺有意思。


他说,作为企业中层管理者,该不该有自己的山头,用于自我保护,这让人很迷茫。


其实我也迷茫过。我猜测,我们迷茫的可能是同一件事。


程序员内部,曾经流传着这样几句圣经:



代码写的好,写得快,会像个闲人。代码有注释,逻辑清晰,任何人都能轻松取代你。


代码写的烂,只有自己能看懂,一次次救火,你反而会成为团队不可缺少的人才。



那么,问题来了:到底该把事情干好呢,还是不要干好呢?


这是一个问题吗?当然是往多快好省了做呀!


我以前的想法就是这样。


我做底层员工时,代码写的清晰简洁,高效严谨。有时候我会因为计算循环次数而费心设计。如果循环层数太多,我会先把关键数据放到Map里,后续可以直接取用。我也会关注代码的可读性,尽量少套几层循环,命名兼顾字符长度和表意指向,如果代码太多就抽离成一个方法函数,并且要在代码里注释清楚。而这些操作,在形成习惯之后,是不会影响开发效率的。反而在某些情况下,还会提高效率。因为不管逻辑多复杂,不管过去多久,一看就能懂,很容易排查问题和他人接手。


我做中层管理时,除了培养团队内每个员工都能做到上述标准外,让我投入很大精力的事情就是“去我化”。也就是通过手段、流程、文化做到团队自治。我在团队时,大家能很高效地完成工作。当我短时间内离开时,大家依然能依靠惯性维持高效的状态。


我的想法很单纯:不管我是一线开发,还是中层管理,我修炼的都是自己。当你具备一定的职场能力时,你就是值钱的。作为员工你能把手头的活干得又快又好,作为管理你能把团队管理得积极健康。这就是亮点。不要在意别人的看法,你只需要修炼自己。当你具备了这个能力,这里不适合你,好多地方都会求此类贤人若渴。


其实,后面慢慢发现,这种想法可能还值得商榷。


因为创业和打工的区别还是挺大的。


创业是给自己干,想干好是肯定的,谁都不愿意面对一团糟。


我看明朝的历史,建文帝朱允炆刚登基时,就想削弱其他藩王的势力,加强自己的权力。当建文帝打算办燕王朱棣时,朱棣就起兵造反,自己做了皇帝。我感觉朱棣其实是自卫。后来,感情朱棣当上皇帝的第一件事,也是继续削弱藩王的势力。其实,大家都一样。


打工就不一样了,干得好不好,不是你说了算,是你的上级领导说了算,周围同事说了算,规章制度说了算。


因此,扁鹊三兄弟的现象就出现了。


扁鹊大哥,医术最高,能预防病人生病。扁鹊二哥,医术很高,能消灭病症在萌芽阶段。到扁鹊这里,只能到人快死了,开刀扎针,救人于生死之间。但是,世人都称扁鹊为神医。


如果你的领导是一个技术型的,同时他还能对你的工作质量做一些审查,那么他对你的评价,可能还具有些客观性。


但是,如果你碰到的是一个行政型的领导,他不是很懂医术,那他就只能像看医生治病一样,觉得救治好了快要死的人,才是高人。而对于预防重症这类防患于未然的事情,他会觉得你在愚弄他。


事实上,不少中小企业的领导,多是行政型领导。他们常常以忠心于老板而被提拔。


因此,为了自己获得一些利益。有些人常常是先把大病整出来,然后再治好,以此来体现自己的价值。


老维修工建设管道,水管里流的是燃气,燃气管的作用是排废水。出了问题,来一批一批的新师傅,都解决不了,越弄越乱。结果老维修工一出手,就把问题解决了。老板觉得,哎呀,看,还得是我的老师傅管用。


相反,如果你把事情打理的井井有条,没有一丝风浪,就像扁鹊大哥一样,我都不得病,还养你干啥,随便换谁都可以。这样的话,往往你的结局就多是被领导忽视。


我有好几个大领导都在大会上说过:你请假一周,你的部门连给你打一个电话的都没有,这说明你平时疏于管理,对于团队没有一丝作用!


从业这么多年,见过各种现实,很讽刺,就像是笑话。有一个同事,给程序加了一个30秒后延时执行。后来,领导让他优化速度,他分4次,将30秒调到5秒。最后领导大喜,速度提高6倍,他被授予“超级工匠”的荣誉称号。


一个是领导的评价。还有一个是同事的评价。


我有一次,在自己的项目组里搞了个考核。考核的核心就是,干好了可以奖,干差了便会罚。我觉得这样挺好,避免伤了好人心,杜绝隧了闲人的意。结果因为其他项目组没有搞,所以我成了众矢之的。他为什么要搞?人家项目组都没有,就他多事,咱们不在他这里干了!


我想,国内的管理可能不是一种客观的结果制。而是另一种客观的”平衡制“。


就像是古代的科举,按照才学,按照成绩来说,状元每届多是江南的。但是,皇帝需要平衡,山西好久没有出个状元了,点一个吧。河南今年学子闹事,罢考,为了稳一稳人心,给一个吧。江南都这么多了,少一个没什么关系的。


他更多是要让各方都满意。一个”平衡“贯穿了整个古今现代的价值观。


有人遵循自己的内心做事,也有人遵循别人的内心做事。不管遵循哪一方,坚持就好,不要去轻易比较,各自有各自的付出和收获。


当路上都在逆行时,你会发

作者:TF男孩
来源:juejin.cn/post/7224764099187966010
现,其实是你在逆行。

收起阅读 »

给同学解决问题有感——天下前端是一家!

web
   在毕设如火如荼进行的过程中,大家设计xxx系统时都会有各种各样的界面,这不就到了本菜鸟的领域!hhh,小时候的画家梦也算实现了一半,只不过画笔变成了code~    最近,给两位同学解决了前端方面的问题,但都不是我学的javascript语言,摸索着平时...
继续阅读 »

   在毕设如火如荼进行的过程中,大家设计xxx系统时都会有各种各样的界面,这不就到了本菜鸟的领域!hhh,小时候的画家梦也算实现了一半,只不过画笔变成了code~


   最近,给两位同学解决了前端方面的问题,但都不是我学的javascript语言,摸索着平时学到的前端思想,还是成功的解决了这些问题,有感而发,记录下来~



  •    第一位出场的是正在自学python的学习委员,也是一位准研究生。他遇到的问题是,在a项目里定义了一个复杂界面,在b项目里定义了一个简单页面。他找到我的时候说,启动了b项目,但打开的却是a项目定义的页面。报错如下:


d60d9f2414f5a2867091a5a14db6e54.png
    看了他的页面,这路由和我学的不长得一毛一样!


8bb4fc7bab1580d1baa6692d0f2b801.png
  打开他的浏览器页面,看看页面的网络请求,404,我第一反应会不会是他路由的问题,导致找不到这个页面,显示了之前项目的界面。。。


dc111956dfdb5bc954fd74c3bfdea11.png
  但我转念一想,404 应该不会显示另一个项目的界面呀,除非请求的是之前项目的服务器。再注意到warning中的,use a server instead,这不就是换一个服务器,那一定是之前的端口被占用了,所以相当于请求之前的服务器。于是,搬出来我只会一个cd的小黑窗:


1684658842987.png
   解决占用之后,重启项目,完整的展示了新项目中的页面~
这个问题准确的说属于计网,但前后端的思想还是在里面。果然还是基础的东西~



  • 第二位出场的是一位在做安卓应用(毕设项目)的女同学,躺在床上收到她的连环问,导航栏隐藏?我直接惊坐起:


1684659076469.png
   仔细听了她的描述之后,在有了导航栏之后,页面某些按钮的位置发生了偏差,如下图:


4637c8740c325359481bc6fb139af42.png
  原本卡其色圆圈应该和下面蓝色圆圈重合,通关之后,显示下面的颜色。
因为下面一层的按钮是嵌套的背景图片里,所以不知道固定位置,不能用绝对定位控制两个按钮的位置完全重合(她是用可视化拖动的方法做的页面)。
让导航栏透明肯定是有办法的,但我想如果只透明 但仍然占据文档流,那还是没用呀!


1684659743090.png
  找到网友的方法,尝试之后,模拟机显示确实ok,隐藏了导航栏,位置也消除了偏差,在鼠标接触到导航栏位置时,导航栏显现,很人性化!但她在手机上通过apk安装包查看,还是有一定的偏差,我想是因为屏幕尺寸的问题吗?看了他的代码用的都是相对单位dp,应该可以自适应的呀,这个就不懂了,毕竟适配所有机型的问题,我在实习的时候也很头疼!




PS:
在此应该鸣谢一下我的老师和队友,坚定让我自己做了前后端分离的一个项目,自己建数据库,自己写接口,对前后端请求的
作者:MindedMier
来源:juejin.cn/post/7235458133505491005
发送接收还是有更细致的了解!
收起阅读 »