注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

史上最污技术解读,我竟然秒懂了!

假设你是个妹子,你有一位男朋友,于此同时你和另外一位男生暧昧不清,比朋友好,又不是恋人。你随时可以甩了现任男友,另外一位马上就能补上。这是冷备份。假设你是个妹子,同时和两位男性在交往,两位都是你男朋友。并且他们还互不干涉,独立运行。这就是双机热备份。假设你是个...
继续阅读 »

假设你是个妹子,你有一位男朋友,于此同时你和另外一位男生暧昧不清,比朋友好,又不是恋人。你随时可以甩了现任男友,另外一位马上就能补上。这是冷备份


假设你是个妹子,同时和两位男性在交往,两位都是你男朋友。并且他们还互不干涉,独立运行。这就是双机热备份

假设你是个妹子,不安于男朋友给你的安全感。在遥远的男友未知的地方,和一位男生保持着联系,你告诉他你没有男朋友,你现在处于纠结期,一旦你和你男朋友分开了,你马上可以把自己感情转移到异地男人那里去。这是异地容灾备份

假设你是个妹子,有一位男朋友,你又付了钱给一家婚姻介绍所,让他帮你留意好的资源,一旦你和你这位男朋友分开,婚姻介绍所马上给你安排资源,你感情不间断运行,这是云备份。。。。

假设你是个妹子,你怀疑男朋友对你的忠诚,在某宝购买了一个测试忠诚度的服务。这是灾难演练。友情提醒,在没有备份的情况下,切忌进行灾难演练,说不好会让你数据血本无归。

假设你是个妹子,你和男友异地恋,你每天晚上都打电话查岗,问他还爱不爱你了,这叫ping

假设你是个妹子,你的男友经常玩失踪,所以你希望时刻掌握他的行踪,你先打电话给他的好基友A,A说好基友B知道,B说好基友C知道,C说好基友D知道,D说你男朋友正在网吧打游戏,你终于知道了男友在哪儿,这叫TraceRoute

假设你是个妹子,你的男友沉迷游戏经常不接电话无故宕机,所以当你们约好下午逛街以后你要时不时的打个电话询问,看看他是不是还能正常提供服务,这叫心跳监测

假设你是个妹子,你想去逛街而你的男友A在打游戏不接电话,于是乎你把逛街的请求发给了替补男友B,从而保障服务不间断运行,这叫故障切换

假设你是个妹子,你有很多需要男朋友完成的事情,于是乎你跟A逛街旅游吃饭不可描述,而B只能陪你逛街,不能拥有全部男朋友的权利,这叫主从配置 master-slave

假设你是个妹子,你败家太厉害,以至于你的男友根本吃不消,于是呼你找了两个男朋友,一三五单号,二四六双号限行,从而减少一个男朋友所面临的压力,这叫负载均衡

假设你是个妹子并且有多个男朋友,配合心跳检测与故障切换和负载均衡将会达到极致的体验,这叫集群LVS,注意,当需求单机可以处理的情况下不建议启用集群,会造成大量资源闲置,提高维护成本。

假设你是个妹子,你的需求越来越高导致一个男朋友集群已经处理不了了,于是乎你又新增了另外几个,这叫多集群横向扩容,简称multi-cluster grid

假设你是个妹子,你的男朋友身体瘦弱从而无法满足需求,于是乎你买了很多大补产品帮你男朋友升级,从而提高单机容量,这叫纵向扩容,Scale up。切记,纵向扩容的成本会越来越高而效果越来越不明显。

假设你是个妹子,你跟男友经常出去游玩,情到深处想做点什么的时候却苦于没有tt,要去超市购买,于是乎你在你们经常去的地方都放置了tt,从而大幅度降低等待时间,这叫CDN

假设你是个妹子,你的男朋友英俊潇洒风流倜傥财大气粗对你唯一,于是乎你遭到了女性B的敌视,B会以朋友名义在周末请求你男朋友修电脑,修冰箱,占用男朋友大量时间,造成男朋友无法为你服务,这叫拒绝服务攻击,简称DOS

假设你是个妹子,你因男朋友被一位女性敌视,但是你男朋友的处理能力十分强大,处理速度已经高于她的请求速度,于是她雇佣了一票女性来轮流麻烦你的男朋友,这叫分布式拒绝服务攻击,简称DDOS

假设你是个妹子,你发现男朋友总是在处理一些无关紧要的其它请求,于是乎你给男朋友了一个白名单,要求他只处理白名单内的请求,而拒绝其它身份不明的人的要求,这叫访问控制

假设你是个妹子,你男朋友风流倜傥,你总担心他出轨,于是你在他身上安装了一个窃听器,里面内置了一些可疑女生勾搭行为的特征库,只要出现疑似被勾搭的情况,就会立刻向你报警,这叫入侵检测系统(IDS)

假设你是个妹子,你改良了上面的窃听器,当可疑女性对你男朋友做出勾搭行为的时候,立刻释放1万伏电压,把可疑人击昏,终止这次勾搭。这叫入侵防御系统(IPS)

假设你是个妹子,虽然你装了各种窃听器、报警器,可是你蓝朋友处处留情,报警器响个不停,让你应接不暇,疲于奔命,于是你搞了个装置集中收集这些出轨告警,进行综合分析,生成你男朋友的出轨报告。这叫SIEM或者SOC

假设你是个妹子,你把男朋友的出轨报告提交给他父母,得到了他们的大力支持,男友父母开始对他严加管教、限期整改,为你们的爱情保驾护航,做到合情合理、合法合规,这叫等级保护

假设你是个妹子,你离男朋友家有点远,你开车去,这叫自建专线,你打车过去,这叫租用专线,你骑摩拜单车过去,这叫SDWAN

假设你是个妹子,你和男朋友的恋爱遭到了双方家长的反对,不准双方往来,你们偷偷挖了一条隧道,便于进行幽会,这叫VPN

假设你是个妹子,你的男朋友太优秀而造人窥视,于是乎它们研究了一下你的男朋友,稍微修改了一点点生产出一个男朋友B,与你的男朋友百分制99相似,这不叫剽窃,这叫逆向工程,比如男朋友外挂。

假设你是个妹子,你要求你的男朋友坚持十分钟,然后十五分钟继而二十分钟,以测试你男朋友的极限在哪里,这叫压力测试

假设你是个妹子,为了保证你男朋友的正常运行,于是乎你每天查看他的微信微博等社交资料来寻找可能产生问题的线索,这叫数据分析

假设你是个妹子,你的男朋友属于社交活跃选手,每天的微博知乎微信生产了大量信息,你发现自己的分析速度远远低于他生的速度,于是乎你找来你的闺蜜一起分析,这叫并行计算

假设你是个妹子,你的男朋友太能折腾处处留情产生了天量的待处理信息,你和你的闺蜜们已经累趴也没赶上他创造的速度,于是你付费在知乎上找了20个小伙伴帮你一起分析,这叫云计算

假设你是个妹子,你在得到男朋友经常出没的地点后,根据酒店,敏感时间段等信息确定男朋友因该是出轨了,这叫数据挖掘

假设你是个妹子,在分析男友的数据后,得知他下午又要出去开房,于是乎你在他准备出门前给他发了个短信,问他有没有带tt,没有的话可以在我这里买,这叫精准推送,需要配合数据挖掘。另外,搜索公众号顶级科技后台回复“物联网平台”,获取一份惊喜礼包。

假如你是个妹子,你的男朋友总该出去浪而各种出问题,于是乎你租了间屋子并准备好了所有需要的东西并告诉他,以后不用找酒店了,直接来我这屋子吧,什么都准备好了,这叫容器

假如你是个妹子,你每天都要和男朋友打通一次接口,采集数据。你一天24小时不停地采,这叫实时数据采集。你决定开发新的接口来和男朋友交流,这叫虚拟化。你决定从不同的男友身上采集数据,你就是大数据中心。有一天你决定生一个宝宝,这叫大数据应用。宝宝生下来不知道是谁的,这叫大数据脱敏。但是从宝宝外观来看,黑色皮肤金色头发,这叫数据融合跨域建模。你决定把这个宝宝拿来展览收点门票,这叫大数据变现

你还有什么想要补充的吗?

版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢!
收起阅读 »

Android 实现卡片堆叠,钱包管理效果(带动画)

先上效果图源码 github.com/woshiwzy/Ca…实现原理:1.继承LinearLayout 2.重写onLayout,onMeasure 方法 3.利用ValueAnimator 实施动画 4.在动画回调中requestLayout 实现动画效果...
继续阅读 »


先上效果图


源码 github.com/woshiwzy/Ca…

实现原理:

1.继承LinearLayout
2.重写onLayout,onMeasure 方法
3.利用ValueAnimator 实施动画
4.在动画回调中requestLayout 实现动画效果

思路:

1.用Bounds 对象记录每一个CardView 对象的初始位置,当前位置,运动目标位置

2.点击时计算出对应的view以及可能会产生关联运动的View的运动的目标位置,从当前位置运动到目标位置,然后以这2个位置作为动画参数实施ValueAnimator动画,在动画回调中触发onLayout,达到动画的效果。

重写adView 方法,确保新添加的在这里确保所有的子view 都有一个初始化的bounds位置

   @Override
   public void addView(View child, ViewGroup.LayoutParams params) {
       super.addView(child, params);
       Bounds bounds = getBunds(getChildCount());
  }

确保每个子View的测量属性宽度填满父组件

    boolean mesured = false;
   @Override
   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       super.onMeasure(widthMeasureSpec, heightMeasureSpec);
       if (mesured == true) {//只需要测量一次
           return;
      }
       mesured = true;
       int childCount = getChildCount();
       int rootWidth = getWidth();
       int rootHeight = getHeight();
       if (childCount > 0) {
           View child0 = getChildAt(0);
           int modeWidth = MeasureSpec.getMode(child0.getMeasuredWidth());
           int sizeWidth = MeasureSpec.getSize(child0.getMeasuredWidth());

           int modeHeight = MeasureSpec.getMode(child0.getMeasuredHeight());
           int sizeHeight = MeasureSpec.getSize(child0.getMeasuredHeight());

           if (childCount > 0) {
               for (int i = 0; i < childCount; i++) {
                   View childView = getChildAt(i);
                   childView.measure(MeasureSpec.makeMeasureSpec(sizeWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(sizeHeight, MeasureSpec.EXACTLY));
                   int top = (int) (i * (sizeHeight * carEvPercnet));
                   getBunds(i).setTop(top);
                   getBunds(i).setCurrentTop(top);
                   getBunds(i).setLastCurrentTop(top);
                   getBunds(i).setHeight(sizeHeight);
              }

          }

      }
  }

重写onLayout 方法是关键,是动画触发的主要目的,这里layout参数并不是写死的,而是计算出来的(通过ValueAnimator 计算出来的)

@Override
   protected void onLayout(boolean changed, int sl, int st, int sr, int sb) {
       int childCount = getChildCount();
       if (childCount > 0) {
           for (int i = 0; i < childCount; i++) {
               View view = getChildAt(i);
               int mWidth = view.getMeasuredWidth();
               int mw = MeasureSpec.getSize(mWidth);
               int l = 0, r = l + mw;
               view.layout(l, getBunds(i).getCurrentTop(), r, getBunds(i).getCurrentTop() + getBunds(i).getHeight());
          }
      }
  }

源码

github: github.com/woshiwzy/Ca…

作者:Sand
来源:juejin.cn/post/7073371960150851615

收起阅读 »

程序员的双十一是怎样的?

说起双十一,大部分人的印象就是买买买买买买。大家肯定认为,作为程序员的我们,肯定如你们一样过双十一很轻松惬意吧,事实上,真实情况却远非如此,换句话说,真实情况比你想象的要差很多。与你们而言是轻松惬意,对我们来说更像是一场苦战。下面呢我们来说说程序员过双十一,那...
继续阅读 »

说起双十一,大部分人的印象就是买买买买买买。大家肯定认为,作为程序员的我们,肯定如你们一样过双十一很轻松惬意吧,事实上,真实情况却远非如此,换句话说,真实情况比你想象的要差很多。与你们而言是轻松惬意,对我们来说更像是一场苦战。下面呢我们来说说程序员过双十一,那些不为人知的心酸故事。


正常人过双十一呢,都是在互相比价,互相看折扣,等待满减,然后期待快递,一切看起来稀松平常。

但是程序员所面临的状况却是完全相反,当双十一开始的那一刻,整个服务器就进入了高度运行的状态,而这背后的程序员就要解决各种各样的bug,以防止服务器的崩溃。


举一个通俗的例子,正常来说,一个房间在一定的时间内,所通行的人数和数量基本都是一定的,而且数量在平时相对较小。这时候服务器的上限如果够大的话,在不需要很多程序的情况下是可以正常运行的。


但是到了双十一这一天就不一样了,这个房间不仅人数爆满,而且出入口都异常拥挤,这就是高并发,而程序员一个秩序管理员把出现的各种问题bug都要一一解决,保持人流的正常疏散,以及整个房间内秩序的稳定。

除此之外还有各种新品的上线以及所不为人知的变动,都需要程序员加以维护以防止出现问题。

然后就有人会问,如果没有程序员做这些工作会出现什么样的状况呢?

一个明显的结果就是这个页面崩溃,你就再也不能通过这个页面购买你所心爱的东西了。这种情况就是这个需求量还没加到服务器的上限,但是因为商家的某些改动比如说满减等不及时,或者改多了改错了,不能及时的进行撤销,在这期间产生的交易将会给商家或者是购买者带来极大的损失。

双十一期间程序员生存现状。


双十一期间基本上就是进入了高度加班状态,*吃饭睡觉什么都通通在公司进行,要知道在我们这个行业本来就是一个高强高压的状态,在双十一期间更甚,真的是让我们*苦不堪言。每天都在祈祷自己的维修的页面能够正常运行 少给自己添点bug,多给自己添点头发。


但每一次购物都可能是一个程序员,用自己的头发换来的,大家一定要抓住双***十一***,珍惜这次机会啊!


作者:HelloWorld先生
来源:juejin.cn/post/7160986859512791071

收起阅读 »

B+树的生成过程 怎么去看懂B+树

前提看B+树 看不懂树结构什么意思 这一篇可以帮你理解树结构生成的过程在说B+ 树之前 需要知道 一页的大小是多少show global status like 'innodb_page_size'这个是看出 一页是 16384 也就是 16384/102...
继续阅读 »

前提

看B+树 看不懂树结构什么意思 这一篇可以帮你理解树结构生成的过程

在说B+ 树之前 需要知道 一页的大小是多少

show global status like 'innodb_page_size'


这个是看出 一页是 16384 也就是 16384/1024 = 16kb innodb中一页的大小默认是 16kb

正文

创建表结构 指定引擎为 Innodb

CREATE TABLE tree(
id int PRIMARY key auto_increment,
t_name VARCHAR(20),
t_code int
) ENGINE=INNODB

查看一下当前表的索引情况

show index from tree 

B树和B+树的显示都是BTREE 但是实际使用的B+树 但是B+树也是B树的升级版 称之为B树也是没有问题的


创建数据 这里会有一个小知识点 如果看过上一篇文章的朋友可以明白是为什么

INSERT into tree VALUES(3,"变成派大星",3);
INSERT into tree VALUES(1,"变成派大星",1);
INSERT into tree VALUES(2,"变成派大星",2);
INSERT into tree VALUES(4,"变成派大星",4);
INSERT into tree VALUES(7,"变成派大星",7);
INSERT into tree VALUES(5,"变成派大星",5);
INSERT into tree VALUES(6,"变成派大星",6);
INSERT into tree VALUES(8,"变成派大星",8);


疑问

为什么创建数据的时候数据是乱的但是在创建好数据 被拍好顺序了

基础知识

我们在寻找答案之前 想明白一些基础知识

细心的朋友可以看出来 我们插入Id 时候数据是乱的 插入进去之后 数据就自动帮我通过Id 进行排序了 这是为什么呢?接着往下看

我们如果对于B+树有点了解的话就知道B+树是每页16KB 进行数据储存 在进行数据查询的时候也是一页一页的去查询

相当于下面的数据

首先每一页都有很多数据 就像 我们平常去写分页的时候我们返回给前端的数据也会有很多属性


这个可能比较抽象 我是把他当成平常 分页查询的思想代入进去

我们可以把一页想成是一个对象

@Data
public class page {

List<UserRecords> data;

....省略其余属性
}

我们先看一下 一页数据的图是什么样子 仅仅是进行逻辑思考画的图

这里的Data 就相当于 一页中的数据区域


但是这里是有限制的 上面我们说到 一页的数据只能是16Kb 也就是 一个Page 里面的data只能16Kb 数据 当超过16Kb 就会新开一个对象相当于在进行创建树的时候增加了判断

Java代码思路模拟:


当 Page 对象的大小已经达到16Kb 就算完成这一页 把这一页放到 磁盘中等待使用就行了 到时候进行查询数据的时候会直接返回这一页 里面包含这些数据

我们回到最初的问题 为什么我们在进行插入的时候明明Id 是乱的 等到插入到数据的时候 数据就变成有序的了 我们知道 同时这个数据是根据主键进行排序的 也就Innobd 的数据储存一定是要依赖主键的 有些人会想 我就是不创建主键 他还能排序吗?

疑问二

我们在疑问一的基础上 产生出的疑问 不设置主键 Mysql怎么办

解答

InnoDB对聚簇索引处理如下:

  • 如果定义了主键,那么InnoDB会使用主键作为聚簇索引

  • 如果没有定义主键,那么会使用第一非空的唯一索引(NOT NULL and UNIQUE INDEX)作为聚簇索引

  • 如果既没有主键也找不到合适的非空索引,InnoDB会自动帮你创建一个不可见的、长度为6字节的row_id,而且InnoDB 维护了一个全局的 dictsys.row_id,所以未定义主键的表都共享该row_id,每次插入一条数据,都把全局row_id当成主键id,然后全局row_id加1

很明显,缺少主键的表,InnoDB会内置一列用于聚簇索引来组织数据。而没有建立主键的话就没法通过主键来进行索引,查询的时候都是全表扫描,小数据量没问题,大数据量就会出现性能问题。

但是,问题真的只是查询影响吗?不是的,对于生成的ROW_ID,其自增的实现来源于一个全局的序列,而所以有ROW_ID的表共享该序列,这也意味着插入的时候生成需要共享一个序列,那么高并发插入的时候为了保持唯一性就避免不了锁的竞争,进而影响性能

解答

我们看完疑问二的解答就知道 即便我们不设置主键 数据也会帮我们去生成一个默认的主键 有点像 类默认生成构造器的思想

有了主键之后呢?


为什么会自动排序 大家都知道了 其实在文章之初就会有很多人明白是为什么 大概脑子里会有答案

疑问三

为什么要进行排序

解答

我们都知道 在进行数据查找的时候 比如几个基础的查找算法的 前提都是 先进行排序 再者 List 和 Map 的一些区别肯定都很熟悉了 当然是为了更快 所以无需的Id 会对插入效率造成影响 也就是为什么很多文章说使用自增Id比UUID 或者雪花算效率高的原因 第一个是UUID他们是随机的 每次都要重新排序 甚至可能会因为排序的原因造成页数据的更换 还有就是 UUID 一般都比较长 一页是16Kb 数据越短 一页的数据就会越多 查询的速度也就比较快

这里说完为什么排序 还有一个点就是上面的页目录

疑问三

页目录的作用是什么?

通过上一篇文章可以明白 页目录的作用是减少范围


这里的第三层是数据 上面都是目录 可以增加数据的检索效率


如果没有目录我们需要去直接遍历数据区域 会降低效率 目录能帮我们缩小范围 这里 我们查询 ID = 3 我们可以通过目录知道 1 < 3 < 4 如果 在1 中没有找到对应数据 但是 因为 3 < 4 就不会接着往下查询了 直接返回空结果

当第一页没有的时候去第二页查询 不会直接跳到第二页查询


为了提高效率 当目录数据数量过多时 就会网上延伸一层树 同时可以减少磁盘的IO次数


关于所有叶子节点都处于同一深度是如何实现的?这与B+树具体的插入和删除算法有关。简单解释一下插入时的情况,根据插入值的大小,逐步向下直到对应的叶子节点。如果叶子节点关键字个数小于2t,则直接插入值或者更新卫星数据;如果插入之前叶子节点已经满了,则分裂该叶子节点成两半,并把中间值提上到父节点的关键字中,如果这导致父节点满了的话,则把该父节点分裂,如此递归向上。所以树高是一层层的增加的,叶子节点永远都在同一深度。

小总结

  • 内部节点并不存储真正的信息,而是保存其叶子节点的最小值作为索引。

  • 每次插入删除都进行更新(此时用到parent指针),保持最新状态。

  • B+ 树非叶子节点上是不存储数据的,仅存储键值

  • B+只在底层树储存数据,上层就会存储更多的键值,相应的树的阶数(节点的子节点树)就会更大,树就会更矮更胖,如此一来我们查找数据进行磁盘的 IO 次数又会再次减少,数据查询的效率也会更快。

  • B+ 树的阶数是等于键值的数量的,如果我们的 B+ 树一个节点可以存储 1000 个键值,那么 3 层 B+ 树可以存储 1000×1000×1000=10 亿个数据。

  • 一般根节点是常驻内存的,所以一般我们查找 10 亿数据,只需要 2 次磁盘 IO。

  • 因为 B+ 树索引的所有数据均存储在叶子节点,而且数据是按照顺序排列的。

  • 那么 B+ 树使得范围查找,排序查找,分组查找以及去重查找变得异常简单

  • 有心的读者可能还发现上图 B+ 树中各个页之间是通过双向链表连接的,叶子节点中的数据是通过单向链表连接的。

  • 其实上面的 B 树我们也可以对各个节点加上链表。这些不是它们之前的区别,是因为在 MySQL 的 InnoDB 存储引擎中,索引就是这样存储的。

  • 我们通过数据页之间通过双向链表连接以及叶子节点中数据之间通过单向链表连接的方式可以找到表中所有的数据。

作者:变成派大星
来源:juejin.cn/post/7162856738692005918

收起阅读 »

一个 MySQL 隐式转换的坑,差点把服务器整崩溃了

本来是一个平静而美好的下午,其他部门的同事要一份数据报表临时汇报使用,因为系统目前没有这个维度的功能,所以需要写个SQL马上出一下,一个同事接到这个任务,于是开始在测试环境拼装这条 SQL,刚过了几分钟,同事已经自信的写好了这条SQL,于是拿给DBA,到线上跑...
继续阅读 »

本来是一个平静而美好的下午,其他部门的同事要一份数据报表临时汇报使用,因为系统目前没有这个维度的功能,所以需要写个SQL马上出一下,一个同事接到这个任务,于是开始在测试环境拼装这条 SQL,刚过了几分钟,同事已经自信的写好了这条SQL,于是拿给DBA,到线上跑一下,用客户端工具导出Excel 就好了,毕竟是临时方案嘛。

就在SQL执行了之后,意外发生了,先是等了一下,发现还没执行成功,猜测可能是数据量大的原因,但是随着时间滴滴答答流逝,逐渐意识到情况不对了,一看监控,CPU已经上去了,但是线上数据量虽然不小,也不至于跑成这样吧,眼看着要跑死了,赶紧把这个事务结束掉了。

什么原因呢?查询的条件和 join 连接的字段基本都有索引,按道理不应该这样啊,于是赶紧把SQL拿下来,也没看出什么问题,于是限制查询条数再跑了一次,很快出结果了,但是结果却大跌眼镜,出来的查询结果并不是预期的。


经过一番检查之后,最终发现了问题所在,是 join 连接中有一个字段写错了,因为这两个字段有一部分名称是相同的,于是智能的 SQL 客户端给出了提示,顺手就给敲上去了。但是接下来,更让人迷惑了,因为要连接的字段是 int 类型,而写错的这个字段是 varchar 类型,难道不应该报错吗?怎么还能正常执行,并且还有预期外的查询结果?

难道是 MySQL 有 bug 了,必须要研究一下了。

复现当时的情景

假设有两张表,这两张表的结构和数据是下面这样的。

第一张 user表。

CREATE TABLE `user` (
 `id` int(11NOT NULL AUTO_INCREMENT,
 `name` varchar(50COLLATE utf8_bin DEFAULT NULL,
 `age` int(3DEFAULT NULL,
 `create_time` datetime DEFAULT NULL,
 `update_time` datetime DEFAULT NULL,
 PRIMARY KEY (`id`)
ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;


INSERT INTO `user` VALUES (1'张三'28'2022-09-06 07:40:56''2022-09-06 07:40:59');


第二张 order

CREATE TABLE `order` (
 `id` int(11NOT NULL AUTO_INCREMENT,
 `user_id` int(11DEFAULT NULL,
 `order_code` varchar(64COLLATE utf8_bin DEFAULT NULL,
 `money` decimal(20,0DEFAULT NULL,
 `title` varchar(255COLLATE utf8_bin DEFAULT NULL,
 `create_time` datetime DEFAULT NULL,
 `update_time` datetime DEFAULT NULL,
 PRIMARY KEY (`id`)
ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;


INSERT INTO `order` VALUES (12'1d90530e-6ada-47c1-b2fa-adba4545aabd'100'xxx购买两件商品''2022-09-06 07:42:25''2022-09-06 07:42:27');


目的是查看所有用户的 order 记录,假设数据量比较少,可以直接查,不考虑性能问题。

本来的 SQL 语句应该是这样子的,查询 order表中用户iduser_iduser表的记录。

select o.* from `user` u 
left JOIN `order` o on u.id = o.user_id;

但是呢,因为手抖,将 on 后面的条件写成了 u.id = o.order_code,完全关联错误,这两个字段完全没有联系,而且u.id是 int 类型,o.order_codevarchar类型。

select o.* from `user` u 
left JOIN `order` o on u.id = o.order_code;

这样的话, 当我们执行这条语句的时候,会不会查出数据来呢?

我的第一感觉是,不仅不会查出数据,而且还会报错,因为连接的这两个字段类型都不一样,值更不一样。

结果却被啪啪打脸,不仅没有报错,而且还查出了数据。


可以把这个问题简化一下,简化成下面这条语句,同样也会出现问题。

select * from `order` where order_code = 1;


明明这条记录的 order_code 字段的值是 1d90530e-6ada-47c1-b2fa-adba4545aabd,怎么用 order_code=1的条件就把它给查出来了。

根源所在

相信有的同学已经猜出来了,这里是 MySQL 进行了隐式转换,由于查询条件后面跟的查询值是整型的,所以 MySQL 将 order_code字段进行了字符串到整数类型的转换,而转换后的结果正好是 1

通过 cast函数转换验证一下结果。

select cast('1d90530e-6ada-47c1-b2fa-adba4545aabd' as unsigned);


再用两条 SQL 看一下字符串到整数类型转换的规则。

select cast('223kkk' as unsigned);
select cast('k223kkk' as unsigned);


223kkk转换后的结果是 223,而k223kkk转换后的结果是0。总结一下,转换的规则是:

1、从字符串的左侧开始向右转换,遇到非数字就停止;

2、如果第一个就是非数字,最后的结果就是0;

隐式转换的规则

当操作符与不同类型的操作数一起使用的时候,就会发生隐式转换。

例如算数运算符的前后是不同类型时,会将非数字类型转换为数字,比如 '5a'+2,就会将5a转换为数字类型,然后和2相加,最后的结果就是 7 。


再比如 concat函数是连接两个字符串的,当此函数的参数出现非字符串类型时,就会将其转换为字符串,例如concat(88,'就是发'),最后的结果就是 88就是发


MySQL 官方文档有以下几条关于隐式转换的规则:

1、两个参数至少有一个是 NULL 时,比较的结果也是 NULL,例外是使用 <=> 对两个 NULL 做比较时会返回 1,这两种情况都不需要做类型转换;

也就是两个参数中如果只有一个是NULL,则不管怎么比较结果都是 NULL,而两个 NULL 的值不管是判断大于、小于或等于,其结果都是1。

2、两个参数都是字符串,会按照字符串来比较,不做类型转换;

3、两个参数都是整数,按照整数来比较,不做类型转换;

4、十六进制的值和非数字做比较时,会被当做二进制字符串;

例如下面这条语句,查询 user 表中name字段是 0x61 的记录,0x是16进制写法,其对应的字符串是英文的 'a',也就是它对应的 ASCII 码。

select * from user where name = 0x61;

所以,上面这条语句其实等同于下面这条

select * from user where name = 'a';

可以用 select 0x61;验证一下。

5、有一个参数是 TIMESTAMP 或 DATETIME,并且另外一个参数是常量,常量会被转换为 时间戳;

例如下面这两条SQL,都是将条件后面的值转换为时间戳再比较了,只不过


6、有一个参数是 decimal 类型,如果另外一个参数是 decimal 或者整数,会将整数转换为 decimal 后进行比较,如果另外一个参数是浮点数(一般默认是 double),则会把 decimal 转换为浮点数进行比较;

在不同的数值类型之间,总是会向精度要求更高的那一个类型转换,但是有一点要注意,在MySQL 中浮点数的精度只有53 bit,超过53bit之后的话,如果后面1位是1就进位,如果是0就直接舍弃。所以超大浮点数在比较的时候其实只是取的近似值。

7、所有其他情况下,两个参数都会被转换为浮点数再进行比较;

如果不符合上面6点规则,则统一转成浮点数再进行运算

避免进行隐式转换

我们在平时的开发过程中,尽量要避免隐式转换,因为一旦发生隐式转换除了会降低性能外, 还有很大可能会出现不期望的结果,就像我最开始遇到的那个问题一样。

之所以性能会降低,还有一个原因就是让本来有的索引失效。

select * from `order` where order_code = 1;

order_code 是 varchar 类型,假设我已经在 order_code 上建立了索引,如果是用“=”做查询条件的话,应该直接命中索引才对,查询速度会很快。但是,当查询条件后面的值类型不是 varchar,而是数值类型的话,MySQL 首先要对 order_code 字段做类型转换,转换为数值类型,这时候,之前建的索引也就不会命中,只能走全表扫描,查询性能指数级下降,搞不好,数据库直接查崩了。

作者:古时的风筝
来源:juejin.cn/post/7161991470268825631

收起阅读 »

马斯克裁员过猛,推特又恳求数十名员工回公司上班

  马斯克在斥资440亿美元收购推特后,为了改变该平台持续亏损的现状便于11月4日开始了“大裁员计划”,裁掉了近3700人,约一半的员工。然而,据知情人士透露,推特发现自己可能裁错了一些人,目前他们正在联系数十名被裁的员工,请他们重返工作岗位。  彭博社报道截...
继续阅读 »

  马斯克在斥资440亿美元收购推特后,为了改变该平台持续亏损的现状便于11月4日开始了“大裁员计划”,裁掉了近3700人,约一半的员工。然而,据知情人士透露,推特发现自己可能裁错了一些人,目前他们正在联系数十名被裁的员工,请他们重返工作岗位。


  彭博社报道截图

  据彭博社11月7日报道,据两名知情人士透露,这些被要求返回公司上班的员工是被错误地开除了。这些员工工作领域和经验其实是“推特”仍然需要的,但管理层在裁掉了他们后才意识到这一点。

  彭博社称,邀请部分员工返岗的情况表明,推特的裁员显得仓促而混乱。

  目前,“推特”方面尚未就彭博社披露的这一情况做出回应。

  10月27日,马斯克完成了价值440亿美元的推特收购交易,并裁掉了推特领导层的大部分人员,包括首席执行官(CEO)、首席财务官(CFO)和两名高级法务人员,马斯克亲自担任CEO。11月4日,推特通过电子邮件宣布裁员近3700人,以此来削减成本。许多员工在访问电子邮件和Slack等公司系统遭拒后,才意识到失去了工作。

  推特安全与诚信主管约尔·罗斯(Yoel Roth)稍早前在推特上表示,该公司解雇了50%的员工,其中包括信任与安全团队成员。同时,负责沟通、内容策划、机器学习伦理的团队,以及部分产品和工程团队人员都在被解雇之列。

  在宣布大裁员当天(11月4日),马斯克在推特上写道:“关于推特裁员,不幸的是,当公司每天亏损超过400万美元时,我们别无选择。”

  今年二季度,推特实现营收11.8亿美元,同比下降1%。季度净亏损2.7亿美元,上年同期净利润为0.66亿美元。广告业务营收为10.8亿美元,同比增长2%;订阅和其他收入总计1.01亿美元,同比下降27%。

  据知情人士透露,推特裁员后仍有近3700名员工。马斯克正在敦促留在公司的员工迅速推出新功能。在某些情况下,员工甚至会在办公室留宿,以便在最后期限前完成任务。

  不过,在推特正式宣布大裁员的前一天(11月3日),Twitter员工已向旧金山联邦法院提出一起集体诉讼。员工们表示,Twitter在没有提前通知的情况下进行裁员,违反了联邦和加州法律。根据美国联邦《工人调整和再培训通知法》(WARN)的要求,大型公司在进行大规模裁员之前,要至少提前60天发出解雇通知。

  11月5日,联合国人权事务高级专员沃尔克·蒂尔克(Volker Türk)发布了一封写给推特的新老板马斯克的公开信,敦促在马斯克的带领下,“人权成为推特管理的核心”。同一天(11月5日),推特联合创始人、前首席执行官杰克·多西针对推特裁员事件在社交媒体发文称,他对所有员工所面临的艰难处境负有责任,“我把公司的规模扩张得太快了,我为此道歉”。

来源:观察者网

收起阅读 »

假ArrayList导致的线上事故......

线上事故回顾 晚饭时,当我正沉迷于排骨煲肉质鲜嫩,汤汁浓郁时,产研沟通群内发出一条消息,显示用户存在可用劵,但进去劵列表却什么也没有,并附含了一个视频。于是我一边吃了排骨,一边查看消息点开了视频,en~,视频跟描述一样。但没有系统告警,用户界面也没有明显的报错...
继续阅读 »

线上事故回顾


晚饭时,当我正沉迷于排骨煲肉质鲜嫩,汤汁浓郁时,产研沟通群内发出一条消息,显示用户存在可用劵,但进去劵列表却什么也没有,并附含了一个视频。于是我一边吃了排骨,一边查看消息点开了视频,en~,视频跟描述一样。但没有系统告警,用户界面也没有明显的报错提示,怀疑是小部分特殊情况导致的,查看消息后几秒,我直接被@来处理问题,擦,只好把外卖盒重新盖好,先去处理问题。




处理经过


通过群内产品发的用户邮箱查到了用户id,再根据接口的相关日志结合uid在日志平台进行关联查询,查到日志后,再拿到traceId进行链路查询,果不其然,发现了异常日志,如下部分日志所示


java.lang.UnsupportedOperationException: null
at java.util.AbstractList.add(AbstractList.java:148) ~[na:1.8.0_151]
at java.util.AbstractList.add(AbstractList.java:108) ~[na:1.8.0_151]

乍一看,这不是空指针嘛,so easy啊


image-20221029144806796


仔细一瞧,这UnsupportedOperationException是个什么玩意


于是,根据日志找到代码中报错的那一行,下面给大家简单模拟下


@Slf4j
@SpringBootTest
public class Demo {

public void test(Context context) {
context.getList().add("Code皮皮虾");
}

}

@Data
class Context {

private List<String> list;

}

基本操作就是拿到上下文中的List,然后再add一个元素


image-20221029145443989


讲道理,add操作是不会有问题的,有问题的还得是List,追根溯源,让我康康这个List是怎么来的


于是我一顿狂点,来到了set这个list的位置


@Slf4j
@SpringBootTest
public class Demo {

public void test(Context context) {
context.setList(Arrays.asList("Code皮皮虾"));
}

}

@Data
class Context {

private List<String> list;

}

context.setList(Arrays.asList("Code皮皮虾")); 这行看起来好像没问题啊


Arrays.asList(T... a)我们平时也会用,传入一个数组,返回出一个List没啥问题呀


image-20221029151336521


那我再试试add方法


image-20221029151419519


擦,问题复现了,还真是Arrays.asList(T... a)生成的List的add方法报错


由于线上存在问题,则先修改为以下代码上线,也就是修改为我们平时正常的写法


image-20221029151638593


上线后,观察了下日志,群里回复已解决问题,也让用户重试,发现没问题,自此问题解决。


接下来,咱们来看看为啥Arrays.asList(T... a)add方法会报错




追根溯源


进入asList方法,发现底层new了一个ArrayList,并将数组传入作为List的元素


@SafeVarargs
@SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}

emm,看起来很简单啊,没问题啊,咋会报错呢


别着急,咱们在点开这个ArrayList瞅瞅


private static class ArrayList<E> extends AbstractList<E>
implements RandomAccess, java.io.Serializable
{
private static final long serialVersionUID = -2764017481108945198L;
private final E[] a;

ArrayList(E[] array) {
a = Objects.requireNonNull(array);
}
// ... 省略
}

擦,这ArrayListArrays类的一个静态内部类,不是我们经常用的java.util.ArrayList


image-20221029152405421


真是离谱他妈给离谱开门,离谱大家了,还是我源码看得太少了,呜呜呜~


继续看,这个静态内部类ArrayList继承了AbstractList,而且默认是没有实现add方法的


image-20221029152630097


也就是说调用add方法会直接调用父类,也就是AbstractListadd方法,源码点开一看,真相大白了


AbstractListadd方法直接抛出UnsupportedOperationException异常,跟线上报错一模一样!!!


public boolean add(E e) {
add(size(), e);
return true;
}

public void add(int index, E element) {
throw new UnsupportedOperationException();
}

至此,排查结束,继续吃饭去排骨去咯~~~


image-20221029153737494




小彩蛋


在使用Arrays.asList(T... a)方法时,如果只是单个元素的话,Idea会提示我们更建议Collections.singletonList


image-20221029153915201


别用!!!真的别用!!!


因为Collections.singletonList底层跟Arrays.asList(T... a)差不多


SingletonList也是继承了AbstractList的一个内部类,调用add一样会报UnsupportedOperationException异常


public static <T> List<T> singletonList(T o) {
return new SingletonList<>(o);
}

private static class SingletonList<E>
extends AbstractList<E>
implements RandomAccess, Serializable {

private static final long serialVersionUID = 3093736618740652951L;

private final E element;

SingletonList(E obj) {
element = obj;
}
}



结尾


当然咯,也不是禁止使用Collections.singletonListArrays.asList(T... a),只是我们在使用的时候一定要区分一下场景,如果创建的是一个不会再添加元素的List,那么则可以使用


但我们平时不想写那么麻烦,想要在创建的时候就把元素塞到List中,那咋办呢?


我们其实能使用google的工具类


<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>

如下写法


@Slf4j
@SpringBootTest
public class Demo {

public static void main(String[] args) {
List<String> strings = Lists.newArrayList("Code皮皮虾", "哈哈哈");
strings.add("憨憨熊");
System.out.println(strings);
}
}

其内部已经为我们封装好了,拿来即用即可,哈哈


@SafeVarargs
@CanIgnoreReturnValue // TODO(kak): Remove this
@GwtCompatible(serializable = true)
public static <E> ArrayList<E> newArrayList(E... elements) {
checkNotNull(elements); // for GWT
// Avoid integer overflow when a large array is passed in
int capacity = computeArrayListCapacity(elements.length);
ArrayList<E> list = new ArrayList<>(capacity);
Collections.addAll(list, elements);
return list;
}

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

韩国程序员面试考什么?

大家好,我是老三,在G站闲逛的时候,从每日热门上,看到一个韩国的技术面试项目,感觉有点好奇,忍不住点进去看看。 韩国的面试都考什么?有没有国内的卷呢? 可以看到,有8.k star,2.2k forks,在韩国应该算是顶流的开源项目了。 再看看贡献者,嗯,...
继续阅读 »

大家好,我是老三,在G站闲逛的时候,从每日热门上,看到一个韩国的技术面试项目,感觉有点好奇,忍不住点进去看看。


韩国的面试都考什么?有没有国内的卷呢?
瘦巴巴的老爷们


可以看到,有8.k star,2.2k forks,在韩国应该算是顶流的开源项目了。


star


再看看贡献者,嗯,明显看出来是韩国人。
贡献者


整体看一下内容。


第一大部分是计算机科学,有这些小类:



  • 计算机组成


计算机组成原理



  • 数据结构


数据结构




  • 数据库
    数据库




  • 网络




网络



  • 操作系统


操作系统


软件工程


先不说内容,韩文看起来也够呛,但是基础这一块,内容结构还是比较完整的。


第二大部分是算法:
算法


十大排序、二分查找、DFS\BFS…… 大概也是那些东西。


第三大部分是设计模式,内容不多。
设计模式


第四大部分是面试题:
面试题


终于到了比较感兴趣的部分了,点进语言部分,进去看看韩国人面试都问什么,随便抽几道看看:
面试题



  • Vector和ArrayList的区别?

  • 值传递 vs 引用传递?

  • 进程和线程的区别?

  • 死锁的四个条件是什么?

  • 页面置换算法?

  • 数据库是无状态的吗?

  • oracle和mysql的区别?

  • 说说数据库的索引?

  • OSI7层体系结构?

  • http和https的区别是?

  • DI(Dependency Injection)?

  • AOP(Aspect Oriented Programming)?

  • ……


定睛一看,有种熟悉的感觉,天下八股都一样么?


第五大部分是编程语言:
编程语言


包含了C、C++、Java、JavaScript、Python。


稍微看看Java部分,也很熟悉的感觉:



  • Java编译过程

  • 值传递 vs 引用传递

  • String & StringBuffer & StringBuilder

  • Thread使用


还有其它的Web、Linux、新技术部分就懒得再一一列出了,大家可以自己去看。


这个仓库,让我来评价评价,好,但不是特别好,为什么呢?大家可以看看国内类似的知识仓库,比如JavaGuide,那家伙,内容丰富的!和国内的相比,这个仓库还是单薄了一些——当然也可能是韩国的IT环境没那么卷,这些就够用了。


再扯点有点没的,我对韩国的IT稍微有一点点了解,通过Kakao。之前对接过Kakao的支付——Kakao是什么呢?大家可以理解为韩国的微信就行了,怎么说呢,有点离谱,他们的支付每天大概九点多到十点多要停服维护,你能想象微信支付每天有一个小时不可用吗?


也有同事对接过Kakao的登录,很简单的一个Oauth2,预估两三天搞定,结果也是各种状况,搞了差不多两周。


可能韩国的IT环境真的没有那么卷吧!


有没有对韩国IT行业、IT面试有更多了解的读者朋友呢?欢迎和老三交流。


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

封装一个有趣的 Loading 组件

组件定义 loading组件共定义4个入口参数: 前景色:绘制图形的前景色; 背景色:绘制图形的背景色; 图形尺寸:绘制图形的尺寸; 加载文字:可选,如果有文字就显示,没有就不显示。 得到的Loading组件类如下所示: class LoadingAnim...
继续阅读 »



组件定义


loading组件共定义4个入口参数:



  • 前景色:绘制图形的前景色;

  • 背景色:绘制图形的背景色;

  • 图形尺寸:绘制图形的尺寸;

  • 加载文字:可选,如果有文字就显示,没有就不显示。


得到的Loading组件类如下所示:


class LoadingAnimations extends StatefulWidget {
final Color bgColor;
final Color foregroundColor;
String? loadingText;
final double size;
LoadingAnimations(
{required this.foregroundColor,
required this.bgColor,
this.loadingText,
this.size = 100.0,
Key? key})
: super(key: key);

@override
_LoadingAnimationsState createState() => _LoadingAnimationsState();
}

圆形Loading


我们先来实现一个圆形的loading,效果如下所示。
circle_loading.gif
这里绘制了两组沿着一个大圆运动的轴对称的实心圆,半径依次减小,圆心间距随着动画时间逐步拉大。实际上实现的核心还是基于PathPathMetrics。具体实现代码如下:


_drawCircleLoadingAnimaion(
Canvas canvas, Size size, Offset center, Paint paint) {
final radius = boxSize / 2;
final ballCount = 6;
final ballRadius = boxSize / 15;

var circlePath = Path()
..addOval(Rect.fromCircle(center: center, radius: radius));

var circleMetrics = circlePath.computeMetrics();
for (var pathMetric in circleMetrics) {
for (var i = 0; i < ballCount; ++i) {
var lengthRatio = animationValue * (1 - i / ballCount);
var tangent =
pathMetric.getTangentForOffset(pathMetric.length * lengthRatio);

var ballPosition = tangent!.position;
canvas.drawCircle(ballPosition, ballRadius / (1 + i), paint);
canvas.drawCircle(
Offset(size.width - tangent.position.dx,
size.height - tangent.position.dy),
ballRadius / (1 + i),
paint);
}
}
}

其中路径比例为lengthRatio,通过animationValue乘以一个系数使得实心圆的间距越来越大 ,同时通过Offset(size.width - tangent.position.dx, size.height - tangent.position.dy)绘制了一组对对称的实心圆,这样整体就有一个圆形的效果了,动起来也会更有趣一点。


椭圆运动Loading


椭圆和圆形没什么区别,这里我们搞个渐变的效果看看,利用之前介绍过的Paintshader可以实现渐变色绘制效果。


oval_loading.gif


实现代码如下所示。


final ballCount = 6;
final ballRadius = boxSize / 15;

var ovalPath = Path()
..addOval(Rect.fromCenter(
center: center, width: boxSize, height: boxSize / 1.5));
paint.shader = LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [this.foregroundColor, this.bgColor],
).createShader(Offset.zero & size);
var ovalMetrics = ovalPath.computeMetrics();
for (var pathMetric in ovalMetrics) {
for (var i = 0; i < ballCount; ++i) {
var lengthRatio = animationValue * (1 - i / ballCount);
var tangent =
pathMetric.getTangentForOffset(pathMetric.length * lengthRatio);

var ballPosition = tangent!.position;
canvas.drawCircle(ballPosition, ballRadius / (1 + i), paint);
canvas.drawCircle(
Offset(size.width - tangent.position.dx,
size.height - tangent.position.dy),
ballRadius / (1 + i),
paint);
}
}

当然,如果渐变色的颜色更丰富一点会更有趣些。


colorful_loading.gif


贝塞尔曲线Loading


通过贝塞尔曲线构建一条Path,让一组圆形沿着贝塞尔曲线运动的Loading效果也很有趣。


bezier_loading.gif


原理和圆形的一样,首先是构建贝塞尔曲线Path,代码如下。


var bezierPath = Path()
..moveTo(size.width / 2 - boxSize / 2, center.dy)
..quadraticBezierTo(size.width / 2 - boxSize / 4, center.dy - boxSize / 4,
size.width / 2, center.dy)
..quadraticBezierTo(size.width / 2 + boxSize / 4, center.dy + boxSize / 4,
size.width / 2 + boxSize / 2, center.dy)
..quadraticBezierTo(size.width / 2 + boxSize / 4, center.dy - boxSize / 4,
size.width / 2, center.dy)
..quadraticBezierTo(size.width / 2 - boxSize / 4, center.dy + boxSize / 4,
size.width / 2 - boxSize / 2, center.dy);

这里实际是构建了两条贝塞尔曲线,先从左边到右边,然后再折回来。之后就是运动的实心圆了,这个只是数量上多了,ballCount30,这样效果看着就有一种拖影的效果。


var ovalMetrics = bezierPath.computeMetrics();
for (var pathMetric in ovalMetrics) {
for (var i = 0; i < ballCount; ++i) {
var lengthRatio = animationValue * (1 - i / ballCount);
var tangent =
pathMetric.getTangentForOffset(pathMetric.length * lengthRatio);

var ballPosition = tangent!.position;
canvas.drawCircle(ballPosition, ballRadius / (1 + i), paint);
canvas.drawCircle(
Offset(size.width - tangent.position.dx,
size.height - tangent.position.dy),
ballRadius / (1 + i),
paint);
}
}

这里还可以改变运动方向,实现一些其他的效果,例如下面的效果,第二组圆球的绘制位置实际上是第一组圆球的x、y坐标的互换。


bezier_loading_transform.gif


var lengthRatio = animationValue * (1 - i / ballCount);
var tangent =
pathMetric.getTangentForOffset(pathMetric.length * lengthRatio);

var ballPosition = tangent!.position;
canvas.drawCircle(ballPosition, ballRadius / (1 + i), paint);
canvas.drawCircle(Offset(tangent.position.dy, tangent.position.dx),
ballRadius / (1 + i), paint);

组件使用


我们来看如何使用我们定义的这个组件,使用代码如下,我们用Future延迟模拟了一个加载效果,在加载过程中使用loading指示加载过程,加载完成后显示图片。


class _LoadingDemoState extends State<LoadingDemo> {
var loaded = false;

@override
void initState() {
super.initState();
Future.delayed(Duration(seconds: 5), () {
setState(() {
loaded = true;
});
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Text('Loading 使用'),
),
body: Center(
child: loaded
? Image.asset(
'images/beauty.jpeg',
width: 100.0,
)
: LoadingAnimations(
foregroundColor: Colors.blue,
bgColor: Colors.white,
size: 100.0,
),
),
);
}

最终运行的效果如下,源码已提交至:绘图相关源码,文件名为loading_animations.dart


loading_usage.gif


总结


本篇介绍了Loading组件的封装方法,核心要点还是利用Path和动画控制绘制元素的运动轨迹来实现更有趣的效果。在实际应用过程中,也可以根据交互设计的需要,做一些其他有趣的加载动效,提高等待过程的趣味性。


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

忙里偷闲IdleHandler

在Android中,Handler是一个使用的非常频繁的东西,输入事件机制和系统状态,都通过Handler来进行流转,而在Handler中,有一个很少被人提起但是却很有用的东西,那就是IdleHandler,它的源码如下。 /** * Callback in...
继续阅读 »

在Android中,Handler是一个使用的非常频繁的东西,输入事件机制和系统状态,都通过Handler来进行流转,而在Handler中,有一个很少被人提起但是却很有用的东西,那就是IdleHandler,它的源码如下。


/**
* Callback interface for discovering when a thread is going to block
* waiting for more messages.
*/
public static interface IdleHandler {
/**
* Called when the message queue has run out of messages and will now
* wait for more. Return true to keep your idle handler active, false
* to have it removed. This may be called if there are still messages
* pending in the queue, but they are all scheduled to be dispatched
* after the current time.
*/
boolean queueIdle();
}

从注释我们就能发现,这是一个IdleHandler的静态接口,可以在消息队列没有消息时或是队列中的消息还没有到执行时间时才会执行的一个回调。


这个功能在某些重要但不紧急的场景下就非常有用了,比如我们要在主页上做一些处理,但是又不想影响原有的初始化逻辑,避免卡顿,那么我们就需要等系统闲下来的时候再来执行我们的操作,这个时候,我们就可以通过IdleHandler来进行回调。


它的使用也非常简单,代码示例如下。


Looper.myQueue().addIdleHandler {
// Do something
false
}

在Handler的消息循环中,一旦队列里面没有需要处理的消息,该接口就会回调,也就是Handler空闲的时候。


这个接口有返回值,代表是否需要持续执行,如果返回true,那么一旦Handler空闲,就会执行IdleHandler中的回调,而如果返回false,那么就只会执行一次。



当返回true时,可以通过removeIdleHandler的方式来移除循环的处理,如果是false,那么在处理完后,它自己会移除。



综上,IdleHandler的使用主要有下面这些场景。



  • 低优先级的任务处理:替换之前为了不在初始化的时候影响性能而使用的Handler.postDelayed方法,通过IdleHandler来自动获取空闲的时机。

  • Idle时循环处理任务:通过控制返回值,在系统空闲时,不断重复某个操作。


但是要注意的是,如果Handler过于繁忙,那么IdleHandler的执行时机是有可能被延迟很久的,所以,要注意一些比较重要的处理逻辑的处理时机。


在很多第三方库里面,都有IdleHandler的使用,例如LeakCanary,它对内存的dump分析过程,就是在IdleHandler中处理的,从而避免对主线程的影响。


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

项目维护几年了,为啥还这么卡?

浅谈 前段时间有个客户问我,为啥你们项目都搞了好几年了,为啥线上还会经常反馈卡顿,呃呃呃。。 于是根据自己的理解以及网上大佬们的思路总结了一篇关于卡顿优化这块的文章。 卡顿问题是一个老生常谈的话题了,一个App的好坏,卡顿也许会占一半,它直接决定了用户的留存...
继续阅读 »

浅谈


前段时间有个客户问我,为啥你们项目都搞了好几年了,为啥线上还会经常反馈卡顿,呃呃呃。。


无言以对.webp


于是根据自己的理解以及网上大佬们的思路总结了一篇关于卡顿优化这块的文章。


卡顿问题是一个老生常谈的话题了,一个App的好坏,卡顿也许会占一半,它直接决定了用户的留存问题,各大app排行版上,那些知名度较高,但是排行较低的,可能就要思考思考是不是和你app本身有关系了。


卡顿一直是性能优化中相对重要的一个点,因为其涉及了UI绘制垃圾回收(GC)、线程调度以及BinderCPU,GPU方面等JVM以及FrameWork相关知识


如果能做好卡顿优化,那么也就间接证明你对Android FrameWork的理解之深。


下面我们就来讲解下卡顿方面的知识。


什么是卡顿:


对用户来讲就是界面不流畅,滞顿。
场景如下



  • 1.视频加载慢,画面卡顿,卡死,黑屏

  • 2.声音卡顿,音画不同步。

  • 3.动画帧卡顿,交互响应慢

  • 4.滑动不跟手,列表自动更新,滚动不流畅

  • 5.网络响应慢,数据和画面展示慢、

  • 6.过渡动画生硬。

  • 7.界面不可交互,卡死,等等现象。


卡顿是如何发生的


卡顿产生的原因一般都比较复杂,如CPU内存大小,IO操作,锁操作,低效的算法等都会引起卡顿


站在开发的角度看:
通常我们讲,屏幕刷新率是60fps,需要在16ms内完成所有的工作才不会造成卡顿


为什么是16ms,不是17,18呢?


下面我们先来理清在UI绘制中的几个概念:


SurfaceFlinger:


SurfaceFlinger作用是接受多个来源的图形显示数据Surface,合成后发送到显示设备,比如我们的主界面中:可能会有statusBar,侧滑菜单,主界面,这些View都是独立Surface渲染和更新,最后提交给SF后,SF根据Zorder,透明度,大小,位置等参数,合成为一个数据buffer,传递HWComposer或者OpenGL处理,最终给显示器


SurfaceFlinger.image


在显示过程中使用到了bufferqueue,surfaceflinger作为consumer方,比如windowmanager管理的surface作为生产方产生页面,交由surfaceflinger进行合成。
sf2.image


VSYNC


Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,VSYNC是一种在PC上很早就有应用,可以理解为一种定时中断技术。


tearing 问题:


早期的 Android 是没有 vsync 机制的,CPU 和 GPU 的配合也比较混乱,这也造成著名的 tearing 问题,即 CPU/GPU 直接更新正在显示的屏幕 buffer 造成画面撕裂
后续 Android 引入了双缓冲机制,但是 buffer 的切换也需要一个比较合适的时机,也就是屏幕扫描完上一帧后的时机,这也就是引入 vsync 的原因。


早先一般的屏幕刷新率是 60fps,所以每个 vsync 信号的间隔也是 16ms,不过随着技术的更迭以及厂商对于流畅性的追求,越来越多 90fps 和 120fps 的手机面世,相对应的间隔也就变成了 11ms 和 8ms。


VSYNC信号种类:



  • 1.屏幕产生的硬件VSYNC:硬件VSYNC是一种脉冲信号,起到开关和触发某种操作的作用。

  • 2.由SurfaceFlinger将其转成的软件VSYNC信号,经由Binder传递给Choreographer


Choreographer:


编舞者用于注册VSYNC信号并接收VSYNC信号回调,当内部接收到这个信号时最终会调用到doFrame进行帧的绘制操作


Choreographer在系统中流程


Choreographer.png


如何通过Choreographer计算掉帧情况:原理就是:



通过给Choreographer设置FrameCallback,在每次绘制前后看时间差是16.6ms的多少倍,即为前后掉帧率。



使用方式如下:


//Application.java
public void onCreate() {
super.onCreate();
//在Application中使用postFrameCallback
Choreographer.getInstance().postFrameCallback(new FPSFrameCallback(System.nanoTime()));
}
public class FPSFrameCallback implements Choreographer.FrameCallback {

private static final String TAG = "FPS_TEST";
private long mLastFrameTimeNanos = 0;
private long mFrameIntervalNanos;

public FPSFrameCallback(long lastFrameTimeNanos) {
mLastFrameTimeNanos = lastFrameTimeNanos;
mFrameIntervalNanos = (long)(1000000000 / 60.0);
}

@Override
public void doFrame(long frameTimeNanos) {

//初始化时间
if (mLastFrameTimeNanos == 0) {
mLastFrameTimeNanos = frameTimeNanos;
}
final long jitterNanos = frameTimeNanos - mLastFrameTimeNanos;
if (jitterNanos >= mFrameIntervalNanos) {
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
if(skippedFrames>30){
//丢帧30以上打印日志
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
}
}
mLastFrameTimeNanos=frameTimeNanos;
//注册下一帧回调
Choreographer.getInstance().postFrameCallback(this);
}
}

UI绘制全路径分析:


有了前面几个概念,这里我们让SurfaceFlinger结合View的绘制流程用一张图来表达整个绘制流程:
生产者消费2.awebp



  • 生产者:APP方构建Surface的过程。

  • 消费者:SurfaceFlinger


UI绘制全路径分析卡顿原因:


接下来,我们逐个分析,看看都会有哪些原因可能造成卡顿:


1.渲染流程




  • 1.Vsync 调度:这个是起始点,但是调度的过程会经过线程切换以及一些委派的逻辑,有可能造成卡顿,但是一般可能性比较小,我们也基本无法介入;




  • 2.消息调度:主要是 doframe Message 的调度,这就是一个普通的 Handler 调度,如果这个调度被其他的 Message 阻塞产生了时延,会直接导致后续的所有流程不会被触发




  • 3.input 处理:input 是一次 Vsync 调度最先执行的逻辑,主要处理 input 事件。如果有大量的事件堆积或者在事件分发逻辑中加入大量耗时业务逻辑,会造成当前帧的时长被拉大,造成卡顿,可以尝试通过事件采样的方案,减少 event 的处理




  • 4.动画处理:主要是 animator 动画的更新,同理,动画数量过多,或者动画的更新中有比较耗时的逻辑,也会造成当前帧的渲染卡顿。对动画的降帧和降复杂度其实解决的就是这个问题;




  • 5.view 处理:主要是接下来的三大流程,过度绘制、频繁刷新、复杂的视图效果都是此处造成卡顿的主要原因。比如我们平时所说的降低页面层级,主要解决的就是这个问题;




  • 6.measure/layout/draw:view 渲染的三大流程,因为涉及到遍历和高频执行,所以这里涉及到的耗时问题均会被放大,比如我们会降不能在 draw 里面调用耗时函数,不能 new 对象等等;




  • 7.DisplayList 的更新:这里主要是 canvas 和 displaylist 的映射,一般不会存在卡顿问题,反而可能存在映射失败导致的显示问题;




  • 8.OpenGL 指令转换:这里主要是将 canvas 的命令转换为 OpenGL 的指令,一般不存在问题




  • 9.buffer 交换:这里主要指 OpenGL 指令集交换给 GPU,这个一般和指令的复杂度有关




  • 10.GPU 处理:顾名思义,这里是 GPU 对数据的处理,耗时主要和任务量和纹理复杂度有关。这也就是我们降低 GPU 负载有助于降低卡顿的原因;




  • 11.layer 合成:Android P 修改了 Layer 的计算方法 , 把这部分放到了 SurfaceFlinger 主线程去执行, 如果后台 Layer 过多, 就会导致 SurfaceFlinger 在执行 rebuildLayerStacks 的时候耗时 , 导致 SurfaceFlinger 主线程执行时间过长。
    可以选择降低Surface层级来优化卡顿。




Layer过多.image



  • 12.光栅化/Display:这里暂时忽略,底层系统行为;
    Buffer 切换:主要是屏幕的显示,这里 buffer 的数量也会影响帧的整体延迟,不过是系统行为,不能干预。


2.系统负载



  • 内存:内存的吃紧会直接导致 GC 的增加甚至 ANR,是造成卡顿的一个不可忽视的因素;

  • CPU:CPU 对卡顿的影响主要在于线程调度慢、任务执行的慢和资源竞争,比如


    • 1.降频会直接导致应用卡顿




    • 2.后台活动进程太多导致系统繁忙,cpu \ io \ memory 等资源都会被占用, 这时候很容易出现卡顿问题 ,这种情况比较常见,可以使用dumpsys cpuinfo查看当前设备的cpu使用情况:




    • 3.主线程调度不到 , 处于 Runnable 状态,这种情况比较少见




    • 4.System 锁:system_server 的 AMS 锁和 WMS 锁 , 在系统异常的情况下 , 会变得非常严重 , 如下图所示 , 许多系统的关键任务都被阻塞 , 等待锁的释放 , 这时候如果有 App 发来的 Binder 请求带锁 , 那么也会进入等待状态 , 这时候 App 就会产生性能问题 ; 如果此时做 Window 动画 , 那么 system_server 的这些锁也会导致窗口动画卡顿






锁.image



  • GPU:GPU 的影响见渲染流程,但是其实还会间接影响到功耗和发热;

  • 功耗/发热:功耗和发热一般是不分家的,高功耗会引起高发热,进而会引起系统保护,比如降频、热缓解等,间接的导致卡顿


如何监控卡顿


线下监控:


我们知道卡顿问题的原因错综复杂,但最终都可以反馈到CPU使用率上来


1.使用dumpsys cpuinfo命令


这个命令可以获取当时设备cpu使用情况,我们可以在线下通过重度使用应用来检测可能存在的卡顿点


A8S:/ $ dumpsys cpuinfo
Load: 1.12 / 1.12 / 1.09
CPU usage from 484321ms to 184247ms ago (2022-11-02 14:48:30.793 to 2022-11-02 1
4:53:30.866):
2% 1053/scanserver: 0.2% user + 1.7% kernel
0.6% 934/system_server: 0.4% user + 0.1% kernel / faults: 563 minor
0.4% 564/signserver: 0% user + 0.4% kernel
0.2% 256/ueventd: 0.1% user + 0% kernel / faults: 320 minor
0.2% 474/surfaceflinger: 0.1% user + 0.1% kernel
0.1% 576/vendor.sprd.hardware.gnss@2.0-service: 0.1% user + 0% kernel / faults
: 54 minor
0.1% 286/logd: 0% user + 0% kernel / faults: 10 minor
0.1% 2821/com.allinpay.appstore: 0.1% user + 0% kernel / faults: 1312 minor
0.1% 447/android.hardware.health@2.0-service: 0% user + 0% kernel / faults: 11
75 minor
0% 1855/com.smartpos.dataacqservice: 0% user + 0% kernel / faults: 755 minor
0% 2875/com.allinpay.appstore:pushcore: 0% user + 0% kernel / faults: 744 mino
r
0% 1191/com.android.systemui: 0% user + 0% kernel / faults: 70 minor
0% 1774/com.android.nfc: 0% user + 0% kernel
0% 172/kworker/1:2: 0% user + 0% kernel
0% 145/irq/24-70900000: 0% user + 0% kernel
0% 575/thermald: 0% user + 0% kernel / faults: 300 minor
...

2.CPU Profiler


这个工具是AS自带的CPU性能检测工具,可以在PC上实时查看我们CPU使用情况。
AS提供了四种Profiling Model配置:



  • 1.Sample Java Methods:在应用程序基于Java的代码执行过程中,频繁捕获应用程序的调用堆栈
    获取有关应用程序基于Java的代码执行的时间和资源使用情况信息。

  • 2.Trace java methods:在运行时对应用程序进行检测,以在每个方法调用的开始和结束时记录时间戳。收集时间戳并进行比较以生成方法跟踪数据,包括时序信息和CPU使用率。


请注意与检测每种方法相关的开销会影响运行时性能,并可能影响性能分析数据。对于生命周期相对较短的方法,这一点甚至更为明显。此外,如果您的应用在短时间内执行大量方法,则探查器可能会很快超过其文件大小限制,并且可能无法记录任何进一步的跟踪数据。



  • 3.Sample C/C++ Functions:捕获应用程序本机线程的示例跟踪。要使用此配置,您必须将应用程序部署到运行Android 8.0(API级别26)或更高版本的设备。

  • 4.Trace System Calls:捕获细粒度的详细信息,使您可以检查应用程序与系统资源的交互方式
    您可以检查线程状态的确切时间和持续时间,可视化CPU瓶颈在所有内核中的位置,并添加自定义跟踪事件进行分析。在对性能问题进行故障排除时,此类信息可能至关重要。要使用此配置,您必须将应用程序部署到运行Android 7.0(API级别24)或更高版本的设备。


使用方式


Debug.startMethodTracing("");
// 需要检测的代码片段
...
Debug.stopMethodTracing();

优点:**有比较全面的调用栈以及图像化方法时间显示,包含所有线程的情况


缺点:本身也会带来一点的性能开销,可能会带偏优化方向**


火焰图:可以显示当前应用的方法堆栈:


cpuprofiler.png


3.Systrace


Systrace在前面一篇分析启动优化的文章讲解过


这里我们简单来复习下:


Systrace用来记录当前应用的系统以及应用(使用Trace类打点)的各阶段耗时信息包括绘制信息以及CPU信息等


使用方式


Trace.beginSection("MyApp.onCreate_1");
alt(200);
Trace.endSection();

在命令行中:


python systrace.py -t 5 sched gfx view wm am app webview -a "com.chinaebipay.thirdcall" -o D:\trac1.html

记录的方法以及CPU中的耗时情况:


systrace1.png
优点



  • 1.轻量级,开销小,CPU使用率可以直观反映

  • 2.右侧的Alerts能够根据我们应用的问题给出具体的建议,比如说,它会告诉我们App界面的绘制比较慢或者GC比较频繁。


4.StrictModel


StrictModel是Android提供的一种运行时检测机制,用来帮助开发者自动检测代码中不规范的地方。
主要和两部分相关:
1.线程相关
2.虚拟机相关


基础代码:


private void initStrictMode() {
// 1、设置Debug标志位,仅仅在线下环境才使用StrictMode
if (DEV_MODE) {
// 2、设置线程策略
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectCustomSlowCalls() //API等级11,使用StrictMode.noteSlowCode
.detectDiskReads()
.detectDiskWrites()
.detectNetwork() // or .detectAll() for all detectable problems
.penaltyLog() //在Logcat 中打印违规异常信息
// .penaltyDialog() //也可以直接跳出警报dialog
// .penaltyDeath() //或者直接崩溃
.build());
// 3、设置虚拟机策略
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
// 给NewsItem对象的实例数量限制为1
.setClassInstanceLimit(NewsItem.class, 1)
.detectLeakedClosableObjects() //API等级11
.penaltyLog()
.build())
;
}
}


线上监控:


线上需要自动化的卡顿检测方案来定位卡顿,它能记录卡顿发生时的场景。


自动化监控原理


blockCan原理.png



采用拦截消息调度流程,在消息执行前埋点计时,当耗时超过阈值时,则认为是一次卡顿,会进行堆栈抓取和上报工作



首先,我们看下Looper用于执行消息循环的loop()方法,关键代码如下所示:


/**
* Run the message queue in this thread. Be sure to call
* {@link #quit()} to end the loop.
*/

public static void loop() {

...

for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;

// This must be in a local variable, in case a UI event sets the logger
final Printer logging = me.mLogging;
if (logging != null) {
// 1
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}

...

try {
// 2
msg.target.dispatchMessage(msg);
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
} finally {
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}

...

if (logging != null) {
// 3
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}

在Looper的loop()方法中,在其执行每一个消息(注释2处)的前后都由logging进行了一次打印输出。可以看到,在执行消息前是输出的">>>>> Dispatching to ",在执行消息后是输出的"<<<<< Finished to ",它们打印的日志是不一样的,我们就可以由此来判断消息执行的前后时间点。


具体的实现可以归纳为如下步骤



  • 1、首先,我们需要使用Looper.getMainLooper().setMessageLogging()去设置我们自己的Printer实现类去打印输出logging。这样,在每个message执行的之前和之后都会调用我们设置的这个Printer实现类。

  • 2、如果我们匹配到">>>>> Dispatching to "之后,我们就可以执行一行代码:也就是在指定的时间阈值之后,我们在子线程去执行一个任务,这个任务就是去获取当前主线程的堆栈信息以及当前的一些场景信息,比如:内存大小、电脑、网络状态等。

  • 3、如果在指定的阈值之内匹配到了"<<<<< Finished to ",那么说明message就被执行完成了,则表明此时没有产生我们认为的卡顿效果,那我们就可以将这个子线程任务取消掉。


这里我们使用blockcanary来做测试:


BlockCanary


APM是一个非侵入式的性能监控组件,可以通过通知的形式弹出卡顿信息。它的原理就是我们刚刚讲述到的卡顿监控的实现原理。
使用方式



  • 1.导入依赖


implementation 'com.github.markzhai:blockcanary-android:1.5.0'


  • Application的onCreate方法中开启卡顿监控


// 注意在主进程初始化调用
BlockCanary.install(this, new AppBlockCanaryContext()).start();


  • 3.继承BlockCanaryContext类去实现自己的监控配置上下文类


public class AppBlockCanaryContext extends BlockCanaryContext {
...
...
/**
* 指定判定为卡顿的阈值threshold (in millis),
* 你可以根据不同设备的性能去指定不同的阈值
*
* @return threshold in mills
*/

public int provideBlockThreshold() {
return 1000;
}
....
}


  • 4.在Activity的onCreate方法中执行一个耗时操作


try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}


  • 5.结果:


可以看到一个和LeakCanary一样效果的阻塞可视化堆栈图


BlockCan效果.png


那有了BlockCanary的方法耗时监控方式是不是就可以解百愁了呢,呵呵。有那么容易就好了


根据原理:我们拿到的是msg执行前后的时间和堆栈信息,如果msg中有几百上千个方法,就无法确认到底是哪个方法导致的耗时,也有可能是多个方法堆积导致


这就导致我们无法准确定位哪个方法是最耗时的。如图中:堆栈信息是T2的,而发生耗时的方法可能是T1到T2中任何一个方法甚至是堆积导致。


apm原理2过程.image


那如何优化这块


这里我们采用字节跳动给我们提供的一个方案:基于 Sliver trace 的卡顿监控体系


Sliver trace


整体流程图


sliver2.awebp
主要包含两个方面:



  • 检测方案
    在监控卡顿时,首先需要打开 Sliver 的 trace 记录能力,Sliver 采样记录 trace 执行信息,对抓取到的堆栈进行 diff 聚合和缓存。


同时基于我们的需要设置相应的卡顿阈值,以 Message 的执行耗时为衡量。对主线程消息调度流程进行拦截,在消息开始分发执行时埋点,在消息执行结束时计算消息执行耗时,当消息执行耗时超过阈值,则认为产生了一次卡顿。



  • 堆栈聚合策略
    当卡顿发生时,我们需要为此次卡顿准备数据,这部分工作是在端上子线程中完成的,主要是 dump trace 到文件以及过滤聚合要上报的堆栈。分为以下几步:

    • 1.拿到缓存的主线程 trace 信息并 dump 到文件中。

    • 2.然后从文件中读取 trace 信息,按照数据格式,从最近的方法栈向上追溯,找到当前 Message 包含的全部 trace 信息,并将当前 Message 的完整 trace 写入到待上传的 trace 文件中,删除其余 trace 信息。

    • 3.遍历当前 Message trace,按照(Method 执行耗时 > Method 耗时阈值 & Method 耗时为该层堆栈中最耗时)为条件过滤出每一层函数调用堆栈的最长耗时函数,构成最后要上报的堆栈链路,这样特征堆栈中的每一步都是最耗时的,且最底层 Method 为最后的耗时大于阈值的 Method。




之后,将 trace 文件和堆栈一同上报,这样的特征堆栈提取策略保证了堆栈聚合的可靠性和准确性,保证了上报到平台后堆栈的正确合理聚合,同时提供了进一步分析问题的 trace 文件。


可以看到字节给的是一整套监控方案,和前面BlockCanary不同之处就在于,其是定时存储堆栈,缓存,然后使用diff去重的方式,并上传到服务器,可以最大限度的监控到可能发生比较耗时的方法。


开发中哪些习惯会影响卡顿的发生


1.布局太乱,层级太深。



  • 1.1:通过减少冗余或者嵌套布局来降低视图层次结构。比如使用约束布局代替线性布局和相对布局。

  • 1.2:用 ViewStub 替代在启动过程中不需要显示的 UI 控件。

  • 1.3:使用自定义 View 替代复杂的 View 叠加。


2.主线程耗时操作



  • 2.1:主线程中不要直接操作数据库,数据库的操作应该放在数据库线程中完成。

  • 2.2:sharepreference尽量使用apply,少使用commit,可以使用MMKV框架来代替sharepreference。

  • 2.3:网络请求回来的数据解析尽量放在子线程中,不要在主线程中进行复制的数据解析操作。

  • 2.4:不要在activity的onResume和onCreate中进行耗时操作,比如大量的计算等。

  • 2.5:不要在 draw 里面调用耗时函数,不能 new 对象


3.过度绘制


过度绘制是同一个像素点上被多次绘制,减少过度绘制一般减少布局背景叠加等方式,如下图所示右边是过度绘制的图片。


过度绘制.image


4.列表


RecyclerView使用优化,使用DiffUtil和notifyItemDataSetChanged进行局部更新等。


5.对象分配和回收优化


自从Android引入 ART 并且在Android 5.0上成为默认的运行时之后,对象分配和垃圾回收(GC)造成的卡顿已经显著降低了,但是由于对象分配和GC有额外的开销,它依然又可能使线程负载过重。 在一个调用不频繁的地方(比如按钮点击)分配对象是没有问题的,但如果在在一个被频繁调用的紧密的循环里,就需要避免对象分配来降低GC的压力。


减少小对象的频繁分配和回收操作。


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

Android 13这些权限废弃,你的应用受影响了吗?

无论是更改个人头像、分享照片、还是在电子邮件中添加附件,选择和分享媒体文件是用户最常见的操作之一。在听取了 Android 用户反馈之后,我们对应用程序访问媒体文件的方式做了一些改变。Android 13 已被废弃的权限许多用户告诉我们,文件和媒体权限让他们很...
继续阅读 »

无论是更改个人头像、分享照片、还是在电子邮件中添加附件,选择和分享媒体文件是用户最常见的操作之一。在听取了 Android 用户反馈之后,我们对应用程序访问媒体文件的方式做了一些改变。

Android 13 已被废弃的权限

许多用户告诉我们,文件和媒体权限让他们很困扰,因为他们不知道应用程序想要访问哪些文件。

在 Android 13 上废弃了 READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE 权限,用更好的文件访问方式代替这些废弃的 API。

从 Android 10 开始向共享存储中添加文件不需要任何权限。因此,如果你的 App 只在共享存储中添加文件,你可以停止在 Android 10+ 上申请任何权限。

在之前的系统版本中 App 需要申请 READ_EXTERNAL_STORAGE 权限访问设备的文件和媒体,然后选择自己的媒体选择器,这为开发者增加了开发和维护成本,另外 App 依赖于通过 ACTION_GET_CONTENT 或者 ACTION_OPEN_CONTENT 的系统文件选择器,但是我们从开发者那里了解到,它感觉没有很好地集成到他们的 App 中。


图片选择器

在 Android 13 中,我们引入了一个新的媒体工具 Android 照片选择器。该工具为用户提供了一种选择媒体文件的方法,而不需要授予对其整个媒体库的访问权限。

它提供了一个简洁界面,展示照片和视频,按照日期排序。另外在 "Albums" 页面,用户可以按照屏幕截图或下载等等分类浏览,通过指定一些用户是否仅看到照片或视频,也可以设置选择最大文件数量,也可以根据自己的需求定制照片选择器。简而言之,这个照片选择器是为私人设计的,具有干净和简洁的 UI 易于实现。


我们还通过谷歌 Play 系统更新 (2022 年 5 月 1 日发布),将照片选择器反向移植到 Android 11 和 12 上,以将其带给更多的 Android 用户。

开发一个照片选择器是一个复杂的项目,新的照片选择器不需要团队进行任何维护。我们已经在 ActivityX 1.6.0 版本中为它创建了一个 ActivityResultContract。如果照片选择器在你的系统上可用,将会优先使用照片选择器。

// Registering Photo Picker activity launcher with a max limit of 5 items
val pickMultipleVisualMedia = registerForActivityResult(PickMultipleVisualMedia(5)) { uris ->
   // TODO: process URIs
}
// Launching the photo picker (photos & video included)
pickMultipleVisualMedia.launch(PickVisualMediaRequest(PickVisualMedia.ImageAndVideo))
复制代码

如果希望添加类型进行筛选,可以采用这种方式。

// Launching the photo picker (photos only)
pickMultipleVisualMedia.launch(PickVisualMediaRequest(PickVisualMedia.ImageOnly))
// Launching the photo picker (video only)
pickMultipleVisualMedia.launch(PickVisualMediaRequest(PickVisualMedia.VideoOnly))
// Launching the photo picker (GIF only)
pickMultipleVisualMedia.launch(PickVisualMediaRequest(PickVisualMedia.SingleMimeType("image/gif")))
复制代码

可以调用 isPhotoPickerAvailable 方法来验证在当前设备上照片选择器是否可用。

ACTION_GET_CONTENT 将会发生改变

正如你所见,使用新的照片选择器只需要几行代码。虽然我们希望所有的 Apps 都使用它,但在 App 中迁移可能需要一些时间。

这就是为什么我们使用 ACTION_GET_CONTENT 将系统文件选择器转换为照片选择器,而不需要进行任何代码更改,从而将新的照片选择器引入到现有的 App 中。


针对特定场景的新权限

虽然我们强烈建议您使用新的照片选择器,而不是访问所有媒体文件,但是您的 App 可能有一个场景,需要访问所有媒体文件(例如图库照片备份)。对于这些特定的场景,我们将引入新的权限,以提供对特定类型的媒体文件的访问,包括图像、视频或音频。您可以在文档中阅读更多关于它们的内容。

如果用户之前授予你的应用程序 READ_EXTERNAL_STORAGE 权限,系统会自动授予你的 App 访问权限。否则,当你的 App 请求任何新的权限时,系统会显示一个面向用户的对话框。

所以您必须始终检查是否仍然授予了权限,而不是存储它们的授予状态。


下面的决策树可以帮助您更好的浏览这些更改。


我们承诺在保护用户隐私的同时,继续改进照片选择器和整体存储开发者体验,以创建一个安全透明的 Android 生态系统。

新的照片选择器被反向移植到所有 Android 11 和 12 设备,不包括 Android Go 和非 gms 设备。


原文: medium.com/androiddeve…
译者:程序员 DHL
来源:
juejin.cn/post/7161230716838084616



收起阅读 »

程序员被开除,老板:“有你参与的项目全黄了!”

而我发现,之前跟我对接算法的同事好像很久都没有来了,因为我们公司程序员经常有出差的安排,所以我也没当回事。直到有一次,我在跟经理汇报工作的时候,因为算法部分还有部分功能有待完善,我就提了下之前跟我对接的算法同事。听到这话我着实有些惊讶,因为这位被开除了的算法同...
继续阅读 »

我现在所在的公司是一家专注于做机器视觉的公司,简单点解释就是像车牌号识别、工厂工件自动质检、摄像头抓拍盯梢等等这一类的功能,我们公司都是可以做的。而我在公司内是负责ERP模块的,大多数还是负责流程模块的开发。对图像进行识别处理的功能,则需要公司另外一批专攻算法、图像识别的同事去解决,然后我直接拿他们给的结果就行,这样的同事我们称之为“算法同事”。

而我发现,之前跟我对接算法的同事好像很久都没有来了,因为我们公司程序员经常有出差的安排,所以我也没当回事。直到有一次,我在跟经理汇报工作的时候,因为算法部分还有部分功能有待完善,我就提了下之前跟我对接的算法同事。

结果我经理跟我说:“他已经被老板开除了!”。

听到这话我着实有些惊讶,因为这位被开除了的算法同事我之前一直以为他在公司的地位是很高的,想不到他竟然被开除了,于是我就问经理他被开除的原因。

经理回答说:“基本上有他参与的项目都黄了,所以老板找他谈了下,后来就直接把他开除了!”。

事情经过简单地讲就是:

这个同事来了公司快三年了,也算公司的老员工了,大大小小的项目也做了不少,但是只要是他参与的项目,算法部分总是会出现各种问题。基本上解决一个旧的问题,新的问题就又来了。因为我们的项目大多数都涉及到自动化,所以,项目总是出问题的话,甲方就不会放心项目在无人值守的情况下运行,就得安排人手。这样一来,自动化的意义何在?所以很多甲方就不愿意去验收项目。

而为了项目能够顺利验收,我们公司就得派专人去客户现场调试,然后对项目中存在的问题进行反馈。这样一来,如果问题始终得不到解决,公司派去的调试人员就无法回来。用我经理的话讲:“曾经有个同事在客户那待了三个多月,硬是没解决问题,最后项目黄了才不得不回来!”。

我听我们经理这么一说,还蛮惊讶,因为我觉得之前跟那个算法同事对接的时候,感觉到他身上有一股子“大牛”程序员的气质,想不到他竟然是个“马大哈”!

老板觉得是这个算法同事的问题,白白搭进去近三年的工资,还有那么多人力物力,这都不算什么,主要是大多数的项目黄了都是因为验收不过关导致的。公司不光损失了客户,还损失了大把的时间。于是,最后决定把这个算法同事开除!

其实看到这里,也许有一部分人会觉得老板的做法有点草率,感觉是这个算法同事“背锅”了。其实我开始也这么觉得,但是最后了解了事情的原委以后,我觉得老板的想法并不是毫无根据,反而我觉得这个算法同事被开除,其实一点也不冤!

我们公司的研发部门结构还算完整,从产品设计到项目研发,然后到测试到实施其实都是有专人负责的。只不过在测试和实施阶段需要在客户现场进行,所以如果一个研发人员做事不细心的话,就会拖累测试和实施的同时。

而其他管项目中算法部分的同事在以往的项目当中,均没有出现太大纰漏,大部分项目都能够正常验收。而我们公司的项目中,算法和图像处理看似占比不多,但却是软件当中一个重要的环节,其他环节只是为了辅助算法和图像处理而已。所以,如果算法和图像处理部分没做好,即使其他部分做得再好也是徒劳的。

最后老板将问题定位在这个算法同事身上,其实也是比较合理的。

其实对于这个同事被开除,我觉得可能存在两方面的原因:

第一点就是可能这个算法同事的能力有问题导致的!简单地打个比方,比如一个摄像头在抓拍到车牌号以后,能够正确识别,那就没什么问题。但是,如果这个算法同事在分析摄像头抓拍到的图片的时候,在相机和其他设备都没有问题的情况下,始终无法稳定的获得图像识别结果,那可能就是这个同事的能力问题了。

第二点可能就是这个算法同事的做事态度问题!因为常见的图像识别目前都有公开的解决方案,在此基础上做一些个性化的修改,无可厚非。但是,当一些常见的解决方案放在他手上的时候,他如果做事不细心,总是在小的地方出纰漏,那么即使把代码照搬给他,在他手上都可能会出问题。其实这个也是很多程序员虽然能力看似很强,但对于代码总是不去自查,不先过自己那关,给人的感觉就很不靠谱,他们写的代码让人很不放心的原因。

但不管是哪种原因,被开除总是一个不太好的经历。希望这个算法同事经过这个事情也能弥补自己能力上的不足,端正自己的工作态度。

而且,我觉得公司也存在一些问题。因为近三年在一个人的手上的项目总是无法验收的时候,公司应该要做一些事情来避免这种事情的发生,并不是说把一个人开除了就能够解决根本问题了。虽然我对公司没有什么太好的建议,但是我还是希望公司在未来能够避免这种情况发生,毕竟少一单生意,对于公司和员工都是损失!

来源:http://www.163.com/dy/article/HJBBCM5R0552RA7X.html

收起阅读 »

一台服务器最大能支持多少条TCP连接

一、一台服务器最大能打开的文件数1、限制参数我们知道在Linux中一切皆文件,那么一台服务器最大能打开多少个文件呢?Linux上能打开的最大文件数量受三个参数影响,分别是:fs.file-max (系统级别参数):该参数描述了整个系统可以打开的最大文件数量。但...
继续阅读 »

一、一台服务器最大能打开的文件数

1、限制参数

我们知道在Linux中一切皆文件,那么一台服务器最大能打开多少个文件呢?Linux上能打开的最大文件数量受三个参数影响,分别是:

  • fs.file-max (系统级别参数):该参数描述了整个系统可以打开的最大文件数量。但是root用户不会受该参数限制(比如:现在整个系统打开的文件描述符数量已达到fs.file-max ,此时root用户仍然可以使用ps、kill等命令或打开其他文件描述符)

  • soft nofile(进程级别参数):限制单个进程上可以打开的最大文件数。只能在Linux上配置一次,不能针对不同用户配置不同的值

  • fs.nr_open(进程级别参数):限制单个进程上可以打开的最大文件数。可以针对不同用户配置不同的值

这三个参数之间还有耦合关系,所以配置值的时候还需要注意以下三点:

  1. 如果想加大soft nofile,那么hard nofile参数值也需要一起调整。如果因为hard nofile参数值设置的低,那么soft nofile参数的值设置的再高也没有用,实际生效的值会按照二者最低的来。

  2. 如果增大了hard nofile,那么fs.nr_open也都需要跟着一起调整(fs.nr_open参数值一定要大于hard nofile参数值)。如果不小心把hard nofile的值设置的比fs.nr_open还大,那么后果比较严重。会导致该用户无法登录,如果设置的是*,那么所有用户都无法登录

  3. 如果加大了fs.nr_open,但是是用的echo "xxx" > ../fs/nr_open命令来修改的fs.nr_open的值,那么刚改完可能不会有问题,但是只要机器一重启,那么之前通过echo命令设置的fs.nr_open值便会失效,用户还是无法登录。所以非常不建议使用echo的方式修改内核参数!!!

2、调整服务器能打开的最大文件数示例

假设想让进程可以打开100万个文件描述符,这里用修改conf文件的方式给出一个建议。如果日后工作里有类似的需求可以作为参考。

  • vim /etc/sysctl.conf

fs.file-max=1100000 // 系统级别设置成110万,多留点buffer
fs.nr_open=1100000 // 进程级别也设置成110万,因为要保证比 hard nofile大
  • 使上面的配置生效sysctl -p

  • vim /etc/security/limits.conf

// 用户进程级别都设置成100完
soft nofile 1000000
hard nofile 1000000

二、一台服务器最大能支持多少连接

我们知道TCP连接,从根本上看其实就是client和server端在内存中维护的一组【socket内核对象】(这里也对应着TCP四元组:源IP、源端口、目标IP、目标端口),他们只要能够找到对方,那么就算是一条连接。那么一台服务器最大能建立多少条连接呢?

  • 由于TCP连接本质上可以理解为是client-server端的一对socket内核对象,那么从理论上将应该是【2^32 (ip数) * 2^16 (端口数)】条连接(约等于两百多万亿)

  • 但是实际上由于受其他软硬件的影响,我们一台服务器不可能能建立这么多连接(主要是受CPU和内存限制)。

如果只以ESTABLISH状态的连接来算(这些连接只是建立,但是不收发数据也不处理相关的业务逻辑)那么一台服务器最大能建立多少连接呢?以一台4GB内存的服务器为例!

  • 这种情况下,那么能建立的连接数量主要取决于【内存的大小】(因为如果是)ESTABLISH状态的空闲连接,不会消耗CPU(虽然有TCP保活包传输,但这个影响非常小,可以忽略不计)

  • 我们知道一条ESTABLISH状态的连接大约消耗【3.3KB内存】,那么通过计算得知一台4GB内存的服务器,【可以建立100w+的TCP连接】(当然这里只是计算所有的连接都只建立连接但不发送和处理数据的情况,如果真实场景中有数据往来和处理(数据接收和发送都需要申请内存,数据处理便需要CPU),那便会消耗更高的内存以及占用更多的CPU,并发不可能达到100w+)

上面讨论的都是进建立连接的理想情况,在现实中如果有频繁的数据收发和处理(比如:压缩、加密等),那么一台服务器能支撑1000连接都算好的了,所以一台服务器能支撑多少连接还要结合具体的场景去分析,不能光靠理论值去算。抛开业务逻辑单纯的谈并发没有太大的实际意义

服务器的开销大头往往并不是连接本身,而是每条连接上的数据收发,以及请求业务逻辑处理!!!

三、一台客户端机器最多能发起多少条连接

我们知道客户端每和服务端建立一个连接便会消耗掉client端一个端口。一台机器的端口范围是【0 ~ 65535】,那么是不是说一台client机器最多和一台服务端机器建立65535个连接呢(这65535个端口里还有很多保留端口,可用端口可能只有64000个左右)?

由TCP连接的四元组特性可知,只要四元组里某一个元素不同,那么就认为这是不同的TCP连接。所以需要分情况讨论:

  • 【情况一】、如果一台client仅有一个IP,server端也仅有一个IP并且仅启动一个程序,监听一个端口的情况下,client端和这台server端最大可建立的连接条数就是 65535 个。

    • 因为源IP固定,目标IP和端口固定,四元组中唯一可变化的就是【源端口】,【源端口】的可用范围又是【0 ~ 65535】,所以一台client机器最大能建立65535个连接

  • 【情况二】、如果一台client有多个IP(假设客户端有 n 个IP),server端仅有一个IP并且仅启动一个程序,监听一个端口的情况下,一台client机器最大能建立的连接条数是:n * 65535

    • 因为目标IP和端口固定,有 n 个源IP,四元组中可变化的就是【源端口】+ 【源IP】,【源端口】的可用范围又是【0 ~ 65535】,所以一个IP最大能建立65535个连接,那么n个IP最大就能建立n * 65535个连接了

    • 以现在的技术,给一个client分配多个IP是非常容易的事情,只需要去联系你们网管就可以做到。

  • 【情况三】、如果一台client仅有一个IP,server端也仅有一个IP但是server端启动多个程序,每个程序监听一个端口的情况下(比如server端启动了m个程序,监听了m个不同端口),一台client机器最大能建立的连接数量为:65535 * m

    • 源IP固定,目标IP固定,目标端口数量为m个,可变化的是源端口,而源端口变化范围是【0 ~ 65535】,所以一台client机器最大能建立的TCP连接数量是 65535 * m

  • 其余情况类推,但是客户端的可用端口范围一般达不到65535个,受内核参数net.ipv4.ip_local_port_range限制,如果要修改client所能使用的端口范围,可以修改这个内核参数的值。

  • 所以,不光是一台server端可以接收100w+个TCP连接,一台client照样能发出100w+个连接

四、其他

  • 三次握手里socket的全连接队列长度由参数net.core.somaxconn来控制,默认大小是128,当两台机器离的非常近,但是建立连接的并发又非常高时,可能会导致半连接队列或全连接队列溢出,进而导致server端丢弃握手包。然后造成client超时重传握手包(至少1s以后才会重传),导致三次握手连接建立耗时过长。我们可以调整参数net.core.somaxconn来增加去按连接队列的长度,进而减小丢包的影响

  • 有时候我们通过 ctrl + c方式来终止了某个进程,但是当重启该进程的时候发现报错端口被占用,这种问题是因为【操作系统还没有来得及回收该端口,等一会儿重启应用就好了】

  • client程序在和server端建立连接时,如果client没有调用bind方法传入指定的端口,那么client在和server端建立连接的时候便会自己随机选择一个端口来建立连接。一旦我们client程序调用了bind方法传入了指定的端口,那么client将会使用我们bind里指定的端口来和server建立连接。所以不建议client调用bind方法,bind函数会改变内核选择端口的策略

    public static void main(String[] args) throws IOException {
       SocketChannel sc = SocketChannel.open();
    // 客户端还可以调用bind方法
       sc.bind(new InetSocketAddress("localhost", 9999));
       sc.connect(new InetSocketAddress("localhost", 8080));
       System.out.println("waiting..........");
    }
  • 在Linux一切皆文件,当然也包括之前TCP连接中说的socket。进程打开一个socket的时候需要创建好几个内核对象,换一句直白的话说就是打开文件对象吃内存,所以Linux系统基于安全角度考虑(比如:有用户进程恶意的打开无数的文件描述符,那不得把系统搞奔溃了),在多个位置都限制了可打开的文件描述符的数量

  • 内核是通过【hash表】的方式来管理所有已经建立好连接的socket,以便于有请求到达时快速的通过【TCP四元组】查找到内核中对应的socket对象

    • 在epoll模型中,通过红黑树来管理epoll对象所管理的所有socket,用红黑树结构来平衡快速删除、插入、查找socket的效率

五、相关实际问题

在网络开发中,很多人对一个基础问题始终没有彻底搞明白,那就是一台机器最多能支撑多少条TCP连接。不过由于客户端和服务端对端口使用方式不同,这个问题拆开来理解要容易一些。

注意,这里说的是客户端和服务端都只是角色,并不是指某一台具体的机器。例如对于我们自己开发的应用程序来说,当他响应客户端请求的时候,他就是服务端。当他向MySQL请求数据的时候,他又变成了客户端。

1、"too many open files" 报错是怎么回事,该如何解决

你在线上可能遇到过too many open files这个错误,那么你理解这个报错发生的原理吗?如果让你修复这个错误,应该如何处理呢?

  • 因为每打开一个文件(包括socket),都需要消耗一定的内存资源。为了避免个别进程不受控制的打开了过多文件而让整个服务器奔溃,Linux对打开的文件描述符数量有限制。如果你的进程触发到内核的限制,那么"too many open files" 报错就产生了

  • 可以通过修改fs.file-maxsoft nofilefs.nr_open这三个参数的值来修改进程能打开的最大文件描述符数量

    • 需要注意这三个参数之间的耦合关系!

2、一台服务端机器最大究竟能支持多少条连接

因为这里要考虑的是最大数,因此先不考虑连接上的数据收发和处理,仅考虑ESTABLISH状态的空连接。那么一台服务端机器上最大可以支持多少条TCP连接?这个连接数会受哪些因素的影响?

  • 在不考虑连接上数据的收发和处理的情况下,仅考虑ESTABLISH状态下的空连接情况下,一台服务器上最大可支持的TCP连接数量基本上可以说是由内存大小来决定的。

  • 四元组唯一确定一条连接,但服务端可以接收来自任意客户端的请求,所以根据这个理论计算出来的数字太大,没有实际意义。另外文件描述符限制其实也是内核为了防止某些应用程序不受限制的打开【文件句柄】而添加的限制。这个限制只要修改几个内核参数就可以加大。

  • 一个socket大约消耗3kb左右的内存,这样真正制约服务端机器最大并发数的就是内存,拿一台4GB内存的服务器来说,可以支持的TCP连接数量大约是100w+

3、一条客户端机器最大究竟能支持多少条连接

和服务端不同的是,客户端每次建立一条连接都需要消耗一个端口。在TCP协议中,端口是一个2字节的整数,因此范围只能是0~65535。那么客户单最大只能支持65535条连接吗?有没有办法突破这个限制,有的话有哪些办法?

  • 客户度每次建立一条连接都需要消耗一个端口。从数字上来看,似乎最多只能建立65535条连接。但实际上我们有两种办法破除65535这个限制

    • 方式一,为客户端配置多IP

    • 方式二,分别连接不同的服务端

  • 所以一台client发起百万条连接是没有任何问题的

4、做一个长连接推送产品,支持1亿用户需要多少台机器

假设你是系统架构师,现在老板给你一个需求,让你做一个类似友盟upush这样的产品。要在服务端机器上保持一个和客户端的长连接,绝大部分情况下连接都是空闲的,每天也就顶多推送两三次左右。总用户规模预计是1亿。那么现在请你来评估一下需要多少台服务器可以支撑这1亿条长连接。

  • 对于长连接推送模块这种服务来说,给客户端发送数据只是偶尔的,一般一天也就顶多一两次。绝大部分情况下TCP连接都是空闲的,CPU开销可以忽略

  • 再基于内存来考虑,加色服务器内存是128G的,那么一台服务器可以考虑支持500w条并发。这样会消耗掉大约不到20GB内存用来保存这500w条连接对应的socket。还剩下100GB以上的内存来应对接收、发送缓冲区等其他的开销足够了。所以,一亿用户,仅仅需要20台服务器就差不多够用了!

参考:《深入理解Linux网络》

作者:文攀
来源:juejin.cn/post/7162824884597293086

收起阅读 »

环信Discord场景创意编程大赛开启招募啦!

这一届年轻人最喜欢用的社交软件有什么?很多人会想到TikTok、Snapchat、Instagram,但很多人却忽略了一个隐藏王者“Discord”。2021年微软以120亿美元提出收购Discord惨遭拒绝引爆了行业讨论,相较于TikTok、Instagra...
继续阅读 »


这一届年轻人最喜欢用的社交软件有什么?很多人会想到TikTok、Snapchat、Instagram,但很多人却忽略了一个隐藏王者“Discord”。2021年微软以120亿美元提出收购Discord惨遭拒绝引爆了行业讨论,相较于TikTok、Instagram这种大众开放社交平台,Discord似乎更为私密和闭环,但相对于Messenger、WhatsApp这种点对点的IM工具,Discord又更加开放且丰富。就是这样一款富有魔力的社交产品,以1.6亿月活、2亿多美元收入、150亿美元估值,一时风头无二,同时也成为了国内和出海企业竞相模仿的对方!




国内首届Discord场景创意编程大赛由环信联合华为主办,将以 环信超级社区SDK 应用场景创意为方向,号召开发者聚焦「类 Discord」热门场景,发挥天马行空的创意,探索超大型万人社区更多行业实践,根据大赛提供的创意channel、场景功能、UI素材包等,设计和开发出创意兼具实用性的场景应用。

环信超级社区(Circle)是一款基于环信 IM 打造的类 Discord 实时社区应用场景方案,支持社区(Server)、频道(Channel) 和子区(Thread) 三层结构。一个 App 下可以有多个社区,同时支持陌生人/好友单聊。用户可创建和管理自己的社区,在社区中设置和管理频道将一个话题下的子话题进行分区,在频道中根据感兴趣的某条消息发起子区讨论,实现万人实时群聊,满足超大规模用户的顺畅沟通需求。旨在一站式帮助客户快速开发和构建稳定超大规模用户即时通讯的"类Discord超级社区",作为构建实时交互社区的第一选择,环信超级社区自发布以来很好地满足了类 Discord 实时社区业务场景的客户需求,并支持开发者结合业务需要灵活自定义产品形态,目前已经广泛服务于国内头部出海企业以及海外东南亚和印度企业。











参赛作品需基于超级社区接口或超级社区Demo进行开发,使用至少一项基于超级社区的功能,参考下方功能建议,作品至少包含2个与场景相关的新功能。
以下场景及功能供参考:


  • 参赛作品需提供图片、代码、文字说明等,参赛作品的 Readme 文档中应包含详细的项目介绍、使用说明;
  • 参赛作品须保证原创性,不违反相关法律法规,不存在任何法律或合规风险,作品中使用的素材(包括但不限于开源代码、图片、视频等)不存在版权问题;
  • 参赛作品的源代码均以 MIT 协议对外进行开源;

报名须知:
  • 超级社区功能需联系平台进行开通,请入群联系活动负责人或环信商务进行开通;
  • 参赛者需在规定时间内报名,不限个人或团队参赛,团队参赛请指定一名联系人,活动期间可随时报名和提交作品;
  • 请注意作品截止提交时间,将作品内容在规定时间内推送至官方仓库,否则将不参与作品评选







  • Fork 本次创意赛官方作品提交仓库至你的个人 GitHub 仓库;
  • Clone 你的 GitHub 仓库代码「https://github.com/你的GitHub名/EasemobCircle-2022-Innovation-Challenge」;
  • 在本地的 「Innovation-Challenge」文件夹下新创建个人项目文件夹,命名格式为“项目序号-队伍名-作品名”,将参赛作品的相关文件与代码放置在该文件夹内,勿要“感染”其他文件或者文件夹;
  • 最后通过 Pull Request 将作品内容推送至官方仓库;
  • Review 通过,合入本次比赛的主分支。



编程赛交流群:




- 注册环信:

https://console.easemob.com/user/register


- 超级社区介绍:

https://www.easemob.com/product/im/circle


- 超级社区SDK 集成文档:

https://docs-im.easemob.com/ccim/circle/overview


-超级社区Demo体验

https://www.easemob.com/download/demo#discord


- 技术支持社区:

https://www.imgeek.org/topic/27685


-大赛报名:

https://www.wjx.top/vm/eGIlI4V.aspx


-下载设计资源:

https://www.figma.com/community/file/1169587212861993948


-作品提交:

https://github.com/easemob/EasemobCircle-2022-Innovation-Challenge




欢迎访问大赛官网:https://www.easemob.com/event/discord/

收起阅读 »

因面试提到 Handler 机制后,引发连环炮轰(我已承受不来~)

因业务繁忙,有段时间没有和大家进行技术分享了,今日特此抽出时间先来分享!!!今日头条面试题:讲讲ThreadLocal底层原理和Handler的关系竟然提到了Handler机制就不得不提到这几大将了:Handler,Looper,MessageQueue,Me...
继续阅读 »

因业务繁忙,有段时间没有和大家进行技术分享了,今日特此抽出时间先来分享!!!

今日头条面试题:讲讲ThreadLocal底层原理和Handler的关系

竟然提到了Handler机制就不得不提到这几大将了:Handler,Looper,MessageQueue,Message。延伸重点ThreadLocal!!!

当UI的主线程在初始化第一个 Handler时,就会通过ThreadLocal创建一个Looper,该Looper与UI主线程一一对应。而使用ThreadLocal的目的是保证每一个线程只创建唯一一个Looper。

Looper初始化的时候会创建一个 消息队列MessageQueue。至此,主线程、消息循环、消息队列之间的关系是1:1:1。

Handler、Looper、MessageQueue的初始化流程如下图所示:Hander持有对UI主线程消息队列MessageQueue和消息循环Looper的引用,子线程可以通过 Handler 将消息发送到UI线程的消息队列MessageQueue中。

二问:主线程为啥不用初始化looper呢?

那是因为looper早在ActivityThread初始化的时候就声明好了,可以直接拿来用的。通过分析源码我们知道MessageQueue在Looper中,Looper初始化后作为对象丢给了Handler,并且又存在了 ThreadLocal 里面,ThreadLocal 对象的话作为Key又存在了ThreadLocalMap,ThreadLocalMap对象是Thread里面的一个属性值,也就是说Looper作为桥梁连接了Handler与Looper所在的线程。

三问:Handler机制有了解过没?跟我说说?

在理解Handler 机制前,我们需要先搞懂ThreadLocal。

ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

ThreadLocal的原理

想搞懂原理那就得先从源码入手开始分析。

我们先从set方法看起:

从上面的代码不难看出,ThreadLocal  set赋值的时候首先会获取当前线程thread,并获取thread线程中的ThreadLocalMap属性。如果map中属性不为空,则直接更新value值,如果map中找不到此ThreadLocal对象,则在threadLocalMap创建一个,并将value值初始化。显然ThreadLocal对象存的值是根据线程走的!

那么ThreadLocalMap又是什么呢,还有createMap又是怎么做的,我们继续往下看。

首先第一步我们得要知道这个东西

每个Thread有一个属性,类型是ThreadLocalMap,从代码不难看出ThreadLocalMap是ThreadLocal的内部静态类。它是与线程所绑定联系在一起的,可以看成一个线程只有一个ThreadLocalMap,知道这一点我们再往下看。

ThreadLocalMap的构成主要是用Entry来保存数据 ,而且还是继承的弱引用。在Entry内部使用ThreadLocal对象作为key,使用我们设置的value作为value。

get的话就比较简单了就是获取当前线程的ThreadLocalMap属性值,在获取Map中对应ThreadLocal对象的value值并返回。

 ThreadLocal总结一下就是:每个线程Thread自身有一个属性ThreadLocalMap,然后ThreadLocalMap是一个键值对,它的key值是ThreadLocal对象,它的value值则是我们想要保存处理的数据值。getMap是找到对应线程的ThreadLocalMap属性值,然后通过判断可以初始化或者更新数值。

ThreadLocal分析完了我们接着来看Handler吧。

因为主线程在ActivityThread的main方法中已经创建了Looper,所以主线程使用Handler时可以直接new;子线程使用Handler时需要调用Looper的prepare和loop方法才能进行使用,否则会抛出异常。所以我们从Looper的prepare来分析。

Looper 提供了 Looper.prepare() 方法来创建 Looper ,并且会借助 ThreadLocal 来实现与当前线程的绑定功能。Looper.loop() 则会开始不断尝试从 MessageQueue 中获取 Message , 并分发给对应的 Handler,也就是说 Handler 跟线程的关联是靠 Looper 来实现的。

Looper.loop() 负责对消息的分发,也是和prepare配套使用的方法,两者缺一不可。

msg.target是个啥呢,我们追到Message里面不难发现其实它就是我们发送消息的Handler,这写法是不是很聪明,当从MessageQueen中捞出Message后,我们就能直接调用Handler的dispatchMessage,然后就会走到我们的Handler的handleMessage了。直接上源码:

处理分析完了,我们看个简单点的,消息发送吧。Handler 提供了一些列的方法让我们来发送消息,如 send()系列 post()系列 。不过不管我们调用什么方法,最终都会走到 MessageQueue的enqueueMessage(Message,long) 方法。也就是实现了消息的发送,将Message插入到我们的MessageQueue中。

注意:dispatchMessage() 方法针对 Runnable 的方法做了特殊处理,如果是 ,则会直接执行 Runnable.run() 。(判断依据是上述msg.callback !=null这句)

MessageQueue是个单链表。

MessageQueue里消息按时间排序

MessageQueue的next()是个堵塞方法

总结分析:

Looper.loop() 是个死循环,会不断调用 MessageQueue.next() 获取 Message ,并调用 msg.target.dispatchMessage(msg) 回到了 Handler 来分发消息,以此来完成消息的回调。

四问:Handler什么会出现内存泄漏问题呢?

Handler使用是用来进行线程间通信的,所以新开启的线程是会持有Handler引用的,如果在Activity等中创建Handler,并且是非静态内部类的形式,就有可能造成内存泄漏。

非静态内部类是会隐式持有外部类的引用,所以当其他线程持有了该Handler,线程没有被销毁,则意味着Activity会一直被Handler持有引用而无法导致回收。

MessageQueue中如果存在未处理完的Message,Message的target也是对Activity等的持有引用,也会造成内存泄漏。

解决的办法:

使用静态内部类 + 弱引用的方式: 静态内部类不会持有外部类的的引用,当需要引用外部类相关操作时,可以通过弱引用还获取到外部类相关操作,弱引用是不会造成对象该回收回收不掉的问题,不清楚的可以查阅JAVA的几种引用方式的详细说明。

在外部类对象被销毁时,将MessageQueue中的消息清空。

五问:Looper死循环为什么不会导致应用卡死?

对于线程即是一段可执行的代码,当可执行代码执行完成后,线程生命周期便该终止了,线程退出。而对于主线程,我们是绝不希望会被运行一段时间,自己就退出,那么如何保证能一直存活呢?简单做法就是可执行代码是能一直执行下去的,死循环便能保证不会被退出,例如,binder线程也是采用死循环的方法,通过循环方式不同与Binder驱动进行读写操作,当然并非简单地死循环,无消息时会休眠。但这里可能又引发了另一个问题,既然是死循环又如何去处理其他事务呢?通过创建新线程的方式。真正会卡死主线程的操作是在回调方法onCreate/onStart/onResume等操作时间过长,会导致掉帧,甚至发生ANR,looper.loop本身不会导致应用卡死。

六问:主线程的死循环一直运行是不是特别消耗CPU资源呢?

其实不然,这里就涉及到Linux pipe/epoll机制,简单说就是在主线程的MessageQueue没有消息时,便阻塞在Loop的queue.next()中的nativePollOnce()方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。这里采用的epoll机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。

好了,这轮面试中问道的Handler 就问了这么多了,大家可以好好的吸收一下,如有什么疑议欢迎在评论区进行讨论!!!


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

Flutter桌面开发-项目工程化框架搭建

前言 在本专栏前面的几篇文章中,我们对桌面应用实现了可定制的窗口化,适配了多种分辨率的屏幕,并且实现了小组件 “灵动岛”。前面的文章算是一些基础建设的搭建,这篇文章我将基于状态管理库GetX,搭建一个成熟完善的,可投入生产开发的项目架构。这也是我们后面继续开发...
继续阅读 »

前言


在本专栏前面的几篇文章中,我们对桌面应用实现了可定制的窗口化适配了多种分辨率的屏幕,并且实现了小组件 “灵动岛”。前面的文章算是一些基础建设的搭建,这篇文章我将基于状态管理库GetX,搭建一个成熟完善的,可投入生产开发的项目架构。这也是我们后面继续开发桌面应用的基础。


搭建原则


此次项目框架的搭建,完全基于GetX库。虽说之前我也分析过GetX的优势和弊端,但对于我们一个开源的项目,GetX这种“全家桶”库再适合不过啦。同时还会提供在Windows开发过程中一些区别于移动端开发的小技巧。


GetX全家桶的搭建


为啥说GetX是全家桶,因为它不仅可以满足MVVM的状态管理,还能满足:国际化、路由配置、网络请求等等,着实方便,而且亲测可靠!GetX


1. 国际化


GetX提供了应用的顶层入口GetMaterialApp,这个控件封装了Flutter的MaterialApp,我们只需要按照GetX给定的规则传入多语言的配置即可。配置也是非常简单的,只需要在类中提供get声明的map对象即可。Map的key由语言的代码和国家地区组成无需处理系统语言环境变化等事件。


import 'package:get/get.dart';

class Internationalization extends Translations {
@override
Map<String, Map<String, String>> get keys => {
'en_US': {
'appName': 'Flutter Windows',
'hello':'Hello World!'
},
'zh_CN': {
'appName': 'Flutter桌面应用',
'hello':'你好,世界!'
},
'zh_HK': {
'appName': 'Flutter桌面應用',
'hello':'你好,世界!'
},
};
}

2. 路由配置


如果没有使用GetX,路由管理很大情况是使用Fluro,大量的define、setting、handle真的配置的很枯燥。在GetX中,你只需要配置路由名称和对应的Widget即可。


class RouteConfig {
/// home模块
static const String home = "/home/homePage";

/// 我的模块
static const String mine = "/mine/myPage";

static final List<GetPage> getPages = [
GetPage(name: home, page: () => HomePage()),
GetPage(name: mine, page: () => MinePage()),
];
}

至于参数,可以直接像web端的url一样,使用?、&传递。

同时GetX也提供了路由跳转的方式,相比Flutter Navigator2提供的api,GetX的路由跳转明显更加方便,可以脱离context进行跳转,我们可以在VM层随意处理路由,这点真的很爽。


// 跳转到我的页面
Get.toNamed('${RouteConfig.mine}?userId=123&userName=karl');

// 我的页面接收参数
String? userName = Get.parameters['userName'];

3. GetX状态管理


状态管理才是GetX的重头戏,GetX中实现的Obx机制,能非常轻量级的帮我们定点刷新。Obx是通过创建定向的Stream,来局部setState的。而且作者还提供了ide的插件,我们来创建一个GetX的页面。

image.png
通过插件快捷创建之后我们可以得到:logic、state、view的分层结构,通过logic绑定数据和视图,并且实现数据驱动UI刷新。
image.png
当然,通过Obx的方式会触发创建较多的Stream,有时使用update()来主动刷新也是可以的。

关于GetX的状态管理,有个细节要提示下:



  • 如果listview.build下的item都有自己的状态管理,那么每个item需要向logic传递自己的tag才能产生各自的Obx stream;


Get.put(SwiperItemLogic(), tag: model.key);

GetX相对其他的状态管理,最重点是基于Stream实现了真正的跨组件通信,包括兄弟组件;只需要保证logic层Put一次,其余组件去Find即可直接更新logic的值,实现视图刷新。


4. 网络请求


在网络请求上,GetX的封装其实并没有dio来的好,Get_connect插件集成了REST API请求和GraphQL规范,我们开发过程中其实不会两者都用。虽然GraphQL提高了健壮性,但在定义请求对象的时候,往往会增加一些工作量,特别是对于小项目。



  1. 我们可以先创建一个基础内容提供,完成通用配置;


/// 网络请求基类,配置公共属性
class BaseProvider extends GetConnect {
@override
void onInit() {
super.onInit();
httpClient.baseUrl = Api.baseUrl;
// 请求拦截
httpClient.addRequestModifier<void>((request) {
request.headers['accept'] = 'application/json';
request.headers['content-type'] = 'application/json';
return request;
});

// 响应拦截;甚至已经把http status都帮我们区分好了
httpClient.addResponseModifier((request, response) {
if (response.isOk) {
return response;
} else if (response.unauthorized) {
// 账户权限失效
}
return response;
});
}
}


  1. 然后按照模块化去配置请求,提高可维护性。


import 'package:get/get.dart';

import 'base_provider.dart';

/// 按照模块去制定网络请求,数据源模块化
class HomeProvider extends BaseProvider {
// get会带上baseUrl
Future<Response> getHomeSwiper(int id) => get('home/swiper');
}

日志记录


日志我们采用Logger进行记录,桌面端一般使用txt文件格式。以时间命名,天为单位建立日志文件即可。如果有需要,也可以加一些定时清理的逻辑。

我们需要重写下LogOutput的方法,把颜色和表情都去掉,避免编码错误,然后实现下单例。


Logger? logger;

Logger get appLogger => logger ??= Logger(
filter: CustomerFilter(),
printer: PrettyPrinter(
printEmojis: false,
colors: false,
methodCount: 0,
noBoxingByDefault: true),
output: LogStorage(),
);

class LogStorage extends LogOutput {
// 默认的日志文件过期时间,以小时为单位
static const _logExpiredTime = 72;

/// 日志文件操作对象
File? _file;

/// 日志目录
String? logDir;

/// 日志名称
String? logName;

LogStorage({this.logDir, this.logName});

@override
void destroy() {
deleteExpiredLogs(_logExpiredTime);
}

@override
void init() async {
deleteExpiredLogs(_logExpiredTime);
}

@override
void output(OutputEvent event) async {
_file ??= await createFile(logDir, logName);
String now = CommonUtils.formatDateTime(DateTime.now());
String version = packageInfo.version;
_file!.writeAsStringSync('>>>> $version $now [${event.level.name}]\n',
mode: FileMode.writeOnlyAppend);

for (var line in event.lines) {
_file!.writeAsStringSync('${line.toString()}\n',
mode: FileMode.writeOnlyAppend);
debugPrint(line);
}
}

Future<File> createFile(String? logDir, String? logName) async {
logDir = logDir;
logName = logName;
if (logDir == null) {
Directory documentsDirectory = await getApplicationSupportDirectory();
logDir =
"${documentsDirectory.path}${Platform.pathSeparator}${Constants.logDir}";
}
logName ??=
"${CommonUtils.formatDateTime(DateTime.now(), format: 'yyyy-MM-dd')}.txt";

String path = '$logDir${Platform.pathSeparator}$logName';
debugPrint('>>>>日志存储路径:$path');
File file = File(path);
if (!file.existsSync()) {
file = await File(path).create(recursive: true);
}
return file;
}

吐司提示


吐司用的还是fluttertoast的方式。但是windows的实现比较不一样,在windows上的实现toast提示只能显示在应用窗体内。


static FToast fToast = FToast().init(Get.overlayContext!);

static void showToast(String text, {int? timeInSeconds}) {
// 桌面版必须使用带context的FToast
if (Platform.isWindows || Platform.isMacOS) {
cancelToastForDesktop();
fToast.showToast(
toastDuration: Duration(seconds: timeInSeconds ?? 3),
gravity: ToastGravity.BOTTOM,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25.0),
color: const Color(0xff323334),
),
child: Text(
text,
style: const TextStyle(
fontSize: 16,
color: Colors.white,
),
),
),
);
} else {
cancelToast();
Fluttertoast.showToast(
msg: text,
gravity: ToastGravity.BOTTOM,
timeInSecForIosWeb: timeInSeconds ?? 3,
backgroundColor: const Color(0xff323334),
textColor: Colors.white,
fontSize: 16,
);
}
}

一些的小技巧


代码注入,更简洁的实现单例和构造引用


在开发过程中,我还会使用get_itinjectable来生成自动单例、工厂构造函数等类。好处是让代码更为简洁可靠,便于维护。下面举个萌友上报的例子,初始配置只需要在create中写入即可,然后业务方调用只需要使用GetIt.get<YouMengReport>().report()上报就行了。这就是一个非常完整的单例,使用维护都很方便。


/// 声明单例,并且自动初始化
@singleton(signalsReady: true)
class YouMengReport {
/// 声明工厂构造函数,自动初始化的时候会自动自行create方法
@factoryMethod
create() {
// 这里可以做一些初始化工作
}
report() {}
}

json生成器


由于不支持反射,导致Flutter的json解析一直为人诟病。因此使用json_serializable会是一个不错的选择,其原理是通过AOP注解,帮我们生成json编码和解析。通过插件Json2json_serializable可以帮我们自动生成dart文件,如下图:
image.png


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

Dagger2四种使用方式

1. 什么是Dagger2 Dagger2是用来解决对象之间的高度耦合的框架。介绍Dagger2四种方式实现。 具体的四种使用场景 0. 配置 app模块下的build.gradle dependencies { //...其他依赖信息 im...
继续阅读 »

1. 什么是Dagger2


Dagger2是用来解决对象之间的高度耦合的框架。介绍Dagger2四种方式实现。


image.png


具体的四种使用场景


0. 配置


app模块下的build.gradle
dependencies {
//...其他依赖信息
implementation 'com.google.dagger:dagger:2.44'
annotationProcessor 'com.google.dagger:dagger-compiler:2.44'
}

1. 第一种实现方式


自己实现的代码可以在代码的构造函数上通过@Inject修饰,实现代码注入,如下:


image.png


1. 实现细节


public class SingleInstance {

@Inject
User user;

@Inject
public SingleInstance() {
DaggerApplicationComponent.create().inject(this);
Log.i("TAG", "SingleInstance === " + user);
}
}


public class User {
//自定义的类通过@Inject注解修饰,在使用的地方使用@Inject初始化时,dagger会去找被@Inject修饰的类进行初始化。
@Inject
public User() {

}
}


@Component
public interface ApplicationComponent {
//指的将对象注入到那个地方,这里指的注入到MainActivity
void inject(MainActivity activity);
//指定将对象注入到什么位置,这里值注入到SingleInstance中
void inject(SingleInstance activity);
}


//TODO 被注入的类
public class MainActivity extends AppCompatActivity {
@Inject
SingleInstance instance;
@Inject
User user;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
DaggerApplicationComponent.create().inject(this);
Log.i("TAG", "instance == " + instance);
Log.i("TAG", "user == " + user);
}
}

2. 小结


  1. 在需要注入的地方通过@Inject注解进行标记。

  2. @Component修饰的是一个接口,并在该接口中提供一个将对象注入到哪里的方法。示例中为注入MainActivitySingleInstance两个类中。

  3. 需要在自定义类的构造函数通过@Inject注解进行修饰。

  4. 关键一步是将Component注入到目标类中,在需要实现注入的地方调用由@Component修饰的接口生成的对应的类。名字规则为Dagger+被@Component修饰的接口名代码为DaggerApplicationComponent.create().inject(this);


2. 第二种实现方式


第三方库可以Module+主Component的方式实现。该种方式解决对第三方库的初始化。


image.png


@Module
public class HttpModule {

//TODO 在module提供实例
@NetScope
@Provides
String providerUrl() {
return "http://www.baidu.com";
}

@NetScope
@Provides
GsonConverterFactory providerGsonConverterFactory() {
return GsonConverterFactory.create();
}

@NetScope
@Provides
RxJava2CallAdapterFactory providerRxjava2CallAdapterFactory() {
return RxJava2CallAdapterFactory.create();
}

/**
* 1. 这里可以通过作用域限制实例使用的范围,这里的作用域必须和自己的Component使用的一样。
* 2. 一个Component只能有一个作用域修饰符。
*
* @return
*/
@NetScope
@Provides
OkHttpClient providerOkHttpClient() {
return new OkHttpClient.Builder()
//TODO 这里可以添加各种拦截器
.build();
}

@NetScope
@Provides
Retrofit providerRetrofit(String url,
OkHttpClient okHttpClient,
GsonConverterFactory gsonConverterFactory,
RxJava2CallAdapterFactory rxJava2CallAdapterFactory) {
return new Retrofit.Builder()
.baseUrl(url)
.client(okHttpClient)
.addConverterFactory(gsonConverterFactory)
.addCallAdapterFactory(rxJava2CallAdapterFactory)
.build();
}

@NetScope
@Provides
ApiService providerApiService(Retrofit retrofit) {
return retrofit.create(ApiService.class);
}
}



@Module
public class DaoModule {
@Name01
@Provides
Student providerStudent01() {
return new Student("tom01");
}

@Name02
@Provides
Student providerStudent02() {
return new Student("tom02", 20);
}

@Named("threeParam")
@Provides
Student providerStudent03() {
return new Student("tom03", 20, 1);
}

}


//NetScope这里的直接是为了限制其作用域
@NetScope
@Component(modules = {HttpModule.class,DaoModule.class})//装载多个Module
public interface HttpComponent {
void inject(MainActivity activity);
}

//被注入的类
public class MainActivity extends AppCompatActivity {


@Inject
ApiService apiService;

@Inject
ApiService apiService2;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
DaggerHttpComponent.builder()
.build()
.inject(this);
Log.i(" TAG", "apiService === " + apiService);
Log.i(" TAG", "apiService2 === " + apiService2);

}
}


小结


  1. 通过Module提供了创建实例的方法。这里的@NetScope用于限制了创建实例的作用域。

  2. providerRetrofit方法中的参数是由其他几个方法提供的,如果没有提供@Providers修饰的方法提供实例外,Dagger2会去找被@Inject修饰的构造方法创建实例,如果都没有提供方法参数的实例则会报错。

  3. 如果相同的类型创建不同的对象可以使用@Named注解解决.

  4. @NetScope注解用来使用限定作用域。


3. 第三种实现方式


通过多组件实现相互依赖并提供实例。


image.png


//提供了全局的组件
@Component(modules = UserModule.class)
public interface ApplicationComponent {
User createUser();//通过这种方式需要将UserModule中的参数暴露出来,需要提供要暴露出来的相关方法。
// HttpComponent createHttpComponent();
}

//提供基础的实例
@BaseHttpScope
@Component(modules = BaseHttpModule.class, dependencies = ApplicationComponent.class)
public interface BaseHttpComponent {

//TODO 在其他子组件需要依赖时,需要将对应的方法暴露出来
Retrofit providerRetrofit();
}



//这里依赖了BaseHttpComponent组件中提供的实例
@NetScope
@Component(modules = ApiServiceModule.class, dependencies = BaseHttpComponent.class)
public interface ApiServiceComponent {
void inject(MainActivity activity);
}


//具体的实现注入的地方的初始化
public class MainActivity extends AppCompatActivity {

@Inject
ApiService apiService;

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

DaggerApiServiceComponent.builder()
.baseHttpComponent(DaggerBaseHttpComponent.builder()
.applicationComponent(DaggerApplicationComponent.create())
.build()).build()
.inject(this);
Log.i("TAG", "apiService == " + apiService);

}
}

小结



  1. 这里需要注意的是ApiServiceComponent组件依赖BaseHttpComponent组件,BaseHttpComponent组件以来的是ApplicationComponent组件,结果就是在注入的地方如


 DaggerApiServiceComponent.builder()
.baseHttpComponent(DaggerBaseHttpComponent.builder()
.applicationComponent(DaggerApplicationComponent.create())
.build()).build()
.inject(this);


  1. 需要被子组件使用的实例需要在XXComponent中暴露出来,如果没有暴露出来会去找被@Inject修饰的构造方法创建实例,如果没有找到则会报错。不能提供对应的实例。


4. 第四种实现方式


跟上面多个Component提供创建实例时,如果在子组件中需要使用父组件中提供的实例,父组件需要手动暴露出提供对应实例的方法。


image.png



@Module
public class ApiServiceModule {
//TODO 在module提供实例
@NetScope
@Provides
String providerUrl() {
return "http://www.baidu.com";
}

@NetScope
@Provides
GsonConverterFactory providerGsonConverterFactory() {
return GsonConverterFactory.create();
}

@NetScope
@Provides
RxJava2CallAdapterFactory providerRxjava2CallAdapterFactory() {
return RxJava2CallAdapterFactory.create();
}

/**
* 1. 这里可以通过作用域限制实例使用的范围,这里的作用域必须和自己的Component使用的一样。
* 2. 一个Component只能有一个作用域修饰符。
*
* @return
*/
@NetScope
@Provides
OkHttpClient providerOkHttpClient() {
return new OkHttpClient.Builder()
//TODO 这里可以添加各种拦截器
.build();
}

@NetScope
@Provides
Retrofit providerRetrofit(String url,
OkHttpClient okHttpClient,
GsonConverterFactory gsonConverterFactory,
RxJava2CallAdapterFactory rxJava2CallAdapterFactory) {
return new Retrofit.Builder()
.baseUrl(url)
.client(okHttpClient)
.addConverterFactory(gsonConverterFactory)
.addCallAdapterFactory(rxJava2CallAdapterFactory)
.build();
}

@NetScope
@Provides
ApiService providerApiService(Retrofit retrofit) {
return retrofit.create(ApiService.class);
}
}

@NetScope
@Subcomponent(modules = ApiServiceModule.class)
public interface ApiServiceComponent {
@Subcomponent.Factory
interface Factory {
ApiServiceComponent create();
}

void inject(MainActivity activity);
}

@Module(subcomponents = ApiServiceComponent.class)
public class ApiServiceComponentModule {

}


@Component(modules = {ApiServiceComponentModule.class})
public interface ApplicationComponent {
//这里在主组件中需要把子组件暴露出来
ApiServiceComponent.Factory createApiServiceComponent();
}


public class MainActivity extends AppCompatActivity {

@Inject
ApiService apiService;

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

DaggerApplicationComponent.create()
.createApiServiceComponent()
.create()
.inject(this);
Log.i("TAG", "apiService == " + apiService);
}
}

小结



  1. 这里需要注意的一点是SubComponent装载到主组件中时需要使用一个Module链接。


其他几个使用方法Binds和Lazy 参考demo app05



@Module
public abstract class DaoModule {
/**
* 这里的参数类型决定了调用哪一个实现方法
*
* @param impl01
* @return
*/
@Binds
public abstract BInterface bindBInterface01(Impl01 impl01);


@Provides
static Impl01 providerBInterface01() {
return new Impl01();
}

@Provides
static Impl02 providerBInterface02() {
return new Impl02();
}

}

public class MainActivity extends AppCompatActivity {

@Inject
BInterface impl01;

@Inject
Lazy<BInterface> impl02;

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

Log.i("TAG", "impl01 === " + impl01.getClass().getSimpleName());

Log.i("TAG", "impl02 === " + impl02.getClass().getSimpleName());

Log.i("TAG", "impl02 impl02.get()=== " + impl02.get().getClass().getSimpleName());
}
}


参考资料


demo地址


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

Android 13这些权限废弃,你的应用受影响了吗?

无论是更改个人头像、分享照片、还是在电子邮件中添加附件,选择和分享媒体文件是用户最常见的操作之一。在听取了 Android 用户反馈之后,我们对应用程序访问媒体文件的方式做了一些改变。 Android 13 已被废弃的权限 许多用户告诉我们,文件和媒体权限让他...
继续阅读 »

无论是更改个人头像、分享照片、还是在电子邮件中添加附件,选择和分享媒体文件是用户最常见的操作之一。在听取了 Android 用户反馈之后,我们对应用程序访问媒体文件的方式做了一些改变。


Android 13 已被废弃的权限


许多用户告诉我们,文件和媒体权限让他们很困扰,因为他们不知道应用程序想要访问哪些文件。


在 Android 13 上废弃了 READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE 权限,用更好的文件访问方式代替这些废弃的 API。


从 Android 10 开始向共享存储中添加文件不需要任何权限。因此,如果你的 App 只在共享存储中添加文件,你可以停止在 Android 10+ 上申请任何权限。


在之前的系统版本中 App 需要申请 READ_EXTERNAL_STORAGE 权限访问设备的文件和媒体,然后选择自己的媒体选择器,这为开发者增加了开发和维护成本,另外 App 依赖于通过 ACTION_GET_CONTENT 或者 ACTION_OPEN_CONTENT 的系统文件选择器,但是我们从开发者那里了解到,它感觉没有很好地集成到他们的 App 中。


System File Picker using ACTION_OPEN_CONTENT


图片选择器


在 Android 13 中,我们引入了一个新的媒体工具 Android 照片选择器。该工具为用户提供了一种选择媒体文件的方法,而不需要授予对其整个媒体库的访问权限。


它提供了一个简洁界面,展示照片和视频,按照日期排序。另外在 "Albums" 页面,用户可以按照屏幕截图或下载等等分类浏览,通过指定一些用户是否仅看到照片或视频,也可以设置选择最大文件数量,也可以根据自己的需求定制照片选择器。简而言之,这个照片选择器是为私人设计的,具有干净和简洁的 UI 易于实现。



我们还通过谷歌 Play 系统更新 (2022 年 5 月 1 日发布),将照片选择器反向移植到 Android 11 和 12 上,以将其带给更多的 Android 用户。


开发一个照片选择器是一个复杂的项目,新的照片选择器不需要团队进行任何维护。我们已经在 ActivityX 1.6.0 版本中为它创建了一个 ActivityResultContract。如果照片选择器在你的系统上可用,将会优先使用照片选择器。


// Registering Photo Picker activity launcher with a max limit of 5 items
val pickMultipleVisualMedia = registerForActivityResult(PickMultipleVisualMedia(5)) { uris ->
// TODO: process URIs
}
// Launching the photo picker (photos & video included)
pickMultipleVisualMedia.launch(PickVisualMediaRequest(PickVisualMedia.ImageAndVideo))

如果希望添加类型进行筛选,可以采用这种方式。


// Launching the photo picker (photos only)
pickMultipleVisualMedia.launch(PickVisualMediaRequest(PickVisualMedia.ImageOnly))
// Launching the photo picker (video only)
pickMultipleVisualMedia.launch(PickVisualMediaRequest(PickVisualMedia.VideoOnly))
// Launching the photo picker (GIF only)
pickMultipleVisualMedia.launch(PickVisualMediaRequest(PickVisualMedia.SingleMimeType("image/gif")))

可以调用 isPhotoPickerAvailable 方法来验证在当前设备上照片选择器是否可用。


ACTION_GET_CONTENT 将会发生改变


正如你所见,使用新的照片选择器只需要几行代码。虽然我们希望所有的 Apps 都使用它,但在 App 中迁移可能需要一些时间。


这就是为什么我们使用 ACTION_GET_CONTENT 将系统文件选择器转换为照片选择器,而不需要进行任何代码更改,从而将新的照片选择器引入到现有的 App 中。



针对特定场景的新权限


虽然我们强烈建议您使用新的照片选择器,而不是访问所有媒体文件,但是您的 App 可能有一个场景,需要访问所有媒体文件(例如图库照片备份)。对于这些特定的场景,我们将引入新的权限,以提供对特定类型的媒体文件的访问,包括图像、视频或音频。您可以在文档中阅读更多关于它们的内容。


如果用户之前授予你的应用程序 READ_EXTERNAL_STORAGE 权限,系统会自动授予你的 App 访问权限。否则,当你的 App 请求任何新的权限时,系统会显示一个面向用户的对话框。


所以您必须始终检查是否仍然授予了权限,而不是存储它们的授予状态。



下面的决策树可以帮助您更好的浏览这些更改。



我们承诺在保护用户隐私的同时,继续改进照片选择器和整体存储开发者体验,以创建一个安全透明的 Android 生态系统。


新的照片选择器被反向移植到所有 Android 11 和 12 设备,不包括 Android Go 和非 gms 设备。



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

一些有用的技巧帮助你开发 flutter

前言 你好今天给你带来了些有用的建议,让我们开始吧。 正文 1. ElevatedButton.styleFrom 快速样式 你是否厌倦了 container 里那些乏味的 decorations ,想要轻松实现这个美丽的按钮别担心,我给你准备了一些魔法密码...
继续阅读 »

前言


你好今天给你带来了些有用的建议,让我们开始吧。



正文


1. ElevatedButton.styleFrom 快速样式


你是否厌倦了 container 里那些乏味的 decorations ,想要轻松实现这个美丽的按钮别担心,我给你准备了一些魔法密码。



示例代码


SizedBox(
height: 45,
width: 200,
child: ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
elevation: 10, shape: const StadiumBorder()),
child: const Center(child: Text('Elevated Button')),
),
),


示例代码


SizedBox(
height: 45,
width: 60,
child: ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
elevation: 10, shape: const CircleBorder()),
child: const Center(child: Icon(Icons.add)),
),
),

2. TextInputAction.next 焦点切换


你们都知道“焦点节点”。这基本上是用来识别 Flutter 的“焦点树”中特定的 TextField 这允许您在接下来的步骤中将焦点放在 TextField 上。但你知道 Flutter 提供了一个神奇的一行代码同样..。


示例代码


Padding(
padding: const EdgeInsets.all(30.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextFormField(
textInputAction: TextInputAction.next,
),
const SizedBox(
height: 50,
),
TextFormField(
textInputAction: TextInputAction.next,
),
const SizedBox(
height: 50,
),
TextFormField(
textInputAction: TextInputAction.done,
),
const SizedBox(
height: 50,
),
],
),
);

3. 设置 status Bar 状态栏颜色


你的状态栏颜色破坏了你的页面外观吗? 让我们改变它..。



示例代码


void main() {
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
statusBarColor: Colors.transparent, // transparent status bar
));
runApp(const MyApp());
}

4.设置 TextStyle.height 段落间距


如果您在页面上显示了一个段落(例如: 产品描述、关于我们的内容等) ,并且它看起来不如 xd 设计那么好!使用这个神奇的代码,使它有吸引力和顺利。



示例代码


 Text(
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
style: TextStyle(
fontSize: 17.0,
height: 1.8,
),
)

5. 设置文字 3D


想让你的“标题”文字更有吸引力吗? 给它一个有阴影的 3D 效果..。



示例代码


Center(
child: Text(
'Hello, world!',
style: TextStyle(
fontSize: 50,
color: Colors.pink,
fontWeight: FontWeight.w900,
shadows: <Shadow>[
const Shadow(
offset: Offset(4.0, 4.0),
blurRadius: 3.0,
color: Color.fromARGB(99, 64, 64, 64),
),
Shadow(
offset: const Offset(1.0, 1.0),
blurRadius: 8.0,
color: Colors.grey.shade100),
],
),
),
)

6. vscode 插件 Pubspec Assist


你知道 Flutter extensions 吗?你当然是! !我正在分享我最喜欢的 Flutter extensions..。



Pubspec Assist 是一个 VisualStudio 代码扩展,它允许您轻松地向 Dart 和 Flutter 项目的 Pubspec 添加依赖项。Yaml 不需要你编辑。你必须试试。


7. 应用 app 尺寸控制


Application 大小很重要!应用程序大小的 Flutter 应用程序是非常重要的。当它是一个更大的应用程序时,尺寸变得更加重要,因为你需要在设备上有更多的空间。更大的应用程序下载时间也更长。它扩大了 Flutter 应用程序,可以是两个、三个或更多的安装尺寸。因此,在 Android 平台上减小 Flutter 应用程序的大小是非常重要的。


这里有一些减小 Flutter 应用程序大小的技巧 ~ ~ ~



  1. 减小应用程序元素的大小

  2. 压缩所有 JPEG 和 PNG 文件

  3. 使用谷歌字体

  4. 在 Android Studio 中使用 Analyzer

  5. 使用命令行中的分析 Analyzer

  6. 减少资源数量和规模

  7. 使用特定的 Libraries


谢谢你的阅读...


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

哈啰 Quark Design 正式开源,下一代跨技术栈前端组件库

官网:quark-design.hellobike.comQuark(夸克) Design 是由哈啰平台 UED 和增长&电商前端团队联合打造的一套面向移动端的跨框架 UI 组件库。与业界第三方组件库不一样,Quark Design 底层基于 Web ...
继续阅读 »

Quark Design 是什么?

官网:quark-design.hellobike.com

github:github.com/hellof2e/qu…

Quark(夸克) Design 是由哈啰平台 UED 和增长&电商前端团队联合打造的一套面向移动端的跨框架 UI 组件库。与业界第三方组件库不一样,Quark Design 底层基于 Web Components 实现,它能做到一套代码,同时运行在各类前端框架中。

Quark Design 历经一年多的开发时间,已在集团内部大量业务中得到验证,本着“共创、共建、共享”的开源精神,我们于即日起将 Quark 正式对外开源!Github地址:github.com/hellof2e/qu… (求star、求关注~😁)


注:文档表现/样式参考了HeadlessUI/nutui/vant等。

Quark Design 与现有主流组件库的区别是什么?

Quark(夸克)有别于业界主流的移动端组件库,Quark 能同时运行在业界所有前端框架/无框架工程中,做到真正的技术栈无关 !我们不一样,:)

  • 不依赖技术栈(eg. Vue、React、Angular等)

  • 不依赖技术栈版本(eg. Vue2.x、Vue3.x)

  • 全新的Api设计(eg. 弹窗的打开属性由传统的 Visible 调整为符合浏览器原生弹窗的 open等)

  • 公司前端技术生态项目技术栈多时,保持视觉/交互统一

  • 完全覆盖您所需要的各类通用组件

  • 支持按需引用

  • 详尽的文档和示例

  • 支持定制主题

性能优势-优先逻辑无阻塞

我们以对 React 组件的 Web Components 化为例,一个普通的 React 组件在初次执行时需要一次性走完所有必须的节点逻辑,而这些逻辑的执行都同步占用在 js 的主线程上,那么当你的页面足够复杂时,一些非核心逻辑就将会阻塞后面的核心逻辑的执行。

比如首次加载时,你的页面中有一个复杂的交互组件,交互组件中又包含 N多逻辑和按钮等小组件,此时页面的首次加载不应该优先去执行这些细节逻辑,而首要任务应当是优先渲染出整体框架或核心要素,而后再次去完善那些不必要第一时间完成的细节功能。 例如一些图像处理非常复杂,但你完全没必要在第一时间就去加载它们。

当我们使用 Web Components 来优化 React的时候,这个执行过程将会变得简洁的多,比如我们注册了一个复杂的逻辑组件,在 React 执行时只是执行了一个 createElement 语句,创建它只需要 1-2 微秒即可完成,而真正的逻辑并不在同时执行,而是等到“核心任务”执行完再去执行,甚至你可以允许它在合适的时机再去执行。

我们也可以简单的理解为,部分逻辑在之后进行执行然后被 render 到指定 id 的 Div 中的,那么为什么传统的组件为什么不能这么做呢?而非得 Web Components 呢?那就不得不提到它所包含的另一个技术特性:Shadow DOM


组件隔离(Shadow Dom)

Shadow DOM 为自定义的组件提供了包括 CSS、事件的有效隔离,不再担心不同的组件之间的样式、事件污染了。 这相当于为自定义组件提供了一个天然有效的保护伞。

Shadow DOM 实际上是一个独立的子 DOM Tree,通过有限的接口和外部发生作用。 我们都知道页面中的 DOM 节点数越多,运行时性能将会越差,这是因为 DOM 节点的相互作用会时常在触发重绘(Repaint)和重排(reflow)时会关联计算大量 Frame 关系。


而对 CSS 的隔离也将加快选择器的匹配速度,即便可能是微秒级的提升,但是在极端的性能情况下,依然是有效的手段。

Quark 能为你带来什么?

提效降本几乎是所有企业的主旋律,Quark 本身除了提供了通用组件之外,我们还为大家提供了开箱即用的 CLI,可以让大家在直接在日常开发中开发横跨多个技术栈/框架的业务组件。比如一个相同样式的营销弹窗,可以做到:

  • 同时运行在不同技术栈(Angular、Vue、React等)的前端工程中

  • 同时运行在不同版本的技术栈中,比如能同时运行在 Vue2.x、Vue3.x 中

CLI 内部 Beta 版本目前初版已完成,github 地址:github.com/hellof2e/qu…

适合场景:前端团队想发布一个独立的组件或npm包,让其他各类技术栈的工程使用,从而达到提效降本的目的。

npm i -g @quarkd/quark-cli
npx create-quark


相关链接

作者:Allan91
来源:juejin.cn/post/7160483409691672606

收起阅读 »

一个瞬间让你的代码量暴增的脚本

web
在某些特殊情况下,需要凑齐一定的代码量,或者一定的提交次数,为了应急不得不采用一些非常规的手段来保证达标。本文分享的是一段自动提交代码的脚本,用于凑齐code review流程数量,将单次code review代码修改行数拉下来(备注:如果git开启自动生成c...
继续阅读 »

1 功能概述

在某些特殊情况下,需要凑齐一定的代码量,或者一定的提交次数,为了应急不得不采用一些非常规的手段来保证达标。本文分享的是一段自动提交代码的脚本,用于凑齐code review流程数量,将单次code review代码修改行数拉下来(备注:如果git开启自动生成code review流程,则每次push操作就会自动生成一次code review流程)。

2 友情提示

本脚本仅用于特殊应急场景下,平时开发中还是老老实实敲代码。

重要的事情说三遍:

千万不要在工作中使用、千万不要在工作中使用、千万不要在工作中使用

3 实现思路

3.1 准备示例代码

可以多准备一些样例代码,然后随机取用, 效果会更好。例如:

需要确保示例代码是有效的代码,有些项目可能有eslint检查,如果格式不对可能导致无法自动提交

function huisu(value, index, len, arr, current) {
 if (index >= len) {
     if (value === 8) {
         console.log('suu', current)
    }
     console.log('suu', current)
     return
}
 for (let i = index; i < len; i++) {
     current.push(arr[i])
     console.log('suu', current)
     if (value + arr[i] === 8) {
         console.log('结果', current)
         return
    }
     huisu(value + arr[i], i + 1, len, arr, [...current])
     console.log('suu', value)
     current.pop()
     onsole.log('suu', current)
}
}

3.2、准备一堆文件名

准备一堆文件名,用于生成新的问题,如果想偷懒,直接随机生成问题也不大。例如:

// 实现准备好的文件名称,随机也可以
const JS_NAMES = ['index.js', 'main.js', 'code.js', 'app.js', 'visitor.js', 'detail.js', 'warning.js', 'product.js', 'comment.js', 'awenk.js', 'test.js'];

3.3 生成待提交的文件

这一步策略也很简单,就是根据指定代码输出文件夹内已有的文件数量,来决定是要执行新增文件还是删除文件

if (codeFiles.length > MIN_COUNT) {
 rmFile(codeFiles);
} else {
 createFile(codeDir);
}

【新增文件】

根据前面两步准备的示例代码和文件命名,随机获取文件名和代码段,然后创建新文件

// 创建新的代码文件
function createFile(codeDir) {
 const ran = Math.floor(Math.random() * JS_NAMES.length);
 const name = JS_NAMES[ran];
 const filePath = `${codeDir}/${name}`;
 const content = getCode();
 writeFile(filePath, content);
}

【删除文件】

这一步比较简单,直接随机删除一个就行了

// 随机删除一个文件
function rmFile(codeFiles) {
 const ran = Math.floor(Math.random() * codeFiles.length);
 const filePath = codeFiles[ran];
 try {
   if (fs.existsSync(filePath)) {
     fs.unlinkSync(filePath);
  }
} catch (e) {
   console.error('removeFile', e);
}
}

3.4 准备commit信息

这一步怎么简单怎么来,直接准备一堆,然后随机取一个就可以了

const msgs = ['feat:消息处理', 'feat:详情修改', 'fix: 交互优化', 'feat:新增渠道', 'config修改'];
const ran = Math.floor(Math.random() * msgs.length);
console.log(`${msgs[ran]}--测试提交,请直接通过`);

3.5 扩大增幅

上述步骤执行一次可能不太够,咱们可以循环多来几次。随机生成一个数字,用来控制循环的次数

const ran = Math.max(3, parseInt(Math.random() * 10, 10));
console.log(ran);

3.6 组合脚本

组合上述步骤,利用shell脚本执行git提交,详细代码如下:

#! /bin/bash

git pull

cd $(dirname $0)

# 执行次数
count=$(node ./commit/ran.js)
echo $count

# 循环执行
for((i=0;i<$count;i++))
do
node ./commit/code.js
git add .

msg=$(node ./commit/msg.js)
git commit -m "$msg"

git push
done

总结

总的来就就是利用shell脚本执行git命令,随机生成代码或者删除代码之后执行commit提交,最后push推送到远程服务器。

源码

欢迎有需要的朋友取用,《源码传送门》

作者:先秦剑仙
来源:juejin.cn/post/7160649931928109092

收起阅读 »

程序员转行做运营,降薪降得心甘情愿

自2019年末新冠疫情爆发以来,近三年的就业形势一直不太乐观,大厂裁员的消息接踵而至。身边的朋友都在感慨:现阶段能保住工作就不错了,新工作就算了。但,就是在这样严峻的大环境下,我的前同事不三不仅跳槽还转岗,1年的转行之路,经受了各种磨难。通过小摹的热情邀请,和...
继续阅读 »

自2019年末新冠疫情爆发以来,近三年的就业形势一直不太乐观,大厂裁员的消息接踵而至。身边的朋友都在感慨:现阶段能保住工作就不错了,新工作就算了。

但,就是在这样严峻的大环境下,我的前同事不三不仅跳槽还转岗,1年的转行之路,经受了各种磨难。通过小摹的热情邀请,和不三聊了聊程序员转运营过程中的经验与心得。

小摹把这份干货分享出来,希望能为每一位即将转行的伙伴提供动力支撑,也能给其他岗位的朋友新增一些不同视角的思考。

试用期差点被劝退

小摹:从事前端四年,是什么让你下定决心转行?

不三:后续有创业的打算,所以希望自己在了解产品研发的基础上,也多了解一下市场,为自己创业做准备吧。

小摹:你做的是哪方面的运营呢?这一年的感触如何?

不三:运营岗位细分很多:新媒体运营、产品运营、用户运营、活动运营、市场推广等,我所从事的是内容运营和用户运营。

公司是SaaS通信云服务提供商,对于之前从未接触过这方面工作的我而言,门槛比较高。为了能尽快熟悉产品业务,也能让我更了解用户,为后续用户运营和内容运营打基础,领导安排我前期先接触和客户相关的工作。

我试用期大部分的工作都涉及到和用户打交道,他们总会反馈给我们各种产品的需求和bug,我基本都冲在第一线安抚用户。Bug提交给开发后或许还能尽快修复,而需求反馈过去后,只能等到那句再熟悉不过的话“等排期吧”。


刚做运营的前三个月,提给开发的需求大多都被驳回了,要么做出来的东西无法达到预期。那段时间,每天上班心态濒临崩溃,颇有打道回府之意。

转正之前,领导找我谈了一次话,让我醍醐灌顶:

运营身为提需求大户,你连需求都没规划好,想一出是一出,产品开发为啥会帮你做?

你之前是前端,设身处地的想,是不是非常反感产品或运营给你提莫名其妙的需求?不注重用户体验、忽略了产品的长远发展,即便当下你的KPI完成了,你有获得真正的成长,产品有迭代得更好吗?

在和领导沟通的过程中慢慢意识到,我把自己的位置摆错了,即使运营是结果驱动,但我直面用户,所以我必须要学会洞察用户的心理,重视产品的长远发展,这样才能让我有所进度。

跟领导聊完之后,我便开始调整了工作状态和节奏,明白了自己的不足,接下来就是有目标、有计划的解决问题。

回到岗位后,我梳理了公司的业务方向,写好MRD(市场需求报告),重新制定了我的运营策略,提交给了领导。

三天后,人事找到我:我通过了试用期,成功转正了。


我很感谢我的领导,尽管试用期我做得很烂,但他仍然愿意给我机会,让我转正,继续工作。现在回过头看这一年,试用期阶段很痛苦,找不到工作的方向,但后来越来越熟悉了解后,也能更快上手了。

小摹:你认为一名优秀的运营要具备什么样的特质?

不三:现在的我只能说刚刚入门,我发现身边的运营大佬身上有以下特点,我希望自己能尽快向他靠拢。

  • 用户体感:所有的产品研发出来后,面向对象一定是用户,那么产品的使用体验、页面设计、活动机制、规则设定是否都能满足用户的胃口。

如果只是冲着所谓的KPI目标,而忽略了用户体验,或许你会收获万人骂的情况。

例如,随时随地朋友圈砍一刀的拼夕夕。

  • 把控热点能力:无论做什么方向的运营,都逃不了蹭热点,你可以说蹭热点low,但不可否认它会给自己和产品带来新机遇。

例如,写一篇文章蹭了热点之后,爆的几率更大;疫情刚出现时,异地办公、社区团购也随之应运而生。

  • 产品思维:互联网运营和产品经理的联系是非常紧密的,所以在推广的过程中,需要和产品部门多多碰撞。这样不仅能收获创意灵感,还能学到不少的产品思维。

在需求迭代时,应该站在更高的层次思考问题,一味给产品做加法,根本行不通。

  • 数据思维:运营以结果为导向,从数据中发现不足,从数据中发现增长点,弥补缺陷,让增长幅度更大。程序员比较有优势,可以写SQL导数据,但拿到数据只是第一步,还要懂得分析才行。

  • 抵御公关风险:例如我们在做活动时,我们要提前考虑活动的风险有哪些,如何积极应对,当有别有用心的人利用规则薅羊毛时,也应该有相应的解决方案。


这段简单且干货的采访随着烧烤啤酒的上桌步入了尾声。最后不三给我说到:

一年前我调整了自己的职业方向,从前端步入运营,苦涩欢笑并存,有时看着达到目标很是激动,有时苦于KPI的折磨。一年间,我经历了人生的成长,思想也更加成熟。但我还没有达到最终目的地,现在的一切只是为了以后的创业蓄力。我不想一辈子为别人打工,也想为自己活一次。


===

后记

小摹见过太多转行失败的案例,所以很为不三感到高兴,不仅仅是为他的转行成功,更多的是他坚定人生的方向,并为之做出了各种努力而高兴。

给大家分享这段采访经历,是希望大家能尽早对自己的职业生涯有所规划,有了目标后,再细分到某一阶段,这样工作起来积极性也会更高。停止摆烂,对自己负责!

人生之难,一山又一山,愿你我共赴远山。

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

收起阅读 »

入坑两个月自研创业公司

一、拿 offer 其实入职前,我就感觉到有点不对劲,居然要自带电脑。而且人事是周六打电话发的 offer!自己多年的工作经验,讲道理不应该入这种坑,还是因为手里没粮心中慌,工作时间长的社会人,还是不要脱产考研、考公,疫情期间更是如此,本来预定 2 月公务员面...
继续阅读 »

一、拿 offer


其实入职前,我就感觉到有点不对劲,居然要自带电脑。而且人事是周六打电话发的 offer!自己多年的工作经验,讲道理不应该入这种坑,还是因为手里没粮心中慌,工作时间长的社会人,还是不要脱产考研、考公,疫情期间更是如此,本来预定 2 月公务员面试,结果一直拖到 7 月。


二、入职工作


刚入职工作时,一是有些抗拒,二呢是有些欣喜。抗拒是因为长时间呆家的惯性,以及人的惰性,我这只是呆家五个月,那些呆家一年两年的,再进入社会,真的很难,首先心理上他们就要克服自己的惰性和惯性,平时生活习惯也要发生改变


三、人言可畏


刚入职工作时,有工作几个月的老员工和我说,前公司的种种恶心人的操作,后面呢我也确实见识到了:无故扣绩效,让员工重新签署劳动协议,但是,也有很多不符实的,比如公司在搞幺蛾子的时候,居然传出来我被劝退了……


四、为什么离开


最主要的原因肯定还是因为发不出工资,打工是为了赚钱,你想白嫖我?现在公司规模也不算小了,想要缓过来,很难。即便缓过来,以后就不会出现这样的状况了?公司之前也出现过类似的状况,挺过来的老员工们我也没看到有什么优待,所以这家公司不值得我去熬。技术方面我也基本掌握了微信和支付宝小程序开发,后面不过是需求迭代。个人成长方面,虽然我现在是前端部门经理,但前端组跑的最快,可以预料后面我将面临无人可用的局面,我离职的第二天,又一名前端离职了,约等于光杆司令,没意义。


五、收获


1. 不要脱产,不要脱产
2. 使用 uniapp 进行微信和支付宝小程序开发
3. 工作离家近真的很爽
4. 作为技术人员,只要你的上司技术还行,你的工期他是能正常估算,有什么难点说出来,只要不是借口,他也能理解,同时,是借口他也能一下识别出来,比如,一个前端和我说:“后端需求不停调整,所以没做好。” 问他具体哪些调整要两个星期?他又说不出来。这个借口就不要用了,但是我也要走了,我也没必要去得罪他。
5. 进公司前,搞清楚公司目前是盈利还是靠融资活,靠融资活的创业公司有风险…


六、未来规划


关于下一份工作:
南京真是外包之城,找了两周只有外包能满足我目前 18k 的薪资,还有一家还降价了 500…
目前 offer 有
vivo 外包,20k
美的外包,17.5k
自研中小企业,18.5k


虽然美的外包薪资最低,但我可能还是偏向于美的外包。原因有以下几点:
1. 全球手机出货量下降,南京的华为外包被裁了不少,很难说以后 vivo 会不会也裁。
2. 美的目前是中国家电行业的龙头老大,遥遥领先第二名,目前在大力发展 b2c 业务,我进去做的也是和商场相关。
3. 美的的办公地点离我家更近些
4. 自研中小企业有上网限制,有过类似经验的开发人,懂得都懂,很难受。


关于考公:
每年 10 月到 12 月准备下,能进就进,不能再在考公上花费太多时间了。


作者:哇哦谢谢你
链接:https://juejin.cn/post/7160138475688165389

收起阅读 »

Android性能优化 -- 内存优化

内存,是Android应用的生命线,一旦在内存上出现问题,轻者内存泄漏,重者直接crash,因此一个应用保持健壮,内存这块的工作是持久战,而且从写代码这块就需要注意合理性,所以想要了解内存优化如何去做,要先从基础知识开始。 1 JVM内存原理 这一部分确实很枯...
继续阅读 »

内存,是Android应用的生命线,一旦在内存上出现问题,轻者内存泄漏,重者直接crash,因此一个应用保持健壮,内存这块的工作是持久战,而且从写代码这块就需要注意合理性,所以想要了解内存优化如何去做,要先从基础知识开始。


1 JVM内存原理


这一部分确实很枯燥,但是对于我们理解内存模型非常重要,这一块也是面试的常客


image.png


从上图中,我将JVM的内存模块分成了左右两大部分,左边属于共享区域(方法区、堆区),所有的线程都能够访问,但也会带来同步问题,这里就不细说了;右边属于私有区域,每个线程都有自己独立的区域。


1.1 方法执行流程


class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
execute()
}

private fun execute(){

val a = 2.5f
val b = 2.5f
val c = a + b

val method = Method()

val d = getD()
}

private fun getD(): Int {
return 0
}

}

class Method{
private var a:Int = 0
}

我们看到在MainActivity的onCreate方法中,执行了execute方法,因为当前是UI线程,每个线程都有一个Java虚拟机栈,从上图中可以看到,那么每执行一个方法,在Java虚拟机栈中都对应一个栈帧。


image.png


每次调用一个方法,都代表一个栈帧入栈,当onCreate方法执行完成之后,会执行execute方法,那么我们看下execute方法。


execute方法在Java虚拟机栈中代表一个栈帧,栈帧是由四部分组成:


(1)局部变量表:局部变量是声明在方法体内的,例如a,b,c,在方法执行完成之后,也会被回收;
(2)操作数栈:在任意方法中,涉及到变量之间运算等操作都是在操作数栈中进行;例如execute方法中:


val a = 2.5f

当执行这句代码时,首先会将 2.5f压入操作数栈,然后给a赋值,依次类推

(3)返回地址:例如在execute调用了getD方法,那么这个方法在执行到到return的时候就结束了,当一个方法结束之后,就要返回到该方法的被调用处,那么该方法就携带一个返回地址,告诉JVM给谁赋值,然后通过操作数栈给d赋值

(4)动态链接:在execute方法中,实例化了Method类,在这里,首先会给Method中的一些静态变量或者方法进行内存分配,这个过程可以理解为动态链接。


1.2 从单例模式了解对象生命周期


单例模式,可能是众多设计模式中,我们使用最频繁的一个,但是单例真是就这么简单吗,使用不慎就会造成内存泄漏!


interface IObserver {

fun send(msg:String)

}

class Observable : IObserver {

private val observers: MutableList<IObserver> by lazy {
mutableListOf()
}

fun register(observer: IObserver) {
observers.add(observer)
}

fun unregister(observer: IObserver) {
observers.remove(observer)
}

override fun send(msg: String) {
observers.forEach {
it.send(msg)
}
}

companion object {
val instance: Observable by lazy {
Observable()
}
}
}

这里是写了一个观察者,这个被观察者是一个单例,instance是存放在方法区中,而创建的Observable对象则是存在堆区,看下图


image.png


因为方法区属于常驻内存,那么其中的instance引用会一直跟堆区的Observable连接,导致这个单例对象会存在很长的时间


btnRegister.setOnClickListener {
Observable.instance.register(this)
}
btnSend.setOnClickListener {
Observable.instance.send("发送消息")
}

在MainActivity中,点击注册按钮,注意这里传入的值,是当前Activity,那么这个时候退出,会发生什么?我们先从profile工具里看一下,退出之后,有2个内存泄漏的地方,如果使用的leakcannary(后面会介绍)就应该会明白


image.png


那么在MainActivity中,哪个地方发生的了内存泄漏呢?我们紧跟一下看看GcRoot的引用,发现有这样一条引用链,MainActivity在一个list数组中,而且这个数组是Observable中的observers,而且是被instance持有,前面我们说到,instance的生命周期很长,所以当Activity准备被销毁时,发现被instance持有导致回收失败,发生了内存泄漏。


image.png


那么这种情况,我们该怎么处理呢?一般来说,有注册就有解注册,所以我们在封装的时候一定要注意单例中传入的参数


override fun onDestroy() {
super.onDestroy()
Observable.instance.unregister(this)
}

再次运行我们发现,已经不存在内存泄漏了


image.png


1.3 GcRoot


前面我们提到了,因为instance是Gcroot,导致其引用了observers,observers引用了MainActivity,MainActivity退出的时候没有被回收,那么什么样的对象能被看做是GcRoot呢?


(1)静态变量、常量:例如instance,其内存是在方法区的,在方法区一般存储的都是静态的常量或者变量,其生命周期非常长;

(2)局部变量表:在Java虚拟机栈的栈帧中,存在局部变量表,为什么局部变量表能作为gcroot,原因很简单,我们看下面这个方法


private fun execute() {

val a = 2.5f
val method = Method()
val d = getD()
}

a变量就是一个局部变量表中的成员,我们想一下,如果a不是gcroot,那么垃圾回收时就有可能被回收,那么这个方法还有什么意义呢?所以当这个方法执行完成之后,gcroot被回收,其引用也会被回收。


2 OOM


在之前我们简单介绍了内存泄漏的场景,那么内存泄漏一旦发生,就会导致OOM吗?其实并不是,内存泄漏一开始并不会导致OOM,而是逐渐累计的,当内存空间不足时,会造成卡顿、耗电等不良体验,最终就会导致OOM,app崩溃


那么什么情况下会导致OOM呢?

(1)Java堆内存不足

(2)没有连续的内存空间

(3)线程数超出限制


其实以上3种状况,前两种都有可能是内存泄漏导致的,所以如何避免内存泄漏,是我们内存优化的重点


2.1 leakcanary使用


首先在module中引入leakcanary的依赖,关于leakcanary的原理,之后会单独写一篇博客介绍,这里我们的主要工作是分析内存泄漏


debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'

配置依赖之后,重新运行项目,会看到一个leaks app,这个app就是用来监控内存泄漏的工具


image.png
那我们执行之前的应用,打开leaks看一下gcroot的引用,是不是跟我们在as的profiler中看到的是一样的


image.png


如果使用过leakcanary的伙伴们应该知道,leakcanary会生成一个hprof文件,那么通过MAT工具,可以分析这个hprof文件,查找内存泄漏的位置,下面的链接能够下载MAT工具
http://www.eclipse.org/mat/downloa…


2.2 内存泄漏的场景


1. 资源性的对象没有关闭


例如,我们在做一个相机模块,通过camera拿到了一帧图片,通常我们会将其转换为bitmap,在使用完成之后,如果没有将其回收,那么就会造成内存泄漏,具体使用完该怎么办呢?


if(bitmap != null){
bitmap?.recycle()
bitmap = null
}

调用bitmap的recycle方法,然后将bitmap置为null


2. 注册的对象没有注销


这种场景其实我们已经很常见了,在之前也提到过,就是注册跟反注册要成对出现,例如我们在注册广播接收器的时候,一定要记得,在Activity销毁的时候去解注册,具体使用方式就不做过多的赘述。


3. 类的静态变量持有大数据量对象


因为我们知道,类的静态变量是存储在方法区的,方法区空间有限而且生命周期长,如果持有大数据量对象,那么很难被gc回收,如果再次向方法区分配内存,会导致没有足够的空间分配,从而导致OOM


4. 单例造成的内存泄漏


这个我们在前面已经有一个详细的介绍,因为我们在使用单例的时候,经常会传入context或者activity对象,因为有上下文的存在,导致单例持有不能被销毁;


因此在传入context的时候,可以传入Application的context,那么单例就不会持有activity的上下文可以正常被回收;


如果不能传入Application的context,那么可以通过弱引用包装context,使用的时候从弱引用中取出,但这样会存在风险,因为弱引用可能随时被系统回收,如果在某个时刻必须要使用context,可能会带来额外的问题,因此根据不同的场景谨慎使用。


object ToastUtils {

private var context:Context? = null

fun setText(context: Context) {
this.context = context
Toast.makeText(context, "1111", Toast.LENGTH_SHORT).show()
}

}

我们看下上面的代码,ToastUtils是一个单例,我们在外边写了一个context:Context? 的引用,这种写法是非常危险的,因为ToastUtils会持有context的引用导致内存泄漏


object ToastUtils {

fun setText(context: Context) {
Toast.makeText(context, "1111", Toast.LENGTH_SHORT).show()
}

}

5. 非静态内部类的静态实例


我们先了解下什么是静态内部类和非静态内部类,首先只有内部类才能设置为静态类,例如


class MainActivity : AppCompatActivity() {

private var a = 10

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main2)
}

inner class InnerClass {
fun setA(code: Int) {
a = code
}
}
}

InnerClass是一个非静态内部类,那么在MainActivity声明了一个变量a,其实InnerClass是能够拿到这个变量,也就是说,非静态内部类其实是对外部类有一个隐式持有,那么它的静态实例对象是存储在方法区,而且该对象持有MainActivity的引用,导致退出时无法被释放。


解决方式就是:将InnerClass设置为静态类


class InnerClass {

fun setA(code: Int) {
a = code //这里就无法使用外部类的对象或者方法
}
}

大家如果对于kotlin不熟悉的话,就简单介绍一下,inner class在java中就是非静态的内部类;而直接用class修饰,那么就相当于Java中的 public static 静态内部类。


6. Handler


这个可就是老生常谈了,如果使用过Handler的话都知道,它非常容易产生内存泄漏,具体的原理就不说了,感觉现在用Handler真的越来越少了


其实说了这么多,真正在写代码的时候,不能真正的避免,接下来我就使用leakcanary来检测某个项目中存在的内存泄漏问题,并解决


3 从实际项目出发,根除内存泄漏


1. 单例引发的内存泄漏


image.png


我们从gcroot中可以看到,在TeachAidsCaptureImpl中传入了LifeCycleOwner,LifeCycleOwner大家应该熟悉,能够监听Activity或者Fragment的生命周期,然后CaptureModeManager是一个单例,传入的mode就是TeachAidsCaptureImpl,这样就会导致一个问题,单例的生命周期很长,Fragment被销毁的时候因为TeachAidsCaptureImpl持有了Fragment的引用,导致无法销毁


fun clear() {
if (mode != null) {
mode = null
}
}

所以,在Activity或者Fragment销毁前,将model置为空,那么内存泄漏就会解决了,直到看到这个界面,那么我们的应用就是安全的了


image.png


2.使用Toast引发的内存泄漏


image.png


在我们使用Toast的时候,需要传入一个上下文,我们通常会传入Activity,那么这个上下文给谁用的呢,在Toast中也有View,如果我们自定过Toast应该知道,那么如果Toast中的View持有了Activity的引用,那么就会导致内存泄漏


Toast.makeText(this,"Toast内存泄漏",Toast.LENGTH_SHORT).show()

那么怎样避免呢?传入Application的上下文,就不会导致Activity不被回收。


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

线上kafka消息堆积,consumer掉线,怎么办?

线上kafka消息堆积,所有consumer全部掉线,到底怎么回事? 最近处理了一次线上故障,具体故障表现就是kafka某个topic消息堆积,这个topic的相关consumer全部掉线。 整体排查过程和事后的复盘都很有意思,并且结合本次故障,对kafka使...
继续阅读 »

线上kafka消息堆积,所有consumer全部掉线,到底怎么回事?


最近处理了一次线上故障,具体故障表现就是kafka某个topic消息堆积,这个topic的相关consumer全部掉线。


整体排查过程和事后的复盘都很有意思,并且结合本次故障,对kafka使用的最佳实践有了更深刻的理解。


好了,一起来回顾下这次线上故障吧,最佳实践总结放在最后,千万不要错过。


1、现象



  • 线上kafka消息突然开始堆积

  • 消费者应用反馈没有收到消息(没有处理消息的日志)

  • kafka的consumer group上看没有消费者注册

  • 消费者应用和kafka集群最近一周内没有代码、配置相关变更


2、排查过程


服务端、客户端都没有特别的异常日志,kafka其他topic的生产和消费都是正常,所以基本可以判断是客户端消费存在问题。


所以我们重点放在客户端排查上。


1)arthas在线修改日志等级,输出debug


由于客户端并没有明显异常日志,因此只能通过arthas修改应用日志等级,来寻找线索。


果然有比较重要的发现:


2022-10-25 17:36:17,774 DEBUG [org.apache.kafka.clients.consumer.internals.AbstractCoordinator] - [Consumer clientId=consumer-1, groupId=xxxx] Disabling heartbeat thread

2022-10-25 17:36:17,773 DEBUG [org.apache.kafka.clients.consumer.internals.AbstractCoordinator] - [Consumer clientId=consumer-1, groupId=xxxx] Sending LeaveGroup request to coordinator xxxxxx (id: 2147483644 rack: null)

看起来是kafka-client自己主动发送消息给kafka集群,进行自我驱逐了。因此consumer都掉线了。


2)arthas查看相关线程状态变量

用arthas vmtool命令进一步看下kafka-client相关线程的状态。



可以看到 HeartbeatThread线程状态是WAITING,Cordinator状态是UNJOINED。


此时,结合源码看,大概推断是由于消费时间过长,导致客户端自我驱逐了。


于是立刻尝试修改max.poll.records,减少一批拉取的消息数量,同时增大max.poll.interval.ms参数,避免由于拉取间隔时间过长导致自我驱逐。


参数修改上线后,发现consumer确实不掉线了,但是消费一段时间后,还是就停止消费了。


3、最终原因


相关同学去查看了消费逻辑,发现了业务代码中的死循环,确认了最终原因。



消息内容中的一个字段有新的值,触发了消费者消费逻辑的死循环,导致后续消息无法消费。

消费阻塞导致消费者自我驱逐,partition重新reblance,所有消费者逐个自我驱逐。



这里核心涉及到kafka的消费者和kafka之间的保活机制,可以简单了解一下。



kafka-client会有一个独立线程HeartbeatThread跟kafka集群进行定时心跳,这个线程跟lisenter无关,完全独立。


根据debug日志显示的“Sending LeaveGroup request”信息,我们可以很容易定位到自我驱逐的逻辑。



HeartbeatThread线程在发送心跳前,会比较一下当前时间跟上次poll时间,一旦大于max.poll.interval.ms 参数,就会发起自我驱逐了。


4、进一步思考


虽然最后原因找到了,但是回顾下整个排查过程,其实并不顺利,主要有两点:



  • kafka-client对某个消息消费超时能否有明确异常?而不是只看到自我驱逐和rebalance

  • 有没有办法通过什么手段发现 消费死循环?


4.1 kafka-client对某个消息消费超时能否有明确异常?


4.1.1 kafka似乎没有类似机制


我们对消费逻辑进行断点,可以很容易看到整个调用链路。



对消费者来说,主要采用一个线程池来处理每个kafkaListener,一个listener就是一个独立线程。


这个线程会同步处理 poll消息,然后动态代理回调用户自定义的消息消费逻辑,也就是我们在@KafkaListener中写的业务。



所以,从这里可以知道两件事情。


第一点,如果业务消费逻辑很慢或者卡住了,会影响poll。


第二点,这里没有看到直接设置消费超时的参数,其实也不太好做。


因为这里做了超时中断,那么poll也会被中断,是在同一个线程中。所以要么poll和消费逻辑在两个工作线程,要么中断掉当前线程后,重新起一个线程poll。


所以从业务使用角度来说,可能的实现,还是自己设置业务超时。比较通用的实现,可以是在消费逻辑中,用线程池处理消费逻辑,同时用Future get阻塞超时中断。


google了一下,发现kafka 0.8 曾经有consumer.timeout.ms这个参数,但是现在的版本没有这个参数了,不知道是不是类似的作用。


4.1.2 RocketMQ有点相关机制


然后去看了下RocketMQ是否有相关实现,果然有发现。


在RocketMQ中,可以对consumer设置consumeTimeout,这个超时就跟我们的设想有一点像了。


consumer会启动一个异步线程池对正在消费的消息做定时做 cleanExpiredMsg() 处理。



注意,如果消息类型是顺序消费(orderly),这个机制就不生效。


如果是并发消费,那么就会进行超时判断,如果超时了,就会将这条消息的信息通过sendMessageBack() 方法发回给broker进行重试。



如果消息重试超过一定次数,就会进入RocketMQ的死信队列。



spring-kafka其实也有做类似的封装,可以自定义一个死信topic,做异常处理



4.2 有没有办法通过什么手段快速发现死循环?


一般来说,死循环的线程会导致CPU飙高、OOM等现象,在本次故障中,并没有相关异常表现,所以并没有联系到死循环的问题。


那通过这次故障后,对kafka相关机制有了更深刻了解,poll间隔超时很有可能就是消费阻塞甚至死循环导致。


所以,如果下次出现类似问题,消费者停止消费,但是kafkaListener线程还在,可以直接通过arthas的 thread id 命令查看对应线程的调用栈,看看是否有异常方法死循环调用。


5、最佳实践


通过此次故障,我们也可以总结几点kafka使用的最佳实践:




  • 使用消息队列进行消费时,一定需要多考虑异常情况,包括幂等、耗时处理(甚至死循环)的情况。




  • 尽量提高客户端的消费速度,消费逻辑另起线程进行处理,并最好做超时控制。




  • 减少Group订阅Topic的数量,一个Group订阅的Topic最好不要超过5个,建议一个Group只订阅一个Topic。




  • 参考以下说明调整参数值:max.poll.records:降低该参数值,建议远远小于<单个线程每秒消费的条数> * <消费线程的个数> * <max.poll.interval.ms>的积。max.poll.interval.ms: 该值要大于<max.poll.records> / (<单个线程每秒消费的条数> * <消费线程的个数>)的值。


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

深入学习 Kotlin 特色之 Sealed Class 和 Interface

前言sealed class 以及 1.5 里新增的 sealed interface 可谓是 Kotlin 语言的一大特色,其在类型判断、扩展和实现的限制场景里非常好用。本文将从特点、场景和原理等角度综合分析 sealed 语法。Sealed ClassSe...
继续阅读 »

前言

sealed class 以及 1.5 里新增的 sealed interface 可谓是 Kotlin 语言的一大特色,其在类型判断、扩展和实现的限制场景里非常好用。

本文将从特点、场景和原理等角度综合分析 sealed 语法。

  • Sealed Class
  • Sealed Interface
  • Sealed Class & Interface VS Enum
  • Sealed Class VS Interface

🏁 Sealed Class

sealed class,密封类。具备最重要的一个特点:

  • 其子类可以出现在定义 sealed class 的不同文件中,但不允许出现在与不同的 module 中,且需要保证 package 一致

这样既可以避免 sealed class 文件过于庞大,又可以确保第三方库无法扩展你定义的 sealed class,达到限制类的扩展目的。事实上在早期版本中,只允许在 sealed class 内部或定义的同文件内扩展子类,这些限制在 Kotlin 1.5 中被逐步放开。

如果在不同 module 或 package 中扩展子类的话,IDE 会显示如下的提示和编译错误:

Inheritor of sealed class or interface declared in package xxx but it must be in package xxx where base class is declared

sealed class 还具有如下特点或限制:

  1. sealed class 是抽象类,可以拥有抽象方法,无法直接实例化。否则,编译器将提示如下:

    Sealed types cannot be instantiated

  2. sealed class 的构造函数只能拥有两种可见性:默认情况下是 protected,还可以指定成 private,public 是不被允许的。

    Constructor must be private or protected in sealed class

  3. sealed class 子类可扩展局部以及匿名类以外的任意类型子类,包括普通 class、data classobject、sealed class 等,子类信息在编译期可知。

    假使匿名类扩展自 sealed class 的话,会弹出错误提示:

    This type is sealed, so it can be inherited by only its own nested classes or objects

  4. sealed class 的实例,可配合 when 表达式进行判断,当所有类型覆盖后可以省略 else 分支

    如果没有覆盖所有类型,也没有 else 统筹则会发生编译警告或错误

    1.7 以前:

    Non-exhaustive 'when' statements on sealed class/interface will be prohibited in 1.7.

    1.7 及以后:

    'when' expression must be exhaustive, add ...

当 sealed class 没有指定构造方法或定义任意属性的时候,建议子类定义成单例,因为即便实例化成多个实例,互相之间没有状态的区别:

'sealed' subclass has no state and no overridden 'equals()'

下面结合代码看下 sealed class 的使用和原理:

示例代码:

 // TestSealed.kt
 sealed class GameAction(times: Int) {
     // Inner of Sealed Class
     object Start : GameAction(1)
     data class AutoTick(val time: Int) : GameAction(2)
     class Exit : GameAction(3)
 }

除了在 sealed class 内嵌套子类外,还可以在外部扩展子类:

 // TestSealed.kt
 sealed class GameAction(times: Int) {
    ...
 }
 
 // Outer of Sealed Class
 object Restart : GameAction(4)

除了可以在同文件下 sealed class 外扩展子类外,还可以在同包名不同文件下扩展。

 // TestExtendedSealedClass.kt
 // Outer of Sealed Class file
 class TestExtendedSealedClass: GameAction(5)

对于不同类型的扩展子类,when 表达式的判断亦不同:

  • 判断 sealed class 内部子类类型自然需要指定父类前缀
  • object class 的话可以直接进行实例判断,也可以用 is 关键字判断类型匹配
  • 普通 class 类型的话则必须加上 is 关键字
  • 判断 sealed class 外部子类类型自然无需指定前缀
 class TestSealed {
     fun test(gameAction: GameAction) {
         when (gameAction) {
             GameAction.Start -> {}
             // is GameAction.Start -> {}
             is GameAction.AutoTick -> {}
             is GameAction.Exit -> {}
 
             Restart -> {}
             is TestExtendedSealedClass -> {}
        }
    }
 }

如下反编译的 Kotlin 代码可以看到 sealed class 本身被编译为 abstract class。

扩展自其的内部子类按类型有所不同:

  • object class 在 class 内部集成了静态的 INSTANCE 实例
  • 普通 class 仍是普通 class
  • data Class 则是在 class 内部集成了属性的 gettoString 以及 hashCode 函数
 public abstract class GameAction {
    private GameAction(int times) { }
 
    public GameAction(int times, DefaultConstructorMarker $constructor_marker) {
       this(times);
    }
     
    // subclass:object
    public static final class Start extends GameAction {
       @NotNull
       public static final GameAction.Start INSTANCE;
 
       private Start() {
          super(1, (DefaultConstructorMarker)null);
      }
 
       static {
          GameAction.Start var0 = new GameAction.Start();
          INSTANCE = var0;
      }
    }
 
    // subclass:class
    public static final class Exit extends GameAction {
       public Exit() {
          super(3, (DefaultConstructorMarker)null);
      }
    }
 
    // subclass:data class
    public static final class AutoTick extends GameAction {
       private final int time;
 
       public final int getTime() {
          return this.time;
      }
 
       public AutoTick(int time) {
          super(2, (DefaultConstructorMarker)null);
          this.time = time;
      }
      ...
       @NotNull
       public String toString() {
          return "AutoTick(time=" + this.time + ")";
      }
 
       public int hashCode() { ... }
 
       public boolean equals(@Nullable Object var1) { ... }
    }
 }

而外部子类则自然是定义在 GameAction 抽象类外部。

 public abstract class GameAction {
    ...
 }
 
 public final class Restart extends GameAction {
    @NotNull
    public static final Restart INSTANCE;
 
    private Restart() {
       super(4, (DefaultConstructorMarker)null);
    }
 
    static {
       Restart var0 = new Restart();
       INSTANCE = var0;
    }
 }

文件外扩展子类可想而知。

 public final class TestExtendedSealedClass extends GameAction {
    public TestExtendedSealedClass() {
       super(5, (DefaultConstructorMarker)null);
    }
 }

🏴 Sealed Interface

sealed interface 即密封接口,和 sealed class 有几乎一样的特点。比如:

  • 限制接口的实现:一旦含有包含 sealed interface 的 module 经过了编译,就无法再有扩展的实现类了,即对其他 module 隐藏了接口

还有些额外的优势:

  • 帮助密封类、枚举类等类实现多继承和扩展性,比如搭配枚举,以处理更复杂的分类逻辑

    Additionally, sealed interfaces enable more flexible restricted class hierarchies because a class can directly inherit more than one sealed interface.

    比如 Flappy Bird 游戏的过程中会产生很多 Action 来触发数据的计算以推动 UI 刷新以及游戏的进程,Action 可以用 enum class 来管理。

    其中有些 Action 是关联的,有些则没有关联、不是同一层级。但是 enum class 默认扩展自 Enum 类,无法再嵌套 enum。

    Enum class cannot inherit from classes

    这将导致层级混乱、阅读性不佳,甚至有的时候功能相近的时候还得特意取个不同的名称。

     enum class Action {
         Tick,
         // GameAction
         Start, Exit, Restart,
         // BirdAction
         Up, Down, HitGround, HitPipe, CrossedPipe,
         // PipeAction
         Move, Reset,
         // RoadAction
         // 防止和 Pipe 的 Action 重名导致编译出错,
         // 将功能差不多的 Road 移动和重置 Action 定义加上了前缀
         RoadMove, RoadReset
     }
     
     fun dispatch(action: Action) {
         when (action) {
             Action.Tick -> TODO()
     
             Action.Start -> TODO()
             Action.Exit -> TODO()
             Action.Restart -> TODO()
     
             Action.Up -> TODO()
             Action.Down -> TODO()
             Action.HitGround -> TODO()
             Action.HitPipe -> TODO()
             Action.CrossedPipe -> TODO()
     
             Action.Move -> TODO()
             Action.Reset -> TODO()
     
             Action.RoadMove -> TODO()
             Action.RoadReset -> TODO()
        }
     }

    借助 sealed interface 我们可以给抽出 interface,并将 enum 进行层级拆分。更加清晰、亦不用担心重名。

     sealed interface Action
     
     enum class GameAction : Action {
         Start, Exit, Restart
     }
     
     enum class BirdAction : Action {
         Up, Down, HitGround, HitPipe, CrossedPipe
     }
     
     enum class PipeAction : Action {
         Move, Reset
     }
     
     enum class RoadAction : Action {
         Move, Reset
     }
     
     object Tick: Action

    使用的时候就可以对抽成的 Action 进行嵌套判断:

     fun dispatch(action: Action) {
         when (action) {
             Tick -> TODO()
             
             is GameAction -> {
                 when (action) {
                     GameAction.Start -> TODO()
                     GameAction.Exit -> TODO()
                     GameAction.Restart -> TODO()
                }
            }
             is BirdAction -> {
                 when (action) {
                     BirdAction.Up -> TODO()
                     BirdAction.Down -> TODO()
                     else -> TODO()
                }
            }
             is PipeAction -> {
                 when (action) {
                     PipeAction.Move -> TODO()
                     PipeAction.Reset -> TODO()
                }
            }
             is RoadAction -> {
                 when (action) {
                     RoadAction.Move -> TODO()
                     RoadAction.Reset -> TODO()
                }
            }
        }
     }

🤔 总结

1. Sealed Class & Interface VS Enum

总体来说 sealed class 和 interface 和 enum 有相近的地方,也有明显区别,需要留意:

  • 每个 enum 常量只能以单例的形式存在
  • sealed class 子类可以拥有多个实例,不受限制,每个均可以拥有自己的状态
  • enum class 不能扩展自 sealed class 以及其他任何 Class,但他们可以实现 sealed 等 interface

2. Sealed Class VS Interface

Sealed classes and interfaces represent restricted class hierarchies that provide more control over inheritance.

sealed class 和 interface 都意味着受限的类层级结构,便于在继承和实现上进行更多控制。具备如下的共同特性:

  • 其 sub class 需要定义在同一 Module 以及同一 package,不局限于 sealed 内部或同文件内

看下对比:

Sealed适用/优势原理
Class限制类的扩展abstract class
Interface限制接口的实现 帮助类实现多继承和复杂的扩展性interface


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

带你深入理解Flutter及Dart单线程模型

前言 大家好,我是未央歌,一个默默无闻的移动开发搬砖者~ 众所周知,Java 是一种多线程语言,适量并合适地使用多线程,会极大提高资源利用率和运行效率,但缺点也明显,比如开启过多的线程会导致资源和性能的消耗过大以及多线程共享内存容易死锁。 而 Dart 则是一...
继续阅读 »

前言


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


众所周知,Java 是一种多线程语言,适量并合适地使用多线程,会极大提高资源利用率和运行效率,但缺点也明显,比如开启过多的线程会导致资源和性能的消耗过大以及多线程共享内存容易死锁


而 Dart 则是一种单线程语言,单线程语言就意味着代码执行顺序是有序的,下面结合一个demo带大家深入了解单线程模型。


demo 示例


点击 APP 右下角的刷新按钮,会调用如下方法,读取一个约 2M 大小的 json 文件。


void loadAssetsJson() async {
var jsonStr = await DefaultAssetBundle.of(context).loadString("assets/list.json");

VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
}

如下图所示,点击刷新按钮之后,中间的 loading 会卡一下。很多同学一看这个代码就知道,肯定会卡,解析一个 2M 的文件,而且是同步解析,主页面肯定是会卡的。

那如果我换成异步解析呢?还卡不卡?大家可以脑海中思考下这个问题。


异步解析


void loadAssetsJson() async {
var jsonStr = await DefaultAssetBundle.of(context).loadString("assets/list.json");

// 异步解析
Future(() {
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
}).then((value) {});
}


大家可以看到,我已经放在异步里解析了,为什么还是会卡呢?大家可以先思考下这个问题。


前面已经提到了 Dart 是一种单线程语言,单线程语言就意味着代码执行顺序是有序的。当然 Dart 也是支持异步的。这两点其实并不冲突。


Dart 线程解析



我们来看看 Dart 的线程,当我们 main() 方法启动之后,Dart已经开启了一个线程,这个线程的名字就叫 Isolate。每一个 Isolate 线程都包含了图示的两个队列,一个 Microtask queue,一个 Event queue


如图,Isolate 线程会优先执行 Microtask queue 里的事件,当 Microtask queue 里的事件变成空了,才会去执行 Event queue 里的事件。如果正在执行 Microtask queue 里的事件,那么 Event queue 里的事件就会被阻塞,就会导致渲染、手势响应等都得不到响应(绘制图形,处理鼠标点击,处理文件IO等都是在 Event Queue 里完成)。


所以为了保证功能正常使用不卡顿,尽量少在 Microtask queue 做事情,可以放在 Event queue 做


为什么单线程可以做一个异步操作呢?



  • 因为 APP 只有在你滑动或者点击操作的时候才会响应事件。没有操作的时候进入等待时间,两个队列里都是空的。这个时间正是可以进行异步操作的,所以基于这个特点,单线程模型可以在等待过程中做一些异步操作,因为等待的过程并不是阻塞的,所以给我们的感觉就像同时在做多件事情,但自始至终只有一个线程在处理事情。


Future


当方法加上 async 关键字,就代表这个方法开启了一个异步操作,如果这个方法有返回值,就必须要返回一个 Future。


void loadAssetsJson() async {
var jsonStr = await DefaultAssetBundle.of(context).loadString("assets/list.json");

// 异步解析
Future(() {
...
}).then((value) {});
}

一个 Future 异步任务的执行,相对简单。在我们声明一个 Future 之后,Dart 会将异步里的代码函数体放在 Event queue 里执行然后返回。这里注意下,Future 和 then 是放在同一个 Event queue 里的。


假设,我执行 Future 代码之后没有立即执行 then 方法,而是等 Future 执行之后5秒,才调用 then 方法,这时候还是放在同一个 Event queue 里吗?显然是不可能的,我们看一下源码是怎么实现的。


Future<R> then<R>(FutureOr<R> f(T value), {Function? onError}) {
...
_addListener(new _FutureListener<T, R>.then(result, f, onError));
return result;
}

bool get _mayAddListener => _state <= (_statePendingComplete | _stateIgnoreError);

void _addListener(_FutureListener listener) {
assert(listener._nextListener == null);
if (_mayAddListener) {
// 待完成
listener._nextListener = _resultOrListeners;
_resultOrListeners = listener;
} else {
// 已完成
...
_zone.scheduleMicrotask(() {
_propagateToListeners(this, listener);
});
}
}

可以看到 then 方法里有一个监听,Future 执行之后5秒才调用,很明显是已完成状态,走 else 那里的 scheduleMicrotask() 方法,就是说把 then 里面的方法放到 Microtask queue 里。


Future 为何卡顿


再来说一下刚刚的问题,我已经放在异步里解析了,为什么还是会卡呢?


其实很简单,Future 里的代码可能需要执行10s,也就是 Event queue 需要10s才能执行完。那这个10s内其他代码肯定就无法执行了。所以 Future 里的代码执行时间过长,还是会卡 UI 的。


以 Android 为例,Android的刷新频率是60帧/秒,Android系统中每隔16.6ms会发送一次 VSYNC(同步)信号,触发UI的渲染。所以我们就要考虑下,一旦代码执行时间超过16.6ms,到底应不应该放在 Future 里执行?


这时候是不是有同学有疑问,我网络请求也是用 Future 写的,为什么就不卡呢?


这个大家就需要注意一下,网络请求不是放在 Dart 层面执行的,它是由操作系统提供的异步线程去执行的,当这个异步执行完系统又返回给 Dart。所以即使 http 请求需要耗时十几秒,也不会感到卡顿。


compute


既然 Future 执行也会卡顿,那要怎么去优化呢?这时候我们可以开一个线程操作,Flutter 为我们封装好了一个 compute()方法,这个方法可以为我们开一个线程。我们用这个方法来优化一下代码,然后再看下执行效果。


void loadAssetsJson() async {
var jsonStr = await DefaultAssetBundle.of(context).loadString("assets/list.json");

var result = compute(parse,jsonStr);
}

static VideoListModel parse(String jsonStr){
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
return VideoListModel.fromJson(json.decode(jsonStr));
}


可以看到此时点击刷新按钮,已经不再卡顿了。遇到一些耗时的操作,这确实是一种比较好的解决方式。


我们再看看 DefaultAssetBundle.of(context).loadString("assets/list.json") 方法里面是怎么执行的。


Future<String> loadString(String key, { bool cache = true }) async {
final ByteData data = await load(key);
if (data == null)
throw FlutterError('Unable to load asset: $key');
// 50 KB of data should take 2-3 ms to parse on a Moto G4, and about 400 μs
// on a Pixel 4.
if (data.lengthInBytes < 50 * 1024) {
return utf8.decode(data.buffer.asUint8List());
}
// For strings larger than 50 KB, run the computation in an isolate to
// avoid causing main thread jank.
return compute(_utf8decode, data, debugLabel: 'UTF8 decode for "$key"');
}

从官方源码可以看到,当文件的大小超过 50kb 时,也是采用 compute() 方法开一个线程去操作的。


多线程机制


Dart 作为一个单线程语言,虽然提供了多线程的机制,但是在多线程的资源是隔离的,两个线程之间资源是不互通的


Dart 的多线程数据交互需要从 A 线程传给 B 线程,再由 B 线程返回给 A 线程。而像 Android 在主线程开一个子线程,子线程可以直接拿主线程的数据,而不用让主线程传给子线程。


总结



  • Future 适合耗时小于 16ms 的操作

  • 可以通过 compute() 进行耗时操作

  • Dart 是单线程原因,但也支持多线程,但是线程间数据不互通

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

Kotlin Sequence 是时候派上用场了

前言 在进入Flow世界之前,先来分析Sequence,进而自然延伸到Flow。 通过本篇文章,你将了解到: Java与Kotlin 对集合的处理 Java Stream 的简单使用 Sequence 的简单使用 Sequence 的原理 Sequence...
继续阅读 »

前言


在进入Flow世界之前,先来分析Sequence,进而自然延伸到Flow。

通过本篇文章,你将了解到:




  1. Java与Kotlin 对集合的处理

  2. Java Stream 的简单使用

  3. Sequence 的简单使用

  4. Sequence 的原理

  5. Sequence 的优劣势



1. Java与Kotlin 对集合的处理


场景分析


客户有个场景想考验一下Java和Kotlin:

从一堆数据里(0--10000000)找到大于1000的偶数的个数。


Java和Kotlin 均表示so easy,跃跃欲试。

秉着尊老爱幼的优良传统,老大哥Java先出场。


Java 出场


    public List<Integer> dealCollection() {
List<Integer> evenList = new ArrayList<>();
for (Integer integer : list) {
//筛选出偶数
if (integer % 2 == 0) {
evenList.add(integer);
}
}

List<Integer> bigList = new ArrayList<>();
for (Integer integer : evenList) {
//从偶数中筛选出大于1000的数
if (integer > 1000) {
bigList.add(integer);
}
}
//返回筛选结果列表
return bigList;
}

Java解释说:“先将偶数的结果保存到列表里,再从偶数列表里筛选出大于1000的数。”


Kotlin 出场


Kotlin 看到Java的解决方案,表示写法有点冗余,不够灵活,于是拿出自己的方案:


    fun testCollection() {
var time = measureTimeMillis {
var list = (0..10000000).filter {
it % 2 == 0
}.filter {
it > 1000
}
println("kotlin collection list size:${list.size}")
}
println("kotlin collection use time:$time")
}

Kotlin 说:“老大哥,看看我这个写法,只需要几行代码,简洁如斯。”

Java 淡定到:“确实够简洁,但是表面的简洁掩盖了背后的许多冗余,能一层一层剥开你的心吗?”

Kotlin道:“你我赤诚相对,士为知己者死,刀来!”

Java赶紧递上自己随身携带的水果刀...


Kotlin 反编译


遇事不决反编译:


    public final void testCollection() {
//构造迭代器
Iterable $this$filter$iv = (Iterable)(new IntRange(var8, 10000000));
//构造链表用来存储偶数
Collection destination$iv$iv = (Collection)(new ArrayList());
//取出迭代器
Iterator var13 = $this$filter$iv.iterator();

//遍历取出偶数
while(var13.hasNext()) {
element$iv$iv = var13.next();
it = ((Number)element$iv$iv).intValue();
var16 = false;
if (it % 2 == 0) {
destination$iv$iv.add(element$iv$iv);
}
}

$this$filter$iv = (Iterable)((List)destination$iv$iv);
$i$f$filter = false;
//构造链表用来存储>1000的偶数
destination$iv$iv = (Collection)(new ArrayList());
$i$f$filterTo = false;
//取出迭代器
var13 = $this$filter$iv.iterator();
//遍历链表
while(var13.hasNext()) {
element$iv$iv = var13.next();
it = ((Number)element$iv$iv).intValue();
var16 = false;
if (it > 1000) {
destination$iv$iv.add(element$iv$iv);
}
}

//最终的结果
List list = (List)destination$iv$iv;
}

看到这,Java恍然大悟到:“原来如此,你也是分步存储结果,我俩想到一起了,真机智啊。”

Kotlin:“彼此彼此。”


客户说:“你俩就不要商业互吹了,我就想要一个结果而已,你们就给我弄了两个循环,若是此时我再加一两个条件,你们是不是得要再加几个循环遍历?那不是白白增加耗时吗?”

Java 神秘的笑道:“非也非也,我好歹也是沉浸代码界几十年的存在,早有预案。”

客户说:“那就开始你的表演吧...”


2. Java Stream 的简单使用


什么是流


Java说:“我从Java8开始就支持Stream(流) API了,可以满足你的需求。”

客户不解道:“什么是流?”

Java:“流就是一个过程,比如说你之前的需求就可以当做一个流,可以在中途对流做一系列的处理,而后在流的末尾取出处理后的结果,这个结果就是最终的结果。”

Kotlin补充道:“老大哥,你说的比较抽象,我举个例子吧。”



image.png


在一个管道的入口处放入了各种鱼,如草鱼、鲤鱼、鲢鱼、金鱼等,管道允许接入不同的小管道用以筛选不同组合的鱼类。

比如有个客户只想要金鱼,于是它分别接了4个小管道,第一个管道用来将草鱼分流,第二个管道用来分流鲤鱼,第三个管道用来分流鲢鱼,最后剩下的就是金鱼。

当然,他也可以只分流草鱼,剩下的鲤鱼、鲢鱼、金鱼他都需要,这就增加了操作的灵活性。

客户说:“talk is cheap, show me the code。”


Java Stream


Java 撸起袖子,几个呼吸之间就写好了如下代码:


    public long dealCollectionWithStream() {
Stream<Integer> stream = list.stream();
return stream.filter(value -> value % 2 == 0)
.filter(value -> value > 1000)
.count();
}

客户不解地问:“这确实很简洁了,但是和Kotlin写法一样的嘛?”

Java道:“No No No,别被简洁的外表迷惑了,我们直接来看看处理的耗时即可。”


    public static void main(String args[]) {
Java8Stream java8Stream = new Java8Stream();
//普通集合耗时
long startTime = System.currentTimeMillis();
List<Integer> list = java8Stream.dealCollection();
System.out.println("java7 list size:" + list.size() + " use time:" + (System.currentTimeMillis() - startTime) + "ms");

//Stream API 的耗时
long startTime2 = System.currentTimeMillis();
long count = java8Stream.dealCollectionWithStream();
System.out.println("java8 stream list size:" + count + " use time:" + (System.currentTimeMillis() - startTime2) + "ms");
}

打印结果如下:



image.png


Java 继续解释:“既然只关心最后的结果,那么对于流来说,可以在各个位置指定条件对流的内容进行筛选,对于同一个内容来说只有上一个条件满足了,才会继续处理下一个条件,否则将会处理流里的其它内容。如此一来,再也不用反复存取中间结果了,对于大批量的数据来说,大大减少了耗时。”

客户赞赏:“不错,能解决我的痛点。”

Java 说:“不仅如此,我还可以并行操作流,最后将结果汇总,又可以减少一些耗时了。”

客户:“优秀,那我就选...”

Kotlin 急道:“住口...不,等等,我有话说。”

客户:“你快说,说不出子丑寅卯,我就...”


3. Sequence 的简单使用


Sequence 引入


Kotlin:“和Java老大哥一样,我也可以对流进行操作,主要是用sequence实现”


    fun testSequence() {
var time = measureTimeMillis {
val count = (0..10000000)
.asSequence()//转换为sequence
.filter {
it % 2 == 0//过滤偶数
}.filter {
it > 1000//过滤>1000
}.count() //统计个数
println("kotlin sequence list size:${count}")
}
println("kotlin sequence use time:$time")
}

和未使用sequence 对比耗时:


    public static void main(String args[]) {
SequenceDemo sequenceDemo = new SequenceDemo();
//使用集合操作
sequenceDemo.testCollection();
//使用sequence操作
sequenceDemo.testSequence();
}


image.png


可以看出,使用了sequence后,可以大大减少耗时。


Kotlin 反编译Sequence



image.png


由此可见,并没有对中间结果进行存储遍历,而是通过嵌套调用进而操作流的。


4. Sequence 的原理


集合转Sequence


(0..10000000)

这表示的是0到10000000的集合,它的实现类是:



image.png


IntRange 里定义了集合的开始值和结束值,重点在其父类:IntProgression。

IntProgression 实现了Iterable接口,并实现了该接口里的唯一方法:iterator()

具体实现类为:


internal class IntProgressionIterator(first: Int, last: Int, val step: Int) : IntIterator() {
private val finalElement: Int = last
private var hasNext: Boolean = if (step > 0) first <= last else first >= last
private var next: Int = if (hasNext) first else finalElement

override fun hasNext(): Boolean = hasNext

override fun nextInt(): Int {
val value = next
if (value == finalElement) {
if (!hasNext) throw kotlin.NoSuchElementException()
hasNext = false
}
else {
next += step
}
return value
}
}

通常来说,迭代器有三个重要元素:




  1. 起始值

  2. 步长

  3. 结束值



对应的两个核心方法:




  1. 检测是否还有下个元素

  2. 取出下个元素



对于当前的Int迭代器来说:它的起始值为0,步长是1,结束值是10000000,当我们调用迭代器时就可以取出里面的每个数。


迭代器有了,接下来看看如何构造为一个Sequence。


public fun <T> Iterable<T>.asSequence(): Sequence<T> {
//取当前的迭代器,也就是IntProgressionIterator
return Sequence { this.iterator() }
}
//构造一个Sequence
public inline fun <T> Sequence(crossinline iterator: () -> Iterator<T>): Sequence<T> = object : Sequence<T> {
override fun iterator(): Iterator<T> = iterator()
}

Sequence 是个接口,它的唯一接口是:


public interface Sequence<out T> {
public operator fun iterator(): Iterator<T>
}

结合两者分析可知:



asSequence() 构造了Sequence匿名内部类对象,而其实现的方法就是iterator(),该方法最终返回IntProgressionIterator 对象
也就是说Sequence初始迭代器即为Collection的迭代器



Sequence中间操作符


以filter为例:


public fun <T> Sequence<T>.filter(predicate: (T) -> Boolean): Sequence<T> {
//构造Sequence 子类,该子类用来过滤流
return FilteringSequence(this, true, predicate)
}

override fun iterator(): Iterator<T> = object : Iterator<T> {
//上一个Sequence的迭代器
val iterator = sequence.iterator()
var nextState: Int = -1 // -1 for unknown, 0 for done, 1 for continue
var nextItem: T? = null

private fun calcNext() {
//先判断上一个Sequence的迭代器
while (iterator.hasNext()) {
val item = iterator.next()
//拿到值后判断本Sequence的逻辑
//是否符合过滤条件,符合就取出值,交个下一个条件,不符合则找下一个元素
if (predicate(item) == sendWhen) {
nextItem = item
nextState = 1
return
}
}
nextState = 0
}

//重写next()与hasNext(),里边调用了calcNext
override fun next(): T {
if (nextState == -1)
calcNext()
if (nextState == 0)
throw NoSuchElementException()
val result = nextItem
nextItem = null
nextState = -1
@Suppress("UNCHECKED_CAST")
return result as T
}

override fun hasNext(): Boolean {
if (nextState == -1)
calcNext()
return nextState == 1
}
}

我们调用了两次filter操作符,最终形成的结构如下:



image.png


此处用到了设计模式里的装饰模式:




  1. Sequence 只有普通的迭代功能,现在需要为它增强过滤偶数的功能,因此新建了FilteringSequence 对象A,并持有Sequence对象,当需要调用过滤偶数的功能时,先借助Sequence获取基本数据,再使用FilteringSequenceA过滤偶数

  2. 同样的,还需要在1的基础上继续增强FilteringSequence的过滤功能,再新建FilteringSequence B持有FilteringSequence对象A
    A,当需要调用过滤>1000的数时,先借助FilteringSequence 对象A获取偶数,再使用FilteringSequenceB过滤>1000的数



如此一来,通过嵌套调用就实现了众多操作过程。


Sequence终端操作符


你可能已经发现了:中间操作符仅仅只是建立了装饰(引用)关系,并没有触发迭代啊,那什么时候触发迭代呢?

这个时候就需要用到终端操作符(也叫末端操作符)。

比如count方法:


    public fun <T> Sequence<T>.count(): Int {
var count = 0
//触发遍历,统计个数
for (element in this) checkCountOverflow(++count)
return count
}

当调用了count()方法后,将会触发遍历,最终调用栈如下:



image.png


只有调用了终端操作符,流才会动起来,这也就是为啥说Sequence、Java Stream 中间操作符是惰性操作符的原因。


Sequence与普通集合链式调用区别


还是之前的Demo

普通集合链式调用



image.png


每次操作(如filter)都需要遍历集合找到符合条件的条目加入到新的集合,然后再在新的集合基础上再次进行操作。

如上图,先执行紫色区块,再执行蓝色区块。


Sequence 调用



image.png


每次先对某个条目进行所有的操作(比如filter),先判断每一步该条目是否符合,不符合则再找下一个条目进行所有的操作。

如上图:从左到右按顺序执行紫色区块。


5. Sequence 的优劣势


与普通集合链式调用对比,Sequence也有链式调用。

前者链式调用每次都需要完整遍历集合并将中间结果缓存,下一次调用依赖上一次调用缓存的结果。

而后者链式调用先是将每个操作关联起来,然后当触发终端操作符时针对每一个条目(元素)先执行所有的操作(这些操作在上一步已经关联)。

由此可见,如果集合里元素很多,Sequence可以大大节约时间(没有多次遍历,没有暂存结果)


除此之外,Sequence 只做最少的操作,尽可能地节约时间。

怎么理解呢?还是上面的例子,我们只想取前10个偶数,代码如下:


    fun testSequence1() {
var time = measureTimeMillis {
val count = (0..10000000)
.asSequence()//转换为sequence
.filter {
it % 2 == 0//过滤偶数
}.take(10).count()
println("kotlin sequence1 list size:${count}")
}
println("kotlin sequence1 use time:$time")
}

该序列只会执行到集合里的条目=18就终止了,因为到了0~18已经有10个偶数了,而剩下的一堆条目都无需执行,大大节省了时间。


因此,Sequence 优势:




  1. 不暂存数据,不进行多次遍历

  2. 只做最少的操作

  3. 可以产生无限的序列



以上以filter/take/count 操作符阐述了Sequence的原理及其优势,当然还有更多的具体的使用场景/方式待挖掘。


此时,Kotlin 迫不及待跳出来说:“怎么样,我这个Sequence 6吧?”

客户说:“看你字多的份上,我选择信你,那我就选...”

Java急忙道:“我有问题,我的Stream支持并行,你支持吗?”

Kotlin:“...”

想了一会儿,Kotlin继续道:“Sequence 虽然不支持切换线程,但是它的兄弟支持,它就是Flow。”

Java补充说:“你有Flow,我有LiveData,那我俩继续PK?”

没等Kotlin回话,客户急忙道:“哎哎,行了,时间不够了,下次再继续吧,散会...”

Java:“...”

Kotlin:“...”


第100篇博客,不忘初心,砥砺前行,继续输出高质量、成体系的博客。

下次将会进入Flow的世界。


本文基于Kotlin 1.5.3,文中完整Demo请点击


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

uni-app跨端开发之疑难杂症

今年,公司决定解决各个团队移动端开发的混战局面,由架构部出一套移动端框架,规范化开发标准。经过一段时间的调研,考虑到跨端以及公司主要技术栈为vue,最终选择了uni-app作为移动端框架,在大家都“很忙”的情况下,我成为了移动端框架的主要开发。以前就总听同事说...
继续阅读 »

前言

今年,公司决定解决各个团队移动端开发的混战局面,由架构部出一套移动端框架,规范化开发标准。经过一段时间的调研,考虑到跨端以及公司主要技术栈为vue,最终选择了uni-app作为移动端框架,在大家都“很忙”的情况下,我成为了移动端框架的主要开发。以前就总听同事说,uni-app有很多坑,我对其也只是有些许了解,这回的全身心投入,才知道一入深坑愁似海

这段时间也做了一些成效,头大如斗的路由拦截、必不可少的http请求封装、提高成效的组件库、仿照微信的oAuth 2.0登录、复杂逻辑的离线存储、用户需要的增量更新包

有成效也踩了一些坑,百思不得解的console.log、烦到吐血的网络调试、爬坑许久的APP与h5通讯、性能极差的微信小程序端uni.canvasToTempFilePath

今天就要聊聊一些疑难杂症,有些忘记了,有些还没碰到,后续持续更新吧!

百思不得解的console.log

移动端框架是采用npm包的方式提供给业务部门使用,其中包含oAuth2.0登录方式,这其中涉及到了h5通过scheme协议唤醒app并且带回code等参数,相应的参数会存放在plus.runtime.arguments,其他情况下,plus.runtime.arguments的值为空。在给同事排查问题时我就简单操作,在node_modules对应的npm包里面写了不是很严谨的如下代码:

const args = plus.runtime.arguments;
// 这个是业务部门出错时,我添加的调试代码
console.log('>>>>>>'args)
if (args) {
 const isLogout = args.includes('logout');
 if (isLogout) {
   await this.handleSession();
else {
   await this.handleAuthorization(args);
}
}

我测试是正常的,args是空值,所以是不会执行if内的逻辑的,但是他这边会执行if内的逻辑的,初步判断args由于某个原因导致存在值了,为了简单明了的查看输出内容,然后我就写了毁一生的console.log('>>>>>>', args),这行调试代码的输出内容如下,我一直以为args是空值,但是判断依旧为true,有点颠覆了我的人生观,后来灵机一动,删掉了第一个修饰参数,发现args原来是有值的,经过排查,是因为添加了微信小程序打开指定页面,导致记录当前页面数据。


烦到吐血的网络调试

网络调试对于我们的日常开发是很重要的,有助于快速判断资源请求问题,但uni-app在这方面有很大的缺陷,在讨论这个问题时,先来看一下uni-app的真机调试方式。

终端调试工具

当项目运行时,点击终端上的调试按钮,会弹出一个调试界面。


从调试面板中,可以看到仅有ConsoleElementsSources三个选项,期待许久的Network并没有出现,这种调试方式没办法实现网络请求调试。


webview调试控制台

点击工具栏的 运行 -> 运行到手机或模拟器 -> 显示webview调试控制台 会出现一个跟谷歌浏览器一样的调试界面,虽然这里有Network,但是很可惜,这个功能存在问题,没办法监听到网络请求。


Fiddler 抓取网络请求

在走投无路之下,只能另辟蹊径,借助工具,抓取真机的网络请求,接下来阐述一下怎么使用Fiddler抓取真机的网络请求,配置完需要重启才生效。

下载Fiddler

这是一个免费工具,自行在网络上下载即可。

Fiddler 基础配置

点击工具栏的tools,选择options就会弹出一个配置界面



HTTPS 配置

选择HTTPS选项,勾选选矿中的Capture HTTPS CONNECTsDecrypt HTTPs trfficIgnore server certificate errors


Connections 配置

这边配置的端口号后面配置代理的时候需要使用到。


手机配置代理

注意需要和电脑连接同一网络,点击进入手机WIFI详情界面,有个代理,选择手动模式,输入电脑的IP地址和Fiddler的监听端口,即可拦截到真机的所有网络请求,包含我们app对应的网络请求。


过滤

这边可以选择过滤对应的ip或域名,多个的话通过分号隔开即可。


爬坑许久的APP与h5通讯

谈论这个问题时,先描述一下uni-app实现的app怎么和h5通讯

app端

对于app端的通讯,.vue.nvue有两点区别,1. 获取webView实例不一致,2. 监听方法不一致。app向h5传递数据时,需要借助webview.evalJS执行h5的全局方法,而h5向app传递参数时,类似于h5发送postMessage,可以在webview的message/onPostMessage监听函数获取数据。

vue

获取webView示例

webView实例的获取,对于vue文件不是特别友好,需要借助于this.$scope.$getAppWebview(),如果是在组件中需要使用this.$parent.$scope.$getAppWebview(),添加延时的原因是,h5页面可能未加载完成,无法获取到对应的全局函数,会提示xxx函数undefined;

<template>
   <web-view src="http://www.juejin.com"></web-view>
</template>
<script>
   export default {
       onReady() {
           const currentWebview = this.$scope.$getAppWebview();
           const account = '清欢bx'
           setTimeout(() => {
               const webView = currentWebview.children()[0];
               webView.evalJS(`setAccountInfo(${account})`);
          }, 1000);
      }
  }
</script>

监听方法

vue文件采用@message触发监听函数

<template>
   <web-view @message="handleMessage" src="http://www.juejin.com"></web-view>
</template>
<script>
   export default {
       methods: {
           handleMessage(data) {
               console.log(data)
          }
      }
  }
</script>

nvue

获取webView示例

在nvue获取webView实例就很流畅了,直接通过this.$refs.webview就能获取到。

<template>
   <web-view ref="webview" src="http://www.juejin.com"></web-view>
</template>
<script>
   export default {
       onReady() {
           const account = '清欢bx'
           this.$refs.webview.evalJs(`setAccountInfo(${account})`);
      }
  }
</script>

监听方法

nvue文件采用@onPostMessage触发监听函数

<template>
   <web-view @onPostMessage="handleMessage" src="http://www.juejin.com"></web-view>
</template>
<script>
   export default {
       methods: {
           handleMessage(data) {
               console.log(data)
          }
      }
  }
</script>

h5 端

发送数据

需要引入一个uni-app的sdk,uni.webview.1.5.4.js,最低版本需要1.5.4,可以在index.html引入,也可以在main.js引入,注意点是传递的参数必须写在data里面,也就是维持这样的数据结构。

uni.postMessage({
   data: {
     xxxxxx,
     xxxxxx
  }
});

如果是页面加载完成时就需要发送数据,需要等待UniAppJSBridgeReady钩子结束后触发postMessage;

<script>
   export default {
       mounted() {
           document.addEventListener('UniAppJSBridgeReady'function() {
               uni.webView.getEnv(function(res) {
                   console.log('当前环境:' + JSON.stringify(res));
              });
               uni.postMessage({
                   data: {
                     action'message'
                  }
              });
          });
      }
  }
</script>

如果是通过事件点击发送数据,因为这时候页面已经加载完成,不需要再去监听UniAppJSBridgeReady钩子,直接触发uni.postMessage即可。

<template>
   <view>
       <button @click="handlePostMessage">发送数据</button>
   </view>
</template>
<script>
   export default {
       methods: {
           handlePostMessage() {
               uni.postMessage({
                   data: {
                     action'message'
                  }
              });
          }
      }
  }
</script>

获取数据

获取数据的函数,需要挂载到window上,可以直接写在main.js里面,数据需要共享到具体页面内,可以使用本地村存储localStorage、事件总线eventBusvuex,根据自己的需求选择。

window.setAccountInfo = function(data) {
   console.log(data)
}

踩坑点

uni is not defined

app需要涉及到离线或者内网,索引uni.webview.js下载到本地进行引入,因为uni.webview.js已经被编译成了umd格式,在vue项目中在进行一次打包后,导致this指向不是window,所以没有把uni挂在到全局上,将this指向改为window即可。

未改造之前的代码


改造后


或者


app向h5传递参数时,无法传递对象,并且传递的参数需要字符串序列化

在传递参数时,对象传递过去没办法识别,同时传递的参数需要执行JSON.stringify(),多个参数时,可以多个参数传递,也可以把多个参数进行字符串拼接,然后再h5端进行拆分处理。

const { accountpassword } = accountInfo;
const _account = JSON.stringify(account);
const _password = JSON.stringify(password);
setTimeout(() => {
   const webView = currentWebview.children()[0];
   webView.evalJS(`setAccountInfo(${_account}, ${_password})`);
}, 1000);

四、性能极差的canvas转图片

自定义组件库里包含手写签名组件,需要用到uni.canvasToTempFilePathcanvas转成图片,这个方法的生成基础图片大小是根据当前屏幕分辨率,在模拟器上运行感觉性能还可以,但是在真机上的性能不高,如果笔画多的话,有时需要十几秒时间,这是没办法接受的,不过也有解决方式,可以通过设置destWidthdestHeight来自定义图片生成的大小,牺牲一些图片清晰度,来提高性能。

uni.canvasToTempFilePath(
  {
     canvasIdthis.canvaId,
     destWidththis.imgWidth,
     destHeightthis.imgHeight,
     success: (res) => {
       console.log('success')
    },
     fail(e) {
       console.error(e);
    },
  },
   this,
);

小结

我目前主要负责公司uni-app移动端框架的开发,包含组件库相应的生态工具多端适配离线存储hybrid,如果你也正在做相同的事,或者在使用uni-app开发,或者在学习uni-app都可以相互探讨,在这踩坑的过程中,我会持续完善此系类文章,帮助大家和自己更好的使用uni-app开发项目,fighting~

作者:清欢bx
来源:juejin.cn/post/7156017191169556511

收起阅读 »

三个多月,被现实雪藏了的锐气

距离 7.15 已经过去了三个多月。体内的热情不能说熄灭,但不足以点燃残留的激情。三个多月不算长,没能把梦想耗尽,遗憾变少;也不算短,没能让想法付出实践,自由变得可贵。回看自己的脚印,有延续,也有分叉,但只有少数几步能够留下青草的芳香,大部分脚印风干后让人难以...
继续阅读 »

距离 7.15 已经过去了三个多月。体内的热情不能说熄灭,但不足以点燃残留的激情。三个多月不算长,没能把梦想耗尽,遗憾变少;也不算短,没能让想法付出实践,自由变得可贵。回看自己的脚印,有延续,也有分叉,但只有少数几步能够留下青草的芳香,大部分脚印风干后让人难以辨认,甚至不相信是从前的自己。像溅到了油渍的白衬衫,但不同的是,白衬衫还有机会回到出厂时的样子。

二十多岁也不是一个少不经事的年纪了,随着自我意识变强,属于自己的世界观正在构建。慢慢的对生活、工作、理想有了自己的看法。当发现课本上所教授的和社会需要你所掌握的相差甚远,自己也就有了想逃避的情绪。不愿随波逐流,像片枯黄的叶子飘落在水中,没有方向。路人不会因为一片叶子而驻足,或许有少部分人会感叹,在寒冬里积极汲取的养分敌不过春去秋来的自然规律和经典力学中的万有引力。


手的一个作用就是捂住耳朵,隔绝噪音

三个多月的时间,锻炼了写作,结识了新朋友,捡起了读书时最爱的篮球,经历疫情,重温友情,放空自己。

惊喜

写作是带来惊喜最大的尝试,分享技术的同时加深了对于某项知识点的理解,并且还能提升自己的文笔,让自己在面试中多一个加分项。在这个过程中,自己还加了一位远在厦门独立开发者的微信,大家分享想法,交流行业的运作和进程,交换资源,挺好。虽然写作没能带来实际的收益,但也让我在点滴生活中找到一点满足感。在这也想问下,大家最近是都成了 优秀毕业生吗 ?两个多月前写的一篇 一位 98 年程序员离职后 的阅读量和点赞量最近又多了起来,是不是当看到 离职 这个字眼,都想点进去看两眼。当然我也希望各位能从中找到了一些方向和归属感,让自己疲惫的心能得到片刻的缓解。毕竟大家在两微一抖上看到了很多的精英人士,觉得全世界都在挣着自己的钱。

友情

联系了初中同学、大学同学。先说说初中同学,距今认识已超过 10 年。初中时,在同一屋檐下共同生活了三年,也有幸 “同床异梦” 过。到现在也还记得初中时那啼笑皆非的日子,一起罚站过,像展品一样立在操场被路过的人打量;一起挑灯夜聊,讲着班里的女生和男生,调侃授课老师,一遍遍模仿着他们略带喜感的动作和回荡在耳边的经典语录;期末前在宿舍厕所嚼着白加黑,背着让人痛不欲生的课文;上午的最后一声下课铃响后,等在对方的桌前或课室外,一起走向校园中承载了不少话题量的饭堂。

毕业后的初中同学也还是会有联系。前些日子和他们吃了几顿饭。我想想啊,有东南亚菜、江浙菜、新疆菜、顺德菜、粤菜。对这几个菜系排序,粤菜 = 顺德菜 > 东南亚菜 > 新疆菜 > 江浙菜。


西湖龙井虾,姿色不错,口感上个人认为没什么很特别的


顺德醉鹅,肉多,对得起这价


新疆烤包子,牛肉馅的,皮薄馅多,不错


罗布泊烤鱼,唯一一道让我们产生分歧的菜,只因我只吃了我面前的那面,他吃另一面


咕噜肉,酸甜的口感,不腻

一个进入了广告业,刚找到了工作,结束了家里蹲的日子;另一个成了编导,不加班,目前积累作品中;而我正试图通过写作和运动摆脱焦虑和迷茫带来的副作用。

大学同学当了两年兵,九月份退伍回来。外表变化不大,但性格变了挺多。大学做事时会想的比较多,属于那种给了机会都要思前想后的,现在呢?没有机会,创造机会都要上,胆子大了不少。在兵营里不仅身体得到了锻炼,心态更是被蹂躏到需要推倒重建。跟他交流时,能感觉到和社会脱离的有点久,有些事情想的过于简单。但对朋友,他还像从前那样。

篮球

女篮世界杯夺得了第二名的好成绩,国庆在家看了世界杯的几场比赛。比男篮强的不是一星半点,不管是队内的配合、队员的基本功、防守和进攻的态度都让我感觉女篮未来可期。这次世界杯也让我回忆起以前那块让我无比留恋的场地,不管是水泥地还是塑胶地,篮架是崭新还是磨损,边界是清晰还是模糊,这些元素加起来都足以让那时的我顶着烈日,不厌其烦的追逐着那颗用青春编织起的篮球,一上一下也像极了年轻人那有力的心跳。


我追过的人不多,但追过的球,不少

上班后打球的时间呈指数级下降。原以为对篮球的热爱就到这了,但把手从键盘放到篮球上时,体内有关它的一切都被唤醒了。


灯光一点都不耀眼

疫情后踏上球场,竟然有种疫情从未发生过的错觉。不用戴口罩,每个人分享着球权,对抗时肌肉之间的碰撞让人忘记了在这两年里不停被提起的一米安全线。如果有读者也在广州,也爱打球,可以私信我约场球,让自己痛快一场,酣畅淋漓。

放空自己

脑子空着的时候,大部分想的是创业的东西。创业这个想法从大学时期就有了,但不具备所需的条件,于是一门心思的想横向扩展。结果出来后又开始纠正大学时的想法,横向扩展行不通,个人认为一万小时定律忽略了实际环境对结果的影响,于是决定踹开这扇门。鉴于本人对于创业还是萌新一个,就不花篇幅了。有兴趣的,私下交流。

放空自己的时候,除了想创业,也想过其他东西。比如,自己的优势是啥,如何能把优势更好的发挥出来,如何说服自己目前是个平凡人的事实,为啥 boss 直聘上一堆已读未回等等。

作者:对方正在输入
来源:juejin.cn/post/7158817364467777550

收起阅读 »

离职交接,心态要好

话说今年经历了几次项目交接?主动和被动的都算!01实在是没想到,都到年底快收尾的时候,还要突然接手离职人员的项目;不断拉扯和管理内心情绪,避免原地裂开;年度中再次经历突发的交接事宜,并且团队要在极短的时间内完成所有事项的交接流程;毫无征兆的变动必然会引起一系列...
继续阅读 »

话说今年经历了几次项目交接?主动和被动的都算!

01

实在是没想到,都到年底快收尾的时候,还要突然接手离职人员的项目;

不断拉扯和管理内心情绪,避免原地裂开;

年度中再次经历突发的交接事宜,并且团队要在极短的时间内完成所有事项的交接流程;

毫无征兆的变动必然会引起一系列问题,最直接的就是影响团队现有节奏进度,需要重新调整和规划;

人员的小规模变动,对部门甚至公司产生的影响是显而易见的,道理都懂;

但是从理性上思考,这个问题并非是无解的,是可以在各个团队中,进行内部消化的;

而人力减少带来的成本降低,以及确保公司的可持续,这是极具确定性的,也是核心目的;

所以感性上说,这个梦幻的职场,可能真的是"爱了";

02

如果是常规情况下的离职流程,交接并不是一件复杂的事情,因为有时间有心情来处理这事,好聚好散;

然而最骚的是,奇袭一般的裁员手段,几分钟谈话结束直接走人;

丝毫不顾及由此带来的影响,认定留下的人应该兜底相应的责任,实现无缝接坑;

当然并不是什么公司都有底气这么做的,大部分还是在裁员通知后,留有一定的时间处理交接事项;

对于交的过程是否有质量,完全看接的一方是否聪明;

从感性上分析,都已经被裁了自然要牢牢把握摸鱼的机会,根本不会在意交出的事项谁来维护,不反越防线就不错了;

而压力会直接传送后闪现到接的人正上方;

03

面对被动离职的交接,确实很难妥善处理,情绪化容易导致事情变质,能真正理性对待的并不多;

交接涉及到三方的核心利益:公司、交出人、接手人,不同角度对待这件事件,态度完全不同;

公司,并不关心交接的质量,只要项目有人兜底即可;

交出方,感性上说直接敷衍交接单上的流程即可,并不在意后续的影响;

接手方,项目交接完成后的第一责任人,可能会关心项目的质量状况;

至于说接手的人能否有时间,有能力,有心情接下这种天降大任,可能除了自己以外,不到出问题的时候关注的很少;

因为项目交接过程没有处理好,从而导致后续的事故与甩锅,情绪化的现象并不少见;

如果是在内部矛盾突出的团队中,由此引发的离职效应也并不少见;

04

人的情绪真的是很奇怪,能让复杂的事情变的简单,也能让简单的事情变的离谱;

情绪上头的时候,事情本身是否真的复杂就已经不太重要了;

接手方最大的问题在于吃力不讨好,如果接了一个质量奇差的项目,意味之后很长一段时间内,工作状态都会陷入混乱的节奏中;

对于大部分研发团队来说,都是存在排期规划的,如果被交接的项目横插一脚,重新调规划影响面又偏大;

向上反馈,多半是回答一句:自行消化;

何谓自行消化,就是占用空闲时间处理,比如下班后,比如周末,比如摸鱼,这些都是对工作情绪的持续伤害;

最终兜底的个人或者团队,可能需要带着夜宵去公司搬砖;

05

吐槽归吐槽,裂开归裂开,成熟的搬砖人不该表现出明显的情绪化;

先捋一捋在面对离职交接时的注意事项,虽然说离职后有一个过渡期,但是真正涉及交接的时间通常一周左右;

作为接手一方,自然期待的是各种文档齐全,对于坑坑洼洼的描述足够清楚;

然而对于被离职的交出方,会带着若隐若现的情绪化状态,很难用心处理交接事项,能不挖坑就已经是良心队友了;

接手方作为后续的兜底人员,兜不住就是一地鸡毛;

如果兜住了呢?那是职责所在、理所应当、不要多想、安心搬砖;

06

面对项目交接,这种隔三差五个月就会突发的事,完全可以用一套固定的模式和节奏去执行;

强烈建议:不排斥、不积极、不情绪化;

但是在处理的过程中要理性且严谨,这样可以规避掉许多可能出现的麻烦,毕竟签了交接单,从此该项目问题根本甩不开;

职场几年,在多次"交"与"接"的角色转换过程中,总结以下几点是研发需要注意的;

P1:文档,信息的核心载体;

不管项目涉及多少文档,照单全收;

如果文档严重缺失甚至没有,直接在交接单上写明情况,并且得加粗划重点展示;

文档和项目的维护极有可能是线性不相关,但是手有文档心里不慌,因为方便后续再把项目交接给其他人;

所以,敷衍一时爽,出事火葬场;

07

P2:代码工程,坑与不坑全看此间;

接到手里的项目,是否会导致情绪崩塌,全看项目代码工程的质量,遇上一堆烂摊子,心情会持续的跌跌跌,然后裂开;

直接把人打包送走的情况也并不少见;

如果代码工程质量极高,架构设计稳定,组件集成比较常规,分包井然有序,悬着的情绪可以适当下落;

P3:库表设计,就怕没注释;

对于数据库层面的设计,与代码工程和业务文档三者相辅相成,把握其中的主线逻辑即可;

但前提是表的设计得有清晰的注释,如果是纯中式英文混搭拼音,且缺乏注释,必然会成为解决问题的最佳卡点;

P4:核心接口,应当关注细节;

从项目的核心业务中选出2-3个复杂的接口读一读;需要将注意点放在细节逻辑上,给内心积蓄一丢丢解决问题的底气;

熟悉接口的基本思路:请求从客户端发出,业务服务的处理逻辑,对数据层面的影响,最终响应的主体;

08

P5:遗留问题,考验职场关系的时候到了;

公司一片祥和的时候,员工之间还可以做做样子;

但是已经走到了一别两宽的地步,从感性上来说只要不藏着掖着就行,还想窥探别人安稳摸鱼的秘密,确实想的不错;

老练的开发常干的事,为了解决某个问题临时上线一段代码,处理好后关闭触发的入口,但是会保留代码主体;

这还算常规操作,最骚的是在本地写一段脚本工具解决线上的问题;

这些隐藏的接口和脚本只有开发的人自己清楚,如果不给个说明文档,这不单是挖坑,还顺手倒了一定比例的水进行混合;

P6:结尾事项,寒暄几句还是要的;

安全意识好的公司,会对员工的账号权限做好备份,以便离职时快速处理,不会留下风险隐患;

在所有权限关闭之后,接手人就可以在交接单上完成签字仪式;

交接完成后还是得适当的寒暄几句,万一接了个坑,转头就得再联系也不稀奇,所以职场留一线方便语音再连线;

09

年度收到的离职交接,已经累计好几份,对这种事情彻底麻了;

事来了先兜着,等兜不住的时候自然会有解决办法;

抗拒与烦躁都不会影响流程的持续推进,这种心态需要自己用清醒的意识不断的说服自己;

最后想探讨一个话题,跟项目前负责人联系,用什么话术请教问题,才能显得不卑不亢?

作者:知了一笑
来源:juejin.cn/post/7157651258046677029

收起阅读 »

Flutter之事件节流、防抖封装

在应用开发过程中经常会遇到因用户短时间内连续多次重复触发某个事件,导致对应事件的业务逻辑重复执行而出现业务异常,此时就需要对事件进行节流或者防抖处理避免出现业务异常。本文将介绍在 Flutter 开发中如何实现节流和防抖的统一封装。 前言 首先我们来了解一下节...
继续阅读 »

在应用开发过程中经常会遇到因用户短时间内连续多次重复触发某个事件,导致对应事件的业务逻辑重复执行而出现业务异常,此时就需要对事件进行节流或者防抖处理避免出现业务异常。本文将介绍在 Flutter 开发中如何实现节流和防抖的统一封装。


前言


首先我们来了解一下节流和防抖的定义,以及在什么场景下需要用到节流和防抖。


节流


节流是在事件触发时,立即执行事件的目标操作逻辑,在当前事件未执行完成时,该事件再次触发时会被忽略,直到当前事件执行完成后下一次事件触发才会被执行。


throttle.png


按指定时间节流


按指定时间节流是在事件触发时,立即执行事件的目标操作逻辑,但在指定时间内再次触发事件会被忽略,直到指定时间后再次触发事件才会被执行。


throttle-timeout.png


防抖


防抖是在事件触发时,不立即执行事件的目标操作逻辑,而是延迟指定时间再执行,如果该时间内事件再次触发,则取消上一次事件的执行并重新计算延迟时间,直到指定时间内事件没有再次触发时才执行事件的目标操作。


debounce.png


使用场景


节流多用于按钮点击事件的限制,如数据提交等,可有效防止数据的重复提交。防抖则多用于事件频繁触发的场景,如滚动监听、输入框输入监听等,可实现滚动停止间隔多久后触发事件的操作或输入框输入变化停止多久后触发事件的操作。


效果


先看一下最终封装完成后的使用示例及效果,实现计数器功能,对点击分别进行节流、指定时间节流、防抖限制。


/// 事件目标操作
void increase() {
count += 1;
}

/// 节流
() async{
await Future.delayed(Duration(seconds: 1));
increase();
}.throttle()

/// 指定时间节流
increase.throttleWithTimeout(2000)

///防抖
increase.debounce(timeout: 1000)


increase 是事件目标操作,即这里的数字加一,分别进行节流、指定时间节流、防抖限制,调用封装的 throttlethrottleWithTimeoutdebounce 扩展方法实现。其中节流为了模拟事件耗时操作增加了一秒延迟。


实现效果:


flutter-throttle-demo.gif


实现


接下来将通过从单事件的节流/防抖限制到封装抽取一步一步实现对节流和防抖的通用封装。


简单节流实现


首先来看一下节流的简单实现,前面讲了节流的原理,就是在事件未执行完成时忽略事件的再次触发,根据这个原理添加一个变量标识事件是否可执行,默认为 true 可执行,当事件执行时设置为 false,执行完成后重新设置为 true,当标识为 false 时忽略事件,这样就实现了对事件的节流,代码实现如下:


Future increase() async{
count += 1;
await Future.delayed(const Duration(seconds: 1));
}

/// 事件是否可执行
bool enable = true;

void throttleIncrease() async{
if(enable){
enable = false;
await increase();
enable = true;
}
}

添加一个 enable 变量标识事件是否可执行。这里为了模拟事件的耗时操作在 increase 方法里添加了一秒的延时。这样就简单实现了事件的节流,运行看一下效果:


flutter-throttle-simple.gif


节流封装


通过上面的简单代码实现了对事件的节流,但是只对某一个确定的事件有效,如果还有其他事件也需要实现节流效果那就得重新写一遍上面的代码,这样很明显是不科学的。那么我们就需要对上面的代码进行封装,使其能应用到多个事件上。


上面的代码事件调用是直接写在节流的实现里的,那么将事件进行抽象,把事件的具体执行方法抽取为一个参数,这样就能满足多个事件的节流控制了,实现如下:


bool enable = true;
void throttle(Function func) async{
if(enable){
enable = false;
await func();
enable = true;
}
}

/// 使用
throttle(increase);

throttle(decrease);

进一步封装


经过前面的封装后,确实可以对多个事件进行节流限制,但在实际开发过程中发现有两个问题:


**问题一:**所有事件的节流控制使用的是一个 enable 变量控制,这样就会导致在事件 1 执行过程中事件 2 会被忽略,这显然不是我们想要的效果。


举一个典型的场景,在 Flutter 中跳转新页面并获取页面的返回值,此时实现如下:


Future toNewPage() async{
var result = await Navigator.pushNamed(context, "/newPage");
/// do something
}

此时如果对 toNewPage 进行节流控制,并且跳转的页面里的按钮事件也做了同样的节流控制,就会导致新界面的按钮事件无法执行,因为我们节流用的是同一个变量进行控制,而 toNewPage 需要接收页面返回值,事件未执行完一直在等待页面返回值导致 enable 变量一直为 false 所以新界面的点击事件就会被忽略。


**问题二:**当事件的执行报错,会导致后续所有使用该方式节流的事件都不会被触发。原理跟上面的一样,当事件执行报错时不会继续向下执行,此时 enable 无法赋值为 true,一直为 false 从而导致后续事件都不会被执行。


怎么解决上面两个问题呢?首先解决简单的问题二,问题二很好解决,加一个 try-catch-finally 即可:


void throttle(Function func) async{
if(enable){
enable = false;
try {
await func();
} catch (e) {
rethrow;
} finally {
enable = true;
}
}
}

在方法调用上增加 try-catch-finally ,在 finally 中将 enable 设置为 true,在 catch 中不对异常做任何处理,使用 rethrow 将异常重新抛出去即可,这样就解决了问题二。


再来看问题一,既然使用同一个 enable 会有问题,那就使用多个变量来控制,每个事件用一个 enable 变量来控制,实现如下:


Map _funcThrottle = {};

void throttle(Function func) async{
String key = func.hashCode.toString();
bool enable = _funcThrottle[key] ?? true;
if(enable){
_funcThrottle[key] = false;
try {
await func();
} catch (e) {
rethrow;
} finally {
_funcThrottle.remove(key);
}
}
}

使用一个 Map 来存放事件的 enable 变量,使用事件方法的 hashCode 作为事件的 key,这样就解决了问题一。


但实际开发过程中发现还是有问题,封装后的 throttle 方法在使用时有下面两种方式:


/// 1
Future increase() async{
count += 1;
await Future.delayed(const Duration(seconds: 1));
}

throttle(increase);

/// 2
throttle(() async{
count += 1;
await Future.delayed(const Duration(seconds: 1));
});

使用第一种方式时是没有问题,但是第二种发现就有问题,节流不起作用了,为什么呢?是因为第二种使用的是匿名函数或者叫 lambda 函数,这种方式每次触发事件相当于都会重新创建一个函数参数传入 throttle 就会导致 func.hashCode.toString() 获取的值每次都不一样,所以导致节流无效。


那这种情况又该怎么解决呢?首先想到的是给 throttle 增加一个参数 key ,不同的事件传入不同的 key 值。这样确实能解决问题,但是增加了使用成本,每个事件都得传入一个 key,对于已有代码改造也相对来说不方便。于是想到了另外一种解决办法,也是本方案最终实现的方法,用一个对象来代理执行事件,具体实现如下:


class FunctionProxy {
static final Map _funcThrottle = {};
final Function? target;

FunctionProxy(this.target);

void throttle() async {
String key = hashCode.toString();
bool enable = _funcThrottle[key] ?? true;
if (enable) {
_funcThrottle[key] = false;
try {
await target?.call();
} catch (e) {
rethrow;
} finally {
_funcThrottle.remove(key);
}
}
}
}

创建一个方法的代理类,在该类里实现 throttle ,此时使用的 key 为该代理类的 hashCode , 使用如下:


onPressed: FunctionProxy(increase).throttle
/// or
onPressed: FunctionProxy(() async{
count += 1;
await Future.delayed(const Duration(seconds: 1));
}).throttle

这样最终返回给 onPressed 的是 FunctionProxythrottle 函数,而 throttle 是一个确定的函数,这就最终解决了上述问题。


但是使用时每次都创建 FunctionProxy 类,看着不太友好,给 Function 增加一个 throttle 方法,让使用更加简单:


extension FunctionExt on Function{
VoidCallback throttle(){
return FunctionProxy(this).throttle;
}
}

/// 使用
Future increase() async{
count += 1;
await Future.delayed(const Duration(seconds: 1));
}

onPressed: increase.throttle()

onPressed: () async{
count += 1;
await Future.delayed(const Duration(seconds: 1));
}.throttle()

指定时间节流


节流是事件执行完后才允许下次事件执行,指定时间节流是事件开始执行指定时间后允许下次事件执行,使用延迟指定时间后将 enable 设置为 true 来实现,代码如下:


class FunctionProxy {
static final Map _funcThrottle = {};
final Function? target;

final int timeout;

FunctionProxy(this.target, {int? timeout}) : timeout = timeout ?? 500;

void throttleWithTimeout() {
String key = hashCode.toString();
bool enable = _funcThrottle[key] ?? true;
if (enable) {
_funcThrottle[key] = false;
Timer(Duration(milliseconds: timeout), () {
_funcThrottle.remove(key);
});
target?.call();
}
}
}

增加了 timeout 参数,即指定的节流时间,使用 Timer 延迟指定时间后将 key 从 _funcThrottle 中移除,这里没有加 try-catch ,因为事件异常并不会影响 Timer 的执行,同样的为 Function 增加一个 throttleWithTimeout 扩展:


extension FunctionExt on Function{
VoidCallback throttleWithTimeout({int? timeout}){
return FunctionProxy(this, timeout: timeout).throttleWithTimeout;
}
}

使用:


Future increase() async{
count += 1;
await Future.delayed(const Duration(seconds: 1));
}
onPressed: increase.throttleWithTimeout(timeout: 1000)

onPressed: () async{
count += 1;
await Future.delayed(const Duration(seconds: 1));
}.throttleWithTimeout(timeout: 1000)

防抖


防抖是在事件触发指定时间内该事件未再次触发时再执行,同样可以使用 Timer 来实现:


class FunctionProxy {
static final Map _funcDebounce = {};
final Function? target;

final int timeout;

FunctionProxy(this.target, {int? timeout}) : timeout = timeout ?? 500;

void debounce() {
String key = hashCode.toString();
Timer? timer = _funcDebounce[key];
timer?.cancel();
timer = Timer(Duration(milliseconds: timeout), () {
Timer? t = _funcDebounce.remove(key);
t?.cancel();
target?.call();
});
_funcDebounce[key] = timer;
}
}

同样增加 timeout 参数用于指定防抖的时间间隔,与节流不同的是防抖的 Map 的 value 不是 bool 类型而是 Timer 类型,当事件触发时创建一个 Timer 设置延迟 timeout 后执行,并将 Timer 添加到 Map 中,如果在 timeout 时间内事件再次触发则将 Map 中的 Timer 取消再重新创建新的 Timer,从而实现防抖效果。


同样为 Function 添加 debounce 防抖扩展方法:


extension FunctionExt on Function{
VoidCallback debounce({int? timeout}){
return FunctionProxy(this, timeout: timeout).debounce;
}
}

使用:


Future increase() async{
count += 1;
await Future.delayed(const Duration(seconds: 1));
}
onPressed: increase.debounce(timeout: 1000)

onPressed: () async{
count += 1;
await Future.delayed(const Duration(seconds: 1));
}.debounce(timeout: 1000)

完整代码


至此节流、防抖的封装就完成了,完整代码如下:


extension FunctionExt on Function{
VoidCallback throttle(){
return FunctionProxy(this).throttle;
}

VoidCallback throttleWithTimeout({int? timeout}){
return FunctionProxy(this, timeout: timeout).throttleWithTimeout;
}

VoidCallback debounce({int? timeout}){
return FunctionProxy(this, timeout: timeout).debounce;
}
}

class FunctionProxy {
static final Map _funcThrottle = {};
static final Map _funcDebounce = {};
final Function? target;

final int timeout;

FunctionProxy(this.target, {int? timeout}) : timeout = timeout ?? 500;

void throttle() async {
String key = hashCode.toString();
bool enable = _funcThrottle[key] ?? true;
if (enable) {
_funcThrottle[key] = false;
try {
await target?.call();
} catch (e) {
rethrow;
} finally {
_funcThrottle.remove(key);
}
}
}

void throttleWithTimeout() {
String key = hashCode.toString();
bool enable = _funcThrottle[key] ?? true;
if (enable) {
_funcThrottle[key] = false;
Timer(Duration(milliseconds: timeout), () {
_funcThrottle.remove(key);
});
target?.call();
}
}

void debounce() {
String key = hashCode.toString();
Timer? timer = _funcDebounce[key];
timer?.cancel();
timer = Timer(Duration(milliseconds: timeout), () {
Timer? t = _funcDebounce.remove(key);
t?.cancel();
target?.call();
});
_funcDebounce[key] = timer;
}
}

点击组件封装


完成对节流、防抖的封装后,我们还可以对点击组件进行封装,这样不管是对现有 Flutter 代码还是新代码增加节流、防抖功能都会更加的简单。比如对 GestureDetector 组件可做如下封装:


enum ClickType {
none,
throttle,
throttleWithTimeout,
debounce
}

class ClickWidget extends StatelessWidget {
final Widget child;
final Function? onTap;
final ClickType type;
final int? timeout;

const ClickWidget(
{Key? key,
required this.child,
this.onTap,
this.type = ClickType.throttle,
this.timeout})
: super(key: key);

@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
child: child,
onTap: _getOnTap(),
);
}

VoidCallback? _getOnTap() {
if (type == ClickType.throttle) {
return onTap?.throttle();
} else if (type == ClickType.throttleWithTimeout) {
return onTap?.throttleWithTimeout(timeout: timeout);
}else if (type == ClickType.debounce) {
return onTap?.debounce(timeout: timeout);
}
return () => onTap?.call();
}
}

增加 type,用于指定节流、指定时间节流、防抖或者不限制,分别调用对应的方法。默认为节流,可根据项目实际需求设置默认方式或对项目中使用到的其他点击组件进行封装,经过封装后,修改已有代码增加默认限制功能就可以直接替换组件名字而无需改动其他代码实现事件限制的功能。


使用:


/// before
GestureDetector(
child: Text("xxx"),
onTap: increase,
)
/// after
ClickWidget(
child: Text("xxx"),
onTap: increase,
)

ClickWidget(
child: Text("指定时间节流"),
type: ClickType.throttleWithTimeout,
timeout: 1000,
onTap: increase,
)

ClickWidget(
child: Text("防抖"),
type: ClickType.debounce,
timeout: 1000,
onTap: increase,
)

总结


开发过程中,大部分的事件处理都需要进行节流或者防抖限制,防止事件的重复处理导致业务的异常,经过封装后不管是对老项目的优化改造还是新项目的开发,节流和防抖的处理都将变得更简单快捷。


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

常见的Android编译优化问题

编译常见问题 在开发过程中,有碰到过一些由于编译优化导致的代码修改并不符合我们预期的情况。这也就是之前为什么我经常说编译产物其实是不太可以被信任的。 方法签名变更,底层仓库的方法变更但是上层模块并没有跟随一起重新编译导致的这个问题。 常量优化,将一些常量的调...
继续阅读 »

编译常见问题


在开发过程中,有碰到过一些由于编译优化导致的代码修改并不符合我们预期的情况。这也就是之前为什么我经常说编译产物其实是不太可以被信任的。



  1. 方法签名变更,底层仓库的方法变更但是上层模块并没有跟随一起重新编译导致的这个问题。

  2. 常量优化,将一些常量的调用点直接替换成常量的值。

  3. 删除空导包, 没有用的一些导包就会做一次剔除。


最近倒霉了


我们最近碰到一个pipeline相关而且很妖怪的问题。我们一个pipeline会检查apk产物中是否存在异常的方法调用,就是之前介绍的在R8的基础上开发出来的A8。但是最近有一个类被删除了之后呢,但是代码中还有一处调用点。但是这个检测竟然被通过了,然后这部分代码就被合入了master。


image.png


这个引用的文件就如上图所示,是一个debug buildType中的,所以并不是所有的apk中都会存在这部分代码。


然后呢,这个MergeRequest就被合入了master分支,因为当天是我们出下一个版本包的时间,然后交付给测试的就是全量编译的debugrelease包。别的开发同学rebase完master之后就发现piepline都跑不过了,就导致了他们当天的代码无法被合入。


这个就是事情大概的起因和经过,但是各位有没有想过为什么会发生这个问题吗。这个是不是我们的pipeline出现了bug,导致了这种问题无法被识别出来了呢。


以前有说过,如果简单的说我们的快编系统就是把模块替换成对应的aar,从而达到编译提速。所以因为我们使用的是这个模块对应的aar产物,所以大概率就是因为这个模块的编译产物和源代码有差异导致了这个问题。


其实这个问题一出现我就已经知道大概率是由空导包优化导致的这个问题,因为在pipeline检查的时候,检测的apk产物中确实不存在这个导包。因为我们使用的是一个历史版本的aar,其中无效导包的部分已经被编译器做了删除空导包的优化了。接下来我们看下我写的一个demo中的无效导包。


image.png


image.png


图一呢是源代码java文件,图二呢则是jar包中的代码。可以简单的看出来行号呢是可以对应的上的,但是这个AppCompatActivity的无效导包在产物中已经被优化掉了。这里也就回答了在编译过程中会保留行号,但是也会优化掉一部分不需要的代码,让我们编译出来的产物更小。


所以也就导致了我们的产物和我们的源代码之间的差异,另外一个角度就是说从apk中我们确实是不存在这个类的导包。但是呢在我们把这部分代码重新编译成aar的时候,就会出现source缺失,导致的语法树无法生成,之后导致的编译失败问题。


这也就是所以我一直和大家说编译产物是不可以被信任的呢。


以前倒霉过


这个是之前的一个故事了,我们之前呢在模块中定义了一些静态常量吧,然后用来标识当前SDK的版本,然后这个值在别的模块中被引用到了。


有一次因为需求变更,我们更改了这个静态变量的值,然后呢我就把这个需求提测了。之后测试反馈给我为什么这边的这个值没有变化啊。


image.png


我的天,当时我就是这样,发生了什么情况。然后呢我全量打了个包好了,我当时也就以为只是编译时的一个bug而已。然后后来呢,我查了下资料发现这个就是一个java编译时的常量优化问题。过了一阵子吧,我面试了下字节跳动,然后我和面试官也聊了下这个话题,然后呢在这个方法签名变更的问题上,当时我略输一筹,哈哈哈哈。接下来我们就看下一个demo。


image.png


image.png


图1呢也是java代码,图2呢则是aar中的编译产物。其中我们可以看到,这个静态常量在编译成产物之后就会被编译成这样。


所以这个就解释了我一开始碰到的这个问题,他就是由于我们的编译器已经把aar中的这部分静态常量编译成了直接的值,然后呢我们的源变化之后如果没有重新编译对应的模块,就会导致这个值一直无法被更新到最新的值。


结论


如果大家对安卓编译相关有兴趣的话,这些问题很可能都会在面试的时候被问到。希望这不仅仅只是一篇我对于这些问题的思考,也能对各位有所帮助吧。


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

KeyValueX:消除样板代码,让 Android 项目不再 KV 爆炸

背景源于深夜一段独白:Key Value 定义几十上百个是常见事,目前有更简便方式么,此为项目中为数不多不受控制之地,指数膨胀,且易埋下一致性隐患,每新增一 value,需兼顾 key、get、put、init,5 处 …public class Config...
继续阅读 »

背景

源于深夜一段独白:

Key Value 定义几十上百个是常见事,目前有更简便方式么,

此为项目中为数不多不受控制之地,指数膨胀,且易埋下一致性隐患,

每新增一 value,需兼顾 key、get、put、init,5 处 …

public class Configs {
 
...
   
 private static int TEST_ID;
 
 public final static String KEY_TEST_ID = "KEY_TEST_ID";
 
 public static void setTestId(int id) {
   TEST_ID = id;
   SPUtils.getInstance().put(KEY_TEST_ID, id);
}
 
 public static int getTestId() {
   return TEST_ID;
}
 
 public static void initConfigs() {
   TEST_ID = SPUtils.getInstance().getInt(KEY_TEST_ID, 0);
}
}

随后陆续收到改善建议,有小伙伴提到 “属性代理”,并推荐了群友 DylanCai 开源库 MMKV-KTX

与此同时,受 “属性代理” 启发,本人萌生 Java 下 key、value、get、put、init 缩减为一设计。

Github:KeyValueX

V1.0

V1.0 使用 3 步曲

1.如读写 POJO,需实现 Serializable 接口

public class User implements Serializable {
 public String title;
 public String content;
}

2.像往常一样,创建项目配置管理类,如 Configs

//Configs 中不再定义一堆 KEY、VALUE 常量和 get、put、init 静态方法,
//只需一条条 KeyValue 静态变量:

public class Configs {
 public final static KeyValueString accountId = new KeyValueString("accountId");
 public final static KeyValueSerializable<User> user = new KeyValueSerializable<>("user");
}

3.在页面等处通过 get( ) set( ) 方法读写 KeyValue

public class MainActivity extends AppCompatActivity {
...
         
 //测试持久化写入
 Configs.user.set(u);

 //测试读取
 Log.d("---title", Configs.user.get().title);
 Log.d("---content", Configs.user.get().content);
}

V1.0 复盘

KeyValueX v1.0 一出,很快在群里引发热议,群友 DylanCai 提到多模块或存在 KeyName 重复问题,群友彭旭锐提议通过 “注解” 消除重复等问题。

与此同时我也发现,KeyValueX 虽然已消除 key、value、get、put、init 样板代码,但还是有两处一致性问题:

final 修饰符和 KeyName 一致性,

—— final 在 Java 下必写,以免开发者误给 KeyValue 直接赋值:

public class MainActivity extends AppCompatActivity {
...
     
 //正常使用
 Configs.user.set(u);
 
 //误用
 Configs.user = u;
}

那么有开发者可能说,我每次新增 KeyValue,通过 ctrl + D 复制行不就行了

public class Configs {
 public final static KeyValueString accountId = new KeyValueString("accountId");
 public final static KeyValueBoolean isAdult = new KeyValueBoolean("accountId");
}

确实这样可解决 final 一致性,但同时也会滋生 KeyName 一致性问题,也即记得改 KeyValue 变量名,忘改 KeyName,且这种疏忽编译器无法发现,唯有线上出事故专门排查时才可发现。

故综合多方面因素考虑,v2.0 采取注解设计:

V2.0

V2.0 使用 3 步曲

1.创建 KeyValueGroup 接口

@KeyValueGroup
public interface KeyValues {
 @KeyValue KeyValueInteger days();
 @KeyValue KeyValueString accountId();
 @KeyValue KeyValueSerializable<User> user();
}

2.像往常一样,创建项目配置管理类,如 Configs

//Configs 中不再定义一堆 KEY、VALUE 常量和 get、put、init 静态方法,
//只需一条 KeyValueGroup 静态变量:

public class Configs {
 public final static KeyValues keyValues = KeyValueCreator.create(KeyValues.class);
}

3.在页面等处通过 get( ) set( ) 方法读写 KeyValue

public class MainActivity extends AppCompatActivity {
...
         
 //测试持久化写入
 Configs.keyValues.user().set(u);

 //测试读取
 Log.d("---title", Configs.keyValues.user().get().title);
 Log.d("---content", Configs.keyValues.user().get().content);
}

V2.0 复盘

V2.0 通过接口 + 注解设计,一举消除 final 和 KeyName 一致性问题,

且通过无参反射实现 KeyValues 的实例化,使写代码过程中无需特意 build 生成 Impl 类,对 build 一次便需数分钟的巨型项目较友好。

根据上图可见,无参反射加载类,耗时仅次于 new,故 V2.0 设计本人还算满意,已在 Java 项目中全面使用,欢迎测试反馈。

Github:KeyValueX

KeyValue-Dispatcher

期间群友 DylanCai 提出可改用动态代理实现,也即效仿 Retrofit,根据接口定义运行时动态生成方法,

如此无需声明注解,使接口定义更简洁,看起来就像:

public interface KeyValues {
 KeyValueInteger days();
 KeyValueString accountId();
 KeyValueSerializable<User> user();
}

与此同时,可根据适配器模式实现个转换器,例如转换为 UnPeek-LiveData,如此即可顺带完成高频操作 —— 更新配置后通知部分页面刷新 UI。

public interface KeyValues {
Result<Integer> days();
Result<String> accountId();
Result<User> user();
}

Configs.keyValues.days().observer(this, result -> {
...
});

不过动态代理有个硬伤,即类名方法名不可混淆,不然运行时难调到对应方法,

故动态代理方式最终未考虑,不过转换器设计我甚是喜欢,加之 Java 后端 Properties 启发,故萌生 Dispatcher 设计 ——

基于 MVI-Dispatcher 实现 KeyValue-Dispatcher。具体思路即通过 HashMap 内聚 KeyValue,如此只需声明 Key,而无需考虑 value、getter、setter、init:

KV-D 使用 3 步曲

1.定义 Key 列表

public class Key {
 public final static String TEST_STRING = "test_string";
 public final static String TEST_BOOLEAN = "test_boolean";
}

2.读写

//读
boolean b = GlobalConfigs.getBoolean(Key.TEST_BOOLEAN);
//写
GlobalConfigs.put(Key.TEST_STRING, value);

3.顺带可通知 UI 刷新

GlobalConfigs.output(this, keyValueEvent -> {
 switch (keyValueEvent.currentKey) {
   case Key.TEST_STRING: ... break;
   case Key.TEST_BOOLEAN: ... break;
}
});

依托 MVI-Dispatcher 消息聚合设计,任何由配置变更引发的 UI 刷新,皆从这唯一出口响应。

目前已更新至 MVI-Dispatcher 项目,感兴趣可自行查阅。

KV-D 复盘

KV-D 旨在消除学习成本,让开发者像 SPUtils 一样使用,与此同时自动达成内存快读、消除样板代码、规避不可预期错误。

不过 KV-D 只适合 Java 项目用。如欲于 Kotlin 实现属性代理,还需基于 KeyValueX 那类设计。

于是 KeyValueX 再做升级:

1.简化注解:只需接口处声明此为 KeyValueX 接口,

2.自动分组:以 KeyValueX 接口为单位生成路径 MD5,KeyName 根据 MD5 自动分组,

3.全局内存快读:如 ViewModelProvider 使用,并提供全局内存快读。

V3.0

V3.0 使用 2 步曲

1.创建 KeyValueGroup 接口,例如

@KeyValueX
public interface Configs {
 KeyValueInteger days();
 KeyValueString accountId();
 KeyValueSerializable<User> user();
}

2.在页面等处通过 get( ) set( ) 方法读写 KeyValue

public class MainActivity extends AppCompatActivity {
 private final Configs configs = KeyValueProvider.get(Configs.class);
 
...

 //写
 configs.user().set(user);

 //读
 configs.user().get().title;
 configs.user().get().content;
}

已更新至 KeyValueX 项目,感兴趣可自行查阅。


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

Flutter Clip 用来实现文本标签的效果

Clip 是Material Design的一个 Widget,用来实现标签效果,Clip中通过属性可配置一个文本、完整的 Widget、一个动作(比如按钮点击)。 1 基本使用效果如下 class ClipHomeState extends State {...
继续阅读 »

Clip 是Material Design的一个 Widget,用来实现标签效果,Clip中通过属性可配置一个文本、完整的 Widget、一个动作(比如按钮点击)。


1 基本使用效果如下


在这里插入图片描述


class ClipHomeState extends State {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Clip")),
body: const Center(
///-----------------------------------------------------
///核心代码
child: Chip(
//左侧的小组件
avatar: CircleAvatar(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
child: Text('早'),
),
//右侧的文本
label: Text('Flutter 基础 '),
),

///-----------------------------------------------------
),
);
}
}

Chip 的 avatar 属性配置的是一个Widget,所这里可以组合足够复杂的 Widget合集,在本实例中只是使用了 CircleAvatar,CircleAvatar常用来展示圆形的小头像。


2 结合 Wrap 组件实现多标签效果


class ClipWrapHomeState extends State {
final List<String> _tagList = [
"早起", "晚睡", "测试", "努力",
"不想睡觉", "清晨的太阳", "这是什么", "哈哈"
];

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Clip")),
body: Padding(
///-----------------------------------------------------
///核心代码
padding: EdgeInsets.all(12),
child: Wrap(
spacing: 10,
children: _tagList.map((e) => Chip(label: Text(e))).toList(),
),
///-----------------------------------------------------
),
);
}
}

在这里插入图片描述


3 Clip 的属性概述


3.1 label 文本相关配置

label 是用来配置文本的,labelStyle是用来设置这个文本的样式,如颜色、大小 、粗细等等,labelPadding是用来设置文本四周的边距的。


  Chip buildChip() {
return const Chip(
//左侧的小组件
avatar: CircleAvatar(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
child: Text('早'),
),

///右侧的文本
label: Text('Flutter 基础 '),
labelStyle: TextStyle(color: Colors.red),
labelPadding: EdgeInsets.only(left: 12, right: 12),
);
}
}

在这里插入图片描述


3.2 右侧的删除按钮配置 deleteIcon

avatar 是用来配置左侧的显示的小 Widget,deleteIcon是用来配置右侧的删除按钮的。


  Chip buildChip() {
return Chip(
//左侧的小组件
avatar: const CircleAvatar(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
child: Text('早'),
),

///右侧的文本
label: Text('Flutter 基础 '),
labelStyle: TextStyle(color: Colors.red),
labelPadding: EdgeInsets.only(left: 12, right: 12),

///
deleteIcon: Icon(Icons.close),
//删除按钮颜色
deleteIconColor: Colors.red,
//长按的提示
deleteButtonTooltipMessage: "删除",
onDeleted: () {
print("--点击了删除");
},
);
}

在这里插入图片描述


3.3 阴影设置


属性 elevation 用来设置阴影高度,shadowColor属性用来设置阴影颜色


Chip buildChip() {
return Chip(
//左侧的小组件
avatar: const CircleAvatar(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
child: Text('早'),
),

///右侧的文本
label: Text('Flutter 基础 '),
labelStyle: TextStyle(color: Colors.red),
labelPadding: EdgeInsets.only(left: 12, right: 12),

///
deleteIcon: Icon(Icons.close),
//删除按钮颜色
deleteIconColor: Colors.red,
//长按的提示
deleteButtonTooltipMessage: "删除",
onDeleted: () {
print("--点击了删除");
},

///
elevation: 10,//阴影 高度
shadowColor: Colors.blue,//阴影颜色
backgroundColor: Colors.grey,//背景色

);
}



完毕


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

Flutter状态管理-Bloc的使用

前言 目前Flutter三大主流状态管理框架分别是provider、flutter_bloc、getx,三大状态管理框架各有优劣,本篇文章将介绍其中的flutter_bloc框架的使用,他是bloc设计思想模式在flutter上的实现,bloc全程全称 bus...
继续阅读 »

前言


目前Flutter三大主流状态管理框架分别是providerflutter_blocgetx,三大状态管理框架各有优劣,本篇文章将介绍其中的flutter_bloc框架的使用,他是bloc设计思想模式在flutter上的实现,bloc全程全称 business logic ,业务逻辑的意思,核心思想就是最大程度的将页面ui与数据逻辑的解耦,使我们的项目可读性、可维护性、健壮性增强。


两种使用模式


首先第一步引入插件:


flutter_bloc: ^8.1.1

引入之后,flutter_bloc使用支持以下两种模式管理。


Bloc模式,分别有ui层(view)、数据层(state)、事件层(event)、逻辑处理层(bloc),适合大型复杂页面使用。这四层结构bloc在源码处就进行了封装处理,所以我们使用的时候是必须要分离出来的,比如eventstate是要强制分开去写的。这也导致了简单页面使用此模式复杂化的问题,所以这种模式对于简单页面是非常没有必要的,但是如果是复杂页面的话,非常建议使用此模式,相信你在处理页面数据逻辑的时候会非常的清晰。

下面我们以计数器为例写个demo,记住bloc模式有四层,且必须分开,我们建四个文件分别代表这四层,

image.png

数据层: 用来存放数据,这个很简单。


/// 数据层
class DemoState {
// 自增数字
late int num;

DemoState init() {
// 初始化为0
return DemoState()..num = 0;
}

DemoState clone() {
// 获取最新对象
return DemoState()..num = num;
}
}

事件层: 用来存放页面所有事件的地方,比如计数器页面只有初始化事件和自增事件。


/// 页面中所有的交互事件
abstract class DemoEvent {}
/// 初始化事件
class InitEvent extends DemoEvent {}
/// 自增事件
class IncrementEvent extends DemoEvent {}

Bloc逻辑处理层: 处理上方数据和事件逻辑的地方,通过源码就能发现作者的意图,泛型里必须分开传入事件和数据,也足以说明这个模式的特点,就是为复杂页面准备的。所以如果写计数器的话,你就会感觉非常没有必要,因为计数器页面很简单,但是当你的state层里的数据非常多且复杂的时候,你就能体会出分开的好处了。
image.png
代码:


/// 逻辑处理层 继承Bloc
class DemoBloc extends Bloc<DemoEvent, DemoState> {
///构造方法
DemoBloc() : super(DemoState().init()) {
/// on 注册所有事件 on固定写法
on<InitEvent>(_init);
on<IncrementEvent>(_add);
}

/// 私有化逻辑方法 暴露Event事件即可
void _init(InitEvent event, Emitter<DemoState> emit) {
// emit方法,通知更新状态 类似于 ChangeNotifier的notifyListeners方法。
emit(state.clone());
}

_add(IncrementEvent event, Emitter<DemoState> emit) {
state.num++;
// 调用emit方法更新状态
emit(state.clone());
}
}

UI层: UI层只负责页面的编写,而无需关心数据的生成,根节点返回BlocProvider,并实现create方法,返回我们的bloc实体类。child实现我们的 UI页面。


/// UI层
class BlocNumPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 以下固定写法
return BlocProvider(
create: (BuildContext context) => DemoBloc()..add(InitEvent()),
child: Builder(builder: (context) => _buildPage(context)),
);
}

Widget _buildPage(BuildContext context) {
// 获取bloc实例
final bloc = BlocProvider.of<DemoBloc>(context);
return Stack(
children: [
Center(
// 对于需要更新状态的组件 外层包裹一个BlocBuilder,传入bloc、state
child: BlocBuilder<DemoBloc, DemoState>(
builder: (context, state) {
// 返回具体ui组件
return Text("点击了${bloc.state.num}次");
},
),
),
Positioned(
bottom: 20,
right: 20,
child: FloatingActionButton(
onPressed: () {
// 调用add方法触发自增事件,
bloc.add(IncrementEvent());
},
child: Icon(Icons.add),
),
)
],
);
}
}

效果:

Oct-30-2022 18-33-01.gif


Cubit模式


Cubit模式,分别有ui层(view)、数据层(state)、逻辑处理层(cubit),相较于bloc模式去掉了event层,适合比较简单的页面。跟bloc模式只是逻辑处理哪里发生了改变,数据层、页面ui层代码一模一样。


可以看到Cubit模式的逻辑就少了很多代码,而且是直接处理数据即可。通过源码,作者意图也很明显,只是传递了数据层。
image.png


/// 写逻辑
class CubitCubit extends Cubit<CubitState> {
CubitCubit() : super(CubitState().init());

///自增
void increment() => emit(state.clone()..num = state.num + 1);
}

其他写法跟Bloc是一样的,就不粘贴了,那看到这有的小伙伴可能就要问了,一个页面要创建3、4个文件,这也太麻烦了吧,高端的程序员往往不会去写一些重复性较高的代码,其实上面的四个文件都是可以通过插件自动生成的,这里下面两个插件,一个是官方的,官方的不会自动生成ui层的文件,一个是小呆呆写的,可以自动生成ui层重复性的代码文件,两者区别不大,推荐小呆呆的,因为可以多生成一个文件。
image.png


最后


Bloc本质上是一种数据逻辑和UI解耦思想,上面的演示只是非常非常简单的用法,就可以看出作者在源码层给我们强制性的设定了非常明确的各个模型,每个模型只专心负责一个事情,这样看起来一个页面会非常的清晰,可以说Flutter_Bloc是一个非常适合大型项目使用的状态管理框架。


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

公司没钱了,工资发不出来,作为员工怎么办?

现在大环境不好,很多公司都会遇到一些困难。如果公司真的有现金流还好,如果没有,还等着客户回款。那么就有点难办了。很多公司会采取延期发工资,先发 80%、50% 工资这样的操作。 员工遇到这种情况,无非以下几种选择。1认同公司的决策,愿意跟公司共同进退。2不认同...
继续阅读 »

现在大环境不好,很多公司都会遇到一些困难。如果公司真的有现金流还好,如果没有,还等着客户回款。那么就有点难办了。很多公司会采取延期发工资,先发 80%、50% 工资这样的操作。


员工遇到这种情况,无非以下几种选择。

1认同公司的决策,愿意跟公司共同进退。

2不认同公司的决策,我要离职。

3不认同公司的决策,但感觉自己反对也没用。所以嘴上先答应,事后会准备去找新的工作机会。

4不认同公司的决策,我也不主动离职。准备跟公司 battle,” 你们这么做是不合法滴 “


你可以代入这个场景看看自己是哪一类。首先由于每个人遇到的真实情况不一样,所以所有的选择只有适合自己的,并没有对错之分。


我自己的应对思路是,抛开存量,看增量。存量就是我在公司多少多少年了,公司开除我要给我 N+1 的补偿。公司之前对我特别特别好,老板对我有知遇之恩等等。你就当做自己已经不在公司了,现在公司给你发了 offer,现在的你是否愿意接受公司开给你的条件?如果愿意,那么你可以选择方案一。如果不愿意,那么你就可以选择方案三。现在这环境,骑驴找马,否则离职后还要自己交社保。还不如先苟着。


为什么不选择方案四?因为不值得,如果公司属于违法操作,你先苟着,后面离职后还是可以找劳动局仲裁的。这样既不耽误你换工作,也不耽误你要赔偿。如果公司是正规操作,那么闹腾也没用,白浪费自己的时间。


离职赔偿还是比较清晰明确的,如果是散伙那可能会牵扯到更多利益。我自己的经验是,不能什么都想着要。当最优解挺难获得的时候,拿个次优解也可以。当然,不管你选择的哪个,我都有一个建议。那就是当一天和尚,敲一天钟。在职期间,还是要把事情干好的,用心并不全是为了公司,更多是为了自己。人生最大的投资是投资自己的工作和事业,浪费时间就是浪费生命。哪怕公司没有事情安排给你做,也要学会自己找事情做。


如果公司后面没钱了,欠的工资还拿得到吗?



我们作为员工是很难知道公司财务状况的,所以出了这样的事就直接去仲裁,最好是跟同事凑齐十个人去,据说会优先处理。公司如果还要做生意,一般会在仲裁前选择和解,大概是分几个月归还欠款。如果公司不管,那么仲裁后,会冻结公司公账。但有没有钱就看情况了。


如果公司账上没钱且股东已经实缴了股本金,那么公司是可以直接破产清算的。公司破产得话,基本上欠员工的钱就没有了。如果没有实缴,那么股东还需要按照股份比例偿还债务。


链接:https://juejin.cn/post/7156242740034928671

收起阅读 »

阿里面试官:请设计一个不能操作DOM和调接口的环境

web
前言四面的时候被问到了这个问题,当时第一时间没有反应过来,觉得这个需求好奇特面试官给了一些提示,我才明白这道题目的意思,最后回答的也是磕磕绊绊后来花了一些时间整理了下思路,那么如何设计这样的环境呢?最终实现实现思路:1)利用 iframe 创建沙箱,取出其中的...
继续阅读 »

前言

四面的时候被问到了这个问题,当时第一时间没有反应过来,觉得这个需求好奇特

面试官给了一些提示,我才明白这道题目的意思,最后回答的也是磕磕绊绊

后来花了一些时间整理了下思路,那么如何设计这样的环境呢?

最终实现

实现思路:

1)利用 iframe 创建沙箱,取出其中的原生浏览器全局对象作为沙箱的全局对象

2)设置一个黑名单,若访问黑名单中的变量,则直接报错,实现阻止\隔离的效果

3)在黑名单中添加 document 字段,来实现禁止开发者操作 DOM

4)在黑名单中添加 XMLHttpRequest、fetch、WebSocket 字段,实现禁用原生的方式调用接口

5)若访问当前全局对象中不存在的变量,则直接报错,实现禁用三方库调接口

6)最后还要拦截对 window 对象的访问,防止通过 window.document 来操作 DOM,避免沙箱逃逸

下面聊一聊,为何这样设计,以及中间会遇到什么问题

如何禁止开发者操作 DOM ?

在页面中,可以通过 document 对象来获取 HTML 元素,进行增删改查的 DOM 操作

如何禁止开发者操作 DOM,转化为如何阻止开发者获取 document 对象

1)传统思路

简单粗暴点,直接修改 window.document 的值,让开发者无法获取 document

// 将document设置为null
window.document = null;

// 设置无效,打印结果还是document
console.log(window.document);

// 删除document
delete window.document

// 删除无效,打印结果还是document
console.log(window.document);

好吧,document 修改不了也删除不了🤔

使用 Object.getOwnPropertyDescriptor 查看,会发现 window.document 的 configurable 属性为 false(不可配置的)

Object.getOwnPropertyDescriptor(window, 'document');
// {get: ƒ, set: undefined, enumerable: true, configurable: false}

configurable 决定了是否可以修改属性描述对象,也就是说,configurable为false时,value、writable、enumerable和configurable 都不能被修改,以及无法被删除

此路不通,推倒重来

2)有点高大上的思路

既然 document 对象修改不了,那如果环境中原本就没有 document 对象,是不是就可以实现该需求?

说到环境中没有 document 对象,Web Worker 直呼内行,我曾在《一文彻底了解Web Worker,十万、百万条数据都是弟弟🔥》中聊过如何使用 Web Worker,和对应的特性

并且 Web Worker 更狠,不但没有 document 对象,连 window 对象也没有😂

在worker线程中打印window

onmessage = function (e) {
console.log(window);
postMessage();
};

浏览器直接报错


在 Web Worker 线程的运行环境中无法访问 document 对象,这一条符合当前的需求,但是该环境中能获取 XMLHttpRequest 对象,可以发送 ajax 请求,不符合不能调接口的要求

此路还是不通……😓

如何禁止开发者调接口 ?

常规调接口方式有:

1)原生方式:XMLHttpRequest、fetch、WebSocket、jsonp、form表单

2)三方实现:axios、jquery、request等众多开源库

禁用原生方式调接口的思路:

1)XMLHttpRequest、fetch、WebSocket 这几种情况,可以禁止用户访问这些对象

2)jsonp、form 这两种方式,需要创建script或form标签,依然可以通过禁止开发者操作DOM的方式解决,不需要单独处理

如何禁用三方库调接口呢?

三方库很多,没办法全部列出来,来进行逐一排除

禁止调接口的路好像也被封死了……😰

最终方案:沙箱(Sandbox)

通过上面的分析,传统的思路确实解决不了当前的需求

阻止开发者操作DOM和调接口,沙箱说:这个我熟啊,拦截隔离这类的活,我最拿手了😀

沙箱(Sandbox) 是一种安全机制,为运行中的程序提供隔离环境,通常用于执行未经测试或不受信任的程序或代码,它会为待执行的程序创建一个独立的执行环境,内部程序的执行不会影响到外部程序的运行

前端沙箱的使用场景:

1)Chrome 浏览器打开的每个页面就是一个沙箱,保证彼此独立互不影响

2)执行 jsonp 请求回来的字符串时或引入不知名第三方 JS 库时,可能需要创造一个沙箱来执行这些代码

3)Vue 模板表达式的计算是运行在一个沙箱中,模板字符串中的表达式只能获取部分全局对象,详情见源码

4)微前端框架 qiankun ,为了实现js隔离,在多种场景下均使用了沙箱

沙箱的多种实现方式

先聊下 with 这个关键字:作用在于改变作用域,可以将某个对象添加到作用域链的顶部

with对于沙箱的意义:可以实现所有变量均来自可靠或自主实现的上下文环境,而不会从全局的执行环境中取值,相当于做了一层拦截,实现隔离的效果

简陋的沙箱

题目要求: 实现这样一个沙箱,要求程序中访问的所有变量,均来自可靠或自主实现的上下文环境,而不会从全局的执行环境中取值

举个🌰: ctx作为执行上下文对象,待执行程序code可以访问到的变量,必须都来自ctx对象

// ctx 执行上下文对象
const ctx = {
func: variable => {
  console.log(variable);
},
foo: "f1"
};

// 待执行程序
const code = `func(foo)`;

沙箱示例:

// 定义全局变量foo
var foo = "foo1";

// 执行上下文对象
const ctx = {
 func: variable => {
   console.log(variable);
},
 foo: "f1"
};

// 非常简陋的沙箱
function veryPoorSandbox(code, ctx) {
 // 使用with,将eval函数执行时的执行上下文指定为ctx
 with (ctx) {
   // eval可以将字符串按js代码执行,如eval('1+2')
   eval(code);
}
}

// 待执行程序
const code = `func(foo)`;

veryPoorSandbox(code, ctx);
// 打印结果:"f1",不是最外层的全局变量"foo1"

这个沙箱有一个明显的问题,若提供的ctx上下文对象中,没有找到某个变量时,代码仍会沿着作用域链一层层向上查找

假如上文示例中的 ctx 对象没有设置 foo属性,打印的结果还是外层作用域的foo1

With + Proxy 实现沙箱

题目要求: 希望沙箱中的代码只在手动提供的上下文对象中查找变量,如果上下文对象中不存在该变量,则提示对应的错误

举个🌰: ctx作为执行上下文对象,待执行程序code可以访问到的变量,必须都来自ctx对象,如果ctx对象中不存在该变量,直接报错,不再通过作用域链向上查找

实现步骤:

1)使用 Proxy.has() 来拦截 with 代码块中的任意变量的访问

2)设置一个白名单,在白名单内的变量可以正常走作用域链的访问方式,不在白名单内的变量,会继续判断是否存 ctx 对象中,存在则正常访问,不存在则直接报错

3)使用new Function替代eval,使用 new Function() 运行代码比eval更为好一些,函数的参数提供了清晰的接口来运行代码

new Function与eval的区别

沙箱示例:

var foo = "foo1";

// 执行上下文对象
const ctx = {
 func: variable => {
   console.log(variable);
}
};

// 构造一个 with 来包裹需要执行的代码,返回 with 代码块的一个函数实例
function withedYourCode(code) {
 code = "with(shadow) {" + code + "}";
 return new Function("shadow", code);
}

// 可访问全局作用域的白名单列表
const access_white_list = ["func"];

// 待执行程序
const code = `func(foo)`;

// 执行上下文对象的代理对象
const ctxProxy = new Proxy(ctx, {
 has: (target, prop) => {
   // has 可以拦截 with 代码块中任意属性的访问
   if (access_white_list.includes(prop)) {
     // 在可访问的白名单内,可继续向上查找
     return target.hasOwnProperty(prop);
  }
   if (!target.hasOwnProperty(prop)) {
     throw new Error(`Not found - ${prop}!`);
  }
   return true;
}
});

// 没那么简陋的沙箱
function littlePoorSandbox(code, ctx) {
 // 将 this 指向手动构造的全局代理对象
 withedYourCode(code).call(ctx, ctx);
}
littlePoorSandbox(code, ctxProxy);

// 执行func(foo),报错: Uncaught Error: Not found - foo!

执行结果:


天然的优质沙箱(iframe)

iframe 标签可以创造一个独立的浏览器原生级别的运行环境,这个环境由浏览器实现了与主环境的隔离

利用 iframe 来实现一个沙箱是目前最方便、简单、安全的方法,可以把 iframe.contentWindow 作为沙箱执行的全局 window 对象

沙箱示例:

// 沙箱全局代理对象类
class SandboxGlobalProxy {
 constructor(sharedState) {
   // 创建一个 iframe 标签,取出其中的原生浏览器全局对象作为沙箱的全局对象
   const iframe = document.createElement("iframe", { url: "about:blank" });
   iframe.style.display = "none";
   document.body.appendChild(iframe);
   
   // sandboxGlobal作为沙箱运行时的全局对象
   const sandboxGlobal = iframe.contentWindow;

   return new Proxy(sandboxGlobal, {
     has: (target, prop) => {
       // has 可以拦截 with 代码块中任意属性的访问
       if (sharedState.includes(prop)) {
         // 如果属性存在于共享的全局状态中,则让其沿着原型链在外层查找
         return false;
      }
       
       // 如果没有该属性,直接报错
       if (!target.hasOwnProperty(prop)) {
         throw new Error(`Not find: ${prop}!`);
      }
       
       // 属性存在,返回sandboxGlobal中的值
       return true;
    }
  });
}
}

// 构造一个 with 来包裹需要执行的代码,返回 with 代码块的一个函数实例
function withedYourCode(code) {
 code = "with(sandbox) {" + code + "}";
 return new Function("sandbox", code);
}
function maybeAvailableSandbox(code, ctx) {
 withedYourCode(code).call(ctx, ctx);
}

// 要执行的代码
const code = `
 console.log(history == window.history) // false
 window.abc = 'sandbox'
 Object.prototype.toString = () => {
     console.log('Traped!')
 }
 console.log(window.abc) // sandbox
`;

// sharedGlobal作为与外部执行环境共享的全局对象
// code中获取的history为最外层作用域的history
const sharedGlobal = ["history"];

const globalProxy = new SandboxGlobalProxy(sharedGlobal);

maybeAvailableSandbox(code, globalProxy);

// 对外层的window对象没有影响
console.log(window.abc); // undefined
Object.prototype.toString(); // 并没有打印 Traped

可以看到,沙箱中对window的所有操作,都没有影响到外层的window,实现了隔离的效果😘

需求实现

继续使用上述的 iframe 标签来创建沙箱,代码主要修改点

1)设置 blacklist 黑名单,添加 document、XMLHttpRequest、fetch、WebSocket 来禁止开发者操作DOM和调接口

2)判断要访问的变量,是否在当前环境的 window 对象中,不在的直接报错,实现禁止通过三方库调接口

// 设置黑名单
const blacklist = ['document', 'XMLHttpRequest', 'fetch', 'WebSocket'];

// 黑名单中的变量禁止访问
if (blacklist.includes(prop)) {
 throw new Error(`Can't use: ${prop}!`);
}

但有个很严重的漏洞,如果开发者通过 window.document 来获取 document 对象,依然是可以操作 DOM 的😱

需要在黑名单中加入 window 字段,来解决这个沙箱逃逸的漏洞,虽然把 window 加入了黑名单,但 window 上的方法,如 open、close 等,依然是可以正常获取使用的

最终代码:

// 沙箱全局代理对象类
class SandboxGlobalProxy {
constructor(blacklist) {
// 创建一个 iframe 标签,取出其中的原生浏览器全局对象作为沙箱的全局对象
const iframe = document.createElement("iframe", { url: "about:blank" });
iframe.style.display = "none";
document.body.appendChild(iframe);

// 获取当前HTMLIFrameElement的Window对象
const sandboxGlobal = iframe.contentWindow;

return new Proxy(sandboxGlobal, {
// has 可以拦截 with 代码块中任意属性的访问
has: (target, prop) => {

// 黑名单中的变量禁止访问
if (blacklist.includes(prop)) {
throw new Error(`Can't use: ${prop}!`);
}
// sandboxGlobal对象上不存在的属性,直接报错,实现禁用三方库调接口
if (!target.hasOwnProperty(prop)) {
throw new Error(`Not find: ${prop}!`);
}

// 返回true,获取当前提供上下文对象中的变量;如果返回false,会继续向上层作用域链中查找
return true;
}
});
}
}

// 使用with关键字,来改变作用域
function withedYourCode(code) {
code = "with(sandbox) {" + code + "}";
return new Function("sandbox", code);
}

// 将指定的上下文对象,添加到待执行代码作用域的顶部
function makeSandbox(code, ctx) {
withedYourCode(code).call(ctx, ctx);
}

// 待执行的代码code,获取document对象
const code = `console.log(document)`;

// 设置黑名单
// 经过小伙伴的指导,新添加Image字段,禁止使用new Image来调接口
const blacklist = ['window', 'document', 'XMLHttpRequest', 'fetch', 'WebSocket', 'Image'];

// 将globalProxy对象,添加到新环境作用域链的顶部
const globalProxy = new SandboxGlobalProxy(blacklist);

makeSandbox(code, globalProxy);

打印结果:


持续优化

经过与评论区小伙伴的交流,可以通过 new Image() 调接口,确实是个漏洞

// 不需要创建DOM 发送图片请求
let img = new Image();
img.src= "http://www.test.com/img.gif";

黑名单中添加'Image'字段,堵上这个漏洞。如果还有其他漏洞,欢迎交流讨论💕

总结

通过解决面试官提出的问题,介绍了沙箱的基本概念、应用场景,以及如何去实现符合要求的沙箱,发现防止沙箱逃逸是一件挺有趣的事情,就像双方在下棋一样,你来我往,有攻有守😄

关于这个问题,小伙伴们如果有其他可行的方案,或者有要补充、指正的,欢迎交流讨论


参考资料:
浅析 JavaScript 沙箱机制

作者:海阔_天空
来源:juejin.cn/post/7157570429928865828

收起阅读 »

数据结构:7种哈希散列算法,你知道几个?

一、前言 哈希表的历史 哈希散列的想法在不同的地方独立出现。1953 年 1 月,汉斯·彼得·卢恩 ( Hans Peter Luhn ) 编写了一份IBM内部备忘录,其中使用了散列和链接。开放寻址后来由 AD Linh 在 Luhn 的论文上提出。大约在同一...
继续阅读 »

一、前言


哈希表的历史


哈希散列的想法在不同的地方独立出现。1953 年 1 月,汉斯·彼得·卢恩 ( Hans Peter Luhn ) 编写了一份IBM内部备忘录,其中使用了散列和链接。开放寻址后来由 AD Linh 在 Luhn 的论文上提出。大约在同一时间,IBM Research的Gene Amdahl、Elaine M. McGraw、Nathaniel Rochester和Arthur Samuel为IBM 701汇编器实现了散列。 线性探测的开放寻址归功于 Amdahl,尽管Ershov独立地有相同的想法。“开放寻址”一词是由W. Wesley Peterson在他的文章中创造的,该文章讨论了大文件中的搜索问题。


二、哈希数据结构


哈希表的存在是为了解决能通过O(1)时间复杂度直接索引到指定元素。


这是什么意思呢?通过我们使用数组存放元素,都是按照顺序存放的,当需要获取某个元素的时候,则需要对数组进行遍历,获取到指定的值。如图所示;





而这样通过循环遍历比对获取指定元素的操作,时间复杂度是O(n),也就是说如果你的业务逻辑实现中存在这样的代码是非常拉胯的。那怎么办呢?这就引入了哈希散列表的设计。




在计算机科学中,一个哈希表(hash table、hash map)是一种实现关联数组的抽象数据结构,该结构将键通过哈希计算映射到值。


也就是说我们通过对一个 Key 值计算它的哈希并与长度为2的n次幂的数组减一做与运算,计算出槽位对应的索引,将数据存放到索引下。那么这样就解决了当获取指定数据时,只需要根据存放时计算索引ID的方式再计算一次,就可以把槽位上对应的数据获取处理,以此达到时间复杂度为O(1)的情况。如图所示;





哈希散列虽然解决了获取元素的时间复杂度问题,但大多数时候这只是理想情况。因为随着元素的增多,很可能发生哈希冲突,或者哈希值波动不大导致索引计算相同,也就是一个索引位置出现多个元素情况。如图所示;





李二狗拎瓢冲都有槽位的下标索引03的 叮裆猫 发生冲突时,情况就变得糟糕了,因为这样就不能满足O(1)时间复杂度获取元素的诉求了。


那么此时就出现了一系列解决方案,包括;HashMap 中的拉链寻址 + 红黑树、扰动函数、负载因子ThreadLocal 的开放寻址、合并散列、杜鹃散列、跳房子哈希、罗宾汉哈希等各类数据结构设计。让元素在发生哈希冲突时,也可以存放到新的槽位,并尽可能保证索引的时间复杂度小于O(n)


三、实现哈希散列


哈希散列是一个非常常见的数据结构,无论是我们使用的 HashMap、ThreaLocal 还是你在刷题中位了提升索引效率,都会用到哈希散列。


只要哈希桶的长度由负载因子控制的合理,每次查找元素的平均时间复杂度与桶中存储的元素数量无关。另外许多哈希表设计还允许对键值对的任意插入和删除,每次操作的摊销固定平均成本。


好,那么介绍了这么多,小傅哥带着大家做几个关于哈希散列的数据结构,通过实践来了解会更加容易搞懂。



1. 哈希碰撞


说明:通过模拟简单 HashMap 实现,去掉拉链寻址等设计,验证元素哈新索引位置碰撞。


public class HashMap01<K, V> implements Map<K, V> {

private final Object[] tab = new Object[8];

@Override
public void put(K key, V value) {
int idx = key.hashCode() & (tab.length - 1);
tab[idx] = value;
}

@Override
public V get(K key) {
int idx = key.hashCode() & (tab.length - 1);
return (V) tab[idx];
}

}





  • HashMap01 的实现只是通过哈希计算出的下标,散列存放到固定的数组内。那么这样当发生元素下标碰撞时,原有的元素就会被新的元素替换掉。


测试


@Test
public void test_hashMap01() {
Map<String, String> map = new HashMap01<>();
map.put("01", "花花");
map.put("02", "豆豆");
logger.info("碰撞前 key:{} value:{}", "01", map.get("01"));

// 下标碰撞
map.put("09", "蛋蛋");
map.put("12", "苗苗");
logger.info("碰撞前 key:{} value:{}", "01", map.get("01"));
}




06:58:41.691 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:花花
06:58:41.696 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:苗苗

Process finished with exit code 0


  • 通过测试结果可以看到,碰撞前 map.get("01") 的值是花花,两次下标索引碰撞后存放的值则是苗苗

  • 这也就是使用哈希散列必须解决的一个问题,无论是在已知元素数量的情况下,通过扩容数组长度解决,还是把碰撞的元素通过链表存放,都是可以的。


2. 拉链寻址


说明:既然我们没法控制元素不碰撞,但我们可以对碰撞后的元素进行管理。比如像 HashMap 中拉链法一样,把碰撞的元素存放到链表上。这里我们就来简化实现一下。


public class HashMap02BySeparateChaining<K, V> implements Map<K, V> {

private final LinkedList<Node<K, V>>[] tab = new LinkedList[8];

@Override
public void put(K key, V value) {
int idx = key.hashCode() & (tab.length - 1);
if (tab[idx] == null) {
tab[idx] = new LinkedList<>();
tab[idx].add(new Node<>(key, value));
} else {
tab[idx].add(new Node<>(key, value));
}
}

@Override
public V get(K key) {
int idx = key.hashCode() & (tab.length - 1);
for (Node<K, V> kvNode : tab[idx]) {
if (key.equals(kvNode.getKey())) {
return kvNode.value;
}
}
return null;
}

static class Node<K, V> {
final K key;
V value;

public Node(K key, V value) {
this.key = key;
this.value = value;
}

public K getKey() {
return key;
}

public V getValue() {
return value;
}

}

}



  • 因为元素在存放到哈希桶上时,可能发生下标索引膨胀,所以这里我们把每一个元素都设定成一个 Node 节点,这些节点通过 LinkedList 链表关联,当然你也可以通过 Node 节点构建出链表 next 元素即可。

  • 那么这时候在发生元素碰撞,相同位置的元素就都被存放到链表上了,获取的时候需要对存放多个元素的链表进行遍历获取。


测试


@Test
public void test_hashMap02() {
Map<String, String> map = new HashMap02BySeparateChaining<>();
map.put("01", "花花");
map.put("05", "豆豆");
logger.info("碰撞前 key:{} value:{}", "01", map.get("01"));

// 下标碰撞
map.put("09", "蛋蛋");
map.put("12", "苗苗");
logger.info("碰撞前 key:{} value:{}", "01", map.get("01"));
}


07:21:16.654 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:花花
07:22:44.651 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:花花

Process finished with exit code 0


  • 此时第一次和第二次获取01位置的元素就都是花花了,元素没有被替代。因为此时的元素是被存放到链表上了。


3. 开放寻址


说明:除了对哈希桶上碰撞的索引元素进行拉链存放,还有不引入新的额外的数据结构,只是在哈希桶上存放碰撞元素的方式。它叫开放寻址,也就是 ThreaLocal 中运用斐波那契散列+开放寻址的处理方式。


public class HashMap03ByOpenAddressing<K, V> implements Map<K, V> {

private final Node<K, V>[] tab = new Node[8];

@Override
public void put(K key, V value) {
int idx = key.hashCode() & (tab.length - 1);
if (tab[idx] == null) {
tab[idx] = new Node<>(key, value);
} else {
for (int i = idx; i < tab.length; i++) {
if (tab[i] == null) {
tab[i] = new Node<>(key, value);
break;
}
}
}
}

@Override
public V get(K key) {
int idx = key.hashCode() & (tab.length - 1);
for (int i = idx; i < tab.length; i ++){
if (tab[idx] != null && tab[idx].key == key) {
return tab[idx].value;
}
}
return null;
}

static class Node<K, V> {
final K key;
V value;

public Node(K key, V value) {
this.key = key;
this.value = value;
}

}

}



  • 开放寻址的设计会对碰撞的元素,寻找哈希桶上新的位置,这个位置从当前碰撞位置开始向后寻找,直到找到空的位置存放。

  • 在 ThreadLocal 的实现中会使用斐波那契散列、索引计算累加、启发式清理、探测式清理等操作,以保证尽可能少的碰撞。


测试


@Test
public void test_hashMap03() {
Map<String, String> map = new HashMap03ByOpenAddressing<>();
map.put("01", "花花");
map.put("05", "豆豆");
logger.info("碰撞前 key:{} value:{}", "01", map.get("01"));
// 下标碰撞
map.put("09", "蛋蛋");
map.put("12", "苗苗");
logger.info("碰撞前 key:{} value:{}", "01", map.get("01"));
}


07:20:22.382 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:花花
07:20:22.387 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:花花
07:20:22.387 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 数据结构:HashMap{tab=[null,{"key":"01","value":"花花"},{"key":"09","value":"蛋蛋"},{"key":"12","value":"苗苗"},null,{"key":"05","value":"豆豆"},null,null]}

Process finished with exit code 0


  • 通过测试结果可以看到,开放寻址对碰撞元素的寻址存放,也是可用解决哈希索引冲突问题的。


4. 合并散列


说明:合并散列是开放寻址和单独链接的混合,碰撞的节点在哈希表中链接。此算法适合固定分配内存的哈希桶,通过存放元素时识别哈希桶上的最大空槽位来解决合并哈希中的冲突。


public class HashMap04ByCoalescedHashing<K, V> implements Map<K, V> {

private final Node<K, V>[] tab = new Node[8];

@Override
public void put(K key, V value) {
int idx = key.hashCode() & (tab.length - 1);
if (tab[idx] == null) {
tab[idx] = new Node<>(key, value);
return;
}

int cursor = tab.length - 1;
while (tab[cursor] != null && tab[cursor].key != key) {
--cursor;
}
tab[cursor] = new Node<>(key, value);

// 将碰撞节点指向这个新节点
while (tab[idx].idxOfNext != 0){
idx = tab[idx].idxOfNext;
}

tab[idx].idxOfNext = cursor;
}

@Override
public V get(K key) {
int idx = key.hashCode() & (tab.length - 1);
while (tab[idx].key != key) {
idx = tab[idx].idxOfNext;
}
return tab[idx].value;
}

static class Node<K, V> {
final K key;
V value;
int idxOfNext;

public Node(K key, V value) {
this.key = key;
this.value = value;
}

}

}



  • 合并散列的最大目的在于将碰撞元素链接起来,避免因为需要寻找碰撞元素所发生的循环遍历。也就是A、B元素存放时发生碰撞,那么在找到A元素的时候可以很快的索引到B元素所在的位置。


测试


07:18:43.613 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:花花
07:18:43.618 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:苗苗
07:18:43.619 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 数据结构:HashMap{tab=[null,{"idxOfNext":7,"key":"01","value":"花花"},null,null,null,{"idxOfNext":0,"key":"05","value":"豆豆"},{"idxOfNext":0,"key":"12","value":"苗苗"},{"idxOfNext":6,"key":"09","value":"蛋蛋"}]}

Process finished with exit code 0


  • 相对于直接使用开放寻址,这样的挂在链路指向的方式,可以提升索引的性能。因为在实际的数据存储上,元素的下一个位置不一定空元素,可能已经被其他元素占据,这样就增加了索引的次数。所以使用直接指向地址的方式,会更好的提高索引性能。


5. 杜鹃散列


说明:这个名字起的比较有意思,也代表着它的数据结构。杜鹃鸟在孵化🐣的时候,雏鸟会将其他蛋或幼崽推出巢穴;类似的这个数据结构会使用2组key哈希表,将冲突元素推到另外一个key哈希表中。


private V put(K key, V value, boolean isRehash) {
Object k = maskNull(key);
if (containsKey(k)) {
return null;
}
if (insertEntry(new Entry<K, V>((K) k, value))) {
if (!isRehash) {
size++;
}
return null;
}
rehash(2 * table.length);
return put((K) k, value);
}

private boolean insertEntry(Entry<K, V> e) {
int count = 0;
Entry<K, V> current = e;
int index = hash(hash1, current.key);
while (current != e || count < table.length) {
Entry<K, V> temp = table[index];
if (temp == null) {
table[index] = current;
return true;
}
table[index] = current;
current = temp;
if (index == hash(hash1, current.key)) {
index = hash(hash2, current.key);
} else {
index = hash(hash1, current.key);
}
++count;
}
return false;
}



  • 当多个键映射到同一个单元格时会发生这种情况。杜鹃散列的基本思想是通过使用两个散列函数而不是仅一个散列函数来解决冲突。

  • 这为每个键在哈希表中提供了两个可能的位置。在该算法的一种常用变体中,哈希表被分成两个大小相等的较小的表,每个哈希函数都为这两个表之一提供索引。两个散列函数也可以为单个表提供索引。

  • 在实践中,杜鹃哈希比线性探测慢约 20-30%,线性探测是常用方法中最快的。然而,由于它对搜索时间的最坏情况保证,当需要实时响应率时,杜鹃散列仍然很有价值。杜鹃散列的一个优点是它的无链接列表属性,非常适合 GPU 处理。


测试



07:52:04.010 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:花花
07:52:04.016 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:苗苗
07:52:04.016 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 数据结构:{01=花花, 12=苗苗, 05=豆豆, 09=蛋蛋}

Process finished with exit code 0


  • 从测试结果可以看到,杜鹃散列可以通过两个散列函数解决索引冲突问题。不过这个探测的过程比较耗时。


6. 跳房子散列


说明:跳房子散列是一种基于开放寻址的算法,它结合了杜鹃散列、线性探测和链接的元素,通过桶邻域的概念——任何给定占用桶周围的后续桶,也称为“虚拟”桶。 该算法旨在在哈希表的负载因子增长超过 90% 时提供更好的性能;它还在并发设置中提供了高吞吐量,因此非常适合实现可调整大小的并发哈希表。


public boolean insert(AnyType x) {
if (!isEmpty()) {
return false;
}
int currentPos = findPos(x);
if (currentPos == -1) {
return false;
}
if (array[currentPos] != null) {
x = array[currentPos].element;
array[currentPos].isActive = true;
}
String hope;
if (array[currentPos] != null) {
hope = array[currentPos].hope;
x = array[currentPos].element;
} else {
hope = "10000000";
}
array[currentPos] = new HashEntry<>(x, hope, true);
theSize++;
return true;
}



  • 该算法使用一个包含n 个桶的数组。对于每个桶,它的邻域是H个连续桶的小集合(即索引接近原始散列桶的那些)。邻域的期望属性是在邻域的桶中找到一个项目的成本接近于在桶本身中找到它的成本(例如,通过使邻域中的桶落在同一缓存行中)。在最坏的情况下,邻域的大小必须足以容纳对数个项目(即它必须容纳 log( n ) 个项目),但平均只能是一个常数。如果某个桶的邻域被填满,则调整表的大小。


测试


@Test
public void test_hashMap06() {
HashMap06ByHopscotchHashing<Integer> map = new HashMap06ByHopscotchHashing<>();
map.insert(1);
map.insert(5);
map.insert(9);
map.insert(12);
logger.info("数据结构:{}", map);
}


17:10:10.363 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 数据结构:HashMap{tab=[null,{"element":1,"hope":"11000000","isActive":true},{"element":9,"hope":"00000000","isActive":true},null,{"element":12,"hope":"10000000","isActive":true},{"element":5,"hope":"10000000","isActive":true},null,null]}

Process finished with exit code 0


  • 通过测试可以看到,跳房子散列会在其原始散列数组条目中找到,或者在接下来的H-1个相邻条目之一找到对应的冲突元素。


7. 罗宾汉哈希


说明:罗宾汉哈希是一种基于开放寻址的冲突解决算法;冲突是通过偏向从其“原始位置”(即项目被散列到的存储桶)最远或最长探测序列长度(PSL)的元素的位移来解决的。


public void put(K key, V value) {
Entry entry = new Entry(key, value);
int idx = hash(key);
// 元素碰撞检测
while (table[idx] != null) {
if (entry.offset > table[idx].offset) {
// 当前偏移量不止一个,则查看条目交换位置,entry 是正在查看的条目,增加现在搜索的事物的偏移量和 idx
Entry garbage = table[idx];
table[idx] = entry;
entry = garbage;
idx = increment(idx);
entry.offset++;
} else if (entry.offset == table[idx].offset) {
// 当前偏移量与正在查看的检查键是否相同,如果是则它们交换值,如果不是,则增加 idx 和偏移量并继续
if (table[idx].key.equals(key)) {
// 发现相同值
V oldVal = table[idx].value;
table[idx].value = value;
} else {
idx = increment(idx);
entry.offset++;
}
} else {
// 当前偏移量小于我们正在查看的我们增加 idx 和偏移量并继续
idx = increment(idx);
entry.offset++;
}
}
// 已经到达了 null 所在的 idx,将新/移动的放在这里
table[idx] = entry;
size++;
// 超过负载因子扩容
if (size >= loadFactor * table.length) {
rehash(table.length * 2);
}
}



  • 09、12 和 01 发生哈希索引碰撞,进行偏移量计算调整。通过最长位置探测碰撞元素位移来处理。


测试


public void test_hashMap07() {
Map<String, String> map = new HashMap07ByRobinHoodHashing<>();
map.put("01", "花花");
map.put("05", "豆豆");
logger.info("碰撞前 key:{} value:{}", "01", map.get("01"));
// 下标碰撞
map.put("09", "蛋蛋");
map.put("12", "苗苗");
logger.info("碰撞前 key:{} value:{}", "01", map.get("12"));
logger.info("数据结构:{}", map);
}


07:34:32.593 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:花花
09 1
12 1
01 1
09 9
12 1
05 5
07:35:07.419 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:苗苗
07:35:07.420 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 数据结构:HashMap{tab=[null,{"key":"01","offset":0,"value":"花花"},{"key":"12","offset":1,"value":"苗苗"},null,null,{"key":"05","offset":0,"value":"豆豆"},null,null,null,{"key":"09","offset":0,"value":"蛋蛋"},null,null,null,null,null,null]}

Process finished with exit code 0


  • 通过测试结果和调试的时候可以看到,哈希索引冲突是通过偏向从其“原始位置”(即项目被散列到的存储桶)最远或最长探测序列长度(PSL)的元素的位移来解决。这块可以添加断点调试验证。


四、常见面试问题



  • 介绍一下散列表

  • 为什么使用散列表

  • 拉链寻址和开放寻址的区别

  • 还有其他什么方式可以解决散列哈希索引冲突

  • 对应的Java源码中,对于哈希索引冲突提供了什么样的解决方案

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

高并发技巧-redis和本地缓存使用技巧

在这篇文章中,我主要介绍的是分布式缓存和本地缓存的使用技巧,包括缓存种类介绍,各种的使用场景,以及如何使用,最后再给出实战案例。 众所周知,缓存最主要的目的就是加速访问,缓解数据库压力。最常用的缓存就是分布式缓存,比如redis,在面对大部分并发场景或者一些...
继续阅读 »

在这篇文章中,我主要介绍的是分布式缓存和本地缓存的使用技巧,包括缓存种类介绍,各种的使用场景,以及如何使用,最后再给出实战案例。



众所周知,缓存最主要的目的就是加速访问,缓解数据库压力。最常用的缓存就是分布式缓存,比如redis,在面对大部分并发场景或者一些中小型公司流量没有那么高的情况,使用redis基本都能解决了。但是在流量较高的情况下可能得使用到本地缓存了,比如guava的LoadingCache和快手开源的ReloadableCache。


三种缓存的使用场景


这部分会介绍redis,比如guava的LoadingCache和快手开源的ReloadableCache的使用场景和局限,通过这一部分的介绍就能知道在怎样的业务场景下应该使用哪种缓存,以及为什么。


Redis的使用场景和局限性


如果宽泛的说redis何时使用,那么自然就是用户访问量过高的地方使用,从而加速访问,并且缓解数据库压力。如果细分的话,还得分为单节点问题和非单节点问题。


如果一个页面用户访问量比较高,但是访问的不是同一个资源。比如用户详情页,访问量比较高,但是每个用户的数据都是不一样的,这种情况显然只能用分布式缓存了,如果使用redis,key为用户唯一键,value则是用户信息。


redis导致的缓存击穿


但是需要注意一点,一定要设置过期时间,而且不能设置到同一时间点过期。举个例子,比如用户又个活动页,活动页能看到用户活动期间获奖数据,粗心的人可能会设置用户数据的过期时间点为活动结束,这样会


单(热)点问题


单节点问题说的是redis的单个节点的并发问题,因为对于相同的key会落到redis集群的同一个节点上,那么如果对这个key的访问量过高,那么这个redis节点就存在并发隐患,这个key就称为热key。


如果所有用户访问的都是同一个资源,比如小爱同学app首页对所有用户展示的内容都一样(初期),服务端给h5返回的是同一个大json,显然得使用到缓存。首先我们考虑下用redis是否可行,由于redis存在单点问题,如果流量过大的话,那么所有用户的请求到达redis的同一个节点,需要评估该节点能否抗住这么大流量。我们的规则是,如果单节点qps达到了千级别就要解决单点问题了(即使redis号称能抗住十万级别的qps),最常见的做法就是使用本地缓存。显然小爱app首页流量不过百,使用redis是没问题的。


LoadingCache的使用场景和局限性


对于这上面说的热key问题,我们最直接的做法就是使用本地缓存,比如你最熟悉的guava的LoadingCache,但是使用本地缓存要求能够接受一定的脏数据,因为如果你更新了首页,本地缓存是不会更新的,它只会根据一定的过期策略来重新加载缓存,不过在我们这个场景是完全没问题的,因为一旦在后台推送了首页后就不会再去改变了。即使改变了也没问题,可以设置写过期为半小时,超过半小时重新加载缓存,这种短时间内的脏数据我们是可以接受的。


LoadingCache导致的缓存击穿


虽然说本地缓存和机器上强相关的,虽然代码层面写的是半小时过期,但由于每台机器的启动时间不同,导致缓存的加载时间不同,过期时间也就不同,也就不会所有机器上的请求在同一时间缓存失效后都去请求数据库。但是对于单一一台机器也是会导致缓存穿透的,假如有10台机器,每台1000的qps,只要有一台缓存过期就可能导致这1000个请求同时打到了数据库。这种问题其实比较好解决,但是容易被忽略,也就是在设置LoadingCache的时候使用LoadingCache的load-miss方法,而不是直接判断cache.getIfPresent()== null然后去请求db;前者会加虚拟机层面的锁,保证只有一个请求打到数据库去,从而完美的解决了这个问题。


但是,如果对于实时性要求较高的情况,比如有段时间要经常做活动,我要保证活动页面能近实时更新,也就是运营在后台配置好了活动信息后,需要在C端近实时展示这次配置的活动信息,此时使用LoadingCache肯定就不能满足了。


ReloadableCache的使用场景和局限性


对于上面说的LoadingCache不能解决的实时问题,可以考虑使用ReloadableCache,这是快手开源的一个本地缓存框架,最大的特点是支持多机器同时更新缓存,假设我们修改了首页信息,然后请求打到的是A机器,这个时候重新加载ReloadableCache,然后它会发出通知,监听了同一zk节点的其他机器收到通知后重新更新缓存。使用这个缓存一般的要求是将全量数据加载到本地缓存,所以如果数据量过大肯定会对gc造成压力,这种情况就不能使用了。由于小爱同学首页这个首页是带有状态的,一般online状态的就那么两个,所以完全可以使用ReloadableCache来只装载online状态的首页。


小结


到这里三种缓存基本都介绍完了,做个小结:



  1. 对于非热点的数据访问,比如用户维度的数据,直接使用redis即可;

  2. 对于热点数据的访问,如果流量不是很高,无脑使用redis即可;

  3. 对于热点数据,如果允许一定时间内的脏数据,使用LoadingCache即可;

  4. 对于热点数据,如果一致性要求较高,同时数据量不大的情况,使用ReloadableCache即可;


小技巧


不管哪种本地缓存虽然都带有虚拟机层面的加锁来解决击穿问题,但是意外总有可能以你意想不到的方式发生,保险起见你可以使用两级缓存的方式即本地缓存+redis+db。


缓存使用的简单介绍


这里redis的使用就不再多说了,相信很多人对api的使用比我还熟悉


LoadingCache的使用


这个是guava提供的网上一抓一大把,但是给两点注意事项



  1. 要使用load-miss的话, 要么使用V get(K key, Callable<? extends V> loader);要么使用build的时候使用的是build(CacheLoader<? super K1, V1> loader)这个时候可以直接使用get()了。此外建议使用load-miss,而不是getIfPresent==null的时候再去查数据库,这可能导致缓存击穿;

  2. 使用load-miss是因为这是线程安全的,如果缓存失效的话,多个线程调用get的时候只会有一个线程去db查询,其他线程需要等待,也就是说这是线程安全的。


LoadingCache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(1000L)
.expireAfterAccess(Duration.ofHours(1L)) // 多久不访问就过期
.expireAfterWrite(Duration.ofHours(1L)) // 多久这个key没修改就过期
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
// 数据装载方式,一般就是loadDB
return key + " world";
}
});
String value = cache.get("hello"); // 返回hello world

reloadableCache的使用


导入三方依赖


<dependency>
<groupId>com.github.phantomthief</groupId>
<artifactId>zknotify-cache</artifactId>
<version>0.1.22</version>
</dependency>

需要看文档,不然无法使用,有兴趣自己写一个也行的。


public interface ReloadableCache<T> extends Supplier<T> {

/**
* 获取缓存数据
*/
@Override
T get();

/**
* 通知全局缓存更新
* 注意:如果本地缓存没有初始化,本方法并不会初始化本地缓存并重新加载
*
* 如果需要初始化本地缓存,请先调用 {@link ReloadableCache#get()}
*/
void reload();

/**
* 更新本地缓存的本地副本
* 注意:如果本地缓存没有初始化,本方法并不会初始化并刷新本地的缓存
*
* 如果需要初始化本地缓存,请先调用 {@link ReloadableCache#get()}
*/
void reloadLocal();
}

老生常谈的缓存击穿/穿透/雪崩问题


这三个真的是亘古不变的问题,如果流量大确实需要考虑。


缓存击穿


简单说就是缓存失效,导致大量请求同一时间打到了数据库。对于缓存击穿问题上面已经给出了很多解决方案了。



  1. 比如使用本地缓存

  2. 本地缓存使用load-miss方法

  3. 使用第三方服务来加载缓存


1.2和都说过,主要来看3。假如业务愿意只能使用redis而无法使用本地缓存,比如数据量过大,实时性要求比较高。那么当缓存失效的时候就得想办法保证只有少量的请求打到数据库。很自然的就想到了使用分布式锁,理论上说是可行的,但实际上存在隐患。我们的分布式锁相信很多人都是使用redis+lua的方式实现的,并且在while中进行了轮训,这样请求量大,数据多的话会导致无形中让redis成了隐患,并且占了太多业务线程,其实仅仅是引入了分布式锁就加大了复杂度,我们的原则就是能不用就不用。


那么我们是不是可以设计一个类似分布式锁,但是更可靠的rpc服务呢?当调用get方法的时候这个rpc服务保证相同的key打到同一个节点,并且使用synchronized来进行加锁,之后完成数据的加载。在快手提供了一个叫cacheSetter的框架。下面提供一个简易版,自己写也很容易实现。


import com.google.common.collect.Lists;
import org.apache.commons.collections4.CollectionUtils;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;

/**
* @Description 分布式加载缓存的rpc服务,如果部署了多台机器那么调用端最好使用id做一致性hash保证相同id的请求打到同一台机器。
**/
public abstract class AbstractCacheSetterService implements CacheSetterService {

private final ConcurrentMap<String, CountDownLatch> loadCache = new ConcurrentHashMap<>();

private final Object lock = new Object();

@Override
public void load(Collection<String> needLoadIds) {
if (CollectionUtils.isEmpty(needLoadIds)) {
return;
}
CountDownLatch latch;
Collection<CountDownLatch> loadingLatchList;
synchronized (lock) {
loadingLatchList = excludeLoadingIds(needLoadIds);

needLoadIds = Collections.unmodifiableCollection(needLoadIds);

latch = saveLatch(needLoadIds);
}
System.out.println("needLoadIds:" + needLoadIds);
try {
if (CollectionUtils.isNotEmpty(needLoadIds)) {
loadCache(needLoadIds);
}
} finally {
release(needLoadIds, latch);
block(loadingLatchList);
}

}

/**
* 加锁
* @param loadingLatchList 需要加锁的id对应的CountDownLatch
*/
protected void block(Collection<CountDownLatch> loadingLatchList) {
if (CollectionUtils.isEmpty(loadingLatchList)) {
return;
}
System.out.println("block:" + loadingLatchList);
loadingLatchList.forEach(l -> {
try {
l.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}

/**
* 释放锁
* @param needLoadIds 需要释放锁的id集合
* @param latch 通过该CountDownLatch来释放锁
*/
private void release(Collection<String> needLoadIds, CountDownLatch latch) {
if (CollectionUtils.isEmpty(needLoadIds)) {
return;
}
synchronized (lock) {
needLoadIds.forEach(id -> loadCache.remove(id));
}
if (latch != null) {
latch.countDown();
}
}

/**
* 加载缓存,比如根据id从db查询数据,然后设置到redis中
* @param needLoadIds 加载缓存的id集合
*/
protected abstract void loadCache(Collection<String> needLoadIds);

/**
* 对需要加载缓存的id绑定CountDownLatch,后续相同的id请求来了从map中找到CountDownLatch,并且await,直到该线程加载完了缓存
* @param needLoadIds 能够正在去加载缓存的id集合
* @return 公用的CountDownLatch
*/
protected CountDownLatch saveLatch(Collection<String> needLoadIds) {
if (CollectionUtils.isEmpty(needLoadIds)) {
return null;
}
CountDownLatch latch = new CountDownLatch(1);
needLoadIds.forEach(loadId -> loadCache.put(loadId, latch));
System.out.println("loadCache:" + loadCache);
return latch;
}

/**
* 哪些id正在加载数据,此时持有相同id的线程需要等待
* @param ids 需要加载缓存的id集合
* @return 正在加载的id所对应的CountDownLatch集合
*/
private Collection<CountDownLatch> excludeLoadingIds(Collection<String> ids) {
List<CountDownLatch> loadingLatchList = Lists.newArrayList();
Iterator<String> iterator = ids.iterator();
while (iterator.hasNext()) {
String id = iterator.next();
CountDownLatch latch = loadCache.get(id);
if (latch != null) {
loadingLatchList.add(latch);
iterator.remove();
}
}
System.out.println("loadingLatchList:" + loadingLatchList);
return loadingLatchList;
}
}

业务实现


import java.util.Collection;
public class BizCacheSetterRpcService extends AbstractCacheSetterService {
@Override
protected void loadCache(Collection<String> needLoadIds) {
// 读取db进行处理
// 设置缓存
}
}

缓存穿透


简单来说就是请求的数据在数据库不存在,导致无效请求打穿数据库。


解法也很简单,从db获取数据的方法(getByKey(K key))一定要给个默认值。


比如我有个奖池,金额上限是1W,用户完成任务的时候给他发笔钱,并且使用redis记录下来,并且落表,用户在任务页面能实时看到奖池剩余金额,在任务开始的时候显然奖池金额是不变的,redis和db里面都没有发放金额的记录,这就导致每次必然都去查db,对于这种情况,从db没查出来数据应该缓存个值0到缓存。


缓存雪崩


就是大量缓存集中失效打到了db,当然肯定都是一类的业务缓存,归根到底是代码写的有问题。可以将缓存失效的过期时间打散,别让其集中失效就可以了。


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

摆脱USB线,使用无线连接去开发安卓

前言 工作了大半年,之前一直都需要USB线连接手机才能用Android Studio去调试和安装安卓APP,然后上个礼拜,我突然发现前辈没连无线就可以调试,这让我好奇心一下上来,但又不好意思问,于是搜索了一下关于无线调试的内容,就看到谷歌早就给安卓用上了无线调...
继续阅读 »

前言


工作了大半年,之前一直都需要USB线连接手机才能用Android Studio去调试和安装安卓APP,然后上个礼拜,我突然发现前辈没连无线就可以调试,这让我好奇心一下上来,但又不好意思问,于是搜索了一下关于无线调试的内容,就看到谷歌早就给安卓用上了无线调试,只不过我一直不知道。


image.png
经过我探究了一番,踩了许多坑,最终于今天总算是知道如何稳定的进行无线连接了。


正篇


先感叹一下,不得不说,无线调试真的好用,总算不需要担心线绕来绕去。


一波三折


第一波风雨


为什么我对无线调试这么喜悦,原因就在于我手上的这个手机实在太坑,我几乎半年都被它所折磨,其实我一报型号你们就会明白了,它叫XiaoMi11,将它与电脑连接,结果说是在充电但电量一直在减少,而且那条线也在我一直连接高强度使用后露出了内部结构,所以又换了一根。


第二波浪潮


无线调试功能知晓后,我也是仔细阅读官方文档后,先把adb找到,配置到环境


image.png
我的路径在C:\Users\86152\AppData\Local\Android\Sdk\platform-tools,其实目的就是将这个包含adb.exe的platform-tools路径添加到系统环境变量Path:


image.png
这步完成后,直接win+r,再输入cmd回车之后输入adb devices即可看到自己连接的设备,这时也代表adb已经配好了环境。(本人是在Windows环境下的操作)

接着看看自己手机无线网是不是和电脑在一个局域下,直接ping自己手机ip,如果正常收发包即代表可以无线调试,作者是电脑连着经过路由器的线,而无线网也是该路由器发出的,不一定要电脑也连无线网:


image.png


adb devices

ping 你的手机IP 可以在手机无线调试的地方找到,文中下面的图可以看到

再查看自己手机已经是Android12,是大于官方给出的Android11,于是选择第一种连接方式:


image.png
就是如下图,直接在Android Studio中选择这个功能进行无线连接
image.png
如下图,它提供了两种方式,扫二维码,或者在手机开发者选项的无线调试使用验证码:


image.png
image.png

我又按照文档操作,打开了手机的无线调试:


image.png
结果无论是扫描二维码还是使用验证码都如石沉大海,了无音讯,一直就没成功过,就搁这一直转圈。


60b1098f1ac18e3273cb7dce45ee990.jpg
bb2dbd11f03c0b899bd38a4dc4a0f61.jpg

第三波呼啸


迫不得已,我又看了一些博客的老方法与官网对Android11以下的处理终于连上了,不过我每次都得打开手机无线调试看到IP地址和端口全部输进去才能连,而我就这么傻乎乎的连了一个星期:


6c7581671af66ac12318d52be2c7ed8.jpg

解决方法


所以作者在此不介绍这个不大好用的方法了,直接聊聊最好用的吧:
配置完adb环境后可以重启Android Studio,然后我们打开下图的AS的命令编辑器,同样可以输入adb命令了,当然同样可以ping命令去看看是否可以接通。


image.png
首先我们先输入命令:


image.png
会发现第一次给出failed,再输入一次报already connect即可连接成功


adb connect 手机ip地址(即无线调试中IP地址和端口中":xxxx"前面部分)

下次只要你手机仍连接此WIFI,只需要AS每次打开时输入过上面命令即可,不过在第一次连接时要同意手机上的弹窗哦,不然后面可能无线调试在连WIFI时不会自动开启,需要手动去点。


总结


快快掌握这个方法,让我们开启无线调试的安卓开发生活吧!


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

以感知生命周期的方式观察 Flow 数据

问题 Android 是有生命周期的,在 UI 展示的时候可以接受一些数据更新 UI,在 App 进入后台的时候应该停止接受数据以便释放资源,并且避免一些意想不到的异常; 协程和 Flow 是和 Android 平台无关的 API,正常情况下无法感知 ...
继续阅读 »

问题



  • Android 是有生命周期的,在 UI 展示的时候可以接受一些数据更新 UI,在 App 进入后台的时候应该停止接受数据以便释放资源,并且避免一些意想不到的异常;




  • 协程和 Flow 是和 Android 平台无关的 API,正常情况下无法感知 Android 生命周期方法;


解决方案


我们现在当然是有一些方案来解决上述问题,可能会有以下几种方式。


Flow Lifecycle-aware.gif


方式一:手动取消 Job


Activity 方式:



Fragment 方式:



以上方式显然是比较麻烦的,有一些模板代码。


方式二:使用 repeatOnLifecycle 解决模板代码


Activity 方式:



Fragment 方式:




注:需要添加依赖 androidx.lifecycle:lifecycle-runtime-ktx



这种方式虽然解决了一些模板代码的问题,但是仍然有多层嵌套的问题。


方式三:使用 flowWithLifecycle 解决多层嵌套


Activity 方式:




注:此 API 是在 2.6.0-alpha01 版本中添加。



Fragment 方式类似,使用 flowWithLifecycle 即可。flowWithLifecycle 是新版中新增的一个扩展函数,实现方式如下:



看上去是解决了嵌套的问题,但是并彻底,观察数据的操作还是需要在协程体中进行。


方式四:使用 collectWithLifecycle 彻底解决多层嵌套



自定义扩展函数 collectWithLifecycle 实现大致如下:



这个是我自己的一个扩展,官方库中并没有添加,不过我已经反馈给官方了。这种方式仅仅是解决了嵌套的问题,但是却隐藏了 Flow ,从而导致一系列的操作符将无法使用,不过这个在 UI Element/Compose 中并不是什么大问题。。


扩展阅读


处理 Android UI 的方式之外,Compose 本身也有同样的问题,官方也通过自定义扩展函数的形式添加了支持,大致如下:



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

Flutter3.3对Material3设计风格的支持

在Flutter3.3版本以上,支持Material3,使用Material3样式首先是要配置启用Material3。 Material3 主要体现在 圆角风格、颜色、文本样式等方面。 1 配置启用 Material3 查看当前 Flutter的版本 在程序...
继续阅读 »

在Flutter3.3版本以上,支持Material3,使用Material3样式首先是要配置启用Material3。


Material3 主要体现在 圆角风格、颜色、文本样式等方面。


1 配置启用 Material3


查看当前 Flutter的版本


image.png
在程序的入口 MaterialApp中设置主题ThemeData中的useMaterial3属性为true.


///flutter应用程序中的入口函数
void main() => runApp(TextApp());

///应用的根布局
class TextApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
///构建Materia Desin 风格的应用程序
return MaterialApp(
title: "Material3",
///在主题中启用
theme: ThemeData(
brightness: Brightness.light,
colorSchemeSeed: Colors.blue,
//启用
useMaterial3: true,
),
///默认的首页面
home: Material3Home(),
);
}
}

2 按钮样式的改变


按钮默认的圆角大小改变


2.1 ElevatedButton

ElevatedButton(
onPressed: (){},
child: const Text("Elevated"),
),


image.png


2.2 ElevatedButton 自定义样式

  ElevatedButton(
style: ElevatedButton.styleFrom(
// 前景色
// ignore: deprecated_member_use
onPrimary: Theme.of(context).colorScheme.onPrimary,
// 背景色
// ignore: deprecated_member_use
primary: Theme.of(context).colorScheme.primary,
).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)),
onPressed: (){},
child: const Text('Filled'),
),

image.png


2.3 OutlinedButton

OutlinedButton(
onPressed: (){},
child: const Text("Outlined"),
),

image.png


2.4 FloatingActionButton.small

 FloatingActionButton.small(
onPressed: () {},
child: const Icon(Icons.add),
),

image.png


2.5 FloatingActionButton

FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
),

image.png


2.6 FloatingActionButton.extended

   FloatingActionButton.extended(
onPressed: () {},
icon: const Icon(Icons.add),
label: const Text("Create"),
),

image.png


2.7 FloatingActionButton.large

  FloatingActionButton.large(
onPressed: () {},
child: const Icon(Icons.add),
),

image.png


3 AlertDialog 的边框圆角改变


  void openDialog(BuildContext context) {
showDialog<void>(
context: context,
builder: (context) => AlertDialog(
title: const Text("Basic Dialog Title"),
content: const Text(
"A dialog is a type of modal window that appears in front of app content to provide critical information, or prompt for a decision to be made."),
actions: <Widget>[
TextButton(
child: const Text('Dismiss'),
onPressed: () => Navigator.of(context).pop(),
),
TextButton(
child: const Text('Action'),
onPressed: () => Navigator.of(context).pop(),
),
],
),
);
}

image.png


4 主题文本样式的默认大小改变


final textTheme = Theme.of(context)
.textTheme
.apply(displayColor: Theme.of(context).colorScheme.onSurface);


TextStyle large =textTheme.displayLarge;
TextStyle displayMedium =textTheme.displayMedium;
TextStyle displaySmall =textTheme.displaySmall;


image.png


5 ColorScheme 的变更


Widget buildMaterial() {
///当前颜色主题
ColorScheme colorScheme = Theme.of(context).colorScheme;
//背景色
final Color color = colorScheme.surface;
//阴影色
Color shadowColor = colorScheme.shadow;
Color surfaceTint = colorScheme.primary;
return Material(
borderRadius: BorderRadius.all(Radius.circular(4.0)),
elevation: 4,//阴影
color: color,//背景色
shadowColor: shadowColor,//阴影色
surfaceTintColor: surfaceTint,//
type: MaterialType.card,
child: Padding(
padding: const EdgeInsets.all(38.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'测试1',
style: Theme.of(context).textTheme.labelMedium,
),
Text(
'测试2',
style: Theme.of(context).textTheme.labelMedium,
),
],
),
),
);
}

image.png


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

环信文档提升计划|提建议找bug,领京东卡,环信文档“捉虫“活动进行中!

文档是帮助开发者更好地使用产品的重要一环。为更好的提升集成环信产品的便捷性和易用性,环信文档进行了重大升级,此次改版主要提升内容: 1.导航点击转跳丝滑顺畅 ;2.使用体验更好的搜索插件;3.开发者可以直接对文档进行反馈,提交github pul...
继续阅读 »

文档是帮助开发者更好地使用产品的重要一环。为更好的提升集成环信产品的便捷性和易用性,环信文档进行了重大升级,此次改版主要提升内容:

1.导航点击转跳丝滑顺畅 ;2.使用体验更好的搜索插件;3.开发者可以直接对文档进行反馈,提交github pull request


此次版本的重大更新离不开环友们的日常反馈和建议需求,目前环信正式发起文档提升计划,邀您一起打造更加丝滑的文档体验。无论你是第一次接触环信的新朋友还是深度用户,都欢迎参与此次计划。我们将有专人跟进您的反馈和建议。在参与过程中,你可以感受到文档一点点被打磨被完善的过程,感受到所提的建议被采纳的乐趣,还能累计积分兑换环信周边礼品。


活动时间:

即日至2024年12月31日


参与方式:

此次活动文档包含环信即时通讯云文档、超级社区文档。

反馈在文档中发现或遇到的问题,或针对文档提出的建议意见,发送邮件至:market@easemob.com

3个工作日内平台进行反馈。

经评估确认后,可获得一定积分奖励,累计的积分可兑换环信周边或京东卡。


积分规则:

错误类型

具体描述

积分

不规范或低错类

错别字

1分

拼写错误

表述不通顺,病句等

文档格式错乱

内容结构不合理、语言表达不清晰

图形、表格、文字等晦涩难懂

2分

描述存在歧义或有误

缺少必要的条件,说明,注意事项等

不合理的文档结构

内容错误或缺失

界面/功能和文档描述不一致

3分

代码有误,无法指导操作

链接错误

关键步骤错误或缺失

响应结果与文档描述不符

文档接口与SDK不符

内容未更新

内容过时,文档接口过时等,无法指导集成步骤

3分


活动规则:

1、请将详细的文档问题描述发送邮件至 market@easemob.com,邮件内容经官方确认属实,邮件反馈您提交的问题及获得的积分;

2、除以上表格罗列的内容以外,您发现的任何问题都可以邮件反馈给我们,我们都会酌情归类计分。

3、本次活动积分有效期至2024年6月30日,您可在活动期间联系官方兑换礼品。


积分兑换

达到以下积分,即可扣除相应积分进行礼品兑换。

积分

奖励

3分

优秀环友定制徽章

5分

环信定制钥匙扣/小风扇2选1

10分

50元京东卡/环信定制保温杯 2选1

15分

100元京东卡


加入官方活动群


扫码备注“捉虫”,添加冬冬好友拉你进群。


相关地址及开发文档获取

即时通讯云文档:https://docs-im-beta.easemob.com/document/android/quickstart.html

环信超级社区文档:https://docs-im.easemob.com/ccim/circle/overview


*本活动解释权归环信所有

收起阅读 »

Sourcery 的 Swift Package 命令行插件

什么是Sourcery?Sourcery 是当下最流行的 Swift 代码生成工具之一。其背后使用了 SwiftSyntax[1],旨在通过自动生成样板代码来节省开发人员的时间。Sourcery 通过扫描一组输入文件,然后借助模板的帮助,自动生成模板中定义的 ...
继续阅读 »

什么是Sourcery?

Sourcery 是当下最流行的 Swift 代码生成工具之一。其背后使用了 SwiftSyntax[1],旨在通过自动生成样板代码来节省开发人员的时间。Sourcery 通过扫描一组输入文件,然后借助模板的帮助,自动生成模板中定义的 Swift 代码。

示例

考虑一个为摄像机会话服务提供公共 API 的协议:

protocol Camera {
func start()
func stop()
func capture(_ completion: @escaping (UIImage?) -> Void)
func rotate()
}

当使用此新的 Camera service 进行单元测试时,我们希望确保 AVCaptureSession 没有被真的创建。我们仅仅希望确认 camera service 被测试系统(SUT)正确的调用了,而不是去测试 camera service 本身。
因此,创建一个协议的 mock 实现,使用空方法和一组变量来帮助我们进行单元测试,并断言(asset)进行了正确的调用是有意义的。这是软件开发中非常常见的一个场景,如果你曾维护过一个包含大量单元测试的大型代码库,这么做也可能有点乏味。
好吧~不用担心!Sourcery 会帮助你!⭐️ 它有一个叫做 AutoMockable[2] 的模板,此模板会为任意输入文件中遵守 AutoMockable 协议的协议生成 mock 实现。

注意:在本文中,我扩展地使用了术语 Mock,因为它与 Sourcery 模板使用的术语一致。Mock 是一个相当重载的术语,但通常,如果我要创建一个 双重测试[3],我会根据它的用途进一步指定类型的名称(可能是 Spy 、 Fake 、 Stub 等)。如果您有兴趣了解更多关于双重测试的信息,马丁·福勒(Martin Fowler)有一篇非常好的文章,可以解释这些差异。

现在,我们让 Camera 遵守 AutoMockable。该接口的唯一目的是充当 Sourcery 的目标,从中查找并生成代码。

import UIKit

// Protocol to be matched
protocol AutoMockable {}

public protocol Camera: AutoMockable {
func start()
func stop()
func capture(_ completion: @escaping (UIImage?) -> Void)
func rotate()
}

此时,可以在上面的输入文件上运行 Sourcery 命令,指定 AutoMockable 模板的路径:

sourcery --sources Camera.swift --templates AutoMockable.stencil --output .

💡 本文通过提供一个 .sourcery.yml 文件来配置 Sourcery 插件。如果提供了配置文件或 Sourcery 可以找到配置文件,则将忽略与其值冲突的所有命令行参数。如果您想了解有关配置文件的更多信息,Sourcery的 repo 中有一节[4]介绍了该主题。
命令执行完毕后,在输出目录下会生成一个 模板名 加 .generated.swift 为后缀的文件。在此例是 ./AutoMockable.generated.swift:

// Generated using Sourcery 1.8.2 — https://github.com/krzysztofzablocki/Sourcery
// DO NOT EDIT
// swiftlint:disable line_length
// swiftlint:disable variable_name

import Foundation
#if os(iOS) || os(tvOS) || os(watchOS)
import UIKit
#elseif os(OSX)
import AppKit
#endif

class CameraMock: Camera {

//MARK: - start

var startCallsCount = 0
var startCalled: Bool {
return startCallsCount > 0
}
var startClosure: (() -> Void)?

func start() {
startCallsCount += 1
startClosure?()
}

//MARK: - stop

var stopCallsCount = 0
var stopCalled: Bool {
return stopCallsCount > 0
}
var stopClosure: (() -> Void)?

func stop() {
stopCallsCount += 1
stopClosure?()
}

//MARK: - capture

var captureCallsCount = 0
var captureCalled: Bool {
return captureCallsCount > 0
}
var captureReceivedCompletion: ((UIImage?) -> Void)?
var captureReceivedInvocations: [((UIImage?) -> Void)] = []
var captureClosure: ((@escaping (UIImage?) -> Void) -> Void)?

func capture(_ completion: @escaping (UIImage?) -> Void) {
captureCallsCount += 1
captureReceivedCompletion = completion
captureReceivedInvocations.append(completion)
captureClosure?(completion)
}

//MARK: - rotate

var rotateCallsCount = 0
var rotateCalled: Bool {
return rotateCallsCount > 0
}
var rotateClosure: (() -> Void)?

func rotate() {
rotateCallsCount += 1
rotateClosure?()
}

}

上面的文件(AutoMockable.generated.swift)包含了你对mock的期望:使用空方法实现与目标协议的一致性,以及检查是否调用了这些协议方法的一组变量。最棒的是… Sourcery 为您编写了这一切!🎉

怎么运行 Sourcery?

怎么使用 Swift package 运行 Sourcery?
至此你可能在想如何以及怎样在 Swift package 中运行 Sourcery。你可以手动执行,然后讲文件拖到包中,或者从包目录中的命令运行脚本。但是对于 Swift Package 有两种内置方式运行可执行文件:
通过命令行插件,可根据用户输入任意运行
通过构建工具插件,该插件作为构建过程的一部分运行。
在本文中,我将介绍 Sourcery 命令行插件,但我已经在编写第二部分,其中我将创建构建工具插件,这带来了许多有趣的挑战。

创建插件包

让我们首先创建一个空包,并去掉测试和其他我们现在不需要的文件夹。然后我们可以创建一个新的插件 Target 并添加 Sourcery 的二进制文件作为其依赖项。
为了让消费者使用这个插件,它还需要被定义为一个产品:

// swift-tools-version: 5.6
import PackageDescription

let package = Package(
name: "SourceryPlugins",
products: [
.plugin(name: "SourceryCommand", targets: ["SourceryCommand"])
],
targets: [
// 1
.plugin(
name: "SourceryCommand",
// 2
capability: .command(
intent: .custom(verb: "sourcery-code-generation", description: "Generates Swift files from a given set of inputs"),
// 3
permissions: [.writeToPackageDirectory(reason: "Need access to the package directory to generate files")]
),
dependencies: ["Sourcery"]
),
// 4
.binaryTarget(
name: "Sourcery",
path: "Sourcery.artifactbundle"
)
]
)

让我们一步一步地仔细查看上面的代码:

1.定义插件目标。
2.以 custom 为意图,定义了 .command 功能,因为没有任何默认功能( documentationGeneration 和 sourceCodeFormatting)与该命令的用例匹配。给动词一个合理的名称很重要,因为这是从命令行调用插件的方式。
3.插件需要向用户请求写入包目录的权限,因为生成的文件将被转储到该目录。
4.为插件定义了一个二进制目标文件。这将允许插件通过其上下文访问可执行文件。
💡 我知道我并没有详细介绍上面的一些概念,但如果您想了解更多关于命令插件的信息,这里有一篇由 Tibor Bödecs 写的超级棒的文章⭐。如果你还想了解更多关于 Swift Packages 中二级制的目标(文件),我同样有一篇现今 Swift 包中的二进制目标。

编写插件

现在已经创建了包,是时候编写一些代码了!我们首先在 Plugins/SourceryCommand 下创建一个名为 SourceryCommand.swift 的文件,然后添加一个 CommandPlugin 协议的结构体,这将作为该插件的入口:

import PackagePlugin
import Foundation

@main
struct SourceryCommand: CommandPlugin {
func performCommand(context: PluginContext, arguments: [String]) async throws {

}
}

然后我们为命令编写实现:

func performCommand(context: PluginContext, arguments: [String]) async throws {
// 1
let configFilePath = context.package.directory.appending(subpath: ".sourcery.yml").string
guard FileManager.default.fileExists(atPath: configFilePath) else {
Diagnostics.error("Could not find config at: \(configFilePath)")
return
}

//2
let sourceryExecutable = try context.tool(named: "sourcery")
let sourceryURL = URL(fileURLWithPath: sourceryExecutable.path.string)

// 3
let process = Process()
process.executableURL = sourceryURL

// 4
process.arguments = [
"--disableCache"
]

// 5
try process.run()
process.waitUntilExit()

// 6
let gracefulExit = process.terminationReason == .exit && process.terminationStatus == 0
if !gracefulExit {
Diagnostics.error("🛑 The plugin execution failed")
}
}

让我们仔细看看上面的代码:

1.首先 .sourcery.yml 文件必须在包的根目录,否则将报错。这将使 Sourcery 神奇的工作,并使包可配置。
2.可执行文件路径的 URL 是从命令的上下文中检索的。
3.创建一个进程,并将 Sourcery 的可执行文件的 URL 设置为其可执行文件路径。
4.这一步有点麻烦。Sourcery 使用缓存来减少后续运行的代码生成时间,但问题是这些缓存是在包文件夹之外读取和写入的文件。插件的沙箱规则不允许这样做,因此 --disableCache 标志用于禁用此行为并允许命令运行。
5.进程同步运行并等待。
6.最后,检查进程终止状态和代码,以确保进程已正常退出。在任何其他情况下,通过 Diagnostics API 向用户告知错误。
就这样!现在让我们使用它

使用(插件)包

考虑一个用户正在使用插件,该插件将依赖项引入了他们的 Package.swift 文件:

// swift-tools-version: 5.6
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "SourceryPluginSample",
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "SourceryPluginSample",
targets: ["SourceryPluginSample"]),
],
dependencies: [
.package(url: "https://github.com/pol-piella/sourcery-plugins.git", branch: "main")
],
targets: [
.target(
name: "SourceryPluginSample",
dependencies: [],
exclude: ["SourceryTemplates"]
),
]
)

💡 注意,与构建工具插件不同,命令插件不需要应用于任何目标,因为它们需要手动运行。
用户只使用了上面的 AutoMockable 模板(可以在 Sources/SourceryPluginSample/SourceryTemplates 下找到),与本文前面显示的示例相匹配:

protocol AutoMockable {}

protocol Camera: AutoMockable {
func start()
func stop()
func capture(_ completion: @escaping (UIImage?) -> Void)
func rotate()
}

根据插件的要求,用户还提供了一个位于 SourceryPluginSample 目录下的 .sourcery.yml 配置文件:

sources:
- Sources/SourceryPluginSample
templates:
- Sources/SourceryPluginSample/SourceryTemplates
output: Sources/SourceryPluginSample

运行命令

用户已经设置好了,但是他们现在如何运行包?🤔 有两种方法:

命令行

运行插件的一种方法是用命令行。可以通过从包目录中运行 swift package plugin --list 来检索特定包的可用插件列表。然后可以从列表中选择一个包,并通过运行 swift package <command's verb> 来执行,在这个特殊的例子中,运行: swift package sourcery-code-generation。
注意,由于此包需要特殊权限,因此 --allow-writing-to-package-directory 必须与命令一起使用。
此时,你可能会想,为什么我要费心编写一个插件,仍然必须从命令行运行,而我可以用一个简单的脚本在几行 bash 中完成相同的工作?好吧,让我们来看看 Xcode 14 中会出现什么,你会明白为什么我会提倡编写插件📦。

Xcode

这是运行命令插件最令人兴奋的方式,但不幸的是,它仅在 Xcode 14 中可用。因此,如果您需要运行命令,但尚未使用 Xcode 14,请参阅命令行部分。
如果你正好在使用 Xcode 14,你可以通过在文件资源管理器中右键单击包,从列表中找到要执行的插件,然后单击它来执行包的任何命令。

下一步

这是插件的初始实现。我将研究如何改进它,使它更加健壮。和往常一样,我非常致力于公开构建,并使我的文章中的所有内容都开源,这样任何人都可以提交问题或创建任何具有改进或修复的 PRs。这没有什么不同😀, 这是 公共仓库的链接。
此外,如果您喜欢这篇文章,请关注即将到来的第二部分,其中我将制作一个 Sourcery 构建工具插件。我知道这听起来不多,但这不是一项容易的任务!

收起阅读 »

后端一次给你10万条数据,如何优雅展示,到底考察我什么

web
前言大家好,我是林三心,用最通俗的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心,今天跟大家来唠唠嗑,如果后端真的返回给前端10万条数据,咱们前端要怎么优雅地展示出来呢?(哈哈假设后端真的能传10万条数据到前端)前置工作先把前置工作给做好,后面才能进...
继续阅读 »

前言

大家好,我是林三心,用最通俗的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心,今天跟大家来唠唠嗑,如果后端真的返回给前端10万条数据,咱们前端要怎么优雅地展示出来呢?(哈哈假设后端真的能传10万条数据到前端)


前置工作

先把前置工作给做好,后面才能进行测试

后端搭建

新建一个server.js文件,简单起个服务,并返回给前端10w条数据,并通过nodemon server.js开启服务

没有安装nodemon的同学可以先全局安装npm i nodemon -g

// server.js

const http = require('http')
const port = 8000;

http.createServer(function (req, res) {
 // 开启Cors
 res.writeHead(200, {
   //设置允许跨域的域名,也可设置*允许所有域名
   'Access-Control-Allow-Origin': '*',
   //跨域允许的请求方法,也可设置*允许所有方法
   "Access-Control-Allow-Methods": "DELETE,PUT,POST,GET,OPTIONS",
   //允许的header类型
   'Access-Control-Allow-Headers': 'Content-Type'
})
 let list = []
 let num = 0

 // 生成10万条数据的list
 for (let i = 0; i < 100000; i++) {
   num++
   list.push({
     src: 'https://p3-passport.byteacctimg.com/img/user-avatar/d71c38d1682c543b33f8d716b3b734ca~300x300.image',
     text: `我是${num}号嘉宾林三心`,
     tid: num
  })
}
 res.end(JSON.stringify(list));
}).listen(port, function () {
 console.log('server is listening on port ' + port);
})

前端页面

先新建一个index.html

// index.html

// 样式
<style>
   * {
     padding: 0;
     margin: 0;
  }
   #container {
     height: 100vh;
     overflow: auto;
  }
  .sunshine {
     display: flex;
     padding: 10px;
  }
   img {
     width: 150px;
     height: 150px;
  }
 </style>

// html部分
<body>
 <div id="container">
 </div>
 <script src="./index.js"></script>
</body>

然后新建一个index.js文件,封装一个AJAX函数,用来请求这10w条数据

// index.js

// 请求函数
const getList = () => {
   return new Promise((resolve, reject) => {
       //步骤一:创建异步对象
       var ajax = new XMLHttpRequest();
       //步骤二:设置请求的url参数,参数一是请求的类型,参数二是请求的url,可以带参数
       ajax.open('get', 'http://127.0.0.1:8000');
       //步骤三:发送请求
       ajax.send();
       //步骤四:注册事件 onreadystatechange 状态改变就会调用
       ajax.onreadystatechange = function () {
           if (ajax.readyState == 4 && ajax.status == 200) {
               //步骤五 如果能够进到这个判断 说明 数据 完美的回来了,并且请求的页面是存在的
               resolve(JSON.parse(ajax.responseText))
          }
      }
  })
}

// 获取container对象
const container = document.getElementById('container')

直接渲染

最直接的方式就是直接渲染出来,但是这样的做法肯定是不可取的,因为一次性渲染出10w个节点,是非常耗时间的,咱们可以来看一下耗时,差不多要消耗12秒,非常消耗时间


const renderList = async () => {
   console.time('列表时间')
   const list = await getList()
   list.forEach(item => {
       const div = document.createElement('div')
       div.className = 'sunshine'
       div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
       container.appendChild(div)
  })
   console.timeEnd('列表时间')
}
renderList()

setTimeout分页渲染

这个方法就是,把10w按照每页数量limit分成总共Math.ceil(total / limit)页,然后利用setTimeout,每次渲染1页数据,这样的话,渲染出首页数据的时间大大缩减了


const renderList = async () => {
   console.time('列表时间')
   const list = await getList()
   console.log(list)
   const total = list.length
   const page = 0
   const limit = 200
   const totalPage = Math.ceil(total / limit)

   const render = (page) => {
       if (page >= totalPage) return
       setTimeout(() => {
           for (let i = page * limit; i < page * limit + limit; i++) {
               const item = list[i]
               const div = document.createElement('div')
               div.className = 'sunshine'
               div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
               container.appendChild(div)
          }
           render(page + 1)
      }, 0)
  }
   render(page)
   console.timeEnd('列表时间')
}

requestAnimationFrame

使用requestAnimationFrame代替setTimeout,减少了重排的次数,极大提高了性能,建议大家在渲染方面多使用requestAnimationFrame

const renderList = async () => {
   console.time('列表时间')
   const list = await getList()
   console.log(list)
   const total = list.length
   const page = 0
   const limit = 200
   const totalPage = Math.ceil(total / limit)

   const render = (page) => {
       if (page >= totalPage) return
       // 使用requestAnimationFrame代替setTimeout
       requestAnimationFrame(() => {
           for (let i = page * limit; i < page * limit + limit; i++) {
               const item = list[i]
               const div = document.createElement('div')
               div.className = 'sunshine'
               div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
               container.appendChild(div)
          }
           render(page + 1)
      })
  }
   render(page)
   console.timeEnd('列表时间')
}

文档碎片 + requestAnimationFrame

文档碎片的好处

  • 1、之前都是每次创建一个div标签就appendChild一次,但是有了文档碎片可以先把1页的div标签先放进文档碎片中,然后一次性appendChildcontainer中,这样减少了appendChild的次数,极大提高了性能

  • 2、页面只会渲染文档碎片包裹着的元素,而不会渲染文档碎片

const renderList = async () => {
   console.time('列表时间')
   const list = await getList()
   console.log(list)
   const total = list.length
   const page = 0
   const limit = 200
   const totalPage = Math.ceil(total / limit)

   const render = (page) => {
       if (page >= totalPage) return
       requestAnimationFrame(() => {
           // 创建一个文档碎片
           const fragment = document.createDocumentFragment()
           for (let i = page * limit; i < page * limit + limit; i++) {
               const item = list[i]
               const div = document.createElement('div')
               div.className = 'sunshine'
               div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
               // 先塞进文档碎片
               fragment.appendChild(div)
          }
           // 一次性appendChild
           container.appendChild(fragment)
           render(page + 1)
      })
  }
   render(page)
   console.timeEnd('列表时间')
}

懒加载

为了比较通俗的讲解,咱们启动一个vue前端项目,后端服务还是开着

其实实现原理很简单,咱们通过一张图来展示,就是在列表尾部放一个空节点blank,然后先渲染第1页数据,向上滚动,等到blank出现在视图中,就说明到底了,这时候再加载第二页,往后以此类推。

至于怎么判断blank出现在视图上,可以使用getBoundingClientRect方法获取top属性

IntersectionObserver 性能更好,但是我这里就拿getBoundingClientRect来举例


<script setup lang="ts">
import { onMounted, ref, computed } from 'vue'
const getList = () => {
 // 跟上面一样的代码
}

const container = ref<HTMLElement>() // container节点
const blank = ref<HTMLElement>() // blank节点
const list = ref<any>([]) // 列表
const page = ref(1) // 当前页数
const limit = 200 // 一页展示
// 最大页数
const maxPage = computed(() => Math.ceil(list.value.length / limit))
// 真实展示的列表
const showList = computed(() => list.value.slice(0, page.value * limit))
const handleScroll = () => {
 // 当前页数与最大页数的比较
 if (page.value > maxPage.value) return
 const clientHeight = container.value?.clientHeight
 const blankTop = blank.value?.getBoundingClientRect().top
 if (clientHeight === blankTop) {
   // blank出现在视图,则当前页数加1
   page.value++
}
}

onMounted(async () => {
 const res = await getList()
 list.value = res
})
</script>

<template>
 <div id="container" @scroll="handleScroll" ref="container">
   <div class="sunshine" v-for="(item) in showList" :key="item.tid">
     <img :src="item.src" />
     <span>{{ item.text }}</span>
   </div>
   <div ref="blank"></div>
 </div>
</template>

结语

如果你觉得此文对你有一丁点帮助,点个赞,鼓励一下林三心哈哈。

作者:Sunshine_Lin
来源:juejin.cn/post/7031923575044964389

收起阅读 »

超好用的官方core-ktx库,了解一下(终)~

ktx
Handler.postDelayed()简化lambda传入 不知道大家在使用Handler下的postDelayed()方法是不是感觉很不简洁,我们看下这个函数源码: public final boolean postDelayed(@NonNull Ru...
继续阅读 »

Handler.postDelayed()简化lambda传入


不知道大家在使用Handler下的postDelayed()方法是不是感觉很不简洁,我们看下这个函数源码:


public final boolean postDelayed(@NonNull Runnable r, long delayMillis) {
return sendMessageDelayed(getPostMessage(r), delayMillis);
}

可以看到Runnable类型的参数r放在第一位,在Kotlin中我们就无法利用其提供的简洁的语法糖,只能这样使用:


private fun test11(handler: Handler) {
handler.postDelayed({
//编写代码逻辑
}, 100)
}

有没有感觉很别扭,估计官方也发现了这个问题,就提供了这样一个扩展方法:


public inline fun Handler.postDelayed(
delayInMillis: Long,
token: Any? = null,
crossinline action: () -> Unit
): Runnable {
val runnable = Runnable { action() }
if (token == null) {
postDelayed(runnable, delayInMillis)
} else {
HandlerCompat.postDelayed(this, runnable, token, delayInMillis)
}
return runnable
}

可以看到将函数类型(相当于上面的Runnable中的代码执行逻辑)放到了方法参数的最后一位,这样利用kotlin的语法糖就可以这样使用:


private fun test11(handler: Handler) {
handler.postDelayed(200) {

}
}

可以看到这个函数类型使用了crossinline修饰,这个是用来加强内联的,因为其另一个Runnable的函数类型中进行了调用,这样我们就无法在这个函数类型action中使用return关键字了(return@标签除外),避免使用return关键字带来返回上的歧义不稳定性


除此之外,官方core-ktx还提供了类似的扩展方法postAtTime()方法,使用和上面一样!!


Context.getSystemService()泛型实化获取系统服务


看下以往我们怎么获取ClipboardManager:


private fun test11() {
val cm: ClipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
}

看下官方提供的方法:


public inline fun <reified T : Any> Context.getSystemService(): T? =
ContextCompat.getSystemService(this, T::class.java)

借助于内联泛型实化简化了获取系统服务的代码逻辑:


private fun test11() {
val cm: ClipboardManager? = getSystemService()
}

泛型实化的用处有很多应用场景,大家感兴趣可以参考我另一篇文章:Gson序列化的TypeToken写起来太麻烦?优化它


Context.withStyledAttributes简化操作自定义属性


这个扩展一般只有在自定义View中较常使用,比如读取xml中设置的属性值,先看下我们平常是如何使用的:


private fun test11(
@NonNull context: Context,
@Nullable attrs: AttributeSet,
defStyleAttr: Int
) {
val ta = context.obtainStyledAttributes(
attrs, androidx.cardview.R.styleable.CardView, defStyleAttr,
androidx.cardview.R.style.CardView
)
//获取属性执行对应的操作逻辑
val tmp = ta.getColorStateList(androidx.cardview.R.styleable.CardView_cardBackgroundColor)

ta.recycle()
}

在获取完属性值后,还需要调用recycle()方法回收TypeArray,这个一旦忘记写就不好了,能让程序保证的写法那就尽量避免人为处理,所以官方提供了下面的扩展方法:


public inline fun Context.withStyledAttributes(
@StyleRes resourceId: Int,
attrs: IntArray,
block: TypedArray.() -> Unit
) {
obtainStyledAttributes(resourceId, attrs).apply(block).recycle()
}

使用如下:


private fun test11(
@NonNull context: Context,
@Nullable attrs: AttributeSet,
defStyleAttr: Int
) {
context.withStyledAttributes(
attrs, androidx.cardview.R.styleable.CardView, defStyleAttr,
androidx.cardview.R.style.CardView
) {
val tmp = getColorStateList(androidx.cardview.R.styleable.CardView_cardBackgroundColor)
}
}

上面的写法就保证了recycle()不会漏写,并且带接收者的函数类型block: TypedArray.() -> Unit也能让我们省略this直接调用TypeArray中的公共方法。


SQLiteDatabase.transaction()自动开启事务读写数据库


平常对SQLite进行写操作时为了效率及安全保证需要开启事务,一般我们都会手动进行开启和关闭,还是那句老话,能程序自动保证的事情就尽量避免手动实现,所以一般我们都会封装一个事务开启和关闭的方法,如下:


private fun writeSQL(sql: String) {
SQLiteDatabase.beginTransaction()
//执行sql写入语句
SQLiteDatabase.endTransaction()
}

官方core-ktx也提供了相似的扩展方法:


public inline fun <T> SQLiteDatabase.transaction(
exclusive: Boolean = true,
body: SQLiteDatabase.() -> T
): T {
if (exclusive) {
beginTransaction()
} else {
beginTransactionNonExclusive()
}
try {
val result = body()
setTransactionSuccessful()
return result
} finally {
endTransaction()
}
}

大家可以自行选择使用!


<K : Any, V : Any> lruCache()简化创建LruCache


LruCache一般用作数据缓存,里面使用了LRU算法来优先淘汰那些近期最少使用的数据。在Android开发中,我们可以使用其设计一个Bitmap缓存池,感兴趣的可以参考Glide内存缓存这块的源码,就利用了LruCache实现。


相比较于原有创建LruCache的方式,官方库提供了下面的扩展方法简化其创建流程:


inline fun <K : Any, V : Any> lruCache(
maxSize: Int,
crossinline sizeOf: (key: K, value: V) -> Int = { _, _ -> 1 },
@Suppress("USELESS_CAST")
crossinline create: (key: K) -> V? = { null as V? },
crossinline onEntryRemoved: (evicted: Boolean, key: K, oldValue: V, newValue: V?) -> Unit =
{ _, _, _, _ -> }
): LruCache<K, V> {
return object : LruCache<K, V>(maxSize) {
override fun sizeOf(key: K, value: V) = sizeOf(key, value)
override fun create(key: K) = create(key)
override fun entryRemoved(evicted: Boolean, key: K, oldValue: V, newValue: V?) {
onEntryRemoved(evicted, key, oldValue, newValue)
}
}
}

看下使用:


private fun createLRU() {
lruCache<String, Bitmap>(3072, sizeOf = { _, value ->
value.byteCount
}, onEntryRemoved = { evicted: Boolean, key: String, oldValue: Bitmap, newValue: Bitmap? ->
//缓存对象被移除的回调方法
})
}

可以看到,比之手动创建LruCache要稍微简单些,能稍微节省下使用成本。


bundleOf()快捷写入并创建Bundle对象


image.png


bundleOf()方法的参数被vararg声明,代表一个可变的参数类型,参数具体的类型为Pair,这个对象我们之前的文章有讲过,可以借助中缀表达式函数to完成Pair的创建:


private fun test12() {
val bundle = bundleOf("a" to "a", "b" to 10)
}

这种通过传入可变参数实现的Bundle如果大家不太喜欢,还可以考虑自行封装通用扩展函数,在函数类型即lambda中实现更加灵活的Bundle创建及写入:


1.自定义运算符重载方法set实现Bundle写入:


operator fun Bundle.set(key: String, value: Any?) {
when (value) {
null -> putString(key, null)

is Boolean -> putBoolean(key, value)
is Byte -> putByte(key, value)
is Char -> putChar(key, value)
is Double -> putDouble(key, value)
is Float -> putFloat(key, value)
is Int -> putInt(key, value)
is Long -> putLong(key, value)
is Short -> putShort(key, value)

is Serializable -> putSerializable(key, value)
//其实数据类型自定参考bundleOf源码实现
}
}

2.自定义BundleBuild支持向Bundle写入多个值


class BundleBuild(private val bundle: Bundle) {

infix fun String.to(that: Any?) {
bundle[this] = that
}
}

其中to()方法使用了中缀表达式的写法


3.暴漏扩展方法实现在lambda中完成Bundle的写入和创建


private fun bundleOf(block: BundleBuild.() -> Unit): Bundle {
return Bundle().apply {
BundleBuild(this).apply(block)
}
}

然后就可以这样使用:


private fun test12() {
val bundle = bundleOf {
"a" to "haha"
//经过一些逻辑操作获取结果后在写入Bundle
val t1 = 10 * 5
val t2 = ""
t2 to t1
}
}

相比较于官方库提供的bundleOf()提供的创建方式,通过函数类型也就是lambda创建并写入Bundle的方式更加灵活,并内部支持执行操作逻辑获取结果后再进行写入。


总结


关于官方core-ktx的研究基本上已经七七八八了,总共输出了五篇相关文章,对该库了解能够节省我们编写模板代码的时间,提高开发效率,大家如果感觉写的不错,可以点个赞支持下哈,感谢!!


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

【Android爬坑周记】用SplashScreen做一个会动的开屏!

Android 12以上加入了SplashScreen,并且支持开屏动画了!因此我在【小鹅事务所】项目中加入了一个开屏动画,如下(为方便动图展示,我故意延长了几秒钟): SplashScreen 简单介绍一下SplashScreen,仅在冷启动或者温启动的时...
继续阅读 »

Android 12以上加入了SplashScreen,并且支持开屏动画了!因此我在【小鹅事务所】项目中加入了一个开屏动画,如下(为方便动图展示,我故意延长了几秒钟):


开屏.gif


SplashScreen


简单介绍一下SplashScreen,仅在冷启动或者温启动的时候会展示SplashScreen,支持VAD动画、帧动画。我就先使用帧动画实现这个开屏动画,后面会考虑换成VAD动画。关于SplashScreen具体就不细讲啦,我讲这些讲不明白,没有官方文档讲得好,直接进入实战!!


注意裁切


image_bbWXUd5R2b.png


ICON在设计的时候只能够占用三分之二大小的圆,超出这部分的会被裁切掉,所以这点需要注意!


设计


首先打开UI设计软件,我此处用Figma,新建一个方形的框框,方形的框框里面整一个三分二大小的圆圈,像这样。


image_zN3wn3Qj0_.png


然后呢,就把设计好的Icon放进去


image_nPXCuEjgOv.png


这个时候一张静态图就做好啦,但是帧动画需要让图片动起来的话,就需要多张静态图。怎么设计它动起来呢?我的思路是让它扭头!像这样。


image_bKPGvPkiso.png


然后再把框框的颜色隐藏掉,我们只需要透明背景的Icon


image_9vcZawLYyF.png


注意,为了展示外边需要留空间,我给它们的框框加上描边,实际不需要!这个时候就可以导出图片啦,我这边选择导出矢量图,也就是SVG格式。


image_tg-XgUBqeW.png


导入动画


打开Android Studio,右键点击res → new → Vector Asset,再导入图片,将静态图都导进去就可以做动画啦。


image_7DgCf5ExTe.png


新建anim_little_goose.xml,根标签是animation-list,并在里面放4个item。


<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<item
android:drawable="@drawable/ic_little_goose"
android:duration="50" />
<item
android:drawable="@drawable/ic_little_goose_back"
android:duration="150" />
<item
android:drawable="@drawable/ic_little_goose"
android:duration="50" />
<item
android:drawable="@drawable/ic_little_goose_fore"
android:duration="150" />
</animation-list>

根据命名可以看出




  • 第一帧为正常的小鹅,展示50毫秒




  • 第二帧为向后扭头的小鹅,展示150毫秒




  • 第三帧为正常的小鹅,展示50毫秒




  • 第四帧为向前扭头的小鹅,展示150毫秒




一次循环就是400毫秒,点开Split界面,就能在右边预览动画了,这个时候,动画就简简单单做好了。


As.gif


SplashScreen


引入依赖


由于SplashScreen是Android12以上才有的,而Android12以下需要适配,但是!Jetpack提供了同名适配库,去gradle引用就好了。


//SplashScreen
implementation 'androidx.core:core-splashscreen:1.0.0'

设置开屏主题


然后在res/values/themes中新建一个style标签,并将其父标签设为Theme.SplashScreen,需要注意的是,如果适配了黑夜模式的话,也可以在values-night/themes文件下单独配置。


<style name="Theme.AppSplashScreen" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/primary_color</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/anim_little_goose</item>
<item name="windowSplashScreenAnimationDuration">3000</item>
<item name="postSplashScreenTheme">@style/Theme.Account</item>
</style>

我这边配置一个无ICON背景的动画,因此不用windowSplashScreenIconBackgroundColor标签设置ICON背景。


简单介绍一下我设置的4个标签




  • windowSplashScreenBackground设置整个开屏动画的背景颜色。




  • windowSplashScreenAnimatedIcon设置的是开屏动画播放的动画文件,也就是上面写的动画文件。




  • windowSplashScreenAnimationDuration设置的是动画的播放时长,也就是说小鹅抖三秒钟头就会停止播放。




  • postSplashScreenTheme这个设置的是开屏动画播放完需要回到的主题,此处设置了我的主题。


    <style name="Theme.Account" parent="Theme.MaterialComponents.DayNight.NoActionBar">
    ...
    </style>



在Manifest注册


    <application
android:label="@string/app_name"
...
android:theme="@style/Theme.Account">

...
<activity
android:name=".ui.MainActivity"
android:theme="@style/Theme.AppSplashScreen"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

</application>

可以看到在打开应用打开的第一个Activity,即MainActivity中设置了开屏主题,而在Application中设置了自己的主题。在Application设置主题的话,这个Application中的除了特殊设置Theme的Activity,其它都默认使用Application主题。


去MainActivity吧!


class MainActivity : BaseActivity() {

private val binding by viewBinding(ActivityMainBinding::inflate)

override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
splashScreen.setKeepOnScreenCondition { !isAppInit }
super.onCreate(savedInstanceState)
initView()
}
...

}

重写onCreate函数,并在调用super.onCreate之前加载SplashScreen,即调用installSplashScreen,获得一个splashScreen实例,理论上来说调用installSplashScreen函数已经可以实现开屏动画了,可是我想等到一部分数据加载完再进入APP怎么办?


可以看到我调用了setKeepOnScreenCondition 函数,传入一个接口,这个接口返回一个Boolean值,如果返回true则继续展示开屏,如果返回false则进入APP。而此函数在每次绘制之前都会调用,是主线程调用的,因此不能在这里处理太多东西阻塞主线程!


我这边就设置了一个顶层变量,每次都去看看这个顶层变量的值,不会阻塞主线程。


class AccountApplication : Application() {

val supervisorScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

//初始化数据 防止第一次打开还要加载
private fun initData() {
supervisorScope.launch {
val initIconDataDeferred = async { TransactionIconHelper.initIconData() }
val initTransactionDeferred = async { TransactionHelper.initTransaction() }
val initScheduleDeferred = async { ScheduleHelper.initSchedule() }
val initNoteDeferred = async { NoteHelper.initNote() }
val initMemorialsDeferred = async { MemorialHelper.initMemorials() }
val initTopMemorialDeferred = async { MemorialHelper.initTopMemorial() }
val initDataStoreDeferred = async { DataStoreHelper.INSTANCE.initDataStore() }
initIconDataDeferred.await()
initTransactionDeferred.await()
initScheduleDeferred.await()
initNoteDeferred.await()
initMemorialsDeferred.await()
initTopMemorialDeferred.await()
initDataStoreDeferred.await()
isAppInit = true
}
}
}

var isAppInit = false

我在Application中对所有需要初始化的东西先初始化一遍,初始化完之后再将isAppInit设置为true,此时在闪屏那边获取的为false,也就是说就会进入APP了。


到这里就结束了,去运行一下吧!


开屏.gif


总结


说实话,在我看来,SplashScreen其实用处不大,因为我们的闪屏一般是用来放advertisement的,而不是放有趣的动画的!


参考


SplashScreen: developer.android.google.cn/develop/ui/…


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

Android APT实战学习技巧

apt
简介 APT(Annotation Processing Tool)即注解处理器,在编译的时候可以处理注解然后搞一些事情,也可以在编译时生成一些文件之类的。ButterKnife和EventBus都使用了APT技术,如果不会APT技术就很难看懂这两个框架的源码...
继续阅读 »

简介


APT(Annotation Processing Tool)即注解处理器,在编译的时候可以处理注解然后搞一些事情,也可以在编译时生成一些文件之类的。ButterKnife和EventBus都使用了APT技术,如果不会APT技术就很难看懂这两个框架的源码。


作用


使用APT可以在编译时来处理编译时注解,生成额外的Java文件,有如下效果:



  • 可以达到减少重复代码手工编写的效果。



如ButterKnife,我们可以直接使用注解来减少findviewbyid这些代码,只需要通过注解表示是哪个id就够了。




  • 功能封装。将主要的功能逻辑封装起来,只保留注解调用。

  • 相对于使用Java反射来处理运行时注解,使用APT有着更加良好的性能。


Android基本编译流程


Android中的代码编译时需要经过:Java——>class ——> dex 流程,代码最终生成dex文件打入到APK包里面。


编译流程如图所示:




  • APT是在编译开始时就介入的,用来处理编译时注解。

  • AOP(Aspect Oridnted Programming)是在编译完成后生成dex文件之前,通过直接修改.class文件的方式,来对代码进行修改或添加逻辑。常用在在代码监控,代码修改,代码分析这些场景。


基本使用


基本使用流程主要包括如下几个步骤:



  1. 创建自定义注解

  2. 创建注解处理器,处理Java文件生成逻辑

  3. 封装一个供外部调用的API

  4. 项目中调用


整理思路



  1. 首先我们需要创建两个JavaLibrary

  2. 一个用来定义注解,一个用来扫描注解

  3. 获取到添加注解的成员变量名

  4. 动态生成类和方法用IO生成文件


实战


创建一个空项目



创建两个JavaLibrary



  • 注解的Lib: apt-annotation

  • 扫描注解的Lib: apt-processor




创建完之后



app模块依赖两个Library


implementation project(path: ':apt-annotation')
annotationProcessor project(path: ':apt-processor')


注解Lib中创建一个注解类


如果还不会自定义注解的同学,可以先去看我之前写的一篇Java自定义注解入门到实战


@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface Print {

}


扫描注解的Lib添加依赖


dependencies {
//自动注册,动态生成 META-INF/...文件
implementation 'com.google.auto.service:auto-service:1.0-rc6'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
//依赖apt-annotation
implementation project(path: ':apt-annotation')
}


创建扫描注解的类



重写init方法,输出Hello,APT


注意: 这里是JavaLib,所以不能使用Log打印,这里可以使用Java的println()或注解处理器给我们提供的方法,建议使用注解处理器给我们提供的



现在我们已经完成了APT的基本配置,现在我们可以build一下项目了,成败在此一举



如果你已经成功输出了文本,说明APT已经配置好,可以继续下一步了


继续完成功能


现在我们可以继续完成上面要实现的功能了,我们需要先来实现几个方法


/**
* 要扫描扫描的注解,可以添加多个
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> hashSet = new HashSet<>();
hashSet.add(Print.class.getCanonicalName());
return hashSet;
}

/**
* 编译版本,固定写法就可以
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return processingEnv.getSourceVersion();
}


定义注解


我们先在MianActivity中添加两个成员变量并使用我们定义的注解



定义注解


真正解析注解的地方是在process方法,我们先试试能不能拿到被注解的变量名


/**
* 扫描注解回调
*/
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//拿到所有添加Print注解的成员变量
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Print.class);
for (Element element : elements) {
//拿到成员变量名
Name simpleName = element.getSimpleName();
//输出成员变量名
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE,simpleName);
}
return false;
}


编译试一下



生成类


既然能拿到被注解的变量名,后面就简单了,我们只需要用字符串拼出来一个工具类,然后用IO流写到本地就ok了



查看效果


现在点击一下编译,然后我们可以看到app模块下的build文件已经有我们生成的类了



调用方法


现在我们回到MainActivity,就可以直接调用这个动态生成的类了



总结


优点:


它可以做任何你不想做的繁杂的工作,它可以帮你写任何你不想重复代码,将重复代码抽取出来,用AOP思想去编写。 它可以生成任何java代码供你在任何地方使用。


难点:


在于设计模式和解耦思想的灵活应用。在于代理类代码生成的繁琐:你可以手动进行字符串拼接,也可以用squareup公司的javapoet库来构建出任何你想要的java代码。


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