注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

吵上热搜第一,华为和小鹏在争什么?

来源 | 伯虎财经(bohuFN) 作者 | 陈平安 大概没有人能想到,这场有关AEB技术的讨论会引起如此之大的风波,以至于不仅仅是行业内在热切讨论,甚至于直接冲上了微博热搜第一。 事情的起因源于小鹏汽车董事长和小鹏在一期采访里表达了对AEB技术的看法::“...
继续阅读 »


来源 | 伯虎财经(bohuFN)


作者 | 陈平安



大概没有人能想到,这场有关AEB技术的讨论会引起如此之大的风波,以至于不仅仅是行业内在热切讨论,甚至于直接冲上了微博热搜第一。


事情的起因源于小鹏汽车董事长和小鹏在一期采访里表达了对AEB技术的看法::“友商讲了AEB(汽车自动紧急制动系统),我认为99%是假的,它就是造假,那些宣传都不是公司官方发布的,全是来自小视频。我们的人也去问了,它的AEB根本不能开,路上误刹车的情况太多了。”


虽然这番言论并没有直接点出友商的品牌,但是AEB是华为智能汽车解决方案BU CEO余承东在在新款问界 M7 发布会上专门提到的竞争力之一:“华为 ADS2.0 系统的 AEB 能力进一步提高,前向刹停速度已经从上海车展期间的 60km/h 提升至 90km/h。”


很快余承东便在朋友圈给出回应,称“连AEB是什么,居然有车企的一把手还根本没有搞懂呢!”。



不仅仅是两家大佬,其他品牌的掌门人包括腾势销售事业部总经理赵长江、哪吒汽车CEO张勇也纷纷表达了自己的看法。


那么到底什么是AEB,为什么小鹏和问界会因为这个技术激烈讨论?


01 到底什么是AEB


到目前为止讨论已经走向尾声,而敲响结束钟声的,是昨晚车媒懂车帝的夜间 AEB 实测。


AEB是Automatic Emergency Braking的缩写,中文翻译是自动紧急制动。这项技术的好处是,它可以让车在符合一定条件的工况下,识别到碰撞危险并且接管车辆以紧急刹车,从而避免碰撞或减轻碰撞后果。


简单来说,就是能让你在面对碰撞来不及踩下制动踏板时,汽车也能自动制动,从而避免碰撞。


从实测的结果来看,搭载了问界ADS 2.0 系统的问界 M5 和阿维塔 11的表现要明显优于小鹏的两款车型——G6和G9。比如,在面对静态假车时,问界 M5 和阿维塔 11 都能在80km/h时完成刹停,小鹏的车型则是40km/h ,当面对静态两轮电动车时,问界 M5 和阿维塔 11则是能在 60km/h时刹车,小鹏 G6 则是30km/h。


事实上,在卖点宣传上,高速行驶时做到及时刹停是相当重要的一部分。不仅仅是问界新车M7,腾势的赵长江日前也发文表示,腾势现在可以做到60km/h的状态下启动AEB,以后的高阶智能驾驶版本可以实现80km/h及以上。


不过,AEB真的是自动刹停速度越高,就越好吗?


持相反观点的人还不少。何小鹏今早就在微博给出了回应。何小鹏的核心观点是:首先高速AEB存在安全隐患和体验的问题,当前要做的是优化智能辅助驾驶能力,降低误触率;其次在有人驾驶的主动安全策略方面,要更加谨慎。



事实上,AEB并不是新能源汽车独占的优势,而是现代汽车的一项基本配置。根据乘联会数据,截止今年年中,乘用车配置AEB功能占比为49.5%,其中新能源车配置该功能占比已达到55.8%。


一些汽车测试工程师表示,AEB建立在雷射雷达、无线电波雷达以及影像辨识系统上,工作原理是经过雷达测距、识别,根据距离和车速进行运算得出结果并相应。而影响其结果的因素有很多,比如系统的误判或者极端天气等。此前,业内发生多起的幽灵刹车也是产生于此。而这种误判在高速行驶的状态下,带来的安全风险会更大。


哪吒汽车CEO张勇也在微博表示,AEB场景的复杂性太高,“要做得好难度确实很高,需要极大里程和样本量的测试”。


因此能否在更高速的情况下完成刹停并不是衡量AEB好坏的标准,如何用更好的智能辅助驾驶去帮助驾驶员进行预先制动,躲避危险场景才是更好的选择。


02 争夺“智能驾驶”的标签


无论是华为加持下的问界,还是小鹏,都刚刚从生死边缘走回来。在刚刚过去的10月,小鹏交付新车20002台,环比增长31%,同比增长292%。而在整个三季度,小鹏的总交付量为40008辆汽车。


无独有偶的是,10月也是问界爆发的月份。10月份问界系列交付了12700辆汽车,其中新款问界M7贡献了绝大部分的10547辆。而在今年8月,问界的全系销量也仅为5018辆。不仅如此,根据问界的官方消息,新款问界M7上市50天累计大定突破80000辆。


如果要探究两家销量起死回生的秘诀,智能驾驶一定是绕不过的关键。无论是小鹏还是华为,都把自动驾驶作为一个重要的卖点,在宣传上,两家也一直在暗暗较劲。比如在发布ADS 2.0时,余承东用他一如既往的风格表示:“(智能驾驶)和新势力、特斯拉相比,我们遥遥领先”。


何小鹏则在接受采访时表示:“我们有的人负责吆喝,让大家关注起来。有些人负责干活,把这些东西干好,再把客户拿过来,我觉得这个配合是天作之合。”


在业内,这两位也确实是毫无争议的第一梯队。今年在智能驾驶能力的比拼上,“开城”成为了最主要的比较维度之一。何小鹏在今年的1024 小鹏汽车科技日上介绍,将于 11 月底在 25 个城市开放,紧接着在 12 月底扩大至 50 个城市。


余承东表示,华为高阶智能驾驶系统将在 2023 年 12 月于全国开放。


从市场的表现来看,智能驾驶也已经在事实上成为了消费者购车的一个重要影响因素。根据博主孙少军的分享,无论是小鹏还是问界,智驾的选装率都超过了六成。


在政策端,据HiEV报道,智能网联汽车准入试点通知最快将在11月发布,这或许意味着国家层面的自动驾驶L3标准即将到来。


各家车企也都在加大对于智能驾驶的投入。例如蔚来宣布10月起要把增强领航辅助NOP+的使用场景从高速进入城区,预计明年一季度,将累计开通城区领航路线里程20万公里;二季度,将累计开通城区领航路线里程40万公里。


理想则表示公司此前对智驾的重视度不够,应该提前半年全力投入,而不是今年年初才开始:“账上趴着 700 多亿的现金,要把它转化成持续的竞争力。”


在这个背景下,这两家头部企业需要迅速为自己和智能驾驶划上等号。就像比亚迪通过迅速吃下“油转电”市场完成市场认知从而滚起规模一样,在智驾全面提速的时代,即便是领先者华为和小鹏,也需要加一脚油门了。


*文章封面首图及配图,版权归版权所有人所有。若版权者认为其作品不宜供大家浏览或不应无偿使用,请及时联系我们,本平台将立即更正。


作者:用户5117553812944
来源:juejin.cn/post/7299016228579377202
收起阅读 »

如何优雅的合并两个对象

提出需求 相信很多时候,你可能有这样的需求,比如A对象的属性1、属性2是你需要的,其余属性是空值,而B对象的属性3、属性4是你需要的,其余属性是空值,那么如何将A对象的属性与B对象的属性结合呢? 思路解析 要实现将两个对象的属性结合,需要用到反射技术,遍历每个...
继续阅读 »

提出需求


相信很多时候,你可能有这样的需求,比如A对象的属性1、属性2是你需要的,其余属性是空值,而B对象的属性3、属性4是你需要的,其余属性是空值,那么如何将A对象的属性与B对象的属性结合呢?


思路解析


要实现将两个对象的属性结合,需要用到反射技术,遍历每个对象的属性,将非null的属性复制给null的属性,最后实现合并属性的效果。


常用的复制对象属性的方法(传统方案)


使用Java Bean Utils,这是Apache Commons BeanUtils库中的一个工具类,使用此工具类,则需要自己去实现判断属性的业务逻辑。


这个工具类有两个复制属性的方法,copyProperties和copyProperty。


static voidcopyProperties(Object dest, Object orig)Copy property values from the origin bean to the destination bean for all cases where the property names are the same.
static voidcopyProperty(Object bean, String name, Object value)Copy the specified property value to the specified destination bean, performing any type conversion that is required.

copyProperties方法会将orig(第二个对象)的属性复制给dest(第二个对象),这种复制是完全复制,会完全覆盖dest的属性。


copyProperty方法将复制value给bean的特定属性。


hutool合并对象的方案


Hutool是一个Java工具包类库,对文件、流、加密解密、转码、正则、线程、XML等JDK方法进行封装,组成各种Util工具类。


hutool常常被广大spring程序员唾弃,抱怨其在项目中的稳定性,bug问题。但我认为,hutool是一个在不断迭代和维护的库,虽然是国内程序员开发的,但是其中很多功能仍然是简单好用的,不能够以偏概全的全面否定它。


回归正题,下面做详细的说明


pom导入合并对象所必须的hutool库


<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>5.8.16</version>
</dependency>

方法解释



  • cn.hutool.core.bean.BeanUtil


static <T> TcopyProperties(Object source, Class<T> tClass, String... ignoreProperties)按照Bean对象属性创建对应的Class对象,并忽略某些属性
static voidcopyProperties(Object source, Object target, boolean ignoreCase)复制Bean对象属性
static voidcopyProperties(Object source, Object target, CopyOptions copyOptions)复制Bean对象属性 限制类用于限制拷贝的属性,例如一个类我只想复制其父类的一些属性,就可以将editable设置为父类
static voidcopyProperties(Object source, Object target, String... ignoreProperties)复制Bean对象属性 限制类用于限制拷贝的属性,例如一个类我只想复制其父类的一些属性,就可以将editable设置为父类

hutool的复制对象方法copyProperties有4个重载类型,在apache common beanutils的基础上,增加了一些复制选项,这些选项是非常实用的,比如忽略大小写、忽略某些属性、某些特定的复制选项等。


copyProperties(Object source, Object target, CopyOptions copyOptions)


因为我实际用了copyProperties(Object source, Object target, CopyOptions copyOptions),重点讲解此方法的传入参数。


CopyOptions包括以下的一些属性或选项,可以根据你的项目需求来选择


限定符和类型字段和说明
protected TypeConverterconverter自定义类型转换器,默认使用全局万能转换器转换
protected Class<?>editable限制的类或接口,必须为目标对象的实现接口或父类,用于限制拷贝的属性,例如一个类我只想复制其父类的一些属性,就可以将editable设置为父类 如果目标对象是Map,源对象是Bean,则作用于源对象上
protected BiFunction<String,Object,Object>fieldValueEditor字段属性值编辑器,用于自定义属性值转换规则,例如null转""等
protected booleanignoreCase是否忽略字段大小写
protected booleanignoreError是否忽略字段注入错误
protected booleanignoreNullValue是否忽略空值,当源对象的值为null时,true: 忽略而不注入此值,false: 注入null
protected booleanoverride是否覆盖目标值,如果不覆盖,会先读取目标对象的值,非null则写,否则忽略。
protected booleantransientSupport是否支持transient关键字修饰和@Transient注解,如果支持,被修饰的字段或方法对应的字段将被忽略。

自己写的合并工具类


该工具类仅供参考,可以根据自己情况调整


public class MergeUtil {
private static final CopyOptions options = CopyOptions.create().setIgnoreNullValue(true).setOverride(false);
private static final CopyOptions optionsAllowOverride = CopyOptions.create().setIgnoreNullValue(true).setOverride(true);

//将sourceBean中的属性合并到tagetBean,忽略Null值,非Null值不允许覆盖
public static Object merge(Object sourceBean, Object targetBean) {
BeanUtil.copyProperties(sourceBean, targetBean, options);

return targetBean;
}

//将sourceBean中的属性合并到tagetBean,忽略Null值,非Null值允许覆盖
public static Object mergeAllowOverride(Object sourceBean, Object targetBean) {
BeanUtil.copyProperties(sourceBean, targetBean, optionsAllowOverride);

return targetBean;
}
}

作者:XiaoJi
来源:juejin.cn/post/7290475242274258978
收起阅读 »

人需借以虚名而成事

前言 自从我看冯唐老师推荐的《资治通鉴》之后,对历史有那么一丢丢的兴趣,直到在短视频里面刷到《百家讲坛》里面几位老师的讲解片段,以及跟自己经历结合,瞬间满满的共鸣,是啊时代一直在变化,而里面的为人处事、社会规则凡此种种却从来没有太大变化,这就是我们需要去学习...
继续阅读 »

bcf7ea99ded21c1a1ac9a1d85b59724c.jpeg


前言




自从我看冯唐老师推荐的《资治通鉴》之后,对历史有那么一丢丢的兴趣,直到在短视频里面刷到《百家讲坛》里面几位老师的讲解片段,以及跟自己经历结合,瞬间满满的共鸣,是啊时代一直在变化,而里面的为人处事、社会规则凡此种种却从来没有太大变化,这就是我们需要去学习历史的原因。


所以有了以下感慨。



<<百家讲坛>> 不愧是经典,整本历史歪歪斜斜写着两字:权谋,谋事、谋人,加上不确定因素,天时地利人和,最终变成命运。
人的一生就像书中一行字轻描淡写,绘成一笔~



人需借以虚名而成事




这里解释一下,人需要倚靠外部的这些名号、头衔从而能有比较大的作为。为什么这么讲呢,听我细细道来~


人生需要四行


这是王立群老师讲的,人生要想有点成就需要四行,首先你自己行,你自身本身够硬,能够做好事,其次是有人说你行,从古代的推荐机制,比如说优秀的人推荐其他优秀的人才对不对,李白的大名远扬,唐玄宗在公主引荐之下见了皇帝,第三个说你行的人要行,推荐你的那个人本身能力要强,不然没有说服力,第四是身体行,很多机会是熬出来的,比如司马懿伺候了3位君主,兢兢业业,最终才能完成自己的大业。


何为借以虚名?


1、刘备的例子,他在比较年长的时候还无法实现自己的基业,寄人篱下,他登场的时候是怎样的呢,名号叫刘皇叔,翻开长长的祖谱,八辈子打不到杆哈哈哈,不过因为当时被曹操挟持,所以需要有汉族帮忙自己,所以顺理成章的提拔上来。


2、项羽出生在一个贵族家庭。他的父亲项梁是楚国将领,曾经在楚汉战争中和刘邦一起作战。而他的母亲也是楚国的贵族,是楚王的女儿。


相比之下,刘邦的出身则比较低微。他的祖父是一个农民,父亲是一个小县官。他自己也曾经当过一个普通的县尉,但因为功绩不够而被罢免。


尽管项羽出身高贵,但他的性格比较豁达,喜欢豪情壮志和享乐人生,行事大胆不拘小节。刘邦则更加谨慎稳重,注重政治规划和长远发展。


3、王立群老师讲述过他自身的故事,小学的时候成绩优秀得到保送,后面名额被学校给了别人,自己去了私立的学校,半工半读,到了大学还是遇到了同样的遭遇,后面就到比较好的大学教书,但是有一天校长跟他讲他的成绩还是出身问题需要去另一所低一点的学校教书,他当时就哭了,命运比较坎坷的。


4、工作中,我们常常会听到,某位领导自我介绍某某大厂经历,还有某某大学毕业,还有当你投一些大厂的核心部门的时候,都会对你学历进行考核。


所以我们从上面3个例子来看,人生确实正如王立群老师讲的,需要四行,当你自己不行的时候,即使有机会也会最终流失掉;当没有推荐你的时候,也会埋没在茫茫人海中,并不是说是金子总会发光,现实是伯乐更加重要;当说你行的人没有能力的时候也不行,名牌大学、大厂经历也是这样,通过一个有名声的物体从而表示你也可以,也很厉害哈哈;最后身体是本钱,这个就不用过多的讲解了。


读历史的目的,从前人的故事、经历中,学习到点什么,化为自身的经验、阅历。


我发现现在很多人都喜欢出去玩,说的开阔视野,扩展见识,在我看来读历史才是真正的拓宽自己的眼界,原来几百年前也有人跟我一样遇到同样的问题,也有那些胸怀大志的三国名将郁郁不得志。


历史是一本书籍,它可以很薄,几句话、几行字就概括了一个朝代,它也可以很厚,厚到整个朝代的背景,比如气候情况、经济情况、外交情况、内政情况,各个出名的人物事迹,那岂是几页纸能讲得完的,是多么的厚重。


如果人不能借以虚名怎么做事?


修炼两件事情




同样百家讲坛也给出了一个比较好的见解,一个人想要成事,需要两个明,一个是高明、一是精明。高明是你有足够的眼光去发现,发现机会也好,发现危机也好,往往人生的成就需要这种高瞻远瞩的;另一个精明,是指在做事上打磨,细心主动,王阳明讲的知行合一,里面很关键的一点就是事上练,方法论在实战中去验证总结。


心态上练




李连杰在采访中说了这么一个耐人寻味的故事,他发现不管是有钱的人还是没有钱的人都会生气,比如一些老板在为几百万损失破口大骂,也有人因为几万的丢失是否生气,也有人丢了几块钱很愤怒。他觉得人都会愤怒,只是那个导致他愤怒的级别不一样而已。


他早年给自己定目标,发现当达到目标之后,发现还是有人比自己厉害,所以即使达到目标也没有想象快乐;即使有钱人无法解决痛苦的问题,比如生老病死。


所以人生意义在哪里?


我认为如果你奔着成事的想法,多修炼上面两件事,如果没有发展机会,不如躺平,积累自己的能力,就像道德经里面的无为而为,而心态的修炼是人生终极修炼,因为生死才是人生最大的难关,人应该怎么脱离时间束缚,不受衰老影响。


恰恰历史给了我们启示,要有所作为,能够为人类历史上留下精神粮食,做出浓厚贡献,就像我们都没有见过孔子、老子吧,但是他们的精神至今都在影响着我们。


作者:大鸡腿同学
来源:juejin.cn/post/7252251628090490917
收起阅读 »

仿写el-upload组件,彻底搞懂文件上传

web
用了那么久的Upload组件,你知道是怎么实现的么,今天就来仿写一个饿了么el-upload vue组件,彻底搞懂前端的文件上传相关知识! 要实现的props 参数说明action必选参数,上传的地址headers设置上传的请求头部multiple是否支持多选...
继续阅读 »

用了那么久的Upload组件,你知道是怎么实现的么,今天就来仿写一个饿了么el-upload vue组件,彻底搞懂前端的文件上传相关知识!


要实现的props


参数说明
action必选参数,上传的地址
headers设置上传的请求头部
multiple是否支持多选文件
data上传时附带的额外参数
name上传的文件字段名
with-credentials支持发送 cookie 凭证信息
show-file-list是否显示已上传文件列表
drag是否启用拖拽上传
accept接受上传的文件类型
on-preview点击文件列表中已上传的文件时的钩子
on-remove文件列表移除文件时的钩子
on-success文件上传成功时的钩子
on-error文件上传失败时的钩子
on-progress文件上传时的钩子
on-change添加文件时被调用
before-upload上传文件之前的钩子,参数为上传的文件,若返回 false 或者返回 Promise 且被 reject,则停止上传。
before-remove删除文件之前的钩子,参数为上传的文件和文件列表,若返回 false 或者返回 Promise 且被 reject,则停止删除。
list-type文件列表的类型
auto-upload是否在选取文件后立即进行上传
file-list上传的文件列表, 例如: [{name: 'food.jpg', url: 'xxx.cdn.com/xxx.jpg'}]
limit最大允许上传个数
on-exceed文件超出个数限制时的钩子

参考:element.eleme.cn/#/zh-CN/com…


这里面有几个重要的点:



  1. input file 的美化

  2. 多选

  3. 拖拽


一个个实现


创建upload组件文件


src/components/upload/index.vue


<template></template>
<script setup>
  // 属性太多,把props单独放一个文件引入进来
  import property from './props'
  const props = defineProps(property)
</script>
<style></style>

./props.js


export default {
  action: {
    typeString
  },
  headers: {
    typeObject,
    default: {}
  },
  multiple: {
    typeBoolean,
    defaultfalse
  },
  data: {
    typeObject,
    default: {}
  },
  name: {
    typeString,
    default'file'
  },
  'with-credentials': {
    typeBoolean,
    defaultfalse
  },
  'show-file-list': {
    typeBoolean,
    defaulttrue,
  },
  drag: {
    typeBoolean,
    defaultfalse
  },
  accept: {
    typeString,
    default''
  },
  'list-type': {
    typeString,
    default'text' // text、picture、picture-card
  },
  'auto-upload': {
    typeBoolean,
    defaulttrue
  },
  'file-list': {
    typeArray,
    default: []
  },
  disabled: {
    typeBoolean,
    defaultfalse
  },
  limit: {
    typeNumber,
    defaultInfinity
  },
  'before-upload': {
    typeFunction,
    default() => {
      return true
    }
  },
  'before-remove': {
    typeFunction,
    default() => {
      return true
    }
  }

具体的编写upload组件代码


1. 文件上传按钮的样式


我们都知道,<input type="file">的默认样式是这样的: 很丑,并且无法改变其样式。


解决办法:可以把input隐藏,重新写个按钮点击来触发input的文件选择。


<template>
  <input 
     type="file" 
     id="file" 
     @change="handleChange"
  >

  <button 
     class="upload-btn" 
     @click="choose"
  >

    点击上传
  </button>
</template>
<script setup>
  // 触发选择文件
  const choose = () => {
    document.querySelector('#file').click()
  }
  // input选择文件回调
  const handleChange = (event) => {
    files = Array.from(event.target.files)
    console.log('[ files ] >', files)
  }
</script>
<style scoped>
  #file {
    display: none;
  }
  .upload-btn {
    border: none;
    background-color#07c160;
    color#fff;
    padding6px 10px;
    cursor: pointer;
  }
</style>

效果:



这样也是可以调起文件选择框,并触发input的onchange事件。



2. 多选


直接在input上加一个Booelan属性multiple,根据props中的值动态设置


顺便把accept属性也加上


<template>
  <input 
     type="file" 
     id="file" 
     :multiple="multiple"
     :accept="accept"
     @change="handleChange"
  >
</template>

3. 拖拽


准备一个接收拖拽文件的区域,props传drag=true就用拖拽,否则就使用input上传。


<template>
  <input 
    type="file" 
    id="file" 
    :multiple="multiple"
    :accept="accept"
    @change="handleChange"
  >
  <button 
     class="upload-btn" 
     v-if="!drag" 
     @click="choose"
  >
    点击上传
  </button>
  <div 
    v-else 
    class="drag-box" 
    @dragover="handleDragOver"
    @dragleave="handleDragLeave"
    @drop="handleDrop"
    @click="choose"
    :class="{'dragging': isDragging}"
  >
    将文件拖到此处,或<span>点击上传</span>
  </div>
</template>

dragging用来拖拽鼠标进入时改变样式


<script setup>
  const isDragging = ref(false)
  // 拖放进入目标区域
  const handleDragOver = (event) => {
    event.preventDefault()
    isDragging.value = true
  }
  const handleDragLeave = (event) => {
    isDragging.value = false
  }
  let files = []
  // 拖拽放置
  const handleDrop = (event) => {
    event.preventDefault()
    isDragging.value = false
    files = Array.from(event.dataTransfer.files);
    console.log(files);
  }
</script>

.drag-box {
    width240px;
    height150px;
    line-height150px;
    text-align: center;
    border1px dashed #ddd;
    cursor: pointer;
    border-radius8px;
  }
  .drag-box:hover {
    border-color: cornflowerblue;
  }
  .drag-box.dragging {
    background-colorrgb(131161216, .2);
    border-color: cornflowerblue;
  }
  .drag-box span {
    color: cornflowerblue;
  }

跟使用input上传效果一样


4. 上传到服务器


并实现on-xxx钩子函数


  const emit = defineEmits()
  const fileList = ref([])
  let files = []

  // 拖拽放置
  const handleDrop = (event) => {
    event.preventDefault()
    isDragging.value = false
    files = Array.from(event.dataTransfer.files);
    console.log('[ files ] >', files)
    handleBeforeUpload(files)
  }

  // input选择文件回调
  const handleChange = (event) => {
    files = Array.from(event.target.files)
    console.log('[ files ] >', files)
    handleBeforeUpload(files)
  }

  const handleBeforeUpload = (files) => {
    if (files.length > props.limit - fileList.value.length) {
      console.error(`当前限制选择 ${props.limit} 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.value.length} 个文件`)
      emit('on-exceed', files, toRaw(fileList.value))
      return
    }
    // 可以把锁哥文件放到一个formData中一起上传,
    // 遍历文件一个个上传,这里一个个上传是为了实现钩子函数回调时返回对应的file对象。
    files.forEach(async file => {
      emit('on-change', file, files)
      if (!props.beforeUpload()) {
        return
      }
      if (props.autoUpload) {
        uploadRequest(file, files)
      }
    })
  }

  // 手动上传已选择的文件
  const submit = () => {
    files.forEach(async file => {
      uploadRequest(file, files)
    })
  }
  
  // 保存xhr对象,用于后面取消上传
  let xhrs = []
  const uploadRequest = async (file, files) => {
    let xhr = new XMLHttpRequest();
    // 调用open函数,指定请求类型与url地址。请求类型必须为POST
    xhr.open('POST', props.action);
    // 设置自定义请求头
    Object.keys(props.headers).forEach(k => {
      xhr.setRequestHeader(k, props.headers[k])
    })
    // 额外参数
    const formData = new FormData()
    formData.append('file', file);
    Object.keys(props.data).forEach(k => {
      formData.append(k, props.data[k]);
    })
    // 携带cookie
    xhr.withCredentials = props.withCredentials
    xhr.upload.onprogress = (e) => {
      emit('on-progress', e, file, files)
    }
    // 监听状态
    xhr.onreadystatechange = () => {
      if (xhr.readyState === 4) {
        const res = JSON.parse(xhr.response)
        const fileObj = {
          name: file.name,
          percentage: 100,
          raw: file,
          response: res,
          status: 'success',
          size: file.size,
          uid: file.uid,
        }
        fileList.value.push(fileObj)
        if (xhr.status === 200 || xhr.status === 201) {
          emit('on-success', res, fileObj, toRaw(fileList.value))
        } else {
          emit('on-error', res, fileObj, toRaw(fileList.value))
        }
      }
    }
    // 发起请求
    xhr.send(formData);
    xhrs.push({
      xhr,
      file
    })
  }

  const preview = (file) => {
    emit('on-preview', file)
  }

  const remove = (file, index) => {
    if (!props.beforeRemove()) {
      return
    }
    fileList.value.splice(index, 1)
    emit('on-remove', file, fileList.value)
  }

  // 取消上传
  const abort = (file) => {
    // 通过file对象找到对应的xhr对象,然后调用abort
    // xhr.abort()
  }

  defineExpose({
    abort,
    submit
  })

全部代码


<template>
  <input 
    type="file" 
    id="file" 
    :multiple="multiple"
    :accept="accept"
    @change="handleChange"
  >

  <button class="upload-btn" v-if="!drag" @click="choose">
    点击上传
  </button>
  <div 
    v-else 
    class="drag-box" 
    @dragover="handleDragOver"
    @dragleave="handleDragLeave"
    @drop="handleDrop"
    @click="choose"
    :class="{'dragging': isDragging}"
  >

    将文件拖到此处,或<span>点击上传</span>
  </div>
  <template v-if="showFileList">
    <template v-if="listType === 'text'">
      <p class="file-item" v-for="(file, index) in fileList" :key="index" @click="preview(file)">
        <span>{{file.name}}</span>
        <span class="remove" @click.stop="remove(file, index)">×</span>
      </p>
    </template>
  </template>
</template>

<script setup>
  import { ref, toRaw, onMounted } from 'vue'
  import property from './props'
  const props = defineProps(property)
  const emit = defineEmits()

  const fileList = ref([])
  const isDragging = ref(false)

  // 触发选择文件
  const choose = () => {
    document.querySelector('#file').click()
  }

  // 拖放进入目标区域
  const handleDragOver = (event) => {
    event.preventDefault()
    isDragging.value = true
  }

  const handleDragLeave = (event) => {
    isDragging.value = false
  }

  let files = []

  // 拖拽放置
  const handleDrop = (event) => {
    event.preventDefault()
    isDragging.value = false
    files = Array.from(event.dataTransfer.files);
    console.log('[ files ] >', files)
    handleBeforeUpload(files)
  }

  // input选择文件回调
  const handleChange = (event) => {
    files = Array.from(event.target.files)
    console.log('[ files ] >', files)
    handleBeforeUpload(files)
  }

  const handleBeforeUpload = (files) => {
    if (files.length > props.limit - fileList.value.length) {
      console.error(`当前限制选择 ${props.limit} 个文件,本次选择了 ${files.length} 个文件,共选择了 ${files.length + fileList.value.length} 个文件`)
      emit('on-exceed', files, toRaw(fileList.value))
      return
    }
    files.forEach(async file => {
      emit('on-change', file, files)
      if (!props.beforeUpload()) {
        return
      }
      if (props.autoUpload) {
        uploadRequest(file, files)
      }
    })
  }

  // 手动上传已选择的文件
  const submit = () => {
    files.forEach(async file => {
      uploadRequest(file, files)
    })
  }

  let xhrs = []
  const uploadRequest = async (file, files) => {
    let xhr = new XMLHttpRequest();
    // 调用open函数,指定请求类型与url地址。请求类型必须为POST
    xhr.open('POST', props.action);
    // 设置自定义请求头
    Object.keys(props.headers).forEach(k => {
      xhr.setRequestHeader(k, props.headers[k])
    })
    // 额外参数
    const formData = new FormData()
    formData.append('file', file);
    Object.keys(props.data).forEach(k => {
      formData.append(k, props.data[k]);
    })
    // 携带cookie
    xhr.withCredentials = props.withCredentials
    xhr.upload.onprogress = (e) => {
      emit('on-progress', e, file, files)
    }
    // 监听状态
    xhr.onreadystatechange = () => {
      if (xhr.readyState === 4) {
        const res = JSON.parse(xhr.response)
        const fileObj = {
          name: file.name,
          percentage100,
          raw: file,
          response: res,
          status'success',
          size: file.size,
          uid: file.uid,
        }
        fileList.value.push(fileObj)
        if (xhr.status === 200 || xhr.status === 201) {
          emit('on-success', res, fileObj, toRaw(fileList.value))
        } else {
          emit('on-error', res, fileObj, toRaw(fileList.value))
        }
      }
    }
    // 发起请求
    xhr.send(formData);
    xhrs.push({
      xhr,
      file
    })
  }

  const preview = (file) => {
    emit('on-preview', file)
  }

  const remove = (file, index) => {
    if (!props.beforeRemove()) {
      return
    }
    fileList.value.splice(index, 1)
    emit('on-remove', file, fileList.value)
  }

  // 取消上传
  const abort = (file) => {
    // 通过file对象找到对应的xhr对象,然后调用abort
    // xhr.abort()
  }

  defineExpose({
    abort,
    submit
  })
</script>

<style scoped>
  #file {
    display: none;
  }
  .upload-btn {
    border: none;
    background-color#07c160;
    color#fff;
    padding6px 10px;
    cursor: pointer;
  }
  .drag-box {
    width240px;
    height150px;
    line-height150px;
    text-align: center;
    border1px dashed #ddd;
    cursor: pointer;
    border-radius8px;
  }
  .drag-box:hover {
    border-color: cornflowerblue;
  }
  .drag-box.dragging {
    background-colorrgb(131161216, .2);
    border-color: cornflowerblue;
  }
  .drag-box span {
    color: cornflowerblue;
  }
  .file-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-top12px;
    padding0 8px;
    border-radius4px;
    cursor: pointer;
  }
  .file-item:hover {
    background-color#f5f5f5;
    color: cornflowerblue;
  }
  .file-item .remove {
    font-size20px;
  }
</style>

如何使用


<template>
<upload
ref="uploadRef"
action="http://localhost:3000/upload"
multiple
show-file-list
drag
auto-upload
upload-folder
:headers="headers"
:data="data"
:limit="3"
:before-upload="beforeUpload"
:before-remove="beforeRemove"
@on-change="handleChange"
@on-success="handleSuccess"
@on-error="handleError"
@on-preview="handlePreview"
@on-remove="handleRemove"
@on-exceed="handleExceed"
@on-progress="handleProgress"
>

</upload>
</template>

<script setup>
import { ref } from 'vue'
import upload from '@/components/upload/index.vue'

const uploadRef = ref(null)

const data = {
name: '张三',
age: 20
}

const headers = {
a: 111
}

const handleChange = (file, fileList) => {
// console.log('onChange', file, fileList)
}
const handleSuccess = (res, file, fileList) => {
console.log(res, file, fileList)
}
const handleError = (err, file, fileList) => {
console.log(err, file, fileList)
}
const handlePreview = (file) => {
console.log('handlePreview', file)
}
const handleRemove = (file, fileList) => {
console.log('handleRemove', file, fileList)
}
const handleExceed = (files, fileList) => {
console.log('文件个数超限', files, fileList)
}
const handleProgress = (e, file, fileList) => {
console.log('上传进度');
if (e.lengthComputable) {
const percentComplete = Math.ceil((e.loaded / e.total) * 100)
console.log('[ percentComplete ] >', percentComplete)
}
}
const beforeUpload = (file) => {
return true
}
const beforeRemove = (file, fileList) => {
return true
}
</script>

<style>

</style>


作者:xintianyou
来源:juejin.cn/post/7292302859964727346
收起阅读 »

前端如何直接上传文件夹

web
前面写了一篇仿写el-upload组件,彻底搞懂文件上传,实现了选择/拖拽文件上传,我们经常看到一些网站支持直接选择整个文件夹上传,例如:宝塔面板、cloudflare托管、对象存储网站等等需要模拟文件路径存储文件的场景。那是怎么实现的呢? 依然从两方面来说:...
继续阅读 »

前面写了一篇仿写el-upload组件,彻底搞懂文件上传,实现了选择/拖拽文件上传,我们经常看到一些网站支持直接选择整个文件夹上传,例如:宝塔面板、cloudflare托管、对象存储网站等等需要模拟文件路径存储文件的场景。那是怎么实现的呢?


依然从两方面来说:



  1. input选择文件夹

  2. 拖拽文件夹


input选择文件夹


在props.js中加一个属性,upload-folder是否支持上传文件夹


export default {
// 前面的省略了...
// 是否支持选择文件夹
'upload-folder': {
type: Boolean,
default: false
}
}

改一下input标签,依然是根据props的值动态判断是否支持上传文件夹。主要是webkitdirectory这个属性,由于不是一个标准属性,需要加浏览器前缀。


<input 
type="file"
id="file"
:multiple="multiple"
:accept="accept"
:webkitdirectory="uploadFolder"
:mozdirectory="uploadFolder"
:odirectory="uploadFolder"
@change="handleChange"
>


注意:支持选择文件夹时就只能选择文件夹,无法选择文件。


那么如何获取选择的文件夹呢?其实我们最终要上传的依然是文件,也就是file对象,文件夹也是一个特殊的文件。


依然是通过inputonchange事件回调拿到上传的event


或者直接获取input这个dom对象,然后拿到files属性,结果是一样的。


// input选择文件回调
const handleChange = (event) => {
console.log('[ files ] >', event.target.files)
const inputDom = document.querySelector('#file')
console.log('[ files ] >', inputDom.files)
}


可以看到,比选择单个文件时,多了一个webkitRelativePath属性,并且它是递归选择的文件夹,拿到这个文件夹及其子文件夹下所有的文件,我们可以通过这个属性拿到上传时文件所在的文件夹名称路径


拖拽文件夹


上篇文章讲过拖拽如何拿到文件,首先要准备一个用于拖拽放置的区域。
调用upload组件时,传入drag=true


<div 
class="drag-box"
@dragover="handleDragOver"
@dragleave="handleDragLeave"
@drop="handleDrop"
>

将文件拖到此处,或<span>点击上传span>
div>

// 拖放进入目标区域
const handleDragOver = (event) => {
event.preventDefault()
}
// 拖拽放置
const handleDrop = (event) => {
event.preventDefault()
console.log('[ event ] >', event)
}

注意:和input上传不同,拖拽时,是可以同时拖拽文件和文件夹的。


因为可以同时拖拽文件和文件夹,我们就不能直接使用event.dataTransfer.files,如果刚好拖拽进来的是一个文件,那可以这么获取,如果是个文件夹呢?那就不行了。


同时拖拽一个文件和一个文件夹


这时候就要用到event.dataTransfer.items


// 拖拽放置
const handleDrop = (event) => {
event.preventDefault()
console.log(event.dataTransfer.items)
}

打印一下看看:

得到一个List类型的数据,里面是两个DataTransferItem,控制台无法直接查看它到底是个什么玩意儿。

看MDN,也看不出它具体是个啥。既然是List,遍历一下看看:


const handleDrop = (event) => {
event.preventDefault()
console.log(event.dataTransfer.items)
for (const item of event.dataTransfer.items) {
console.log('[ item ] >', item)
}
}


可以看到不管是文件还是文件夹,都被识别成了file,只不过图片是直接能识别出type为image/png


查看MDN,developer.mozilla.org/zh-CN/docs/…


点击查看itemPrototype,发现里面有个webkitGetAsEntry方法,执行它就能拿到item的具体信息。


看方法名,带了个webkit,但是这个方法除了Android Firefox浏览器以外都可以用。


for (const item of event.dataTransfer.items) {
const entry = item.webkitGetAsEntry()
console.log(entry)
}

依然拖动上面那个图片文件和一个文件夹:


可以看出,文件夹里面还有文件和文件夹,但是只显示了一个文件和一个文件夹,看来拖拽和input上传不一样,它不会自动的把里面所有的文件递归列出来。


通过isDirectory属性,就能区分是文件还是文件夹。除了这些基础属性以外,继续查看Prototype,可以看到还有一系列方法:


先看怎么拿到文件


entry是一个文件时,它有两个方法:createWriter()file(),查看MDN,developer.mozilla.org/en-US/docs/…

createWriter()已经废弃了,而且也不是我们今天要用的。

file()才是我们要找的。


这不就是我们熟悉的file对象吗,跟input上传拿到的一毛一样。


再看怎么拿到文件夹


查看MDN的Drop API webkitGetAsEntry()方法,developer.mozilla.org/zh-CN/docs/… 可得,如果是文件夹,可以通过createReader方法创建一个文件目录阅读器,然后通过readEntries方法,重新拿到每个item,这就是event.dataTransfer.items里面的每个item

我们写一下试试

依然是之前那个图片和文件夹

只打印出了跟目录下一级的一个文件和一个文件夹,那下面还有一个文件怎么办呢?


递归呀!


写一个递归读文件的方法。


const readFiles = async (item) => {
if (item.isDirectory) {
// 是一个文件夹
console.log('=======文件夹=======');
const directoryReader = item.createReader();
// readEntries是一个异步方法
const entries = await new Promise((resolve, reject) => {
directoryReader.readEntries(resolve, reject);
});

let files = [];
for (const entry of entries) {
const resultFiles = await readFiles(entry);
files = files.concat(resultFiles);
}
return files;
} else {
// 是一个文件
console.log('=======文件=======');
// file也是一个异步方法
const file = await new Promise((resolve, reject) => {
item.file(resolve, reject);
});
console.log('[ file ] >', file);
return [file];
}
}

handleDrop方法也要改一下


// 拖拽放置
const handleDrop = async (event) => {
event.preventDefault()
console.log(event.dataTransfer.items)

const files = [];
const promises = [];
for (const item of event.dataTransfer.items) {
const entry = item.webkitGetAsEntry();
console.log('[ entry ] >', entry);
promises.push(readFiles(entry));
}

const resultFilesArrays = await Promise.all(promises);
const allFiles = resultFilesArrays.flat();

console.log('[ All files ] >', allFiles);
}

再次拖拽上传看看

三个文件我们都拿到了。


总结


上传文件夹,还是直接使用input比较简单,使用它能直接拿到文件夹下所有的文件,以及每个文件在本地的路径,代码量也少很多。


拖拽的好处是文件和文件夹能一起上传。


作者:xintianyou
来源:juejin.cn/post/7292323606875553843
收起阅读 »

vue3 轮播图的实现

web
最近开发过程中,有一些轮播图的需求,虽然公司的组件库已经有swiper的组件,但是功能不全,很多效果实现不了,于是经过翻找swiper的官网,发现你想要的样式都有,下面来说一下swiper的简单使用。 想实现的效果 点击prev和next可实现图片的切换 安...
继续阅读 »

最近开发过程中,有一些轮播图的需求,虽然公司的组件库已经有swiper的组件,但是功能不全,很多效果实现不了,于是经过翻找swiper的官网,发现你想要的样式都有,下面来说一下swiper的简单使用。


想实现的效果


点击prev和next可实现图片的切换


image.png


安装


swiper的安装是比较简单的。

中文官网:http://www.swiper.com.cn/index.html

英文官网: swiperjs.com/


npm i swiper

使用


接下来就是swiper的使用了,swiper的使用非常简单。可查看官网例子

codesandbox.io/p/sandbox/2…


例子有归有,使用简单归简单,但是实现的样式和自己想要的差距还是很大,查了一波资料,现将代码放出,哈哈。

html


<swiper
:navigation="{
nextEl: '.swiper-button-next1',
prevEl: '.swiper-button-prev1'
}"

:modules="modules"
class="mySwiper"
:slides-per-view="3"
>

<swiper-slide>
<img src="图片地址" alt="" />
</swiper-slide>
<swiper-slide>
<img src="图片地址" alt="" />
</swiper-slide>
<swiper-slide>
<img src="图片地址" alt="" />
</swiper-slide>
<swiper-slide>
<img src="图片地址" alt="" />
</swiper-slide>
<swiper-slide>
<img src="图片地址" alt="" />
</swiper-slide>
<swiper-slide>
<img src="图片地址" alt="" />
</swiper-slide>
<swiper-slide>
<img src="图片地址" alt="" />
</swiper-slide>
<swiper-slide>
<img src="图片地址" alt="" />
</swiper-slide>
<div class="swiper-button-prev-one" slot="button-prev">
<Button>
</Button>
</div>
<div class="swiper-button-next-one" slot="button-next">
<Button>
</Button>
</div>
</swiper>

通过navigation来控制轮播图的上一页,下一页,通过slides-per-view来控制每页显示几张图片。
js


import { Swiper, SwiperSlide, navigationPrev } from 'swiper/vue'
import 'swiper/css'
import 'swiper/css/navigation'
import { Navigation } from 'swiper/modules'
const modules = [Navigation]

js部分也是毕竟简单的,把该引入的文件引入即可。这样难道就实现效果了吗,当然不是,还需要改css样式


css(css部分部分采用tailwindcss编写)


.mySwiper {
@apply pb-2xl;
.swiper-button-prev-one {
@apply text-[#333] absolute text-[.875rem] left-0 bottom-0 cursor-pointer;
:deep(.m-button) {
@apply w-6 h-6;

span {
@apply text-sm #{!important};
}
}
}
.swiper-button-next-one {
@apply text-[#333] absolute text-[.875rem] left-[2.5rem] bottom-0 cursor-pointer;
:deep(.m-button) {
@apply w-6 h-6 bg-[#000] text-[#fff];
span {
@apply text-sm #{!important};
}
}
}
}

至此轮播图的效果就实现了,在做轮播图的需求时,需要仔细认真地查看文档,我是比较喜欢看英文文档,我觉得讲述比较全,大家学习的时候自行选择即可。


作者:zhouzhouya
来源:juejin.cn/post/7298907435061100595
收起阅读 »

使用 promise 重构 Android 异步代码

背景 业务当中写Android异步任务一直是一项挑战,以往的回调和线程管理方式比较复杂和繁琐,造成代码难以维护和阅读。在前端领域中JavaScript其实也面临同样的问题,Promise 就是它的比较主流的一种解法。 在尝试使用Promise之前我们也针对An...
继续阅读 »

背景


业务当中写Android异步任务一直是一项挑战,以往的回调和线程管理方式比较复杂和繁琐,造成代码难以维护和阅读。在前端领域中JavaScript其实也面临同样的问题,Promise 就是它的比较主流的一种解法。 在尝试使用Promise之前我们也针对Android现有的一些异步做了详细的对比。


文章思维导图


image.png


What:什么是Promise?


对于Android开发的同学,可能很多人不太熟悉Promise,它主要是前端的实践,所以先解析概念。
Promise 是 JavaScript 语言提供的一种标准化的异步管理方式,它的总体思想是,需要进行 io、等待或者其它异步操作的函数,不返回真实结果,而返回一个“承诺”,函数的调用方可以在合适的时机,选择等待这个承诺兑现(通过 Promise 的 then 方法的回调)。


最简单例子(JavaScript)


const promise = new Promise(function(resolve, reject) {
// ... some code

if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
}).then(function(value) {
console.log('resolved.');
}).catch(function(error) {
console.log('发生错误!', error);
});


实例化一个Promise对象,构造函数接受一个函数作为参数,该参数分别是resolvereject


resolve函数:将Promise 对象状态从pending 变成 resolved


reject函数:将Promise 对象状态从 pending 变成 rejected


then函数:回调 resolved状态的结果
catch函数:回调 rejected状态的结果



可以看到Promise的状态是非常简单且清晰的,这也让它在实现异步编程减少很多认知负担。


Why:为什么要考虑引入Promise


前面说的Promise 不就是 JavaScript 异步编程的一种思想吗,那这跟 Android 开发有什么关系? 虽然前端终端领域有所不同,但面临的问题其实是大同小异的,比如常见的异步回调导致回调地狱,逻辑处理不连贯等问题。
从事Android开发的同学应该对以下异步编程场景比较熟悉:



  • 单个网络请求

  • 多个网络请求竞速

  • 等待多个异步任务返回结果

  • 异步任务回调

  • 超时处理

  • 定时轮询


这里可以停顿思考一下,如果利用 Android常规的方式去实现以上场景,你会怎么做?你的脑子可能有以下解决方案:



  • 使用 Thread 创建

  • 使用 Thread + Looper + Handler

  • 使用 Android 原生 AsyncTask

  • 使用 HandlerThread

  • 使用 IntentService

  • 使用 线程池

  • 使用 RxJava 框架


以上方案都能在Android中实现异步任务处理,但或多或少存在一些问题和适用场景,我们详细剖析下各自的优缺点:


image.png


通过不同的异步实现方式的对比,可以发现每种实现方式都有适用场景,我们面对业务复杂度也是不一样的,每一种解决方案都是为了降低业务复杂度,用更低成本的方式来编码,但我们也知道代码写出来是给人看的,是需要持续迭代和维护,类似RxJava 这种框架于我们而言太复杂了,繁琐的操作符容易写出不易维护的代码,简单易理解应该是更好的追求,而不是炫技,所以我们才会探索用更轻量更简洁的编码方式来提升团队的代码一致性,就目前而言使用 Promise 来写代码将会有以下好处:



  • 解决回调地狱:Promise 可以把一层层嵌套的 callback 变成  .then().then()... ,从而使代码编写和阅读更直观

  • 易于处理错误:Promise 比 callback 在错误处理上更清晰直观

  • 非常容易编写多个异步操作的代码


How:怎么使用 Promise 重构业务代码?


这里由于我们的Java版本的Promise组件未开源,所以本部分只分析重构Case使用案例。


重构case1: 如何实现一个带超时的网络接口请求?


这是一段未重构前的获取付款码的异步代码:




可以看到以上代码存在以下问题:



  • 需要定义异步回调接口

  • 很多 if-else 判断,圈复杂度较高

  • 业务实现了一个超时类,为了不受网络库默认超时影响

  • 逻辑不够连贯,不易于维护


使用 Promise重构后:




可以看到有以下变化:



  • 消除了异步回调接口,链式调用让逻辑更连贯更清晰了

  • 通过 Promise 包装了网络请求调用,统一返回 Promise

  • 指定了 Promise 超时时间,无需额外实现繁琐的超时逻辑

  • 通过 validate 方法 替代 if - else 的判断,如果需要还可以定义校验规则

  • 统一处理异常错误,逻辑变得更加完备


重构case2:如何更优雅的实现长链接降级短链接?


重构前的做法:


代码存在以下问题:



  • 处理长链接请求超时,通过回调再处理降级逻辑

  • 使用Handler实现定时器轮询请求异步结果并处理回调

  • 处理各种逻辑判断,代码难以维护

  • 不易于模拟超时降级,代码可测试性差


使用Promise重构后:




第一个Promise处理长链接Push监听 ,设置5s超时,超时异常发生回调except方法,判断throwable 类型,如果为PromiseTimeoutException实例对象,则执行降级短链接。短链接是另外一个Promise,通过这种方式将逻辑都完全结果,代码不会割裂,逻辑更连贯。
短链接轮训查单逻辑使用Promise实现:




  • 最外层Promise,控制整体的超时,即不管轮询的结果如何,超过限定时间直接给定失败结果

  • Promise.delay(),这个比较细节,我们认定500ms轮询一定不会返回结果,则通过延迟的方式来减少一次轮询请求

  • Promise.retry(),真正重试的逻辑,限定了最多重试次数和延时逻辑,RetryStrategy定义的是重试的策略,延迟(delay)多少和满足怎样的条件(condition)才允许重试


这段代码把复杂的延时、条件判断、重试策略都通过Promise这个框架实现了,少了很多临时变量,代码量更少,逻辑更清晰。


重构case3:实现 iLink Push支付消息和短链接轮训查单竞速


后面针对降级策略重构成竞速模型,采用Promise.any很轻松得实现代码重构,代码如下图所示。



总结


本文提供一种异步编程的思路,借鉴了Promise思想来重构了Android的异步代码。通过Promise组件提供的多种并发模型能够更优雅的解决绝大部分的场景需求。


防踩坑指南


如果跟Activity或Fragment生命周期绑定,需要在生命周期结束时,取消掉promise的线程运行,否则可能会有内存泄露;这里可以采用AbortController来实现更优雅的中断 Promise。


并发模型



● 多任务并行请求


Promise.all():接受任意个Promise对象,并发执行异步任务。全部任务成功,有一个失败则视为整体失败。


Promise.allSettled(): 任务优先,所有任务必须执行完毕,永远不会进入失败状态。


Promise.any():接受任意个Promise对象,并发执行异步任务。等待其中一个成功即为成功,全部任务失败则进入错误状态,输出错误列表。


● 多任务竞速场景


Promise.race(): 接受任意个Promise对象,并发执行异步任务。时间是第一优先级,多个任务以最先返回的那个结果为准,此结果成功即为整体成功,失败则为整体失败。



扩展思考



  1. Promise 最佳实践




  1. 避免过长的链式调用:虽然Promise可以通过链式调用来避免回调地狱,但是如果Promise的链过长,代码的可读性和维护性也会变差。

  2. 及时针对Promise进行abort操作:Promise使用不当可能会造成内存泄露,比如未调用abort,页面取消未及时销毁proimse。

  3. 需要处理except异常回调,处理PromiseException.

  4. 可以使用validation来实现规则校验,减少if-else的规则判断




  1. Java Promise 组件实现原理




  1. 状态机实现(pending、fulfilled、rejected)

  2. 默认使用 ForkJoinPool 线程池,适合计算密集型任务。针对阻塞IO类型,可以使用内置ThreadPerTaskExecutor 简单线程池模型。




  1. Promise vs Kotlin协程



Promise 链式调用,代码清晰,上手成本较低;底层实现仍然是线程,通过线程池管理线程调度
Koitlin 协程,更轻量的线程,使用比较灵活,可以由开发者控制,比如挂起和恢复




  1. 可测试性的思考



根据 Promise 的特点,可以通过Mock状态(resolve、reject、outTime)来实现模拟成功,拒绝、超时;
实现思路:
● 自定义注解类辅助定位Hook点
● 使用ASM字节码对Promise 进行代码插桩



附录


Promise - JavaScript | MDN


Promises/A+


欢迎关注我的公众号,一起进步~


qrcode_for_gh_f3c52aa46d49_430 (1).jpg


作者:巫山老妖
来源:juejin.cn/post/7298955315621789730
收起阅读 »

容易忽视前端点击劫持

web
有一句话叫做:你看到的,不一定是真的,但可能是想让你看到的。在学习前端之前,有幸学习过一阵子PPT和Flash设计,其中有一个知识点就是,视觉效果,最常用的套路就是使用遮罩层来制作效果,今天就聊聊基于遮罩导致的前端攻击:点击劫持。 前端点击劫持 前端点击劫持实...
继续阅读 »

有一句话叫做:你看到的,不一定是真的,但可能是想让你看到的。在学习前端之前,有幸学习过一阵子PPT和Flash设计,其中有一个知识点就是,视觉效果,最常用的套路就是使用遮罩层来制作效果,今天就聊聊基于遮罩导致的前端攻击:点击劫持。


前端点击劫持


前端点击劫持实际上就是通过层叠样式,在原有的页面样式上叠加自己的内容,然后通过色彩或者透明消除用户的警惕,当用户点击看到的功能的时候,实际上点击的是隐藏的功能,这样满足攻击者的需求。比如:


<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta http-equiv="X-UA-Compatible" content="IE=edge">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>前端劫持</title>
   <style>
       .myidea{
           width: 100%;
           height: 100%;
           position: absolute;
           top: 0;
           left: 0;
           opacity: 0.0;
      }
   
</style>
</head>
<body>
   <div>
      视觉效果展示内容
       <img src="F:\dazejiuzhang\图片资料\yqkfdx1.jpg">
   </div>
   <div class="myidea">
       <a href="http://www.jiece.com">跳转链接</a>
   </div>
</body>

</html>

视觉效果
image.png


但实际上


image.png


当然,这个案例当中只是采用了一个透明的div,在实际的劫持场景当中,更多的是采用复杂的iframe来覆盖。


前端点击劫持防御


知道原理之后,防御也就需要琢磨了:


1、在自己不使用iframe的前提下,对iframe进行限制,这个可以通过HTTP头进行设置X-Frame-Options属性可以实现,X-Frame-Options有三种模式:


属性描述
DENY浏览器拒绝解析当前页面任何frame;
SAMEORIGIN浏览器只解析当前页面同源的frame;
ALLOW-FROM origin浏览器只解析当前页面当中设置的origin的的frame;

配置在nginx当中设置:


add_header X-Frame-Options DENY;

2、检测所有提交的内容,防止通过前端XSS攻击携带前端点击劫持内容,这个和防御XSS攻击类似。


3、通过JS限制顶级的嵌套样视:


<head>
 <style id="click_box">
   html {
     display: none !important;
  }
 
</style>
</head>
<body>
 <script>
   if (self == top) {
     var style = document.getElementById('click_box')
     document.body.removeChild(style)
  } else {
     top.location = self.location
  }
 
</script>
</body>

思考


前端点击劫持本身攻击成本比XSS要高一点,并且容易被防御,但是,更容易出现在大家不注意,不经意的地方,这里忽然想到之前一个大佬聊的一句话:搞安全的最后,最难的就是要搞懂人心。“关于前端点击劫持就先聊这么多,还请各位大佬多多指点。


作者:老边
来源:juejin.cn/post/7228932662814769207
收起阅读 »

假如互联网人都很懂冒犯

大家好,我是老三,最近沉迷于听脱口秀,并且疯狂安利同事。 脱口秀演员常常说的一句话是:“脱口秀是冒犯的艺术”。最近我发现,同事们好像有点不一样了。 阳光灿烂的早上,趿拉着我的宝马拖鞋,跨上包浆的小黄车,屁股感受着阳光积累的炙热,往公司飞驰而去。 一步跨进电梯...
继续阅读 »

大家好,我是老三,最近沉迷于听脱口秀,并且疯狂安利同事。


脱口秀演员常常说的一句话是:“脱口秀是冒犯的艺术”。最近我发现,同事们好像有点不一样了。




阳光灿烂的早上,趿拉着我的宝马拖鞋,跨上包浆的小黄车,屁股感受着阳光积累的炙热,往公司飞驰而去。


一步跨进电梯间,我擦汗的动作凝固住了,挂上了矜持的微笑:“老板,早上好。”


老板:“早,你还在呢?又来带薪划水了?”


我:“嗨,我这再努力,最后不也就让你给我们多换几个嫂子嘛。”


老板:“没有哈哈,我开玩笑。”


我:“我也是,哈哈哈。”


今天的电梯似乎比往常慢了很多。


我:“老板最近在忙什么?”


老板:“昨天参加了一个峰会,马xx知道吧?他就坐我前边。”


我:“卧槽,真能装。没有,哈哈。”


老板:“哈哈哈”。


电梯到了,我俩都步履匆匆地进了公司。


小组内每天早上都有一个晨会,汇报工作进度和计划。


开了一会,转着椅子,划着朋友圈的我停了下来——到我了。


我:“昨天主要……今天计划……”


Leader:“你这不能说没有一点产出,也可以说一点产出都没有。其实,我对你是有一些失望的,原本今年绩效考评给你一个……”


我:“影响你合周报了是吗?不是哈哈。”


Leader、小组同事:“哈哈哈“。


Leader:“好了,我们这次顺便来对齐一下双月OKR,你们OKR都写的太保守了,一看就是能完成的,往大里吹啊。开玩笑哈哈。”。


我:”我以前就耕一亩田,现在把整个河北平原都给犁了。不是,哈哈。”


同事:“我要带公司打上月球,把你踢下来,我来当话事人。唉,哈哈”


Leader、同事、我:“哈哈哈“。


晨会开完,开始工作,产品经理拉我和和前端对需求。


产品经理:“你们程序员懂Java语言、Python语言、Go语言,就是不懂汉语言,真不想跟你们对需求。开个玩笑,哈哈。”


我:“没啥,你吹牛皮像狼,催进度像狗,做需求像羊,就这需求文档,还没擦屁股纸字多,没啥好对的。不是哈哈。”


产品经理、前端、我:“哈哈哈”。


产品经理:“那我们就对到这了,你们接着聊技术实现。”


前端:“没啥好聊的,后端大哥看着写吧,反正你们那破接口,套的比裹脚布还厚,没事还老出BUG。没有哈哈。”


我:“还不是为了兼容你们,一点动脑子的逻辑都不写,天天切图当然不出错。不是哈哈。”


前端、我:“哈哈哈”。


经过一番拉扯之后,我终于开始写代码了。


看到一段代码,我皱起了眉头,同事写的,我顺手写下了这样一段注释:


/**
* 写这段代码的人,建议在脑袋开个口,把水倒掉。不是哈哈,开个玩笑。
**/


代码写完了,准备上线,找同事给我Review,同事看了一会,给出了评论。



又在背着我们偷偷写烂代码了,建议改行。没有哈哈。



同事、我:“哈哈哈”。


终于下班了,路过门口,HR小姐姐还在加班。


我:“小姐姐怎么还没下班?别装了,老板都走了。开玩笑哈哈。”


HR小姐姐:“这不是看看怎么优化你们嘛,任务比较重。不是,哈哈。”


HR小姐姐、我:“哈哈哈”。


我感觉到一种不一样的氛围在公司慢慢弥散开来,我不知道怎么形容,但我想到了一句话——


“既分高下,也决生死”。




写这篇的时候,想到两年前,有个叫码农小说家的作者横空出世,写了一些生动活泼、灵气十足的段子,我也跟风写了两篇,这就是“荒腔走板”系列的来源。


后来,他结婚了。


看(抄)不到的我只能自己想,想破头也写不不来像样的段子,这个系列就不了了之,今天又偶尔来了灵感,写下一篇,也顺带缅怀一下光哥带来的快乐。


作者:三分恶
来源:juejin.cn/post/7259036373579350077
收起阅读 »

Android电量优化,让你的手机续航更持久

节能减排,从我做起。一款Android应用如果非常耗电,是一定会被主人嫌弃的。自从Android手机的主人用了你开发的app,一天下来,也没干啥事,电就没了。那么他就会想尽办法找出耗电量杀手,当他找出后,很有可能你开发的app就被无情的卸载了。为了避免这种事情...
继续阅读 »

节能减排,从我做起。一款Android应用如果非常耗电,是一定会被主人嫌弃的。自从Android手机的主人用了你开发的app,一天下来,也没干啥事,电就没了。那么他就会想尽办法找出耗电量杀手,当他找出后,很有可能你开发的app就被无情的卸载了。为了避免这种事情发生,我们就要想想办法让我们的应用不那么耗电,电都用在该用的时候和地方。


通过power_profile.xml查看各个手机硬件的耗电量


Google要求手机硬件生产商都要放入power_profile.xml文件到ROM里面。有些不太负责的手机生产商,就乱配,也没有真正测试过。但我们还是可以大概知道耗电的硬件都有哪些。


先从ibotpeaches.github.io/Apktool/ 下载apktool反编译工具,然后执行adb命令,将手机framework的资源apk拉取出来。


adb pull /system/framework/framework-res.apk ./

然后我们用下载好的反编译工具,将framework-res.apk进行反编译。


java -jar apktool_2.7.0.jar d framework-res.apk

apktool_2.7.0.jar换成你下载的具体的jar包名称。
power_profile.xml文件的目录如下:



framework-res/res/xml/power_profile.xml



<?xml version="1.0" encoding="utf-8"?>
<device name="Android">
<item name="ambient.on">0.1</item>
<item name="screen.on">0.1</item>
<item name="screen.full">0.1</item>
<item name="bluetooth.active">0.1</item>
<item name="bluetooth.on">0.1</item>
<item name="wifi.on">0.1</item>
<item name="wifi.active">0.1</item>
<item name="wifi.scan">0.1</item>
<item name="audio">0.1</item>
<item name="video">0.1</item>
<item name="camera.flashlight">0.1</item>
<item name="camera.avg">0.1</item>
<item name="gps.on">0.1</item>
<item name="radio.active">0.1</item>
<item name="radio.scanning">0.1</item>
<array name="radio.on">
<value>0.2</value>
<value>0.1</value>
</array>
<array name="cpu.active">
<value>0.1</value>
</array>
<array name="cpu.clusters.cores">
<value>1</value>
</array>
<array name="cpu.speeds.cluster0">
<value>400000</value>
</array>
<array name="cpu.active.cluster0">
<value>0.1</value>
</array>
<item name="cpu.idle">0.1</item>
<array name="memory.bandwidths">
<value>22.7</value>
</array>
<item name="battery.capacity">1000</item>
<item name="wifi.controller.idle">0</item>
<item name="wifi.controller.rx">0</item>
<item name="wifi.controller.tx">0</item>
<array name="wifi.controller.tx_levels" />
<item name="wifi.controller.voltage">0</item>
<array name="wifi.batchedscan">
<value>.0002</value>
<value>.002</value>
<value>.02</value>
<value>.2</value>
<value>2</value>
</array>
<item name="modem.controller.sleep">0</item>
<item name="modem.controller.idle">0</item>
<item name="modem.controller.rx">0</item>
<array name="modem.controller.tx">
<value>0</value>
<value>0</value>
<value>0</value>
<value>0</value>
<value>0</value>
</array>
<item name="modem.controller.voltage">0</item>
<array name="gps.signalqualitybased">
<value>0</value>
<value>0</value>
</array>
<item name="gps.voltage">0</item>
</device>

抓到不负责任的手机生产商一枚,好家伙,这么多0.1,明眼人一看就知道这是为了应付Google。尽管这样,我们还是可以从中知道,耗电的有Screen(屏幕亮屏)、Bluetooth(蓝牙)、Wi-Fi(无线局域网)、Audio(音频播放)、Video(视频播放)、Radio(蜂窝数据网络)、Camera的Flashlight(相机闪光灯)和GPS(全球定位系统)等。


电量杀手简介


Screen


屏幕是非常耗电的一个硬件,不要问我为什么。屏幕主要有LCD和OLED两种。LCD屏幕白色光线从屏幕背后的灯管发出,尽管屏幕显示黑屏,依旧耗电,这种屏幕逐渐被淘汰,如果你翻出个早点的功能机,或许能看到。那么大部分Android手机都是OLED的屏幕,每个像素点都是独立的发光单元,屏幕黑屏时,所有像素都不发光。有必要时,让屏幕息屏很重要,当然手机也有自动息屏的时间设置,这个不太需要我们操心。


Radio数据网络和Wi-Fi无线网络


网络也是非常耗电的,其中又以数据网络的耗电更多于Wi-Fi的耗电。所以请尽量引导用户使用Wi-Fi网络使用app的部分功能,比如下载文件。


GPS


GPS也是很耗电的硬件,所以不要动不动就请求地理位置,GPS平常是要关闭的,除非你在使用定位和导航等功能,这样你的手机续航会更好。


WakeLock


如果使用了WakeLock,是可以有效防止息屏情况下的CPU休眠,但是如果不用了,你不释放掉锁的话,则会带来很大的电量的开销。


查看手机耗电的历史记录


// 上次拔掉电源到现在的耗电情况
adb shell dumpsys batterystats --unplugged

你在逗我?让我看命令行的输出?后面我们来使用Battery Historian的图表进行分析。


使用Battery Historian分析手机耗电量


安装Docker


Docker下载网址 docs.docker.com/desktop/ins…


使用Docker容器编排


docker run -p 9999:9999 gcr.io/android-battery-historian/stable:3.0 --port 9999

获取bugreport文件


Android7.0及以上


adb bugreport bugreport.zip

Android6.0及以下


adb bugreport > bugreport.txt

上传bugreport文件进行分析


在浏览器地址栏输入http://localhost:9999
截屏2023-02-05 05.39.12.png
点击Browse按钮并上传bugreport.zip或bugreport.txt生成分析图表。
截屏2023-02-05 05.44.59.png
我们可以通过时间轴来分析应用当下的电池使用情况,比较耗电的是哪部分硬件。


使用JobScheduler来合理执行后台任务


JobScheduler是Android5.0版本推出的API,允许开发者在符合某些条件时创建执行在后台的任务。比如接通电源的情况下才执行某些耗电量大的操作,也可以把一些不紧急的任务在合适的时候批量处理,还可以避开低电量的情况下执行某些任务。


作者:dora
来源:juejin.cn/post/7196321890301575226
收起阅读 »

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

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

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


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


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


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


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


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


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


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


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



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


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


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


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



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


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


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



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


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


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


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


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


总结


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


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


作者:ali老蒋
来源:juejin.cn/post/7257784425044705340
收起阅读 »

告诉你为什么视频广告点不了关闭

前言 我们平时玩游戏多多少少会碰到一些视频广告,看完后是能领取游戏奖励的,然后你会发现有时候看完点击那个关闭按钮,结果是跳下载,你理所当然的认为是点击到了外边,事实真的是这样的吗?有些东西不好那啥,你们懂的,所以以下内容纯属我个人猜测,纯属虚构 1. 整个广告...
继续阅读 »

前言


我们平时玩游戏多多少少会碰到一些视频广告,看完后是能领取游戏奖励的,然后你会发现有时候看完点击那个关闭按钮,结果是跳下载,你理所当然的认为是点击到了外边,事实真的是这样的吗?有些东西不好那啥,你们懂的,所以以下内容纯属我个人猜测,纯属虚构


1. 整个广告流程的各个角色


要想对广告这东西有个大概的了解,你得先知道你看广告的过程中都有哪些角色参与了进来。


简单来说,是有三方参与了进来:

(1)广告提供商:顾名思义负责提供广告,比如你看的广告是一款游戏的广告,那这个游戏的公司就是广告的提供商。

(2)当前应用:就是播放这个广告的应用。

(3)平台:播放广告这个操作就是平台负责的,它负责连接上面两方,从广告提供商中拿到广告,然后让当前应用接入。


平台有很多,比如字节、腾讯都有相对应的广告平台,或者一些小公司自己做广告平台。他们之间的py交易是这样的:所有广告的功能是由平台去开发,然后他会提供一套sdk或者什么的让应用接入,应用你接入之后每播放1次广告,平台就给你多少钱,但是播放的是什么广告,这个就是平台自己去下发。然后广告提供商就找到平台,和他谈商业合作,你帮我展示我家的产品的广告多少次,我给你多少钱。 简单来说他们之间的交易就是这样。


简单来说,就是广告提供商想要影响力,其它两方要钱,他们都希望广告能更多的展示。


2. 广告提供商的操作


广告提供商是花钱让平台推广广告的,那我肯定是希望尽量每次广告的展示都有用户去点击然后下载我们家的应用。


所以广告提供商想出了一个很坏的办法,相信大家都遇到过,就是我播放视频,我在视频的最后几帧里面的图片的右上角放一个关闭图片,误导用户这个关闭图片是点了之后能关闭的,其实它是视频的一部分,所以你点了就相当于点了视频,那就触发跳转下载应用这些操作。


破解的方法也很简单,你等到计算结束后的几秒再点关闭按钮,不要一看到关闭按钮的图片出来马上点。


3. 应用的操作


应用是很难在广告播放的时候去做手脚,因为这部分的代码不是他们写的,他们只是去调用平台写的代码。


那他们想让广告尽可能多的展示,唯一能做的就是把展示广告的地方增加,尽可能多的让更多场景能展示广告。当然这也有副作用,你要是这个应用点哪里都是广告,这不得把用户给搞吐了,砸了自己的口碑,如果只有一些地方有,用户还是能理解的,毕竟赚钱嘛,不寒参。


4. 平台的操作


平台的操作那就丰富了,代码是我写的,兄弟,我想怎么玩就怎么玩,我能有一百种方法算计你。


猜测的,注意,是猜测的[狗头]


有的人说,故意把关闭按钮设置小,让我们误触关闭按钮以外的区域。我只能说,你让我来做,我都不屑于把关闭按钮设置小。


我们都知道平时开发时,我们觉得点击按钮不灵,所以我们想扩大图标的点击区域,但是又不想改变图标的大小,所以我们用padding来实现。同样的,我也能做到不改变图标的大小,然后缩小点击的范围


我写一个自定义view(假设就是关闭图标)


public class TestV extends View {

public TestV(Context context) {
super(context);
}

public TestV(Context context, AttributeSet attrs) {
super(context, attrs);
}

public TestV(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
int w = getMeasuredWidth();
int h = getMeasuredHeight();
Log.d("mmp", "============ view点击");
if (event.getX() < w / 4 || event.getX() > 3 * w / 4 || event.getY() < h / 4 || event.getY() > 3 * h / 4) {
return super.dispatchTouchEvent(event);
} else {
Log.d("mmp", "============ view点击触发-》关闭");
return true;
}
}
return super.dispatchTouchEvent(event);
}
}

代码很简单就不过多讲解,能看出我很简单就实现让点击范围缩小1/4。所以当你点到边缘的时候,其实就相当于点到了广告。


除了缩小范围之外,我还能设置2秒前后点击是不同的效果,你有没有一种感觉,第一次点关闭按钮就是跳到下载应用,然后返回再点击就是关闭,所以你觉得是你第一次点击的时候是误触了外边。


public class TestV extends View {

private boolean canClose = true;

public TestV(Context context) {
super(context);
}

public TestV(Context context, AttributeSet attrs) {
super(context, attrs);
}

public TestV(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

@Override
public void setVisibility(int visibility) {
super.setVisibility(visibility);
if (visibility == View.VISIBLE) {
canClose = false;
}
}

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
int w = getMeasuredWidth();
int h = getMeasuredHeight();
Log.d("mmp", "============ view点击");
if (!canClose) {
return super.dispatchTouchEvent(event);
} else {
Log.d("mmp", "============ view点击触发-》关闭");
return true;
}
}
return super.dispatchTouchEvent(event);
}

// 播放完成
public void playFinish() {
setVisibility(VISIBLE);
Handler handler = new Handler(Looper.getMainLooper());
handler.postDelayed(new Runnable() {
@Override
public void run() {
canClose = true;
}
}, 2000);
}

}

播放完成之后调用playFinish方法,然后会把canClose这个状态设置为false,2秒后再设为true。这样你在2秒前点击按钮,其实就是点击外部的效果,也就会跳去下载。


而且你注意,这些限制,我可以不写死在代码里面,可以用后台返回,比如这个2000,我可以后台返回。我就能做到比如第一天我返回0,你觉得没什么问题,能正常点关闭,我判断你是第二天,我直接返2000给你,然后你一想,之前都是正常的,那这次不正常,肯定是我点错。


你以为的意外只不过是我想让你以为是意外罢了。那这个如何去破解呢?我只能说无解,他能有100种方法让你点中跳出去下载,那还能有是什么解法?


作者:流浪汉kylin
来源:juejin.cn/post/7197611189244592186
收起阅读 »

自定义View模仿即刻点赞数字切换效果

即刻点赞展示 点赞的数字增加和减少并不是整个替换,而是差异化替换。再加上动画效果就看的很舒服。 自己如何实现这种数字切换呢? 下面用一张图来展示我的思路: 现在只需要根据这张图,写出对应的动画即可。 分为2种场景: 数字+1: 差异化的数字从3号区域由...
继续阅读 »

即刻点赞展示


like.gif


点赞的数字增加和减少并不是整个替换,而是差异化替换。再加上动画效果就看的很舒服。


自己如何实现这种数字切换呢?


下面用一张图来展示我的思路:


number_dance.png


现在只需要根据这张图,写出对应的动画即可。
分为2种场景:



  • 数字+1:

    • 差异化的数字从3号区域由渐变动画(透明度 0- 255) + 偏移动画 (3号区域绘制文字的基线,2号区域绘制文字的基线),将数字移动到2号位置处

    • 差异化的数字从2号区域由渐变动画(透明度 255- 0) + 偏移动画(2号区域绘制文字的基线,1号区域绘制文字的基线),将数字移动到1号位置处



  • 数字-1

    • 差异化的数字从1号区域由渐变动画(透明度 0- 255) + 偏移动画 (1号区域绘制文字的基线,2号区域绘制文字的基线),将数字移动到2号位置处

    • 差异化的数字从2号区域由渐变动画(透明度 255- 0) + 偏移动画(2号区域绘制文字的基线,3号区域绘制文字的基线),将数字移动到3号位置处




公共部分就是:
不变的文字不需要做任何处理,绘制在2号区域就行。绘制差异化文字时,需要加上不变的文字的宽度就行。


效果展示


show-gif.gif


源码


class LikeView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

private val paint = Paint().also {
it.isAntiAlias = true
it.textSize = 200f
}

private val textRect0 = Rect(300, 100, 800, 300)
private val textRect1 = Rect(300, 300, 800, 500)
private val textRect2 = Rect(300, 500, 800, 700)

private var nextNumberAlpha: Int = 0
set(value) {
field = value
invalidate()
}

private var currentNumberAlpha: Int = 255
set(value) {
field = value
invalidate()
}

private var offsetPercent = 0f
set(value) {
field = value
invalidate()
}

private val fontMetrics: FontMetrics = paint.fontMetrics
private var currentNumber = 99
private var nextNumber = 0
private var motionLess = currentNumber.toString()
private var currentMotion = ""
private var nextMotion = ""

private val animator: ObjectAnimator by lazy {
val nextNumberAlphaAnimator = PropertyValuesHolder.ofInt("nextNumberAlpha", 0, 255)
val offsetPercentAnimator = PropertyValuesHolder.ofFloat("offsetPercent", 0f, 1f)
val currentNumberAlphaAnimator = PropertyValuesHolder.ofInt("currentNumberAlpha", 255, 0)
val animator = ObjectAnimator.ofPropertyValuesHolder(
this,
nextNumberAlphaAnimator,
offsetPercentAnimator,
currentNumberAlphaAnimator
)
animator.duration = 200
animator.interpolator = DecelerateInterpolator()
animator.addListener(
onEnd = {
currentNumber = nextNumber
}
)
animator
}

override fun onDraw(canvas: Canvas) {
paint.alpha = 255

paint.color = Color.LTGRAY
canvas.drawRect(textRect0, paint)

paint.color = Color.RED
canvas.drawRect(textRect1, paint)

paint.color = Color.GREEN
canvas.drawRect(textRect2, paint)

paint.color = Color.BLACK
if (motionLess.isNotEmpty()) {
drawText(canvas, motionLess, textRect1, 0f)
}

if (nextMotion.isEmpty() || currentMotion.isEmpty()) {
return
}

val textHorizontalOffset =
if (motionLess.isNotEmpty()) paint.measureText(motionLess) else 0f
if (nextNumber > currentNumber) {
paint.alpha = currentNumberAlpha
drawText(canvas, currentMotion, textRect1, textHorizontalOffset, -offsetPercent)
paint.alpha = nextNumberAlpha
drawText(canvas, nextMotion, textRect2, textHorizontalOffset, -offsetPercent)
} else {
paint.alpha = nextNumberAlpha
drawText(canvas, nextMotion, textRect0, textHorizontalOffset, offsetPercent)
paint.alpha = currentNumberAlpha
drawText(canvas, currentMotion, textRect1, textHorizontalOffset, offsetPercent)
}
}

private fun drawText(
canvas: Canvas,
text: String,
rect: Rect,
textHorizontalOffset: Float = 0f,
offsetPercent: Float = 0f
) {
canvas.drawText(
text,
rect.left.toFloat() + textHorizontalOffset,
rect.top + (rect.bottom - rect.top) / 2f - (fontMetrics.bottom + fontMetrics.top) / 2f + offsetPercent * 200,
paint
)
}


override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
animator.end()
}

fun plus() {
if (currentNumber == Int.MAX_VALUE) {
return
}
nextNumber = currentNumber + 1

processText(findEqualsStringIndex())

if (animator.isRunning) {
return
}
animator.start()
}

fun minus() {
if (currentNumber == 0) {
return
}
nextNumber = currentNumber - 1
processText(findEqualsStringIndex())
if (animator.isRunning) {
return
}
animator.start()
}

private fun findEqualsStringIndex(): Int {
var equalIndex = -1
val nextNumberStr = nextNumber.toString()
val currentNumberStr = currentNumber.toString()

val endIndex = min(currentNumberStr.length, nextNumberStr.length) - 1

for (index in 0..endIndex) {
if (nextNumberStr[index] != currentNumberStr[index]) {
break
}
equalIndex = index
}
return equalIndex
}

private fun processText(index: Int) {
val currentNumberStr = currentNumber.toString()
val nextNumberStr = nextNumber.toString()
if (index == -1) {
motionLess = ""
currentMotion = currentNumberStr
nextMotion = nextNumberStr
} else {
motionLess = currentNumberStr.substring(0, index + 1)
currentMotion = currentNumberStr.substring(index + 1)
nextMotion = nextNumberStr.substring(index + 1)
}
}
}

作者:timer
来源:juejin.cn/post/7179181214530551867
收起阅读 »

简单清理掉项目中没用的180+文件

web
遇到的痛点 这篇文章或许有另一个不太优雅的名字--“屎山治理”。 在繁重的业务开发当中,我们会面临一些问题。伴随着项目的不断发展,项目出现代码冗余,存在大片没用代码的情况。 举个栗子,重构优化后,某位同学没有删除掉冗余代码,项目残留着废弃的没用文件,导致跨文件...
继续阅读 »

遇到的痛点


这篇文章或许有另一个不太优雅的名字--“屎山治理”。


在繁重的业务开发当中,我们会面临一些问题。伴随着项目的不断发展,项目出现代码冗余,存在大片没用代码的情况。


举个栗子,重构优化后,某位同学没有删除掉冗余代码,项目残留着废弃的没用文件,导致跨文件引用混乱。还有,业务变更所导致逻辑代码的废弃,项目中重复的定义代码,这些情况在一个长期的项目发展的阶段里面会造成逻辑混乱,重复定义,二义性等等。


其实,程序员都是写代码的,但是很少人敢删代码,久而久之,也就没人敢动废弃代码了。


虽然在项目构建工具的加持下,tree-shaking能够控制项目的包产物体积,但是从开发体验(DX)的角度出发,这往往都是一些心智负担。结合我自己的一些优化经验,简单分享一下:


优化手段


手段一:eslint的unused检查


首先我们应该考虑的是,通过 eslint 的规则有效的去规避一些项目当中已有的没用的变量和方法,这样保证单文件代码的可用性,我们可以很容易的发现哪个import或者variable没有被使用。import的冗余控制也能够有效控制打包的范围,减少包体积。


eslint最常用的就是官方的no-unused-vars这一条规则。


当然还有一些,第三方的unused-exports规则,例如eslint-plugin-canonical的no-unused-exports或者eslint-plugin-unused-imports,这种大家可以适度采用,毕竟eslint是把“双刃剑”。


手段二:静态代码工具扫描


通过一些静态分析工具可以有效地分析代码语法,根据语法树信息来判断内容是有用还是没用。


ts-unused-exports是一个很成熟的分析工具,它可以通过 ts-compiler 对 typescript代码语法进行分析,(tsconfig可以配置allowjs,分析javascript语法),通过TS语法树有效地找到语法中没用的 export。


该工具能够把所有的没用的 export 找到。这时候我们会很自然地想到一个问题,能否找到完全没有使用的废弃文件。这里分两种情况,情况一,该文件所有的 export 都已经被废弃了,这种情况出现在代码重构的情况,另外一种情况是部分的export没有被使用,那这种需要case by case的判断,到底这个代码有没有存在意义?


暂时这个工具只能找到所有的 export 函数,并没有文件粒度,并不能满足我们的“诉求”。我们希望能把完全没用的文件直接删除掉,所以我提了一个issue。


找出所有 export 的文件


我查看了源码,parse过后,会通过getExportMap获取每个文件,且它的所有exports内容。我写了一个PR,在和作者沟通交流下,尽量以最小的 api 改动情况来处理。利用一个参数findCompletelyUnusedFiles来控制是否找出完全没有被使用的文件,参考PR#254


PR 细节


改动涉及最核心内容,如下。将该文件的真实所有 export 和 unused export 作对比,以此判断它是完全没用的文件。


const realExportNames = Object.keys(exports);

if (
extraOptions?.findCompletelyUnusedFiles &&
isEqual(realExportNames, unusedExports)
) {
unusedFiles.push(path);
}
});

当我们得到了这个结果后,我们可以通过自己编写的脚本“大胆”的删除文件了。


在删除脚本内,我们要想清楚几个事情:



  1. 有范围的扫描(避免错删,所有改动在可控的范围内)

  2. 后缀名白名单(多市场的代码可能会存在“多态”,例如,id代表印尼,index.id.ts它不应该被清除掉)


const result = analyzeTsConfig('./tsconfig.json', ['--findCompletelyUnusedFiles']);

const outputPattern = ['/pages/partner/', '/pages/store/', '/pages/staff/', '/services/'];
const excludePattern = ['.id.', '.my.', '.ph.', '.sg.', '.vn.', '.th.', '.br.'];

function filterOutput(name: string) {
for (let index = 0; index < outputPattern.length; index++) {
if (name.includes(outputPattern[index]) ) {
return true;
}
}
return false;
}

function filterExclude(name: string) {
for (let index = 0; index < excludePattern.length; index++) {
if (name.includes(excludePattern[index]) ) {
return false;
}
}
return true;
}

const { unusedFiles, ...rest } = result;

Object.keys(rest)
.filter(r => filterOutput(r))
.filter(r => filterExclude(r))
.map((key) => {
const exportNames = rest[key].map(r=> r.exportName).join(',')
console.log(chalk.green(key) + ' ' + exportNames);
})

if(result.unusedFiles) {
console.log('no used files: ');
result.unusedFiles
.filter(r => filterOutput(r))
.filter(r => filterExclude(r))
.forEach((r) => {
fs.unlinkSync(r);
})
}

手段三:人工调整已有代码的合理性


在删除完代码后,项目中 ts-unused-export 还会扫描出一些部分 export 废弃的文件,我们只能按照自身的情况做出调整。每个团队的代码分层情况有所不同。这些文件可能不需要改动,也可能是需要调整该纯函数位置。我们应该把它们放在合理的位置。


代码优化


总结


首先“清除废弃代码”是一个低频操作。可能我们一年或者几年,清理一次即可,保证代码的“清爽”。所以放在 webpack 等构建工具执行反而不太合适,脚本偶尔扫描,把一些废弃代码清干净,你的DX(developing experience)又回来了。


当然你忍受能力很强也可以“不做”。这篇文章适合具有轻度“代码强迫症”的同学食用。


PS:加餐,也可以参考knip,功能更强大噢。


作者:brandonxiang
来源:juejin.cn/post/7298918307746267174
收起阅读 »

关于我很不情愿地帮一个破电脑优化了首屏时间75%这件事

web
前言 大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~ 背景 最近我们公司有一个页面,这个页面其实结构很简单,就是三个列表同时出现在某个项目的首页里,大概页面是这样的,差不多每一个列表都有100-1000条数据吧~数...
继续阅读 »

前言


大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~


背景


最近我们公司有一个页面,这个页面其实结构很简单,就是三个列表同时出现在某个项目的首页里,大概页面是这样的,差不多每一个列表都有100-1000条数据吧~数据量也不算多,三个列表顶天就3000条数据,并且数据也不复杂,所以也没做什么处理



开发阶段没发现什么问题,但是上了生产后,问题来了,很多“用户”反馈说这个首屏加载超级慢,这个项目是公司内部使用的,“用户”都是公司内部某个部门的员工们,由于他们的公司电脑配置比较差,所以这点数据量就足够他们电脑吃不消了,所以导致了首屏加载会非常慢~~有多慢呢?最慢的居然达到 8s。。。




  • Scripting: JavaScript逻辑处理时间

  • Rendering: UI渲染时间


有人会问,不应该是数据量多导致渲染慢吗?那为啥主要耗时都在 Scripting 呢?


那是因为 Vue3 在渲染前会进行一系列的 JavaScript 逻辑处理,包括:



  • 1、数据创建

  • 2、模板解析

  • 3、虚拟DOM创建

  • 4、虚拟DOM转真实DOM


不过最耗时的肯定就是后两步了,所以需要针对这个问题,做一个解决方案


页面代码先贴一下



懒加载分页?虚拟滚动?不行!


很多人第一想象到的解决方案就是做分页,或者做懒加载,其实分页和懒加载都是一个道理,就是按需去加载数据,但是不好意思,后端接口是老接口并且没有做分页,团队不想要耗费后端精力去重构这个接口,所以这条路就别想啦!!!


又有人说虚拟滚动,我这么说吧,虚拟滚动用在性能好的电脑上效果是很不错的,如果用在性能差的电脑上,那么效果会非常糟糕,毫无疑问虚拟滚动确实会减少首屏加载的时间,但是性能差的电脑滚动快了,会有白屏现象,而且很严重,熟悉虚拟滚动的人都知道,虚拟滚动是根据滚动时间去重新计算渲染区域的,这个计算时需要时间的,但是用户滚动是很快的,性能差的电脑根本计算不过来所以导致会有短暂白屏现象。。


又有人说虚拟滚动不是可以做预加载吗?可以解决短暂白屏现象。还是那句话,在性能好的电脑上确实可以,但是性能差的电脑上,你预渲染再多也没用,该白屏还是得白屏



分片渲染


不能做分页,不能做懒加载,不能做虚拟滚动,那么咋办呢?我还是选择了分片渲染来进行渲染,也就是在浏览器渲染的每一帧中去不断渲染列表数据,一直到渲染出整个列表数据为止。


这样做就能保证首屏时不会一股脑把整个列表都渲染出来了,而是先进首页后,再慢慢把所有列表都渲染完成



实施


要怎么才能保证在每一个帧中去控制列表渲染呢?可以使用requestAnimationFrame,我们先封装一个useDefer



  • frame: 记录当前的帧数

  • checkIsRender: 拿列表每一项的索引去跟当前帧数比较,到达了指定索引帧数才渲染这一项



页面里直接使用这个 hooks 即可




这样就能保证了达到一定帧数时,才去渲染一定的列表数据,我们来看看效果,可以看到首屏快了很多,从8s -> 2s,因为首屏并不会一股脑加载所有数据,而是逐步加载,这一点看滚动条的变化就知道了~



滚动条一直在变,因为数据在不断逐步渲染



已经尽力了,实在不行这边劝你还是换电脑吧~



优化点


我们在完成一个功能后,可以看出这个功能有什么



  • 列表渲染完毕后,可以停止当前帧的计算

  • 现在是一帧渲染一条数据,能否控制一帧渲染的多条数据?




作者:Sunshine_Lin
来源:juejin.cn/post/7298646156426526754
收起阅读 »

自定义view实战(12):安卓粒子线条效果

自定义view实战(12):安卓粒子线条效果 前言 很久没写代码了,忙工作、忙朋友、人也懒了,最近重新调整自己,对技术还是要有热情,要热情的话还是用自定义view做游戏有趣,写完这个粒子线条后面我会更新几个小游戏博文及代码,希望读者喜欢。 这个粒子效果的控件是...
继续阅读 »

自定义view实战(12):安卓粒子线条效果


前言


很久没写代码了,忙工作、忙朋友、人也懒了,最近重新调整自己,对技术还是要有热情,要热情的话还是用自定义view做游戏有趣,写完这个粒子线条后面我会更新几个小游戏博文及代码,希望读者喜欢。


这个粒子效果的控件是去年写的,写的很差劲,这几天又重构了一下,还是难看的要命,勉强记录下吧。


需求


主要就是看到博客园的粒子线条背景很有意思,就想模仿一下。核心思想如下:



  • 1、随机出现点

  • 2、范围内的点连线

  • 3、手指按下,加入点,范围内点向手指移动


效果图


效果图就是难看,没得说。
在这里插入图片描述


代码


import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import java.lang.ref.WeakReference
import kotlin.math.pow
import kotlin.math.sqrt

/**
* 模仿博客粒子线条的view
*
* 核心思想简易版
*
* 1、随机出现点
* 2、范围内的点连线
* 3、手指按下,加入点,范围内点向手指移动
*
* @author silence
* @date 2022-11-09
*
*/

class ParticleLinesBgView @JvmOverloads constructor(
context: Context,
attributeSet: AttributeSet? = null,
defStyleAttr: Int = 0
): View(context, attributeSet, defStyleAttr){

companion object{
// 屏幕刷新时间,每秒20次
const val SCREEN_FLUSH_TIME = 50L

// 新增点的间隔时间
const val POINT_ADD_TIME = 200L

// 粒子存活时间
const val POINT_ALIVE_TIME = 18000L

// 吸引的合适距离
const val ATTRACT_LENGTH = 250f

// 维持的合适距离
const val PROPER_LENGTH = 150f

// 粒子被吸引每次接近的距离
const val POINT_MOVE_LENGTH = 30f

// 距离计算公式
fun getDistance(x1: Float, y1: Float, x2: Float, y2: Float): Float {
return sqrt(((x1 - x2).toDouble().pow(2.0)
+ (y1 - y2).toDouble().pow(2.0)).toFloat())
}
}

// 存放的粒子
private val mParticles = ArrayList<Particle>(64)

// 手指按下位置
private var mTouchParticle: Particle? = null

// 处理的handler
private val mHandler = ParticleHandler(this)

// 画笔
private val mPaint = Paint().apply {
color = Color.LTGRAY
strokeWidth = 3f
style = Paint.Style.STROKE
flags = Paint.ANTI_ALIAS_FLAG
}

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
// 通过发送消息给handler实现间隔添加点
mHandler.removeMessages(0)
mHandler.sendEmptyMessageDelayed(0, POINT_ADD_TIME)
}

override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 绘制点和线
for (i in 0 until mParticles.size) {
val point = mParticles[i]
canvas.drawPoint(point.x, point.y, mPaint)
// 连线
for (j in (i + 1) until mParticles.size) {
val another = mParticles[j]
val distance = getDistance(point.x, point.y, another.x, another.y)
if (distance <= PROPER_LENGTH) {
canvas.drawLine(point.x, point.y, another.x, another.y, mPaint)
}
}
}

mTouchParticle?.let {
// 手指按下点与附近连线
for(point in mParticles) {
val distance = getDistance(point.x, point.y, it.x, it.y)
if (distance <= PROPER_LENGTH) {
canvas.drawLine(point.x, point.y, it.x, it.y, mPaint)
}
}

// 吸引范围显示
mPaint.color = Color.BLUE
canvas.drawCircle(it.x, it.y, PROPER_LENGTH, mPaint)
mPaint.color = Color.LTGRAY
}
}


@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
when(event.action) {
MotionEvent.ACTION_DOWN -> {
mTouchParticle = Particle(event.x, event.y, 0)
}
MotionEvent.ACTION_MOVE -> {
mTouchParticle!!.x = event.x
mTouchParticle!!.y = event.y
invalidate()
}
MotionEvent.ACTION_UP -> {
mTouchParticle = null
}
}
return true
}

// 粒子
class Particle(var x: Float, var y: Float, var counter: Int)

// kotlin自动编译为Java静态类,控件引用使用弱引用
class ParticleHandler(view: ParticleLinesBgView): Handler(Looper.getMainLooper()){
// 控件引用
private val mRef: WeakReference<ParticleLinesBgView> = WeakReference(view)
// 粒子出现控制
private var mPointCounter = 0

override fun handleMessage(msg: Message) {
mRef.get()?.let {view->
// 新增点
mPointCounter++
if (mPointCounter == (POINT_ADD_TIME / SCREEN_FLUSH_TIME).toInt()) {
// 随机位置
val x = (Math.random() * view.width).toFloat()
val y = (Math.random() * view.height).toFloat()
view.mParticles.add(Particle(x, y, 0))
mPointCounter = 0
}

val iterator = view.mParticles.iterator()
while (iterator.hasNext()) {
val point = iterator.next()

// 移除失活粒子
if (point.counter == (POINT_ALIVE_TIME / SCREEN_FLUSH_TIME).toInt()) {
iterator.remove()
}

// 手指按下时,粒子朝合适的距离移动
view.mTouchParticle?.let {
val distance = getDistance(point.x, point.y, it.x, it.y)
if(distance in PROPER_LENGTH..ATTRACT_LENGTH) {
// 横向接近
if (point.x < it.x) point.x += POINT_MOVE_LENGTH
else point.x -= POINT_MOVE_LENGTH
// 纵向接近
if (point.y < it.y) point.y += POINT_MOVE_LENGTH
else point.y -= POINT_MOVE_LENGTH
}else if(distance <= PROPER_LENGTH) {
// 横向远离
if (point.x < it.x) point.x -= POINT_MOVE_LENGTH
else point.x += POINT_MOVE_LENGTH
// 纵向远离
if (point.y < it.y) point.y -= POINT_MOVE_LENGTH
else point.y += POINT_MOVE_LENGTH
}
}
}

// 循环发送
view.invalidate()
view.mHandler.sendEmptyMessageDelayed(0, POINT_ADD_TIME)
}
}
}
}

这里没写onMeasure,注意下不能用wrap-content,布局的话改个黑色背景就行了。


主要问题


下面简单讲讲吧。


粒子


这里用了个数据类构造了粒子,用了一个ArrayList来存放,本来想用linkedHashMap来保存并实现下LRU的,结果连线的时候比较复杂,重构的时候直接删了,后面用了一个counter来控制粒子的存活时间。


逻辑控制


一开始的时候想的比较复杂,实现来弄得自己头疼,后面觉得何不将逻辑和绘制分离,在ondraw里面只进行绘制不就行了,逻辑通过handler来更新,实际这样在我看来是对的。


我这用了一个Handler配合嵌套循环发送空消息,实现定时更新效果,每隔一段时间更新一下逻辑,Handler内部通过弱引用获得view,并对其中的内容修改,修改完成后,通过invalidate出发线程更新。


新增点


Handler会定时更新,只需要在handleMessage里面添加点就行了,为了控制点出现的频率,我这又引入了控制变量。


粒子生命周期


handleMessage里面会检查粒子是否失活,失活了就通过iterator去移除,移除数组内内容还是尽量通过iterator去实现吧,特别是for-eacn循环以及for循环内删除多个时,会出错的!


粒子趋向于手指


手指按下时设置mTouchParticle,移动时更新这个mTouchParticle,手指抬起时对mTouchParticle赋空,这样在handleMessage里面只要在mTouchParticle不为空时稍稍改变下其他粒子的位置,就可以达到趋向效果。


粒子连线


这里我没有想到什么好办法,就是暴力破解,直接两两计算,并对合适距离的粒子进行连线。


作者:方大可
来源:juejin.cn/post/7221565450249027641
收起阅读 »

记录一次EasyExcel的使用

1.这篇文章写什么? 在工作中碰到了这样一个需求,要从数据库里面读取一段时间内,每天某些时刻对应的温度数据,一天可能会设置有多个时间点,导出到excel中的模板需要支持多种格式的,比如一个月,一个季度,上半年,下半年等,并且需要支持灵活的配置导出的时间段范围。...
继续阅读 »

1.这篇文章写什么?


在工作中碰到了这样一个需求,要从数据库里面读取一段时间内,每天某些时刻对应的温度数据,一天可能会设置有多个时间点,导出到excel中的模板需要支持多种格式的,比如一个月,一个季度,上半年,下半年等,并且需要支持灵活的配置导出的时间段范围。超过温度的点需要用红色来进行标识


2.为什么写这篇文章


在工作中碰到了需要导出excel报表的需求,写这篇文章记录从需求到实现的过程,供后续参考和改进


3.如何实现?


要实现导出excel,首先就想到了easyexcel,它可以支持读、写、填充excel,针对现在这个需求,如果需要自定义时间的话可以通过配置字典,前端读取字典用dayjs来进行解析,把解析到的时间范围给到后端,后端再根据时间范围去导出数据,至于excel方面,想到了先用占位符在excel模板中预设好位置,然后用代码进行填充,主要的实现流程就大概有了


4.具体实现


根据上面的主要流程,下面就来进行每一步的细化,首先是前端


4.1 解析时间


通过字典来读取时间范围的代码,至于为什么不用后端来实现,之前也考虑过,不过因为无法确定具体的时间,就没用后端去实现,因为一个月不固定有多少天,不固定有多少周,如果用后端去实现的话可能会得不到准确的时间,反观前端的dayjs,就可以很简单的实现这个功能,通过解析dayjs代码来实现时间的获取,代码如下:


<template>
<div>
{{getDates()}}
</div>

</template>

<script lang="ts" setup>
import dayjs from "dayjs";
const getDates = () =>{
const str = "const startTime = dayjs().startOf('year').format('YYYY-MM-DD HH:mm:ss');const endTime = dayjs().endOf('year').format('YYYY-MM-DD HH:mm:ss');return [startTime, endTime]"
const timeFunc = new Function('dayjs', str);
const data = timeFunc(dayjs)
console.log(data[0])
console.log(data[1])
return timeFunc(dayjs)
}
</script>



用str来模拟从字段中读取到的时间范围代码,用dayjs解析出来,这样页面上就会直接打印出当前的时间:
[ "2023-01-01 00:00:00", "2023-12-31 23:59:59" ]


到此,前端解析时间的任务就完成了


4.2 导出excel


万丈高台起于垒土,要实现本次需求,首先要能导出excel,然后再考虑样式,最后再考虑现有可行方案的兼容性,能否兼容多种格式的模板。经过考察官网,发现了两种看起来比较可行和符合目前需求的导出方式:
首先把依赖导入先:


<dependency>  
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.2.1</version>
</dependency>

方式1: 定义一个模板,里面就搞几个占位符,每个占位符替换一个内容


image.png


直接上代码:


@Test
public void simpleFill() {
// 模板注意 用{} 来表示你要用的变量 如果本来就有"{","}" 特殊字符 用"\{","\}"代替
String templateFileName = "F:/excel/" + "simple.xlsx";
// 方案1 根据对象填充
String fileName = "F:/excel/" + System.currentTimeMillis() + ".xlsx";
// 这里会填充到第一个sheet, 然后文件流会自动关闭
Map<String, Object> map = new HashMap<>();
map.put("name", "张三");
map.put("number", 5.2);
map.put("month", 5);
EasyExcel.write(fileName).withTemplate(templateFileName)
.sheet().doFill(map);
}

看效果,替换成功了,能正常替换,这种方式能跑:
image.png


方式2 定义一个模板,里面搞几个占位符,以.开头的为循环的占位符


image.png
直接上代码:


    @Test
public void horizontalFill() {
String templateFileName = "F:/excel/" + "simpleHeng.xlsx";
String fileName = "F:/excel/" + System.currentTimeMillis() + "heng" + ".xlsx";
// 方案1
try (ExcelWriter excelWriter = EasyExcel.write(fileName).withTemplate(templateFileName)
.build()) {
WriteSheet writeSheet = EasyExcel.writerSheet().build();
excelWriter.write(data(),writeSheet);
}
}

private List<FillData> data() {
List<FillData> list = ListUtils.newArrayList();
for (int i = 0; i < 10; i++) {
FillData fillData = new FillData();
list.add(fillData);
fillData.setName("张三" + i);
fillData.setNumber(5.2);
fillData.setDate(new Date());
}
return list;
}

@Getter
@Setter
@EqualsAndHashCode
public class FillData {
private String name;
private double number;
private Date date;
}

看效果,替换成功(虽然哪里感觉不对劲):


image.png


现在基本的替换模板实现了,那么就进一步深入,给表格里面的内容添加颜色样式


经过网上搜索了一下,发现自定义样式需要写一个类去实现CellWriteHandler,从而自定义样式


@Slf4j
public class CustomCellWriteHandler implements CellWriteHandler {

@Override
public void afterCellDispose(CellWriteHandlerContext context) {
Cell cell = context.getCell();
Workbook workbook = context.getWriteWorkbookHolder().getWorkbook();
CellStyle cellStyle = workbook.createCellStyle();
Font writeFont = workbook.createFont();
writeFont.setColor(IndexedColors.RED.getIndex());
writeFont.setBold(true);
cellStyle.setFont(writeFont);
cell.setCellStyle(cellStyle);
context.getFirstCellData().setWriteCellStyle(null);
}
}

然后在原有导出代码的基础上加个registerWriteHandler(new CustomCellWriteHandler())


EasyExcel.write(fileName).withTemplate(templateFileName)
.registerWriteHandler(new CustomCellWriteHandler())
.sheet().doFill(map);

然后就可以看到导出的效果了
image.png


再试试第二种方式,也就是循环写入的,加个样式:


WriteSheet writeSheet = EasyExcel.writerSheet().registerWriteHandler(new CustomCellWriteHandler()).build();

然后导出,发现这种方式的导出的excel未能成功设置颜色,后面也尝试了很多次,确定了这种方式没法带上样式,所以就放弃这种方式了
image.png


到这里就只剩下excel中每个位置的都用一个占位符来占位替换的方式了


4.3 实现思路


确定了可行的实现方式,现在就要在这种方式上思考如何实现兼容多种时间段,多种模板的导出。需求中每天可能会设置若干个点,也可能需要导出不同时间段的温度数据,例如月、季度、半年等,或者是其他更细的时间点,每次的模板可能也不大一样,例如下面的几种:



老王的模板



image.png



老张的模板



image.png



老李的模板



image.png


可以看出需要兼容不同的模板确实是个伤脑筋的事情,后面发现了个比较折中的办法:因为目前能实现excel上导出带样式的就只有替换占位符的方法,如何在不改代码的前提下适配多种模板呢,就直接把模板上每个格子都对应一个占位符,只要后端每次都是按照固定规则去生成数据的,那么只要设置好模板里每个格子的占位符就可以解决问题了。只要每次生成数据的顺序都是按照{t1}、{t2}、{t3}这样,依次类推下去,只要模板配置好占位符即可。例如每天一个监测点,那{t1}就对应第一天监测点的温度,{t2}就对应第二天监测点的温度,如果每天两个监测点,那么{t1}和{t2}就分别对应第一天的第一、第二个监测点的温度。这样就可以实现导出月、季度、半年的数据而不用改代码了,如果要导出某个时间段的,例如1月15日到2月15日的,这种时间段的话就使用大一级的模板,使用季度的模板即可兼容。


4.4 代码实现


有了上面的思路,大致实现流程就出来了


image.png


4.4.1 前端解析字符串,获取时间范围


这个上面有提及,这里就不再赘述,跳过


4.4.2 后端获取每天配置的时间点


由于配置的时间点是12、13、15这种,并不是具体的一个时间,因此需要结合时间范围来解析成具体的时间,首先需要获取时间段里面的每一天,然后再根据配置的时间点得到每个具体的时间点,获取到了时间点之后就可以去数据库中查询这个时间点对应的数据,模拟代码如下:
首先说明,这里引入了hutool来处理时间


<dependency>  
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.18</version>
</dependency>

public static void main(String[] args) {
DateTime startTime = DateUtil.parse("2023-07-01 00:00:00");
DateTime endTime = DateUtil.parse("2023-10-31 23:59:59");
List<Date> timeRangeDates = getTimeRangeDates(startTime, endTime);
List<String> times = Arrays.asList("12");
Map<DateTime, Float> resultMap = getTemperature(timeRangeDates, times);
}

/**
* 获取要导出数据的Map, key为时间,value为时间对应的值
*
* @param timeRangeDates 时间范围
* @param times 每天定义的时间点
* @return 导出的map
*/

private static Map<DateTime, Float> getTemperature(List<Date> timeRangeDates, List<String> times) {
Map<DateTime, Float> resultMap = new HashMap<>();
for (Date timeRangeDate : timeRangeDates) {
for (String time : times) {
// eg: 12 ==> 2023-11-07 12:00:00
String tempTime = DateUtil.formatDateTime(timeRangeDate).substring(0, 11) + time + ":00:00";
DateTime recordTime = DateUtil.parse(tempTime);
// 如果是将来的时间,就不执行查询,认为是没有数据的
if (recordTime.isAfter(new Date())) {
resultMap.put(recordTime, null);
continue;
}
// 模拟从数据库拿到的温度,10.5 °C
resultMap.put(recordTime, selectTemperature());
}
}
return resultMap;
}

/**
* 模拟从数据库查询数据,随机生成2到10之间的一个数字,保留一位小数
*
* @return 2到10之间的一个数字, 保留一位小数
*/

private static float selectTemperature() {
Random random = new Random();
float minValue = 2.0f;
float maxValue = 10.0f;
float range = maxValue - minValue;
float randomFloat = (random.nextFloat() * range) + minValue;
DecimalFormat df = new DecimalFormat("#.0");
String formattedFloat = df.format(randomFloat);
return Float.parseFloat(formattedFloat);
}

/**
* 获取起止时间内的每一天
*
* @param start 开始时间
* @param end 结束时间
* @return 每天
*/

private static List<Date> getTimeRangeDates(Date start, Date end) {
List<Date> dates = new ArrayList<>();
DateTime current = DateUtil.date(start);
DateTime endTime = DateUtil.date(end);
while (current.isBefore(endTime)) {
dates.add(current.toJdkDate());
current = current.offset(DateField.DAY_OF_MONTH, 1);
}
return dates;
}

经过上面一通操作,就可以得到了一个Map,里面的key是时间点,value是时间点对应的温度。
按道理来说,map已经构造好了,直接编写模板导出就完事了,就在这时,又碰到了一个问题,map上的{t1}、{t2}等占位符如何跟时间匹配的问题,模板上肯定是每个月都以最大的时间来定义,也就是31天,但并不是每个月都有31天,因此要在导出数据的时候把那些应该为空的占位符也计算出来,拿老王的模板来举例子:


今年是2023年,9月只有30天,那么模板上31号的占位符应该导出的时候就应该设置个空值,也就是在碰到{t93}的时候需要把值设置为空,然后再导出excel


image.png
完整代码如下:


package com.example.demo.test;

import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import com.alibaba.excel.EasyExcel;
import com.example.demo.handler.CustomCellWriteHandler;

import java.text.DecimalFormat;
import java.time.YearMonth;
import java.time.ZoneId;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

public class ExportTest {

public static void main(String[] args) {
DateTime startTime = DateUtil.parse("2023-07-01 00:00:00");
DateTime endTime = DateUtil.parse("2023-12-31 23:59:59");
List<Date> timeRangeDates = getTimeRangeDates(startTime, endTime);
List<String> times = Collections.singletonList("12");
Map<DateTime, Float> resultMap = getTemperature(timeRangeDates, times);
// 获取日期范围内的月份
List<Integer> distinctMonths = getDistinctMonths(timeRangeDates);
// 获取月份对应的天数
Map<Integer, Integer> daysInMonths = getDaysInMonths(timeRangeDates);
// 获取应该置空的下标
List<Integer> emptyIndexList = getEmptyIndexList(daysInMonths, times.size());
// 获取理论上一共有多少天,按照一个月31天算
int totalDaysCount = getTotalDaysCount(distinctMonths, timeRangeDates.size());
Map<String, Object> exportDataMap = new HashMap<>();
// i快 j慢
for (int i = 0, j = 0; i < totalDaysCount; i++) {
int currentDateIndex = j;
int currentCount = (i + 1) * times.size() - (times.size() - 1);
for (String time : times) {
// 本月不足31天的填充null
if (emptyIndexList.contains(currentCount)) {
exportDataMap.put("t" + currentCount++, null);
continue;
}
// 12 ==> 2023-10-25 12:00:00
String tempTime = DateUtil.formatDateTime(timeRangeDates.get(currentDateIndex))
.substring(0, 11) + time + ":00:00";
if (currentDateIndex == j) {
j++;
}
// 根据date查询数据
DateTime recordTime = DateUtil.parse(tempTime);
exportDataMap.put("t" + currentCount++, resultMap.get(recordTime));
}
}
// 模板注意 用{} 来表示你要用的变量 如果本来就有"{","}" 特殊字符 用"\{","\}"代替
String templateFileName = "F:/excel/" + "老王.xlsx";
// 方案1 根据对象填充
String fileName = "F:/excel/" + System.currentTimeMillis() + "老王.xlsx";
EasyExcel.write(fileName).withTemplate(templateFileName)
.registerWriteHandler(new CustomCellWriteHandler())
.sheet().doFill(exportDataMap);
}

private static int getTotalDaysCount(List<Integer> distinctMonths, int timeRangeSize) {
if (timeRangeSize <= 31) {
return Math.min(distinctMonths.size() * 31, timeRangeSize);
}
return distinctMonths.size() * 31;
}

private static List<Integer> getEmptyIndexList(Map<Integer, Integer> daysInMonths, int pointCount) {
List<Integer> list = new ArrayList<>();
AtomicInteger count = new AtomicInteger();
daysInMonths.forEach((key, value) -> {
// 本月的开始长度
int monthLength = count.get();
// 总本月虚拟长度
count.addAndGet(31 * pointCount);
// 本月实际长度
int realLength = value * pointCount;
// 本月开始下标
int startIndex = monthLength + realLength;
// 多出的下标
int extraCount = count.get() - realLength - monthLength;
// 记录需要存空值的占位符位置
for (int i = startIndex + 1; i <= startIndex + extraCount; i++) {
list.add(i);
}
});
return list;
}

/**
* 获取今年的某些月份的天数
*
* @param timeRangeDates 时间范围
* @return 该月份对应的年
*/

private static Map<Integer, Integer> getDaysInMonths(List<Date> timeRangeDates) {
List<Integer> distinctMonths = getDistinctMonths(timeRangeDates);
Date firstDate = timeRangeDates.get(0);
Calendar calendar = Calendar.getInstance();
calendar.setTime(firstDate);
int currentYear = calendar.get(Calendar.YEAR);
return distinctMonths.stream()
.collect(Collectors.toMap(
month -> month,
month -> YearMonth.of(currentYear, month).lengthOfMonth()
));
}

/**
* 获取时间范围内的所有月份、去重、转成String类型
*
* @param timeRangeDates 时间范围集合
* @return 月份集合
*/

public static List<Integer> getDistinctMonths(List<Date> timeRangeDates) {
return timeRangeDates.stream()
.map(date -> date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate().getMonthValue())
.distinct()
.sorted()
.collect(Collectors.toList());
}

/**
* 获取要导出数据的Map, key为时间,value为时间对应的值
*
* @param timeRangeDates 时间范围
* @param times 每天定义的时间点
* @return 导出的map
*/

private static Map<DateTime, Float> getTemperature(List<Date> timeRangeDates, List<String> times) {
Map<DateTime, Float> resultMap = new HashMap<>();
for (Date timeRangeDate : timeRangeDates) {
for (String time : times) {
// eg: 12 ==> 2023-11-07 12:00:00
String tempTime = DateUtil.formatDateTime(timeRangeDate).substring(0, 11) + time + ":00:00";
DateTime recordTime = DateUtil.parse(tempTime);
// 如果是将来的时间,就不执行查询,认为是没有数据的
if (recordTime.isAfter(new Date())) {
resultMap.put(recordTime, null);
continue;
}
// 模拟从数据库拿到的温度,10.5 °C
resultMap.put(recordTime, selectTemperature());
}
}
return resultMap;
}

/**
* 模拟从数据库查询数据,随机生成2到10之间的一个数字,保留一位小数
*
* @return 2到10之间的一个数字, 保留一位小数
*/

private static float selectTemperature() {
Random random = new Random();
float minValue = 2.0f;
float maxValue = 10.0f;
float range = maxValue - minValue;
float randomFloat = (random.nextFloat() * range) + minValue;
DecimalFormat df = new DecimalFormat("#.0");
String formattedFloat = df.format(randomFloat);
return Float.parseFloat(formattedFloat);
}

/**
* 获取起止时间内的每一天
*
* @param start 开始时间
* @param end 结束时间
* @return 每天
*/

private static List<Date> getTimeRangeDates(Date start, Date end) {
List<Date> dates = new ArrayList<>();
DateTime current = DateUtil.date(start);
DateTime endTime = DateUtil.date(end);
while (current.isBefore(endTime)) {
dates.add(current.toJdkDate());
current = current.offset(DateField.DAY_OF_MONTH, 1);
}
return dates;
}
}

自定义设置样式


package com.example.demo.handler;

import com.alibaba.excel.write.handler.CellWriteHandler;
import com.alibaba.excel.write.handler.context.CellWriteHandlerContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;

@Slf4j
public class CustomCellWriteHandler implements CellWriteHandler {

@Override
public void afterCellDispose(CellWriteHandlerContext context) {
Cell cell = context.getCell();
double numericCellValue;
try {
numericCellValue = cell.getNumericCellValue();
} catch (Exception e) {
log.error("解析到了非数值类型,直接返回");
return;
}
if (numericCellValue >= 2 && numericCellValue <= 8) {
return;
}
Workbook workbook = context.getWriteWorkbookHolder().getWorkbook();
CellStyle cellStyle = workbook.createCellStyle();
Font writeFont = workbook.createFont();
writeFont.setColor(IndexedColors.RED.getIndex());
writeFont.setBold(true);
cellStyle.setFont(writeFont);
cell.setCellStyle(cellStyle);
context.getFirstCellData().setWriteCellStyle(null);
}
}

运行效果如下:
9月31日有占位符,但是9月只有30日,因此31日无数据,并且数据不在2和8范围之间的数据都可以用红色字体展示


image.png


至于返回数据给前端这里就省略了,毕竟文件都能生成了,其他的都是小问题了


5. 小结


本次的设计实现了支持不同模板,按照月(月初到月末)、季度、半年的数据进行导出,初步实现了功能,但是面对更复杂的场景,例如需要支持不同模板在某一段时间的导出功能暂时没法实现。不过也算是积累了一次使用easyexcel导出报表的经验,以此来记录本次开发的过程,为后续类似功能的开发提供借鉴


代码存放链接: gitee.com/szq2021/eas… (主要是怕找不着了,先存着)


作者:用户5024878678107
来源:juejin.cn/post/7298642789092474889
收起阅读 »

互联网人参与企业数字化转型的过程与误区

做数字化转型业务已经有大半年,虽然以前在做智能表格类工具的时候,就有思考过如何通过智能表格类工具解决企业流程的问题,但远不如这大半年深更业务来得更真切。在这过程中也逐步习得了数字化转型的过程,并扫除了以前对数字化转型的一些误区,通过此文给大家分享。 第一步:业...
继续阅读 »

做数字化转型业务已经有大半年,虽然以前在做智能表格类工具的时候,就有思考过如何通过智能表格类工具解决企业流程的问题,但远不如这大半年深更业务来得更真切。在这过程中也逐步习得了数字化转型的过程,并扫除了以前对数字化转型的一些误区,通过此文给大家分享。


第一步:业务线上化


数字化从字面意思看,就是要将业务都数字化,后面基于数据再开展一系列业务的降本增效提质。数据怎么来呢?那必须尽可能将业务线上化了。


初入门者看似很简单,实则深似水。从互联网公司转型过来的从业者会发现以前的经验都失效了,很多时候也难以站在一个用户的角度来审视如何线上化这个问题。譬如以前做社交产品,还能自己站在普通用户的角度,YY出不少看似说得通的需求,到了企业数字化领域,不能一头扎入业务中,多跟业务分析师、一线业务专家沟通,业务这潭深水就可以够呛一肚子水。


业务的线上化并非易事,需要遵循以下的步骤:


建立系统之间的关系



建立系统之间的关系,本质是建立了数据流动的链条。系统主要是承载企业的数据,将系统连接起来即是将他这些数据串联起来,产生1+1>2的价值。


作为数字化专家,比较少见可以在企业成立之初就参与数字化的建设。企业草创阶段更多是企业的负责人担当这一角色。数字化专家半路加入,往往可能是面对一系列外部采购的系统,如:存储员工信息的飞书,存储财务预算的飞书文档和财务经营数据的金蝶,存放客户资料和销售线索的纷享销客等等。如果有研发实力的企业,可能还养着一个小团队,自研搭建了一些简单的业务系统。


企业数字化的过程,就像在下一盘围棋一样,每通过外采/自研建设一个系统,就像在棋盘上下子,并且我们在下子的过程中都会寻求棋子之间的联系,比如每个系统的用户信息,都可以通过飞书获取;生产经营的信息导出成文档,基于这些文档的数据可以在飞书上做财务预算;生产系统可以基于客户资料和销售线索调整生产计划等等。这些系统只有像棋子一样联系起来,整盘棋才能盘货起来,在市场竞争中占据优势。如果所有系统都没有规划,各自为政,一盘散沙,那势必会被竞争对手吞掉。想象一下,如果每建设一个系统的员工信息都需要重新建设,那该多头疼。


至于这些系统是怎么串联起来,一般来说通过数据的导入导出、开放接口等技术手段,采用哪种基于成本考虑,在此就不作赘述。


梳理标准作业流程



在已有信息输入的基础上,我们建立了系统之间的联系,这些系统有的是已经存在的,有的是亟待规划落地的。良好的系统关系的梳理与建立,能够帮助企业高屋建瓴地做好后续信息化的规划,知道系统建设的优先级,毕竟数字化的资源(预算、人力)是有限的。


确认了规划,我们才好知道下一步该建设哪个系统,这时我们便进入了第二阶段,梳理标准作业流程。这一阶段,可能通过翻阅过往标准作业流程文档、与业务专家进行访谈等手段,全面地梳理清楚这些流程。有了标准作业流程,数字化专家才好去对这些流程做归类,形成系统的功能模块,并规划相应的功能模块将这些标准作业流程线上化。


自研还是采购?


系统的落地无非是通过自研或者采购。不过这个命题对于那些中小企业来说看来是无需烦恼的,采购只能是唯一划算的出路。


对于有自研能力的公司,这是个头疼的问题。即使自研能较强的互联网公司,也不可能所有的能力都由自己建设。最常见的代码仓库、发布系统等等,往往是“采购”自公司内部的研效团队。选择自研,无非是希望可以更灵活、更可控;选择采购无法是看种成本——资金成本、时间成本。基于这些考虑,公司对那些服务较为核心、战略级业务,且愿意花更多时间等待成长成熟的系统,更多倾向于自研。而那些非核心的,等太久公司会瘫痪的系统(时间成本),也不需要花大价钱(有许多同样采购的客户分摊了成本)购买的,如财税系统、办公软件,则更多倾向于采购。


自研与采购其实也并非一定要二选一。有些提供软件SAAS的厂商,也愿意将源码售卖给客户,更有甚者,越来越多的厂商会提供对自研更友好的PAAS产品。这可以看作是一个先采购后迭代自研的过程。


第二步:降本增效的两大利器——体验&标准作业流程优化


数字化过程中有一个常见的误区,就是只要我努力地将标准作业流程搬到线上,就可以一定能帮公司降本增效提质,能让公司发展红红火火。这里的误区其实是有两个重要的误区:


误区



误区一: 数字化一定能公司降本增效提质,能让公司发展红红火火。再好的数字化建设也只是工具,如果使用者的意识没有跟上,可能达不到效果。比如强推系统而培训不足,那就会导致使用率不足,数据失真严重;有好的数据分析能力却没有很好使用的知识,那就无法帮公司提升做出正确决策的效率。作为数字化专家,有义务跟老公和团队沟通清楚数字化系统的能力范围,并采取相应的措施让人与系统的磨合做得更好。


误区二: 将标准作业流程搬到线上即大功告成。要达到降本增效的效果,只“搬”是远远不够的,还需要优化体验和流程。


体验优化


优化体验应该是互联网人独有的优势,互联网业务中,多少工程师被设计师毒打,摁在电脑桌前一个个设计稿还原,天生自带好坏体验的滤镜。体验差相信是很多传统行业信息系统的通病,慢,卡,难用基本是一线用户想到的形容词。


如果说互联网产品体验不好用户可以逃走,那公司的系统用户是无法选择的。体验差造成的问题有很多,比如新系统推广难,老系统用户敷衍了事,造假应付导致数据失真。因此在设计产品的时候虽然我们多数侧重功能要先有,但体验也万万不能忽视。


交互体验在传统行业数字化转型中扮演着重要的角色。由于传统行业的一线人员往往对电子设备的使用水平不如IT从业人员,因此需要设计出更符合他们使用习惯的数字化产品,从而提高交互体验,降低使用难度。


以银行业为例,招行银行推出了“一网通”服务,通过简洁的界面设计、便捷的操作流程,大大提高了用户的使用体验。此外,该服务还提供了丰富的数字化工具,如在线客服、手机银行等,使得用户可以随时随地进行业务办理,提高了业务效率。


(标准)作业流程优化


虽然体验对于互联网人而言驾轻就熟,但流程确实门外汉。往往在跟业务专家梳理和讨教流程的时候,惮于提出质疑和优化,这就损失了一个大好机会进行流程调整优化。


一个业务流程,线下和线上的操作可能大相径庭。以物业行业为例,一个巡逻的检查任务,以前只需要到每个巡逻点通过纸质签到即可,换成线上后,可能是扫码也可能是NFC近场检测,整个流程都会被颠覆、重塑。因此全新的流程理应跟业务专家、一线的代码员工沟通确认清楚再落地,那样对后续的推广都更为便利,反对的声音也自然更少。


细心的你可能还会发现,这里的(标准)被括弧括起来了,这表示流程的标准化是非常重要的,尤其对于上了规模的公司,如果同一性质的业务单元同一个作业流程完全不同,那将流程统一为一套标准就已经是生产效率的极大提升。但标准又不是必须的,如果公司的规模小,那标准化的工作可能并不是必须的,只针对流程做优化也是足够的,毕竟做流程的统一本身就非常费劲,而且在公司规模小、起步早期,过早统一标准也有可能束缚了业务单元自身的想象力。


那应该以什么样的标准来说明流程被优化了呢?目前主要是有两个,一个是能节省多少工时,另一个可以用最近比较流行的是否做到无纸化,前者是大部份老板都关注的可以节约的成本,后者除了成本的节约也可以更体验公司的社会责任与彰显社会形象,突显公司对节能减排方面的贡献。


第三步:智能化



如果说前两步的路径还尚算明确、既定,那智能化在大多数企业中还是在探索进行时。关于智能化我想到了个生动的形容:在智能化之前,信息系统是工具人士兵,使用者得思考、得指挥他们执行;在智能化之后,这些系统纷纷跃升为军师,可以给使用者出谋划策。


智能化的根基是数据。如果我们的终极目标是智能化,那么我们需要在智能化之前就要收集充分质量高的数据。收集充分,首先种类要是充分的,人员数据、销售数据、库存数据、生产数据等等,一切与业务相关的数据都可以收集。其次是对象要充分,一切可以被智能化所斌能的都可以收集数据,人员、设备、流程均可进行收集。质量高,可以通过标准化管理,建立数据质量控制体系,确保数据的准确性、完整性和一致性,需要的时候可以对数据进行校验和清洗,确保数据的准确性。


那收集的数据如何存放呢?一般而言,我们需要建立数据中台,整合公司各部门的数据源,实现数据的互联互通,减少重复工作,提高数据共享和利用效率。数据中台除了存储,还可以提供加工、计算等能力,给予业务系统更好的数据能力支撑。


当数据达到充分、标准、高质量的状态,数据便可以在以下这些场景给业务赋能:



  1. 数据驱动决策:通过数据分析,为企业的决策提供数据支持。例如,通过分析销售数据,预测未来的销售趋势,为生产、库存等部门提供决策依据。

  2. 个性化服务:通过智能化技术,实现服务的个性化定制。例如,通过分析客户的消费习惯和需求,提供定制化的产品和服务方案。

  3. 自动化生产:通过引入自动化生产线和机器人等设备,实现生产的自动化和智能化。例如,在汽车制造领域,引入自动化生产线代替传统的人工生产线,提高生产效率和产品质量。

  4. 智能化调度与优化:通过引入智能化调度系统,对生产和服务进行实时监控和优化。例如,通过分析生产数据和客户需求,动态调整生产和配送计划,提高效率和减少浪费。


综上所述,智能化需要数据作为基础,在数据之上构建智能化能力,并可以在不同的场景给业务提供数据决策、个性化服务、自动化生产、智能化调度与优化等能力。只有这样,才能让智能化真正成为企业发展的助推器。


总结


相信这篇文章能给初入数字化深水区的互联网人有比较清晰的实操脉络,也能避开不少坑。后续也会陆续出一系列关于数字化转型的关于产品思考、技术设计的系列文章,敬请期待!


作者:李CHENGXI
来源:juejin.cn/post/7298646156438765568
收起阅读 »

接口优化🚀68474ms->1329ms

小菜的一次接口优化:从68474ms到1329ms 前言 突然,有人大喊一声:小菜,你过来一下 小菜被吓得抖了一抖,连忙切出开发界面,看了一眼,原来是项目经理在喊 小菜屁颠屁颠的过去后 项目经理:小菜,有空你看看后台管理里的商品信息导出Excel功能,导出数据...
继续阅读 »

小菜的一次接口优化:从68474ms到1329ms


前言


突然,有人大喊一声:小菜,你过来一下


小菜被吓得抖了一抖,连忙切出开发界面,看了一眼,原来是项目经理在喊


小菜屁颠屁颠的过去后


项目经理:小菜,有空你看看后台管理里的商品信息导出Excel功能,导出数据只有几千条但是要等特别久


小菜:没问题,等我忙完手上的活就来看看怎么回事


分析与优化


小菜回到工位后,立马看了看后台管理系统的商品信息导出功能


该功能是通过导入规定格式的Excel(比如商品名称),然后导出这些商品的所有信息


小菜用对应的模板(大概数据量5千)使用此功能,大概等了1分钟多才导出结果


小菜:我可以先用arthas的trace监听这个接口,看看接口里哪些方法耗时,再具体进行分析


5ebf0da94e3d187b59b79e81cb66baa.png


使用arthas的trace命令监听端口后,发现总耗时70284ms,其中XXMessage耗时68s,导出Excel花费1.8s


小菜:那具体的业务处理应该在XXMessage里了,我先来看看


     public List<ExportVO> xxMessage(MultipartFile file, HttpServletRequest request, HttpServletResponse response) {
//导出结果
List<ExportVO> exportVOS = new ArrayList<>();
try {
//EasyExcel 读取模板数据
//使用AnalysisEventListener 在读取数据时加入导出结果,在读完后进行封装操作
EasyExcel.read(file.getInputStream(), Product.class, new AnalysisEventListener<Product>() {
private final List<Product> list = new ArrayList<>();

//解析完一行后如何处理
@Override
public void invoke(Product p, AnalysisContext analysisContext) {
doLine(p);
}

//解析完所有数据后如何处理
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
doAfter();
}
}).sheet().doRead();
} catch (IOException e) {
e.printStackTrace();
}
return exportVOS;
}

小菜:使用的EasyExcel读取,那真正的处理应该在实现的AnalysisEventListener中


小菜:让我先来看看每解析一行如何处理的


 //存储要处理的数据
List<Product> products = new ArrayList<>();

private void doLine(Product data) {
try {
//拿到所有使用ExcelProperty的字段
List<Field> fields = Arrays.stream(data.getClass().getDeclaredFields())
.filter(f -> f.isAnnotationPresent(ExcelProperty.class))
.collect(Collectors.toList());

//判断字段是否为空,为空则集合添加false不为空添加true
List<Boolean> lines = new ArrayList<>(fields.size());
for (Field field : fields) {
field.setAccessible(true);
Object value = field.get(data);
if (value == null) {
lines.add(Boolean.TRUE);
} else {
lines.add(Boolean.FALSE);
}
}

//ExcelProperty的所有字段不为空 就加入集合
if(lines.stream().allMatch(Boolean.TRUE::equals)){
products.add(data);
}
} catch (Exception e) {
log.error("parse data fail: " + e.getMessage());
}
}

(ExcelProperty注解用于标记表格中的列)


小菜拿出做算法题分析时间复杂度的思路


小菜:这里总共有三个循环分别是:获取使用ExcelProperty的字段、判断每个字段是否为空、allMatch匹配数组中所有元素为true


小菜:那么用时间复杂度表示就是O(3N),N为数据量,而这些集合的数据量则是使用ExcelProperty的字段,好像是固定的,并不会随着Excel表格中数据量的提升而提升,那么可以把它们看成常量,那最终时间复杂度就是常量级别O(1)


小菜:但是还用了反射会有些性能开销


小菜:咦?为啥要判断实体每个字段不为空才加入要处理的集合呢?


小菜:好像直接判断该商品名不为空就可以了吧?


小菜:用反射来实现通用性,难道这段代码是前辈复制的?


于是,小菜洋洋得意的将代码改成:


 //存储要处理的数据
List<Product> products = new ArrayList<>();

private void doLine(Product data) {
if (StringUtils.isNotEmpty(data.getProductName())) {
products.add(data);
}
}

为了担心自己改错,小菜还保留原始代码,方便回滚


再来看下解析完数据后的处理方法


小菜看着这一望无际一百多行没有注释、多层if嵌套的代码,整个人都呆了


大致观看了一遍后,小菜将shit mountain代码梳理成以下代码:


 //存储要处理的数据
List<Product> products = new ArrayList<>();

private void doLine(Product data) {
if (StringUtils.isNotEmpty(data.getProductName())) {
products.add(data);
}
}

private void doAfter() {
//要处理的数据为空直接返回
if (Empty.isEmpty(products)) {
return;
}

//循环处理数据
products.forEach(product->{
//根据商品名查询出商品列表 IO
List<Sku> skus = skuService.list(product.getProductName());
//查到商品数据为空跳过
if (Empty.isEmpty(skus)) {
continue;
}

//查询商品具体数据 IO

//查询分类、规格... IO

//封装实体 添加到导出列表
});
}

看到这里小菜一下就明白为什么接口这么慢了


小菜:好好好,你这样写代码是吧


小菜:不考虑查数据库的网络IO是吧,肯定是不想写联表SQL,偷懒直接用MP


小菜直接用一次联表查询替代这么多的查询,为了避免数据量太大,小菜设置每次处理的最大数据量,分多次处理


 private void doAfter() {
//要处理的数据为空直接返回
if (Empty.isEmpty(products)) {
return;
}

int batchSize = 520;
//将大集合拆分为多个小集合 分批次处理
List<List<Product>> lists = CollectionUtils.split(products,batchSize);

//循环处理数据
lists.forEach(products->{
//转换为商品名列表
List<String> productNames = products.stream().map(Product::getProductName).collect(Collectors.toList());
//联表查询 IO
List<SkuDetails> skus = skuService.list(productNames);
skus.forEach(skus->{
//封装实体 添加到导出列表
});
});
}

小菜优化完代码后,再用arthas监听一遍,发现这次只需要3s,速度提升近23倍


0dadfdaf5e5b37810bf2266d03a2250.png


最后


接口优化的方式有很多种,在优化前我们需要进行分析哪里需要优化


在平时的开发中,也要多考虑时间、空间复杂度,并不是什么场景下都要去避免关联多张表查询


循环查数据库会造成多次网络IO,等待时间会很久,需要降低网络IO的次数,这种场景就可以联表查询


如果担心查的数据太多,联表查询性能慢,可以考虑分析执行计划增加索引,又或者分批次进行处理


其他接口优化的方式还有很多种,比如数据库优化、缓存、异步....


缓存,可以使用本地缓存、分布式缓存、多级缓存,但引入缓存又会带来一致性问题,要分析业务场景使用适合使用缓存


异步,可以使用MQ去做异步,也可以使用多线程去做异步,各有各的特点


在一些业务场景中,不要为了使用某项技术而去使用


技术是用来服务业务的,使用技术前要考虑到当前项目采用该技术是否合适,就像找伴侣一样,强扭的瓜不甜


有什么问题可以在评论区交流,如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下~


关注菜菜,分享更多干货,公众号:菜菜的后端私房菜


彩蛋


小菜装作忧愁的来到项目经理旁边


小菜:经理,这个接口对应的实现有些复杂,我估计下周忙完手上的事情就可以优化,你先帮我提个需求吧


项目经理:ok没问题,下周忙完就尽快优化吧


作者:菜菜的后端私房菜
来源:juejin.cn/post/7280007832702795795
收起阅读 »

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

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

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


十年,谁来成就你?


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


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


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


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


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


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


核心竞争力,祭出三板斧


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



  1. 多面试,验本事。

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

  3. 拥有自己的 Github 项目。 



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


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


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


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


记忆与思考,经验与思维


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




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




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






  •  




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


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



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



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

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

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


多面试,验本事


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


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


风平浪静,居安思危


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



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

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

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

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



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


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


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


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


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


乘风破浪,未雨绸缪


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


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


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


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



放平心态,查缺补漏


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



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

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


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


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


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


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


持续进步


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


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


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


狠下心来,坚持到底


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


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


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


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


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



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


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


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


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


幸运,需要自己争取


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


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



意外地成为了讲师


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


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


跌跌撞撞,逆水行舟


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


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


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


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


伺机而动,用心准备


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


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


建立个人 IP


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


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



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

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

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


链接资源,提影响力


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


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


主动出击,勿失良机


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


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


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


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


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


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



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


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


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


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


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


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


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


零碎时间,稳中求进


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



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


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


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


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


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


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


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


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


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


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


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



作者:雾岛听风来
来源:juejin.cn/post/7246593325935493178
收起阅读 »

echart 桑基图的点击高亮

web
先上效果图 引入echarts-for-react import ReactEcharts from 'echarts-for-react'; 增加点击事件, 这里需要注意的是当用的是setState时,在onChartReady里获取的state的值一直...
继续阅读 »

先上效果图


image.png
引入echarts-for-react


import ReactEcharts from 'echarts-for-react';

增加点击事件, 这里需要注意的是当用的是setState时,在onChartReady里获取的state的值一直是空值,所以增加useRef来临时存放curHighLight的值;


const [curHighLight, setCurHighLight] = useState(null);
const curHighLightRef = useRef(null);

<ReactEcharts
notMerge={true}
option={chartOption}
onChartReady={(EChartsInstance) =>
{
ChartsInstance.current = EChartsInstance;
// 双击高亮
ChartsInstance.current.on('click', (params) => {
console.log('点击高亮', params);

if (isHighlighted(params, curHighLightRef.current)) {
setCurHighLight(null);
curHighLightRef.current = null;
} else {
const cur = {
dataType: params?.dataType,
name: params?.data?.name,
source: params?.data?.source,
target: params?.data?.target
}
setCurHighLight(cur);
curHighLightRef.current = cur;
}

return false;
});
}}
/>


判断是否已经被点击过


const isHighlighted = (params, curHighLight) => {
if (params.dataType === 'node') {
return params?.data?.name === curHighLight?.name;
}

if (params.dataType === 'edge') {
return params?.data?.source === curHighLight?.source && params?.data?.target === curHighLight?.target;
}

return false;
}

点击事件增加后,把当前点击节点或连接线存起来后,再通过useEffect更新option



  1. 调整lineStyle和itemStyle里 opacity 值


const temp = cloneDeep(chartOption);
temp.series[0].lineStyle.opacity = curHighLight === null ? lineOpacity / 100 : 0.1;
temp.series[0].itemStyle.opacity = curHighLight === null ? 1 : 0.1;
temp.series[0].emphasis.disabled = curHighLight !== null;


  1. 调整高亮节点的


// 获取高亮详情
const getHighLightInfo = ({ curHighLight, links, nodes }) => {
// 当取消高亮时,文字颜色恢复正常
if (curHighLight === null) {
const isHighLight = false;
links?.forEach(item => {
item.isHighLight = isHighLight;
item.lineStyle.opacity = null;
});

nodes.forEach(item => {
item.isHighLight = isHighLight;
item.itemStyle.opacity = null;
item.label = {
color: null
}
});
}

// 节点
if (curHighLight?.dataType === 'node') {
const selectList = [];
links.forEach(item => {
const isHighLight = item.source === curHighLight.name || item.target === curHighLight.name;
item.isHighLight = isHighLight;
item.lineStyle.opacity = isHighLight ? opacityHL_link : 0.1;

if (isHighLight) {
selectList.push(item);
}
});

nodes.forEach(item => {
const isIn = selectList.find(v => v.source === item.name || v.target === item.name);
const isHighLight = !!isIn;

item.isHighLight = isHighLight;
item.itemStyle.opacity = isHighLight ? opacityHL_node : 0.1;
item.label = {
color: !isHighLight ? 'rgba(0, 0, 0, 0.35)' : null
}
});
}

// 连线
if (curHighLight?.dataType === 'edge') {
links?.forEach(item => {
const isHighLight = item.source === curHighLight?.source && item.target === curHighLight?.target;
item.isHighLight = isHighLight;
item.lineStyle.opacity = isHighLight ? opacityHL_link : 0.1;
});

nodes.forEach(item => {
const isHighLight = item.name === curHighLight.source || item.name === curHighLight.target;
item.isHighLight = isHighLight;
item.itemStyle.opacity = isHighLight ? opacityHL_node : 0.1;
item.label = {
color: !isHighLight ? 'rgba(0, 0, 0, 0.35)' : null
}
});
}
}

作者:一切随意
来源:juejin.cn/post/7293788137662677026
收起阅读 »

通知栏的那些奇技淫巧

一、问题的由来 前几天,一个网友在微信群提了一个问题: 通知栏监听模拟点击如何实现? 我以为业务情景是在自己应用内,防止脚本模拟点击而引申出来的一个需求,心里还在想,是否可以使用自定义View——onTouchEvent的参数MotionEvent的ge...
继续阅读 »

一、问题的由来




前几天,一个网友在微信群提了一个问题:



通知栏监听模拟点击如何实现?



我以为业务情景是在自己应用内,防止脚本模拟点击而引申出来的一个需求,心里还在想,是否可以使用自定义View——onTouchEvent的参数MotionEventgetPressure来判断是否是模拟点击。后来经过沟通得知,业务需求是如何监听第三方应用的通知栏,实现具体按钮的点击。如下图:


通知栏.jpg


上面是多家音频应用的通知栏在小米手机的样式,而网友的需求是如何实现针对某一个应用通知栏的某一个按钮的点击,比如监听喜马拉雅APP,当接收到通知的时候,需要点击关闭按钮。这个需求该如何接住呢?


二、实现方案之无障碍服务




当需求清晰以后,我心里面想到的第一个方案就是无障碍服务。但是无障碍服务点击通知栏简单,点击通知栏的某一个控件需要打开通知栏,然后找到这个控件的id,然后调用点击方法。同时由于几年前有过写抢红包脚本的经验,提出了一些疑问:



  • 用户使用的业务情景是什么?是否需要正规渠道上架?

  • 无障碍服务权限相当敏感,是否接受交出权限的选择?


沟通结果是正规渠道上架和业务情景不用考虑,但是权限的敏感需要换一个思路。网友指出,NotificationListenerService可以实现监听通知栏,能否在这个地方想点办法呢?而且他还提到一个业务情景:当收到通知的时候,不需要用户打开通知栏列表,不管用户在系统桌面,还是第三方应用页面,均需要实现点击具体按钮的操作。
虽然我此时对NotificationListenerService不熟悉,但是一听到这个反常识的操作,我顿时觉得不现实,至少是需要一些黑科技才能在部分设备实现这个效果。因为操作UI需要在主线程,但是系统当前的主线程可能在其它进程,所以我觉得这个地方反常识了!


三、实现方案之通知监听服务




由于上面的沟通过程因为我不熟悉 NotificationListenerService导致我battle的时候都不敢大声说话,因此我决定去熟悉一下,然后我看到了黄老师的这篇 Android通知监听服务之NotificationListenerService使用篇


看到黄老师实现微信抢红包以后,我也心动了,既然黄老师可以抢红包,那么是不是我也可以抢他的红包?于是就开始了踩坑之旅。


3.1 通知栏的那些事


我们知道,通知栏的显示、刷新、关闭都是依赖于Notification来实现,而通知栏的UI要么是依托系统主题实现,要么是通过自定义RemoteViews实现,而UI的交互则是通过PendingIntent包装的Intent来实现具体的意图。


// 通知栏的`UI`依托系统主题实现
NotificationCompat.Builder(context, Notification.CHANNEL_ID)
.setStyle(androidx.media.app.NotificationCompat.MediaStyle()
// show only play/pause in compact view
.setShowActionsInCompactView(playPauseButtonPosition)
.setShowCancelButton(true)
.setCancelButtonIntent(mStopIntent)
.setMediaSession(mediaSession)
)
.setDeleteIntent(mStopIntent)
.setColorized(true)
.setSmallIcon(smallIcon)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setOnlyAlertOnce(true)
.setContentTitle(songInfo?.songName) //歌名
.setContentText(songInfo?.artist) //艺术家
.setLargeIcon(art)


/**
* 创建RemoteViews
*/

private fun createRemoteViews(isBigRemoteViews: Boolean): RemoteViews {
val remoteView: RemoteViews = if (isBigRemoteViews) {
RemoteViews(packageName, LAYOUT_NOTIFY_BIG_PLAY.getResLayout())
} else {
RemoteViews(packageName, LAYOUT_NOTIFY_PLAY.getResLayout())
}
remoteView.setOnClickPendingIntent(ID_IMG_NOTIFY_PLAY.getResId(), playIntent)
remoteView.setOnClickPendingIntent(ID_IMG_NOTIFY_PAUSE.getResId(), pauseIntent)
remoteView.setOnClickPendingIntent(ID_IMG_NOTIFY_STOP.getResId(), stopIntent)
remoteView.setOnClickPendingIntent(ID_IMG_NOTIFY_FAVORITE.getResId(), favoriteIntent)
remoteView.setOnClickPendingIntent(ID_IMG_NOTIFY_LYRICS.getResId(), lyricsIntent)
remoteView.setOnClickPendingIntent(ID_IMG_NOTIFY_DOWNLOAD.getResId(), downloadIntent)
remoteView.setOnClickPendingIntent(ID_IMG_NOTIFY_NEXT.getResId(), nextIntent)
remoteView.setOnClickPendingIntent(ID_IMG_NOTIFY_PRE.getResId(), previousIntent)
remoteView.setOnClickPendingIntent(ID_IMG_NOTIFY_CLOSE.getResId(), closeIntent)
remoteView.setOnClickPendingIntent(ID_IMG_NOTIFY_PLAY_OR_PAUSE.getResId(), playOrPauseIntent)
return remoteView
}

// 通过自定义`RemoteViews`实现
val notificationBuilder = NotificationCompat.Builder(context, CHANNEL_ID)
notificationBuilder
.setOnlyAlertOnce(true)
.setSmallIcon(smallIcon)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentTitle(songInfo?.songName) //歌名
.setContentText(songInfo?.artist) //艺术家

1. StatusBarNotification的逆向之旅


有了上面的了解,那么我们可以考虑通过Notification来获取PendingIntent,实现通知栏模拟点击的效果。
通过NotificationListenerService的回调方法,我们可以获得StatusBarNotification,源码如下:


override fun onNotificationPosted(sbn: StatusBarNotification?) {
super.onNotificationPosted(sbn)
}

接下来,我们需要从这个地方开始,抽丝剥茧般地一步一步找到我们想要的PendingIntent
先观察一下StatusBarNotification的源码:


@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) 
private final Notification notification;

public StatusBarNotification(String pkg, String opPkg, int id,
String tag, int uid, int initialPid, Notification notification, UserHandle user,
String overrideGr0upKey, long postTime) {
if (pkg == null) throw new NullPointerException();
if (notification == null) throw new NullPointerException();

this.pkg = pkg;
this.opPkg = opPkg;
this.id = id;
this.tag = tag;
this.uid = uid;
this.initialPid = initialPid;
this.notification = notification;
this.user = user;
this.postTime = postTime;
this.overrideGr0upKey = overrideGr0upKey;
this.key = key();
this.groupKey = groupKey();
}

/**
* The {@link android.app.Notification} supplied to
* {@link android.app.NotificationManager#notify(int, Notification)}.
*/
public Notification getNotification() {
return notification;
}

这里我们可以直接获取到Notification这个对象,然后我们继续观察源码,


/**
* The view that will represent this notification in the notification list (which is pulled
* down from the status bar).
*
* As of N, this field may be null. The notification view is determined by the inputs
* to {@link Notification.Builder}; a custom RemoteViews can optionally be
* supplied with {@link Notification.Builder#setCustomContentView(RemoteViews)}.
*/

@Deprecated
public RemoteViews contentView;

虽然这个contentView已经标记为不建议使用了,但是我们可以先尝试跑通流程。然后再将这个思路拓展到非自定义RemoteViews的流程。
经过测试,这里我们已经可以获取到RemoteViews了。按照惯例,这里我们需要继续观察RemoteViews的源码,从设置点击事件开始:


public void setOnClickPendingIntent(@IdRes int viewId, PendingIntent pendingIntent) {
setOnClickResponse(viewId, RemoteResponse.fromPendingIntent(pendingIntent));
}
// 👇
public static class RemoteResponse {
private PendingIntent mPendingIntent;
public static RemoteResponse fromPendingIntent(@NonNull PendingIntent pendingIntent) {
RemoteResponse response = new RemoteResponse();
response.mPendingIntent = pendingIntent;
return response;
}

}
// 👆
public void setOnClickResponse(@IdRes int viewId, @NonNull RemoteResponse response) {
addAction(new SetOnClickResponse(viewId, response));
}


// 响应事件 👆
private class SetOnClickResponse extends Action {

SetOnClickResponse(@IdRes int id, RemoteResponse response) {
this.viewId = id;
this.mResponse = response;
}

SetOnClickResponse(Parcel parcel) {
viewId = parcel.readInt();
mResponse = new RemoteResponse();
mResponse.readFromParcel(parcel);
}

public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(viewId);
mResponse.writeToParcel(dest, flags);
}

@Override
public void apply(View root, ViewGr0up rootParent, final InteractionHandler handler,
ColorResources colorResources)
{
final View target = root.findViewById(viewId);
if (target == null) return;

if (mResponse.mPendingIntent != null) {
// If the view is an AdapterView, setting a PendingIntent on click doesn't make
// much sense, do they mean to set a PendingIntent template for the
// AdapterView's children?
if (hasFlags(FLAG_WIDGET_IS_COLLECTION_CHILD)) {
Log.w(LOG_TAG, "Cannot SetOnClickResponse for collection item "
+ "(id: " + viewId + ")");
ApplicationInfo appInfo = root.getContext().getApplicationInfo();

// We let this slide for HC and ICS so as to not break compatibility. It should
// have been disabled from the outset, but was left open by accident.
if (appInfo != null
&& appInfo.targetSdkVersion >= Build.VERSION_CODES.JELLY_BEAN) {
return;
}
}
target.setTagInternal(R.id.pending_intent_tag, mResponse.mPendingIntent);
} else if (mResponse.mFillIntent != null) {
if (!hasFlags(FLAG_WIDGET_IS_COLLECTION_CHILD)) {
Log.e(LOG_TAG, "The method setOnClickFillInIntent is available "
+ "only from RemoteViewsFactory (ie. on collection items).");
return;
}
if (target == root) {
// Target is a root node of an AdapterView child. Set the response in the tag.
// Actual click handling is done by OnItemClickListener in
// SetPendingIntentTemplate, which uses this tag information.
target.setTagInternal(com.android.internal.R.id.fillInIntent, mResponse);
return;
}
} else {
// No intent to apply, clear the listener and any tags that were previously set.
target.setOnClickListener(null);
target.setTagInternal(R.id.pending_intent_tag, null);
target.setTagInternal(com.android.internal.R.id.fillInIntent, null);
return;
}
target.setOnClickListener(v -> mResponse.handleViewInteraction(v, handler));
}

@Override
public int getActionTag() {
return SET_ON_CLICK_RESPONSE_TAG;
}

final RemoteResponse mResponse;
}




private void addAction(Action a) {
if (hasMultipleLayouts()) {
throw new RuntimeException("RemoteViews specifying separate layouts for orientation"
+ " or size cannot be modified. Instead, fully configure each layouts"
+ " individually before constructing the combined layout.");
}
if (mActions == null) {
mActions = new ArrayList<>();
}
mActions.add(a);
}


上面代码有点多,我画个图方便大家理解:


未命名文件.jpg


至此,我们就知道了PendingIntent的藏身之处了!
通过反射,正常情况下我们就能拿到所有属于SetOnClickResponse#PendingIntent了,上代码:


override fun onNotificationPosted(sbn: StatusBarNotification?) {
super.onNotificationPosted(sbn)
sbn?:return
if(sbn.packageName == "com.***.******"){
// 获取通知
val cls = sbn.notification.contentView.javaClass
// 点击事件容器
val field = cls.getDeclaredField("mActions")
field.isAccessible = true
// 点击事件容器对象
val result = field.get(sbn.notification.contentView)
// 强转
(result as? ArrayList<Any>?)?.let { list ->
// 筛选点击事件的实现类集合
// 此处需要判断具体的点击事件
list.filter { item -> item.javaClass.simpleName == "SetOnClickResponse" }.first().let { item ->
// 获取响应对象
val response = item.javaClass.getDeclaredField("mResponse")
response.isAccessible = true
// 强转
(response.get(item) as? RemoteViews.RemoteResponse)?.let { remoteResponse ->
// 获取PendingIntent
val intentField = remoteResponse.javaClass.getDeclaredField("mPendingIntent")
intentField.isAccessible = true
val target = intentField.get(remoteResponse) as PendingIntent
Log.e("NotificationMonitorService","最终目标:${Gson().toJson(target)}")
}

}
}

}

}

2. 反射的拦路鬼——Android平台限制对非SDK接口的调用


不出意外的还是有了意外,明明反射的字段存在,就是反射获取不到。


反射失败.png
就在一筹莫展之际,有朋友提出了一个思路——针对非SDK接口的限制。然后经过查询,果然是反射失败的罪魁祸首!


WechatIMG889.png
既然确诊了病症,那么就可以开始开方抓药了!
根据轮子bypassHiddenApiRestriction绕过 Android 9以上非SDK接口调用限制的方法,我们成功的获取到了PendingIntent.


override fun onNotificationPosted(sbn: StatusBarNotification?) {
super.onNotificationPosted(sbn)
sbn?:return
if(sbn.packageName == "com.lzx.starrysky"){
// 获取通知
val cls = sbn.notification.contentView.javaClass
// 点击事件容器
val field = cls.getDeclaredField("mActions")
field.isAccessible = true
// 点击事件容器对象
val result = field.get(sbn.notification.contentView)
// 强转
(result as? ArrayList<Any>?)?.let { list ->
// 筛选点击事件的实现类集合
// 此处需要判断具体的点击事件
list.filter { item -> item.javaClass.simpleName == "SetOnClickResponse" }.forEach { item ->
// 获取响应对象
val response = item.javaClass.getDeclaredField("mResponse")
response.isAccessible = true
// 强转
(response.get(item) as? RemoteViews.RemoteResponse)?.let { remoteResponse ->
// 获取PendingIntent
val intentField = remoteResponse.javaClass.getDeclaredField("mPendingIntent")
intentField.isAccessible = true
val target = intentField.get(remoteResponse) as PendingIntent
Log.e("NotificationMonitorService","最终目标:${Gson().toJson(target)}")
}

}
}

}

}

WechatIMG892.jpeg
这里的筛选结果有十几个点击事件的响应对象,我们需要做的就是一个一个的去尝试,找到那个目标对象的pendingIntent,通过调用send方法就可以实现模拟点击的效果了!


...
// 获取PendingIntent
val intentField = remoteResponse.javaClass.getDeclaredField("mPendingIntent")
intentField.isAccessible = true
val target = intentField.get(remoteResponse) as PendingIntent
Log.e("NotificationMonitorService","最终目标:${Gson().toJson(target)}")
// 延迟实现点击功能
Handler(Looper.getMainLooper()).postDelayed({
target.send()
},500)

总结




综上,如果第三方应用的通知栏UI是自定义View的话,那么这里的方案是可以直接使用;如果第三方应用的通知栏UI使用的是系统主题,那么按照这个思路应该也可以通过反射实现。
步骤如下:





    1. 接入第三方轮子bypassHiddenApiRestriction(PS:远程依赖的时候使用并未成功,我将项目clone下来打包为aar,导入项目后使用正常!),并初始化:




HiddenApiBypass.startBypass()




    1. AndroidManifest中注册NotificationListenerService,然后启动服务




private fun startService(){
if (NotificationManagerCompat.getEnabledListenerPackages(this).contains(packageName)){
val intent = Intent(this,NotificationMonitorService::class.java)
startService(intent)
}else{
startActivity(Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"))
}

}


  • 3.在NotificationListenerService监听通知栏


override fun onNotificationPosted(sbn: StatusBarNotification?) {
super.onNotificationPosted(sbn)
sbn?:return
if(sbn.packageName == "com.***.******"){
// 获取通知
val cls = sbn.notification.contentView.javaClass
// 点击事件容器
val field = cls.getDeclaredField("mActions")
field.isAccessible = true
// 点击事件容器对象
val result = field.get(sbn.notification.contentView)
// 强转
(result as? ArrayList<Any>?)?.let { list ->
// 筛选点击事件的实现类集合
// 此处需要判断具体的点击事件
list.filter { item -> item.javaClass.simpleName == "SetOnClickResponse" }.first().let { item ->
// 获取响应对象
val response = item.javaClass.getDeclaredField("mResponse")
response.isAccessible = true
// 强转
(response.get(item) as? RemoteViews.RemoteResponse)?.let { remoteResponse ->
// 获取PendingIntent
val intentField = remoteResponse.javaClass.getDeclaredField("mPendingIntent")
intentField.isAccessible = true
val target = intentField.get(remoteResponse) as PendingIntent
Log.e("NotificationMonitorService","最终目标:${Gson().toJson(target)}")
// 延迟实现点击功能
Handler(Looper.getMainLooper()).postDelayed({
target.send()
},500)
}

}
}

}

}

参考:


Android通知监听服务之NotificationListenerService使用篇


另一种绕过Android 9以上非SDK接口调用限制的方法


作者:苏灿烤鱼
来源:juejin.cn/post/7190280650283778106
收起阅读 »

算法(TS):只出现一次的数字

web
给你一个非空整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。 你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。 上题要求时间复杂度为 O(n),空间复杂度为O(1) 解法一...
继续阅读 »

给你一个非空整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。


你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。



上题要求时间复杂度为 O(n),空间复杂度为O(1)



解法一:维护一个 Set 对象


创建一个空的 Set 对象,遍历数组 nums,每遍历出一个 num,便在 Set 对象中查找是否存在它,不存在则加入,存在则删除,等数组遍历结束,Set 对象中剩下的就是只出现一次的数字。


function singleNumber(nums: number[]): number {
const uniqueSet = new Set<number>()
for (const num of nums) {
if (uniqueSet.has(num)) {
uniqueSet.delete(num)
} else {
uniqueSet.add(num)
}
}

return [...uniqueSet][0]
}

存在一次遍历数组,因此时间复杂度为 O(n),uniqueSet.size的最大值为 n/2,最小为 1, 空间复杂度为O(n)


解法二:位运算符(异或)


异或运算符有下面 3 个特性



  1. a ^ 0 = a,即,任何数与数字 0 异或,得到的结果都等于它本身

  2. a ^ a = 0,即,任何数与自身异或,得到的结果都等于 0

  3. a ^ b ^ c = a ^ c ^ b,异或运算符,满足交换率


遍历 nums,让数组中的数字两两异或,最终得到的结果便是数组中只出现一次的数字


function singleNumber(nums: number[]): number {
let uniqueNumber = 0

for (const num of nums) {
uniqueNumber ^= num
}

return uniqueNumber
}

存在一次遍历数组,因此时间复杂度为 O(n),没有额外的中间量空间复杂度为O(1)


作者:何遇er
来源:juejin.cn/post/7298674250965155877
收起阅读 »

人生天地之间,若白驹之过隙,忽然而已

人生天地之间,若白驹之过隙,忽然而已 这段时间有个同事离职了,其实身边不断有老人走、有新人来,但这回走的同事和别的有些不同,当时我入职面试的时候就是他面试的我,工作中有啥问题都会请教他,转眼间一起共事三年多,亦师亦友,忽然离去,心中不免有些不舍。 同事 其实自...
继续阅读 »

人生天地之间,若白驹之过隙,忽然而已


这段时间有个同事离职了,其实身边不断有老人走、有新人来,但这回走的同事和别的有些不同,当时我入职面试的时候就是他面试的我,工作中有啥问题都会请教他,转眼间一起共事三年多,亦师亦友,忽然离去,心中不免有些不舍。


同事


其实自从工作以来,我们每天接触最多的不是家人、朋友,而是同事,毕业几年来也认识了很多同事,有很多乐于助人、兢兢业业的,当然不乏一些泛泛之辈。


对于我个人而言,一般和同事都保持不错的一个状态,但如果问我同事间的关系如何相处,那我肯定告诉你:点头之交即可,不要想着你和同事怎么怎么好,转过头来就有可能把你给卖了,这台正常不过了,除非特别熟,不然尽量别说别的。


朋友


已经很久没和朋友好好聊天,吃个饭了,工作以来虽说是双休,但周末一直有事,即使没事,也只想躺在床上躺会,早上晚起一会,起来弄点饭吃完就中午了,又想再睡会,因为在公司里没有午休的习惯,一觉起来就不早了,出去溜达溜达就到晚上了,时间、时间都去哪了,都用来睡觉、吃饭。。。。


记得高中、大学,认识不少同学,但真正从自己内心算作朋友的,其实少之又少。


高中时同宿舍、同学都在一起玩,我以为他们是我的朋友,所以即使高中毕业了,每次回到家都联系着之前同宿舍的一块出来聚一聚,第一年还出来了,之后不是这个有事就是那个有事,或许是真的有事,亦或许不想和我聚一聚,还或许根本没人把你当成朋友。


大学更是这样,大学认识的朋友屈指可数。


其实有时想想,可能并不是别人的问题,是我自己的问题。但转念想想:”人生天地之间,若白驹之过隙,忽然而已“,想那么多干啥?古人曾说:”人生得一知己足矣“,况且我还有朋友呢!


家人


其实自从上小学开始,和家人在一起的时间就没有多少了,上大学后,一年到头也见不了几次,工作后更甚。


尽力对家人好点吧,多打打电话、多打打视频,现在的技术发达,视频通话可以直接看到对方,非常方便,家人最想要的就是在外漂泊的自己能过得好一点。


瞎写


转眼间毕业工作也好几年了,自我感觉技术上没有特别巨大的提升,但是技术厚度及深度较前几年有了不小的提升,也算是一些收获。


此文因一亦师亦友的大哥离职后产生的一些瞎想,希望大哥在新的城市里顺风顺水,未来更好吧!也希望自己能够不忘初心,在技术上更上一层楼。


努力,共勉


作者:Zhujiang
来源:juejin.cn/post/7265609829091557433
收起阅读 »

Leader:组织面对不确定性的压舱石

今天来跟大家分享一下Leader在一个团队里面到底起着什么样的作用?有一个最为简单的说法,Leader就是把整个组织的所有人通过文化规则和流程组织起来,能够顺利的完成一件或者多件重要的事情的角色。 组织的流程化 我们知道,组织越大事情和人就越多,为了组织好这...
继续阅读 »

今天来跟大家分享一下Leader在一个团队里面到底起着什么样的作用?有一个最为简单的说法,Leader就是把整个组织的所有人通过文化规则和流程组织起来,能够顺利的完成一件或者多件重要的事情的角色。



组织的流程化


我们知道,组织越大事情和人就越多,为了组织好这些人和事,一个组织往往就伴随着有各种各样的标准化流程,又称之为SOP。这个也不难理解,动辄几千上万员工的组织,如果连流程都没有,混乱一团,那可想而知会是一个巨大的灾难,在沟通和协同上面就会耗费大量的成本。


所以相对稳定的公司或者规模越大的公司,它的流程就会越多,这往往导致在做一个事情的时候,进行决策的时间也会更长。流程在一定程度上是解决了很多混乱的问题,但是流程本身也会带来效率低下的问题。但是总体而言,没有流程会造成更大的问题。


那是不是在建立起了流程以后就高枕无忧了呢?如果流程非常完善的情况,Leader又只是把流程做一个管理和编排和组织,那么Leader的价值基本上体现不出来,这种流程极度完善的情况下,那么还要Leader干什么呢?因此我们当代追求的Leader不仅仅是维护和管理这些流程和员工,而应该是企业在对面不确定性事情的法宝。


核心一句话就是Leader是公司面向不确定的事情的压舱石。


面向不确定性的未来


大型组织在流程在规章制度已经比较完善的情况,很多事情员工只要按流程走就可以了,但是我们知道如果一个组织通过流程就能处理所有的事情,那我相信这应该是一个极度老化、极度传统的公司,这种公司已经没有了任何的创造力,所有员工只是机械的去执行公司的命令和流程而已。没有创造性的公司,能面对千变万化的市场吗?


一个不需要公司里面的任何人都有创造力的组织,我相信本身组织就没有创新力。这种组织可能就是非常传统的国有企业,每个人不要有创造力,你只要完成你所要处理的工作就可以了,那这种情况下也只有垄断组织才能做得到,民营企业和国际化的企业是不可能能够实现的,一招鲜吃遍天,在当前这个时代已经不存在了。


所以我认为只有灵活的能够处理不确定的业务,不确定的事情的团队和公司才有成长和逆袭的可能性。因为市场是变化的,客户也是变化的,不可能所有的东西一成不变,用一套服务,一套标准,一套产品就能够解决客户永远的问题的时代早已经一去不复返了。不确定才是当前社会的主旋律,只有一个组织能够尽快的满足变化的需求,才能够拿得出最好最新的产品,才能赢得客户。



那些不确定的事情谁来负责比较合适呢?首先作为整个公司的负责人、一把手、CEO理应是去处理市面上的变化最好的任选,但是不可能所有的变化全部由负责人一个人来处理。这些负责人只能说把握五年,十年的趋势,从这些变化的趋势里面找到确定性的方向来组织和施公司的战略。那不可能所有鸡毛蒜皮,每个分公司,每个分部门,每个普通产品线的业务,如果都要负责人来解决的话,那么这个公司直接在负责人这个层面上面就先崩溃了,比如阿里巴巴而言面对几亿的用户、几千万的商家和合作方、几十万的员工,如果都要负责人亲力亲为,这是完全不现实的。


那能否让一线的员工来处理呢?这个听起来非常美好,每个员工都是公司一份子,理应去接受市场上面的变化,但想想也是不大现实的,因为对于基层一些员工来说,他们往往只有执行的权利,而没有对于很多不确定的情况下的判断的权利。而且很多基层员工由于工作的时间不久,对整个公司的了解也不是特别深,所以往往就算给了他权限,他也没办法能够处理好这些不确定的事情,权限过大反而有可能做出错误的事情。


那公司谁是处理这些不确定的事情里面最好的对象呢?答案就是每一位Leader,也就是每一个层面上不同的Leader,比如财务Leader、业务Leader、销售Leader和技术Leader等等。


一方面公司里面的Leader是整个公司的最核心的节点,他们能够接触到各种各样从上到下的信息,另外由于大量的Leader也是基础一线的负责人,所以往往对于前线的客户需求感知非常的强烈。所以现在的Leader往往是最能够洞察到前线客户需求变化的人,同时也因为Leader一般都有实际执行的权利,通常来说Leader往往在不同规模和不同层面上都有一定的决策权,因此可以快速的组织团队去应对客户需求和市场上的变化。


这个才是Leader最核心的意义,就是要组织小团队去处理这些不确定性的事情和风险,从而给团队,给公司,给业务带来巨大的增量价值。


Leader是不确定性的压舱石


这也是为什么说Leader是整个公司面对不确定性的事情的压舱石,只有整个公司的一线Leader都具备了这样的能力,整个组织才是一个具备有活力的组织,是能够迎接创新和拥抱变化的组织。


这种模式仿佛就是一个动车组一样,每个团队就犹如一起车厢,每个车厢都有自己独特的动力闭环,能够快速的迎接变化,快速的可拆卸组装,而且能够快速的在整个更高层战略的情况下实现更灵活的组合,以应对更大的确定性,以解决更难的问题。


从一些具体的层面上来说,当较大的业务系统发生了比较大的故障的时候,客户有强烈的投诉情绪,这个时候对于一线的开发同学来说,他很难能够确定到底是什么系统引起的,这个时候就需要团队的Leader作为第一业务的接口人来跟进和负责整个事情,从而使得各个团队之间不会相互推诿。所以一旦面对一些不确定性的问题的情况下,在一线同学无法判断问题是什么样根源、什么样的上下游以及什么样的背景的情况下,Leader就是处理问题最好的应急指挥人。


我们的线上有客户提出了大量的需求的时候,我们一些同学往往没有权利去决策是否能支持这个需求。或者有流程可以让一线同学会把这个问题层层上交,一直提交到最高层来审批为止,而往往由于这样流程上的阻碍或者麻烦,所以同学就不愿意去花费这么大的精力向更高层去建议到底要做什么样的东西,大部分员工干脆就不会去提这个建议,直接当这个事情没发生就行了。


有很多非常大的公司往往会倒闭,就是因为适应不了市场上面的变化,当整个市场都发生了很大的变化的时候,这些高层往往感觉不到发生了变化,而很多时候一线的员工往往已经意识到了这些敏锐的变化,但是各种各样的机制,一线员工无法高层传递建议,进而导致了整个公司在面对极大的变化的情况下的话无法应变而轰然倒塌,诺基亚就是一个非常鲜活的例子。


而作为一线的Leader,往往就能够敏锐的去捕获这些信息,而且在整个公司制度流程比较有保障的情况下的话,也能够放权给一线Leader快速做出决策,或者把这些问题快速的反馈给更高层的领导。


比如我在公司的时候就经常去和客户进行共创,就会经常被客户问到我们为什么PC上面还没有定制工作台的能力,而那个时候我们在2018年就有了移动端的能力,但是由于战略方向的问题,我们迟迟没有在PC上面去补足这个短板,进而反复被竞争对手攻击。我接收到了大量的来自于一线客户的反馈,我才做了一个决策帮助整个公司补足了在这一个产品上的短板,进而吸引到大量的增量客户,不仅创造了客户的活跃价值,还创造了商业化的价值。这就是Leader在足够授权下能替组织带来的增量贡献,而远远非整个团队坐等事情发生更有价值。


所以总结一下,各种公司在当今的社会都会面临各种各样的挑战和不确定性。一个公司如何来应对这些挑战和不确定确定性的能力就就决定了这个公司的命运。而面对这些不确定的核心就在于我们组织里面的关键的Leader节点,只有当Leader带领团队主动去拥抱和解决不确定性,才能给组织带来活力,重塑组织的命运。



因此一个公司想要发展的好,就一定要授权给所有听见炮火,两腿带泥的Leader,并且要构建出信任的文化和价值感让Leader带领团队能够以最好的姿态去面对和拥抱不确定性。


作者:ali老蒋
来源:juejin.cn/post/7298635806475026468
收起阅读 »

触摸Android的心脏跳动

在Android开发中,主线程扮演着至关重要的角色。毫不夸张的说,它就相当于Android的心脏。只要它还在跳动的运行,Android应用就不会终止。 它负责处理UI事件、界面更新、以及与用户交互的各种操作。本文将深入分析Android主线程的原理、独特机制以...
继续阅读 »

在Android开发中,主线程扮演着至关重要的角色。毫不夸张的说,它就相当于Android的心脏。只要它还在跳动的运行,Android应用就不会终止。


它负责处理UI事件、界面更新、以及与用户交互的各种操作。本文将深入分析Android主线程的原理、独特机制以及应用,为开发者提供全面的了解和掌握主线程的知识。


主线程的原理


Android应用的核心原则之一是单线程模型,也就是说,大多数与用户界面相关的操作都必须在主线程中执行。这一原则的背后是Android操作系统的设计,主要有以下几个原因:




  • UI一致性:在单线程模型下,UI操作不会被多线程竞争导致的不一致性问题,确保了用户界面的稳定性和一致性。




  • 性能优化:单线程模型简化了线程管理,降低了多线程带来的复杂性,有助于提高应用性能。




  • 安全性:通过将UI操作限制在主线程,可以减少因多线程竞争而引发的潜在问题,如死锁和竞争条件。




主线程的原理可以用以下伪代码表示:


public class MainThread {
public static void main(String[] args) {
// 初始化应用
Application app = createApplication();

// 创建主线程消息循环
Looper.prepareMainLooper();

// 启动主线程
while (true) {
Message msg = Looper.getMainLooper().getNextMessage();
if (msg != null) {
// 处理消息
app.handleMessage(msg);
}
}
}
}

在上述伪代码中,主线程通过消息循环(Message Loop)来不断处理消息,这些消息通常包括UI事件、定时任务等。应用的UI操作都会被封装成消息,然后由主线程依次处理。


主线程的独特机制


主线程有一些独特的机制,其中最重要的是消息队列(Message Queue)和Handler。


消息队列(Message Queue)


消息队列是主线程用来存储待处理消息的数据结构。每个消息都有一个与之相关的Handler,它负责将消息放入队列中,然后由主线程依次处理。消息队列的机制确保了消息的有序性和及时性。


public Message next() {
final long ptr = mPtr;
if (ptr == 0) {
return null;
}

int pendingIdleHandlerCount = -1;
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}

nativePollOnce(ptr, nextPollTimeoutMillis);

synchronized (this) {
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
if (msg != null && msg.target == null) {
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
if (now < msg.when) {
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
msg.markInUse();
return msg;
}
} else {
nextPollTimeoutMillis = -1;
}

...
}

...
}
}

Handler


Handler是一个与特定线程关联的对象,它可以用来发送和处理消息。在主线程中,通常使用new Handler(Looper.getMainLooper())来创建一个与主线程关联的Handler。开发者可以使用Handler来将任务提交到主线程的消息队列中。


Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
// 在主线程执行
}
});

同步屏障


在Android中,消息可以分为同步消息和异步消息。通常,我们发送的消息都是同步消息。
然而,有一种特殊情况,即开启同步屏障。同步屏障是一种消息机制的特性,可以阻止同步消息的处理,只允许异步消息通过。通过调用MessageQueue的postSyncBarrier()方法,可以开启同步屏障。在开启同步屏障后,发送的这条消息它的target为null。


    private int postSyncBarrier(long when) {
synchronized (this) {
final int token = mNextBarrierToken++;
final Message msg = Message.obtain();
msg.markInUse();
// 没有设置target,target为null
msg.when = when;
msg.arg1 = token;

Message prev = null;
Message p = mMessages;
if (when != 0) {
while (p != null && p.when <= when) {
prev = p;
p = p.next;
}
}
if (prev != null) { // invariant: p == prev.next
msg.next = p;
prev.next = msg;
} else {
msg.next = p;
mMessages = msg;
}
return token;
}
}

那么,开启同步屏障后,所谓的异步消息又是如何被处理的呢?
我们又可以回到之前MessageQueue中的next方法了


public Message next() {
// 省略部分代码,只体现出来同步屏障的代码
...
for (;;) {
...

synchronized (this) {
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
//注意这里,开始出来同步屏障
//如果target==null,认为它就是屏障,进行循环遍历,直到找到第一个异步的消息
if (msg != null && msg.target == null) {
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}

...
}

...
}
}

所以同步屏障是会让消息顺序进行调整,让其忽略现有的同步消息,来直接处理临近的异步消息。
现在听起来已经知道了同步屏障的作用,但它的实际应用又有哪些呢?


应用场景


虽然在日常应用开发中,同步屏障的使用频率较低,但在Android系统源码中,同步屏障的使用场景非常重要。一个典型的使用场景是在UI更新时,例如在View的绘制、布局调整、刷新等操作中,系统会开启同步屏障,以确保与UI相关的异步消息得到优先处理。当UI更新完成后,同步屏障会被移除,允许后续的同步消息得以处理。


对应的是ViewRootImpl#scheduleTraversals()


    void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
// 设置同步屏障
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}

void unscheduleTraversals() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
// 移除同步屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
mChoreographer.removeCallbacks(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}


经典问题


Android 主线程的消息循环是通过 LooperHandler 来实现的。以下是一段伪代码示例:


// 创建一个 Looper,关联当前线程
Looper.prepare();
Looper loop = Looper.myLooper();

// 创建一个 Handler,它将和当前 Looper 关联
Handler handler = new Handler();

// 进入消息循环
Looper.loop();

开启loop后的核心代码如下:


    public static void loop() {
final Looper me = myLooper();
...
for (;;) {
if (!loopOnce(me, ident, thresholdOverride)) {
return;
}
}
}

private static boolean loopOnce(final Looper me,
final long ident, final int thresholdOverride)
{
// 注意没消息会被阻塞,进入休眠状态
Message msg = me.mQueue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return false;
}

...

try {
msg.target.dispatchMessage(msg);
if (observer != null) {
observer.messageDispatched(token, msg);
}
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
} catch (Exception exception) {
if (observer != null) {
observer.dispatchingThrewException(token, msg, exception);
}
throw exception;
} finally {
ThreadLocalWorkSource.restore(origWorkSource);
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
...
msg.recycleUnchecked();

return true;
}


在这段示例中,主线程的消息循环被启动,它会等待来自消息队列的消息。有了这个基础下面的问题就简单了:




  1. 为什么主线程不会陷入无限循环?


    主线程的消息循环不会陷入无限循环,因为它不断地从消息队列中获取消息并处理它们。如果没有消息要处理,消息循环会进入休眠状态,不会持续消耗 CPU 资源。只有在有新消息到达时,主线程才会被唤醒来处理这些消息。这个机制确保主线程能够响应用户的操作,而不陷入死循环。




  2. 如果没有消息,主线程会如何处理?


    如果消息队列为空,主线程的消息循环会等待,直到有新消息到达。在等待期间,它不会执行任何操作,也不会陷入循环。这是因为 Android 的消息循环是基于事件驱动的,只有当有事件(消息)到达时,才会触发主线程执行相应的处理代码。当新消息被投递到消息队列后,主线程会被唤醒,执行相应的处理操作,然后再次进入等待状态。




这种事件驱动的消息循环机制使得 Android 应用能够高效地管理用户交互和异步操作,同时保持了响应性和低能耗。所以,主线程不会陷入无限循环,而是在需要处理事件时才会执行相应的代码。


结论


Android主线程是应用的核心,负责处理UI事件、界面更新和定时任务等。了解主线程的原理和独特机制是Android开发的关键,它有助于确保应用的稳定性和性能。通过消息队列和Handler,开发者可以在主线程中安全地处理各种任务,提供流畅的用户体验。


推荐


android_startup: 提供一种在应用启动时能够更加简单、高效的方式来初始化组件,优化启动速度。不仅支持Jetpack App Startup的全部功能,还提供额外的同步与异步等待、线程控制与多进程支持等功能。


AwesomeGithub: 基于Github的客户端,纯练习项目,支持组件化开发,支持账户密码与认证登陆。使用Kotlin语言进行开发,项目架构是基于JetPack&DataBinding的MVVM;项目中使用了Arouter、Retrofit、Coroutine、Glide、Dagger与Hilt等流行开源技术。


flutter_github: 基于Flutter的跨平台版本Github客户端,与AwesomeGithub相对应。


android-api-analysis: 结合详细的Demo来全面解析Android相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点。


daily_algorithm: 每日一算法,由浅入深,欢迎加入一起共勉。


作者:午后一小憩
来源:juejin.cn/post/7296692742876758027
收起阅读 »

触摸Android的心脏跳动

在Android开发中,主线程扮演着至关重要的角色。毫不夸张的说,它就相当于Android的心脏。只要它还在跳动的运行,Android应用就不会终止。 它负责处理UI事件、界面更新、以及与用户交互的各种操作。本文将深入分析Android主线程的原理、独特机制以...
继续阅读 »

在Android开发中,主线程扮演着至关重要的角色。毫不夸张的说,它就相当于Android的心脏。只要它还在跳动的运行,Android应用就不会终止。


它负责处理UI事件、界面更新、以及与用户交互的各种操作。本文将深入分析Android主线程的原理、独特机制以及应用,为开发者提供全面的了解和掌握主线程的知识。


主线程的原理


Android应用的核心原则之一是单线程模型,也就是说,大多数与用户界面相关的操作都必须在主线程中执行。这一原则的背后是Android操作系统的设计,主要有以下几个原因:




  • UI一致性:在单线程模型下,UI操作不会被多线程竞争导致的不一致性问题,确保了用户界面的稳定性和一致性。




  • 性能优化:单线程模型简化了线程管理,降低了多线程带来的复杂性,有助于提高应用性能。




  • 安全性:通过将UI操作限制在主线程,可以减少因多线程竞争而引发的潜在问题,如死锁和竞争条件。




主线程的原理可以用以下伪代码表示:


public class MainThread {
public static void main(String[] args) {
// 初始化应用
Application app = createApplication();

// 创建主线程消息循环
Looper.prepareMainLooper();

// 启动主线程
while (true) {
Message msg = Looper.getMainLooper().getNextMessage();
if (msg != null) {
// 处理消息
app.handleMessage(msg);
}
}
}
}

在上述伪代码中,主线程通过消息循环(Message Loop)来不断处理消息,这些消息通常包括UI事件、定时任务等。应用的UI操作都会被封装成消息,然后由主线程依次处理。


主线程的独特机制


主线程有一些独特的机制,其中最重要的是消息队列(Message Queue)和Handler。


消息队列(Message Queue)


消息队列是主线程用来存储待处理消息的数据结构。每个消息都有一个与之相关的Handler,它负责将消息放入队列中,然后由主线程依次处理。消息队列的机制确保了消息的有序性和及时性。


public Message next() {
final long ptr = mPtr;
if (ptr == 0) {
return null;
}

int pendingIdleHandlerCount = -1;
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}

nativePollOnce(ptr, nextPollTimeoutMillis);

synchronized (this) {
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
if (msg != null && msg.target == null) {
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
if (now < msg.when) {
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
msg.markInUse();
return msg;
}
} else {
nextPollTimeoutMillis = -1;
}

...
}

...
}
}

Handler


Handler是一个与特定线程关联的对象,它可以用来发送和处理消息。在主线程中,通常使用new Handler(Looper.getMainLooper())来创建一个与主线程关联的Handler。开发者可以使用Handler来将任务提交到主线程的消息队列中。


Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
// 在主线程执行
}
});

同步屏障


在Android中,消息可以分为同步消息和异步消息。通常,我们发送的消息都是同步消息。
然而,有一种特殊情况,即开启同步屏障。同步屏障是一种消息机制的特性,可以阻止同步消息的处理,只允许异步消息通过。通过调用MessageQueue的postSyncBarrier()方法,可以开启同步屏障。在开启同步屏障后,发送的这条消息它的target为null。


    private int postSyncBarrier(long when) {
synchronized (this) {
final int token = mNextBarrierToken++;
final Message msg = Message.obtain();
msg.markInUse();
// 没有设置target,target为null
msg.when = when;
msg.arg1 = token;

Message prev = null;
Message p = mMessages;
if (when != 0) {
while (p != null && p.when <= when) {
prev = p;
p = p.next;
}
}
if (prev != null) { // invariant: p == prev.next
msg.next = p;
prev.next = msg;
} else {
msg.next = p;
mMessages = msg;
}
return token;
}
}

那么,开启同步屏障后,所谓的异步消息又是如何被处理的呢?
我们又可以回到之前MessageQueue中的next方法了


public Message next() {
// 省略部分代码,只体现出来同步屏障的代码
...
for (;;) {
...

synchronized (this) {
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
//注意这里,开始出来同步屏障
//如果target==null,认为它就是屏障,进行循环遍历,直到找到第一个异步的消息
if (msg != null && msg.target == null) {
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}

...
}

...
}
}

所以同步屏障是会让消息顺序进行调整,让其忽略现有的同步消息,来直接处理临近的异步消息。
现在听起来已经知道了同步屏障的作用,但它的实际应用又有哪些呢?


应用场景


虽然在日常应用开发中,同步屏障的使用频率较低,但在Android系统源码中,同步屏障的使用场景非常重要。一个典型的使用场景是在UI更新时,例如在View的绘制、布局调整、刷新等操作中,系统会开启同步屏障,以确保与UI相关的异步消息得到优先处理。当UI更新完成后,同步屏障会被移除,允许后续的同步消息得以处理。


对应的是ViewRootImpl#scheduleTraversals()


    void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
// 设置同步屏障
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}

void unscheduleTraversals() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
// 移除同步屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
mChoreographer.removeCallbacks(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}


经典问题


Android 主线程的消息循环是通过 LooperHandler 来实现的。以下是一段伪代码示例:


// 创建一个 Looper,关联当前线程
Looper.prepare();
Looper loop = Looper.myLooper();

// 创建一个 Handler,它将和当前 Looper 关联
Handler handler = new Handler();

// 进入消息循环
Looper.loop();

开启loop后的核心代码如下:


    public static void loop() {
final Looper me = myLooper();
...
for (;;) {
if (!loopOnce(me, ident, thresholdOverride)) {
return;
}
}
}

private static boolean loopOnce(final Looper me,
final long ident, final int thresholdOverride)
{
// 注意没消息会被阻塞,进入休眠状态
Message msg = me.mQueue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return false;
}

...

try {
msg.target.dispatchMessage(msg);
if (observer != null) {
observer.messageDispatched(token, msg);
}
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
} catch (Exception exception) {
if (observer != null) {
observer.dispatchingThrewException(token, msg, exception);
}
throw exception;
} finally {
ThreadLocalWorkSource.restore(origWorkSource);
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
...
msg.recycleUnchecked();

return true;
}


在这段示例中,主线程的消息循环被启动,它会等待来自消息队列的消息。有了这个基础下面的问题就简单了:




  1. 为什么主线程不会陷入无限循环?


    主线程的消息循环不会陷入无限循环,因为它不断地从消息队列中获取消息并处理它们。如果没有消息要处理,消息循环会进入休眠状态,不会持续消耗 CPU 资源。只有在有新消息到达时,主线程才会被唤醒来处理这些消息。这个机制确保主线程能够响应用户的操作,而不陷入死循环。




  2. 如果没有消息,主线程会如何处理?


    如果消息队列为空,主线程的消息循环会等待,直到有新消息到达。在等待期间,它不会执行任何操作,也不会陷入循环。这是因为 Android 的消息循环是基于事件驱动的,只有当有事件(消息)到达时,才会触发主线程执行相应的处理代码。当新消息被投递到消息队列后,主线程会被唤醒,执行相应的处理操作,然后再次进入等待状态。




这种事件驱动的消息循环机制使得 Android 应用能够高效地管理用户交互和异步操作,同时保持了响应性和低能耗。所以,主线程不会陷入无限循环,而是在需要处理事件时才会执行相应的代码。


结论


Android主线程是应用的核心,负责处理UI事件、界面更新和定时任务等。了解主线程的原理和独特机制是Android开发的关键,它有助于确保应用的稳定性和性能。通过消息队列和Handler,开发者可以在主线程中安全地处理各种任务,提供流畅的用户体验。


推荐


android_startup: 提供一种在应用启动时能够更加简单、高效的方式来初始化组件,优化启动速度。不仅支持Jetpack App Startup的全部功能,还提供额外的同步与异步等待、线程控制与多进程支持等功能。


AwesomeGithub: 基于Github的客户端,纯练习项目,支持组件化开发,支持账户密码与认证登陆。使用Kotlin语言进行开发,项目架构是基于JetPack&DataBinding的MVVM;项目中使用了Arouter、Retrofit、Coroutine、Glide、Dagger与Hilt等流行开源技术。


flutter_github: 基于Flutter的跨平台版本Github客户端,与AwesomeGithub相对应。


android-api-analysis: 结合详细的Demo来全面解析Android相关的知识点, 帮助读者能够更快的掌握与理解所阐述的要点。


daily_algorithm: 每日一算法,由浅入深,欢迎加入一起共勉。


作者:午后一小憩
来源:juejin.cn/post/7296692742876758027
收起阅读 »

利用腾讯地图实现地图选点功能

web
基于腾讯地图组件实现地图选点功能 使用到了腾讯地图官提供的组件,实现了地图选点 <template> <iframe id="mapPage" width="100%" height="100%" frameborder="0" src...
继续阅读 »

基于腾讯地图组件实现地图选点功能


使用到了腾讯地图官提供的组件,实现了地图选点


image.png


<template>
<iframe id="mapPage" width="100%" height="100%" frameborder="0" src="https://apis.map.qq.com/tools/locpicker?search=1&type=1&key=你自己申请的KEY&referer=myapp"></iframe>
</template>

<script setup>
import { ref } from 'vue'

const key = '自己申请到的Key'

window.addEventListener(
'message',
function (event) {
// 接收位置信息,用户选择确认位置点后选点组件会触发该事件,回传用户的位置信息
var loc = event.data
if (loc && loc.module == 'locationPicker') {
//防止其他应用也会向该页面post信息,需判断module是否为'locationPicker'
// loc 这里面存放详细的位置信息
emit('addressInfo', loc)
}
},
true
)
</script>

<style lang="scss">
// 样式自己去修改,可以使用到样式渗透
:deep(.map-wrap) {
height: 60%;
}
</style>

我是将这个代码封装成了组件,在使用的地方直接调用就可以.


其中:


window.addEventListener(
'message',
function (event) {
// 接收位置信息,用户选择确认位置点后选点组件会触发该事件,回传用户的位置信息
var loc = event.data
if (loc && loc.module == 'locationPicker') {
//防止其他应用也会向该页面post信息,需判断module是否为'locationPicker'
// loc 这里面存放详细的位置信息
emit('addressInfo', loc)
}
},
true
)

当这段代码被执行时,它会添加一个事件监听器,用于监听浏览器窗口的message事件。
第一个参数是要监听的事件类型,这里是message,表示监听来自窗口的消息事件。


第二个参数是一个回调函数,当message事件被触发时,回调函数会被执行。


在回调函数中,它首先通过event.data获取传递过来的数据,并将其保存在一个变量loc中。


接下来,通过判断loc对象中的module属性是否为locationPicker来确定这个消息是否来自选点组件。这样做的目的是为了避免处理来自其他应用程序的消息。


如果条件满足,即该消息确实来自选点组件,则会触发一个自定义的事件addressInfo,并将loc对象作为参数传递给该事件。这可以通过一个emit函数来实现,该函数的作用是触发指定名称的事件,并传递相关的数据。这样其他部分的代码就可以订阅并处理addressInfo事件,从而获取位置信息并执行相应的逻辑。


当你在地图选点后点击下面的信息就能看到对应的数据了。


image.png


基于腾讯地图实现地图选点功能(手写)


这是最终实现的效果:


image.png
有时候的腾讯地图组件的选点功能会稳定,或者失效,显示列表更新失败这就导致可能用户使用感受较差,有时候就必须手写一份,下面的代码是手写的代码以及对应的代码说明:👇👇👇👇


1.首先你需要在项目的html文件引入腾讯地图(vue3的项目)


image.png


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
<script charset="utf-8" src="https://map.qq.com/api/js?v=2.exp&key=这里是你申请的key"></script>
</body>
</html>


  1. 将地图选点功能封装成组件,实现代码的高复用。


<template>
<div>
<div class="container" id="container">
<img class="coordinate" src="@/assets/坐标.png" alt="图片加载失败" />
</div>
</div>
</template>

<script setup>
import { onMounted, reactive } from 'vue'
import axios from 'axios'

//地图
const dataMap = reactive({
map: '',
markerLayer: '',
latitude: '', //纬度
lngitude: '', //经度
})

onMounted(() => {
getlocation() //获取经纬度
initMap() // 初始化地图
})

//初始化地图
const initMap = () => {
dataMap.map = new qq.maps.Map(document.getElementById('container'), {
center: new qq.maps.LatLng(45.190524, 124.797766), //设置地图中心点坐标
zoom: 20, //设置地图缩放级别
})
qq.maps.event.addListener(dataMap.map, 'center_changed', center_changed)
}
// 监听地图拖动获取经纬度
const center_changed = () => {
dataMap.latitude = dataMap.map.getCenter().lat
dataMap.lngitude = dataMap.map.getCenter().lng
console.log('选点后的经纬度:', dataMap.latitude, dataMap.lngitude)
}

//获取经纬度
const getlocation = async () => {
const res = await axios.get('/api', {
params: {
key: '自己申请的key',
},
})
dataMap.map.setCenter(new qq.maps.LatLng(res.data.result.location.lat, res.data.result.location.lng))
}
</script>

<style lang="scss" scoped>
.container {
box-sizing: border-box;
margin: 50px;
width: 800px;
height: 400px;
border: 1px solid #999;

.coordinate {
z-index: 9999;
position: relative;
top: 50%;
left: 50%;
}
}
</style>


  1. 其中中心点的图片是自己设置上的,下面给提供了这个图片的地址,大家可以下载使用


坐标.png
图片地址:img1.imgtp.com/2023/11/08/…


中心点的位置是根据定位设置的,如果大家使用的容器的宽度和高度和我的不一样,需要手动的设置。


作者:LuHang
来源:juejin.cn/post/7298361908443463734
收起阅读 »

Android通知栏增加快捷开关的技术实现

我们通常可以在通知栏上看到“飞行模式”、“移动数据”、“屏幕录制”等开关按钮,这些按钮都属于通知栏上的快捷开关,点击快捷开关可以轻易调用某种系统能力或打开某个应用程序的特定页面。那是否可以在通知栏上自定义一个快捷开关呢?答案是可以的,具体是通过TileServ...
继续阅读 »

我们通常可以在通知栏上看到“飞行模式”、“移动数据”、“屏幕录制”等开关按钮,这些按钮都属于通知栏上的快捷开关,点击快捷开关可以轻易调用某种系统能力或打开某个应用程序的特定页面。那是否可以在通知栏上自定义一个快捷开关呢?答案是可以的,具体是通过TileService的方案实现。   


TileService继承自Service,所以它也是Android的四大组件之一,不过它是一个特殊的组件,开发者不需要手动开启调用,系统可以自动识别并完成调用,系统会通过绑定服务(bindService)的方式调用。


创建使用:


快捷开关是Android 7(target 24)的新能力,因此在使用该能力前必须先判断版本大小(大于等于target 24)。


1、自定义一个TileService类。


class MyQSTileService: TileService() {
  override fun onTileAdded() {    
super.onTileAdded()
}

  override fun onStartListening() {    
super.onStartListening()
}

  override fun onStopListening() {    
super.onStopListening()
}

  override fun onClick() {    
super.onClick()
}

  override fun onTileRemoved() {    
super.onTileRemoved()
}
}

TileService是通过绑定服务(bindService)的方式被调用的,因此,绑定服务生命周期包含的四种典型的回调方法(onCreate()、onBind()、onUnbind()和 onDestroy())都会被调用。但是,TileService也包含了以下特殊的生命周期回调方法:



  • onTileAdded():当用户从编辑栏添加快捷开关到通知栏的快速设置中会调用。

  • onTileRemoved():当用户从通知栏的快速设置移除快捷开关时调用。

  • onClick():当用户点击快捷开关时调用。

  • onStartListening():当用户打开通知栏的快速设置时调用。当快捷开关并没有从编辑栏拖到设置栏中不会调用。在TileAdded添加之后会调用一次。

  • onStopListening():当用户打开通知栏的快速设置时调用。当快捷开关并没有从编辑栏拖到设置栏中不会调用。在TileRemoved移除之前会调用一次。


2、在应用程序的清单文件中声明TileService


name=".MyQSTileService"
android:label="@string/my_default_tile_label"
android:icon="@drawable/my_default_icon_label"
android:exported="true"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">

name="android.service.quicksettings.action.QS_TILE" />




  • name:自定义的TileService的类名。

  • label:快捷开关在通知栏上显示的名称。

  • icon:快捷开关在通知栏上显示的图标。

  • exported:该服务能否被外部应用调用。该属性必须为true。如果为false,那么快捷开关的功能将失效,原因是exported="false"时,TileService将不支持外部应用调起,手机系统自然不能再和该快捷开关交互。必须配置。

  • permission:需要给service配置的权限,BIND_QUICK_SETTINGS_TILE即允许应用程序绑定到第三方快速设置。必须配置。

  • intent-filter:意图过滤器,只有匹配内部的action,才能调起该service。必须配置。


监听模式


TileService的监听模式(或理解为启动模式)有两种,一种是主动模式,另一种是标准模式。



  • 主动模式


在主动模式下,TileService被请求时该服务会被绑定,并且TileService的onStartListening也会被调用。该模式需要在AndroidManifeast清单文件中声明:



name="android.service.quicksettings.ACTIVE_TILE"
android:value="true" />
...


通过TileService.requestListeningState()这一静态方法,就可以实现对TileService的请求,示例如下:


      TileService.requestListeningState(
applicationContext, ComponentName(
BuildConfig.APPLICATION_ID,
MyQSTileService::class.java.name
)
)

主动模式下值得注意的是:



  • 用户在通知栏快速设置的地方点击快捷开关时,TileService会自动完成绑定、TileService的onStartListening会被调用。

  • TileService无论是通过点击被绑定还是通过requestListeningState请求被绑定,TileService所在的进程都会被调起。


标准模式


     在标准模式下,TileService可见时(即用户下拉通知栏看见快捷开关)该服务会被绑定,并且TileService的onStartListening也会被调用。标准模式不需要在AndroidManifeast清单文件中进行额外的声明,默认就是标准模式。


标准模式下值得注意的是:



  • 和主动模式相同,TileService被绑定时,TileService所在的进程就会被调起。

  • 而和主动模式不同的是,标准模式绑定TileService是通过用户下拉通知栏实现的,这意味着TileService所在的进程会被多次调起。因此为了避免主进程被频繁调起、避免DAU等数据统计受到影响,我们还需要为TileService指定一个特定的子进程,在Androidmanifest清单文件中设置:


      process="自定义子进程的名称">
......


更新快捷开关


如果需要对快捷开关的数据进行更新,可以通过getQsTile()获取快捷开关的对象,然后通过setIcon(更新icon)、setLable(更新名称)、setState(更新状态,包括STATE_ACTIVE——表示开启或启用状态、STATE_INACTIVE——表示关闭或暂停状态、STATE_UNAVAILABLE:表示暂时不可用状态,在此状态下,用户无法与您的磁贴交互)等方法设置快捷开关新的数据,最后调用updateTile()方法实现。


  override fun onStartListening() {
super.onStartListening()
if (qsTile.state === Tile.STATE_ACTIVE) {
qsTile.label = "inactive"
qsTile.icon = Icon.createWithResource(context, R.drawable.inactive)
qsTile.state = Tile.STATE_INACTIVE
} else {
qsTile.label = "active"
qsTile.icon = Icon.createWithResource(context, R.drawable.active)
qsTile.state = Tile.STATE_ACTIVE
}
qsTile.updateTile()
}

操作快捷开关



  • 如果想要实现点击快捷开关时、关闭通知栏并跳转到某个页面,可以调用以下方法:


startActivityAndCollapse(Intent intent)


  • 如果想要在点击快捷开关时弹出对话框进行交互,可以调用以下方法:


override fun onClick() {
super.onClick()
if(!isLocked()) {
showDialog()
}
}

因为快捷开关有可能在用户锁屏时出现,所以必须加上isLocked()的判断。只有非锁屏的情况下,对话框才会出现。



  • 如果快捷开关含有敏感信息,需要使用isSecure()进行设备安全性判断,当设备安全时,才能执行快捷开关相关的逻辑(如点击的逻辑)。当设备不安全时(手机处于锁屏状态时),可调用unlockAndRun(Runnable runnable),提示用户解锁屏幕并执行自定义的runnable操作。


以上是通知栏增加快捷开关的全部介绍。


作者:度熊君
来源:juejin.cn/post/7190663063631036473
收起阅读 »

安卓知识点-应届生扫盲安卓WebView

作者 大家好,我叫Jack冯; 本人20年硕士毕业于广东工业大学,于2020年6月加入37手游安卓团队; 目前主要负责海外游戏发行安卓相关开发。 背景 最近在接触活动相关需求,其中涉及到一个安卓的WebView; 刚毕业的我,对安卓知识积累比较少,所以在这里对...
继续阅读 »

作者


大家好,我叫Jack冯;


本人20年硕士毕业于广东工业大学,于2020年6月加入37手游安卓团队;


目前主要负责海外游戏发行安卓相关开发。


背景


最近在接触活动相关需求,其中涉及到一个安卓的WebView;


刚毕业的我,对安卓知识积累比较少,所以在这里对Webview进行相关学习,希望自己可以在安卓方面逐步积累。


Webview介绍


1、关于MockView


( 1 ) 在targetSdkVersion 28/29的工程里面查看WebView继承关系


java.lang.Object
↳ android.view.View
↳ android.view.ViewGr0up
​ ↳ android.widget.FrameLayout
↳ android.layoutlib.bridge.MockView
↳ android.webkit.WebView

( 2 ) 使用26/27等低版本SDK,查看源码中的WebView 继承关系


java.lang.Object
↳ android.view.View
↳ android.view.ViewGr0up
↳ android.widget.AbsoluteLayout
↳ android.webkit.WebView

( 3 )对比


两种方式对比,AbsoluteLayout和FrameLayout都是重写ViewGr0up的方法,如与布局参数配置相关的 generateDefaultLayoutParams()、checkLayoutParams()等。两种方式明显不同的是多了一层MockView 。这里来看看MockView是什么:


public class MockView extends FrameLayout{
...
//创建方式
public MockView(Context context) {...}
public MockView(Context context,AttributeSet attrs) {...}
public MockView(Context context,AttributeSet attrs,int defStyleRes) {...}
//重写添加view方法
@Override
public void addView(View child){...}
@Override
public void addView(View child,int index){...}
@Override
public void addView(View child,int width,int height){...}
@Override
public void addView(View child,ViewGr0up.LayoutParams params){...}
@Override
public void addView(View child,int index,ViewGr0up.LayoutParams params){...}
public void setText(CharSequence text){...}
public void setGravity(int gravity){...}
}

MockView,译为"虚假的view"。


谷歌发布的Sdk其实只是为了提供App开发运行接口,实际运行时候替换为当前系统的Sdk。


具体说就是当谷歌在新的系统(Framework)版本上准备对WebView实现机制进行改动,同时又希望把新的sdk提前发出来,不影响用到WebView的App开发,于是谷歌提供给Android开发的sdk中让WebView继承自MockView,这个WebView只是暴露了接口,没有具体实现;这样当谷歌关于WebView新的实现做好,利用WebView,app也就做好了


2、基本使用


(1)创建


①一般方式:


WebView webView = findViewById(R.id.webview);

②建议方式:


LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGr0up.LayoutParams.MATCH_PARENT,ViewGr0up.LayoutParams.MATCH_PARENT);
mWebView = new WebView(getApplicationContext());
mWebView.setLayoutParams(params);

好处:构建不用依赖本地xml文件,自定义页面参数;手动销毁避免内存泄露;


③更多方式 : 继承Webview和主要API等进行拓展


public class BaseWebView extends WebView {...}
public class BaseWebClient extends WebClient {...}
public class BaseWebChromeClient extends WebChromeClient {...}

(2)加载


① 加载某个网页


webView.loadUrl("http://www.google.com/");

②新建assets目录,将html文件放到目录下,通过路径加载本地页面


 webView.loadUrl("file:///android_asset/loadFailed.html");

③使用evaluateJavascript(String script, ValueCallback resultCallback)方法加载,(Android4.4+)


mWebView.evaluateJavascript("file:///android_asset/javascript.html",new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
Log.e("测试", "onReceiveValue:"+value );
}
});

3、WebViewClient


当URL即将加载到当前窗口,如果没有提供WebViewClient,默认情况下WebView将使用Activity管理器为URL选择适当的处理器。


如果提供了WebViewClient,按自定义配置要求来继续加载URL。


(1)常用方法


//加载过程对url的处理(webview加载、系统浏览器加载、其他操作等)
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
super.shouldOverrideUrlLoading(view, url);
}
//加载失败页面
@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl){
view.loadUrl("file:///android_asset/js_error.html");
}
//证书错误处理
@Override
public void onReceivedSslError(WebView view, final SslErrorHandler handler, SslError error) {
}
//开始加载页面(可自定义页面加载计时等)
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
Log.e(TAG, "onPageStarted:" + url);
}
//结束加载页面
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
Log.e(TAG, "onPageFinished: " + url);
}

(2)关于shouldOverrideUrlLoading


如果在点击链接加载过程需要更多的控制,就可以在WebViewClient()中重写shouldOverrideUrlLoading()方法。


涉及shouldOverrideUrlLoading()的情形,大概分为三种:


(1)没有设定setWebViewClient(),点击链接使用默认浏览器打开;


(2)设定setWebViewClient(new WebViewClient()),默认shouldOverrideUrlLoading()返回false,点击链接在Webview加载;


(3)设定、重写shouldOverrideUrlLoading()


返回true:可由应用代码处理该 url,WebView 中止处理(若重写方法没加上view.loadUrl(url),不加载);


返回false:由 WebView 处理加载该 url。(即使没加上view.loadUrl(url),也会在当前Webview加载)


【一般应用】


@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
super.shouldOverrideUrlLoading(view, url);
if (url != null) {
if (!(url.startsWith("http") || url.startsWith("https"))) {
return true;
}
//重定向到别的页面
//view.loadUrl("file:///android_asset/javascript.html");
//区别不同链接加载
view.loadUrl(url);
}
return true;
}

(3)常见误区


【误区1】 : 需要重写 shouldOverrideUrlLoading 方法才能阻止浏览器打开页面。


解释:WebViewClient 源码中 shouldOverrideUrlLoading 方法已经返回 false,不设定setWebViewClient(),默认使用系统浏览器加载。如果重写该方法并返回true, 就可以实现在app页面中加载新链接而不去打开浏览器。


【误区2】 : 每一个url加载过程都会经过 shouldOverrideUrlLoading 方法。


Q1:加载一定会触发shouldOverrideUrlLoading?


Q2:触发时机一定在onPageStarted调用之前?


解释:关于shouldOverrideUrlLoading的触发


1)如果在点击页面链接时通过标签跳转,触发方法如下:


​ shouldOverrideUrlLoading() —> onPageStarted()—> onPageFinished()


2)如果使用loadUrl加载时,触发方法如下:


​ onPageStarted()—>onPageFinished()


3)如果使用loadUrl加载重定向地址时,触发方法如下:


​ shouldOverrideUrlLoadings—>onPageStarted —> onPageFinished


ps:多次重定向的过程,


onPage1Started


—>shouldOverrideUrlLoadings


—>onPage2Started —> xxx...


—> onPageNFinished


结论:shouldOverrideUrlLoading()方法不是每次加载都会调用,WebView的前进、后退等不会调用shouldOverrideUrlLoading方法;非loadUrl方式加载 或者 是重定向的,才会调用shouldOverrideUrlLoading方法。


【误区3 】: 重写 shouldOverrideUrlLoading 方法返回true比false的区别,多调用一次onPageStarted()和onPageFinished()。


解释:返回True:应用代码处理url;返回False,则由 WebView 处理加载 url。


ps:低版本系统(华为6.0),测试 False比True会多调用一次onPageStarted()和onPageFinished(),这点还在求证中。


4、WebChromeClient


对比WebviewClient , 添加了处理JavaScript对话框,图标,标题和进度等。


处理对象 : 影响浏览器的事件


(1)常用方法:


//alert弹出框
public boolean onJsAlert(WebView view, String url, String message,JsResult result){
return true;//true表示拦截
}

//confirm弹出框
public boolean onJsConfirm(WebView view, String url, String message,JsResult result){
return false;//false则允许弹出
}

public boolean onJsPrompt(WebView view, String url, String message,String defaultValue, JsPromptResult result)

//打印 console 信息。return true只显示log,不显示js控制台的输入;false则都显示出来
public boolean onConsoleMessage(ConsoleMessage consoleMessage){
Log.e("测试", "consoleMessage:"+consoleMessage.message());
}

//通知程序当前页面加载进度,结合ProgressBar显示
public void onProgressChanged(WebView view, int newProgress){
if (newProgress < 100) {
String progress = newProgress + "%";
Log.e("测试", "加载进度:"+progress);
webProgress.setProgress(newProgress);
}
}

(2)拦截示例:


JsResult.comfirm() --> 确定按钮的调用方法


JsResult.cancle() --> 取消按钮


示例:拦截H5的弹框,并显示自定义弹框,点击按钮后重定向页面到别的url


@Override
public boolean onJsConfirm(final WebView view, String url, String message, final JsResult result) {
Log.e("测试", "onJsConfirm:"+url+",message:"+message+",jsResult:"+result.toString());
new AlertDialog.Builder(chromeContext)
.setTitle("拦截JsConfirm显示!")
.setMessage(message)
.setPositiveButton(android.R.string.ok,
new AlertDialog.OnClickListener() {
public void onClick(DialogInterface dialog,int which) {
//重定向页面
view.loadUrl("file:///android_asset/javascript.html");
result.confirm();
}
}).setCancelable(false).create().show();
return true;
}

5、WebSettings


用于页面状态设置\插件支持等配置.


(1)常用方法


WebSettings webSettings = webView.getSettings();
/**
* 设置缓存模式、支持Js调用、缩放按钮、访问文件等
*/

webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);
webSettings.setJavaScriptEnabled(true);
webSettings.setSupportZoom(true);
webSettings.setBuiltInZoomControls(true);
webSettings.setDisplayZoomControls(true);

//允许WebView使用File协议,访问本地私有目录的文件
webSettings.setAllowFileAccess(true);

//允许通过file url加载的JS页面读取本地文件
webSettings.setAllowFileAccessFromFileURLs(true);

//允许通过file url加载的JS页面可以访问其他来源内容,包括其他的文件和http,https等来源
webSettings.setAllowUniversalAccessFromFileURLs(true);
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
webSettings.setLoadsImagesAutomatically(true);
webSettings.setDefaultTextEncodingName("utf-8")

if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {
webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}

结束语


过程中有问题或者需要交流的同学,可以扫描二维码加好友,然后进群进行问题和技术的交流等;


企业微信截图_5d79a123-2e31-42cc-b03f-9312b8b99df3.png


作者:37手游移动客户端团队
来源:juejin.cn/post/7245084484756144186
收起阅读 »

马斯克接手Twitter一年后的成果-工作量化的重要性

马斯克接手Twitter的一年后,在10.27其官方团队发布了一条推文展示这一年的工程成果。 有点国内那味了,论工作量化的重要性。 这一年里,我们在工程技术上取得了许多出色的成就,除了大家在应用中看到的明显变化之外,在幕后我们还做了一系列重要的优化和改进。...
继续阅读 »

马斯克接手Twitter的一年后,在10.27其官方团队发布了一条推文展示这一年的工程成果。



有点国内那味了,论工作量化的重要性。



这一年里,我们在工程技术上取得了许多出色的成就,除了大家在应用中看到的明显变化之外,在幕后我们还做了一系列重要的优化和改进。




  • 将「为你推荐」、「关注」、「搜索」、「个人主页」、「列表」、「社区」和「探索」等功能的技术栈整合到了一个统一的产品框架中。




  • 彻底重建了「为你推荐」的服务和排名系统,代码行数从700K减少到70k,减少了90%;同时计算资源减少了50%,处理相同请求的能力提升了80%。




  • 统一了「为你推荐」和视频的个性化、排名模型,显著提高了视频推荐质量。




  • 重构了API中间层的架构,删除了超过10万行代码和数千个未使用的内部废弃接口,同时删除了一些没人用的客户端服务。




  • 将获取帖子元数据的时间减半,全局API超时错误减少90%。




  • 对外部机器人、爬虫的屏蔽,相比2022年,增长了37%。平均每天,阻止了超过100万次机器人注册,并将私信中的无用信息减少了95%。




  • 关闭了位于萨克拉门托的数据中心,重新调配了5200台机架和148000台服务器,每年为公司节约了超1亿美元,总的来说,节约了48兆瓦的电量,60000磅的网络机架。




  • 优化了对云服务厂商的使用,开始在本地进行更多的工作,这一转变使得每月的云服务成本降低60%,同时我们还将所有的媒体和大文件从云端迁出,减少了60%的云端存储空间,除此之外,还成功将云数据处理成本减少了75%。




  • 构建本地GPU超级计算集群,并设计、交付了43.2Tbps 的高性能网络架构。




  • 提升网络主干的容量和冗余性,每年节省1390万美元。




  • 开展了自动化峰值流量故障转移测试,以持续验证平台的可扩展性和可用性。




作者:云舒编程
来源:juejin.cn/post/7295397683397066762
收起阅读 »

近三个月的排错,原来的憧憬消失喽

web
作为应届生,带着无限憧憬来到这里,但是经过这三个月的经历,发现只有无限的消耗,并没有任何想要留下的感觉,每天携着自己早已不属于自己的脑袋来到早已不想来的地方... 动效逻辑实现 将元素布局设置好,调整元素的一个动态css属性让其置于可视视图以外,使用动效类库,...
继续阅读 »

作为应届生,带着无限憧憬来到这里,但是经过这三个月的经历,发现只有无限的消耗,并没有任何想要留下的感觉,每天携着自己早已不属于自己的脑袋来到早已不想来的地方...


动效逻辑实现


将元素布局设置好,调整元素的一个动态css属性让其置于可视视图以外,使用动效类库,去改变他的css属性让其还原回正确的位置。


动效类库




  • ScrollTrigger




  • ScrollMagic



    • ScrollMagic 是一个 JavaScript 库,用于在滚动事件上创建视差滚动和其他滚动效果。

    • ScrollMagic 允许您在页面滚动时触发动画,例如根据滚动位置触发动画、控制元素的可见性、触发事件等。

    • 您可以使用 ScrollMagic 来创建交互式滚动体验,例如滚动到特定部分时触发动画效果。

    • ScrollMagic 可以与 TweenMax 或其他动画库一起使用,以创建复杂的滚动动画。




  • TweenMax



    • TweenMax 是 GreenSock Animation Platform (GSAP) 库的一部分。GSAP 是一个用于创建高性能动画的 JavaScript 库,提供了丰富的动画功能。

    • TweenMax 是 GSAP 的核心库之一,它用于创建各种动画,包括基本的属性动画,缓动动画,时间轴动画等。

    • TweenMax 提供了灵活且易于使用的 API,允许您创建复杂的动画效果,如淡入淡出、缩放、旋转、移动等。

    • 您可以单独使用 TweenMax 来创建动画,或与其他库和插件一起使用,以实现更高级的效果。




  • animation.gsap.js



    • animation.gsap.js 是 ScrollMagic 的插件,它允许您在 ScrollMagic 中使用 GSAP(包括 TweenMax)来控制动画。

    • 此插件通过将 GSAP 和 ScrollMagic 集成,使您能够在滚动事件中触发和控制 TweenMax 动画。

    • 使用 animation.gsap.js,您可以创建更具交互性的滚动动画,将滚动事件与强大的 TweenMax 动画引擎结合使用,实现更丰富的效果。




综上所述,TweenMax 是 GSAP 库的一部分,用于创建各种动画。ScrollMagic 是一个独立的库,用于处理滚动事件和创建滚动效果。animation.gsap.js 是 ScrollMagic 的插件,它使 ScrollMagic 能够与 GSAP(包括 TweenMax)一起使用,以在滚动事件中创建动画效果。这些库和插件可以协同工作,以创建引人入胜的交互式网页效果。


ScrollMagic很久没有维护了。
image.png


浏览器跨页面通信


前几天有这样一个需求,当我们在当前页面点击编辑时,我们跳转到编辑页面,编辑完成后,我们需要刷新当前页面并关闭编辑页面。这就需要用到跨页面通信功能了。


image.png


image.png


下面总结一下前端中实现在一个页面上进行操作,然后刷新其他页面功能的实现方法:



前提条件是两个页面同源


在页面 A:


 // 判断是否是对比项目页面跳转过来的
if (route.query?.type === 'diff') {
localStorage.setItem('diffProjectChanged', 'true');
setTimeout(() => {
window.close();
}, 500);
} else {
router.back();
}

在页面 B:


// 进入页面将localStorage中的 diffProjectChanged 置为false
localStorage.setItem('diffProjectChanged', 'false');
// 监听编辑
onMounted(() => {
window.addEventListener('storage', (event) => {
if (event.key === 'diffProjectChanged' && event.newValue === 'true') {
location.reload();
}
});
});


在页面 A 中触发一个自定义事件,将相关数据传递给其他页面。


// 触发自定义事件
const event = new CustomEvent('dataChanged', { detail: { newData: 'someData' } });
window.dispatchEvent(event);

在页面 B 中监听该自定义事件,并在事件触发时执行刷新操作。


// 监听自定义事件
window.addEventListener('dataChanged', (event) => {
// 获取数据并执行刷新操作
const newData = event.detail.newData;
location.reload();
});


  • 使用 WebSocket


在页面 A 中通过 WebSocket 发送消息,通知其他页面。
在页面 B 中监听 WebSocket 消息,接收通知并执行刷新操作。


这种方法需要在服务器上设置 WebSocket 服务。


当前项目避免使用其他包管理工具


使用一些约束,让当前项目只能通过指定的包管理工具安装,防止项目配置乱七八糟的。


在当前根目录下


    // scripts/preinstall.js
if (!/pnpm/.test(process.env.npm_execpath || '')) {
console.log('只能使用pnpm');
console.warn(
`\u001b[33mThis repository requires using pnpm as the package manager ` +
` for scripts to work properly.\u001b[39m\n`,
);
process.exit(1);
}

在packages.json中scripts配置。


"preinstall": "node ./scripts/preinstall.js"

或者直接配置


"preinstall": "npx only-allow pnpm"

文本省略


这种方式需要设置具体宽高。如果是使用了 flex: 1 / 百分比数据 这种不会生效。动态的宽度是不会出现省略号的。


text-overflow: ellipsis;
width: 100%;
overflow: hidden;
white-space: nowrap;

所以我们可以使用多文本的方式代替。


word-break: break-all;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;

不同大小文字底部对齐方式


在CSS中,要让不同字体大小的内容底部对齐,你可以使用verti**cal-align属性。(设置在对齐元素上,只需要设置在一个元素上即可) 这属性用于控制内联元素(如文本)在其父元素内的垂直对齐方式。你可以将其设置为bottom来实现底部对齐。但需要注意的是,vertical-align属性通常用于内联元素,而不是块级元素。 如果是块级元素,我们可以使用flex布局的align-items: flex-end;来实现父容器内的文本底部对齐。要想实现底部对齐,父容器必须具有足够的高度来容纳最大的字体大小。


<style>
.container {
height: 100px;
border: 1px solid #ccc;
display: flex;
align-items: flex-end; /* 底部对齐的关键 */
}

.text {
font-size: 20px;
}

.small-text {
font-size: 14px;
}
</style>

<div class="container">
<p class="text">这是一些文本</p>
<p class="small-text">这是较小的文本</p>
</div>

了解的一些git操作


git提交错误分支,希望将中间的commit删除掉


先通过git rebase -i commitid切换到删除commit的前一个commitid。-i 表示要进行交互式 rebase,<commit-hash>^ 表示要删除指定提交及之后的提交。这将打开一个文本编辑器,列出了要进行 rebase 的提交。
image.png


image.png
这样他只是删除了本地的记录,但是并没有更新远程仓库。


image.png
所以我们需要强制当前记录提交


git push origin <branch> --force

image.png


如果直接执行git push他会告诉你需要拉取最新代码。如果执行了git pull前面做的工作就没用了。所以我们需要使用--force强制提交。


注意:最后git push一定要强制提交,不然按照他的提示拉取了远程代码,那么前面做的内容都没用了。


回退解决冲突之前的状态


git merge --about

查看当前分支基于那个分支创建的


git reflog show

修改分支名称


// 在当前分支
git branch -m feature/v1.4.1.0

// 不在当前分支
git branch -m old-branch new-branch

将已提交的记录提交到别的分支


ruanyifeng.com/blog/2020/0…


// 切换到需要提交的分支
// 找到需要提交的代码commit
git cherry-pick commitId

props的双向绑定便捷性


我们在使用表格编辑功能时,直接在dataSource中绑定props对应的值,当编辑单元格时,就直接更新props,很方便。由于一些其他的因素,这个模块并没有采用这个,导致以下bug出现。


这个问题是测试发现方改变一些字段时,字段为发送给后端,排查发现我在修改时,并没有通过emits将值更新到props中。所以造成bug。,导致最近需求一直变更代码bug很多,已经没有在去维护的力气了。😑
image.png


watch监听可能出现的问题


这个bug对于当时我写代码来说排查很困难。排查了很久,最后也是找我导师解决的,不得不说我导师排查bug的能力好nb。👍


最开始我是通过监听用户切换不同内容,监听diffProjectId,然后拿到formFirstValues和formSecondValues,去完成逻辑。这样看似没啥问题。但是完成逻辑的时候,拿到的formSecondValues总是上一次的值。这就很懵逼了。


出现这种情况的原因是我们监听的diffProjectId是同步的,而formFirstValues,formSecondValues这两个值是异步获取的。所以就会出现问题。最后通过下图方式实现功能。
image.png


删除后端不必要字段,造成的问题


由于后端定义的查询详情和请求传参字段不统一collectionPlanResp,collectionPlanReq,导致我回写数据不好处理,直接通过collectionPlanResp对象进行处理,在提交时在赋值。当时想着把多余字段删除collectionPlanResp,这样就会出现一个问题,我提交表单后,当后端服务抛出异常提示(比如字数限制),我们修改后,在当前页面再次提交,就会导致collectionPlanReq传递为空值。造成数据未传递给后端的bug。


分析了一下,字段的一些必填项长度限制,前端还是不要偷懒,做一下处理。


image.png


image.png


对于多字段UI处理


我们可以使用Collapse组件去处理,让UI看起来更简洁。


image.png


善于使用margin和定位来解决间距问题


调整间距时,如果margin不好调整,我们可以使用相对定位来配合调整。 这种方式是当时接了一个对比字段差异的需求,为了以后可以直接在当前对比页面编辑,所以采用了两个form去实现的。设置一个form的label,另一个不设置。这样就可以完美的在一行突出标题对比两个不同内容的字段了。为了做到响应式,就有了这样的做法。


image.png


这样看下来工作三个月基本都是在马代码,每天写不完的需求,发不完的版,上午写代码,下午改需求,真的挺无语的,下个月辞职回家种地喽。😅


往期文章



专栏文章



最近也在学习nestjs,有一起小伙伴的@我哦。


作者:Spirited_Away
来源:juejin.cn/post/7292036126269063178
收起阅读 »

几条SQL把服务器干崩了

大家好,我是冰河~~ 今天跟大家分享一个发生在今天凌晨的真实案例,这篇文章也是我事后临时写出来的,处理事情的过程有点无语,又有点气愤! 事件背景 事情的背景是这样的:一个朋友今年年初新开了一家公司,自己是公司的老板,不懂啥技术,主要负责公司的战略规划和经营管理...
继续阅读 »

大家好,我是冰河~~


今天跟大家分享一个发生在今天凌晨的真实案例,这篇文章也是我事后临时写出来的,处理事情的过程有点无语,又有点气愤!


事件背景


事情的背景是这样的:一个朋友今年年初新开了一家公司,自己是公司的老板,不懂啥技术,主要负责公司的战略规划和经营管理,但是他们公司的很多事情他都会过问。手下员工30多人,涵盖技术、产品、运营和推广,从成立之初,一直在做一款社交类的APP。平时,我们一直保持联系,我有时也会帮他们公司处理下技术问题。


事件经过


今天凌晨,我被电话铃声吵醒了,一看是这个朋友打来的,说是他们公司数据库服务器CPU被打满了,并且一直持续这个状态,他说拉个群,把他们后端Java同事拉进来一起沟通下,让我帮忙看看是什么问题,尽快处理下。说话语气很急,听的出来事态很严重,因为目前正在加大力度推广,周末使用人数也比较多,出现这种问题着实让人着急。


后面我加了那个朋友拉的微信群,开始了解服务器出现问题的具体情况,下面就是一些处理的经过了。


注:聊天内容已经获得授权公布。


2023-10-29-006.png


他们后端Java把运维发的监控截图发出来了,咱继续跟他沟通。


2023-10-29-007.png


为啥我说CPU占用高呢?大家看下他们运维发的图就知道了。


2023-10-29-001.png


CPU已经飙升到了400%了,数据库服务器基本已经卡死。拿到他给我发的SQL后,我跟他们老板要了一份他们的数据库表结构,在我电脑上执行了下查询计划。


2023-10-29-008.png


这不看不知道,一看吓一跳,一个C端频繁访问的接口SQL性能极差,Using temporary、Using filesort、Using join buffer、Block Nested Loop全出来了。


我把这个图发出去了,也结合他们团队的实际情况,给出了优化的目标建议:SQL中不要出现Using filesort、Block Nested Loop,尽量不要出现Using join buffer和Using temporary,把Using where尽量优化到Using Index级别。


2023-10-29-009.png


说是尽量不要出现Using join buffer和Using temporary,把Using where尽量优化到Using Index级别,就是怕他们做不到这点,优先把Using filesort、Block Nested Loop优化掉。 但是这货后面说的话实属把我震惊到了。


2023-10-29-封面.png


我看完他的回复,直接有点无语:卧槽,不超过500万rows效率很高?你这SQL 500万数据效果很高?更让我无语的是这货说MySQL一般一亿以上数据量开始优化,这特么不是完全扯淡吗?他说这话时,我大概就知道这货的水平了。。。


后面我就问他说的这些数据的依据是哪里来的。


2023-10-29-011.png


这货说是什么大数据高并发MySQL数据库压测出来的,稍微有过压测经验的应该都知道,压测一个很重要的前提就是要明确压测的环境,最起码要明确压测环境服务器的CPU核数和内存,直接来句MySQL一亿数据是大数据高并发MySQL数据库压测出来的结果,这还是MySQL官方的数据。。。。


不知道是不是因为群里有他们老板的缘故,这货后面还在狡辩。


2023-10-29-012.png


沟通到这里,我特么有种想打人的冲动,生产环境所有业务快被数据库拖死了,数据库服务器CPU被干爆了,监控到慢SQL,并且查看这些慢SQL的执行计划,性能非常低下,SQL里不管是select部分还是where部分大量使用了MySQL自带的函数,这不慢才怪啊。看这货处理问题的态度,要是我下面的人,早就让他卷铺盖走人了。


处理结果


后续我跟他们老板要了一个代码只读权限的账号,将代码拉取下来后,好家伙,到处都是这种SQL查询,要是一两处还好,把SQL修改并优化下,关联的业务逻辑调整下,再把功能测试下,接口压测下,没啥问题就可以发版上线了。


但是,如果到处都是这种SQL的话,那优化SQL就要花费一定的时间了,甚至是新发布的重大功能逻辑都要重写。最终,我跟他们老板说的是回滚版本吧,最新的功能还是先下线,把新功能的SQL、缓存、业务逻辑、接口都优化好,压测没问题后再重新上线。


2023-10-29-013.png


事后总结


无论什么时候,生产环境一旦出现致命问题,第一时间要优先恢复生产环境正常访问,随后再认真排查、定位和解决问题,毕竟生产环境一旦出现问题,每一秒流失的都是真金白银。


搭建技术团队一定要找靠谱的人,最起码团队的核心人员要靠谱,像我朋友团队的这个技术,在他的认知里,MySQL执行计划出现Using temporary、Using filesort、Using join buffer、Block Nested Loop,500W rows效率都很高,殊不知他们生产环境实际主表数据才10几条,要是真达到500W量级就别查询了,数据库直接就趴下了。还有这个MySQL一般一亿以上开始优化,这个依据我也不知道这货是从哪里看到的,并且还说了大数据高并发MySQL数据库压测出来的,这不纯属扯淡吗?


更离谱的是我事后悄悄问了他们老板,他的工作年限是多久,据说工作10多年了,是位80后。


顿时让我想到了一句话:人的认知有几个层次:不知道自己不知道,知道自己不知道,知道自己知道,不知道自己知道。


好了,今天就到这儿吧,我是冰河,我们下期见~~


作者:冰_河
来源:juejin.cn/post/7298635800631459892
收起阅读 »

这款支持安全多人协作的在线终端,真的吓到我了❗️❗️

web
这款支持安全多人协作的在线终端 ☀️ 前言 事情是这样的: 周末一个同事的项目报错了,但是无法精准定位到问题😠。 他希望我帮忙看一下他的报错并协助解决,于是扔了一个链接给我🤔。 我心想你给我链接干啥,你倒是截图报错啊😡。 打开链接后我直呼 wassu...
继续阅读 »

这款支持安全多人协作的在线终端



☀️ 前言



  • 事情是这样的:

    • 周末一个同事的项目报错了,但是无法精准定位到问题😠。

    • 他希望我帮忙看一下他的报错并协助解决,于是扔了一个链接给我🤔。

    • 我心想你给我链接干啥,你倒是截图报错啊😡。



  • 打开链接后我直呼 wassup🔥,我居然可以在一个网页中操作他的终端,并且还是实时协同的!有鼠标动来动去那种!

  • 询问得知原来是用的 sshx ,那么我们本文就来了解一下这个神奇的产品。


🔥 sshx



  • sshx 这是一款基于网络的安全的团队协作终端,它允许您在多人无限画布上通过链接与任何人共享您的终端。

  • 只需要共享的人员执行一下“sshx”再将链接分享给你同事,则它能马上加入到你的终端进行操作。

  • 它具有实时协作远程光标聊天功能。它还具有快速且端到端加密的特点,并具有用 Rust 编写的轻量级服务器。

  • 实时协同代表着什么,这将使远程团队调试终端问题变得更加容易。


🤔 怎么使用


安装命令行界面



  • 通过在终端中运行此命令curl -sSf https://sshx.io/get | sh来获取 sshx CLI。它很小,只需几秒钟即可下载(3 MB)


分享您的终端



  • 直接在你需要分享的终端内执行 sshx,此时这个终端不要关闭,他会生成一个分享链接。




  • 将这个终端用浏览器打开即可,进入到这个网址,会让你输入一个名称方便团队协时展现光标的用户。





  • 在上方的操作栏新建一个虚拟终端即可操作真正的终端了。




  • 为了方便演示我这里打开两个浏览器来模拟别人协同操作我的终端,来我们跑个苹果来看看。




  • 实时对话也是很流畅。




  • 我们可以看到,在页面会出现另一个用户的移动光标,并且可以与他对话,他的延迟是非常低的,这真的可以帮助我们实时协作。




  • (协不协作我不知道,但是可以看到光标是真的帅啊!)




❓ 用来干啥



  • 那么这么一款产品,有的同学就会问了:他的作用是什么呢?看起来很鸡肋啊?

  • 有了这么一款产品,我们可以:

    • 在帮助客户部署相关公司产品的时候不需要远程操控别人的电脑,只需要客户安装这款 cli 并且联网,我们既可以远程帮忙操作。

    • 更好的公司运维,在同事操作的时候,可以随时介入进行操作。

    • 很多群友在前端群中问问题时习惯抛出一个截图,但是又没有说明白上下文,这时候就可以将你终端分享给大佬们定位问题。

    • (手摸手教女同学命令行操作🐶)



  • 那么肯定又会有同学问了:那我不是可以随便删除别人的文件?我直接rm -f * 敢问阁下如何应对?

  • 是的,看了下确实可以执行这些操作,所以还是尽量分享给你信得过的人,我觉得其实作者可以出一个只读模式only-read,这样你就可以让别人在你的终端上阅读和滚动,减少一些权限。


👋 写在最后



作者:快跑啊小卢_
来源:juejin.cn/post/7298642242117238834
收起阅读 »

Node.js如何处理多个请求?

web
前言 在计算机科学领域,关于并发和并行的概念经常被提及。然而,这两个术语常常被混为一谈,导致很多人对它们的理解存在着很多混淆。本文小编将通过对并发和并行的深入解析,帮助读者更好地理解它们之间的不同特点和应用场景。同时,文章还将介绍Node.js如何高效地处理多...
继续阅读 »

前言


在计算机科学领域,关于并发和并行的概念经常被提及。然而,这两个术语常常被混为一谈,导致很多人对它们的理解存在着很多混淆。本文小编将通过对并发和并行的深入解析,帮助读者更好地理解它们之间的不同特点和应用场景。同时,文章还将介绍Node.js如何高效地处理多个请求的技巧和方法。


什么是并发


并发是指两个或多个任务可以在重叠的时间段内开始、运行和完成。这并不一定意味着它们将同时运行,但它们可以交错执行,以便在任何给定的时间,总有一个任务在运行。


下面小编以一个简单的例子给读者详细的解释并发的特点:


假设在一个餐厅里面,有一个服务员从1号桌的客人那里接受了一份点单,然后这个服务员在厨房一直等待1号桌客人的饭做好,做好之后将饭端到1号桌。


这个服务员完成第一桌客人的点单后,再前往下一桌的2号客人处,接受订单,并前往厨房等待准备完成,等饭做好后再将点餐的餐点交给客人。


看到这里,各位读者可能会觉得这个服务员的做法一点都不高效,他完全可以在等第一单饭的时候去第二桌点单,按照这位服务员现在的做法,他在每一单的饭做好之前的这个时间段内什么事情都干不了,这样就浪费了大量的时间。


我们现在修改一下这位服务员的做法,修改后如下:


服务员将前往1号桌接受订单并将其交给厨房,然后返回2号桌接受订单并将其同样交给厨房。在这种情况下,服务员不会等待订单准备完成,而是会继续前往下一个桌子接受订单,直到食物准备好。当食物准备好后,服务员会为所有桌子上的客人上菜。像上述的这种情况,没有增加线程(服务员)的数量,但通过缩短空闲时间来加快处理过程。同时处理多个任务,这个就是并发。


例如:你正在做饭的同时,接到一通电话,于是你接听了电话,当听到炉子发出警报时,你回去关掉炉子,然后再继续接电话。


这个例子很好地展示了并发的概念。做饭的过程中,能够同时处理来自电话和炉子的不同事件。你在不中断一个任务的情况下,暂时切换到另一个任务,然后再回到原来的任务。这种并发的方式能够提高效率并更好地应对多个任务的情况。(同时做两件事,但是一次只做一件事)


什么是并行


并行是指两个或多个任务可以真正同时运行。为了实现这一点,这些任务必须能够在独立的CPU或核心上运行。同样的,小编依然以做饭的例子给大家解释一下什么是并行:


例如:你正在做饭的同时,接到一通电话,你的家人接听了电话,你继续做饭,你和你的家人谁也不会干扰谁,两个不同的事情发生在两个人身上,这个就是并行。


什么是单线程进程?


单线程进程是按照单一顺序执行编程指令的过程。话虽如此,如果一个应用程序具有以下一组指令:


指令A


指令B


指令C


如果这组指令在单线程进程中执行,执行过程将如下所示:


多线程进程是什么?


多线程进程是在多个序列中执行编程指令。因此,除非多个指令被分组在不同的序列中,否则指令不需要等待执行。


为什么Node.js是单线程的?


Node.js是一个单线程的平台。这意味着它一次只能处理一个请求。


例如:服务员从1号桌子上接订单并将其传给厨房,然后去2号桌子接订单。当从2号桌子接订单时,1号桌子的食物已经准备好了,但是服务员不能立即过去将食物送到1号桌子,服务员必须先完成1号桌子的订单,然后将其交给厨房,然后再将准备好的餐点送到1号桌子。


Node.js Web服务器维护一个有限的线程池,为客户端请求提供服务。多个客户端向Node.js服务器发出多个请求。Node.js接收这些请求并将它们放入事件队列中。Node.js服务器有一个内部组件,称为事件循环(Event Loop),它是一个无限循环,接收并处理请求。这个事件循环是单线程的,也就是说,事件循环是事件队列的监听器。


Node.js如何处理多个请求?


Node.js可以通过事件驱动模型轻松处理多个并发请求。


当客户端发送请求时,单个线程会将该请求发送给其他人。当前线程不会忙于处理该请求。服务器有工作人员为其工作。服务器将请求发送给工作人员,工作人员进一步将其发送给其他服务器并等待响应。同时,如果有另一个请求,线程将其发送给另一个工作人员,并等待来自另一个服务器的响应。


这样,单个线程将始终可用于接收客户端的请求。它不会阻塞请求。


Node.js实现多个请求的代码:


const http = require('http');

// 创建一个 HTTP 服务器对象
const server = http.createServer((req, res) => {
// 处理请求
if (req.url === '/') {
// 设置响应头
res.writeHead(200, { 'Content-Type': 'text/plain' });

// 发送响应数据
res.end('Hello, World!');
} else if (req.url === '/about') {
// 设置响应头
res.writeHead(200, { 'Content-Type': 'text/plain' });

// 发送响应数据
res.end('About Us');
} else {
// 设置响应头
res.writeHead(404, { 'Content-Type': 'text/plain' });

// 发送响应数据
res.end('Page Not Found');
}
});

// 监听 3000 端口
server.listen(3000, () => {
console.log('Server listening on port 3000');
});

总结


总的来说,Node.js在处理多个请求方面具有优势。它利用事件驱动和非阻塞式I/O的特性,能够高效地处理并发请求,提供快速响应和良好的可扩展性。同时,通过采用适当的工具和技术,可以进一步优化性能,控制并发量,并提高系统的可靠性和稳定性。


扩展链接:


从表单驱动到模型驱动,解读低代码开发平台的发展趋势


低代码开发平台是什么?


基于分支的版本管理,帮助低代码从项目交付走向定制化产品开发


Redis从入门到实践


一节课带你搞懂数据库事务!


Chrome开发者工具使用教程


作者:葡萄城技术团队
来源:juejin.cn/post/7298646156437438464
收起阅读 »

Android 14 彻底终结大厂流氓应用?

hi 大家好,我是 DHL。大厂程序员,就职于美团、快手、小米。公众号:ByteCode,分享技术干货和编程知识点 在某些大厂内部通常都会有一个神秘的团队,他们的工作内容就是专门研究系统,而的事情就是如何让自家应用在后台存活的更久,达到永生的目的。 其中有个...
继续阅读 »

hi 大家好,我是 DHL。大厂程序员,就职于美团、快手、小米。公众号:ByteCode,分享技术干货和编程知识点



在某些大厂内部通常都会有一个神秘的团队,他们的工作内容就是专门研究系统,而的事情就是如何让自家应用在后台存活的更久,达到永生的目的。


其中有个别公司,甚者利用公开漏洞,达到远程操控用户手机的目的,做更多他们想做的事,可以随意获取用户的隐私,而且一旦安装,普通用户很难删除,之前写了一些揭露他们的文章,但是现在已经被全部删除了,就连评论区抨击他们的内容也全都被删除了。


而 Android 14 的出现,可以说是暂时性的彻底终结了这些流氓软件,想在后台通过保活的方式,让应用在后台达到永生的目的基本不可能了。


为什么这是暂时性的呢,因为没有完美的系统,新的系统虽然修复了公开漏洞,终结了现有的保活的方式,但是新系统可能存在新的漏洞,还是会给某些大厂可乘之机。


我们一起来看一下 Android 工程副总裁 Dave Burke 都介绍 Andorid 14 在性能、隐私、安全性方面做了那些改进,这篇文章是对之前的文章 适配 Android 14,你的应用受影响了吗Android 14 新增权限 的补充。



  • 冻结缓存应用,增强杀进程能力

  • 应用启动更快

  • 减少内存占用

  • 屏幕截图检查

  • 显示全屏系统通知

  • 精确闹钟权限

  • 提供了对照片和视频的部分访问权限

  • 最小 targetSdkVersion 限制

  • 返回手势


本文只会介绍我认为 Android 14 上最大的变化,关于 Android 14 的所有变更,可以前往查看变更
developer.android.com/about/versi…


冻结缓存应用,增强杀进程能力


在 Android 11 以上支持冻结已缓存的应用,当应用切换到后台并且没有其他活动时,系统会在一定时间内通过状态判断,是否冻结该应用,如果一个应用被冻结住了,将完全被 "暂停",不再消耗任何 CPU 资源,可以减少应用在后台消耗的 CPU 资源,从而达到节电的目的。


被冻结已缓存的应用并不会执行终止该应用,冻结的作用只是暂时挂起进程,消耗 CPU 的资源为 0,它有助于提高系统性能和稳定性,同时最大限度地节省设备的资源和电量的消耗,一旦应用再次切换到前台时,系统会将该应用的进程解冻,实现快速启动。


如果你的手机支持冻结已缓存的应用,在开发者选项里会显示 「暂停执行已缓存的应用」设置项。



冻结已缓存应用,在内核层面使用的是 cgroup v2 freezer,相对于使用信号 SIGSTOP 与 SIGCONT 实现的挂起与恢复,cgroup v2 freezer 无法被拦截,也就无法被开发者 Hook,从而彻底终结大厂想在这个基础上做一些事情。


当然 Google 也对 cgroup 进行了封装,提供了 Java API,在上层我们也可以调用对应的方法实现 CPU、内存资源的控制。


public static final native void setProcessFrozen(int pid, int uid, boolean frozen);
public static final native void enableFreezer(boolean enable);

经过测试 Android 14 相比于 Android 13,缓存进程的 CPU 使用量降低了高达 50%,因此,除了传统的 Android 应用生命周期 API,如前台服务、JobScheduler 或 WorkManager,后台工作将受到限制。


另外在 Android 14 上系统在杀进程之前,首先会将应用所有的进程进行 cgroup v2 freezer,被冻结的应用 cpu 资源占用为 0,然后在挨个杀掉进程,想通过进程互相拉取进程的方式,不断的想通过 fork 出子进程,达到应用永生的目的,在 Android 14 以上已经不可能了,这些黑科技都要告别历史的舞台了。


应用启动更快


在 Android 14 上对缓存应用进行优化,增加了缓存应用的最大数量的限制,从而减少了冷启动应用的次数。


而应用的最大缓存数量不是固定的,可以根据设备的内存容量进行调整,Android 测试团队在 8GB 设备上,发现冷启动应用的数量减少了 20%,而在 12GB 设备上减少了超过 30%,冷启动相对于热启动来说速度较慢,而且在电量消耗方面成本较高,这一工作有效地改善了电量使用和整体应用启动时间。


减少内存占用


代码大小是我们关注的关键指标之一,代码量越大虚拟内存占用越高,减少生成的代码大小,对内存(包括 RAM 和存储空间)的影响就越小。


在 Android 14 中,改进 Android 运行时(ART)对 Android 用户体验,ART 包含了优化措施,将代码大小平均减少了 9.3%,而不会影响性能。


屏幕截图检查


在 Android 14 中新增了一个特殊的 API,截取屏幕截图后会有个 ScreenCaptureCallback 的回调,当用户正在使用截取屏幕截图时,将会调用这些回调函数。


要使 API 正常工作,需要在 AndroidManifest 中添加 DETECT_SCREEN_CAPTURE 权限,然后在 onStart() 方法中注册回调,需要在 onStop() 中取消注册。


<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.DETECT_SCREEN_CAPTURE" />
</manifest>


class MainActivity : Activity() {

private val mainExecutor = MainEcxector()

private val screenshotCallback = ScreenCaptureCallback {
// A screenshot was taken
}

override fun onStart() {
super.onStart()
registerScreenCaptureCallback(mainExecutor, screenshotCallback)
}

override fun onStop() {
super.onStop()
unregisterScreenCaptureCallback(screenshotCallback)
}
}

显示全屏系统通知



Android 11 引入了全屏通知,当全屏应用程序运行时,这些通知将在锁屏屏幕上显示,任何应用都可以在手机处于锁定状态时使用 Notification. Builder. setFullScreenIntent 发送全屏 Intent,不过需要在 AndroidManifest 中声明 USE_FULL_SCREEN_INTENT 权限,在应用安装时自动授予此权限。


从 Android 14 开始,使用此权限的应用仅限于提供通话和闹钟的应用。对于不适合此情况的任何应用,Google Play 商店会撤消其默认的 USE_FULL_SCREEN_INTENT 权限。


在用户更新到 Android 14 之前,在手机上已经安装的应用仍拥有此权限,但是用户可以开启和关闭此权限,所以您可以使用新 API NotificationManager.canUseFullScreenIntent 检查应用是否具有该权限。


如果想在 Android 14 上使用这个权限,我们可以提示用户手动打开授权,通过 Intent(ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT) 来跳转到设置界面。


if(NotificationManager.canUseFullScreenIntent()){
startActivity(Intent(ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT))
}

精确闹钟权限


在 Andorid 12 之前我们可以直接调用 setAlarmClock()setExact()
setExactAndAllowWhileIdle() 等等方法设置精确闹钟时间,


但是在 Android 12 上 Google 引入了一个新的权限 SCHEDULE_EXACT_ALARM,如果想调用 setAlarmClock()setExact()
setExactAndAllowWhileIdle() 等等方法设置精确闹钟时间, 需要在 manifest 中申明 android.permission.SCHEDULE_EXACT_ALARM 权限。


所以运行在 Android 12 ~ Android 13 系统上,我们只需要声明一下权限就可以使用了,但是从 Android 14 开始 SCHEDULE_EXACT_ALARM 权限默认被禁止使用了。


如果你还想在 Andorid 14 以上使用精准闹钟的 API,我们可以提示用户手动打开授权,通过 Intent (ACTION_REQUEST_SCHEDULE_EXACT_ALARM) 来跳转到设置界面,代码如下。


val alarmManager: AlarmManager = context.getSystemService<AlarmManager>()!!
when {
// If permission is granted, proceed with scheduling exact alarms.
alarmManager.canScheduleExactAlarms() -> {
alarmManager.setExact(...)
}
else -> {
// Ask users to go to exact alarm page in system settings.
startActivity(Intent(ACTION_REQUEST_SCHEDULE_EXACT_ALARM))
}
}

提供了对照片和视频的部分访问权限


这个限制和 iOS 相似,Android 14 提供了对照片和视频的部分访问权限。当您访问媒体数据时,用户将看到一个对话框,提示用户授予对所有媒体的访问、或者授予单个照片/视频的访问权限,该新功能将适用于 Android 14 上所有应用程序,无论其 targetSdkVersion 是多少。



在 Android 13 上已经引入了单独的照片访问和视频访问权限,但是在 Android 14 上新增了新的权限 READ_MEDIA_VISUAL_USER_SELECTED


<manifest xmlns:android="http://schemas.android.com/apk/res/android" />

<!-- Devices running Android 13 (API level 33) or higher -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />

<!-- To handle the reselection within the app on Android 14 -->
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />

</manifest>

如果没有声明新的权限,当应用程序进入后台或用户终止应用程序时,单独的照片访问和视频访问权限将立即撤销,不会保存 READ_MEDIA_IMAGESREAD_MEDIA_VIDEO 权限的状态,每次都需要检查。


最小 targetSdkVersion 限制


Android 14 当中有一个比较大的变化就是,无法安装 targetSdk <= 23 的应用程序 (Android 6.0),不要将它与 minSdk 混淆。


在 Android 开发中有两个比较重要的版本号:



  • compileSdkVersion :用于编译当前项目的 Android 系统版本

  • targetSdkVersion :App 已经适配好的系统版本,系统会根据这个版本号,来决定是否可以使用新的特性


这个最小 targetSdkVersion 限制,主要是出于安全考虑,在 Android 6.0 中引入了运行时权限机制,App 想要使用一些敏感权限时,必须先弹窗询问用户,用户点击允许之后才能使用相应的权限。


但是一些 App 为了利用权限方便获取到用户的信息,通过不去升级 targetSdk 的版本号的方式,在安装过程中获得所有权限,以最低成本的方式,绕过运行时权限机制。


如果之前已经安装了的 App,就算升级到 Android 14 也会去保留,系统不能代表用户去删除某个应用,其实我在想,为什么不针对这些已经安装好的低版本的 App,Google 给出一些警告提示,让用户可以感知到呢


返回手势


在 Android 13 的时候,Google 已经预示我们在下一个版本中,返回手势将会有一些更新,并以预览屏幕的形式呈现动画,效果如下图所示。



我们来演示一下使用后退导航的动画。



在 Android 14 增加了在 App 中创建过渡动画的功能,比如在 OnBackPressedCallback 接口中添加了一个方法 handleonbackprogress() ,这个方法在返回手势执行过程中被调用,我们可以在这个方法中增加一些过渡动画。


OnBackPressedCallback 接口中还提供了两个方法 handleOnBackPressed()handleOnBackCancelled() 分别在动画完成和取消动画时调用,我们来看看在代码中如何使用。


class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
val box = findViewById<View>(R.id.box)
val screenWidth =
Resources.getSystem().displayMetrics.widthPixels
val maxXShift = (screenWidth / 20)

val callback = object : OnBackPressedCallback(
enabled = true
) {

override fun handleOnBackProgressed(
backEvent: BackEvent
)
{
when (backEvent.swipeEdge) {
BackEvent.EDGE_LEFT ->
box.translationX = backEvent.progress *
maxXShift
BackEvent.EDGE_RIGHT ->
box.translationX = -(backEvent.progress *
maxXShift)
}
box.scaleX = 1F - (0.1F * backEvent.progress)
box.scaleY = 1F - (0.1F * backEvent.progress)
}

override fun handleOnBackPressed() {
// Back Gesture competed
}


override fun handleOnBackCancelled() {
// Back Gesture cancelled
// Reset animation objects to initial state
}
}
this.onBackPressedDispatcher.addCallback(callback)
}
}

API 被废弃


在 Android 中使用 overidePendingTransition () 方法实现进入和退出动画,但是在 Android 14 上提供了新的 overrideActivityTransition () 方法,而 overidePendingTransition () 方法已被标记为弃用。


// New API
overrideActivityTransition(
enterAnim = R.anim.open_trans,
exitAnim = R.anim.exit_trans,
backgroundColor = R.color.bgr_color
)

// deprecated
overridePendingTransition(R.anim.open_trans, R.anim.exit_trans)



全文到这里就结束了,感谢你的阅读,坚持原创不易,欢迎在看、点赞、分享给身边的小伙伴,我会持续分享原创干货!!!




我开了一个云同步编译工具(SyncKit),主要用于本地写代码,同步到远程设备,在远程设备上进行编译,最后将编译的结果同步到本地,代码已经上传到 Github,欢迎前往仓库 hi-dhl/SyncKit 查看。





Hi 大家好,我是 DHL,就职于美团、快手、小米。公众号:ByteCode ,分享有用、有趣的硬核原创内容,Kotlin、Jetpack、性能优化、系统源码、算法及数据结构、动画、大厂面经,真诚推荐你关注我。





最新文章



开源新项目




  • 云同步编译工具(SyncKit),本地写代码,远程编译,欢迎前去查看 SyncKit




  • KtKit 小巧而实用,用 Kotlin 语言编写的工具库,欢迎前去查看 KtKit




  • 最全、最新的 AndroidX Jetpack 相关组件的实战项目以及相关组件原理分析文章,正在逐渐增加 Jetpack 新成员,仓库持续更新,欢迎前去查看 AndroidX-Jetpack-Practice




  • LeetCode / 剑指 offer,包含多种解题思路、时间复杂度、空间复杂度分析,在线阅读




作者:程序员DHL
来源:juejin.cn/post/7298699367791411236
收起阅读 »

Nginx与洗脚城技师那些有意思的小故事

FBI WARNING: 内容仅作为举例用途,本人不曾去过洗脚城,也不知道技师是什么 我们今天来讲讲Nginx的一些小常识吧。 什么是Nginx Nginx 是一款是由伟大且牛逼的俄罗斯的程序设计师 Igor Sysoev(伊戈尔 赛索耶夫) 在摸鱼玩耍时...
继续阅读 »

FBI WARNING:



内容仅作为举例用途,本人不曾去过洗脚城,也不知道技师是什么



我们今天来讲讲Nginx的一些小常识吧。


什么是Nginx


Nginx 是一款是由伟大且牛逼的俄罗斯的程序设计师 Igor Sysoev(伊戈尔 赛索耶夫) 在摸鱼玩耍时所开发高性能的 Web反向代理 服务器,也是一个 IMAP/POP3/SMTP 代理服务器。


什么是反向代理


说到反向代理之前,我们来看看什么是 正向代理(跑腿代购)。


正向代理运行在客户端的出口,获取客户端发送的请求,用自己的身份发送到服务器并获取返回数据,最终返回给客户端。


常见的正向代理,如网络爬虫的IP代理、VPN、游戏加速器......


image.png


那么,什么是反向代理呢?


反向代理服务器运行在服务端的入口,根据客户端请求的部分参数进行请求的分发与调度等。(那不就是洗脚城的前台吗?)


常见的反向代理场景:



  • 负载均衡

  • 动静分离    

  • 业务分离

  • ......


image.png


Nginx 有哪些作用?


1. Nginx的虚拟站点服务


Nginx可基于 端口号、域名(IP) 实现多站点服务,已实现前端访问80或443端口,通过上述不同条件访问不同的后端服务



一个洗脚城前台,后面有不知道数量的技师。



server
{
listen 80;
server_name http://www.hamm.cn;
index index.html;
root /home/www/portal_site;
}
server
{
listen 80;
listen 443 ssl http2;
# 证书相关配置
server_name api.hamm.cn;
index index.html;
root /home/www/api_service;
}
server
{
listen 80;
server_name static.hamm.cn;
index index.html;
root /home/www/static_service;
autoindex on;
}

image.png


2. Nginx反向代理实现负载均衡


通过反向代理指定上游多个负载点,配置不同的负载优先级以及调度策略来实现负载均衡。



按业务能力和服务质量,洗脚城前台往往会针对不同的技师分配不同的任务。



upstream api_slb {
server 192.168.123.78:8100 weight=5 max_fails=3 fail_timeout=5;
server 192.168.123.78:8200 weight=3 max_fails=3 fail_timeout=5;
server 192.168.123.79:8100 weight=2 max_fails=3 fail_timeout=5;
# 优先使用局域网测试服务器的服务 按权重进行负载


# 如果测试服务器不可用,可通过下面两台备用服务跑

server 192.168.123.77:8080 weight=2 backup;
# 同事张三 代码一般是最新 优先使用
server 192.168.123.151:8080 weight=1 backup;
# 同事李四 工位最近 做备机 方便沟通
}
server {
listen 80;
server_name api.hamm.cn;
location / {
proxy_pass http://api_slb;
}
}

image.png


3. Nginx业务分离或动静分离


使用Nginx解决一些特定的需求



  • 相同域名,不同路径访问不同资源

  • 不同域名,解决访问跨域等问题
    .......



技师根据不同的客户需求,分配不同业务能力的技师。



upstream api_service {
server 192.168.123.78:8100;
}
upstream web_frontend {
server 192.168.123.66:8010;
}
server {
listen 80;

# 使用统一域名 http://hamm.cn访问所有资源

server_name hamm.cn;

# 匹配 http://hamm.cn/api/****
# 到系统API服务器上
location /api/ {
proxy_pass http://api_service;
}

# 如果资源在本机 可使用Nginx自带的静态资源服务
location /static {
index index.html;
alias /home/www/static
}

# 匹配其他请求到前端服务器上
location / {
proxy_pass http://web_frontend
}
}

image.png


4. Nginx完成其他场景下的小需求




  • 跨域问题




server {
# API服务
listen 80;
server_name api.hamm.cn;

# 设置允许跨域的域名 支持通配符 如* 代表所有
add_header Access-Control-Allow-Origin hamm.cn;

# 跨域时允许提交的header数据
add_header Access-Control-Allow-Headers 'authorization','content-type';

# 允许跨域的请求方法
add_header Access-Control-Allow-Methods 'option,get,put,post';

# 还有很多配置项 自己百度吧:)
}
server {
# 前端
listen 80;
server_name hamm.cn;
}




  • 代理过滤





使用 sub_filter 模块将代理中一些数据进行过滤或替换:)



server {
...

location /api {
...
sub_filter_types application/json;
sub_filter_once off;

sub_filter '搜藏成功' '收藏成功';
}
}


技师服务的时候有点不耐烦,大喊了一声 “傻X客户”,洗脚城前台为了洗脚城的形象,给替换成了 “好帅的客户”





  • 自定义错误页




server
{
listen 80;
server_name api.hamm.cn;
error_page 502 503 404 403 500 /error.json;
}


技师让客户不高兴的时候,洗脚城每次都出来给大家唱首歌缓和下气氛。





  • 流量控制 请求黑名单IP 防盗链 请求拦截





技师提前预判是不是意向客户,或者专门找事的客户,提前处理好这些事情,不让技师烦恼。



server
{
listen 80;
server_name static.hamm.cn;
root /home/www/static_files/images;
location ~ .*\.(jpg|gif|png|bmp)$ {
# 如果是百度爬虫 让它一边去
if ($http_user_agent ~ 'Baiduspider') {
# rewrite * "/getout.jpg";
return 403;
}
# 图片 允许直接访问 如有跨域 只允许指定的域名访问
valid_referers none *.hamm.cn;
if ($invalid_referer) {
# 其他的请求 通通甩掉
return 403;
}
}
location /admin/ {
# 如果是上传到admin目录下的文件
allow 123.45.67.89;
# 只允许指定的IP可以访问
deny all;
# 其他人通通甩掉
}
}

image.png


总结一下


我真的没去过洗脚城。


作者:Hamm
来源:juejin.cn/post/7295995236886396939
收起阅读 »

写了个APP「原色」—— 基于中国传统色

中国传统色 简介 这是一个工具类APP 颜色筛选以及关模糊查询 颜色详情信息查看以及复制 色卡分享 自定义主题色(长按色卡) 小组件支持 已上架应用宝/App Store,搜索原色即可找到 最初是做了个1.0版本(MVP),功能比较简单,后面感觉没什么可...
继续阅读 »

中国传统色



简介


这是一个工具类APP



  • 颜色筛选以及关模糊查询

  • 颜色详情信息查看以及复制

  • 色卡分享

  • 自定义主题色(长按色卡)

  • 小组件支持


已上架应用宝/App Store,搜索原色即可找到


最初是做了个1.0版本(MVP),功能比较简单,后面感觉没什么可加的就放置一边了


1.0版本.jpeg


最近比较空闲又拿起来,bug修一点加一点,界面改了又改哈哈哈,然后现在迭代到2.0版本(预览图为 iOS)


2.0版本.jpeg
除了界面大换新,也增加了一些功能,比如颜色搜索、筛选、小组件等。Android与iOS基本一致,除了搜索筛选界面不一样:


Android搜索筛选.jpg


下面介绍一下一些功能的实现以及碰到的问题


色卡与文字处理


在1.0版本对色卡的背景颜色和文字颜色关系处理比较粗暴简单,当系统出去浅色模式下。文字就在原来颜色的基础上降低亮度;在深色模式下文字就降低亮度,但是这种方式在部分过亮或者过暗背景上还是很难看清。

2.0版本对色卡和文字颜色都做了动态处理:

色卡:渐变处理,从上往下,比例为0——0.3——1.0。



在浅色模式下颜色为color(alpha=0.7)——color——color;


在深色模式下颜色为color(brightness + 0.2)——color——color



色卡文字:根据颜色是否为亮色进行处理,判断规则为:



颜色为亮色,则降低0.3亮度,否则 降低0.1亮度



在iOS上有用于修改view亮度的方法:brightness(Double),可惜安卓没有直接修改视图或者颜色亮度的方法,于是我就通过修改颜色 HSL来达到类似的效果。为了和ios的brightness 一致,changeBrightness的范围我设置为[-1F, 1F],但outHsl[2]的范围是[0F, 1F],所以计算做了一些调整:


// 修改颜色亮度
@ColorInt
fun @receiver:ColorInt Int.brightness(changeBrightness: Float): Int {
val outHsl = FloatArray(3)
ColorUtils.colorToHSL(this, outHsl)
if (changeBrightness <= 0) {
outHsl[2] = outHsl[2] * (1 + changeBrightness)
} else {
outHsl[2] = outHsl[2] + (1 - outHsl[2]) / 10 * changeBrightness * 10
}
return ColorUtils.HSLToColor(outHsl)
}

// 判断颜色为两色或者暗色
fun @receiver:ColorInt Int.isLight(): Boolean {
val red = Color.valueOf(this).red()
val green = Color.valueOf(this).green()
val blue = Color.valueOf(this).blue()
val brightness = (red * 299 + green * 587 + blue * 114) / 1000
return brightness > 0.5
}

颜色信息展示(BottomSheet)


设置BottomSheet默认完全展开,设置方法如下:


override fun onStart() {
super.onStart()
val behavior = BottomSheetBehavior.from(requireView().parent as View)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}

至于圆角处理,只需要在主题文件里写好就行了:


 <!--Rounded Bottom Sheet-->
<style name="ThemeOverlay.App.BottomSheetDialog" parent="ThemeOverlay.Material3.BottomSheetDialog">
<item name="bottomSheetStyle">@style/ModalBottomSheetDialog</item>
</style>

<style name="ModalBottomSheetDialog" parent="Widget.Material3.BottomSheet.Modal">
<item name="shapeAppearance">@style/ShapeAppearance.App.LargeComponent</item>
<item name="shouldRemoveExpandedCorners">false</item>
</style>

<style name="ShapeAppearance.App.LargeComponent" parent="ShapeAppearance.Material3.LargeComponent">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">24dp</item>
</style>

如果不修改sheet背景色(默认为白色/黑色),只需要设置以上主题就可以了,但是如果修改了背景色,就需要在代码里对背景进行圆角处理,不能直接设置背景色,不然在圆角下面还会有颜色:


默认设置圆角背景.png


动态设置圆角背景.png


// 会存在背景色
// binding.bottomSheetLayout.setBackgroundColor(sheetBackground)

// 设置圆角背景
binding.bottomSheetLayout.setCornerBackground(24, 24, 0, 0, sheetBackground)

private fun View.setCornerBackground(leftRadius: Int, topRadius: Int, rightRadius: Int, bottomRadius: Int, @ColorInt color: Int) {
val shape = ShapeDrawable(RoundRectShape(
floatArrayOf(
leftRadius.dp(requireContext()).toFloat(),
leftRadius.dp(requireContext()).toFloat(),
topRadius.dp(requireContext()).toFloat(),
topRadius.dp(requireContext()).toFloat(),
rightRadius.dp(requireContext()).toFloat(),
rightRadius.dp(requireContext()).toFloat(),
bottomRadius.dp(requireContext()).toFloat(),
bottomRadius.dp(requireContext()).toFloat(),
), null, null)
)
shape.paint.color = color
this.background = shape
}

fun Int.dp(context: Context): Int {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
this.toFloat(),
context.resources.displayMetrics
).toInt()
}

小技巧(应该算啊吧):当我们有icon需要适配深色模式的时候,可以把android:tint的值设置为?android:attr/textColorPrimary ,就不用自己做额外处理了


<vector android:autoMirrored="true" android:height="24dp"
android:tint="?android:attr/textColorPrimary" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">

<path android:fillColor="@android:color/white" android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
</vector>

SearchView背景色修改


可以看之前发的文章



MD3——SearchView自定义背景



效果参考上面的搜索筛选界面


滚动到指定位置(带偏移)


点击左上角的骰子图标,可以随机颜色(滚动到某一位置),通常我们使用recyclerView.scrollToPosition(int position)就可以实现。但是这个方法,会滚动到item的最边缘(红线位置),但是我希望他能够保留一定边距(绿色框框),看起来界面会和谐一点


image.png


解决办法如下:


private fun RecyclerView.scrollToPositionWithOffset(position: Int, offset: Int) {
(layoutManager as GridLayoutManager)
.scrollToPositionWithOffset(position, offset)
}

// 调用
binding.recyclerView.scrollToPositionWithOffset(
Random.nextInt(0, adapter.itemCount - 1),
16.dp(this)
)

用了kotlin扩展方法方便调用,这里的layoutManager根据实际情况来,我这里用列表到的是GridLayoutManager


小组件(App Widget)


提供了两种布局,小尺寸只显示颜色名称,大尺寸显示拼音和名称,效果如下:


Android小组件.jpg


iOS小组件.png


可能在部分系小尺寸统显示有问题,懒得搞了,这个组件大小搞的我脑壳疼,也没看到过什么好的解决方案,以下是我的配置:


// 31以下
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="57dp"
android:minHeight="51dp"
android:updatePeriodMillis="0"
android:previewImage="@drawable/appwidget_preview"
android:initialLayout="@layout/layout_wide_widget"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen">

</appwidget-provider>

// 31及以上
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:targetCellWidth="4"
android:targetCellHeight="2"
android:minResizeWidth="57dp"
android:minResizeHeight="51dp"
android:maxResizeWidth="530dp"
android:maxResizeHeight="450dp"
android:updatePeriodMillis="0"
android:previewImage="@drawable/appwidget_preview"
android:initialLayout="@layout/layout_wide_widget"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen"
android:widgetFeatures="reconfigurable">

</appwidget-provider>

因为布局比较简单,所以尺寸兼容效果相对好一点


主动刷新小组件


当我们app没有运行的时候,添加小组件是没有数据的,当我们打开app的时候,通知小组件更新


// 刷新 Widget
sendBroadcast(Intent(this, ColorWidgetProvider::class.java).apply {
action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
val ids = AppWidgetManager.getInstance(application)
.getAppWidgetIds(ComponentName(application, ColorWidgetProvider::class.java))
putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
})

周期更新小组件


可通过配置updatePeriodMillis来设置时间,但是容易失效,所以使用WorkManager来通知更新,虽然WorkManager保证了周期执行,但如果app不在后台的话还是无法更新的,因为发送了广播app收不到,可能再加个服务就可以了,不加不加了


遗留的小问题


MIUI无法添加小组件


这段代码在MIUI上不生效,无法弹出添加小组件的弹窗


AppWidgetManager.getInstance(this).requestPinAppWidget(xxx)

如果添加该权限并授权,可以成功添加,但是无任何弹窗提示


<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />

当然最奇怪的还是我居然在MIUI的安卓小部件里找不到我自己的组件,我在原生都能看得到我的小组件的,也不知道是不是还需要配置什么,再一次头大


总结


这个app断断续续也写了好几个月,也也没啥功能还写了这么久。之前还看了下swiftUI,写了个iOS版本的,给我的感觉就是上手简单,写起来效率快多了,

其实这篇文章早就可以发了,就为了等app上架,可真煎熬。

个人开发者上架应用真的是难于上青天,对于安卓平台,国内一些主流应用市场(华米OV)都不对个人开发者开放了,要求低点的比如酷安、应用宝个人是可以上传的,但是需要软著,这又是一个头疼的事,申请基本一个月起步,除非花几百块找别人,三五天下证;

PS:现在App需要备案了,除非你不联网,应用宝就可以上架,酷安也要强制备案

ios也让我很难受,可能是我自己的问题,我注册流程走到付款了,当时想着先写完app再注册好了,就没付款,后来再去注册就提示账户存在问题,邮件联系后告诉我:



您的账号由于一个或多个原因,您无法完成 Apple Developer Program 的注册。



我想问清楚具体是什么原因,客服告知由系统判定,他们无法知道也无法干预,然后我寻思罢了,我再注册一个,还是失败,这次提示:



您使用另一 Apple ID 通过 Apple Developer App 验证了身份。要继续,请使用之前用于验证您身份的 Apple ID。



问号.jpeg


然后我又去把原来的账号注销掉,依旧无法注册成功...,最后无奈使用别人的信息注册了一个乛 з乛

所以,想注册苹果开发者的,注意最好是在同一个设备上一次性完成注册。


作者:FaceBlack
来源:juejin.cn/post/7294441582983626788
收起阅读 »

支持远程调试的 "vConsole"

web
背景 前阵子一直在做业务需求,是嵌在公司 APP 里的 H5。而且是跨地区协作,我在 A 城市,测试和产品都在 B 城市。 由于是 H5 项目,开发的时候一般都会实例化个 vConsole,方便查看项目的上下文信息。同时我想着当程序出现问题之后,测试小姐姐可以...
继续阅读 »

背景


前阵子一直在做业务需求,是嵌在公司 APP 里的 H5。而且是跨地区协作,我在 A 城市,测试和产品都在 B 城市。


由于是 H5 项目,开发的时候一般都会实例化个 vConsole,方便查看项目的上下文信息。同时我想着当程序出现问题之后,测试小姐姐可以直接截个 vConsole 的图给我,可以减少沟通时间。


痛点


后来发现一切都是想象之中,我们两个在沟通问题上依旧没少花时间!如果把程序出现的问题分级,那么会有:



  • 😄 简单问题:测试小姐姐 描述问题 发生的过程后,基本可以定位、解决;

  • 😅 中等问题:测试流程走不下去或者程序报错,这时候得查看调试信息去分析。此时需要测试小姐姐 截图 vConsole 上面显示的内容发我,但由于截的图并不一定是关键信息或者信息数据不够,导致这中间会产生几轮沟通;

  • 😥 复杂问题:遇到一些依赖外部信息或者奇奇怪怪的问题的时候,可能会 远程视频 操作测试机给我看,同时我会告诉她什么时候打开 vConsole 查看什么面板的信息。


可以看到只要问题牵扯到了项目的运行信息,前前后后就会导致很多沟通上的时间成本


不禁让人思考是什么原因导致的这个问题……


问题的本质


结合前面的描述我们得知,由于物理空间、跨地域的限制,程序的错误信息都是由测试人员转达给技术人员,不得不说这对测试人员有点勉为其难了,而另一方面造成问题的关键就在于此:技术人员无法和 Bug 直接来个正面交锋!


那么该如何解决这个「中间人」的问题呢?


这个问题的答案其实很简单,我们只要将浏览器的原生 API 进行一层包装将运行时调用的参数收集起来,然后再整一套类似控制台的 UI,最后整合成 SDK 处理参数 -> 中间层网络通信 -> UI 控制台展示的样子,开发同学直接和控制台上的 BUG 切磋,就能完美的解决这个问题!


虽然说起来简单,但是这一整套下来开发的工作量可不容小觑:



  • 包装原生 API 的 SDK

  • 负责通信的服务

  • 控制台 UI……


不用慌!开箱即用的 PageSpy 值得你拥有 😄!


PageSpy


Page Spy 是由货拉拉大前端开源的一款用于调试 H5 、或者远程 Web 项目的工具。基于对原生 API 的封装,它将调用原生方法时的参数进行过滤、转化,整理成格式规范的消息供调试端消费;调试端收到消息数据,提供类控制台可交互式的功能界面将数据呈现出来。


PageSpy是一个强大的开源前端远程调试平台,它可以显著提高我们在面对前端问题时的效率。以下是PageSpy的一些主要特点:



  • 一眼查看客户端信息 PageSpy 会对客户端的运行环境进行识别,其中系统识别支持 Mac / iOS / Window / Linux / Android,浏览器识别支持谷歌、火狐、Safari、Edge、微信、UC、百度、QQ;

  • 实时查看输出: PageSpy 可以实时捕获并显示程序输出,包括 Console、Network、Storage 和 Element。这使开发人员能够直观地了解页面的外观和行为,无需依赖用户的描述或截图。

  • 网络请求监控: PageSpy 还可以捕获和显示页面的网络请求,有助于开发人员更好的查看与后端的交互。

  • 远程控制台: PageSpy 支持远程调试JavaScript代码,允许开发人员执行 JavaScript 代码在用户的浏览器上运行。这对于排查特定问题或测试代码修复非常有帮助。

  • 跨浏览器兼容性: SDK 可以在各种主流浏览器中运行,确保你可以检查和调试不同浏览器上的问题。

  • 用户体验提升: 通过快速识别和解决前端问题,PageSpy可以显著提升用户体验,减少用户因前端问题而受到的不便。


使用 PageSpy 进行远程调试


使用PageSpy进行远程调试是相对简单的。以下是一些基本步骤:



  • 部署PageSpy: 首先,PageSpy 提供了 Docker、Node 和 Release 的一键部署方案,点击查看

  • 实例化 SDK: PageSpy 成功部署后,你可以在项目中引入对应的 SDK 文件并进行实例化,它提供了多场景类型的参数,以便于用户对它的行为进行定制。

  • 实时监控页面: 之后,你可以实时查看页面的各种数据,这有助于你直观地理解页面的问题。

  • 监控网络请求: 你还可以查看所有的网络请求,包括请求的URL、响应代码和响应时间。这可以帮助你识别与后端通信相关的问题。

  • 解决问题: 借助PageSpy提供的信息和工具,你可以更快速地定位和解决前端问题,从而提高用户体验。


相关截图


门户首页


image.png


待调试列表


image.png


调试界面


image.png


image.png


结语


前端远程调试对于快速而准确地解决前端问题至关重要。Page Spy 作为一个强大的开源工具,支持开箱即用,为开发人员提供了一个高效的方式来查看用户端的页面输出、网络请求和执行远程调试。它有助于加速问题的定位和解决,减少了对用户反馈和日志的依赖,提高了整体开发效率。除了解决跨地区协同的场景之外,还覆盖了本地开发时的调试 H5 的场景。


希望本文能够帮到大家对 PageSpy 有个初步的认识,感谢阅读。


作者:Blucas
来源:juejin.cn/post/7298161887882592310
收起阅读 »

把jsp重构成vue

web
记录一次重构的经历与感想!望自己将来开发之路越走越顺利。 话说,我在入职之前,公司一直使用的jsp技术,并结合jQuery来处理前端页面逻辑。 但在我入职之后不久,我们领导就要求把它重构成vue。 这时,我对jsp根本不熟,业务也没摸清楚。且在这个关键时刻,另...
继续阅读 »

记录一次重构的经历与感想!望自己将来开发之路越走越顺利。


话说,我在入职之前,公司一直使用的jsp技术,并结合jQuery来处理前端页面逻辑。


但在我入职之后不久,我们领导就要求把它重构成vue。


这时,我对jsp根本不熟,业务也没摸清楚。且在这个关键时刻,另一个前端因为离家较远直接离职了!


这个担子竟然压到我一个人身上,心里一万匹草泥马奔过。。。


但也没办法,只能赶鸭子上架,怀着极其忐忑的心情进入了开发阶段,当然也有点兴奋,于我而言这也是一个难得的实践机会!


我把这次重构的经历大致分成四个阶段:(1)摸清楚jsp项目的代码(2)用Vue CLI把项目工程搭建起来(3)整理业务逻辑(4)写vue代码


1.摸清楚jsp项目的代码


老项目中的jsp文件是长这样子:


jsp页面.png


JSP(JavaServer Pages)技术是一种基于Java的Web应用程序开发技术,它允许开发人员将Java代码嵌入到HTML中,以动态生成Web页面。


虽然这是一个比较古老的技术,我也是一脸懵圈,但秉承着前端框架无非是对html、css、js的结合的原则,我硬着头皮读代码。


经过一段时间的浸泡,并且在分析后,我发现,只需要稍微懂一点jsp技术,其实就完全可以读懂jsp了,jsp页面最大的特点就是可以通过java注入参数,除此,它和所有前端框架一样是由三部分组成:



  • html部分,就把jsp文件看成html文件,虽然里面注入了一些参数,但这些参数可以让后端通过接口返回,再拿去渲染就行了

  • css部分,老项目和vue基本可以共用

  • js部分,新老项目的功能是一样的,老项目中用jquery实现的,再拿vue去实现一遍即可


基于这些,此时我对把jsp重构成vue已经有了一些把握,总体原则大概是:对html、css、js这三部分,可以重用的部分就重用,不能重用的部分就重写。


2.用Vue CLI把项目工程搭建起来


使用脚手架,很快就搭建好了项目,都是傻瓜式的操作,这个没什么好说的,给大家看一下目录结构:


vue目录结构.png


3.整理业务逻辑


我后知后觉才发现,重构最难的不是编码,而是业务逻辑


我对业务逻辑的信息来源有两个:一是看老项目的源代码,二是问其他老员工


但是前者效率极低,后者又困难重重


为何这么说,参考以下两点:



  1. 程序员最痛苦的莫过于阅读别人写的代码

  2. 作为新人,得遵守一些职场潜规则


但又没办法,只得蛮力通关了,忍受着巨大的痛苦,一方面得加班阅读代码,一方面要虚心求教老员工(我司的环境大概是,对业务越熟的人,脾气就越大,问题问多了,他们会很不耐烦,对此,我做了很多心理建设)。


后来,我也是整理出了一份前端业务逻辑资料,然后被放到了公司公共文件夹里,被后来的员工永久查看学习🐶!


前端业务逻辑整理.png


4.写vue代码


最后阶段就是编码了,我把它分为前期和后期。


前期攻坚难点,重点关照那些难实现的功能,后期画页面,要保持效率,基本要能够一天画2个页面。


后来翻看了一下代码库的提交记录,从第一行代码的提交,到进入测试,历时3个月。


编码阶段是枯燥乏味的,前面靠"蛮劲"可以挺过去,但是现在每天得靠"有恒"二字给自己打气🐪!


然后,终于把项目重构完成了,我也长吁了一口气!


但是事情并没有我想象的那么简单,更恶心的事情来了,bug颇多!


短短几天,测试就提了几百个问题单!


问题单数.png


泪奔啊!蛮劲用完了,恒心也消磨的差不多了!但是问题还是不依不饶的出现。。。


可能我这人就属于那种打不死的小强,想着好不容易坚持到这一步,无论如何我都要拿下它!


于是又向bug们发起了"猛攻"!


又渡过了一段漫长且艰难的解bug时期。。。


终于把bug也解完了,我和测试都长吁一口气!


什么?代码要想顺利上线,还要处理CI?


最后,我精疲力竭的处理完了一千多个CI问题,也终于体会到了,有时候,不逼自己一把,你永远不知道自己可以做到什么程度!


CI数.png


至此,项目终于上线了!


这次重构经历,我思考了这么几个问题:




  1. 公司为什么要重构这个项目?


    答:这个项目本来用户量大,将来还有大量的新需求要接,但是技术架构上已经落后了,如果不重构,将来搞不定新需求,老代码也不好维护,毕竟新来的员工会jsp的没多少。




  2. 重构的重点在哪?


    答:在于业务。业务是大于技术的,特别是新员工,别急着钻研项目中用到了哪些技术,还是多花点时间了解业务吧!




  3. 重构的难点在哪?


    答:技术上的困难总有办法,但是沟通上的困难却似不可逾越的鸿沟,因为工作的日常除了编码,更多的是:和产品互怼、与测试撕逼、向领导交差,所以,程序员们,提升情商吧!




我觉得,最重要的,是进行心态建设,遇到难关不要怕,永远相信自己可以挺过去,毕竟知识是死的,人是活的,只要我们"有恒",就算再难的东西,用"蛮劲"去"猛攻",终将拿下!


作者:玄玄子
来源:juejin.cn/post/7298167437526269952
收起阅读 »

破涕为笑,一个node中间层bug我让同事的名字出现在全球用户的页面上

web
前言 近期遇到了一个线上故障,排查花了很多时间精力,在bug复现过程中,我还不小心让同事的名字出现在了全球用户的页面上,从“一把辛酸泪”到“破涕为笑”,感觉特别有意思。本文中的代码、描述都过滤了敏感信息,以简单demo的形式复现该故障。 如下图,不管用户搜索啥...
继续阅读 »

前言


近期遇到了一个线上故障,排查花了很多时间精力,在bug复现过程中,我还不小心让同事的名字出现在了全球用户的页面上,从“一把辛酸泪”到“破涕为笑”,感觉特别有意思。本文中的代码、描述都过滤了敏感信息,以简单demo的形式复现该故障。


如下图,不管用户搜索啥词,每个用户页面回显的都是sivan
image.png


业务功能描述


站点的搜索功能,搜索功能会根据业务场景继续细分为a搜索,b搜索...,每种搜索在node中间层走的可能是不同的链路。


如下图展示,a搜索回显了default


image.png


b搜索回显了sang和for


image.png


image.png


故障描述



  1. 偶现,但是触发频率很高

  2. 现象为在x国家站点上,不管用户搜索什么内容,页面回显的大概率是一串固定的字符串

  3. 只有在x国家站点会出现该故障,其他国家站点没有出现

  4. 测试环境无法复现,只有线上环境会出现该故障


image.png


image.png


排查


炒面代码分析


从线上的故障现象来看,像是搜索词被替换掉了,分析看客户端页面下发的参数是没有任何问题的,找搜索服务的后端协助,后端说他们接收到的搜索词就已经是有问题的搜索词了。


初步猜测是被类似xssFilter之类的转换函数替换掉了原来的搜索词,或者node中间层有某一条链路的代码把搜索词改掉了。于是把node中间层的搜索链路的相关代码都研究了好几遍,通过关键字搜项目全局,把每个可疑的地方都看了,感觉代码逻辑写的都没毛病(node中间层的代码链路写得跟炒面一样,看得头都大了)。


没办法,代码分析不出来问题所在,测试环境又无法复现,只能在代码分析的基础上,把每个有可能改到搜索词的可疑地方打上日志,在搜索链路的一些比较关键的执行地方也打上日志,重新发版,来辅助排查。


// 线上打日志的时候需要注意加条件限制,不然每个用户请求都打日志,一下子就打爆了
if (req.query.sdebug === 's') {
logger.warn({ /** data */ })
}

抓住日志这根救命稻草


之前也有猜测,可能是网关啊、waf啊把请求拦截下来更改了搜索词,所以我们在请求入口那里也打了日志。从日志上来看,从中间层入口进来时,此刻的搜索词还是正常的,说明不是网关、waf搞的鬼。第一次的日志帮助我们缩小了排查范围,但是还不能分析出来,还需要再补充一些日志,意味着还要再发版,没办法,就是这么麻烦。


考验你js能力的时候到了


日志只是一种辅助手段,帮你记录异常数据,缩小排查范围。是否能从一堆代码中找出那一行有问题的代码就要看你自己了,我把有问题的代码写成一个demo了,展示在下面,你能分析出来问题所在吗?


const express = require('express')
const app = express()

const aConfig = Object.freeze({
info: { word: 'default' },
getWord ({ word }) {
return word
},
})
const bConfig = Object.freeze({
info: {},
getWord ({ req, word }) {
// 日志记录到这里word是'sivan',正常word应该是undefined,取的是req.params[0]才对
return word || req.params[0]
},
})

const setRequestData = ({ info: { word }, getWord }) => {
return (req, res, next) => {
word = word || req.query.word
res.end(getWord({ req, word })) // 回显搜索词
}
}

const getHandler = (config) => {
return setRequestData(config)
}
const aSearch = getHandler(aConfig)
const bSearch = getHandler(bConfig)

app.get('/a-search', aSearch)
app.get(/^\/b-search\/([^\/]+)\/?$/, bSearch)

app.listen(2333, () => {
console.log('run')
})

开始揭开谜底


const setRequestData = ({ info: { word }, getWord }) => {
return (req, res, next) => {
word = word || req.query.word // 罪魁祸首
res.end(getWord({ req, word }))
}
}

在a搜索链路中,word是有值的,为'default'。
在b搜索链路中,word和req.query.word都应该是undefined,所以b搜索链路传给getWord的word应该是undefined才对。


观察setRequestData这个函数的实现,它对config解构出了word变量,然后返回了一个中间件函数,word变量的作用域是在setRequestData的函数作用域里的,setRequestData函数只会执行一次,而中间件函数在每一次请求中都会执行。


中间件函数使用了word变量,这就创建了一个闭包,闭包使得word变量可以长期存储和被访问。


复现步骤如下:



  1. 我们第一次输入http://localhost:2333/b-search/sang?word=sivan(拼接上?word=sivan),回显sivan

  2. 之后我们每次输入http://localhost:2333/b-search/xxx,xxx为任意字符串,都会回显sivan


setRequestData函数只会执行一次,中间件函数每一次请求都会执行,所以当我们第一次输入http://localhost:2333/b-search/sang?word=sivan时,word变量被赋值为req.query.word并因为闭包被存储起来,等下一次输入时,由于word = word || xx,会先取存储的word,这就导致了每一次输入都会回显sivan。


改动思路如下截图:虽然闭包还存在,但是这样修改就不会让闭包的变量值被意外篡改,导致意料之外的结果了。


image.png


归因


这个问题其实挺严重的,搜索功能直接没用,用户都搜不了内容了,打工人打工不容易,哭泣。也挺有意思的,我只要在链接后面拼接?word=sivan就可以让全球的用户看到同事的名字,扬名立万(不止万了,起码千万了),破涕为笑。


为什么是偶现的呢?因为是集群,有很多服务器节点,每一次请求都可能打到不同的节点上,你输入b-search/xx?word=sivan时,请求只会打到其中一个节点上,只会污染那一个节点上的那个长期存储的word变量。所以被污染的集群节点有问题,没被污染到的集群节点就没问题。


为什么只有x国家站点出现该故障?测试环境没出现过该故障?因为这个故障的触发条件比较苛刻,必须输入b-search/xx?word=sivan才会触发,而正常情况下b搜索链路是不会拼接word=sivan这个query参数的。猜测最开始之时,就是有人在x国家站点因为一些原因输入了http://localhost:2333/b-search/sang?word=sivan引发问题,其他国家站点和测试环境没有输入就没有问题。


触发条件这么苛刻,是谁触发的呢?



  • 有可能是用户,毕竟几千万用户在用搜索,什么情况在用户那都会发生

  • 有可能是测试人员,测试在线上环境偶然拼接了这个参数

  • 有可能是黑客或者友商(概率很低,因为只影响了部分站点)

  • 前端开发人员,实现了这么一段如此隐晦的bad代码,等哪一天加班太多,心里不爽了,回家敲几个字拼接url访问,网站功能立马下线。


删库跑路的梗大家都耳熟能详,我们前端不止是会在svg里面、console里面吐槽公司,我们还可以在node中间层里写bad bad的代码哦,而且还很难测出来,事后归因到前端身上。


作者:前端爆冲
来源:juejin.cn/post/7294852698460471308
收起阅读 »

记录一次接口加密的实现方案

web
隔了三个月才写了这篇文章,实在是莫得时间去写,踩坑很多但是输出很少,后面有时间也会多记录一些自己的踩坑经历,要是能给各位同学有所帮助那是最好的了,废话不多说,进入正题了。 背景介绍 由于部门业务体量正在提升,为了防止数据盗取或者外部攻击,对接口进行加密提上了日...
继续阅读 »

0002.jpg
隔了三个月才写了这篇文章,实在是莫得时间去写,踩坑很多但是输出很少,后面有时间也会多记录一些自己的踩坑经历,要是能给各位同学有所帮助那是最好的了,废话不多说,进入正题了。


背景介绍


由于部门业务体量正在提升,为了防止数据盗取或者外部攻击,对接口进行加密提上了日程,部门的大佬们也讨论了各种加密方式,考虑各种情况,最终敲定了方案。说到我们常用的数据加密方法,方式是各种各样的,根据我们实际的业务需求,我们可以选择其中的一种或者几种方式方法进行数据加密处理。




  • 加密方法:常用的AES,RSA,MD5,BASE64,SSL等等;

  • 加密方式:单向加密,对称加密,非对称加密,加密盐,数字签名等等;



首先我们来简单分析一下上面说到的这几种加密有什么区别吧:




  • AES加密:对称加密的方法,加解密使用相同的加密规则,密钥最小能够支持128,192,256位(一个字节8位,后面我使用的是16位字符);

  • RSA加密:非对称加密的方法,加解密使用一对公钥私钥进行匹配,客户端使用公钥加密,服务端使用私钥进行解密;

  • MD5加密:单向加密,加密后不可解密,只能通过相同的数据进行相同的加密再与库中数据进行对比;

  • BASE64:一种数据编码方式,伪加密,把数据转化为BASE64的编码形式,通过A-Z,a-z,0-9,+,/ 共64个字符对明文数据进行转化;

  • SSL加密:https协议使用的加密方式,使用多种加密方式进行加密(具体使用哪些,我也不了解,感兴趣的同学可以去搜一下告诉我哈);



想要详细了解各类加密方式方法的同学,可以自行百度一下哈,这里就不进行赘述了,之后就来详细讲一下本次使用的加密方式。本次为了更加全面加密,使用了AES,RSA,以及加密盐,时间戳,BASE64与BASE16转化等方式进行加密处理。


请求体AES加密


请求体使用AES的对称加密方式,每次接口请求会随机生成一个16位的秘钥,使用秘钥对数据进行加密处理,返回的数据也会使用此秘钥进行解密处理。


import CryptoJs from 'crypto-js'// AES加密库
import { weAtob } from './weapp-jwt' // atob方法

// 请求体加密方法
export const encryptBodyEvent = (data, aeskey, isEncryption) => {
// 请求体内容
const wirteData = {
data: data, // 接口数据
token: Taro.getStorageSync("authToken"), // token 校验
nonce: randomNumberEvent(32), // 32位随机数,接口唯一随机数,可查询服务日志
timestamp: new Date().getTime, // 时间戳,用于设置接口调用过期时间
}
const encryptBodyJson = CryptoJs.AES.encrypt(JSON.stringify(wirteData), CryptoJs.enc.Utf8.parse(aeskey), {
mode: CryptoJs.mode.ECB,
padding: CryptoJs.pad.Pkcs7
}).toString()
// 判断接口是否需要加密
// 服务接收BASE16数据,Base64toHex方法为BASE64转化为BASE16方法
return isEncryption ? Base64toHex(encryptBodyJson) : wirteData
}

// BASE64转化BASE16方法
function Base64toHex (base64) {
let raw = weAtob(base64)
let HEX = ""
for (let i=0; i < raw.length; i++) {
let _HEX = raw.charCodeAt(i).toString(16)
HEX = (_HEX.length == 2 ? _HEX : "0" + _HEX)
}
return HEX
}

// 生成n位随机数,默认生成16位
function randomNumberEvent (length = 16) {
let str = ""
let arr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
// 随机产生
for(let i=0; i < length; i++){
let pos = Math.round(Math.random() * (arr.length-1));
str += arr[pos];
}
return str
}


  • mode是分组加密模式:有五种模式(ECB、CBC、CFB、OFB、CTR),这里我们使用最简单的ECB模式,明文分组加密之后的结果将直接成为密文分组,对其他几种模式感兴趣的可以去搜索一下几种模式的区别;

  • padding是填充模式:正常的加密后的字节长度不可能刚刚好满足固定字节的对齐(块大小),所以需要进行一定的填充,常用的有三种模式(PKCS7、PKCS5、Zero,还有其他模式),这里我们使用的是PKCS7模式。假设数据长度需要填充n个字节才对齐,那么填充n个字节,每个字节都是n;假设数据本身就已经对齐了,则填充一个长度为块大小的数据,每个字节都是块大小;

  • weAtob即小程序使用的atob方法:atob是JS的一个全局函数,用于将BASE64编码转化为原始字符串,在正常的H5项目中atob可以直接使用,但是在小程序中此方法不可用,因此使用一个手动实现的方式(文件就不上传了,电脑是加密的,上传也是乱码,网上也是能找到类似的方法);

  • timestamp是用于防止过期调用:这里的时间是为了展示方便直接使用客户端时间,实际是会调用一个服务端的接口获取服务器时间进行时间校准,防止客户端手动修改时间,服务端设置过期时间,会根据传入的时间判断是否过期;


请求头RSA加密


看完上面的请求体加密,我们会想到一个问题,就是我们的aesKey是客户端随机生成的,但是服务端也需要这个aesKey进行数据的加解密,那么我们通过什么形式传给服务端呢?因此我们在请求头中设置一个secret-key字段,使用RSA中的公钥对aesKey进行加密,服务端使用对应私钥进行解密;


// import JSEncrypt from 'jsencrypt' // RSA加密库,小程序不支持
import WxmpRsa from 'wxmp-rsa' // RSA加密库,小程序支持

let public_key = 'xxxxxxxxxxxxxxxx' // 公钥
// 请求头加密方法
export const randomKeyEvent = (aesKey) => {
// JSEncrypt方法小程序不可用
// const RSAUtils = new JSEncrypt() // 新建JSEncrypt对象
// RSAUtils.setPublicKey(public_key) // 设置公钥
// return RSAUtils.encrypt(aesKey).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '')

const RSAUtils = new WxmpRsa() // 新建WxmpRsa对象
RSAUtils.setPublicKey(public_key) // 设置公钥
// 进行RSA加密后,生成字符串中的部分特殊字符在服务端会被自动转化为空格,导致解密失败,所以先进行转换处理
return RSAUtils.encryptLong(aesKey).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '')
}


  • JSEncrypt在小程序不可用是由于库里面存在window对象以及navigator对象,但是小程序没有对应的方法,所以使用了一个优化后的wxmp-rsa库;

  • replaceAll处理字符是因为RSA加密后,生成字符串中的部分特殊字符传给服务端会被自动转化为空格,导致解密失败,所以需要进行转换处理,为了兼容低版本replaceAll方法不支持可使用replace加正则进行替换;


返回体AES解密


服务端返回的数据内容使用了相同的AES加密方法,因此也需要使用AES进行数据解密处理,并且返回的数据是BASE16,因此还需要进行一次编码转换处理;


// 返回体解密方法
export const decryptBodyEvent = (data, aeskey) => {
// HexToBase64为BASE16转化为BASE64方法
const responseData = CryptoJs.AES.decrypt(HexToBase64(data), CryptoJs.enc.Utf8.parse(aeskey), {
mode: CryptoJs.mode.ECB,
padding: CryptoJs.pad.Pkcs7
}).toString(CryptoJs.enc.Uth8)
return JSON.parse(responseData)
}

// base16转base64 网上找个一个方法,应该有其他简单的实现方式
function HexToBase64 (sha1) {
var digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
var base64_rep = ""
var ascv
var bit_arr = 0
var bit_num = 0

for (var n = 0; n < sha1.length; ++n) {
if (sha1[n] >= 'A' && sha1[n] <= 'Z') {
ascv = sha1.charCodeAt(n) - 55
} else if (sha1[n] >= 'a' && sha1[n] <= 'z') {
ascv = sha1.charCodeAt(n) - 87
} else {
ascv = sha1.charCodeAt(n) - 48
}

bit_arr = (bit_arr << 4) | ascv
bit_num += 4
if (bit_num >= 6) {
bit_num -= 6
base64_rep += digits[bit_arr >>> bit_num]
bit_arr &= ~ (-1 << bit_num)
}
}

if (bit_num > 0) {
bit_arr <<= 6 - bit_num
base64_rep += digits[bit_arr]
}
var padding = base64_rep.length % 4
if (padding > 0) {
for (var n = 0; n < 4 - padding; ++n) {
base64_rep += "="
}
}
return base64_rep
}

封装接口


因为是小程序项目,使用的是Taro框架进行封装的,Vue中使用axios封装其实也是类似的,还封装了一套ajax的方法,除了接口这里封装有区别,加密都是类似的。


const baseUrl = 'https://xxx.xxx.com' // 接口请求头

// Taro封装接口方法
export const requestEncHttp ({url, data, isEncryption = true}) => {
// 每次调用都会随机生成一个动态的aesKey,防止接口被复用
const aesKey = randomNumberEvent()
return new Promise((resolve, reject) => {
Taro.request({
method: "POST",
header: {
"content-type": "application/json",
"secret-key": isEncryption ? randomKeyEvent(aesKey): ''
},
dataType: 'text',
data: encryptBodyEvent(data, aesKey, isEncryption),
url: baseUrl + url,
success: (result) => {
if(result.status === 200) {
resolve(isEncryption ? decryptBodyEvent(result.data, aesKey) : JSON.parse(result.data))
} else {
reject(result)
}
}, fail: (err) => {
reject(err)
}
})
})
}


  • dataType使用text:ajax没有此问题,Taro框架会出现接口有返回数据,但是在success中接收不到数据,因为数据是BASE16形式,Taro封装的数据返回格式默认应该是JSON的,所以要单独设置一下。


总结


加密的方式有很多,这篇文章也只是浅尝即止,想更加详细了解的同学可以再搜一些大佬们的总结文章,我这里也只是结合业务做了一点总结,标注一下踩坑点;



  • 刚开始本来是准备一个月最少写一篇的,但是由于八月份刚换工作,不太有时间去写,所以也是一直拖着;而且是用公司电脑写的这篇文章,所以代码都没有直接粘贴过来,可能会存在疏漏,请多多包涵哈;


作者:追风筝的呆子
来源:juejin.cn/post/7298160530291490828
收起阅读 »

拖拽API的简单应用

web
我们在实际开发中经常能遇见拖拽的运用场景,比如说拖拽排序、拖拽删除等,本文将以实现一个简单的课程表来进行拖拽API的简单应用,帮助大家复习一下一些基础知识。 相关拖拽事件 实现一个元素拖拽,我们只需要在HTML标签设置draggable为true <...
继续阅读 »

我们在实际开发中经常能遇见拖拽的运用场景,比如说拖拽排序、拖拽删除等,本文将以实现一个简单的课程表来进行拖拽API的简单应用,帮助大家复习一下一些基础知识。


tutieshi_640x390_9s.gif


相关拖拽事件


实现一个元素拖拽,我们只需要在HTML标签设置draggabletrue


 <div class="left">
<div draggable="true" class="color1 item">语文</div>
<div draggable="true" class="color2 item">数学</div>
<div draggable="true" class="color3 item">英语</div>
<div draggable="true" class="color4 item">音乐</div>
<div draggable="true" class="color5 item">政治</div>
<div draggable="true" class="color6 item">历史</div>
<div draggable="true" class="color7 item">体育</div>
</div>

我们设置了拖拽属性,在拖动的过程中我们会触发很多事件


// 拖动开始
container.ondragstart = (e) => {
console.log('start', e.target)
}

// 拖动覆盖
container.ondragover = (e) => {
console.log('over', e.target)
}

// 拖动进入
container.ondragenter = (e) => {
console.log('enter', e.target)
}

// 拖动结束
container.ondrop = (e) => {
// 一般div、td是不允许有元素置于他们上面,在ondragover设置阻止冒泡
console.log('drop', e.target)
}

如上,我们在这个应用主要用到了这几个拖拽事件,其中要特别注意的是ondrop事件,因为很多的HTML标签是不允许有其他元素覆盖在他们上面的,我们在案例中最外层用了div标签,所以必须要设置阻止冒泡才能让该事件生效


设置拖拽鼠标样式


如效果图所演示,我们在新增课程的时候,鼠标呈现的是一个加号的状态,在移除时又是一个简单的鼠标样式。这里我们是通过datasetondragstart设置相关属性来进行动态实现的


    <div class="left">
<div data-effect="copy" draggable="true" class="color1 item">语文</div>
<div data-effect="copy" draggable="true" class="color2 item">数学</div>
<div data-effect="copy" draggable="true" class="color3 item">英语</div>
<div data-effect="copy" draggable="true" class="color4 item">音乐</div>
<div data-effect="copy" draggable="true" class="color5 item">政治</div>
<div data-effect="copy" draggable="true" class="color6 item">历史</div>
<div data-effect="copy" draggable="true" class="color7 item">体育</div>
</div>

container.ondragstart = (e) => {
// 设置拖拽鼠标样式 默认值为move
e.dataTransfer.effectAllowed = e.target.dataset.effect
}

设置拖拽背景色


依旧根据设置的datakey,并检索父级,通过ondragenter事件动态插入class,实现背景色的显示


  <div class="left" data-drop="move">
<div data-effect="copy" draggable="true" class="color1 item">语文</div>
<div data-effect="copy" draggable="true" class="color2 item">数学</div>
<div data-effect="copy" draggable="true" class="color3 item">英语</div>
<div data-effect="copy" draggable="true" class="color4 item">音乐</div>
<div data-effect="copy" draggable="true" class="color5 item">政治</div>
<div data-effect="copy" draggable="true" class="color6 item">历史</div>
<div data-effect="copy" draggable="true" class="color7 item">体育</div>
</div>


<tr>
<th rowspan="4" class="span">上午</th>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
<td data-drop="copy"></td>
</tr>


function getDropNode(node){
while(node){
if(node?.dataset?.drop){
return node
}
node = node.parentNode
}
}

function clearDropStyle(){
const dropNodes = document.querySelectorAll('.drop-over')
dropNodes.forEach((node) => {
node.classList.remove('drop-over')
})
}

container.ondragenter = (e) => {
clearDropStyle()
const dropNode = getDropNode(e.target)
if(!dropNode){
return
}
if( e.dataTransfer.effectAllowed === dropNode?.dataset?.drop){
dropNode.classList.add('drop-over')
}
}

实现新增删除


根据一开始的设想,我们是新增了dataset进行同类别的有效拖拽,依旧进行比较,根据情况新增、删除节点


let source;

container.ondragstart = (e) => {
// 设置拖拽鼠标样式
e.dataTransfer.effectAllowed = e.target.dataset.effect
source = e.target
}


container.ondrop = (e) => {
// 一般div、td是不允许有元素置于他们上面,在ondragover设置组织冒泡
console.log('drop', e.target)

clearDropStyle()
const dropNode = getDropNode(e.target)
if(!dropNode){
return
}
if(e.dataTransfer.effectAllowed !== dropNode.dataset.drop){
return
}
if(dropNode.dataset.drop === 'copy'){
dropNode.innerHTML = ''
const cloned = source.cloneNode(true)
cloned.dataset.effect = 'move'
dropNode.appendChild(cloned)
}else{
source.remove()
}
}

我们在ondrop是不能拿到拖拽的节点的,设置一个全局变量,在ondragstart中保存节点,同时在复制完节点后要将其dataset-effect改成move


作者:_初七
来源:juejin.cn/post/7297908859176681484
收起阅读 »

【复盘】2023年写过的bug

前言 在默默的算着,2023年300多天的日子已经悄然无息的从我手中流走,还没来得及细细的品味,2023年即将逝去。在最近工作中,遇到了两个线上比较棘手的bug,今天,就对自己2023年写过的bug来个复盘吧 2023年写过的bug 截至今日,以下数据是202...
继续阅读 »

前言


在默默的算着,2023年300多天的日子已经悄然无息的从我手中流走,还没来得及细细的品味,2023年即将逝去。在最近工作中,遇到了两个线上比较棘手的bug,今天,就对自己2023年写过的bug来个复盘吧


2023年写过的bug


截至今日,以下数据是2023年每月我解决操作过的bug数据分布:


image.png
以上大多数是测试阶段的bug,个别是线上的。作为一个码农,自认为在提测前对自己写过的功能保持着一个比较负责任的心态,尽量保持高质量的提测。但,没有谁能保证不写 bug,不出错的。我们要做的,是在bug出现以后,找到问题的根源,解决问题,避免同类问题再次发生。


解决一线上纰漏bug流程复盘


1、bug现场


前几天公司为迎接双十一,搞满减满折活动促销,对全场商品(除个别商品)仅仅开启每满200-70的满减满折活动,收到反馈,个别商品列表活动的标签显示错误,列表有500个左右商品,大概5个左右商品标签差个每字


image.png


2、代码逻辑


后台创建满减满折活动时,会将哪些商品能参加这个活动具体的标签写入缓存中,C端查询商品列表时,会去商品查询缓存,查看这个商品正在参加的进行中的活动的标签返回给前端展示。由于后台满减活动的配置逻辑是我做的,标签也是我写入缓存的,看到这个反馈,我心中第一反应,满减活动创建添加商品标签的缓存逻辑代码有bug?


3、排查思路



  • 测试环境创建一个满减满折活动,看是否个别商品满减标签显示错误,发现没有问题,复现失败

  • 让测试协助,是否测试环境能否复现此bug,测试也无法复现

  • 由于这块我没有特意代码补充日志,公司框架也没有全局去拦截请求的入参,靠日志排查之路走不通

  • 怀疑是否还开启了别的活动,商品是否还参与别的活动,冲突导致,查看数据库数据,发现目前仅仅就开启着这一个活动

  • 怀疑是否曾经开启过满200-70的活动,发现也没有开启过

  • 去看数据库500个左右的商品,5个标签错误的商品找出规律,没有发现

  • 活动创建,后编辑该了类型?发现活动创建成功后,也没有编辑过

  • 活动创建前端参数错误?回头着重去看我写的那块代码,再三斟酌,没有发现有问题,就算前端参数错误,为啥就那几个商品少了个每次,此路也不通

  • 想过并发,没有道理,因为创建活动就当时一个人操作

  • 想过很多人线上复现不了的bug最常见的解释,网络波动,我那也不应该,要么写入失败,不会写入成功,少一个字

  • 想过redis的缓存数据被人工手动改过?毕竟是线上的,不会随便改,也不应该,概率可以忽略

  • 既然C端的标签是从缓存拿去的,无奈我项目全局搜索所有用到设置商品缓存代码逻辑,一个个前后调用去看,最后给我看到了查询满减活动表,然后设置标签的代码,一步步定位最终最外层controller代码,才发现商品编辑那里有修改商品缓存的逻辑,只要涉及到商品编辑,那里会先删除这个商品的所有缓存,然后在添加缓存,由于那块组装商品标签逻辑和我创建活动添加标签逻辑不一致,少了一个类型判断导致


由于商品管理那块,我没有参与过需求的评审,开发、设计,所以从始至终排查问题都没有想到过那里会影响我做的满减满折活动的商品标签


4、问题解决


任何一个bug,能复现找到问题的根源就好解决,最棘手的时没有人能复现,又没有日志,完全去猜各种可能性


image.png


5、总结



  • 针对一些难以复现的bug或者遇到的技术问题 ,找到根本原因很重要

  • 要多了解业务,把各种变更造成的影响,要能提前预知到

  • 如果项目框架没有完善的全局日志记录,重要地方适当打印下日志


作者:千呼万唤始出来
来源:juejin.cn/post/7297917491795902476
收起阅读 »

面试官问我唯一ID如何生成

由于简历上,写了分库分表,还写了精通微服务和分布式系统,然后被面试官一顿蹂躏了这里面的技术细节;尤其给我印象最深的问题是,在分布式环境下,你们数据的唯一ID是如何去生成的? 当时条件反射,无脑的回答到,通过数据库自增方式生成的唯一ID;面试官听了后,叫我回家等...
继续阅读 »

由于简历上,写了分库分表,还写了精通微服务和分布式系统,然后被面试官一顿蹂躏了这里面的技术细节;尤其给我印象最深的问题是,在分布式环境下,你们数据的唯一ID是如何去生成的?


当时条件反射,无脑的回答到,通过数据库自增方式生成的唯一ID;面试官听了后,叫我回家等通知,然后就没有然后了。。。。。。。


目前业界有哪些唯一ID的生成方式了?


大概有5种。如下图


1698825367530.png


UUID模式


uuid全称是通用唯一识别码(Universally Unique Identifier)。底层是通过mac地址+时间+随机数进行生成的128位的二进制,转换为16进制字符串后,长度为32位。

uuid的生成非常简单, 如果用java代码生成,那只需要一行代码


public static void main(String[] args) {      
String uuid = UUID.randomUUID().toString().replaceAll("-","");
System.out.println(uuid); }

优点: uuid生成的ID 能满足唯一性;并且不依赖任何中间件;生成速度超快。


但是uuid的缺点也很明显: 生成的ID太长了,字符长度有32位

在业务系统里,一般ID是要进行存储到库里的,并且很有可能会作为常用的查询条件,比如存储到mysql,比int32的4字节或者int64的8字节来讲,太占空间;


第二个缺点:就是生成的ID是无序的,不是递增的;这样在把ID做为索引字段,在底层插入索引时,还会引起索引节点的分裂,影响插入速度。 所以用UUID生成的ID,对数据库来说很不友好


那么UUID是不是就没有业务使用场景了?

并不是的,像很多大厂的日志链路追踪的ID,就是基于UUID进行生成的。


redis 自增模式


大家都知道 在redis里有一个INCR命令,能从0到Long.maxValue-1进行单一连续的自增,最大19位;9位数字就是亿基本的数了,还剩10位,假设每天1千W的订单数据,即1千W个ID,能支持25亿年;距离地球毁灭时间还差点


redis id的自增代码也很简单,不详述。


优点:

redis 自增生成的ID 能满足唯一性也能满足单调自增性。做底层数据存储或者索引的时候,对数据库也很友好。生成速度还飞快,因为都是在内存中生成。


缺点:

redis 自增ID的缺点也很明显可能会丢数据
我们知道redis是一个kv内存数据库,虽提供了异步复制数据+哨兵模式 来保证服务的高可用+数据高可靠;但redis不保证不丢数据,即使从库变为主库,但也不保证之前的从库数据是最新的,毕竟是异步同步数据。


建议中大厂,或者能稳定运维redis和具有高可用方案+失败重试方案的厂子使用;小厂慎用吧。


数据库自增模式


在早些年,确实看到过依靠数据库自增的方式,进行唯一ID的生成。此种方式主要是依赖数据库的自增来实现;想要一个唯一的ID,那么往带有自增主键的表里插入一条数据吧,这样sql返回的时候,就会把自增ID返回来了


具体sql


INSERT INTO your_table (column1, column2) VALUES ('value1', 'value2'); SELECT LAST_INSERT_ID();

优点:
数据库自增生成的ID 能满足唯一性也基本能满足自增性


缺点:

生成速度较慢

受限于数据库的实现方式,,ID生成速度相较于前两者至少慢一个数量级,毕竟在生成的时候数据库有磁盘IO操作;高并发下,不建议使用此种方式。而且每次要获得一个ID,还需要往数据库里先插入一条记录。本来高并发下,数据库操作就是个瓶颈了,用了此种方式还加剧了数据库的负担。


数据库号段模式


数据库号段模式,可以看做是数据库自增模式的升级版,在数据库自增模式上进行了性能优化,解决了ID生成慢的问题。
思路入下图:


1698825866585.png


数据库自增模式,每次获取一个ID,都需要操作一次库,太慢了。
号段模式每次操作数据库是申请一个号段的范围。
比如操作一次数据库,申请1000到2000是这个应用的业务申请的号段;然后只能这个应用实例的业务能用;应用申请了号段后放到内存中,每次业务需要的时候,从内存里累加1返回,在java里也有现成的工具,比如AtomicLong包装下(省事还能解决并发问题);如果发现快不够了,还能异步提前从数据库中申请,放入内存中,这样就避免了业务需要唯一ID的时候,在去数据库申请,加快业务获取ID的速度。


优点:

能满足唯一性,也能满足自增性,性能也不差,美团开源的leaf使用此种模式,速度能到 5W/秒。


缺点:

对数据库是强依赖;但我想大多数业务系统基本都依赖数据库吧


个人认为此方案最适合小厂;性能不差,而且也不需要依赖太多的中间件;而且大厂里也不缺乏使用此方案身影,比如滴滴开源的Tinyid也是用的此模式


雪花算法模式


雪花算法(Snowflake)出自大名鼎鼎的twitter公司之手;是的就是那个被硅谷钢铁侠 马斯克收购的公司。
该算法开源后,深受国内大厂好评。并且像百度这种大厂,基于雪花算法思想,开发了自己开源的
UidGenerator 分布式ID生成器。


1698825928764.png


Snowflake生成的是Long类型的ID,我们知道一个Long类型占8个字节空间,1个字节又占8比特,也就是说一个Long类型会用88 64个比特。


Snowflake ID组成结构:正数位(占1比特)+ 时间戳(占41比特)+ 机器ID(占5比特)+ 数据中心(占5比特)+ 自增值(占12比特),总共64比特。



  • 第一个bit位(1bit):Java中long的最高位是符号位代表正负,正数是0,负数是1,一般生成ID都为正数,所以默认为0。

  • 时间戳部分(41bit):毫秒级的时间,不建议存当前时间戳,而是用(当前时间戳 - 固定开始时间戳)的差值,可以使产生的ID从更小的值开始;41位的时间戳可以使用69年,(1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69年 (如果你建设的IT系统要使用100年,慎用此法)

  • 工作机器id(10bit):也被叫做workId,这个可以灵活配置,机房或者机器号组合都可以。

  • 序列号部分(12bit),自增值支持同一毫秒内同一个节点可以生成4096个ID


优点:

* 雪花算法生成的ID,能保证唯一性;

* 随这时间齿轮的流转,也能满足自增性;有大厂的背书,生成速度也是超快;

* 对第三方中间件也是弱依赖


缺点:



  • 存在时钟回拨问题,所有机器时钟必须保存同步,否则会导致生成重复ID;但是也能解决

  • 生成的数字会随这时间的流逝增大,不会因为数据量的增大而增大,在某些业务场景不太适用

  • 需要维护机器ID的配置;或者依赖第三方中间件根据机器信息,生成机器ID


这么多的分布式唯一id生成方法,我该选择哪一种?


为了方便选择,列了个表。到底我的应用里,该选哪个方案了?还需结合自身业务,团队大小,技术实力,实现复杂度和维护难易度进行选择。


image.png


想偷懒,有现成的唯一ID生成工具吗?


有的,像比如美团开源的leaf 能支持数据库号段和雪花模式,传输门:github.com/Meituan-Dia…


百度的 UidGenerator 使用的是雪花模式,传输门:github.com/baidu/uid-g…


而滴滴的 Tinyid 使用的是号段模式,可以认为是基于美团leaf的开源版,传输门:github.com/didi/tinyid…


请注意:以上三个工具都是用java语言写的,用java语言的同学有福了,基本是开箱即用。


集成美团leaf 号段模式


1、从git上 github.com/Meituan-Dia… 下载 leaf源码


1698826132995.png


2、mvn clean install -DskipTests 编译打包工程


3、用工程里的script脚本目录下的sql 脚本,创建数据表


CREATE DATABASE leaf
CREATE TABLE `leaf_alloc` (
`biz_tag` varchar(128) NOT NULL DEFAULT '',
`max_id` bigint(20) NOT NULL DEFAULT '1',
`step` int(11) NOT NULL,
`description` varchar(256) DEFAULT NULL,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`biz_tag`)
) ENGINE=InnoDB;

insert int0 leaf_alloc(biz_tag, max_id, step, description) values('leaf-segment-test', 1, 2000, 'Test leaf Segment Mode Get Id')


4、在leaf-server工程里配置数据库连接信息


1698826171611.png


5、启动leaf-server工程,访问
浏览器访问 http://localhost:8080/api/segment/get/leaf-segment-test即可。


注意:这里的leaf-segment-test即时bizTag。如果想要换成其它业务标,往leaf_alloc插入一条数据即可。


不想用leaf的web模式;但又想用号段模式+本地api方式直接调用,是否可行?


可行的。只需要引入美团的leaf-core jar包即可。是的,这么好的工具,居然没有maven信息;不死心的我,去maven官方仓库搜了一把,还真发现了leaf-core的坐标。
如下:


<dependency>
<groupId>com.tencent.devops.leafgroupId>
<artifactId>leaf-coreartifactId>
<version>1.0.2-RELEASEversion>
dependency>

但是下载下来后,包名居然是腾讯的包名。反编译后,发现代码基本上没怎么变。如果想偷懒,可直接下载。


为了保险起间,我还是用了美团的leaf-core(只好做本地jar依赖)。
然后在代码里加入对ID生成器的初始化。代码如下:



@Service
public class LeafIdDBSegmentImpl implements InitializingBean {
IDGen idGen;
DruidDataSource dataSource;

@Override
public void afterPropertiesSet() throws Exception {
Properties properties = PropertyFactory.getProperties();

// Config dataSource
dataSource = new DruidDataSource();
dataSource.setUrl(properties.getProperty("leaf.jdbc.url"));
dataSource.setUsername(properties.getProperty("leaf.jdbc.username"));
dataSource.setPassword(properties.getProperty("leaf.jdbc.password"));
dataSource.init();

// Config Dao
IDAllocDao dao = new IDAllocDaoImpl(dataSource);

// Config ID Gen
idGen = new SegmentIDGenImpl();
((SegmentIDGenImpl) idGen).setDao(dao);
idGen.init();
}

public Long getId(String key){
Result result = idGen.get(key);
if (result.getStatus().equals(Status.EXCEPTION)) {
throw BizException.createBizException(BizErrorCode.LEAF_CREATE_ERROR_REQUIRED);
}
return result.getId();
}

}


该代码主要做了几件事:

1、在该单列类,实例化后,会进行 数据库源的初始化工作,并且自行实例化IDAllocDao;当然你可以把工程里已有的数据源赋给IDAllocDaoImpl

2、获取ID时,调用底层idGen.get获取

3、业务代码使用时 用 LeafIdDBSegmentImpl.get方法 传入数据库里事先配置好的bizTag即可。


作者:程序员猪佩琪
来源:juejin.cn/post/7296089060834312207
收起阅读 »

一文告诉你,如何实现 IP 属地功能

细心的朋友们可能已经发现了,先在抖音、知乎、快手、小红书等这些平台已经上线了“网络用户显示 IP 的功能”,境外用户显示的是国家,国内的用户显示的省份,而且此项显示无法关闭,归属地强制显示。 作为网友,我们可能只是看看戏,但是作为一个努力学习的码农,我们肯定...
继续阅读 »

细心的朋友们可能已经发现了,先在抖音、知乎、快手、小红书等这些平台已经上线了“网络用户显示 IP 的功能”,境外用户显示的是国家,国内的用户显示的省份,而且此项显示无法关闭,归属地强制显示。



作为网友,我们可能只是看看戏,但是作为一个努力学习的码农,我们肯定要来看一下这个功能是怎么实现的,今天这篇文章,就用几分钟的时间来讲述一下这个功能是怎么实现的。



获取用户 IP 地址


HttpServletRequest 获取 IP




首先我们来看一下,在 Java 中,是如何获取到 IP 属地的,主要有以下两步:



  1. 通过 HttpServletRequest 对象,获取用户的 「IP」 地址

  2. 通过 IP 地址,获取对应的省份、城市]


首先,我们这里写一个工具类用于获取 IP 地址,因为用户的每次 Request 请求都会携带请求的 IP 地址放到请求头中,所以我们可以通过截取请求中的 IP 来获取 IP 地址;


/**
* 网络工具类
*
* @author <a href="https://github.com/liyupi">程序员鱼皮</a>
* @from <a href="https://yupi.icu">编程导航知识星球</a>
*/
public class NetUtils {

/**
* 获取客户端 IP 地址
*
* @param request
* @return
*/
public static String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
if (ip.equals("127.0.0.1")) {
// 根据网卡取本机配置的 IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (Exception e) {
e.printStackTrace();
}
if (inet != null) {
ip = inet.getHostAddress();
}
}
}
// 多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ip != null && ip.length() > 15) {
if (ip.indexOf(",") > 0) {
ip = ip.substring(0, ip.indexOf(","));
}
}
// 本机访问
if ("localhost".equalsIgnoreCase(ip) || "127.0.0.1".equalsIgnoreCase(ip) || "0:0:0:0:0:0:0:1".equalsIgnoreCase(ip)){
// 根据网卡取本机配置的IP
InetAddress inet;
try {
inet = InetAddress.getLocalHost();
ip = inet.getHostAddress();
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
// 如果查找不到 IP,可以返回 127.0.0.1,可以做一定的处理,但是这里不考虑
// if (ip == null) {
// return "127.0.0.1";
// }
return ip;
}

/**
* 获取mac地址
*/
public static String getMacAddress() throws Exception {
// 取mac地址
byte[] macAddressBytes = NetworkInterface.getByInetAddress(InetAddress.getLocalHost()).getHardwareAddress();
// 下面代码是把mac地址拼装成String
StringBuilder sb = new StringBuilder();
for (int i = 0; i < macAddressBytes.length; i++) {
if (i != 0) {
sb.append("-");
}
// mac[i] & 0xFF 是为了把byte转化为正整数
String s = Integer.toHexString(macAddressBytes[i] & 0xFF);
sb.append(s.length() == 1 ? 0 + s : s);
}
return sb.toString().trim().toUpperCase();
}
}

获取用户的 IP 地址属地


淘宝库获取用户 IP 地址属地




通过这个方法,就可以重请求头中获取到用户的 IP 地址了,然后接下来就是 IP 地址归属地省份、城市的获取了,这里可以用很多 IP 地址查询的库进行查询,这里用一个库来测试一下。


淘宝 IP 地址库:ip.taobao.com/



不过淘宝的 IP 地址查询库已经在 2022 年 3 月 31 日下线了,这里我们就不能使用它了,只能另辟蹊径了。



这里我们截取一段之前淘宝货期 IP 地址的源码,然后一起来看一下。



这里可以看到,在日志文件中,出现了大量的 the request over max qps for user 问题。


虽然这个方法已经寄了,但是我们求知的道路可以停吗?肯定不可以啊,这里我们就来整一个新的获取 IP 地址属地的方法,也就是我们今天文章的主角:Ip2region。


Ip2region 介绍




这个是在之前的一篇文章看到的,他是一个 Gthub 的开源项目,即 Ip2region 开源项目


地址如下:github.com/lionsoul201…


这个开源库目前已经更新到了 V2 的版本,现在的它是一个强大的离线IP地址定位库和IP定位数据管理框架,其达到了微秒级别的查询效率,还提供了众多主流编程语言的 xdb 数据生成和查询客户端实现,可以说是非常得好用👍👍👍👍,今天这篇文章我们主要针对其 V2 版本进行讲解,如果想要查询 1.0 版本的内容的话,可以去 Github 上面进行查看。


Ip2region 详解


高达 99.9 % 的查询准确率




数据聚合了一些知名 ip 到地名查询提供商的数据,这些是他们官方的的准确率,经测试着实比经典的纯真 IP 定位准确一些。ip2region 的数据聚合自以下服务商的开放 API 或者数据(升级程序每秒请求次数 2 到 4 次),比例如下:





  1. 80%, 淘宝 IP 地址库, ip.taobao.com/%5C




  2. ≈10%, GeoIP, geoip.com/%5C

  3. ≈2%, 纯真 IP 库, http://www.cz88.net/%5C


Ip2region V2.0 特性




1、IP 数据管理框架


xdb 支持亿级别的 IP 数据段行数,默认的 region 信息都固定了格式:国家|区域|省份|城市|ISP,缺省的地域信息默认是0。 region 信息支持完全自定义,例如:你可以在 region 中追加特定业务需求的数据,例如:GPS信息/国际统一地域信息编码/邮编等。也就是你完全可以使用 ip2region 来管理你自己的 IP 定位数据。


2、数据去重和压缩


xdb 格式生成程序会自动去重和压缩部分数据,默认的全部 IP 数据,生成的 ip2region.xdb 数据库是 11MiB,随着数据的详细度增加数据库的大小也慢慢增大。


3、极速查询响应


即使是完全基于 xdb 文件的查询,单次查询响应时间在十微秒级别,可通过如下两种方式开启内存加速查询:



  1. vIndex 索引缓存 :使用固定的 512KiB 的内存空间缓存 vector index 数据,减少一次 IO 磁盘操作,保持平均查询效率稳定在10-20微秒之间。

  2. xdb 整个文件缓存:将整个 xdb 文件全部加载到内存,内存占用等同于 xdb 文件大小,无磁盘 IO 操作,保持微秒级别的查询效率。


多语言以及查询客户端的支持


已经客户端有:Java、C#、php、C、Python、Node.js、PHP 拓展(PHP 5 和 PHP 7)等,主要如下:


binding描述开发状态binary查询耗时b-tree查询耗时memory查询耗时
cANSC c binding已完成0.0x毫秒0.0x毫秒0.00x毫秒
c#c# binding已完成0.x毫秒0.x毫秒0.1x毫秒
Golanggolang binding已完成0.x毫秒0.x毫秒0.1x毫秒
Javajava binding已完成0.x毫秒0.x毫秒0.1x毫秒
Lualua实现 binding已完成0.x毫秒0.x毫秒0.x毫秒
Lua_clua的c扩展已完成0.0x毫秒0.0x毫秒0.00x毫秒
nginxnginx的c扩展已完成0.0x毫秒0.0x毫秒0.00x毫秒
nodejsnodejs已完成0.x毫秒0.x毫秒0.1x毫秒
phpphp实现 binding已完成0.x毫秒0.1x毫秒0.1x毫秒
php5_extphp5的c扩展已完成0.0x毫秒0.0x毫秒0.00x毫秒
php7_extphp7的c扩展已完成0.0毫秒0.0x毫秒0.00x毫秒
pythonpython bindng已完成0.x毫秒0.x毫秒0.x毫秒
rustrust binding已完成0.x毫秒0.x毫秒0.x毫秒

Ip2region xdb Java 查询客户端实现




这里简单展示一下 Java 的实现,这里使用开发中常用的 Maven 实现的方式:


1. 引入 Maven 仓库


由于项目使用Spring 的方式构建,这里可以选择使用引入 Spring 的 starter 的方式进行


<dependency>
<groupId>com.github.hiwepy</groupId>
<artifactId>ip2region-spring-boot-starter</artifactId>
<version>2.0.1.RELEASE</version>
</dependency>

<dependency>
<groupId>org.lionsoul</groupId>
<artifactId>ip2region</artifactId>
<version>2.7.0</version>
</dependency>

在引入 Maven 依赖之后,我们这里引入几种实现的方式:


2. 实现方式 1:【基于文件查询】


import org.lionsoul.ip2region.xdb.Searcher;
import java.io.*;
import java.util.concurrent.TimeUnit;

public class SearcherTest {
public static void main(String[] args) {
// 1、创建 searcher 对象
String dbPath = "ip2region.xdb file path";
Searcher searcher = null;
try {
searcher = Searcher.newWithFileOnly(dbPath);
} catch (IOException e) {
System.out.printf("failed to create searcher with `%s`: %s\n", dbPath, e);
return;
}

// 2、查询
try {
String ip = "1.2.3.4";
long sTime = System.nanoTime();
String region = searcher.search(ip);
long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost);
} catch (Exception e) {
System.out.printf("failed to search(%s): %s\n", ip, e);
}

// 3、备注:并发使用,每个线程需要创建一个独立的 searcher 对象单独使用。
}
}

3. 实现方式 2:【缓存VectorIndex索引】


我们可以提前从 xdb 文件中加载出来 VectorIndex 数据,然后全局缓存,每次创建 Searcher 对象的时候使用全局的 VectorIndex 缓存可以减少一次固定的 IO 操作,从而加速查询,减少 IO 压力。


import org.lionsoul.ip2region.xdb.Searcher;
import java.io.*;
import java.util.concurrent.TimeUnit;

public class SearcherTest {
public static void main(String[] args) {
String dbPath = "ip2region.xdb file path";

// 1、从 dbPath 中预先加载 VectorIndex 缓存,并且把这个得到的数据作为全局变量,后续反复使用。
byte[] vIndex;
try {
vIndex = Searcher.loadVectorIndexFromFile(dbPath);
} catch (Exception e) {
System.out.printf("failed to load vector index from `%s`: %s\n", dbPath, e);
return;
}

// 2、使用全局的 vIndex 创建带 VectorIndex 缓存的查询对象。
Searcher searcher;
try {
searcher = Searcher.newWithVectorIndex(dbPath, vIndex);
} catch (Exception e) {
System.out.printf("failed to create vectorIndex cached searcher with `%s`: %s\n", dbPath, e);
return;
}

// 3、查询
try {
String ip = "1.2.3.4";
long sTime = System.nanoTime();
String region = searcher.search(ip);
long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost);
} catch (Exception e) {
System.out.printf("failed to search(%s): %s\n", ip, e);
}

// 备注:每个线程需要单独创建一个独立的 Searcher 对象,但是都共享全局的制度 vIndex 缓存。
}
}

4. 实现方式 3:「缓存整个 xdb 数据」


我们也可以预先加载整个 ip2region.xdb 的数据到内存,然后基于这个数据创建查询对象来实现完全基于文件的查询,类似之前的 memory search。


import org.lionsoul.ip2region.xdb.Searcher;
import java.io.*;
import java.util.concurrent.TimeUnit;

public class SearcherTest {
public static void main(String[] args) {
String dbPath = "ip2region.xdb file path";

// 1、从 dbPath 加载整个 xdb 到内存。
byte[] cBuff;
try {
cBuff = Searcher.loadContentFromFile(dbPath);
} catch (Exception e) {
System.out.printf("failed to load content from `%s`: %s\n", dbPath, e);
return;
}

// 2、使用上述的 cBuff 创建一个完全基于内存的查询对象。
Searcher searcher;
try {
searcher = Searcher.newWithBuffer(cBuff);
} catch (Exception e) {
System.out.printf("failed to create content cached searcher: %s\n", e);
return;
}

// 3、查询
try {
String ip = "1.2.3.4";
long sTime = System.nanoTime();
String region = searcher.search(ip);
long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost);
} catch (Exception e) {
System.out.printf("failed to search(%s): %s\n", ip, e);
}

// 备注:并发使用,用整个 xdb 数据缓存创建的查询对象可以安全的用于并发,也就是你可以把这个 searcher 对象做成全局对象去跨线程访问。
}
}

5. 编译测试程序


通过 maven 来编译测试程序。


# cd 到 java binding 的根目录
cd binding/java/
mvn compile package

然后会在当前目录的 target 目录下得到一个 ip2region-{version}.jar 的打包文件。


6. 查询测试



  • 可以通过 java -jar ip2region-{version}.jar search 命令来测试查询:


➜  java git:(v2.0_xdb) ✗ java -jar target/ip2region-2.6.0.jar search
java -jar ip2region-{version}.jar search [command options]
options:
--db string ip2region binary xdb file path
--cache-policy string cache policy: file/vectorIndex/content


  • 例如:使用默认的 data/ip2region.xdb 文件进行查询测试:


➜  java git:(v2.0_xdb) ✗ java -jar target/ip2region-2.6.0.jar search --db=../../data/ip2region.xdb
ip2region xdb searcher test program, cachePolicy: vectorIndex
type 'quit' to exit
ip2region>> 1.2.3.4
{region: 美国|0|华盛顿|0|谷歌, ioCount: 7, took: 82 μs}
ip2region>>

输入 ip 即可进行查询测试,也可以分别设置 cache-policy 为 file/vectorIndex/content 来测试三种不同缓存实现的查询效果。


bench 测试


可以通过 java -jar ip2region-{version}.jar bench 命令来进行 bench 测试,一方面确保 xdb 文件没有错误,一方面可以评估查询性能:


➜  java git:(v2.0_xdb) ✗ java -jar target/ip2region-2.6.0.jar bench
java -jar ip2region-{version}.jar bench [command options]
options:
--db string ip2region binary xdb file path
--src string source ip text file path
--cache-policy string cache policy: file/vectorIndex/content

例如:通过默认的 data/ip2region.xdb 和 data/ip.merge.txt 文件进行 bench 测试:


➜  java git:(v2.0_xdb) ✗ java -jar target/ip2region-2.6.0.jar bench --db=../../data/ip2region.xdb --src=../../data/ip.merge.txt
Bench finished, {cachePolicy: vectorIndex, total: 3417955, took: 8s, cost: 2 μs/op}

可以通过分别设置 cache-policy 为 file/vectorIndex/content 来测试三种不同缓存实现的效果。 @Note: 注意 bench 使用的 src 文件要是生成对应 xdb 文件相同的源文件。


作者:夭要7夜宵
来源:juejin.cn/post/7295576148364296231
收起阅读 »

为什么堂堂微信数据库表名、字段名起的如此随意?

1.微信数据库解密 微信数据库在在哪个文件夹 EnMicroMsg.db的父文件加密规则是md5("mm" + uin)这样就可以准确的获取到db文件的位置. uin的获取:/data/data/com.tencent.mm/shared_prefs/au...
继续阅读 »

1.微信数据库解密




  • 微信数据库在在哪个文件夹


    EnMicroMsg.db的父文件加密规则是md5("mm" + uin)这样就可以准确的获取到db文件的位置.


    uin的获取:/data/data/com.tencent.mm/shared_prefs/auth_info_key_prefs.xml`里面有个uinz字段,直接获取value值,示例如下图所示:


    image-20210526135110550




  • 解密微信数据库:目前只涉及两个数据库EnMicroMsg.db(微信数据涉数据库)和WxFileIndex.db(文件索引数据库)


    解密的密码:md5(手机imei+uin)的32位字符串取前7位,如果imei拿不到就用1234567890ABCDEF代替




2. 好友相关信息


微信的好友相关数据涉及三张表:rcontact,bizinfo,img_flag




  • rcontact表存放了该账号涉及的所有账号(微信账号,群账号)的基本信息(eg:微信昵称,备注,原微信号,改之后的微信号,全拼等等)。如下图所示:


    111




  • bizinfo表存放的是该账号的好友微信号,群账号,这里好友包括已经通过的和添加没通过的,如下所示:


    22222




  • img_flag表存放该账号所有涉及的微信(好友,同属一个群不是好友,添加的陌生人)的头像地址,数据如下图所示:


    3333


    总结:rcontact表是一张基础表,存放所有的账号基本信息,bizinfo存放是该账号的好友信息或者群组信息,img_flag存放了微信账号对应的头像信息,以下场景有:




    • 获取微信好友信息,查询sql如下:


      select r.username, r.alias, r.conRemark, r.nickname, r.pyInitial, r.quanPin,r.encryptUserName, i.reserved2 from rcontact r INNER JOIN img_flag i  on r.username = i.username where r.type&2=2  and r.username not like '%@chatroom' and i.lastupdatetime > 0



    • 获取添加未通过的好友信息,此时有两种情况:1)添加同属一个群的好友。2)添加陌生人。比如说通过微信号,扫码什么。这两种情况在数据库的表现形式是不一样的,添加同属一个群的,在bizinfo表会插入一条username为添加好友的微信号记录,而如果是添加陌生人,则username是一个以@stranger结尾的key,对应的数据如下图所示:


      4444


      注意:这里如果通过微信号,扫码添加的陌生人,其username是一长串的以@stranger结尾的key,同 时pyInitial,qunPin两个字段存的并不是这个陌生人的微信号


      查询sql如下:


      SELECT r.username, r.alias, r.conRemark, r.nickname, r.pyInitial, r.quanPin, r.encryptUserName, i.reserved2 FROM rcontact r INNER JOIN bizinfo b ON r.username = b.username INNER JOIN img_flag i ON r.username = i.username 
      WHERE r.type <> 33 and r.type & 2 <> 2 AND r.username <> '当前微信号' AND r.username NOT LIKE '%@chatroom' AND b.updateTime > 0



    • 获取同属一个群但不是好友的基本信息:


      查询sql如下:


      SELECT DISTINCT r.username, r.alias, r.conRemark, r.nickname, r.pyInitial, r.quanPin, i.reserved2 FROM rcontact r  INNER JOIN img_flag i ON r.username = i.username 
      WHERE r.username not in(select username from bizinfo) and i.lastupdatetime >0

    3.微信群组


    微信群组信息表为chatroom,存放着一些基本信息,数据如下图所示:


    555


    注意:微信群组一开始建立显示群昵称是所有好友微信昵称加起来的一个字符串,即displayname字段,但是如果修改了群昵称之后,显示的是修改之后的,这时候需要根据根据群账号chatroomname去rcontact表做关联查询,根据rcontact表的username等于群账号查询出一条记录,此时这条记录的字段nickname即修改后的群昵称,查询sql如下:


    select c.chatroomname, c.memberlist, c.displayname, c.roomowner, c.selfDisplayName, r.nickname from chatroom c inner join rcontact r on r.username = c.chatroomname where c.modifytime > 0


    目前的微信群组的头像在img_flag表没有存储,暂时找不到资源所在


    4.微信聊天数据


    微信的聊天记录是保存在message表中的,数据示例如下图:


    666


    msgSvrId:唯一标示一条聊天记录的id,可以作为更新聊天记录数据的条件


    createTime:发送消息的时间


    talker:如果是群账号,说明这条消息是群会话消息,发送人在content内容前面(发送人微信号:发送内容);如果是好友微信号,说明这条消息是好友会话消息


    isSend:发送或者接收标志。0:接收 1:发送


    type:消息类型 1:文本内容(包括小表情) 3:图片 34:语音 43:视频 47:大表情 49:文件


    436207665:微信红包 419430449:微信转账


    图片,视频,语音, 文件 根据msgId去索引库WxFileIndex的表WxFileIndex2查询




    • 图片查询sql:


      select * from WxFileIndex2 where msgId in(msgIds) and msgType=3 and msgSubType20



    • 语音查询sql:


      select * from WxFileIndex2 where msgId in(msgIds) and msgType=34



    • 视频查询sql:


      select * from WxFileIndex2 where msgId in(msgIds) and msgType=43 and msgSubType=1



    • 文件查询sql:


      select * from WxFileIndex2 where msgId in(msgIds) and msgType=49 and msgSubType = 34



    • 大表情查询sql:根据groupId去找到对应的包名,md5即表情的文件名


      select e.md5, e.groupid, m.msgSvrId from emojiinfo e INNER JOIN message m on e.md5=m.imgpath where m.type=47



    5.总结


    以上分析师基于Android系统端的微信,且微信数据的撤销删除仍需要研究,待补充,未完待续.......




作者:shepherd111
来源:juejin.cn/post/7295160228879122458
收起阅读 »

适合小公司的自动化部署脚本

背景(偷懒) 在小小的公司里面,挖呀挖呀挖。快挖不动了,一件事重复个5次,还在人肉手工,身体和心理就开始不舒服了,并且违背了个人的座右铭:“偷懒”是人类进步的第一推动力。 每次想要去测试环境验证个新功能,又或者被测试无情的催促着部署新版本后;都需要本地打那个2...
继续阅读 »

背景(偷懒)


在小小的公司里面,挖呀挖呀挖。快挖不动了,一件事重复个5次,还在人肉手工,身体和心理就开始不舒服了,并且违背了个人的座右铭:“偷懒”是人类进步的第一推动力


每次想要去测试环境验证个新功能,又或者被测试无情的催促着部署新版本后;都需要本地打那个200多M的jar包;以龟速般的每秒几十KB网络,通过ftp上传到服务器;用烂熟透的jps命令查找到进程,kill后,重启服务。


是的,我想偷懒,想从已陷入到手工部署的沼泽地里走出来。如何救赎?


自我救赎之路


我的诉求很简单,想要一款“一键CI/CD的工具”,然后可以继续偷懒。为了省事,我做了以下工作


找了一款停止服务的脚本,并做了小小的优化


首推 陈皮大哥的停服脚本(我在里面加了个sleep 5);脚本见下文。只需要修改 APP_MAINCLASS的变量“XXX-1.0.0.jar”替换为自己jar的名字即可,其它不用动


该脚本主要是通过jps + jar的名字获得进程号,进行kill。( 脚本很简单,注释也很详细,就不展开了,感兴趣可以阅读下,不到5分钟,写过代码的你能看懂的)


把以下脚本保存为stop.sh


#!/bin/bash
# 主类
APP_MAINCLASS="XXX-1.0.0.jar"
# 进程ID
psid=0
# 记录尝试次数
num=0
# 获取进程ID,如果进程不存在则返回0,
# 当然你也可以在启动进程的时候将进程ID写到一个文件中,
# 然后使用的使用读取这个文件即可获取到进程ID
getpid() {
javaps=`jps -l | grep $APP_MAINCLASS`
if [ -n "$javaps" ]; then
psid=`echo $javaps | awk '{print $1}'`
else
psid=0
fi
}
stop() {
getpid
num=`expr $num + 1`
if [ $psid -ne 0 ]; then
# 重试次数小于3次则继续尝试停止服务
if [ "$num" -le 3 ];then
echo "attempt to kill... num:$num"
kill $psid
sleep 5
else
# 重试次数大于3次,则强制停止
echo "force kill..."
kill -9 $psid
fi
# 检查上述命令执行是否成功
if [ $? -eq 0 ]; then
echo "Shutdown success..."
else
echo "Shutdown failed..."
fi
# 重新获取进程ID,如果还存在则重试停止
getpid
if [ $psid -ne 0 ]; then
echo "getpid... num:$psid"
stop
fi
else
echo "App is not running"
fi
}
stop

编写2行的shell 启动脚本


修改脚本中的XXX-1.0.0.jar为你自己的jar名称即可。保存脚本内容为start.sh。jvm参数可自行修改


basepath=$(cd `dirname $0`; pwd)
nohup java -server -Xmx2g -Xms2g -Xmn1024m -XX:PermSize=128m -Xss256k -XX:+DisableExplicitGC -XX:+UseParNewGC -XX:-UseAdaptiveSizePolicy -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:LargePageSizeInBytes=128m -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails -Xloggc:logs/gc.log -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:HeapDumpPath=logs/dump.hprof -XX:ParallelGCThreads=4 -jar $basepath/XXX-1.0.0.jar &>nohup.log &

复用之前jenkins,自己写部署脚本


脚本一定要放到 Post Steps里


1689757456174.png


9行脚本,主要干了几件事:



  • 备份正在运行的jar包;(万一有啥情况,还可以快速回滚)

  • 把jenkins上打好的包,复制到目标服务上

  • 执行停服脚本

  • 执行启动服务脚本


脚本见下文:


ssh -Tq $IP << EOF 
source /etc/profile
#进入应用部署目录
cd /data/app/test
##备份时间戳
DATE=`date +%Y-%m-%d_%H-%M-%S`
#删除备份jar包
rm -rf /data/app/test/xxx-1.0.0.jar.bak*
#备份历史jar包
mv /data/app/test/xxx-1.0.0.jar /data/app/test/xxx-1.0.0.jar.bak$DATE
#从jenkins上拉取最新jar包
scp root@$jenkisIP:/data/jenkins/workspace/test/target/XXX-1.0.0.jar /data/app/test
# 执行停止应用脚本
sh /data/app/test/stop.sh
#执行重启脚本
sh /data/app/test/start.sh
exit
EOF

注:



  • $IP 是部署服务器ip,$jenkisIP 是jenkins所在的服务器ip。 在部署前请设置jenkins服务器和部署服务器之间ssh免密登录

  • /data/app/test 是部署jar包存放路径

  • stop.sh 是上文的停止脚本

  • start.sh 是上文的启动脚本


总结


如果不想把时间浪费在本地打包,忍受不了上传jar包的龟速网络,人肉停服和启动服务。请尝试下这款自动部署化脚本。小小的投入,带来大大的回报。


原创不易,请 点赞,留言,关注,转载 4暴击^^


参考资料:

xie.infoq.cn/article/52c…
Linux ----如何使用 kill 命令优雅停止 Java 服务

blog.csdn.net/m0_46897923… --ssh免密登录


作者:程序员猪佩琪
来源:juejin.cn/post/7257440759569055802
收起阅读 »

程序员必看的几大定律,你中招了吗?

一 晕轮效应 我们通常会从局部信息形成一个完整的印象,根据最少量的情况对别人或其他事物做出全面的结论。 举个简单的例子,当你看到一个陌生美女的时候,你通常会认为对方长得这么好看,笑起来这么甜,肯定哪哪都好! 当你看到一位发量稀疏的开发老哥,你通常会觉得这位...
继续阅读 »

一 晕轮效应



我们通常会从局部信息形成一个完整的印象,根据最少量的情况对别人或其他事物做出全面的结论。



举个简单的例子,当你看到一个陌生美女的时候,你通常会认为对方长得这么好看,笑起来这么甜,肯定哪哪都好!


当你看到一位发量稀疏的开发老哥,你通常会觉得这位老哥技术能力肯定非常强,做人肯定也很靠谱!



在晕轮效应影响下,一个人或事物的优点或缺点一旦变为光圈被扩大,其缺点或优点也就隐退到光圈的背后,被别人视而不见了。



对于程序员来说,有两点可以考虑:




  1. 打造自己的光晕:让自己成为专家或者像个专家,可以提升自己的话语权。


    比如在掘金写文章,当你拥有几千粉丝,几十万阅读量的时候,即使你什么都不说,别人看到这个账号都会觉得你是个厉害人物,与你交流的时候都会注意几分。




  2. 突破别人的光晕:在我使用npm上各种组件或者工具的时候,常常会感慨,好厉害!这么多star的项目,肯定没有Bug吧,如果有,那一定是我用的方式不对!打开源码看的时候更是惊呼,好厉害,完全看不懂,也不可能看的懂吧?


    但其实,褪去这些光环,你会发现,即使是成熟的项目,也会有bug,高深的代码,也都只是一个个基础的语句组合起来的,当你理解了作者的思想,也就能理解代码。




二 眼不见为净定律



看不见的,就是干净的。



看到这个定律,我的第一反应就是,屎山代码为什么会存在?


还不是因为管理人员看不到这坨屎山,他看到的是一个功能正常运行的系统,所以人家并不觉得这是屎山,而是美丽的风景线!


只有我们这些天天在这座屎山上添砖加瓦的程序员才能会感受到这种绝望!


所以面对屎山代码,不要抱怨,最好的方法就是找个机会把这座屎山丢给其他人,毕竟眼不见为净嘛!


当它不在你手上的时候,你会发现其实它也挺好的,毕竟眼不见为净嘛!


三 虚假同感偏差


你们是否会遇到这种情况:明明一件很重要的事情,催了某个人很久了,他却迟迟未做!


这里就涉及到虚假同感偏差,因为这件事对你来说很重要,所以通常会自我推断,觉得别人也会认为这件事情很重要,然而事实上,对你很重要的事,对他人来说可能回过头就给忘记了!


所以啊,要让别人重视一件你觉得很重要的事情,就是也让他感觉到重要,这样别人就不敢忘记了,比如可以补充一句:某某领导正在关注这件事,麻烦尽快,谢谢!


另外就是当我们非常确信自己观点或意见的时候,也很容易产生虚假同感偏差,这时候如果有人提出不同的观点,我们会下意识的反驳,并且觉得问题来自于他人。


比如我们自信满满地写完一段代码并且自测之后,提交给测试人员进行测试,当测试人员跟你反馈存在某BUG,我相信第一时间反应大多都是:我不信!!!


然后就有以下对话:



你:可能前端有缓存,你刷新一下再试试?


测试:行,我试一下。


过了十分钟......


测试:还是一样的问题啊,你看一下。


你:是不是测试数据有问题啊,我自己都测试过了,不应该有问题!


测试:行吧,我再看看。


过了十分钟......


测试:数据都排查过了,是正常的,你检查一下吧!


你:(还想再挣扎一下)你怎么操作的?


测试:就点击一下这个按钮,我还能玩出什么花吗?


排查了一会,哦~居然是空值的情况没有判断,我还能再白痴一点吗!


你:问题已经修复了,是某某复杂的场景我没考虑清楚,你再测试一下!



四 自我宽恕定律



人性有个根深蒂固的特点,就是容易发现别人的缺点和错误,却不容易看到自己的不足。


正所谓,见人之过易,见己之过难。





  • 当看到别人的代码存在一个空指针异常,心里想:这都能忘记判断,其他代码会不会也有问题!




  • 当发现自己的代码存在一个空指针异常,心里想:只是不小心忘记判断了嘛,加一下就好了!




  • 当接手别人项目的时候:卧槽,这代码写的啥啊,注释没几句,变量命名这么奇葩,逻辑这么混乱,我要咋接啊!!




  • 当项目给别人接手时候:我这代码虽然注释不多,但是很规范的,你看这变量命名不就能知道是什么含义了嘛,逻辑也非常顺,这个方法几百行你按顺序看下来就行了,我都给你写在一起,不需要跳来跳去地看,多方便!




五 补偿作用:



弱点也是一种力量源!



大家应该都听说过这个现象:瞎子的眼睛虽然看不见了,听力通常会变得非常灵敏!


这种生理上的现象吸引了很多有兴趣的心理学家,所谓补偿,就是发挥一个人的最大优势,激发其自信心,抵消其弱点。


看到补偿机制,我第一想到的就是在掘金看到的各种专科大佬。


虽然学历起点比其他人低一些,但有时候正是因为学历劣势,更加激发他们深耕技术的决心,反而达到其他更高学历人员都无法达到的高度。


这又让我想起了一句话:打不倒我的,会让我更强大!!


六 皮尔斯定理



意识到无知,才是知道的开始。



还有一句话,我觉得很适合接在这句话后面:知道的越多,才发现自己不懂的越多!


于是就形成了一个闭环: 意识到无知->开始知道->知道的越多->意识到无知


这句话我相信大部分人都听过很多遍了,不知道你们是从什么时候开始意识到自己的无知呢?


曾经,我还是小白的时候,在福州某公司上班,每天做的事情就是SpringBoot接口的开发,或者修改某些业务逻辑,我以为这差不多就是开发的全部了。


那时候对接的前端是使用Vue写的,我甚至不知道什么是Vue,只知道是某个挺流行的前端技术。


每次部署,我看前端文件里就只有一个index.html文件,我真的非常奇怪,为什么这么大的项目,只有一个html文件?


那时候我对前端认知还停留在html+js+css+jquery的时代,所以完全想不通。


本来还觉得自己前端也是有点基础的,直到接触Vue,我才惊呼,卧槽,前端怎么变成这样子了?什么nodejs,什么npm完全没听说过。


用过一段时间之后,我更是惊呼,卧槽,前端还能这样子?明明我HTML+CSS+JS只懂一点,都能做出这么好看的页面了。


有了各种开源前端组件,即使对原生HTML标签和CSS不太懂,也能算是个还不错的前端开发了。


还有这ES6语法用起来也太爽了吧,比JAVA可自由太多了。


所以很感慨,当我没进入前端圈子的时候,还以为自己懂一些,进入之后,才发现自己真的是一窍不通,啥都要学。


更感慨的是,当我第一次接触掘金,我惊呼,卧槽,这个社区分享的东西都好干啊,好多听都没听过的标题,原来我有这么多东西都不懂!原来前端是个这么卷的领域!


结语


感谢阅读,希望本篇文章对你有所帮助!


作者:林劭敏
来源:juejin.cn/post/7295623585363771443
收起阅读 »

如何优雅的判断一个对象是否为空?

我们在刚开始学习Java的时候,遇到过最多的异常肯定是臭名昭著的空指针异常(NullPointerException),可以说它陪伴了我们整个初学阶段。字符串、对象、集合等等一不留神就容易出现空指针异常! 那么如何优雅的判断一个对象是否为空并且减少空指针异常呢...
继续阅读 »

我们在刚开始学习Java的时候,遇到过最多的异常肯定是臭名昭著的空指针异常(NullPointerException),可以说它陪伴了我们整个初学阶段。字符串、对象、集合等等一不留神就容易出现空指针异常!


那么如何优雅的判断一个对象是否为空并且减少空指针异常呢?


今天来介绍一个容器类——Optional


Optional介绍


Optional是一个容器类,它只有两种情况:



  • 要么包含一个非空对象

  • 要么为空


它有助于编写更健壮的代码,以处理可能为空的值,而不必担心空指针异常!


Optional用法


Optional的创建


Optional有以下两种常见的创建方法:



  • Optional.of(T value):创建一个包含非空值的Optional,如果value为null,则抛出NullPointerException

  • Optional.ofNullable(T value):创建一个Optional,允许value为null


判断Optional容器中是否包含对象


isPresent(): 返回一个布尔值,如果Optional容器中包含一个非空对象则返回true,否则返回false


获取Optional容器的对象



  • get(): 如果Optional包含非空值,返回该值;否则抛出NoSuchElementException

  • orElse(T other): 如果Optional为空,返回指定的默认值other

  • orElseGet(Supplier<? extends T> other): 如果Optional为空,使用提供的Supplier函数生成默认值

  • orElseThrow(Supplier<? extends X> exceptionSupplier): 如果Optional为空,抛出由提供的Supplier函数生成的异常


过滤


filter(Predicate<? super T> predicate): 如果Optional包含非空值且满足predicate条件,则返回当前Optional,否则返回一个空Optional。


映射



  • map(Function<? super T, ? extends U> mapper): 如果Optional包含非空值,应用mapper函数并返回新的Optional。

  • flatMap(Function<? super T, Optional> mapper): 类似于map,但允许mapper函数返回Optional。


Optional示例


假如我们有一个User类,可以使用Optional来处理可能为空的User对象。User类结构如下:


public class User {
private String name;

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

public String getName() {
return name;
}
}

示例:创建Optional



Optional userOptional = Optional.ofNullable(new User("张三"));



示例:判断Optional是否包含对象


if (userOptional.isPresent()) {
System.out.println("用户存在:" + userOptional.get().getName());
} else {
System.out.println("用户不存在");
}

示例:获取Optional容器的对象


User user = userOptional.orElse(new User("李四"));
System.out.println("User: " + user.getName());

示例:过滤


Optional<User> filteredUserOptional = userOptional.filter(u -> u.getName().startsWith("张"));
if (filteredUserOptional.isPresent()) {
System.out.println("结果:" + filteredUserOptional.get().getName());
} else {
System.out.println("未找到对应用户");
}

示例:映射


Optional<String> userNameOptional = userOptional.map(User::getName);
userNameOptional.ifPresent(name -> System.out.println("用户名为: " + name));

使用场景总结



  • 当你从某个方法返回一个值,但该值可能为空,而调用者需要明确知道值是否存在。

  • 在处理方法参数时,你可以用Optional来表示某个参数可以为空,以提醒调用者可能会传入null。

  • 避免繁琐的null检查和条件语句,使代码更简洁和可读!



更多文章干货,推荐公众号【程序员老J】



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