注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

众人围剿,GPT-5招惹了谁

GPT-4 火爆全球,引发了人工智能大浪潮。过去的一个月,OpenAI、微软、谷歌加上百度不断释放王炸,所有人都相信,AI 的就是未来的生产力。俗话说,人红是非多,树未大已招风,这不,反对 AI 继续前进的声音就来了。 千人呼吁暂停AI训练 3月29日,马斯克...
继续阅读 »



GPT-4 火爆全球,引发了人工智能大浪潮。过去的一个月,OpenAI、微软、谷歌加上百度不断释放王炸,所有人都相信,AI 的就是未来的生产力。俗话说,人红是非多,树未大已招风,这不,反对 AI 继续前进的声音就来了。


千人呼吁暂停AI训练


3月29日,马斯克、苹果联合创始人 Steve Wozniak、Stability AI创始人 Emad Mostaque 等上千名科技大佬和AI专家签署公开信,呼吁暂停训练比 GPT-4 更强大的 AI 系统,为期6个月。


image.png
根据公开信的表示,在这 6 个月内,全社会需要完成这些事:



  • 所有 AI 实验室和独立学者都应该合力开发一套共享安全协议,用于高级 AI 的设计和开发

  • 协议完成后,应该由独立的外部专家进行严格的审计和监督

  • 这些协议必须确保这些 AI 系统毋庸置疑的安全

  • 如果不能迅速暂停,就应该让政府介入。


所有的人工智能研究和开发,都应该重新聚焦于这一点——让当如今最强大的 SOTA 模型更加准确、安全、可解释、透明、稳健、对齐,值得人类信赖,对人类忠诚。


代表人物分析


这次呼吁大佬众多,最具代表性的无疑是马斯克和 Stability AI 创始人 Emad Mostaque。


马斯克是 OpenAI 公司的联合创始人之一,可谓是原始股东,但他在2018年离开了 OpenAI 的董事会。马斯克一直对微软和比尔盖茨持批评意见,对于OpenAI也是如此,此前曾表示“ OpenAI 最初是作为一家开源的非营利性公司而创建的,为了抗衡谷歌,但现在它已经成一家闭源的营利性公司,由微软有效控制……这完全不是我的本意。”


言外之意,OpenAI 不应该成为一个赚钱的公司,应该开源,让所有人看到核心代码和核心算法。如果是这样,应该建议特斯拉免费开源所有自动驾驶技术源码,马斯克对这个问题的回复是“如果其他汽车制造商想要获得授权并在他们的汽车上使用特斯拉的自动驾驶技术,这将是非常酷的一件事情,但是考虑到该系统开发成本极高,特斯拉将会收取一定的费用。”


划重点就是自动驾驶技术成本高,所以要收费。ChatGPT 的训练成本高,惨遭无视。


另一位代表人物 Emad Mostaque 是 AIGC 独角兽企业 Stability AI 的创始人,号称“要让10亿人用上开源大模型”。Stability AI 最牛的项目是人工智能文本转图像模型 Stable Diffusion ,如今,这个项目深陷侵权旋涡。在今年一月份,全球知名图片提供商华盖创意(Getty Images)和艺术家萨拉·安德森(Sarah Andersen), 凯利·麦克南(Kelly McKernan)和卡拉·奥尔蒂斯(Karla Ortiz)起诉了Stability AI,认为Stability AI在未经许可或考虑的情况下,使用他人的知识产权,为自己的经济利益服务。


下面这幅图中,左边是知名油画家Erin Hanson的作品 "Crystalline Maples",右边是CNN记者通过 Stable Diffusion 生成的结果。


image.png


以我们受过九年义务教育的眼光来看,这两幅图风格、色彩,线条几乎一样,说是出自同一人之手也不为过。


在自己公司严重侵犯他人知识产权的情况下,去说另一家公司影响了人类安全和社会稳定,不过是五十步笑百步了。


反对原因分析


信息安全


信息安全是过去三个月最容易攻击ChatGPT的理由,联名信提出了一条质询,我们是否应该让机器用宣传和谎言充斥我们的信息渠道?综合起来的观点是,不良行为者可能会故意创建带有事实错误的内容,作为战略影响力活动的一部分,传播错误信息、虚假信息和彻头彻尾的谎言,这可能会对社会和个人造成危害。将这个观点强加于ChatGPT上,是避重就轻之举。


虚假信息,有什么比搜索引擎更多吗?虚假广告,违禁视频,歧视言论等数不胜数,上当受骗的人同样数不胜数。相较而言,ChatGPT的表现已经非常遵守道德和法律了。


错误信息,对于企业而言,文本信息会经过员工的二次编辑和确认,才会发布;错误的代码会经过程序员的修改和验证,才会用于产品中。只要责任制明确,风险是可控的。


人身安全


这次事件,被大家提起最多一条理由就是比GPT-4更先进AI系统将威胁人类安全,AI将杀死人类。若说威胁安全,智能驾驶和机器人更具有天然不安全属性。GPT-5终究是活在互联网世界中,任他搅的天翻地覆,也不会直接对人类进行物理攻击。智能驾驶如果失去控制,将导致车毁人亡,交通瘫痪。未来给机器人装上武器,就是最强特种兵。


不可否认的是,AI未来确实存在风险,但我们不能饮鸩止渴,因为未来的风险而停止新技术的前进。人工智能炒作了这么多年,直到ChatGPT才真正点燃了火炬,我们不应该在刚刚见到光明时,就亲手熄灭了它,技术推进和安全协议制定完全可以同步进行。


失业


根据高盛研究报告,全球预计将有3亿个工作岗位被生成式AI取代。目前欧美约有三分之二的工作岗位都在某种程度上受到AI自动化趋势的影响,而多达四分之一的当前岗位有可能最终被完全取代。该研究计算出美国63%的工作暴露在“AI影响范围”中,其中7%的工作有一半以上的流程可以由AI自动化完成,这使他们很容易马上被人工智能取代。


对于国内来说,目前感觉还好,可能主要在图像创作领域感受到寒意比较强,上周看到有博主表示,公司一次性裁了三个原画师。


b2f5b5034fac9cc366bff4dcc1815a32.jpeg
当新技术出现时,初期给社会带来的冲击会让很多人感到不适应,因为不适应,所以会本能的去排斥它。比如曾经的克隆,刚出现时引起了大家恐慌,认为会制造另一个自己,同时带来繁衍上的伦理问题。再比如前几年新能源起步时,大家纷纷嘲讽新能源车,认为它是来收智商税的,时至今日,新能源车已经是大势所趋。


现在的失业主要是国际经济形势带来的,而不是刚刚发展的AI系统带来的。ChatGPT只是一个工具,若说替代,机器人替换下来的劳动人口更多,但没有千名大佬站出来说要暂停机器人技术的发展。


利益


世上没有无缘无故的爱,也没有无缘无故的恨,天下熙熙,皆为利来,天下攘攘,皆为利往,所谓者,都是为了自身利益。呼吁暂停训练比 GPT-4 更强大的 AI 系统,目前只有OpenAI有能力训练比GPT-4更强大的系统GPT-5。根据预测,作为过渡的 GPT-4.5 模型将在 2023 年 9 月或 10 月推出,刚好就是联名信提出的暂停6个月。因此,所谓的的暂停,完全就是针对OpenAI的GPT-5。


OpenAI和微软在三月份的一系列进展让其他的公司产生了深深的危机感,这次的专家有的是自己拥有AI公司,有的是自己在AI领域深耕多年,通常来说,大多数人已经成为了利益团体的代言人。既生瑜何生亮,我没有的你也不能有,我有了,但你一枝独秀,那就枪打出头鸟。只有减缓OpenAI的发展速度,才能给自己追赶的机会。


正如前谷歌大脑成员吴恩达所说,我们该做的,应该是在AI创造的巨大价值与现实风险之间,取得一个平衡。把“让AI取得超越GPT-4的进展”暂停6个月,这个想法很糟糕。


总结


AI不是洪水猛兽,暂停GPT-5训练的做法解决不了安全问题,只有技术演进和安全协议制定同步进行,才能实现科技繁荣。6个月后的GPT-4.5依然只是一个工具,不存在威胁人类安全的可能,之后需要更多训练时间的GPT-5同样只是一个工具,这段时间,足够制定联名信期望的安全协议了。


所以,当务之急不是暂停训练比 GPT-4 更强大的 AI 系统,而是立即推动安全协议条款的研究。


作者:柒号华仔
来源:juejin.cn/post/7216412604800450621
收起阅读 »

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

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

介绍


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


自定义字体


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


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


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



  1. 将字体文件添加到 assets 或 res/font 文件夹中。

  2. 通过以下代码设置字体:


// 字体文件放到 assets 文件夹的情况
Typeface tf = Typeface.createFromAsset(getAssets(), "fonts/myfont.ttf");
TextView tv = findViewById(R.id.tv);
tv.setTypeface(tf);

// 字体文件放到 res/font 文件夹的情况, 需注意的是此方式在部分低于 Android 8.0 的设备上可能会存在兼容性问题
val tv = findViewById<TextView>(R.id.tv)
val typeface = ResourcesCompat.getFont(this, R.font.myfont)
tv.typeface = typeface

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


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


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


AutoLink


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


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


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


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


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


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


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


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


对齐模式


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


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


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


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

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


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


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


作者:GeekTR
来源:juejin.cn/post/7217082232937283645
收起阅读 »

连续加班一个多月后,反思一下为啥国内程序员加班这么多

连续加班一个多月后,反思一下为啥国内程序员加班这么多防杠指南:本文不适用于资深大佬,若喷必回今年过完年之后一直在加班,关注我的粉丝应该也能看出来,2 月份和 3 月份写的笔记确实比较少,最近才开始恢复加班完毕是得好好思考一下,毕竟咱这班也不能白加了对吧,我得好...
继续阅读 »

连续加班一个多月后,反思一下为啥国内程序员加班这么多

防杠指南:本文不适用于资深大佬,若喷必回

今年过完年之后一直在加班,关注我的粉丝应该也能看出来,2 月份和 3 月份写的笔记确实比较少,最近才开始恢复

加班完毕是得好好思考一下,毕竟咱这班也不能白加了对吧,我得好好想一想到底是为什么会导致我加班,我细数了一下平时导致我加班几个主要原因,大家看看有没有共鸣

业务需求倒排期,改的随意

互联网公司的业务迭代是非常快的,尤其是电商、营销相关的业务,基本上随时都在出需求,需求顺排倒还好,无非就是给了排期之后顺着做就行了

但是有一个非常蛋疼的点,如果这个需求业务方要的非常急,比如说 15 号出的需求 PRD ,月底就得上线,必须得倒排,那么就是说上线的时间定了,测试的时间占用一段,联调的时间再占用一段,留给开发的时间真的不多了

时间不够怎么办?要么加人要么加班,加人还有个问题,有的功能并不是很好拆分,而且人多了管理成本也在增加,1+1 并不是一定能等于 2 ,所以到最后就只能全员加班来肝需求

关于业务需求,还有一个可能导致加班的点是改的随意。

之前我在字节跳动打工的时候,每次需求评审会一堆年轻的 PM ,跟唱戏似的,你方唱罢我方上,哭爹喊娘的说自己的需求是多么多么的重要,常用的话术是:我这个需求是 xx 级别的老板看重的、我这个需求可以为公司创造 xx 的收入等等

一个个的 PRD 写的怎么样不重要,最重要的是抢占研发资源,最好可以把程序员固定在自己手里

等到需求开始做了,发现其实 PRD 里面有很多东西没想明白,这个时候就开始改 PRD ,改了 PRD 但是研发排期却不变,那这咋办呢?程序员加班呗

所以国内经常流行一个调侃的对联:

上联是:这个需求很简单

下联是:怎么实现我不管

横批是:明天上线

虽然这个对联是调侃的,但也暗示了很多公司在研发流程的不规范、管理混乱,这也是大部分程序员加班的重要原因

会议太多,占用时间

会议太多这个事情可能是大公司的通病,有时候屁大点事情就拉个会议,我细数了一下我一个月参加的会议:

  1. 需求评审会
  2. 技术方案评审会
  3. 需求复盘会
  4. 细节对齐会
  5. xx 项目启动会议
  6. xx 横向项目
  7. 技术分享会
  8. 周会
  9. 测试用例评审
  10. OKR 会议
  11. CodeReview 会议
  12. 等等......

其实这里面的会议真的太多了,有的团队还有早晨的站会等等,进一步压缩了写代码的时间

那能不能提升效率呢?我觉得可以

就说这个需求评审会吧,如果说每个人会前都能仔细的过一遍 PRD ,记录好疑点,那评审会完全可以开成答疑会,解答完疑问就差不多了,这样子可以节约很多时间,不至于一个需求评审会就开一两个小时

还有技术分享会,很多 leader 为了提升团队的技术氛围会要求组员进行技术分享,但是有的时候,分享的东西别人不一定感兴趣,深度把握的不好的话组员也会只把它当做任务去完成,这就是纯粹的浪费时间了

总之会议这部分,我觉得是一个存在很大提效空间的地方,一个事情是否需要拉会、是否要拉那么多人,是值得思考的

技术需求,各种丐版轮子

关于技术需求这个问题,我不知道是不是国内程序员的特色哈,就是纯做 PM 提的业务需求是很难得到好绩效和晋升的,因为这些事情是你工作职责范围内的事情,你很难说清楚这些事情带来的收益是 PM 的功劳还是研发的功劳

要想得到好绩效、超出预期,那就必须得做一些纯技术的事情,也就是所谓的“技术需求”,而且必须自己挤时间做,不会为这部分工作量专门划时间

常见的技术需求,比如说这两年特别流行的 LowCode 平台,据我所知很多大公司都在搞这种,并且是投入了很多研发的精力在里面的,美其名曰 LowCode 平台可以提高效率,所以在很多需求开发中强行推,要求研发必须使用 LowCode 平台来完成研发,但是在使用的过程中并没有提升效率,反而让研发增加了很多兼容成本和额外的工作量,不管能不能提供效率,先卷了再说

甚至有时候,多个团队之间在卷同样的技术轮子,一个大公司内部至少有 3 个 LowCode 平台、5 个组件库、3 个部署平台、4 个项目管理平台等等,大家都在加班卷技术项目,卷自己团队的存在感和好绩效

到最后,这个技术项目会出现在晋升答辩的 PPT 和汇报材料上,包装后的数字和成果都很亮眼,技术项目的发起者拿到了好绩效、晋升成功,等到晋升成功之后,这个技术项目的使命也就完成了,从此刻开始它就走上了烂尾的道路,历史项目也就留下了一堆烂摊子代码

老老实实做业务需求的人得不到晋升,做各种丐版技术轮子并且强推的人最后得到了晋升,这个问题在国内大公司非常普遍,这也是造成很多研发被卷着加班的重要原因

杂七杂八的事情,耗费精力

程序员还有一些杂事儿,也是相当的耗费精力了,我举几个例子

首先说线上 oncall ,这个事情其实也算是研发的正常工作范围内的事情了,但是如果一天出一个比较麻烦的线上 bug ,那今天肯定其他的事情就没空做了,或者只能加班去做

更不用说,如果所在的部门是基础架构部门的话,要处理技术之外的一些使用答疑事项,这部分事情毫无技术含量,和客服无异

还有就是非常强调技术要去深入业务,好嘛没问题,但是深入业务也是需要耗费时间的,这就意味着你除了读 PRD 以外还得去看 MRD ,可能你需要去和业务部门、市场部门的同事开会旁听 ta 门关心的事情,除过技术相关的东西以外还需要去关注业务指标

这又给自己增加了工作量,leader 不会说专门给这部分工作量去给你增加时间,只能自己挤时间了,这无形中又增加了加班

总结

我总结的这几个原因是我结合自身加班情况分析而来,可能国外的程序员也存在同样的问题,也可能有的人看法不一样,欢迎交流。

作者:程序员耳东
来源:www.v2ex.com/t/927862
收起阅读 »

关于Android相册实现的一些经验

一、序 我之前发布了个图片加载框架,在JCenter关闭后,“闭关修炼”,想着改好了出个2.0版本。 后来觉得仅增加功能和改进实现不够,得补充一下用例。 相册列表的加载就是很好的用例,然后在Github找了一圈,没有找到满意的,有的甚至好几年没维护了,于是就自...
继续阅读 »

一、序


我之前发布了个图片加载框架,在JCenter关闭后,“闭关修炼”,想着改好了出个2.0版本。

后来觉得仅增加功能和改进实现不够,得补充一下用例。

相册列表的加载就是很好的用例,然后在Github找了一圈,没有找到满意的,有的甚至好几年没维护了,于是就自己写了一个。

代码链接:github.com/BillyWei01/…


相比于图片加载,相册加载在Github上要多很多。

其原因大概是图片加载的input/output比较规范,不涉及UI布局;
而相册则不然,几乎每个APP都会有自己独特的需求,有自己的UI风格。

因此,相册库很难做到通用于大部分APP。

我所实现的这个也一样,并非以实现通用的相册组件为目的,而是作为一个样例,以供参考。


二、 需求描述


网上不少相册的开源库,都是照微信相册来搭的界面,我也是跟着这么做吧,要是说涉及侵权什么的,那些前辈应该先比我收到通知……

主要是自己也不会UI设计,不找个参照对象怕实现的太难看。

话说回来,要是真的涉及侵权,请联系我处理。


相册所要实现的功能,概括来说,就是显示相册列表,点击缩略图选中,点击完成结束选择,返回选择结果。


需求细节,包括但不限于以下列表:



  • 实现目录列表,相册列表,预览页面;

  • 支持单选/多选;

  • 支持显示选择顺序和限定选择数量;

  • 支持自定义筛选条件;

  • 支持自定义目录排序;

  • 支持“原图”选项;

  • 支持再次进入相册时传入已经选中的图片/视频;

  • 支持切换出APP外拍照或删除照片后,回到相册时自动刷新;


效果如图:


easy_album_cn.jpg


三、API设计


由于不同的页面可能需求不一样,所以可以将需求参数封装到”Request“中;

对于通用的选项,以及相册组件的全局配置,可以更封装到“Config"中。

而Request/Config最好是用链式API去设置参数,链式API尤其适合参数是“可选项”的场景。


3.1 全局设置


EasyAlbum.config()
.setImageLoader(GlideImageLoader)
.setDefaultFolderComparator { o1, o2 -> o1.name.compareTo(o2.name)}

GlideImageLoader是相册组件定义的ImageLoader接口的实现类。


public interface ImageLoader {
void loadPreview(MediaData data, ImageView imageView, boolean asBitmap);

void loadThumbnail(MediaData data, ImageView imageView, boolean asBitmap);
}

不同的APP使用的图片加载框架不一样,所以相册组件最好不要强依赖图片加载框架,而是暴露接口给调用者。

当然,对于整个APP而言,不建议定义这样的ImageLoader类,因为APP使用图片加载的地方很多,

定义这样的类,要么需要重载很多方法,要么就是参数列表很长,也就丧失链式API的优点了。


关于目录排序,EasyAlbum中定义的默认排序是按照更新时间(取最新的图片的更新时间)排序。

上面代码举例的是按目录名排序。

如果需要某个目录排在列表前面,可以这样定义(以“Camera”为例):


private val priorityFolderComparator = Comparator<Folder> { o1, o2 ->
val priorityFolder = "Camera"
if (o1.name == priorityFolder) -1
else if (o2.name == priorityFolder) 1
else o1.name.compareTo(o2.name)
}

出个思考题:

如果需要“优先排序”的不只一个目录,比如希望“Camera"第一优先,"Screenshots"第二优先,“Pictures"第三优先……

改如何定义Comparator?


3.2 启动相册


EasyAlbum启动相册以from起头,以start结束。


EasyAlbum.from(this)
.setFilter(TestMediaFilter(option))
.setSelectedLimit(selectLimit)
.setOverLimitCallback(overLimitCallback)
.setSelectedList(mediaAdapter?.getData())
.setAllString(option.text)
.enableOriginal()
.start { result ->
mediaAdapter?.setData(result.selectedList)
}

具体到实现,就是from返回 Request, Request的start方法启动相册页(AlbumActivity)。


public class EasyAlbum {
public static AlbumRequest from(@NonNull Context context) {
return new AlbumRequest(context);
}
}

public final class AlbumRequest {
private WeakReference<Context> contextRef;

AlbumRequest(Context context) {
this.contextRef = new WeakReference<>(context);
}

// ...其他参数..

public void start(ResultCallback callback) {
Session.init(this, callback, selectedList);
if (contextRef != null) {
Context context = contextRef.get();
if (context != null) {
context.startActivity(new Intent(context, AlbumActivity.class));
}
contextRef = null;
}
}
}

启动AlbumActivity,就涉及传参和结果返回。

有两种思路:



  1. 通过intent传参数到AlbumActivity, 用startActivityForResult启动,通过onActivityResult接收。

  2. 通过静态变量传递参数,通过Callback回调结果。


第一种方法,需要所有的参数都能放入Intent, 基础数据可以传,自定义数据类可以实现Parcelable,

但那对于接口的实现,就没办法放 intent 了,到头来还是要走静态变量。

因此,干脆就都走静态变量传递好了。

这个方案可行的前提是, AlbumActivity是封闭的,不会在跳转其他Activity。

在这个前提下,App不会同一个时刻打开多个AlbumActivity,不需要担心共享变量相互干扰的情况。

然后就是,在Activity结束时,做好清理工作。

可以将“启动相册-选择图片-结束相册”抽象为一次“Session”, 在相册结束时,执行一下clear操作。


final class Session {
static AlbumRequest request;
static AlbumResult result;
private static ResultCallback resultCallback;

static void init(AlbumRequest req, ResultCallback callback, List<MediaData> selectedList) {
request = req;
resultCallback = callback;
result = new AlbumResult();
if (selectedList != null) {
result.selectedList.addAll(selectedList);
}
}

static void clear() {
if (request != null) {
request.clear();
request = null;
resultCallback = null;
result = null;
}
}
}

四、媒体文件加载


媒体文件加载似乎很简单,就调ContentResolver query一下的事,但要做到尽量完备,需要考虑的细节还是不少的。


4.1 MediaStore API


查询媒体数据库,需走ContentResolver的qurey方法:


public final Cursor query( 
Uri uri,
String[] projection,
String selection,
String[] selectionArgs,
String sortOrder,
CancellationSignal cancellationSignal)
{
}

媒体数据库记录了各种媒体类型,要过滤其中的“图片”和“视频”,有两种方法:


1、用SDK定义好的MediaStore.Video和MediaStore.Images的Uri。


MediaStore.Video.Media.EXTERNAL_CONTENT_URI
MediaStore.Images.Media.EXTERNAL_CONTENT_URI

2、直接读取"content://external", 通过MEDIA_TYPE字段过滤。


private static final Uri CONTENT_URI = MediaStore.Files.getContentUri("external");

private static final String TYPE_SELECTION = "(" + MediaStore.Files.FileColumns.MEDIA_TYPE + "="
+ MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO
+ " OR " + MediaStore.Files.FileColumns.MEDIA_TYPE + "="
+ MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE
+ ")";

如果需要同时读取图片和视频,第2种方法更省事一些。


至于查询的字段,视需求而定。

以下是比较常见的字段:


private static final String[] PROJECTIONS = new String[]{
MediaStore.MediaColumns._ID,
MediaStore.MediaColumns.DATA,
MediaStore.Files.FileColumns.MEDIA_TYPE,
MediaStore.MediaColumns.DATE_MODIFIED,
MediaStore.MediaColumns.MIME_TYPE,
MediaStore.Video.Media.DURATION,
MediaStore.MediaColumns.SIZE,
MediaStore.MediaColumns.WIDTH,
MediaStore.MediaColumns.HEIGHT,
MediaStore.Images.Media.ORIENTATION
};

DURATION, SIZE, WIDTH, HEIGHT,ORIENTATION等字段有可能是无效的(0或者null),

如果是无效的,可以去从文件本身获取,但读文件比较耗时,

所以可以先尝试从MediaStore读取,毕竟是都访问到这条记录了,从空间局部原理来说,读取这些字段是顺便的事情,代价要比另外读文件本身低很多。

当然,如果确实不需要这些信息,可以直接不读取。


4.2 数据包装


数据查询出来,需要定义Entity来包装数据。


public final class MediaData implements Comparable<MediaData> {
private static final String BASE_VIDEO_URI = "content://media/external/video/media/";
private static final String BASE_IMAGE_URI = "content://media/external/images/media/";

static final byte ROTATE_UNKNOWN = -1;
static final byte ROTATE_NO = 0;
static final byte ROTATE_YES = 1;

public final boolean isVideo;
public final int mediaId;
public final String parent;
public final String name;
public final long modifiedTime; // in seconds
public String mime;

long fileSize;
int duration;
int width;
int height;
byte rotate = ROTATE_UNKNOWN;

public String getPath() {
return parent + name;
}

public Uri getUri() {
String baseUri = isVideo ? BASE_VIDEO_URI : BASE_IMAGE_URI;
return Uri.parse(baseUri + mediaId);
}

public int getRealWidth() {
if (rotate == ROTATE_UNKNOWN || width == 0 || height == 0) {
fillData();
}
return rotate != ROTATE_YES ? width : height;
}

public int getRealHeight() {
if (rotate == ROTATE_UNKNOWN || width == 0 || height == 0) {
fillData();
}
return rotate != ROTATE_YES ? height : width;
}

// ......
}

4.2.1 数据共享


字段的定义中,没有直接定义path字段,而是定义了parent和name,因为图片/视频文件可能有成千上万个,但是目录大概率不会超过3位数,所以,我们可以通过复用parent来节约内存。

同理,mime也可以复用。


截取部分查询的代码:


int count = cursor.getCount();
List<MediaData> list = new ArrayList<>(count);
while (cursor.moveToNext()) {
String path = cursor.getString(IDX_DATA);
String parent = parentPool.getOrAdd(Utils.getParentPath(path));
String name = Utils.getFileName(path);
String mime = mimePool.getOrAdd(cursor.getString(IDX_MIME_TYPE));
// ......
}

复用字符串,可以用HashMap来做,我这边是仿照HashMap写了一个专用的类来实现。

getOrAdd方法:传入一个字符串,如果容器中已经有这个字符串,返回容器保存的字符串,
否则,保存当前字符串并返回。

如此,所有的MediaData共用相同parent和mime字符串对象。


4.2.2 处理无效数据


前面提到,从MediaStore读取的数据,有部分是无效的。

这些可能无效的字段不要直接public, 而是提供get方法,并在返回之前检查数据的有效性,如果数据无效则读文件获取数据。

当然,读文件是耗时操作,虽然一般情况下时间是可控的,但是最好还是放IO线程去访问比较保险。


也有比较折中的做法:



  1. 数据只是用作参考,有的话更好,没有也没关系。

    如果是这样的话,提供不做检查直接返回数据的方法:


    public int getWidth() {
return rotate != ROTATE_YES ? width : height;
}

public int getHeight() {
return rotate != ROTATE_YES ? height : width;
}


  1. 数据比较重要,但也不至于没有就不行。

    这种case,当数据无效时,可以先尝试读取,但是加个timeout, 在规定时间内没有完成读取则直接返回。


    public int getDuration() {
if (isVideo && duration == 0) {
checkData();
}
return duration;
}

void checkData() {
if (!hadFillData) {
FutureTask<Boolean> future = new FutureTask<>(this::fillData);
try {
// Limit the time for filling extra info, in case of ANR.
AlbumConfig.getExecutor().execute(future);
future.get(300, TimeUnit.MILLISECONDS);
} catch (Throwable ignore) {
}
}
}

4.3 数据加载


数据加载部分是最影响相册体验的因素之一。

等待时间、数据刷新,数据有效性等都会影响相册的交互。


4.3.1 缓存MediaData


媒体库查询是一个综合IO读取和CPU密集计算的操作,文件少的时候还好,一旦文件比较多,耗时几秒钟也是有的。

如果用户每次打开相册都要等几秒钟才刷出数据,那体验就太糟糕了。

加个MediaData的缓存,再次进入相册时,就不需要再次读所有字段了,

只需读取MediaStore的ID字段,然后结合缓存,做下Diff, 已删除的移除出缓存,新增的根据ID检索其记录,创建MediaData添加到缓存。

再次进入相册,即使有增删也不会太多。


缓存MediaData的好处不仅仅是加速再次查询MediaStore,还可以减少对象的创建,不需要每次查询都重新创建MediaData对象;

另外,前面也提到,MediaData部分字段有的是无效的,在无效时需要读取原文件获取,缓存MediaData可免去再次读文件获取数据的时间(如果对象是读取MediaStore重新创建的,就又回到无效的状态了)。


还有就是,有缓存的话,就可以做预加载了。

当然这个得看APP是否有这个需求,如果APP是媒体相关的,大概率要访问相册的,可以考虑预加载。


做缓存的代价就是要占用些内存,这也是前面MediaData为什么复用parent和mime的原因。

缓存是空间换时间,复用对象是时间换空间,总体而言这个对冲是赚的,因为读取IO更耗时。

另外,如果有必要,可以提供clearCache接口,在适当的时机清空缓存。


4.3.2 组装结果


相册的UI层所需要的是: 根据Request的查询条件过滤后的MediaData,以目录为分组,按更新时间降序排列的数据。
缓存的MediaData并非查询的终点,但却提供了一个好的起点。

在有缓存好的MediaData列表的前提下,可直接根据MediaData列表做过滤,排序和分组,

而不需要每次都将过滤条件拼接SQL到数据库中查询,而且相比于拼接SQL,在上层直接根据MediaData过滤要更加灵活。


下面是EasyAlbum基于MediaData缓存的查询:


private static List<Folder> makeResult(AlbumRequest request) {
AlbumRequest.MediaFilter filter = request.filter;
ArrayList<MediaData> totalList = new ArrayList<>(mediaCache.size());

if (filter == null) {
totalList.addAll(mediaCache.values());
} else {
// 根据filter过滤MediaData
for (MediaData item : mediaCache.values()) {
if (filter.accept(item)) {
totalList.add(item);
}
}
}

// 先对所有MediaData排序,后面分组后就不需要继续在分组内排序了
// 因为分组时是按顺序放到分组列表的。
Collections.sort(totalList);

Map<String, ArrayList<MediaData>> groupMap = new HashMap<>();
for (MediaData item : totalList) {
String parent = item.parent;
ArrayList<MediaData> subList = groupMap.get(parent);
if (subList == null) {
subList = new ArrayList<>();
groupMap.put(parent, subList);
}
subList.add(item);
}

final List<Folder> result = new ArrayList<>(groupMap.size() + 1);
for (Map.Entry<String, ArrayList<MediaData>> entry : groupMap.entrySet()) {
String folderName = Utils.getFileName(entry.getKey());
result.add(new Folder(folderName, entry.getValue()));
}

// 对目录排序
Collections.sort(result, request.folderComparator);

// 最后,总列表放在最前
result.add(0, new Folder(request.getAllString(), totalList));
return result;
}

MediaFilter的定义如下:


public interface MediaFilter {
boolean accept(MediaData media);

// To identify the filter
String tag();
}

基于MediaData缓存列表的查询虽然比基于数据库的查询快不少,但是当文件很多时,也还是要花一些时间的。
所以我们可以再加一个缓存:缓存最终结果。

再加一个结果缓存,只是增加了些容器,容器指向的对象(MediaData)是之前MediaData缓存列表所引用的对象,所以代价还好。

再次进入相册时,可以先直接取结果显示,然后再去检查MediaStore相对于缓存有没有变更,有则刷新缓存和UI,否则直接返回。

APP可能有多个地方需要相册,不同地方查询条件可能不一样,所以MediaFilter定义了tag接口,用来区分不同的查询。


4.3.3 加载流程


流程图如下:

注意,下图的“结果”是提供给相册页面显示的数据,并非相册返回给调用者的“已选中的媒体”。



做了两层缓存,加载流程是复杂一些。

但好处也是显而易见的,增加了结果缓存之后,再次启动相册就基本是“秒开”了。

查询过程是在后台线程中执行的,结果通过handler发送给AlbumActivity。


图中还有一些小处理没画出来。

比如,首次加载,在发送结果给相册界面之后,还会继续执行一个“检查文件是否已删除”的操作。

针对的是这么一种情况:MediaStore中的记录,DATA字段所对应的文件不存在。

我自己的设备上是没有出现过这种case, 我也是听前辈讲的,或许他们遇到过。

如果确实有设备存在这样的情况,的确应该检查一下,否则相册滑动到这些“文件不存在”的记录时,会只看到一片黑,稍微影响体验。

但由于我自己没有具体考证,所以在EasyAblum的全局配置中留了option, 可以设置不执行。

关于这点大家按具体情况自行评估。


加载流程一般在进入相册页时启动。

考虑到用户在浏览相册时,有时候可能会切换出去拍照或者删除照片,可在onResume的时候也启动一下加载流程,检查是否有媒体文件增删。


五、相册列表


5.1 媒体缩略图


Android系统对相册文件提供了获取缩略图的API,通过该API获取图片要比直接读取媒体文件本身要快很多。
一些图片加载框架中有实现相关逻辑,比如Glide的实现了MediaStoreImageThumbLoader和MediaStoreVideoThumbLoader,但是所用API比较旧,在我的设备(Android 10)上已经不生效了。

如果使用Glide的朋友可以自行实现ModelLoader和ResourceDecoder来处理。

EasyAlbum的Demo中有实现,感兴趣的朋友可以参考一下。


5.2 列表布局


相册列表通常是方格布局,如果RecycleView布局,最好能让每一列都等宽。

下面这个ItemDecoration的实现是其中一种方法:


public class GridItemDecoration extends RecyclerView.ItemDecoration {
private final int n; // 列的数量
private final int space; // 列与列之间的间隔
private final int part; // 每一列应该分摊多少间隔

public GridItemDecoration(int n, int space) {
this.n = n;
this.space = space;
// 总间隔:space * (n - 1) ,等分n份
part = space * (n - 1) / n;
}

@Override
public void getItemOffsets(
@NonNull Rect outRect,
@NonNull View view,
@NonNull RecyclerView parent,
@NonNull RecyclerView.State state)
{
int position = parent.getChildLayoutPosition(view);
int i = position % n;
// 第i列(0开始)的左边部分的间隔的计算公式:space * i / n
outRect.left = Math.round(part * i / (float) (n - 1));
outRect.right = part - outRect.left;
outRect.top = 0;
outRect.bottom = space;
}
}

其原理就是将所有space加起来,等分为n份,每个item分摊1份。

其中第i列(index从0开始)的左边部分的间隔的计算公式为:space * i / n 。

比方说colomn = 4, 那么就有3个space; 如果每个space=4px, 则每个item分摊4 * (4-1)/ 4 = 3px。

第1个item, left=0px, right = 3px;

第2个item, left=1px, right = 2px;

第3个item, left=2px, right =1px;

第4个item, left=3px, right =0px。

于是,每个间隔看起来都是4px, 且每个item的left+right都是相等的,所以留给view的宽度是相等的。

效果如下图:



有的地方是这么去分配left和right的:


        outRect.left = column == 0 ? 0 : space / 2;
outRect.right = column == (n - 1) ? 0 : space / 2;

这样能让每个间隔的大小相等,但是view本身的宽度就不相等了。

效果如下图:



左右两个item分别比中间的item多了2px。

这2px看上去不多,但是可能会导致列表变更(增删)时,图片框架的缓存失效。

例如:

如果删除了最接近的一张照片,原第2-4列会移动到1-3列,原第1列会移动到第4列。

于是第2列的宽度从266变为288,第4列的宽度从288变为266,

而图片加载框架的target宽高是缓存key的计算要素之一,宽度变了,就不能命中之前的缓存了。


六、后序


相册的实现可简单可复杂,我见过的最简单的实现是直接在主线程查询媒体数据库的……

本文从各个方面分享了一些相册实现的经验,尤其是相册加载部分。

目前这个时代,手机存几千上万张图片是很常见的,优化好相册的加载,能提升不少用户体验。


项目已发布到Github和Maven Central:


Githun地址:
github.com/BillyWei01/…


下载方式:


implementation 'io.github.billywei01:easyalbum:1.0.6'

作者:呼啸长风
来源:juejin.cn/post/7215163152907092024
收起阅读 »

2022,这一年,我三十,而未立。

子曰:“吾十有五,而志于学,三十而立”。 一、我的背景 1. 大学之路 我大学时学的是电气工程及其自动化专业,和编程相关的学科有 plc 和单片机,但他们都不是 web 编程。后来机缘巧合,想做个网站,自学起了 web 编程,因为什么也不懂,也没人咨询,盲选了...
继续阅读 »

子曰:“吾十有五,而志于学,三十而立”。


一、我的背景


1. 大学之路


我大学时学的是电气工程及其自动化专业,和编程相关的学科有 plc 和单片机,但他们都不是 web 编程。后来机缘巧合,想做个网站,自学起了 web 编程,因为什么也不懂,也没人咨询,盲选了 PHP,再后来又学了些前端知识,算是入门了吧。


2. 初入职场


2016年,我毕业了。电气工程的工作是真不好找,好在我的 web 编程基础还算扎实,就想着去做个码农吧。我的第一份工作,找的很随性,那时连招聘软件都不了解,网上随意的搜到了家南京的软件小公司,看到官网的电话,打了过去。他招人,我找工作,就这么成了。

面试那天,雨特别大,老板后来和我说只有不是特别菜,冒这么大雨来,就要了。公司很小,一共4个人,老板和我简单聊了聊,看了看我带去的作品,就这么成了。于是我就有了第一份工作,成为了一名 phper

年底时,我跑路了,薪资太低,活不下去了,(公司竟然连五险都没给我交,不过这是后话了)。年后,换了一家继续做 phper,主要是微信相关的开发,虽然还有好几个开发者同事,但微信这块,只有我一个人,所以算是个“全干工程师”吧。


3. 不破不立


18年中,在这家干了一年半的我再次提桶,这一次我又是裸辞。既有自身能力原因,也有大环境因素,一个月时间,我没能找到合适的 php 工作。眼看身无分文,痛定思痛,我离开了南京,到了苏州这个竞争压力小些的地方。不仅是地理上离开了舒适区,工作上我也不得不从 phper 转向前端了。曾经学习与使用多年的 php,简化成了简历上的一句 “熟悉 php 语言” 几个文字。


4. 渐入佳境


靠着网友们的鼓励,我在苏州找到了一份不错的前端工作,公司在调整期,我成了前端接盘者,幸运的是没过多久项目就开始了重构。公司的技术栈是 Vue 和微信小程序。好在这些我之前有所涉及,在跌跌撞撞中也还算应付的不错。之后几年,每次有新的项目,我都会总结之前项目的问题,将学到的知识与经验应用其中,这期间技能得到了不小的成长。后来有了更多小伙伴的加入,我也开始负责了些前端的管理工作。


4. 愈感迷茫


如今,我已在这家公司练习时长四年半。并非现在的公司环境有多舒适或者福利待遇优渥,同一批共事的同事,现在也只剩下了一位。期间也有过多次离开的想法,但近几年的大环境,一直不是很好,生活上的琐事也不得不花费不少精力去处理。

可能我的能力,停留在了2021年初了吧,近几年的前端新技术,都没有触及。我仍然守着我的 Vue 2 与微信小程序这点地方,努力耕耘着。像 Vue 3viteTS 等,还停留在听说过的阶段。我有许多想做和需要做的事情,但时间,总是不够用,年已三十,孑然一身。


二、回顾 2022


这一年感觉经历了很多事情,不管是生活还是工作。但细想,又说不出是哪些。我想从迷茫中走出来。


1. 身份的转变


年初伊始,部门因为人员多了,boss 希望我们的部门经理把权力下放,不要事事躬亲。在 boss 的多次劝说下,经理索性当甩手掌柜,前端组完全交给了我去负责。经过一年的时间,我并未感觉到有多少管理能力的提升,领导经常贩卖焦虑,公司也在业务转型,事情比之前多了,原来他分配各每个人的活大多还能适量,现在都堆积到我这边,平均到每个人可能是1.5个量了,带来无止境的加班。我感觉我没做好。


2. 重拾旧代码


年初,我又折腾起了搁置了挺久的影视介绍网站项目,之前断断续续,以学习为目的折腾了4、5年了。今年终于陆续的折腾完,并开源了出来。因为项目较久,很多代码写的很是拙劣,所以我按照目前的能力,尽可能的做了重构。


今年的晚上下班后与周末的时间,尽数花在这个上了。
下面截取一张项目的更新图吧:


image.png

image.png



周末我一般不提交代码,所以没有提交记录。



3. 开启新篇章


当你看到这篇文章时,我已经成为了掘金的一名技术分享者。选择掘金社区,是因为从掘金中,我学到了挺多的知识。喝水不忘挖井人,之前我是个知识的获取者,现在我希望能将这几年学到的一些技术与经验,分享给大家。


三、展望 2023


1. 不忘初心


2023,是18天后。大环境会变成什么样子,还是个未知数。我无法预测环境,但可以规划自己,我会坚持将自己的知识与大家分享。


2. 重新开始


四年多的前端工作经验,后期能力停滞不前。而我最需要的,就是打破这种状态,“三十而已”嘛。工作上:我在考虑换个工作环境,离开目前的舒适区。职业上:会去了解、学习、应用 Vue3TS微服务LowCode等等不算新的新事物。


结语


相聚掘金,与君共勉。


作者:冰糖雪梨同学
来源:juejin.cn/post/7176262850011693116
收起阅读 »

怎么去选择一个公司?

一家公司的好坏,除去客观情况,更多是个人的主观感受,甲之蜜糖,乙之砒霜,别人口中的“好公司”,未必适合你。 那我们应该怎么挑选一个适合自己的公司呢?还是有一些可以考量的指标的。 企业文化和价值观 行业势头 工资待遇 公司规模 人才水平 企业文化和价值观 无...
继续阅读 »

一家公司的好坏,除去客观情况,更多是个人的主观感受,甲之蜜糖,乙之砒霜,别人口中的“好公司”,未必适合你。


那我们应该怎么挑选一个适合自己的公司呢?还是有一些可以考量的指标的。



  • 企业文化和价值观

  • 行业势头

  • 工资待遇

  • 公司规模

  • 人才水平


企业文化和价值观


无法适应企业文化和价值观的员工,注定会被边缘化,获取不到资源,直到被淘汰。而适应企业文化和价值观的员工,在公司做事情则更能够得心应手。


如何选择适合自己的企业文化和价值观


如果你打算在一个公司长期发展,可以试着找找里面的熟人,聊聊公司内部的做事风格,比如晋升、奖金、淘汰、组内合作、跨部门合作以及如何处理各种意外情况等,这样就能实际感受到企业的文化和价值观了,然后再根据自己的标准,判断是否适合自己。


行业势头


行业一般会有风口期、黄金发展期和下降期三个阶段。



  • 处于下降趋势的行业要慎重考虑。

  • 处于风口期的行业发展趋势还不是很明显,如果你之前从事的行业和新的风口相关,那么不妨试试;如果你对这些风口背后的行业不是很熟悉,那不妨等风口的势头明朗了,再做打算。

  • 处于黄金发展期的行业发展已经稳定,有成熟的盈利模式,在这样的行业中积累经验,会在行业的发展上升期变得越来越值钱。如果你对这些行业感兴趣,不妨考虑相关的公司。


工资待遇


工资待遇不仅仅包括固定工资,还有一次性收入、奖金、股票以及各种福利等。


很多新入职的员工会有一些的奖金,例如签字费、安家费等,这些是一次性的,有时还会附加”规定时间内不能离职”等约束条件。这部分钱的性价比比较低,但一般金额还不错。


奖金主要看公司,操作空间很大,它和公司的经营状况关联紧密,谈Offer时约定的数额到后面不一定能够兑现,尤其是这两年整个互联网行业都不景气,很多公司的奖金都“打骨折”甚至直接取消了。


其他福利一般包括商业医疗保险、年假、体检、补贴等,它和公司所在行业有关联,具有公司特色。


股票也是待遇中很重要的一部分,很多公司在签Offer时会约定一定数量的股票,但是会分四年左右结清,这需要考虑你能坚持四年吗?四年之后没有股票要怎么办?


公司规模


如果待遇和岗位差不多,建议优先选择头部大公司,这样你可以学到更多的经验,接触更有挑战的业务场景,让自己成长的更快。


如果你看好一个行业,那么需要努力进入这个行业的头部公司。


人才水平


一个公司的人才水平,决定了公司对人才的态度和公司内部合作与管理的风格。


举个例子,如果一个公司里程序员的水平都很一般,那么这个公司就更倾向于不相信员工的技术能力,并制定非常细致和严格的管理规范和流程,避免员工犯错。如果你的水平高,就会被各种管理规范和流程束缚住。同时,如果你发现与你合作的人的水平都很“感人”,你也需要调整自己的风格,让自己的工作成果能够适应公司普遍的水平。




此文章为极客时间3月份Day26学习笔记,内容来自《职场生存

手册》课程。

收起阅读 »

掌控情绪,成为自己的主宰

最近看了一本《蛤蟆先生去看心理医生》,这是一本很薄的书,四五个小时就能看完,但看完觉得收获非常大,建议大家都去看看。 其中,对于三种人格状态、人生坐标及其自证预言的让我耳目一新,获益良多。童年时期对世界的态度和看法会让我们树立起人生坐标,在其后的成长阶段影响我...
继续阅读 »

最近看了一本《蛤蟆先生去看心理医生》,这是一本很薄的书,四五个小时就能看完,但看完觉得收获非常大,建议大家都去看看。


其中,对于三种人格状态、人生坐标及其自证预言的让我耳目一新,获益良多。童年时期对世界的态度和看法会让我们树立起人生坐标,在其后的成长阶段影响我们对待事物时所站的角度,或自怨自艾陷入自责螺旋、或怨天尤人指责他人、或理智冷静分析问题并解决问题,从而实现童年时期建立的人生坐标的自证预言。


此外,关于情绪的产生,究竟是目的产生情绪,还是情绪产生目的,文中也提出了一些与以往截然不同的观点。


比如有的挑剔型父母,不会讲道理或者懒得讲道理,把愤怒当成更便捷、更省事的手段,震慑住自己的孩子,进而使他听自己的话。这个例子里,可以说情绪是被被捏造出来的一种可放可收的控制他人的工具。


下文一起探讨一下一些书中的观点。


一、三种人格状态


对于每个人来说,会同时有儿童、父母以及成年人的状态。每一种自我状态都包括完整的思想、情感和行为方式,人与人之间的交往就是各自的三种不同人格状态之间的交往。


当面临训斥或批评等情境时,会触发切换到不同的人格状态。比如被老师、领导、权威、家长训斥,这时会切换到适应性儿童状态,感到无助、自责。此时,有的人会出现缺少自尊的行为,比如讨好或自我贬低,把自己放在弱者的地位,希望得到别人的同情,而此时对方则处在控制型父母状态。


三种人格状态


1.1 儿童自我状态


儿童自我状态源于童年时期的经历,是个体最先诞生的人格状态,是一个人从脆弱、幼小、无助,任何事都要依赖别人的阶段形成的个性部分。


适应性儿童自我状态


适应性儿童自我状态源于童年时期安全感的缺乏,发展出依赖性和迎合性的个性特征。这种状态下,会表现出顺从、听话、讨好等行为,内心常常充满自责、担心、焦虑。


在被批评或者自我批评的时候,常常会进入适应性儿童自我状态,感到无助和沮丧。


自由型儿童自我状态


自由型儿童自我状态则因为在童年时期得到了足够的支持和鼓励,孩子们的个性得到了充分的发展。


在这种状态下,人们表现出冲动、天真、撒泼、贪玩、冒险等行为,像以自我为中心的儿童一样追求快感并能充分表达自我的感情。当我们大哭、大笑的时候就处于这种状态。最典型的表达方式是“我要”或“我不要”。


1.2 父母自我状态


控制型父母自我状态


控制型父母自我状态与人交往表现出教育、批评、教训、控制的一面,他们会用言行重复从父母那里学来的观念和价值观,并试图证明给别人看,让别人接受他们的观念和价值观。


他们会动不动就指责你,还用不可能达到的标准来评判别人。指责你时会假装成营养型父母自我状态,说一些“我比你更心痛”,“我是为了你好”之类的话。


处于这个状态的人总希望扮演法官的角色,不停地控诉别人,给别人定罪,然后顺理成章地惩罚他们。有时甚至会将审判的矛头指向自己,进行毫不留情的自我批判。


他们从来不会抑郁,因为愤怒能够非常有效地抵抗抑郁。愤怒的人从不觉得内疚,因为他们总在怪罪别人。他们自卫的方式,是把自己内在的恐惧对外投射到别人身上,这样就能把对自己的怒火转向别人。


营养型父母自我状态


营养型父母自我状态与人交往表现出温暖、关怀、安慰、鼓励,就像母亲一样温柔体贴地对待身边的人。


1.3 成人自我状态


成人自我状态与人交往表现出理性、冷静、沉稳,而且善于思考利弊,用理性而不是情绪化的方式来行事,能理性地应对正在发生的现实状况。


进入儿童状态和父母状态是被迫还是主动?


在某些场景下,经验会告诉你,现在应该愤怒了,因此你就条件反射产生愤怒。比如父母不经过你的授权就把你的玩具送给了别人,老师冤枉了没有偷东西的你,莫名其妙被路人大妈骂了一顿,在这些状态下,我们很难保持理智,经常会做出情感化的反应。


此时我们会觉得愤怒是别人引发的,是别人为你选择的,因而别人控制了你的情绪。


但除非强迫,没有人能让我们产生什么感受,说到底,是我们选择了自己的感受。是自己选择了愤怒,也自己选择了悲伤。


只有成人自我状态才能理性思考


当处于儿童状态时,你会体验到童年的感受,比如无助、自责、冲动、愤怒,再次体验到过去的情绪,但学不到任何新的东西了。


当处在控制型父母状态时,基本上你不是在挑剔就是在教育别人。旧的思想主宰着你,这就是为什么单靠争论不能改变一个人的想法,只会让人更固执己见。


我们在这两种状态时,像父母或儿童一样行事,几乎不需要去思考,因为我们知道要做什么、说什么,就好像出演一个我们最喜欢、最了解的角色,台词和动作都烂熟于心。


比方说,有个角色叫生气鬼(当然没有比善于打压式教育的中国家长更适合扮演生气鬼这个角色了)。


生气鬼很懂该怎么表达愤怒。遇到适合他演的剧目,他能一字不差地说出台词,而且他经常遇到这样的场景,是不是很奇怪?他能不假思索地切换到愤怒的语调和音高,自动筛选出合适的用词,他的整个姿态都在表达愤怒。总之,他演的生气鬼接近完美,而关键在于,甚至都不用动脑子!就好像为了这场演出他排练了一辈子,而频繁地出演这个角色也使得他每一次表演都更传神。


只有在成人自我状态里,才能学到关于自我的新知识,因为只有在那个时候,才能理智思考当下的事情,评估自己的行为,或者倾听别人对你的看法而不马上驳斥。只有在这个时候,我们所有的知识和技能都能为己所用,而不再被脑子里父母过去的声音所驱使,也不会被童年的情绪所困扰。


二、人生坐标


每一个生命一定都得经历开始、中间和结束这三个阶段,而开始的阶段会显著地影响后来的阶段。因此你对世界的看法是在人生的最初阶段里形成的。


比如在你童年时,大约四到五岁左右,你会试图回答两个问题。




  1. 第一个问题是:我是怎么看自己的?我好吗?




  2. 第二个问题是:我是怎么看别人的?他们好吗?




人生坐标


一旦我们在童年决定用哪种态度和观点,这些态度和观点会变成我们心理的底层架构,在随后的人生里就会始终坚持自己的选择。从那以后,我们便建构出一个世界,不断确认和支持这些信念和预期。换一个词来说,我们把自己的人生变成了一个自证预言


所谓自证预言就是,我们会控制事件的发生,确保自己的世界和预期的一样,从而保证预言会成真。


2.1 我好,你不好


这类人认为我比别人好,表现出自负、偏执,对应于控制型父母自我状态,压制别人,证明自己的优越。


表现为:



  1. 以自我为中心,自以为是;

  2. 喜欢把失败的责任归咎他人;

  3. 固执己见,唯我独尊。


2.2 我不好,你好


这类人认为自己很差劲,别人都比他好,这种想法来源于童年时期的无助感,表现出自卑、依赖、讨好型人格,对应于适应型儿童自我状态。有些低自尊的人认为自己是生活的受害者,爱玩那些受害者游戏,但却善待别人。


遇到问题希望依赖他人解决问题,希望有一个父母、老师这样角色的人直接给出答案。


表现为:



  1. 自卑,易放弃自我或顺从他人;

  2. 喜欢加倍努力去赢得他人赞赏;

  3. 喜欢与父母意识重的人为友。


2.3 我不好,你不好


这类人表现出反社会模式,极端孤独和退缩,常常看不起自己,也看不起别人。


2.4 我好,你好


这种心理模式的人通常非常阳光和健康,以成熟和健康的方式与人交往。


表现为:



  1. 相信他人,能够接纳自己和他人。

  2. 善于发现彼此优点与长处。

  3. 保持积极、乐观、进取的心理状态。


三、心理游戏


人生坐标是一种处世态度,心理游戏是处世行为,当选择了什么样的人生坐标,就会导致你玩什么样的心理游戏。而根据心理游戏,又会导致对应的人生终点。


3.1 受害者游戏


处于悲伤的儿童状态时,会玩一些受害者游戏,把自己置于受虐者的位置。


我真不幸


玩这个游戏的人确信他们是不幸的,会随时报出一长串遭遇过的不幸事件。同时,这些人会竭尽所能地选择记住那些悲伤和不快乐的事件,而忘记或忽略美好的时光,从而让自己的人生更加贴近预想中的人生,让自己更好地扮演一个不幸的角色。


比如有的人会觉得自己非常糟糕,即使真爱来临,也会觉得自己不配,从而主动拒绝美好人生。


可怜弱小的我(PLOM)


可怜弱小的我(PLOM, Poor Littlle Old Me),这种人生活中喜欢用自怜猛烈地攻击自己,总感觉自己能力差,长的丑,事事不如人,处处低人一等。相信自己又弱小,又可怜,简直一无是处。


生活中喜欢做小透明,碰到机遇不会去接,反而第一时间躲开,确保实现自己可怜又弱小这个预言。


当面对对方处于控制型父母状态对自己横加指责和训斥,甚至会偷偷地或无意识地配合对方,来给自己制造不快,从而让在 PLOM 游戏里成为赢家,虽然现实世界里自己是受虐者。


不论做什么都要爱我


相信大家都遇到过那些喜欢挑战人性的人,这其实是不自信的表现。


有的人(男性或女性)会首先预设立场对方不爱我,然后不断突破底线去试探对方,就是想看看别人能宽容他们到什么程度,什么时候会排斥他们。这个过程中慢慢耗尽了爱和热情,直到最后对方忍无可忍离开自己,接着他们就会说:我早说过你会这样对我,证明我是真的很差劲。


这就像先预设杯子会摔碎,然后放到 20 厘米高的地方放下,如果杯子没有被打碎,就提高到 50 厘米、1 米,直到杯子终于被摔碎,然后得意的指着一地碎渣说:你看,我说了杯子会被摔碎吧。


在完成这种逼着爱的人离开自己的自我毁灭行为后,却因为对方的表现证实了自己的预言,这些人甚至有一些得意洋洋,或者说超脱的快感。


3.2 施虐者游戏


这些施虐者游戏中的人利用任何时机来制造一些能让他们审判别人的情境。是他们内心的施虐者让他们这么做,可内心的施虐者是谁呢,这是个值得思考的问题。


玩这类游戏的人常常会寻找弱势群体或者那些容易受到伤害的人,来满足自己的控制欲和优越感,或者至少能让他们占据道德和权威的制高点对别人评头论足。


我抓到你了,你这个坏蛋(NIGYYSOB)


我抓到你了,你这个坏蛋(NIGYYSOB, now I ‘ve got you. you son of bitch) 这种游戏能让愤怒的人找到看似正当的理由来发火,证明别人即无能又缺少道德,从而借此证实“我好,你不好”的人生坐标,接下来,他们就可以理所当然的进行训斥和惩罚。


工作场合不免有人会犯错,这种情况很常见,上司发现之后把犯错的下属叫进来好一顿训斥,小题大做,对下属大声咆哮。这种情况下,占据支配地位的人(如领导、老师、高年级的学生)很容易把自己想象成严厉的父母,或把员工当成顽劣的孩子来惩罚,或体罚所谓不听话的学生,或霸凌低年级的同学。


你为什么总让我失望/你怎么敢


还有些占有支配地位的人会说你竟敢忤逆我!或者你为什么总让我失望,玩这些游戏的人处于挑剔型父母状态,使得别人自卑、自责,从而加强他们的道德优越感,证实自己高人一等,他人一无是处。


猜猜我在想什么


有时候课堂上的老师会对学生玩一个猜猜我在想什么(Guess the word in my head)的游戏,学生因猜不出来自然觉得自己愚蠢,老师赢了无知的学生,从而获得优越感。


还有个典型场景是恋爱中的情侣,想必大家应该很熟悉了吧 😅。


玩这种游戏的人应该直接说出自己的需求,而不是然后要求别人理解自己,还是说,他们根本只是希望利用情绪这个工具直接惩罚别人呢。


四、活得真实


有人说,除了疾病带来的痛苦,所有的痛苦和悲伤都是源于自己的价值观。过去的经历往往会束缚自己的思想,不自觉进入适应型儿童或控制型父母状态,让自己或他人痛苦。


活得真实,就是真诚地面对自己的价值观和需求,打破从童年延续而来的因果循环,让真实的自我摆脱过去经历的束缚,在自由中成为真正的自己。


4.1 摆脱因处于儿童状态而没有主见


有时候,在面临决策时,我们会不自觉地向他人征求建议,放弃自己的主见,把选择的自由交出去。这是因为潜意识里不想承担选择的责任,逃避自由。这样失败发生的时候,我们可以理直气壮的责怪他人。


责怪是人处在儿童自我状态里做的事情,如果你处在成人自我状态,会认识到你对自己是有自主权的,你有力量改变你自己。决策的时候,你应该广开言路,但要允许用自己的方式去尝试,哪怕错了也没关系,从依赖逐步对抗依赖,最终走入了独立的状态。


不要总期望依赖一个智者(老师、家长等)来告诉自己一切的答案,而要逐渐引导自己进入成人状态,当然训练自己进入成人自我状态需要艰辛的努力和刻意的思考。


4.2 成为一个高情商的人


情商真正的意思是理解你内心的情感世界,并且还能掌控它。


高情商的人都拥有强大的自我意识,了解并理解自己的情感。如果你否认自己的情绪,不论是用无视还是压抑的方式,在某种程度上成了一个残缺的人。(虽然很多父母从小就会压抑你显露弱小的一面,比如:不许哭)


他们能管理情绪,能从悲伤和不幸中重新振作。但也许最重要的是,他们能控制冲动,也懂得延迟满足,从而避免轻率的决定和不妥的行为。


最后,把人生的坐标设置为“我好,你也好”,真诚的对待自己,拥抱并接纳自己的情绪,自信的决策并勇敢承担责任。也可以与他人共情,欣赏他人的优点,诚挚的合作,他人取得进步的时候也就可以发自内心的赞赏。


共勉!




PS:本文收录在在下的博客 Github - SHERlocked93/blog 系列文章中,欢迎大家关注我的公众号 前端下午茶,直接搜索即可添加或者点这里添加,持续为大家推送前端以及前端周边相关优质技术文,共同进步,一起加油~



推介阅读:



  1. 蛤蟆先生去看心理医生



另外可以加入「前端下午茶交流qun」微信qun,微信搜索 sherlocked_93 加我好友,备注 1

作者:SHERlocked93
来源:juejin.cn/post/7215185077444886589
>,我拉你入qun~

收起阅读 »

8 款AI 绘画生成器:从文本创建 AI 艺术图像

人工智能正在影响各行各业,近年来它对创意产业的影响越来越大。由于AI绘画生成器的可操作性,许多人有机会用自己的想法进行艺术创作——即使他们没有接受过系统的专业艺术教育。 最先进的人工智能绘画生成器可能会改变我们未来创作艺术的方式。使用 AI 绘画生成软件,您可...
继续阅读 »

人工智能正在影响各行各业,近年来它对创意产业的影响越来越大。由于AI绘画生成器的可操作性,许多人有机会用自己的想法进行艺术创作——即使他们没有接受过系统的专业艺术教育。


最先进的人工智能绘画生成器可能会改变我们未来创作艺术的方式。使用 AI 绘画生成软件,您可以生成肖像、风景和抽象艺术。您甚至可以模仿著名艺术家的风格。


简单说,您可以使用在线 AI 绘画生成器。通过使用在线AI图像生成器,输入文本,就可获得根据您描述而来的逼真的样式图像。


市场上出现了一系列AI绘画生成器,可以尝试一下。本文是对市场上推荐的一些流行的AI绘画生成器的全面回顾。请继续阅读。


1. 福托尔(Fotor)


Fotor,一站式多合一在线照片编辑器,最近发布了一个 精湛的AI图像生成器 。你只需要把你的想法输入到生成器中,然后你可以看到它在几秒钟内变成一个图像。Fotor有多种图像样式供您选择,例如随机,3D,动漫等。


Fotor的AI文本到艺术生成器最显着的特点是它非常适合初学者使用,只需填写文本并选择要生成图像的效果即可。如果您对照片不满意,可以多次重复生成,以确保获得最满意的结果。每个帐户每天都有一个积分可供您免费使用高质量的 AI 艺术


主要特点:



  • 每天生成 10 张免费图片。

  • 9种灯光效果供您选择。

  • 9种不同的转换风格供您选择。

  • 文本到图像和图像到图像的转换模式。

  • 6种作品可供选择。


Fotor AI 绘画生成器


2. 达尔-E 2(DALL-E 2)


AI绘画生成器达勒2


公众已知的最受欢迎的AI绘画生成器是 Dall-E-2图像生成器 ,由OpenAI开发的AI图像生成器。只需几分钟,您就可以使用 AI 技术创建高度逼真的图像。该工具可用于创建插图、设计产品和为业务产生新想法。Dall-E-2 是一个易于使用的界面,任何人都可以使用 AI 创建高质量的图像。DALL-E 2 支持向生成的图像添加详细信息或对其进行其他修改。


主要特点:



  • 高度逼真的图像。

  • 创建插图。

  • 设计产品。

  • 可定制的多层图像。

  • 编辑和修饰功能。

  • 免费试用(尽管您必须通过等候名单获得邀请)。


3. 火锅(Hotpot ai)--支持api


火锅AI绘画生成器


火锅 AI 可帮助您创建令人惊叹的图形、图像和文本。它激发创造力并自动化工作,而易于编辑的模板使任何人都可以创建设备模型、社交媒体帖子、营销图像、应用程序图标和其他工作图形。


火锅AI的文本到图像AI绘画生成器使任何人都可以创建有吸引力的绘画,插图和图像。描述你想要什么,并观看火锅将其变为现实。


付费创作在 3-10 秒内完成。免费请求需要 1-15 分钟,具体取决于流量。付费用户可以获得更快的服务器、更好的图像、商业用途,并避免每日限制。该系统为不太富裕的人免费提供补贴。您还可以免费申请积分以减少等待时间。


主要特点:



  • 无需代码即可创建 API/批量。

  • 快速照片生成(付费)。

  • 每日免费照片生成积分可用。


4. 夜间咖啡厅(NightCafe)


爱画生成器夜咖啡厅


夜咖啡馆是著名的人工智能艺术生成器之一。它以比其他 AI 绘画生成器具有更多的算法和选项而闻名,并且新手很容易上手。您需要做的就是前往他们的网站并根据您的想象力输入文本提示。然后,您需要等待最多 30 秒,一件艺术品才会出现在您面前。Nightcafe有自己的一套积分系统,您可以通过参加各种活动来获得积分,然后拥有可以免费生成图像的次数。此外,您还可以购买积分。


主要特点:



  • 信用赚取系统。

  • 视频生成工具。

  • 有用的社交功能。

  • 获得您生成的艺术品的所有权。

  • 比其他生成器更多的算法


5. 深度人工智能(DeepAI)


深度AI绘画生成器


自 2016 年以来,DeepAI 是首批通过开源软件生成人工智能图像的 AI 绘画生成器之一。


DeepAI允许您创建任意数量的图像,并且每个图像都是唯一的。它是高度可定制的,允许您更改细节、颜色、纹理等的数量。如果您输入插图,DeepAI 可以立即生成与分辨率无关的矢量图像。


这是一个免费的在线AI图像生成器,这意味着您无需下载或进行其他设置。DeepAI还有一个API,开发人员可以使用它来连接到另一个软件项目。


主要特点:



  • 始终免费使用。

  • 为每个提示生成四个输出图像。

  • 开源软件。

  • 更改每个图像的各种细节。

  • 使用卡通 GAN 创建卡通


6. 深度梦境生成器(Deep Dream Generator)


深梦生成器


深度梦境生成器 是另一种流行的 AI 绘画生成器,支持在线人工智能来创建逼真的图像。Deep Dream依赖于用数百万张图像训练的神经网络。它易于使用,只需要您上传图像,然后根据原始图像自动生成新图像,您还可以选择不同地方或时期的绘画风格。


该工具允许您选择一个类别,例如动物或风景,然后基于它创建逼真的图像。最重要的是,Deep Dream允许您从三种样式中进行选择。深风格,薄风格或深梦。选择样式后,可以预览图像。


主要特点:



  • 训练神经网络的数百万张图像。

  • 不同的绘画风格。

  • 图像的分类。

  • 文本到图像,图像到图像。


7. 克雷永(Craiyon)


克雷永爱绘画生成器


Craiyon,以前称为DALL-E mini,是一种人工智能模型,可以从任何文本提示中绘制图像。只需输入文本描述,它将根据您输入的文本生成 9 个不同的图像。该模型需要大量计算,因此Craiyon依靠广告和捐赠来支付其服务器的费用。只要您尊重使用条款,您就可以随意使用它们供个人使用,无论您是想与朋友分享还是将它们打印在 T 恤上。
主要特点:



  • 易于使用。

  • 无需注册或注册。

  • 免费生成无限的AI图像。

  • 一次生成 9 张图像,以有趣和创造性的方式


8. 星空人工智能(StarryAI)


星空AI绘画生成器


星空 AI 是一个 AI 绘画生成器,专注于将您的想法转化为 NFT 艺术。与大多数其他AI艺术生成器类似,Starry AI赋予您生成图像的所有权。这意味着您可以在任何地方使用图像,用于个人或商业用途。


Starry AI最好的部分是它是完全免费的。它是最好的免费 AI NFT 艺术生成器之一。它不需要用户的任何输入。它可以使用机器学习算法处理图像。该技术在不断改进,但已经有令人难以置信的使用该应用程序创作的艺术示例。


主要特点:



  • 自动图像生成器。

  • 无需用户输入。

  • 免费的 NFT 生成器。

  • 文本到图像


结论


这是人工智能的时期。使用 AI 图像生成器的强大功能自己创作令人惊叹的艺术品。自动将您的想象力变成绘画。AI图像生成器是未来。


在本文中,我们简要介绍了市场上顶级的AI绘画生成器,并推荐了八种最好的AI绘画生成器供您尝试。希望本指南对您有所帮助,谢谢!





作者:非优秀程序员
来源:juejin.cn/post/7214164344290951205
收起阅读 »

SpringBoot 项目使用 Sa-Token 完成登录认证

一、设计思路 对于一些登录之后才能访问的接口(例如:查询我的账号资料),我们通常的做法是增加一层接口校验: 如果校验通过,则:正常返回数据。 如果校验未通过,则:抛出异常,告知其需要先进行登录。 那么,判断会话是否登录的依据是什么?我们先来简单分析一下登录...
继续阅读 »

一、设计思路


对于一些登录之后才能访问的接口(例如:查询我的账号资料),我们通常的做法是增加一层接口校验:



  • 如果校验通过,则:正常返回数据。

  • 如果校验未通过,则:抛出异常,告知其需要先进行登录。


那么,判断会话是否登录的依据是什么?我们先来简单分析一下登录访问流程:



  1. 用户提交 name + password 参数,调用登录接口。

  2. 登录成功,返回这个用户的 Token 会话凭证。

  3. 用户后续的每次请求,都携带上这个 Token。

  4. 服务器根据 Token 判断此会话是否登录成功。


所谓登录认证,指的就是服务器校验账号密码,为用户颁发 Token 会话凭证的过程,这个 Token 也是我们后续判断会话是否登录的关键所在。


动态图演示:


登录认证


接下来,我们将介绍在 SpringBoot 中如何使用 Sa-Token 完成登录认证操作。



Sa-Token 是一个 java 权限认证框架,主要解决登录认证、权限认证、单点登录、OAuth2、微服务网关鉴权 等一系列权限相关问题。
Gitee 开源地址:gitee.com/dromara/sa-…



首先在项目中引入 Sa-Token 依赖:


<!-- Sa-Token 权限认证 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.34.0</version>
</dependency>

注:如果你使用的是 SpringBoot 3.x,只需要将 sa-token-spring-boot-starter 修改为 sa-token-spring-boot3-starter 即可。


二、登录与注销


根据以上思路,我们需要一个会话登录的函数:


// 会话登录:参数填写要登录的账号id,建议的数据类型:long | int | String, 不可以传入复杂类型,如:User、Admin 等等
StpUtil.login(Object id);

只此一句代码,便可以使会话登录成功,实际上,Sa-Token 在背后做了大量的工作,包括但不限于:



  1. 检查此账号是否之前已有登录

  2. 为账号生成 Token 凭证与 Session 会话

  3. 通知全局侦听器,xx 账号登录成功

  4. Token 注入到请求上下文

  5. 等等其它工作……


你暂时不需要完整的了解整个登录过程,你只需要记住关键一点:Sa-Token 为这个账号创建了一个Token凭证,且通过 Cookie 上下文返回给了前端


所以一般情况下,我们的登录接口代码,会大致类似如下:


// 会话登录接口 
@RequestMapping("doLogin")
public SaResult doLogin(String name, String pwd) {
// 第一步:比对前端提交的账号名称、密码
if("zhang".equals(name) && "123456".equals(pwd)) {
// 第二步:根据账号id,进行登录
StpUtil.login(10001);
return SaResult.ok("登录成功");
}
return SaResult.error("登录失败");
}

如果你对以上代码阅读没有压力,你可能会注意到略显奇怪的一点:此处仅仅做了会话登录,但并没有主动向前端返回 Token 信息。
是因为不需要吗?严格来讲是需要的,只不过 StpUtil.login(id) 方法利用了 Cookie 自动注入的特性,省略了你手写返回 Token 的代码。


如果你对 Cookie 功能还不太了解,也不用担心,我们会在之后的 [ 前后端分离 ] 章节中详细的阐述 Cookie 功能,现在你只需要了解最基本的两点:



  • Cookie 可以从后端控制往浏览器中写入 Token 值。

  • Cookie 会在前端每次发起请求时自动提交 Token 值。


因此,在 Cookie 功能的加持下,我们可以仅靠 StpUtil.login(id) 一句代码就完成登录认证。


除了登录方法,我们还需要:


// 当前会话注销登录
StpUtil.logout();

// 获取当前会话是否已经登录,返回true=已登录,false=未登录
StpUtil.isLogin();

// 检验当前会话是否已经登录, 如果未登录,则抛出异常:`NotLoginException`
StpUtil.checkLogin();

异常 NotLoginException 代表当前会话暂未登录,可能的原因有很多:
前端没有提交 Token、前端提交的 Token 是无效的、前端提交的 Token 已经过期 …… 等等。


Sa-Token 未登录场景值参照表:


场景值对应常量含义说明
-1NotLoginException.NOT_TOKEN未能从请求中读取到 Token
-2NotLoginException.INVALID_TOKEN已读取到 Token,但是 Token无效
-3NotLoginException.TOKEN_TIMEOUT已读取到 Token,但是 Token已经过期
-4NotLoginException.BE_REPLACED已读取到 Token,但是 Token 已被顶下线
-5NotLoginException.KICK_OUT已读取到 Token,但是 Token 已被踢下线

那么,如何获取场景值呢?废话少说直接上代码:


// 全局异常拦截(拦截项目中的NotLoginException异常)
@ExceptionHandler(NotLoginException.class)
public SaResult handlerNotLoginException(NotLoginException nle)
throws Exception {

// 打印堆栈,以供调试
nle.printStackTrace();

// 判断场景值,定制化异常信息
String message = "";
if(nle.getType().equals(NotLoginException.NOT_TOKEN)) {
message = "未提供token";
}
else if(nle.getType().equals(NotLoginException.INVALID_TOKEN)) {
message = "token无效";
}
else if(nle.getType().equals(NotLoginException.TOKEN_TIMEOUT)) {
message = "token已过期";
}
else if(nle.getType().equals(NotLoginException.BE_REPLACED)) {
message = "token已被顶下线";
}
else if(nle.getType().equals(NotLoginException.KICK_OUT)) {
message = "token已被踢下线";
}
else {
message = "当前会话未登录";
}

// 返回给前端
return SaResult.error(message);
}



注意:以上代码并非处理逻辑的最佳方式,只为以最简单的代码演示出场景值的获取与应用,大家可以根据自己的项目需求来定制化处理

三、会话查询


// 获取当前会话账号id, 如果未登录,则抛出异常:`NotLoginException`
StpUtil.getLoginId();

// 类似查询API还有:
StpUtil.getLoginIdAsString(); // 获取当前会话账号id, 并转化为`String`类型
StpUtil.getLoginIdAsInt(); // 获取当前会话账号id, 并转化为`int`类型
StpUtil.getLoginIdAsLong(); // 获取当前会话账号id, 并转化为`long`类型

// ---------- 指定未登录情形下返回的默认值 ----------

// 获取当前会话账号id, 如果未登录,则返回null
StpUtil.getLoginIdDefaultNull();

// 获取当前会话账号id, 如果未登录,则返回默认值 (`defaultValue`可以为任意类型)
StpUtil.getLoginId(T defaultValue);

四、Token 查询


// 获取当前会话的token值
StpUtil.getTokenValue();

// 获取当前`StpLogic`的token名称
StpUtil.getTokenName();

// 获取指定token对应的账号id,如果未登录,则返回 null
StpUtil.getLoginIdByToken(String tokenValue);

// 获取当前会话剩余有效期(单位:s,返回-1代表永久有效)
StpUtil.getTokenTimeout();

// 获取当前会话的token信息参数
StpUtil.getTokenInfo();

TokenInfo 是 Token 信息 Model,用来描述一个 Token 的常用参数:


{
"tokenName": "satoken", // token名称
"tokenValue": "e67b99f1-3d7a-4a8d-bb2f-e888a0805633", // token值
"isLogin": true, // 此token是否已经登录
"loginId": "10001", // 此token对应的LoginId,未登录时为null
"loginType": "login", // 账号类型标识
"tokenTimeout": 2591977, // token剩余有效期 (单位: 秒)
"sessionTimeout": 2591977, // User-Session剩余有效时间 (单位: 秒)
"tokenSessionTimeout": -2, // Token-Session剩余有效时间 (单位: 秒) (-2表示系统中不存在这个缓存)
"tokenActivityTimeout": -1, // token剩余无操作有效时间 (单位: 秒)
"loginDevice": "default-device" // 登录设备类型
}

五、来个小测试,加深一下理解


新建 LoginAuthController,复制以下代码


package com.pj.cases.use;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import cn.dev33.satoken.stp.SaTokenInfo;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;

/**
* Sa-Token 登录认证示例
*
* @author kong
* @since 2022-10-13
*/

@RestController
@RequestMapping("/acc/")
public class LoginAuthController {

// 会话登录接口 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456
@RequestMapping("doLogin")
public SaResult doLogin(String name, String pwd) {

// 第一步:比对前端提交的 账号名称 & 密码 是否正确,比对成功后开始登录
// 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对
if("zhang".equals(name) && "123456".equals(pwd)) {

// 第二步:根据账号id,进行登录
// 此处填入的参数应该保持用户表唯一,比如用户id,不可以直接填入整个 User 对象
StpUtil.login(10001);

// SaResult 是 Sa-Token 中对返回结果的简单封装,下面的示例将不再赘述
return SaResult.ok("登录成功");
}

return SaResult.error("登录失败");
}

// 查询当前登录状态 ---- http://localhost:8081/acc/isLogin
@RequestMapping("isLogin")
public SaResult isLogin() {
// StpUtil.isLogin() 查询当前客户端是否登录,返回 true 或 false
boolean isLogin = StpUtil.isLogin();
return SaResult.ok("当前客户端是否登录:" + isLogin);
}

// 校验当前登录状态 ---- http://localhost:8081/acc/checkLogin
@RequestMapping("checkLogin")
public SaResult checkLogin() {
// 检验当前会话是否已经登录, 如果未登录,则抛出异常:`NotLoginException`
StpUtil.checkLogin();

// 抛出异常后,代码将走入全局异常处理(GlobalException.java),如果没有抛出异常,则代表通过了登录校验,返回下面信息
return SaResult.ok("校验登录成功,这行字符串是只有登录后才会返回的信息");
}

// 获取当前登录的账号是谁 ---- http://localhost:8081/acc/getLoginId
@RequestMapping("getLoginId")
public SaResult getLoginId() {
// 需要注意的是,StpUtil.getLoginId() 自带登录校验效果
// 也就是说如果在未登录的情况下调用这句代码,框架就会抛出 `NotLoginException` 异常,效果和 StpUtil.checkLogin() 是一样的
Object userId = StpUtil.getLoginId();
System.out.println("当前登录的账号id是:" + userId);

// 如果不希望 StpUtil.getLoginId() 触发登录校验效果,可以填入一个默认值
// 如果会话未登录,则返回这个默认值,如果会话已登录,将正常返回登录的账号id
Object userId2 = StpUtil.getLoginId(0);
System.out.println("当前登录的账号id是:" + userId2);

// 或者使其在未登录的时候返回 null
Object userId3 = StpUtil.getLoginIdDefaultNull();
System.out.println("当前登录的账号id是:" + userId3);

// 类型转换:
// StpUtil.getLoginId() 返回的是 Object 类型,你可以使用以下方法指定其返回的类型
int userId4 = StpUtil.getLoginIdAsInt(); // 将返回值转换为 int 类型
long userId5 = StpUtil.getLoginIdAsLong(); // 将返回值转换为 long 类型
String userId6 = StpUtil.getLoginIdAsString(); // 将返回值转换为 String 类型

// 疑问:数据基本类型不是有八个吗,为什么只封装以上三种类型的转换?
// 因为大多数项目都是拿 int、long 或 String 声明 UserId 的类型的,实在没见过哪个项目用 double、float、boolean 之类来声明 UserId
System.out.println("当前登录的账号id是:" + userId4 + " --- " + userId5 + " --- " + userId6);

// 返回给前端
return SaResult.ok("当前客户端登录的账号id是:" + userId);
}

// 查询 Token 信息 ---- http://localhost:8081/acc/tokenInfo
@RequestMapping("tokenInfo")
public SaResult tokenInfo() {
// TokenName 是 Token 名称的意思,此值也决定了前端提交 Token 时应该使用的参数名称
String tokenName = StpUtil.getTokenName();
System.out.println("前端提交 Token 时应该使用的参数名称:" + tokenName);

// 使用 StpUtil.getTokenValue() 获取前端提交的 Token 值
// 框架默认前端可以从以下三个途径中提交 Token:
// Cookie (浏览器自动提交)
// Header头 (代码手动提交)
// Query 参数 (代码手动提交) 例如: /user/getInfo?satoken=xxxx-xxxx-xxxx-xxxx
// 读取顺序为: Query 参数 --> Header头 -- > Cookie
// 以上三个地方都读取不到 Token 信息的话,则视为前端没有提交 Token
String tokenValue = StpUtil.getTokenValue();
System.out.println("前端提交的Token值为:" + tokenValue);

// TokenInfo 包含了此 Token 的大多数信息
SaTokenInfo info = StpUtil.getTokenInfo();
System.out.println("Token 名称:" + info.getTokenName());
System.out.println("Token 值:" + info.getTokenValue());
System.out.println("当前是否登录:" + info.getIsLogin());
System.out.println("当前登录的账号id:" + info.getLoginId());
System.out.println("当前登录账号的类型:" + info.getLoginType());
System.out.println("当前登录客户端的设备类型:" + info.getLoginDevice());
System.out.println("当前 Token 的剩余有效期:" + info.getTokenTimeout()); // 单位:秒,-1代表永久有效,-2代表值不存在
System.out.println("当前 Token 的剩余临时有效期:" + info.getTokenActivityTimeout()); // 单位:秒,-1代表永久有效,-2代表值不存在
System.out.println("当前 User-Session 的剩余有效期" + info.getSessionTimeout()); // 单位:秒,-1代表永久有效,-2代表值不存在
System.out.println("当前 Token-Session 的剩余有效期" + info.getTokenSessionTimeout()); // 单位:秒,-1代表永久有效,-2代表值不存在

// 返回给前端
return SaResult.data(StpUtil.getTokenInfo());
}

// 会话注销 ---- http://localhost:8081/acc/logout
@RequestMapping("logout")
public SaResult logout() {
// 退出登录会清除三个地方的数据:
// 1、Redis中保存的 Token 信息
// 2、当前请求上下文中保存的 Token 信息
// 3、Cookie 中保存的 Token 信息(如果未使用Cookie模式则不会清除)
StpUtil.logout();

// StpUtil.logout() 在未登录时也是可以调用成功的,
// 也就是说,无论客户端有没有登录,执行完 StpUtil.logout() 后,都会处于未登录状态
System.out.println("当前是否处于登录状态:" + StpUtil.isLogin());

// 返回给前端
return SaResult.ok("退出登录成功");
}

}

代码注释已针对每一步操作做出详细解释,大家可根据可参照注释中的访问链接进行逐步测试。


本示例代码已上传至 Gitee,可参考:
Sa-Token 登录认证示例




参考资料



作者:省长
来源:juejin.cn/post/7215971680349569061
收起阅读 »

给轮播图做一个自适应的高度。

web
不知道大家有没有遇到这样的需求或者说看到类似的效果,就是列表进去详情看轮播图的时候,当手指滚动轮播图时轮播的高度容器会自适应,这样下面的内容就向上挤,滑动的过程会计算高度,释放的时候也会滚到下一张,也会计算对应图片的高度,然后做一个缓动的动画效果。就像下面这张...
继续阅读 »

不知道大家有没有遇到这样的需求或者说看到类似的效果,就是列表进去详情看轮播图的时候,当手指滚动轮播图时轮播的高度容器会自适应,这样下面的内容就向上挤,滑动的过程会计算高度,释放的时候也会滚到下一张,也会计算对应图片的高度,然后做一个缓动的动画效果。就像下面这张图的样子。


1.gif


可以看到上面的图片内容文字,随着轮播的滑动高度也在变化。费话不多说直接上代码。


实现方法


可以通过监听鼠标mounse 或者手指的滑动 touch 事件来控制图片,这里本文只说一下轮播的功能实现思路,重点说的是怎么实现高度的自适应。


直接开始正文,先看 html 代码结构。


html 结构


<div class="container">
 <div class="wrapper">
   <div class="swiper">
     <div class="item">
       <img src="https://ci.xiaohongshu.com/776d1cc7-ff36-5881-ad8f-12a5cd1c3ab3?imageView2/2/w/1080/format/jpg" alt="">
     </div>
     <div class="item">
       <img src="https://ci.xiaohongshu.com/b8e16620-66a0-79a5-8a4b-5bfee1028554?imageView2/2/w/1080/format/jpg" alt="">
     </div>
     <div class="item">
       <img src="https://ci.xiaohongshu.com/e12013c2-3c46-a2cc-7fda-1e0b20b36f3d?imageView2/2/w/1080/format/jpg" alt="">
     </div>
   </div>
 </div>
 <div class="content">这是一段内容</div>
</div>

css 样式


.container {
 width: 100%;
 overflow: hidden;
}
.wrapper {
 width: 100%;
}
.swiper {
 font-size: 0;
 white-space: nowrap;
}
.item {
 display: inline-block;
 width: 100%;
 vertical-align: top; // 一定要使用顶部对齐,不然会出现错位的情况
}
.item img {
 width: 100%;
 height: auto;
 display: block;
}
.content {
 position: relative;
 z-index: 9;
 font-size: 14px;
 text-align: center;
 padding-top: 20px;
 background-color: #fff;
 height: 200px;
}

值得注意的地方有几点;



  1. 在使用父级 white-space 时,子集元素设置 display: inline-block 会出现高度不同的排列错位,解决办法就是加上一句 vertical-align: top ,具体什么原因我也不细讲了。

  2. 另外父级还要设置 font-size: 0 ,如果没加上的话,就会出现两个子集有空隙出现,加上之后空隙就会去掉。

  3. img 图片最好设置成高度自适应,宽度100% 还要加上 display: block ,没有的话底部就会出现间隙。


写好上面的 html容器部分和 样式,下面就看一下 js 上是怎么处理的。


Js 实现


开始之前我们先思考一下去怎么实现这个轮播以及高度的自适应问题,分为几步操作;



  1. 鼠标按下时,需要记录当前的位置和一些其他初始化的信息,并且给当前的父元素添加相应的鼠标事件。

  2. 鼠标移动时,需要通过当前实时移动时点位和按下时点位的相减,得到移动的距离位置,然后再赋值给父元素设置其样式 transform 位置,中间还做其他的边界处理,当然还有高度的变化。

  3. 鼠标释放是,通过移动时记录的距离信息判断是左滑还是右滑,拿到其对应的索引,通过索引就可以计算到滚动下一张的距离,释放之后设置 transition 过渡动画即可。


按照我们试想的思路,开始正文;


初始化数据


const data = {
 ele: null,
 width: 0,
 len: 0,
 proportion: .3,
 type: false,
 heights: [500, 250, 375],
 currentIndex: 0,
 startOffset: 0,
 clientX: 0,
 distanceX: 0,
 duration: 30,
 touching: false
}

const wrapper = data.ele = document.querySelector('.wrapper')
const items = document.querySelectorAll('.item')
data.width = wrapper.offsetWidth
data.len = items.length - 1
wrapper.addEventListener('touchstart', onStart)
wrapper.addEventListener('mousedown', onStart)

注意,这里在做高度之前,我们需要等图片加载完成之后才能拿到每一个元素的高度,我这里为了省懒就没写具体代码,上面的 heights 对应的是每个图片在渲染之后的高度,一般情况下最好让后端传回来带宽高,这样就不需要用 onload 再去处理这个。


鼠标按下时


function onStart(event) {
 if (event.type === 'mousedown' && event.which !== 1) return
 if (event.type === 'touchstart' && event.touches.length > 1) return
 data.type = event.type === 'touchstart'
 const events = data.type ? event.touches[0] || event : event

 data.touching = true
 data.clientX = events.clientX
 data.startOffset = data.currentIndex * -data.width

 data.ele.style.transition = `none`
 window.addEventListener(data.type ? 'touchmove' : 'mousemove', onMove, { passive: false })
 window.addEventListener(data.type ? 'touchend' : 'mouseup', onEnd, false)
}

上面的代码里面我做了PC和移动端的兼容,跟计划的一样,保存一下 clientX 坐标和一个初始的坐标 startOffset 这个由当前索引和父级宽度计算得到,场景是当从第二张图片滚动到第三张图片时,会把之前的第一张图片的距离也要加上去,不然就计算错误,看下面滑动时的代码。


另外在做监听移动的时候加上了 passive: false 是为了在移动端兼容处理。


鼠标移动时


function onMove(event) {
 event.preventDefault()
 if (!data.touching) return
 const events = data.type ? event.touches[0] || event : event

 data.distanceX = events.clientX - data.clientX

 let translatex = data.startOffset + data.distanceX
 if (translatex > 0) {
   translatex = translatex > 30 ? 30 : translatex
} else {
   const d = -(data.len * data.width + 30)
   translatex = translatex < d ? d : translatex
}

 data.ele.style.transform = `translate3d(${translatex}px, 0, 0)`
 data.ele.style.webkitTransform = `translate3d(${translatex}px, 0, 0)`
}

做了一个边界处理的,超了 30 的距离就不让继续滑动了,加上之前保存的 startOffset 的值,得到的就是具体移动的距离了。


鼠标释放时


function onEnd() {
 if (!data.touching) return
 data.touching = false

 // 通过计算 proportion 滑动的阈值拿到释放后的索引
 if (Math.abs(data.distanceX) > data.width * data.proportion) {
   data.currentIndex -= data.distanceX / Math.abs(data.distanceX)
}
 if (data.currentIndex < 0) {
   data.currentIndex = 0
} else if (data.currentIndex > data.len) {
   data.currentIndex = data.len
}
 const translatex = data.currentIndex * -data.width

 data.ele.style.transition = 'all .3s ease'
 data.ele.style.transform = `translate3d(${translatex}px, 0, 0)`
 data.ele.style.webkitTransform = `translate3d(${translatex}px, 0, 0)`

 window.removeEventListener(data.type ? 'touchmove' : 'mousemove', onMove, { passive: false })
 window.removeEventListener(data.type ? 'touchend' : 'mouseup', onEnd, false)
}

通过计算 proportion 滑动的阈值拿到释放后的索引,也就是超过父级宽度的三分之一时释放就会滚动到下一张,拿到索引之后就可以设置需要移动的最终距离,记得加上 transition 做一个缓动效果,最后也别忘记移除事件的监听。


至此上面的简单的轮播效果就大功告成了,但是还缺少一点东西,就是本篇需要讲的自适应高度,为了方便理解就单独拿出来说一下。


高度自适应


在移动时就可以在里面做相关的代码整理了, onMove 函数里加上以下代码,来获取实时的高度。


const index = data.currentIndex
const currentHeight = data.heights[index]
   
// 判断手指滑动的方向拿到下一张图片的高度
let nextHeight = data.distanceX > 0 ? data.heights[index - 1] : data.heights[index + 1]
let diffHeight = Math.abs((nextHeight - currentHeight) * (data.distanceX / data.width))
let realHeight = currentHeight + (nextHeight - currentHeight > 0 ? diffHeight : -diffHeight)

data.ele.style.height = `${realHeight}px`

这里是移动时的高度变化,另外还需要在释放时也要处理, onEnd 函数里加上以下代码。


// ... 因为上面已经拿到了下一张的索引 currentIndex
const currentHeight = data.heights[data.currentIndex]

data.ele.style.height = `${currentHeight}px`

因为上面已经拿到了下一张的索引 currentIndex 所以再滚动到下一张是就直接通过数据获取就可以了。


可以在线预览一下效果。


作者:ZHOUYUANN
来源:juejin.cn/post/7213654163317162045
收起阅读 »

抓包神器 Charles 使用教程(含破解)支持mac ios Android

本文以Mac 系统为例进行讲解 配置手机代理: 手机和 Mac 连接到同一个 WiFi 网络 1.1 Android 系统:「以华为 P20 手机为例」 设置 -> 无线和网络 -> WLAN 长按当前 WiFi -> 修改网络 勾选显...
继续阅读 »

本文以Mac 系统为例进行讲解



  • 配置手机代理:


手机和 Mac 连接到同一个 WiFi 网络


1.1 Android 系统:「以华为 P20 手机为例」



  • 设置 -> 无线和网络 -> WLAN

  • 长按当前 WiFi -> 修改网络

  • 勾选显示高级选项

  • 代理 -> 手动

  • 服务器主机名 -> 填写 Mac 的IP 地址「Mac IP 获取方法:Charles -> Help -> Local IP Address 」

  • 服务器端口 -> 8888

  • 保存


1.2 IOS 系统:「以 iPhone Xs Max 手机为例」



  • 设置 -> 无线局域网

  • 点击当前连接的 WiFi

  • 最底部 HTTP 代理 -> 配置代理 -> 勾选手动

  • 服务器 -> 填写 Mac 的IP 地址「Mac IP 获取方法:Charles -> Help -> Local IP Address 」

  • 端口 -> 8888

  • 存储


核心功能


一、  抓包「以 iPhone Xs Max 为例」



  1. Charles 设置



  • Proxy -> Proxy Settings -> Port -> 8888

  • 勾选 Support HTTP/2

  • 勾选 Enable transparent HTTP proxying

  • OK




  1. 手机设置代理如上「配置手机代理」步骤




  2. 打开手机上任意联网的应用,Charles 会弹出请求连接的确认菜单,点击“Allow“即可完成设置




二、  过滤网络请求




  1. 左侧底部 Filter 栏 -> 过滤关键字




  2. 在 Charles 的菜单栏选择




Proxy -> Recording Settings -> Include -> add「依次填入协议+主机名+端口号,即可只抓取目标网站的包」



  1. 切换到 Sequence,在想过滤的网络请求上右击,选择“Focus“,在 Filter 栏勾选上 Focused


三、  分析 HTTPS 



  1. Mac 安装证书:


Help -> SSL Proxying -> Install Charles Root Certificate -> 输入系统的帐号密码,即可在钥匙串中看到添加好的证书


image.png


如果遇到证书不被信任的问题,解决办法:


Mac本顶栏 前往 -> 实用工具 -> 打开钥匙串访问 -> 找到该证书 -> 双击或右键「显示简介」-> 点开「信任」-> 选择「始终信任」




  1. Charles 设置请求允许 SSL proxying




  2. Charles 默认并不抓取 HTTPS 网络通讯的数据,若想拦截所有 HTTPS 网络请求,需要进行设置:在请求上右击选择 Enable SSL proxying




image.png
2. Charles -> Proxy -> SSL Proxying Settings -> SSL Proxying「添加对应的域名和端口号,为方便也可端口号直接添加通配符*」



  1. 移动端安装证书


a. Charles 选择 Help -> SSL Proxying -> Install Charles Root Certificate on a Mobile Device or Remote Browser


b. 确保手机连上代理的情况下,在手机浏览器栏输入:chls.pro/ssl,下载证书,完成安装。


Android tips


1.1. 用自带浏览器下载证书,自带浏览器下载的证书提示文件格式不对,无法安装,可以尝试用uc浏览器下载后更改后缀为.crt后直接打开安装.(如果提示type the password for credenttial storage,需要给手机设置开机密码重启后再安装)


1.2. 若不能直接安装,需要下载下来,到手机设置 -> 安全 -> 从设备存储空间安装 -> 找到下载的证书 .pem 结尾的 -> 点击安装即可


IOS tips


IOS 需要设置手机信任证书,详见 官方文档。若不能直接安装,需在手机「设置」-> 通用 -> 描述文件与设备管理安装下载的证书,完成安装后 -> 找到关于本机 -> 证书信任设置,打开刚安装的证书的开关。


charles 安装&破解:


Charles的安装
官网最新的版本:http://www.charlesproxy.com/download/
Charles的注册
1.找到这个注册官网 :http://www.zzzmode.com/mytools/cha…
2.自定义"RegisterName",点击生成,复制key值
3.Charles->help->Registered.. 填写RegisterName值和复制的key值即可


抓包内容遇到乱码,解决如下:



  • Proxy -> SSL Proxy Settings -> Add

  • Host:*「代表所有网站都拦截」

  • Port:443

  • 保存后,在抓包数据就会显示正常


四、  模拟弱网




  1. 选择 Proxy -> Throttle Settings -> 勾选 Enable Throttling -> 选择 Throttle Preset 类型
    image.png
    五、  Mock 数据




  2. 以 map local 为例,修改返回值




选择目标请求,右键选择 Save All保存请求的 response 内容到本地文件



  1. 配置 Charles Map Local,Tool -> Map Local -> 勾选 Enable Map Local -> Add 「添加目标请求及需要替换的response 文件地址」-> OK


image.png



  1. 用文本编辑器打开保存的 json 文件,修改内容,进行替换。打开客户端应用重新请求该接口,返回的数据就是本地的文件数据。




作者:CodeCiCi
来源:juejin.cn/post/7215105725387374650
收起阅读 »

简析无感知刷新Token

web
在前后端分离的应用中,使用Token进行认证是一种较为常见的方式。但是,由于Token的有效期限制,需要不断刷新Token,否则会导致用户认证失败。为了解决这个问题,可以实现无感知刷新Token的功能,本文将介绍如何实现无感知刷新Token。 Token认证的...
继续阅读 »

在前后端分离的应用中,使用Token进行认证是一种较为常见的方式。但是,由于Token的有效期限制,需要不断刷新Token,否则会导致用户认证失败。为了解决这个问题,可以实现无感知刷新Token的功能,本文将介绍如何实现无感知刷新Token。


Token认证的原理


在Web应用中,常见的Token认证方式有基于Cookie和基于Token的认证。基于Cookie的认证方式是将认证信息保存在Cookie中,每次请求时将Cookie发送给服务器进行认证;而基于Token的认证方式是将认证信息保存在Token中,每次请求时将Token发送给服务器进行认证。


在基于Token的认证方式中,客户端将认证信息保存在Token中,而不是保存在Cookie中。在认证成功后,服务器将生成一个Access Token和一个Refresh Token,并将它们返回给客户端。Access Token用于访问受保护的API,Refresh Token用于获取新的Access Token。


什么是无感知刷新Token


无感知刷新Token是指,在Token过期之前,系统自动使用Refresh Token获取新的Access Token,从而实现Token的无感知刷新,用户可以无缝继续使用应用。


在实现无感知刷新Token的过程中,需要考虑以下几个方面:



  • 如何判断Token是否过期?

  • 如何在Token过期时自动使用Refresh Token获取新的Access Token?

  • 如何处理Refresh Token的安全问题?


下面将介绍如何实现无感知刷新Token的具体步骤。


实现步骤


步骤一:获取Access Token和Refresh Token


在认证成功后,需要将Access Token和Refresh Token发送给客户端。Access Token用于访问受保护的API,Refresh Token用于获取新的Access Token。可以使用JWT(JSON Web Token)或OAuth2(开放授权)等方式实现认证。


在JWT中,可以使用如下代码生成Access Token和Refresh Token:


const accessToken = jwt.sign({userId: '123'}, 'ACCESS_TOKEN_SECRET', {expiresIn: '15m'});
const refreshToken = jwt.sign({userId: '123'}, 'REFRESH_TOKEN_SECRET', {expiresIn: '7d'});

步骤二:在请求中携带Access Token


在每个需要认证的API请求中,需要在请求头中携带Access Token,如下所示:


GET /api/user HTTP/1.1
Host: example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

在前端中,可以使用Axios等库设置请求头:


axios.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`;

步骤三:拦截401 Unauthorized响应


在服务器返回401 Unauthorized响应时,说明Access Token已经过期,需要使用Refresh Token获取新的Access Token。可以使用Axios拦截器或Fetch API的中间件实现拦截。


在Axios中,可以使用如下代码实现拦截器:


axios.interceptors.response.use(response => {
return response;
}, error => {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true; //防止无限调用
return axios.post('/api/refresh_token', {refreshToken})
.then(response => {
const { access_token, refresh_token } = response.data;
localStorage.setItem('access_token', access_token);
localStorage.setItem('refresh_token', refresh_token);
axios.defaults.headers.common['Authorization'] = `Bearer ${access_token}`;
originalRequest.headers.Authorization = `Bearer ${access_token}`;
return axios(originalRequest);
});
}
return Promise.reject(error);
});

在Fetch中,可以使用如下代码实现中间件:


function authMiddleware(request) {
const access_token = localStorage.getItem('access_token');
if (access_token) {
request.headers.set('Authorization', `Bearer ${access_token}`);
}
return request;
}

function tokenRefreshMiddleware(response) {
if (response.status === 401) {
const refreshToken = localStorage.getItem('refresh_token');
return fetch('/api/refresh_token', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ refreshToken })
}).then(response => {
if (response.ok) {
return response.json();
}
throw new Error('Refresh Token failed');
}).then(data => {
localStorage.setItem('access_token', data.access_token);
localStorage.setItem('refresh_token', data.refresh_token);
return Promise.resolve('refreshed');
}).catch(error => {
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
return Promise.reject(error);
});
}
return Promise.resolve('ok');
}

fetch('/api/user', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
middleware: [authMiddleware, tokenRefreshMiddleware]
}).then(response => {
console.log(response);
}).catch(error => {
console.error(error);
});

在上述代码中,使用Axios或Fetch拦截器拦截401 Unauthorized响应,如果发现Access Token已经过期,则发送Refresh Token请求获取新的Access Token,并将新的Access Token设置到请求头中,重新发送请求。


步骤四:服务器处理Refresh Token请求


在服务器端,需要编写API处理Refresh Token请求,生成新的Access Token,并返回给客户端。


JWT中,可以使用如下代码生成新的Access Token:


const accessToken = jwt.sign({userId: '123'}, 'ACCESS_TOKEN_SECRET', {expiresIn: '15m'});

在刷新Token时,需要验证Refresh Token的合法性,可以使用如下代码验证Refresh Token:


try {
const payload = jwt.verify(refreshToken, 'REFRESH_TOKEN_SECRET');
const accessToken = jwt.sign({userId: payload.userId}, 'ACCESS_TOKEN_SECRET', {expiresIn: '15m'});
const refreshToken = jwt.sign({userId: payload.userId}, 'REFRESH_TOKEN_SECRET', {expiresIn: '7d'});
res.json({access_token: accessToken, refresh_token: refreshToken});
} catch (err) {
res.sendStatus(401);
}

在上述代码中,使用JWT的verify方法验证Refresh Token的合法性,如果验证成功,则生成新的Access Token和Refresh Token,并返回给客户端。


步骤五:设置定时刷新Token


为了避免Access Token过期时间太长,可以设置定时刷新Token的功能。可以使用定时器或Web Workers等方式实现定时刷新Token。在每次刷新Token时,需要重新获取新的Access Token和Refresh Token,并保存到客户端。


function refreshToken() {
const refreshToken = localStorage.getItem('refresh_token');
axios.post('/api/refresh_token', {refreshToken})
.then(response => {
const { access_token, refresh_token } = response.data;
localStorage.setItem('access_token', access_token);
localStorage.setItem('refresh_token', refresh_token);
axios.defaults.headers.common['Authorization'] = `Bearer ${access_token}`;
})
.catch(error => {
console.error(error);
});
}

setInterval(refreshToken, 14 * 60 * 1000); // 每14分钟刷新Token

在上述代码中,使用定时器每14分钟刷新Token。在刷新Token成功后,将新的Access Token和Refresh Token保存到客户端,并将新的Access Token设置到请求头中。


安全性考虑


在实现无感知刷新Token的过程中,需要考虑到Refresh Token的安全性问题。因为Refresh Token具有长期的有效期限,一旦Refresh Token被泄露,攻击者就可以使用Refresh Token获取新的Access Token,从而绕过认证机制,访问受保护的API。


为了增加Refresh Token的安全性,可以考虑以下几种措施:



  • 将Refresh Token保存在HttpOnly Cookie中,可以避免在客户端被JavaScript获取;

  • 对Refresh Token进行加密或签名,可以增加其安
    作者:XinD
    来源:juejin.cn/post/7215569601161150522
    全性。

收起阅读 »

震惊!这个基于GPT-4的代码编辑器让我感到恐慌!

一 首先,我不是标题党。我确确实实受到了震撼。 其次,我今天要写的也不是在chatGPT里面叫AI写什么冒泡排序,鸡兔同笼等网上都已有大量代码示例的问题。 我知道chatGPT已经火出圈了,本人也试验过叫AI写一些简单的程序,太简单的基本上都能写对,稍微复杂点...
继续阅读 »


首先,我不是标题党。我确确实实受到了震撼。


其次,我今天要写的也不是在chatGPT里面叫AI写什么冒泡排序,鸡兔同笼等网上都已有大量代码示例的问题。


我知道chatGPT已经火出圈了,本人也试验过叫AI写一些简单的程序,太简单的基本上都能写对,稍微复杂点的也能介绍个大致思路,代码也能给出,但是很多都无法正常跑起来,也有一些逻辑性的错误。最多也只能用来参考下。


虽然我觉得目前AI能理解一些人类的意图,能给出大致的实现代码,但是还无法代替程序员去写一些稍微复杂点的算法程序。


直到我今天在网上看到这样一款AI写程序的软件:Cursor


官网长这样:


1.png



查了下该软件的特点以及背后的公司,问了new bing:


2.png


好家伙,原来是openAI这个公司出的。


但我又一想,不就是接入了chatGPT的API么,包装成一个IDE的样子。关键是chatGPT的代码功力我领教过,其实问题挺多的,有时候会有很基础的逻辑错误问题,完全不能拿来直接跑。


但是看在是openAI公司出品的份上,我还是下了这个软件,其实我并不报希望。


软件界面长这样,很像一个IDE:


3.png


简单问了一些猜数字,快速排序的问题。全对,直接复制到IDEA里就能直接跑。比如上图就是我问的一个给出猜数字游戏代码的问题。


其实这种程度chatGPT也能做到。但是很明显我的直观感受是cursor给出的代码的速度比chatGPT快太多了,基本是一秒十几行的速度。


我决定上点强度。



我于是不再问一些网上已有大量示例的经典问题,提了一个swing的需求,要求他帮我写一个swing界面,具体描述如下:



用swing写一个秒表程序,请在界面上画一个圆形的红色的秒表图形,图形上有2根针,一根是分针,一根是秒针,分针比秒针要短,初始都指向0分0秒。在秒表下方还有2个按钮,一个是开始,一个是暂停,当点击开始按钮的时候,秒表时钟开始走动,当点击暂停时,秒表停止走动。暂停后再点击开始,会继续走动。



输入进去,然后AI几乎没思考就开始写了:


4.gif


几秒钟就写完了,好像乍看之下还挺像那么回事,因为我看到他定义了颜色,画了线。我复制到IDEA里面一运行,竟然真的可以运行起来,效果如下:


5.gif


这个有点出乎我的意料,整体除了按钮位置有点不对以外,其他功能和我描述完全正确。


接着加大难度,我给他出了一道在swing界面文件对比的题:



请用swing写一个程序,图形界面顶部上有3个按钮,其中2个分别支持上传2个TXT文件,还有一个比较按钮,点击按钮,则会去比较这2个文件中内容的不同之处,如果完全一致,则弹出一个提示框表明2个文件内容一致。如果不一样,则在下方图形界面(和按钮不在同一行)分别显示这2个文件的内容,在文件内容里面用黄色下标箭头在内容不一致的地方打上标记



想解释下,为什么我一直给他出swing的题,因为swing有界面,好验证啊。


依然是秒出代码,大家看动图:


6.gif


程序明显比之前长很多,中间我输入了2次继续。总体挺丝滑的。复制程序到IDEA里面运行:


7.gif


这下彻底震惊到我了,卧槽,核心功能算是全部实现了。但也有瑕疵,我要求的是用黄色箭头把不一样的地方作标记,他则是把不一样的内容用文本的形式列了出来。


GPT-4写程序难道那么厉害了么,只要描述一小段话,就能写出一个小demo程序来。而且还可以直接运行。


我于是把相同的描述贴给了chatGPT,虽然chatGPT也给出了代码,但是运行出来是完全不对的。


这就说明,cursor不仅仅是个套壳软件。它是真正基于代码的方式进行训练的。



除了swing,普通的java多线程并发业务程序能写么,我于是又问了一个常见的业务问题:电商秒杀模拟程序。描述如下:



写一段程序,模拟下以下业务:
举办一个秒杀活动,总共有2个商品,商品A和商品B,各有50件。需要定义出商品的类。用线程模拟1w个人同时进来抢购,1w个人分别用ID1,ID2,ID3,以此类推来表示。
每个人每个商品只能最多抢2件。2个商品均没抢到的顾客信息不用打印,只打印出抢到了商品的顾客信息,格式举例如下:
顾客[ID1]抢到了[商品A]2件,[商品B]1件



我相信我描述的已经挺清楚了,也说明了要进行多线程,顾客ID命名给了一个推论的形式描述,以及打印信息只给了一个范例描述,看看AI能否学样去打印出符合我的结果


操作过程和上面一样,我就不贴动图了。贴一个图片看看:


8.png


运行出来的打印结果为:


9.png


这下我又要卧槽了,结果是100%完全正确的!我又仔细看了AI写多线程并发,发现也是完全正确的。



其实我测试到这里的时候,我心里已经开始焦虑了,没错,目前cursor也只能写一些单一算法的程序,但是正确率和理解力已经让我吃惊了,从chatGPT横空出世到GPT-4这才几个月啊,就已经这么强了。是不是再过几年,我们就要失业了,是不是就再也不需要程序员了?产品经理只要把详细的描述贴给AI,AI半小时吊打一个技术团队一个月的工作量。


细思极恐。


最后我把cursor的官网地址贴一下,大家可以去下载体验:


https://www.cursor.so/

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

白嫖谷歌搭建个人AI绘画(stable-diffusion),A卡救星

💡 最近的AI绘画大火,满心欢喜的准备体验一下,奈何网上大多数网站都是要收费,想着本地搭建一波,结果发现自己是AMD,但是多数开源的都不支持AMD,幸好在B站找到了大佬白嫖教程,这里就小记一下自己白嫖谷歌计算资源,自己生成AI绘图的教程 前置条件:可以访问谷歌...
继续阅读 »

💡 最近的AI绘画大火,满心欢喜的准备体验一下,奈何网上大多数网站都是要收费,想着本地搭建一波,结果发现自己是AMD,但是多数开源的都不支持AMD,幸好在B站找到了大佬白嫖教程,这里就小记一下自己白嫖谷歌计算资源,自己生成AI绘图的教程


前置条件:可以访问谷歌,有谷歌账号,Github



操作步骤:



1.打开Github项目




  1. 项目地址:github.com/camenduru/s…

  2. 分支选择drive





2.项目安装到谷歌的云端硬盘


2.1 按住ctrl点击一号位置,新窗口打开第一个链接,出现一个新的页面 2.2 第一步:复制到云端硬盘,第二步点击运行,第三步出现这个说明成功,点击期间会出现谷歌的弹窗,直接确定就可以了。 谷歌的云端硬盘,每个用户有15G的免费空间,这个项目大概12G,剩下的空间可以装一写model


3.运行stable-diffusion-webui


3.1 回到Github页面,继续按住ctrl点击二号位置,会打开一个新的连接,和第一次一样,保存-运行 3.2 运行需要一段时间,过一会儿,我们就会看见给出了两个连接,选择最后一个 3.3 打开连接,可以看到AI绘画熟悉的页面,默认是有个model的,不过它生成的图不怎么样,可以去换个model


4.更换model


4.1 model网址:civitai.com/ 4.2 这个复制model链接 4.3 添加model,回到Github页面,继续按住ctrl点击三号位置,将刚才复制的链接放到第一行,第二行是model的名字。复制完后,直接运行,等待就可以了


5.查看model,下载完成后model存放位置




6.生成成果


适合大家随意的玩一玩儿,祝大家玩儿的愉快。


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

H5开屏从龟速到闪电,企微是如何做到的

导读|H5开屏龟速常是令开发者头疼的问题。腾讯企业微信团队对该现象进行分析优化,最终H5开屏耗时130ms,达到秒开效果!企微前端开发工程师陈智仁将分享可用可扩展的Hybird H5秒开方案。该团队使用离线包解决了资源请求耗时的问题,在这个基础上通过耗时分析找...
继续阅读 »

导读|H5开屏龟速常是令开发者头疼的问题。腾讯企业微信团队对该现象进行分析优化,最终H5开屏耗时130ms,达到秒开效果!企微前端开发工程师陈智仁将分享可用可扩展的Hybird H5秒开方案。该团队使用离线包解决了资源请求耗时的问题,在这个基础上通过耗时分析找到瓶颈环节,进一步采用“预热”进行优化提速以解决了WebView初始化、数据预拉取、js执行(app初始化)耗时的问题。希望这些通用方法对你有帮助。


图片


背景


服务端渲染(SSR)是Web主流的性能优化手段。SSR直出相比传统的SPA应用加载渲染规避了首屏拉取数据和资源请求的网络往返耗时。团队针对Web开发也已经支持了SSR能力。近期出于动态化运营的考虑,我们选择了Web开发,同时我们也接到了提升体验的诉求。


以企业微信要开发的页面为例:采用SSR方案,从用户点击到首屏渲染的耗时均值约600ms,白屏时间的存在是可以感知到的。为了尽可能消除白屏达到秒开效果,我们尝试做更多探索。


图片


方案思路


1) 方案选型


如何实现页面秒开呢?从最直观的渲染链路来入手分析。下图列出了从用户点击到看到首屏渲染可交互,一个SPA应用主要环节的加载流程。我们调研了业内相关方案,从渲染链路的视角来看下常见方案的优化思路。


图片



  • 传统离线包


在加载渲染过程中,网络IO是很明显的一个耗时瓶颈。传统的离线包方案思路很直接,如果网络耗时那就将资源离线,很好地解决了资源请求的耗时。用Service Worker也能达到离线包的效果,同时也是Web标准。首次渲染优化一般需要结合客户端配置预启动脚本来达到缓存资源的效果。



  • SSR


SSR则从另外的角度出发,在请求页面的时候就进行服务端数据拉取和页面直出,首屏得以在一个网络往返就可以展示,有效地规避了后续需要等待css/js资源加载、数据拉取的时间。性能体验有比较大的提升,在BFF普及的情况下开发模式简单,很受欢迎。



  • 公司内相关工作


考虑到WebView的初始化(冷启动/ 二次启动)、页面网络请求、首屏数据接口的耗时,白屏时间还是可感知地存在的。以我们要开发的页面为例采用SSR首屏耗时均值600ms,可交互时间均值1100ms。如何进一步消除白屏?这里为各位介绍公司内外针对h5首屏性能优化的优秀方案。


手Q团队的VasSonic是集大成者,主要思路是采用WebView和数据预拉取并行的方式。这套方案需要客户端和服务端采用指定协议改造接入,开发时也有一定的改造工作。


微信游戏团队主要思路是利用jsCore做客户端预渲染,用户点击后直接上屏。这个方法也达到了很好的效果,首屏FCP时间从1664ms降低到了411ms。


我们做了一个简要的方案对比,可以看到每个方案都针对渲染链路的某个或多个环节做了优化,其中VasSonic的效果比较显著。不过结合企业微信业务实际情况,我们列出了如下几点考虑:


首先,接入对客户端和服务端有一定的改造成本,业务开发也有一定的改造工作。其次,我们已经有一套的统一发布平台,希望能复用这套发布能力。最后,性能上有没有进一步优化的空间呢?业务需求对体验上的要求是希望达到更好的性能效果或者说尽可能完全地消除白屏。


基于以上考虑,我们在上述方案的基础上做了进一步的实践探索,以期望达到更好的性能效果。



离线包SSRVasSonicCSR
资源加载
图片


图片

图片
数据拉取

图片

图片

图片
JS执行



WebView启动优化


图片

首屏FCP

图片

图片

图片
可交互(取决于JS执行)




2)方案架构


为了达到尽可能完全消除白屏,我们还是从初始问题出发,结合渲染链路进行分析,思路上针对每个环节采取对应的优化方法。


每个环节的优化在具体落地时会存在着方案的利弊取舍。比如预拉取数据一般的思路是交给客户端来做,但是存在着客户端请求和h5请求两套机制(鉴权、请求通道等方面)如何协调的问题。在渲染链路分析时,如果业务的js执行也贡献了不少耗时,有没有可能从通用基础方案的角度来解决这个问题,同时也能减少业务对性能优化的关注?这是个值得各位思考探索的问题。具体的内容会在后面展开来说。


如图展示了方案的优化思路和主流程。方案使用离线包解决了资源请求耗时的问题,在这个基础上通过耗时分析找到瓶颈环节,进一步采用预热的思路进行优化提速,解决了WebView初始化、数据预拉取、js执行(app初始化)耗时的问题,最终达到了理想的性能体验。


图片


图1 上屏流程


图片


图2 方案架构


下面我们具体介绍下方案,包括:离线包技术、预热提速和进一步的优化工作。


图片


离线包加速


为了规避资源请求耗时,我们使用了离线包技术。离线包技术是比较成熟的方案,相关打包、发布拉取的方案这里不多说了,主要说下方案中一些设计上的考量。


1)加载流程


图片


我们通过offid作为离线包应用的标识,fallback机制保证离线资源不可达时用户也可以正常访问页面,通过离线包预拉取和异步检测更新机制提高了离线包命中率,尽可能消除了网络资源加载的耗时。



2)fallback机制


因为用户网络状况的不确定性,离线包加载可能存在失败的情况。**为了保证可用性,我们确定了离线包加载不阻塞渲染的思路。**当用户点击入口url,对应offid离线包在本地不存在时,会fallback请求现网页面,同时异步加载离线包。所以我们针对离线包的打包结构,按照现网URL path来组织资源路径。这样客户端请求拦截处理也会比较方便,不需要理解映射规则。当发现离线包不匹配资源时,放过请求透到现网即可。如图展示了我们的离线包结构示例。


图片



3. 离线包生命周期


为了提高离线包命中率,我们会配置一些时机(e.g.入口曝光)来预拉取离线包。


离线包的更新机制:客户端加载时根据offid检测到本地离线包的存在,则直接使用拉起,同时启动异步版本检测和更新。如果新包版本号大于本地版本号则更新缓存,同时发布平台也支持区分测试环境、正式环境以及按条件灰度。


上了离线包后,可以看到页面的首屏耗时均值从基准无优化的1340ms降到了963ms,离线包的预拉取和更新策略则使离线包命中率达到了95%。首屏耗时得到了一定的降低,但也还有比较大的优化空间,需要更一步的分析优化。


图片


预热提速


通过离线包的加速,我们解决了资源请求耗时的问题,不过从整个渲染链路来看还有很大的优化空间,我们做了具体的耗时分析,找出耗时瓶颈,针对耗时环节做了进一步的优化提速


1)耗时分析


离线包技术规避了资源请求耗时,但是从整个渲染链路来看还有很大的优化空间,我们做了耗时分析如下。


Hybird应用中,WebView初始化是比较耗时的环节,这里我们针对iOS WebView做了测试。



首次冷启动/ms二次打开/ms
iOS(WKWebView)480ms90ms

数据拉取方面,不同入口页面的耗时不一,某些入口页面比较重的接口耗时超过了1s。


图片


图片


此外,我们发现js执行也贡献了不少耗时。以某入口页面为例,框架初始化时间~10ms,app初始化时间~440ms。


图片



2)渲染链路预热提速




  • 预热流程




我们的目标是消除白屏,这里理想的方案是找到一种和业务无关的通用解法。方案的主要思路是预热,把能提前做的都做了。预热是不是就是把WebView提前创建出来就好了呢?不是的,这里的预热涉及到多个渲染环节的优化组合。如图展示了预热的整体流程,下面一个个来解。


图片



2)WebView预创建


为了消除WebView的耗时,我们采取了全局的预创建WebView,时机为配置入口曝光。不过全局复用预热WebView不可避免地会引入可能的业务内存泄露问题,下文会介绍对应的规避方案。





  • 数据预拉取




数据拉取是页面渲染的一个耗时环节。为了消除数据预拉取耗时,在预创建WebView阶段我们同时进行了数据预拉取。


数据预拉取常见的思路是交给客户端来做,但是存在着客户端请求和h5请求两套机制如何协调的问题,以请求鉴权为例,存在以下的问题:


第一,Web团队自身有一层node BFF,实现了相应的数据拉取业务逻辑,而客户端则走的私有协议通道请求C++后台,二者是不同的鉴权机制。


第二,如果交给客户端来做,可以接入HTTP请求这套机制,改造成本比较大,如果复用原有通道,则一份数据业务逻辑需要两套实现。


如何设计一套通用可扩展的方案?我们希望做到客户端只关注容器的能力(预热、资源拦截等),屏蔽掉更深入的对Web的感知,这样的解耦可以有效控制方案的复杂度。因此,这里我们针对离线包配置项增加了preUrl字段,使客户端维护更通用的能力,数据预拉取交给业务团队来做,具体如下:


第一,客户端:拉取某个离线包配置项时会读取该字段,同时针对当前曝光的入口url可能存在多个有着不同的数据需求,这里会进行收集,将曝光url中的业务key参数拼接到preUrl来初始化WebView,这些作为通用能力。


第二,业务:preUrl页面在加载时会拉取相应的业务数据存到localStorage,实际的数据预拉取请求放到业务方发起,也可以很好地兼容已有的技术栈。





  • JS预执行




很接近目标了,最后js执行的耗时能不能消除呢?首先来看下440ms的耗时具体在哪里,通过分析看到,框架初始化仅需要不到10ms的时间,而真正的大头在业务代码的执行,其中代码编译耗时~80ms,其余的都是业务app初始化执行时间,这个是业务本身复杂度造成的。


我们首先考虑了创建两个WebView的方案,一个负责加载preUrl预拉取数据,另一个负责loadUrl上屏,这样设计上比较简洁健壮,不过实践下来发现效果不理想,如图展示了该方案的效果,渲染不稳定可以感知到白屏的存在。在已经有了预拉取数据和离线资源的情况下,理论上用户点击后需要等待的就只有渲染这块的耗时,实际我们发现在复杂应用初始化时存在js执行耗时较大的问题。


图片


最终我们做了一个预执行的解法。结合SPA的特点,将preUrl作为SPA的一个子页面,不需要UI展示,只负责预拉取数据,这样子页面加载完成的同时也完成了app提前初始化。而相应的不同入口切换页面时,不同于复用预热WebView重新reload页面,为了保留app初始化的效果,我们采取了一套Native通知Web SDK,页面切换交给WebView控制的方案。其中,Native通知则以调用SDK全局方法的方式。通过这种方式,入口页面间切换其实只是hashchange触发的子页面渲染,达到了不错的效果。流程图即预热方案的上屏部分。


图片


该方案执行后我们达到了预期目标效果,最大限度地消除了白屏接近Native体验。需求上线后通过监控数据可以看到在命中预热和离线包逻辑的情况下,从用户点击到页面上屏可交互耗时均值约130ms。


图片


图片


进一步优化



1)离线包安全


在离线包安全方面,为了防止包篡改,每我们次打包发布时都会生成包签名和文件md5。客户端在使用解析离线包时会校验完整性,在返回离线资源时会校验文件完整性。


2)稳定性


整体方案在性能上已经达到目标了,保证稳定性对产品体验也很重要。**我们为了消除js执行的耗时,采取了Native通知Web SDK控制页面切换的方式。虽然比较灵活但是也带来了稳定性的问题。**具体来说,如果SDK在做页面切换时异常,之后用户打开每个入口url都会看到相同的页面。入口页面的业务在用户使用过程中如果跳转了非SPA的链接同时没有注入SDK,之后的页面切换也会失效。


如何保证预热容器的可用性呢?我们设计了一套通知机制确保客户端感知到预热容器的可用状态,并在不可用时得以恢复,如图。预热容器会维护isInit和isInvokedSuc两个状态。只有当preUrl成功加载和SDK执行成功上屏时,两个状态才会置true,此时的预热WebView才是可用的,否则会回退到普通容器模式进行load url来加载页面。


图片


此外,在每次入口url曝光时,已有的预热容器也会销毁重建,也有效保证了容器的稳定性。



3)内存泄露


使用全局的预创建WebView,不可避免的会引入可能的业务内存泄露问题。在测试过程中,我们也发现了这种例子。可以看到当点开使用了预热容器的页面后放置一段时间,整个内存在不断上涨,最终会导致PC端页面的白屏或者移动端的Crash,这个状况最终归因是业务逻辑的实现存在缺陷。


图片


不过在基础技术的角度而言,开发者也需要采取措施来尽可能规避内存泄露的情况。主要思路是减少同一个预热容器的常驻,也就是对存活的容器设置有效期,在适当的时机检查并清理过期容器,我们选择的时机是App前后台切换时


4)解决副作用


出于性能考虑,我们选择了通过Web SDK控制页面的方案,同时使用了全局的预创建WebView。这带来了副作用——当页面对容器做了全局的设置,可能会影响到下一个页面的表现。比如:设置document.title、通过私有JSAPI设置了WebView导航栏的表......


当执行这些操作时,在下一个页面也复用预热容器的情况下,全局设置没有得到清理重置或者覆盖,用户会看到上个页面的表现。


为了解决上述问题,业务可以在每个页面主动声明需要的表现来覆盖上个页面的设置,理想的方法还是基础技术来规避这个问题来保证业务开发的一致性。我们在SDK控制切换页面时,进行了一系列的重置操作。


此外,在Windows和Mac端,我们也设计了双预热WebView的方案来完全解决这个问题。每次使用时同时创建新容器,得以保证每次打开入口页面都是使用新创建的容器。当然,方案的另一面则是会带来App内存的上涨。


图片


图片


总结


我们从渲染链路入手,针对每个环节进行分析优化,最终沉淀了一套可用可扩展的Hybird H5秒开方案。从渲染链路的角度来看,方案通过离线包和预热一系列优化,将用户从点击到可交互的时间缩短到了一个SPA路由切换上屏步骤的耗时。


图片


上线后我们监控发现,命中了预热离线逻辑的页面首屏耗时~130ms,相比于离线包、SSR都有优势,同时预热离线容器命中率也达到了97%,达到了理想的体验效果。希望本篇对你有帮助。



图片


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

巧妙利用枚举来替代if语句

前言 亲爱的友友们,我们今天来看一下如何巧妙利用枚举来替代if语句 能实现功能的代码千篇一律,但优雅的代码万里挑一 业务背景 在工作中遇到一个需求,经过简化后就是:需要根据不同的code值,处理逻辑,然后返回对应的对象。 我就简答举个栗子哈💬 根据 不同的c...
继续阅读 »

前言


亲爱的友友们,我们今天来看一下如何巧妙利用枚举来替代if语句


能实现功能的代码千篇一律,但优雅的代码万里挑一


业务背景


在工作中遇到一个需求,经过简化后就是:需要根据不同的code值,处理逻辑,然后返回对应的对象。


我就简答举个栗子哈💬



根据 不同的code,返回不同的对象
传1 返回 一个对象,包含属性:name、age ; 传2,返回一个对象,包含属性name ; 传3,返回一个对象,包含属性sex ....
字段值默认为 test



思路


摇头版


public class TestEnum {
public static void main(String[] args) {
Integer code = 1;//这里为了简单,直接这么写的,实际情况一般是根据参数获取
JSONObject jsonObject = new JSONObject();
if(Objects.equals(0,code)){
jsonObject.fluentPut("name", "test").fluentPut("age", "test");
System.out.println("jsonObject = " + jsonObject);
}
if(Objects.equals(1, code)){
jsonObject.fluentPut("name", "test");
System.out.println("jsonObject = " + jsonObject);
}
if(Objects.equals(2,code)){
jsonObject.fluentPut("sex", "test");
System.out.println("jsonObject = " + jsonObject);
}
}
}

上面的代码在功能上是没有问题滴,但是要扩展的话就💘,比如 当code为4时,ba la ba la,我们只有再去写一遍if语句,随着code的增加,if语句也会随之增加,后面的人接手你的代码时 💔


image-20230327234216250


优雅版


我们首先定义一个枚举类,维护对应Code需要返回的字段


@Getter
@AllArgsConstructor
public enum DataEnum {
/**
* 枚举类
*/
CODE1(1,new ArrayList<>(Arrays.asList("name","age"))),
CODE2(2,new ArrayList<>(Arrays.asList("name"))),
CODE3(3,new ArrayList<>(Arrays.asList("sex")))
;
private Integer code;
private List<String> fields;
//传入code 即可获取对应的 fields
public static List<String> getFieldsByCode(Integer code){
DataEnum[] values = DataEnum.values();
for (DataEnum value : values) {
if(Objects.equals(code, value.getCode())) {
return value.getFields();
}
}
return null;
}
}

客户端代码


public class TestEnum {
public static void main(String[] args) {
//优雅版
JSONObject jsonObject = new JSONObject();
//传入code,获取fields
List<String> fieldsByCode = DataEnum.getFieldsByCode(1);
assert fieldsByCode != null;
fieldsByCode.forEach(x->{
jsonObject.put(x,"test");
});
System.out.println(jsonObject);
}
}

实现的功能和上面的一样,但是我们发现TestEnum代码里面一条if语句都没有也,这时,即使code增加了,我们也只需要维护枚举类里面的代码,压根不用在TestEnum里面添加if语句,是不是很优雅😎


image-20230327235125257


小总结


【Tips】我们在写代码时,一定要考虑代码的通用性


上面的案例中,第一个版本仅仅只是能实现功能,但是当发生变化时难以维护,代码里面有大量的if语句,看着也比较臃肿,后面的人来维护时,也只能不断的添加if语句,而第二个版本巧用枚举类的方法,用一个通用的获取fields的方法,我们的TestEnum代码就变得相当优雅了😎


结语


谢谢你的阅读,由于作者水平有限,难免有不足之处,若读者发现问题,还请批评,在留言区留言或者私信告知,我一定会尽快修改的。若各位大佬有什么好的解法,或者有意义的解法都可以在评论区展示额,万分谢谢。
写作不易,望各位老板点点赞,加个关注!😘😘😘


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

代码优化一下,用线程池管理那些随意创建出来的线程

线程大家一定都用过,项目当中一些比较耗时的操作,比如网络请求,IO操作,我们都会把这类操作放在子线程中进行,因为如果放在主线程中,就会多少造成一些页面卡顿,影响性能,不过是不是放在子线程中就好了呢,我们看看下面这段代码 很简单的一段代码,创建了一个Threa...
继续阅读 »

线程大家一定都用过,项目当中一些比较耗时的操作,比如网络请求,IO操作,我们都会把这类操作放在子线程中进行,因为如果放在主线程中,就会多少造成一些页面卡顿,影响性能,不过是不是放在子线程中就好了呢,我们看看下面这段代码


image.png

很简单的一段代码,创建了一个Thread,然后把耗时工作放在里面进行就好了,如果项目当中只有一两处出现这样的代码,倒也影响不大,但是现在的项目当中,耗时的操作一大堆,比如文件读取,数据库的读取,sp操作,或者需要频繁从某个服务器获取数据显示在屏幕上,比如k线图等,像这些操作如果我们都去通过创建新的线程去执行它们,那么对性能以及内存的开销是很大的,所以我们在平时开发过程当中应该养成习惯,不要去创建新的线程而是通过使用线程池去执行自己的任务


线程池


为什么要使用线程池呢?线程池总结一下有以下几点优势



  • 降低资源消耗:通过复用之前创建过的线程资源,降低线程创建与销毁带来的性能与内存的开销

  • 提高响应速度:无需等待线程创建,直接可以执行任务

  • 提高线程可管理性:使用线程池可以对线程资源统一调优,分配,管理

  • 使用更多扩展功能:使用线程池可以进行一些延迟或者周期性工作


而我们创建线程池的方式有以下几种



  • Executors.newFixedThreadPool:创建一个固定大小的线程池

  • Executors.newCachedThreadPool:创建一个可缓存的线程池

  • Executors.newSingleThreadExecutor:创建单个线程数的线程池

  • Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池

  • Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池

  • Executors.newWorkStealingPool:创建一个抢占式执行的线程池

  • ThreadPoolExecutor:最原始的创建方式,以上六种方式的内部也是通过这个创建的线程池


虽然我们提倡使用线程池,但是有这么多的创建方式,我们如果不在项目当中做一下管理的话,那么各种各样的线程池都有可能被使用到,由于每种创建方式对于线程的管理方式都不一样,如果不合理创建的话,很可能会出现问题,所以我们需要有一个统一创建线程池的地方


统一管理线程


image.png

首先我们先创建一个线程池,使用Executors.newCachedThreadPool()去创建一个
ExecutorService,至于为什么选择newCachedThreadPool(),我们看下它的源码


image.png

从上面一大段英文注释中我们能知道,这是一个可缓存的线程池,并且corePoolSize为0说明这个线程池没有始终存活的线程,如果线程池中没有可用线程,会重新创建新线程,而线程池中如果有可用线程,那么这个线程会被再利用,一个线程如果60秒内没有被使用,那么将会从队列中移除并销毁,所以个人感觉对于并发要求不是特别高的移动端,从性能角度来讲使用这样的一个线程池是比较合适的,当然具体设计方案以业务性质来决定,现在我们可以将项目当中的线程放在我们的线程池里面运行了,再增加一个执行线程的函数


image.png

通过这个函数就可以有效的避免项目当中随意创建线程的现象发生,让项目当中的线程可以井然有序的运行,但是这还没完事,我们知道Runnable在任务执行完成之后是没有返回结果的,因为Runnable接口中的run方法的返回类型是个void,但实际开发当中,我们的确有需求,在执行一些比如查询数据库,读取文件之类的操作中,需要获取任务的执行结果,之前都是通过在线程当中手动添加一个handler将需要的数据传递出来,再专业一点使用RxJava或者Flow,但不管什么方式,这些都会造成代码耦合,我们还有更简单的方式


Callable和Future


这两个类是在java1.5之后推出来的,目的就是解决线程执行完成之后没有返回结果的问题,我们先来对比下Runnable与Callable


image.png

相比较于Runnable,Callable接口里面也有一个call的方法,这个方法是有返回值的,并且可以抛出异常,所以以后当我们需要获取任务的执行结果的时候,我们还可以使用Callable代替Runnable,那么如何使用并获取返回值呢?当然是使用我们已经创建好的ExecutorService,它里面提供了一个函数去执行Callable


image.png

使用submit函数就可以执行我们的Callable,返回值是一个Future,而如何去获取真正的返回结果,就在Future里面,我们看下


image.png

使用get方法就可以获取线程的执行结果,我们现在就来试试Callable和Future,在PoolManager里面再增加一个函数,用来执行Callable


image.png

我们这里有个简单的任务,就是返回一段文字,然后将这段文字显示在界面上,那么第一步,先在布局文件里面添加一个按钮


image.png

然后点击这个按钮,将任务里面返回的文字显示在按钮上,代码如下


image.png

得到的效果如下


aa2.gif


在这边特地把执行结果放在界面上而不是用日志打印出来的原因可能大家已经发现了,Callable在返回执行结果的同时,也帮我们把线程切回到了主线程,所以我们不用在特地去切换线程更新ui界面了


周期性任务


普通的单个任务我们讲完了,但是在项目当中往往会存在一些比较特殊的任务,可能需要你去周期性的去执行,举个常见的例子,在证券类的app里绘制k线图的时候,并不需要将服务器吐出来的数据统统拿出来绘制ui,这样对性能的开销是很大的,我们正确的做法是将数据都先存放在一个buffer里面,然后定时的去buffer里面拿最新数据就好,那这样一个定时刷新的功能如何在我们的线程池里面去实现呢,这里就要用到刚刚说到的另一种创建线程池的方式


image.png

这个函数创建的是一个ScheduledExecutorService对象,可周期性的执行任务,入参的corepoolSize表示可并发的线程数,现在我们在PoolManager里面添加上这个ScheduledExecutorService


image.png

而如何去执行任务,我们使用ScheduledExecutorService里面的scheduleAtFixedRate函数,我们先看下这个函数都有哪些入参


image.png

不用去看注释我们就能知道怎么使用这个函数,command就是执行的任务,第二,第三个参数分别表示延迟执行的时间以及任务执行的周期时间,第四个参数是时间的单位,在看返回值是一个ScheduleFuture,既然也是个Future,那是不是也可以通过它去获取任务执行的结果呢?答案是拿不到的,一个原因是command是一个Runnable而不是Callable,不会返回任务的执行结果,另外我们从注释上就能了解,这个ScheduleFuture只是用来当周期任务中有任务被取消了,或者被异常终止的时候,抛出异常用的,那ScheduledExecutorService一定有入参是Callable的函数的吧,找了找发现并没有,那只有一个办法了,我们在command里面去执行一个Callable任务,再将任务的执行结果回调出来就好了,代码设计如下


image.png

我们创建了一个函数叫executeScheduledJob,也有四个入参,job是一个Callable,用来执行我们的任务,callback是一个回调,用来将任务执行结果回调到上层去处理,后面两个刚刚已经介绍过了,这里设置了默认值,可自定义,现在我们就来实现一个简单的读秒功能,点击刚刚那个按钮,按钮初始值是1,然后每秒钟加一,代码实现如下


image.png

这边创建了一个CounterViewModel用来执行计数器的逻辑,dataState是一个StateFlow并且设置了初始值1,在onCallback里面接收到了任务执行结果并发送至上层展示,上层的代码逻辑如下


image.png

现在这个计时器功能完成了,我们来执行下代码看看效果如何


aa3.gif


我们这边使用StateFlow发送数据还有个好处,当接收的数据中有些数据需要过滤掉的时候,我们还可以使用StateFlow提供的操作符实现,比如这边我们只想展示奇数,那么代码可以改成如下所示


image.png

使用filter操作符将偶数的值过滤掉了,我们再看看效果


aa4.gif


总结


我们的这个线程管理工具到这里已经完成了,不是很复杂,但是项目当中存不存在这样一个工具明显会对整体开发效率,代码的可读性,维护成本,以及一个app的性能角度来讲都会有个很大的提升与改善,后面如果还做了其他优化工作,也会拿出来分享。


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

Moshi 真正意义上的完美解决Gson在kotlin中默认值空的问题

Moshi Moshi是一个对Kotlin更友好的Json库,square/moshi: A modern JSON library for Kotlin and Java. (github.com) 依赖 implementation("com.square...
继续阅读 »

Moshi


Moshi是一个对Kotlin更友好的Json库,square/moshi: A modern JSON library for Kotlin and Java. (github.com)


依赖


implementation("com.squareup.moshi:moshi:1.8.0")
kapt("com.squareup.moshi:moshi-kotlin-codegen:1.8.0")

使用场景


基于kotlin-reflection反射需要额外添加 com.squareup.moshi:moshi-kotlin:1.13.0 依赖


// generateAdapter = true 表示使用codegen生成这个类的JsonAdapter
@JsonClass(generateAdapter = true)
// @Json 标识json中字段名
data class Person(@Json(name = "_name")val name: String, val age: Int)
fun main() {
   val moshi: Moshi = Moshi.Builder()
       // KotlinJsonAdapterFactory基于kotlin-reflection反射创建自定义类型的JsonAdapter
      .addLast(KotlinJsonAdapterFactory())
      .build()
   val json = """{"_name": "xxx", "age": 20}"""
   val person = moshi.adapter(Person::class.java).fromJson(json)
   println(person)
}


  • KotlinJsonAdapterFactory用于反射生成数据类的JsonAdapter,如果不使用codegen,那么这个配置是必要的;如果有多个factory,一般将KotlinJsonAdapterFactory添加到最后,因为创建Adapter时是顺序遍历factory进行创建的,应该把反射创建作为最后的手段




  • @JsonClass(generateAdapter = true)标识此类,让codegen在编译期生成此类的JsonAdapter,codegen需要数据类和它的properties可见性都是internal/public




  • moshi不允许需要序列化的类不是存粹的Java/Kotlin类,比如说Java继承Kotlin或者Kotlin继承Java


存在的问题


所有的字段都有默认值的情况


@JsonClass(generateAdapter = true)
data class DefaultAll(
  val name: String = "me",
  val age: Int = 17
)

这种情况下,gson 和 moshi都可以正常解析 “{}” json字符


部分字段有默认值


@JsonClass(generateAdapter = true)
data class DefaultPart(
  val name: String = "me",
  val gender: String = "male",
  val age: Int
)

// 针对以下json gson忽略name,gender属性的默认值,而moshi可以正常解析
val json = """{"age": 17}"""


产生的原因


Gson反序列化对象时优先获取无参构造函数,由于DefaultPart age属性没有默认值,在生成字节码文件后,该类没有无参构造函数,所有Gson最后调用了Unsafe.newInstance函数,该函数不会调用构造函数,执行对象初始化代码,导致name,gender对象是null。


Moshi 通过adpter的方式匹配类的构造函数,使用函数签名最相近的构造函数构造对象,可以是的默认值不丢失,但在官方的例程中,某些情况下依然会出现我们不希望出现的问题。


Moshi的特殊Json场景


1、属性缺失


针对以下类


@JsonClass(generateAdapter = true)
data class DefaultPart(
   val name: String,
   val gender: String = "male",
   val age: Int
)

若json = """ {"name":"John","age":18}""" Moshi可以正常解析,但如果Json=""" {"name":"John"}"""Moshi会抛出Required value age missing at $ 的异常,


2、属性=null


若Json = """{"name":"John","age":null} ”“”Moshi会抛出Non-null value age was null at $ 的异常


很多时候后台返回的Json数据并不是完全的统一,会存在以上情况,我们可以通过对age属性如gender属性一般设置默认值的方式处理,但可不可以更偷懒一点,可以不用写默认值,系统也能给一个默认值出来。


完善Moshi


分析官方库KotlinJsonAdapterFactory类,发现,以上两个逻辑的判断代码在这里


internal class KotlinJsonAdapter<T>(
 val constructor: KFunction<T>,
   // 所有属性的bindingAdpter
 val allBindings: List<Binding<T, Any?>?>,
   // 忽略反序列化的属性
 val nonIgnoredBindings: List<Binding<T, Any?>>,
   // 反射类得来的属性列表
 val options: JsonReader.Options
) : JsonAdapter<T>() {

 override fun fromJson(reader: JsonReader): T {
   val constructorSize = constructor.parameters.size

   // Read each value into its slot in the array.
   val values = Array<Any?>(allBindings.size) { ABSENT_VALUE }
   reader.beginObject()
   while (reader.hasNext()) {
       //通过reader获取到Json 属性对应的类属性的索引
     val index = reader.selectName(options)
     if (index == -1) {
       reader.skipName()
       reader.skipValue()
       continue
    }
       //拿到该属性的binding
     val binding = nonIgnoredBindings[index]
// 拿到属性值的索引
     val propertyIndex = binding.propertyIndex
     if (values[propertyIndex] !== ABSENT_VALUE) {
       throw JsonDataException(
         "Multiple values for '${binding.property.name}' at ${reader.path}"
      )
    }
// 递归的方式,初始化属性值
     values[propertyIndex] = binding.adapter.fromJson(reader)

       // 关键的地方1
       // 判断 初始化的属性值是否为null ,如果是null ,代表这json字符串中的体现为 age:null
     if (values[propertyIndex] == null && !binding.property.returnType.isMarkedNullable) {
         // 抛出Non-null value age was null at $ 异常
       throw Util.unexpectedNull(
         binding.property.name,
         binding.jsonName,
         reader
      )
    }
  }
   reader.endObject()

   // 关键的地方2
    // 初始化剩下json中没有的属性
   // Confirm all parameters are present, optional, or nullable.
     // 是否调用全属性构造函数标志
   var isFullInitialized = allBindings.size == constructorSize
   for (i in 0 until constructorSize) {
     if (values[i] === ABSENT_VALUE) {
         // 如果等于ABSENT_VALUE,表示该属性没有初始化
       when {
           // 如果该属性是可缺失的,即该属性有默认值,这不需要处理,全属性构造函数标志为false
         constructor.parameters[i].isOptional -> isFullInitialized = false
           // 如果该属性是可空的,这直接赋值为null
         constructor.parameters[i].type.isMarkedNullable -> values[i] = null // Replace absent with null.
           // 剩下的则是属性没有默认值,也不允许为空,如上例,age属性
           // 抛出Required value age missing at $ 异常
         else -> throw Util.missingProperty(
           constructor.parameters[i].name,
           allBindings[i]?.jsonName,
           reader
        )
      }
    }
  }

   // Call the constructor using a Map so that absent optionals get defaults.
   val result = if (isFullInitialized) {
     constructor.call(*values)
  } else {
     constructor.callBy(IndexedParameterMap(constructor.parameters, values))
  }

   // Set remaining properties.
   for (i in constructorSize until allBindings.size) {
     val binding = allBindings[i]!!
     val value = values[i]
     binding.set(result, value)
  }

   return result
}

 override fun toJson(writer: JsonWriter, value: T?) {
   if (value == null) throw NullPointerException("value == null")

   writer.beginObject()
   for (binding in allBindings) {
     if (binding == null) continue // Skip constructor parameters that aren't properties.

     writer.name(binding.jsonName)
     binding.adapter.toJson(writer, binding.get(value))
  }
   writer.endObject()
}


通过代码的分析,是不是可以在两个关键的逻辑点做以下修改



// 关键的地方1
// 判断 初始化的属性值是否为null ,如果是null ,代表这json字符串中的体现为 age:null
if (values[propertyIndex] == null && !binding.property.returnType.isMarkedNullable) {
   // 抛出Non-null value age was null at $ 异常
   //throw Util.unexpectedNull(
   //   binding.property.name,
   //   binding.jsonName,
   //   reader
   //)
   // age:null 重置为ABSENT_VALUE值,交由最后初始化剩下json中没有的属性的时候去初始化
values[propertyIndex] = ABSENT_VALUE
}

// 关键的地方2
// 初始化剩下json中没有的属性
// Confirm all parameters are present, optional, or nullable.
// 是否调用全属性构造函数标志
var isFullInitialized = allBindings.size == constructorSize
for (i in 0 until constructorSize) {
   if (values[i] === ABSENT_VALUE) {
       // 如果等于ABSENT_VALUE,表示该属性没有初始化
       when {
           // 如果该属性是可缺失的,即该属性有默认值,这不需要处理,全属性构造函数标志为false
           constructor.parameters[i].isOptional -> isFullInitialized = false
           // 如果该属性是可空的,这直接赋值为null
           constructor.parameters[i].type.isMarkedNullable -> values[i] = null // Replace absent with null.
           // 剩下的则是属性没有默认值,也不允许为空,如上例,age属性
           // 抛出Required value age missing at $ 异常
           else ->{
               //throw Util.missingProperty(
                   //constructor.parameters[i].name,
                   //allBindings[i]?.jsonName,
                   //reader
          //)
               // 填充默认
               val index = options.strings().indexOf(constructor.parameters[i].name)
               val binding = nonIgnoredBindings[index]
               val propertyIndex = binding.propertyIndex
// 为该属性初始化默认值
               values[propertyIndex] = fullDefault(binding)

          }
      }
  }
}



private fun fullDefault(binding: Binding<T, Any?>): Any? {
       return when (binding.property.returnType.classifier) {
           Int::class -> 0
           String::class -> ""
           Boolean::class -> false
           Byte::class -> 0.toByte()
           Char::class -> Char.MIN_VALUE
           Double::class -> 0.0
           Float::class -> 0f
           Long::class -> 0L
           Short::class -> 0.toShort()
           // 过滤递归类初始化,这种会导致死循环
           constructor.returnType.classifier -> {
               val message =
                   "Unsolvable as for: ${binding.property.returnType.classifier}(value:${binding.property.returnType.classifier})"
               throw JsonDataException(message)
          }
           is Any -> {
               // 如果是集合就初始化[],否则就是{}对象
               if (Collection::class.java.isAssignableFrom(binding.property.returnType.javaType.rawType)) {
                   binding.adapter.fromJson("[]")
              } else {
                   binding.adapter.fromJson("{}")
              }
          }
           else -> {}
      }
  }

最终效果


"""{"name":"John","age":null} ”“” age会被初始化成0,


"""{"name":"John"} ”“” age依然会是0,即使我们在类中没有定义age的默认值


甚至是对象


@JsonClass(generateAdapter = true)
data class DefaultPart(
   val name: String,
   val gender: String = "male",
   val age: Int,
   val action:Action
)
class Action(val ac:String)

最终Action也会产生一个Action(ac:"")的值


data class RestResponse<T>(
val code: Int,
val msg: String="",
val data: T?
) {
fun isSuccess() = code == 1

fun checkData() = data != null

fun successRestData() = isSuccess() && checkData()

fun requsetData() = data!!
}
class TestD(val a:Int,val b:String,val c:Boolean,val d:List<Test> ) {
}

class Test(val a:Int,val b:String,val c:Boolean=true)



val s = """
{
"code":200,
"msg":"ok",
"data":[{"a":0,"c":false,"d":[{"b":null}]}]}
""".trimIndent()

val a :RestResponse<List<TestD>>? = s.fromJson()



最终a为 {"code":200,"msg":"ok","data":[{"a":0,"b":"","c":false,"d":[{"a":0,"b":"","c":true}]}]}


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

抓包神器 Charles 使用教程支持mac ios Android

本文以Mac 系统为例进行讲解 配置手机代理: 手机和 Mac 连接到同一个 WiFi 网络 1.1 Android 系统:「以华为 P20 手机为例」 设置 -> 无线和网络 -> WLAN 长按当前 WiFi -> 修改网络 勾选显...
继续阅读 »

本文以Mac 系统为例进行讲解



  • 配置手机代理:


手机和 Mac 连接到同一个 WiFi 网络


1.1 Android 系统:「以华为 P20 手机为例」



  • 设置 -> 无线和网络 -> WLAN

  • 长按当前 WiFi -> 修改网络

  • 勾选显示高级选项

  • 代理 -> 手动

  • 服务器主机名 -> 填写 Mac 的IP 地址「Mac IP 获取方法:Charles -> Help -> Local IP Address 」

  • 服务器端口 -> 8888

  • 保存


1.2 IOS 系统:「以 iPhone Xs Max 手机为例」



  • 设置 -> 无线局域网

  • 点击当前连接的 WiFi

  • 最底部 HTTP 代理 -> 配置代理 -> 勾选手动

  • 服务器 -> 填写 Mac 的IP 地址「Mac IP 获取方法:Charles -> Help -> Local IP Address 」

  • 端口 -> 8888

  • 存储


核心功能


一、  抓包「以 iPhone Xs Max 为例」



  1. Charles 设置



  • Proxy -> Proxy Settings -> Port -> 8888

  • 勾选 Support HTTP/2

  • 勾选 Enable transparent HTTP proxying

  • OK




  1. 手机设置代理如上「配置手机代理」步骤




  2. 打开手机上任意联网的应用,Charles 会弹出请求连接的确认菜单,点击“Allow“即可完成设置




二、  过滤网络请求



  1. 左侧底部 Filter 栏 -> 过滤关键字




  1. 在 Charles 的菜单栏选择


Proxy -> Recording Settings -> Include -> add「依次填入协议+主机名+端口号,即可只抓取目标网站的包」



  1. 切换到 Sequence,在想过滤的网络请求上右击,选择“Focus“,在 Filter 栏勾选上 Focused


三、  分析 HTTPS 



  1. Mac 安装证书:


Help -> SSL Proxying -> Install Charles Root Certificate -> 输入系统的帐号密码,即可在钥匙串中看到添加好的证书


image.png


如果遇到证书不被信任的问题,解决办法:


Mac本顶栏 前往 -> 实用工具 -> 打开钥匙串访问 -> 找到该证书 -> 双击或右键「显示简介」-> 点开「信任」-> 选择「始终信任」




  1. Charles 设置请求允许 SSL proxying




  2. Charles 默认并不抓取 HTTPS 网络通讯的数据,若想拦截所有 HTTPS 网络请求,需要进行设置:在请求上右击选择 Enable SSL proxying




image.png
2. Charles -> Proxy -> SSL Proxying Settings -> SSL Proxying「添加对应的域名和端口号,为方便也可端口号直接添加通配符*」



  1. 移动端安装证书


a. Charles 选择 Help -> SSL Proxying -> Install Charles Root Certificate on a Mobile Device or Remote Browser


b. 确保手机连上代理的情况下,在手机浏览器栏输入:chls.pro/ssl,下载证书,完成安装。


Android tips


1.1. 小米机型请注意,如果是 MIUI 9 以上的版本,请不要用自带浏览器下载证书,自带浏览器下载的证书文件格式不对,无法安装,uc 浏览器下载没有问题。


1.2. 若不能直接安装,需要下载下来,到手机设置 -> 安全 -> 从设备存储空间安装 -> 找到下载的证书 .pem 结尾的 -> 点击安装即可


IOS tips


IOS 需要设置手机信任证书,详见 官方文档。若不能直接安装,需在手机「设置」-> 通用 -> 描述文件与设备管理安装下载的证书,完成安装后 -> 找到关于本机 -> 证书信任设置,打开刚安装的证书的开关。


抓包内容遇到乱码,解决如下:



  • Proxy -> SSL Proxy Settings -> Add

  • Host:*「代表所有网站都拦截」

  • Port:443

  • 保存后,在抓包数据就会显示正常


四、  模拟弱网




  1. 选择 Proxy -> Throttle Settings -> 勾选 Enable Throttling -> 选择 Throttle Preset 类型
    image.png
    五、  Mock 数据




  2. 以 map local 为例,修改返回值




选择目标请求,右键选择 Save All保存请求的 response 内容到本地文件



  1. 配置 Charles Map Local,Tool -> Map Local -> 勾选 Enable Map Local -> Add 「添加目标请求及需要替换的response 文件地址」-> OK


image.png



  1. 用文本编辑器打开保存的 json 文件,修改内容,进行替换。打开客户端应用重新请求该接口,返回的数据就是本地的文件数据。



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

Android打造专有hook,让不规范的代码扼杀在萌芽之中

俗话说,无规矩不成方圆,同样的放在代码里也是十分的贴切,所谓在代码里的规矩,指的就是规范,在一定规范约束下的项目,无论是参与开发还是后期维护,都是非常的直观与便捷,不能说赏心悦目,也可以用健壮可维护来表示;毕竟协同开发的项目,每个人都有自己的一套开发标准,你没...
继续阅读 »

俗话说,无规矩不成方圆,同样的放在代码里也是十分的贴切,所谓在代码里的规矩,指的就是规范,在一定规范约束下的项目,无论是参与开发还是后期维护,都是非常的直观与便捷,不能说赏心悦目,也可以用健壮可维护来表示;毕竟协同开发的项目,每个人都有自己的一套开发标准,你没有一套规范,或者是规范没有落地执行,想想,长此以往,会发生什么?代码堆积如山?维护成本翻倍增加?新人接手困难?等等,所谓的问题会扑面而来。


正所谓规范是一个项目的基石,也是衡量一个项目,是否健壮,稳定,可维护的标准,可谓是相当重要的。我相信,大部分的公司都有自己的一套规范标准,我也相信,很多可能就是一个摆设,毕竟人员的众多,无法做到一一的约束,如果采取人工的检查,无形当中就会投入大量的时间和人力成本,基于此,所谓的规范,也很难执行下去。


介于人工和时间的投入,我在以往的研究与探索中,开发出了一个可视化的代码检查工具,之前进行过分享,《一个便捷操作的Android可视化规范检查》,想了解的老铁可以看一看,本篇文章不做过多介绍,当时只介绍了工具的使用,没有介绍相关功能的开发过程,后续,有时间了,我会整理开源出来,一直忙于开发,老铁们,多谅解。这个可视化的检查工具,虽然大大提高了检查效率,也节省了人力和时间,但有一个潜在的弊端,就是,只能检查提交之后的代码是否符合规范,对于提交之前没有进行检查,也就说,在提交之前,规范也好,不规范也罢,都能提交上来,用工具检查后,进行修改,更改不规范的地方后然后再提交,只能采取这样的一个模式检查。


这样的一个模式,比较符合,最后的代码检查,适用于项目负责人,开发Leader,对组员提交上来的代码进行规范的审阅,其实并不适用于开发人员,不适用不代表着不可用,只不过相对流程上稍微复杂了几步;应对这样的一个因素,如何适用于开发人员,方便在提交代码之前进行规范检查,便整体提上了研发日程,经过几日的研究与编写,一个简单便捷的Android端Git提交专有hook,便应运而生了。


说它简单,是因为不需要编写任何的代码逻辑,只需要寥寥几步命令,便安装完毕,通过配置文件,便可灵活定制属于自己的检查范围。


为了更好的阐述功能及讲述实现过程,便于大家定制自己的开发规范,再加上篇幅的约束,我总结了四篇文章来进行系统的梳理,还请大家,保持关注,今天这篇,主要讲述最终的开发成果,也就是规范工具如何使用,规范这个东西,其实大差不差,大家完全可以使用我自己已经开发好的这套。


这个工具的开发,利用的是git 钩子(hook),当然也是借助的是Node.js来实现的相关功能,下篇文章会详细介绍,我们先来安装程序,来目睹一下实际的效果,安装程序,只需要执行几步命令即可,无需代码介入,在实际的开发中需要开发人员,分别进行安装。


安装流程


1、安装 Node.js,如果已经安装,可直接第2步:


Node.js中允许使用 JavaScript 开发服务端以及命令行程序,我们可以去官网nodejs.org
下载最新版本的安装程序,然后一步一步进行安装就可以了,这个没什么好说的,都是开发人员。


2、安装android_standard


android_standard是最终的工具,里面包含着拦截代码判断的各种逻辑 在项目根目录下执行如下命令:


npm install android_standard --save-dev

执行完命令后,你会发现,你的项目下已经多了一个目录,还有两个json文件,如下图所示:


image.png


node_modules,用来存放下载安装的包文件夹,里面有我们要使用到的功能,其实和Android中lib目录很类似,都是一些提供功能的库。


package.json文件,是配置文件,比如应用的名字,作者,介绍,还有相关的依赖等,和Android中的build.gradle文件类似。


3、创建git配置文件,执行如下命令


node node_modules/android_standard/gitCommitConfig

命令执行成功会返回如下信息:


image.png


此命令执行完后,会在项目根目录下创建gitCommitConfig文件,这个文件很重要,是我们执行相关命令的配置文件,内容如下,大家可以根据自己实际项目需要进行更改。


项目下生成gitCommitConfig.android文件,.android是我自己定义的,至于什么格式,等你自己开发的时候,完全可以自定义,是个文件就行。


image.png


打开后,文件内容如下,此文件是比较重要的,后续所有的规范检查,都要根据这个文件里的参数来执行,大家在使用的时候,就可以通过这个文件来操作具体的规范检查。


image.png


4、更改执行文件,执行如下命令


执行文件,就是需要在上边生成的package.json文件,添加运行程序,使其在git提交时进行hook拦截。


node node_modules/android_standard/package

5、添加git过滤


因为执行完上述命令后,会产生几个文件,而这几个文件是不需要我们上传到远程仓库的,所以我们需要在.gitignore文件里添加忽略,直接复制即可。


/node_modules
package.json
package-lock.json
gitCommitConfig.android

6、后续如果有更新,可命令进行操作:


注:此命令在更新时执行


npm update android_standard --save-dev

7、删除操作


注:后续不想使用了,便可执行如下命令:


npm uninstall android_standard --save-dev

具体使用


通过上述的安装流程,短短几个命令,我们的规范检查便安装完毕,后续只需要通过gitCommitConfig.android文件,来动态的更改参数即可,是不是非常的方便,接下来,我们来实际的操作一番。


关于配置文件的相关参数,也都有注释,一看便知,这里简单针对最后的参数,做一个说明,也就是gitCommand这个参数,true为工具,false为命令方式;true也好,false也好,在主要的功能验证上,没有区别,唯一的区别就是,命令行的方式提交,会有颜色区分,后面有效果。


image.png


我们先来看下命令行下的执行效果,当配置文件开关gitCommitSwitch已开,并且gitCommand为false,其他的配置参数,大家可以根据需要进行改动,在代码提交的时候如下效果:


image.png


在Android studio中提交代码执行效果


image.png


TortoiseGit提交代码执行效果:


image.png


目前呢,针对Android端的规范检查,无论是java还是Kotlin,还是资源文件,都做了一定的适配,经过多方测试,一切正常,如果大家的公司也需要这样的一个hook工具,欢迎使用,也欢迎继续关注接下来的相关实现逻辑文章。


好了各位老铁,这篇文章先到这里,下篇文章会讲述,具体的实现过程,哦,忘记了,上篇结尾的遗留组件化还未更新,那就更新完组件化,接着更新这个,哈哈,敬请期待!


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

做个清醒的程序员之成为少数派

阅读时长约10分钟,统计2604个字。 这是一篇插队的文章。 本来我是有备稿,准备在下周一的时候发布,结果就在上周二,发生了一件事情。这件事情让我产生很多启发,我在这里把它分享给你,希望对你也有所启发。 周二下午,有位老兄加我微信,备注来自博客。这也不足为奇,...
继续阅读 »

阅读时长约10分钟,统计2604个字。


这是一篇插队的文章。


本来我是有备稿,准备在下周一的时候发布,结果就在上周二,发生了一件事情。这件事情让我产生很多启发,我在这里把它分享给你,希望对你也有所启发。


周二下午,有位老兄加我微信,备注来自博客。这也不足为奇,最近更新比较频繁,加了很多人。这位老兄一上来先是肯定了我的文章,随后指出了文中的错误。坦率地讲,自从复活博客之后,这还是第一位指出我错误的朋友,一下子我就来了兴趣。


在本系列文集的《序》中,我原文是这样写的:



我一直奉行一句话:“有道无术,尚可求也;有术无道,则止于术”。这句话出自老子的《道德经》,而且很好理解。



他指出《道德经》里其实没有这句话。但是呢,本着对读者负责的态度,我在写文章的时候确实去查了一下。程序员这个职业大家都懂,比较较真,至少我是这样的。于是我就找到了一些依据,来证明我说的是对的。但很快便发现事实其实不是这样,这位老兄所言非虚,我引的这句话确实并不出自《道德经》。所以,我要在这里向所有读过我上篇文章的朋友道个歉。澄清一下:“有道无术,尚可求也;有术无道,则止于术”,尽管这句话有几分道理,但真的不是《道德经》原文。


好了,故事就到这里结束了。说到这,大家应该也能理解我为什么要把这篇文章拿来插队。一方面趁热打铁,有错误及时声明,另一方面这个故事对我有新的启发。


这位老兄,名为张鸿羽。稍加细聊后,我得知鸿羽兄是有背过原文的,而我没有。我只是看到大部分都这样说,便信以为真,然后也跟着这样说。显然,我成为了大多数人中的一份子。而鸿羽兄是少数派中的一份子。有时候,真理真的掌握在少数人手中。


回想过去几年的工作历程,特别是刚开始工作的那几年,我做的很多工作都是“探索型”的。所谓“探索型”,就是对新技术,或者说是公司的研发部门未曾使用过的技术进行尝试摸索。当然,尝试新技术之前,要能发现新技术。而一项新技术的诞生,总会伴随着官方的宣传,以及一些支持它、拥护它的人高声叫好。但只有真正尝试过,特别是用新技术来实现较为复杂系统的时候,才会知道这项新技术到底优势在哪,劣势又在哪。


诚然,如果让我来总结我尝试新技术、新框架的经验,我会说:大部分新技术或是框架确实弥补了已有框架的不足,但其弥补的程度往往并不是质变的,只是小步优化。甚至有些新兴技术在弥补的同时,还引入了其它的问题。这对于使用它的开发者来说,的确是个坏消息。


但话说回来,没尝试用过,又怎能知道答案呢?技术的发展本就是这样一步一个坎,有时候走一步还退两步的呀。


这或许就是我等软件开发者的宿命,对于现存的技术框架,总是有这样或那样的不满意,觉得用着不顺手。期盼着某一天,某个技术大佬,或者团体,发明了一种新的框架,甚至是新的编程语言。或是直接起义,自己创造一款新的技术框架,能真正地解决那些令我们不满的问题,让软件开发编程成为真正的享受。


但现实是,很多新的技术框架的诞生,都伴随着类似的口号。也总会有勇敢的开发者尝鲜,也总会经历被坑,然后不断填坑的过程。而这些敢于尝鲜的开发者,就是那些最终会成为“少数派”的人。他们知道在各种美好的宣传背后,隐藏着多深的坑。对于这些坑,又该用什么方法去填。


“少数派”或许才是那些头脑最清醒的那一小撮人群。


但是,成为“少数派”不仅意味着失败的尝试,还有大多数人的不理解。甚至更严重一些,就是诋毁,百口莫辩。这需要一颗强大的内心,和与时间做朋友的勇气以及态度。


不过,我为什么鼓励程序员要做“少数派”,而不是成为“大多数”呢?还有另外一个原因,那就是由行业特征决定的。我相信程序员大多都活跃在互联网行业,这个行业是赢家通吃的指数型结构。有点类似财富分配,大部分的财富掌握在少数人的手里。而且无论如何数学建模,或是提高那些穷人的初始资金,最终推演的结局依然如此。


如今,在中国,乃至全世界,所谓“互联网大厂”无非就是那几家,而剩下的呢?数字上远远超过我们熟知的那些大厂,但拥有的财富值却位于指数图表中的长尾之中。这就是指数型的行业的特征,也是程序员这个群体的特征。


如果大家有查相关的数据,可以发现优秀程序员的工作效率往往是普通程序员的好几倍,尽管薪水上的差距不是这样。而大多数都是普通程序员,优秀程序员只属于“少数派”。优秀程序员,拿到需求,会做足够的分析,到了动手的时候,则像个流水线的工人;普通程序员,拿到需求就想赶快动手,面临的有可能是回炉重造。优秀程序员,会充分考虑到使用场景,采用防御式编程来规避可能带来的缺陷;普通程序员,想的只是实现需求,把程序健壮性扔给测试人员。优秀程序员,会考虑代码的可读性,为代码添加合适的注释、每个方法或函数的功能单一、清晰;普通程序员,急于求成,不注重代码规范,导致日后维护困难……


但是,追求效率和追求质量,大多数公司都会选择前者。但做多和做好,结果往往相差甚远。


大部分人倾向于做多、扩张、追求规模化。但殊不知做大的后果往往是成本的上升,利润却不一定变高。但做好却不一样,它追求的是平衡收支,而不是盲目追求利润。更好的做法其实是在做好之前,不要做大。要相信好产品,自然会带来口碑。过分追求大规模,反倒会使高利润远去。而把事情做好的心态,看似发展得慢,实则是条捷径。


回顾我创作的历程,之前的我总想着多写,多写就是扩张,意味着规模。但这种心态往往做不出好书,因为这是效率当先,质量次之的做法。但我身边也有的人,创作很用心,不着急让书早日面试,很认真地创作,比我的速度慢一些。这便是把事情做好的心态。你猜结果如何?人家一年十几万的稿酬,我却只有可怜的几万块。


所以,上面那套理论并不是我胡乱写的,或是从哪本书里看到,就抄过来的。而是真的付出了血和泪,总结出的道理。在此,我劝你做个“清醒”的人。追求效率没错,一旦做得过火,则会适得其反。


另一方面,如果只想成为大多数,可不可以呢?当然也可以,只不过互联网行业或许不再适合。那些符合正态分布的行业才是想成为大多数的那类人的理想去处。


比如,餐饮行业。现在,大家可以想一想,有没有那家餐馆,或是哪个餐饮品牌,能做到赢家通吃?似乎没有,如果也去查这方面的数据,就会发现餐饮行业其实并不是指数分布,而是呈正态分布的。只要能做到普通中位数的水平,就OK了。


真正的高手一般都是“少数派”。他们不仅能力拔群,思考问题时的方法、对世界的认知和一般人都有区别。若要成为软件开发工程师中的“高手”,必须成为“少数派”

作者:萧文翰
来源:juejin.cn/post/7214855127625302053
,成为战场上的传说。

收起阅读 »

22年回家,治好了我的精神焦虑,终于睡了一周的好觉

2023-1-29 脑海的片段:看灿姐打麻将,拍全家福的情景,在奶奶家吃饭,奶奶给我钱,看外婆,去老街,爬山去龙泉寺,妈妈的眼睛,妈妈打呼噜,电梯外的妈妈,大双给奶奶夹菜,小双给豪儿倒洗脚水,妈妈玩抖音,给妈妈染头发 过年回家,在高铁站等了3个小时,没有带孩子...
继续阅读 »

2023-1-29


脑海的片段:看灿姐打麻将,拍全家福的情景,在奶奶家吃饭,奶奶给我钱,看外婆,去老街,爬山去龙泉寺,妈妈的眼睛,妈妈打呼噜,电梯外的妈妈,大双给奶奶夹菜,小双给豪儿倒洗脚水,妈妈玩抖音,给妈妈染头发


过年回家,在高铁站等了3个小时,没有带孩子,两个人在哪里应该都过得挺好,带着孩子,还得管他吃喝,睡觉,妈耶,真累。坐上火车,第一次坐软卧,有门,两层,床比较大,有靠背,说是以前有拖鞋。但是也不够我和儿子睡的。在上铺又总感觉要掉下来。接着好几天没睡好,半夜醒来,心慌,涌出好多口水,想吐。有点吓着了,从来没这样过,真怕自己的心脏有毛病。而且老贺把40度左右的水给我泡酸辣粉,我不想他难受,吃了半碗。好像是到湖北的某个站,上来两个重庆人,很大声,后来那个中年男人还问晨晨几岁了,我才反应过来,这是重庆人,声大,但是每个人都还是热心肠的,看着两个人好像吵架一样,其实他们在聊天,哈哈哈


到了重庆,中午了,我实在是不想洗漱了,带孩子去哪里,都累。下车去了观音桥,看着外面好像不咋的,进去人挤人,全是好吃的,要不是等会要吃美娃鱼,我觉得我可以把每个都吃个遍。看见了红姐一家,聊天下来,我发现红姐也真的是为人母了,围着孩子转,会为了家里的琐事妥协,以前吐槽的老公,在她那也是很爱的,看得出来。可可很可爱,我能感受到一点有女儿的感觉,哈哈哈


2d87fb7c74cd7cd0443ae32725b3331.jpg


1f427cb7d2a0ac9ac3a0c0da1ee11ac.jpg


e4c382de28bf282462671199173e461.jpg
永川下车,打车回家就是8点多了,回家,一路上就别寻思钱就对了,哪哪都是钱。爸爸妈妈说是等我们吃饭,两年没回家了,每次回去,还是觉得就在昨天,周围一切在改变,又好像没变。到家,老妈就开始拍抖音,这是我没有想到的。不过她有自己的世界,乐趣,也挺好的。我说要吃夹沙肉,妈妈爸爸自己做的红豆馅的馅,应该花了不少时间,很好吃,就是不敢多吃了。家里很漂亮,妈妈开始像我展示各种东西,她买的好货,哈哈哈,儿子玩上了,外婆买的垃圾车。当然两个弟弟还是玩手机,看看啥时候开始有改变


晚上睡得很香,3个人都睡得很香,好几个月没有这种感觉了,睡完觉起来,世界都那么清晰,上了厕所就转进了妈妈的被窝。听妈妈说,听妈妈抱怨。第一天,我还是可以挺住的。妈妈更唠叨了,哈哈哈。在天津,是儿子一天800遍的叫妈妈,现在又多了800遍“莎莎”。事实证明,妈妈老了,需要有人关注了。


21号,30,过年,小插曲,对于我来说,不在意,都不算事儿,我只记得菜很好吃。特别是牛肉,下午吃到塞牙,扣了好久。上午买了烟花,买的时候,两只说不感兴趣,结果晚上玩的好嗨,明年一定要多买点


22号,初一,赶集,一家人去赶集,老爸先带去买气球了,一个可爱的草莓气球,贺小狗自己选的。赶集的人不多,没有以前热闹了,卖烟花的,开始卖纸钱,火炮了。走了一圈去火锅店吃牛肉火锅。本来没抱多大期待,结果好好吃,吃了一盘子牛肉。下午跟妈妈去田间摘了好多青棉花,奈何肚子天天不消化,不然还可以吃青团,下午出去走走,也是开心的,听妈妈吐槽爸爸,带儿子玩泥巴。妈妈还说拿钱给我还贷款,我很感动,我不要她的钱,来的很辛苦,自己留着点,以后养老不用愁。


d13c91f66201c308d98bce12f715a01.jpg


背砍很多青棉花


23号,初二,本来每年初二去舅舅家,今年改成了初三。晚起的早晨,睡得依旧的香。今天给妈妈染头发,爸爸做完饭,吃完打麻将去了,我给妈妈染头发,洗头发,还算成功,妈妈开心,我就开心。我们下午决定去走走,小米跟着我们,他要是知道要走8公里,肯定不会跟我们走了,去田间走走,好舒服。路过观音菩萨,土地公公,走了很多坟堆的山里,可惜没有转角走向龙泉寺。明年补上。我给观音菩萨求妈妈睡得好,希望有用。

2fbfd1d965b24ef0da89d0df4f6b330.jpg


去龙泉寺的路


94bd173a9f6501855b10341346dc7c4.jpg


老公说瓦房就是,天快黑了,没继续下去了


24号,初三,早起了,因为爸爸说早上8点出发,结果好像就我当真了,早起,洗头,我还做了一个瑜伽。我们坐车,爸爸妈妈骑车,社恐的我,喜欢热闹,又不知道怎么与人 交流。上坟,好像是我每年比较期待的事,大家聚在一起去看望已故的老人,热闹之余,想念外婆,从她走了起,我每次去她那,都会给她磕三个头,不是秀,是真想她。灿姐喜欢声张,我不在乎别人说什么,我只在乎我心里有她,磕不磕头,是我心里想做的,不是要别人看的。我以为这么久,我会忘记,好像越来越想她了。《寻梦环游记》里说,生的人,没有人记得你,已故的老人就会消失,所以我的外婆应该还没有,因为我还记得她。去了一直害怕的老宅子看,梦里那么真实,那么黑,和老公去看的时候,仿佛在逛博物馆,不害怕了,多了几分怀念。外面的大门没了,被杜老师占了修了栋房子,里面关着鸡,进门两边的棺材没了,因为老人都走了。天井小时候感觉蛮大,中间的台,要走好多步,现在几步就跨过去了。两边的台子没有了,左边的台子是用来洗衣服的,右边的台子是坐着旁边的老人,她天天会在那里叹息,撑唤(一身疼,叫唤),骂人,使唤她老头子。右边的大门锁上了,人家搬出去了,我还记得那户人家的男的姓曾,女的姓贾(重庆话就是真假)。桃屋(饭厅)也小了,就剩下被舅舅烧纸烧黑的外公的遗照,还有边上慈祥的外婆,一个香炉,几根没烧完的香。睡觉的屋,没有了床,都空了,墙被凿了一个大洞,屋里亮了,不吓人了。下面的大屋,中间的灯绳子还在,小时候跑进去,大概知道绳子在哪里,摸一把没摸着就要跑出去,太黑了,太害怕了。天井旁边的窗户还在,以前我和外婆在那砍红薯,熬猪食。厨房都没了,没了。路过一个空屋子就到了现在舅舅住的楼房,大坝子放了几张桌子,是往年的热闹场景。吃饭前,和老公去了小时候上学走的路,还是那个小河,竹林,再也不用在田坎上跑着去上学,却有点怀念跑着去上学的时候。路走不通了,上面成了一片橘子地。坐在坝子里吃饭,真香,踏实。她们打麻将去了,我和老公去了三姨对面的路,去老街,杜老师说对面可以走去老街。对面路不太熟悉,过河的桥还是玉子板。路修成了大道,唯一不好的是有好多野狗,不敢走,老奶奶说带路,不好意思让她带那么远,我们走了老奶奶说的另一条田间小路,看见了田里的老牛,也是惬意。我们拿着两根棍子走了一道。老街的小学,旁边修了新的,老的还在,是以前一样的,不知道那个像保剑锋,去他家排练,他把衣服内衣裤子扔门后面的语文老师还在那个学校不,哈哈哈哈。老街的东西,老建筑都垮了,转转去了新街,好近,以前好像要走很久,现在不到10分钟就到。我们吃了葱油饼和凉面,凉面是那个味,酸甜辣,4块钱一碗,太便宜了。

e6d9a0829d22c82e9555feeb58236eb.jpg


橘子地


e2b35aada3b72a52092e9e324643865.jpg


玉子板的桥,30多年了吧


微信图片_20230201133535.jpg


耕地的牛


微信图片_20230201133517.jpg


去老街路上


微信图片_20230201133549.jpg


中间的灯绳,小时候的噩梦


5bd221108042713193a315bede266f8.jpg


天井旁边的窗户


微信图片_20230201133556.jpg


有大洞的屋,不吓人了


微信图片_20230201133559.jpg


老街的折叠门,最早的折叠门了吧


25号,初四,今天我们家请客,来了好多人,爸爸总是能一个人做很多菜出来,还挺好吃,肚子不争气,吃不了多少,去帮妈妈修了戒指,没想到我的手指比她粗那么多,还是大了。回来看她们打麻将,大家一起聊了些有的没的,比我想的亲切多了,特别是灿姐,好像姐妹亲更多了些,还有就是小双,他甘愿给豪儿倒洗脚水,豪儿说热了,他赶紧给加凉水。我有点不解,我在想,是不是每个人都有自己的圈子,就是学数学的圈子,交集,并集那种圈子,我们在他的并集区域里,不在他的交集区域,有点意思。他应该是算我们家最任性的人,孩子这样挺好的,长大之前至少任性过,这才是小孩子。

26号,初五,定了快3年的全家福,终于要拍上了。早起的我洗头,敷脸。热饭,跟三姨聊了会,她总是能让人觉得她可怜,或许她就是有点可怜吧,但是她又是那么自私,喜欢占便宜。说我去年初一走,聪明,省了200块钱的事,翻篇了,我不计较了。我生怕爸妈弟弟有一点不配合,急眼,这个全家福拍不上。回家一直没有太照顾老公,我感觉他懂我,也确实很理解我,没陪他,他一切都安排的很好,这点我很开心。还好妈妈没有晕车,下车,我们爬了一会山,到了山腰的影楼。有点简陋,一个区里,也还行啦。没想到给我画那么久,我的初衷是让爸妈好看点,仔细点,毕竟下次再让他们来拍,估计很难。妈妈穿婚纱真美,但是我却好想哭,不知道为什么。妈妈是老了,眼睛开始耷拉下来了,生完一场大病,右眼掉的厉害,我让化妆师再贴一个双眼皮贴,对称一下。拍照的地方好冷,可别给我妈妈感冒了,对了,忘记说,中午他们吃泡面的时候,还蛮温馨的,哈哈哈。特别感谢我老公,在我化妆的时候,给他们拍的照片,很好,其实这就算是我的生日礼物了,真的就想拍全家福。两个弟弟真是长高了,小的胖的可爱,大的帅气,爸爸年轻,妈妈美丽。一路下来,挺开心的。爸爸应该也是开心的,我看出来他尴尬了,笑的有点不自然,逗一逗就笑的开心了。他还拿手机照了一些,也是开心的,一家人都笑着拍完一组,除了不配合的儿子,不强求他们拍第二组了。这样开心的收尾挺好的。妈妈担心没拍完,我说拍多了,他们照片多了加照片要多收钱,她就放心了。跟着老公这些年,我慢慢学会了,解释,沟通,交流,站在对方的角度,再有点耐心,其实妈妈就懂了,像个孩子。只是我们身上的琐事太多,耐心在消耗,跟家人,反而多了些任性。下山大家有说有笑,挺快就到了公交点,爸妈希望去店里看看,两个弟弟肯定是不想走了,开心收尾挺好的,我们回家了。到了爷爷家,由于大伯一家没有来,没人做饭,妈妈生气,爸爸就是踏实干活。锅很黑,煤气罐没有气了,拿着大锅上外面炒的。大双烧火,爸爸炒菜,我和妈妈端菜,奶奶找盘子,爷爷站屋里用有些黑的水洗碗,儿子拿着竹子块在坝子里玩,老公时而拍照录像,时而站在那看爷爷弯腰洗碗,他好奇87岁的老人,弯腰低头洗碗,居然头不晕,这就是劳动人民的腻害之处。别说,虽然到处是鸡屎,炒出来的菜,真的蛮香的。我们坐在浓浓鸡屎味,有些霉臭,旁边是洗完碗装着污水的盆,满桌子的菜,大双吃的很香,奶奶来了,他让他涮涮有葱的碗,给她夹了够不着的豆干,他是个善良的孩子,抛开玩手机不说,其实也是个懂事的孩子。老公吃了好多,喝了蜂蜜酒,好像是他爷爷,哈哈哈。我吃了半碗饭,豆干,豆角,和鱼里的菜,其实挺好吃的。别的不重要,爷爷笑咪了眼睛。我和他们的回忆不太好,没有什么深厚的感情,但是长大后,每次去他们那,会觉得温暖,淳朴。他们也希望我去看他们。那晚,妈妈很开心,睡得很香,我们一起睡得,她摆了一会龙门阵,说这么晚了,快睡觉了,明天要早起。开始打呼噜了。希望你每晚都睡那么香,多好。


28215f5ad2d11c633a1d2206471fe55.jpg


爷爷坝子里做饭


微信图片_20230201134322.jpg


准备拍全家福啦


微信图片_20230201134326.jpg


先偷拍一点


微信图片_20230201134329.jpg


双胞胎,一点不像的弟弟


微信图片_20230201134332.jpg


双胞胎,一点不像的哥哥


27号,初六,早上的票很早,以后得长教训,早点订票了。本以为这次中午的票,可以消停的走,但是我一点不着急,感觉肯定有办法,或许内心不想走吧,不然早就着急了。吃了3碗面,真香,可惜没吃上妈妈做的肥肠鱼,好多菜,看五一吧,能不能回去一趟。大概率不行。回去他们也不放假,在管子里也算不上陪伴了。妈妈每次走的时候,都会说,回来都是晚上,走的都是早上,烧白没吃上,肥肠没吃上,土鸭子鸭杂专门给你留的,也没吃上,冰箱里的笋子也没吃上。。。。即使我在家待一个月,也会那么说,就是舍不得我走,巴不得把家里能带的都给我装下,能装的我都装上了,那是妈妈的爱。晚走一小时,好像也没晚多少,妈妈送到电梯那,我知道她为什么不下去,不是懒,是她要哭。爸爸送我们到车站,走的时候,我把贺小狗做好了,赶紧摆了摆手,生怕车快了,爸爸看不到我了,他看着呢,一直盯着呢,没舍得走,直到车走远。。。。又要在车站等3个小时,带着这只狗,咋熬,冰淇淋只撑了十几分钟。老公问我想要什么生日礼物,我开玩笑的说我想回家,真的只是玩笑,不知道为什么留下了泪水,可能家在内心深处吧。

22年回家,治好了我的精神焦虑,终于睡了一周的好觉


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

从0搭建nestjs项目并部署到本地docker

web
开发目标:快速搭建nestjs项目本地环境,并测试本地打包方便后期部署到服务器。 项目准备:node环境、npm依赖、docker 创建项目并启动 使用typeorm连接mysql 使用class-validate校验入参 使用全局filter处理异常,使用...
继续阅读 »

开发目标:快速搭建nestjs项目本地环境,并测试本地打包方便后期部署到服务器。


项目准备:node环境、npm依赖、docker



  1. 创建项目并启动

  2. 使用typeorm连接mysql

  3. 使用class-validate校验入参

  4. 使用全局filter处理异常,使用全局interceptor处理成功信息

  5. 使用ioredis连接redis

  6. 使用swaager文档

  7. 使用docker-compose打包并运行

  8. 总结


一、创建项目并启动


1、全局安装nestjs并创建项目

npm i -g @nestjs/cli
nest new nest-demo

2、使用热更新模式运行项目

npm run start:dev

此时访问 http://localhost:3000就可以看到 Hello World!


3、使用cli一键生成一个user模块

nest g resource system/user

选择REST API和自动生成CURD


4、设置全局api前缀

src/main.ts


async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api'); // 设置全局api前缀
await app.listen(3000);
}
bootstrap();

更多nestjs入门教程查看:# 跟随官网学nestjs之入门


二、使用typeorm连接并操作mysq


1、安装依赖

npm i @nestjs/typeorm typeorm mysql @nestjs/config -S

2、在src下创建 config/env.ts 用来判断当前环境,抛出配置文件地址

src/config/env.ts


import * as fs from 'fs';
import * as path from 'path';
const isProd = process.env.NODE_ENV == 'prod';

function parseEnv() {
const localEnv = path.resolve('.env');
const prodEnv = path.resolve('.env.prod');

if (!fs.existsSync(localEnv) && !fs.existsSync(prodEnv)) {
throw new Error('缺少环境配置文件');
}

const filePath = isProd && fs.existsSync(prodEnv) ? prodEnv : localEnv;
return { path: filePath };
}
export default parseEnv();

3、在src下创建.env配置文件

src/.env


# default
PORT=9000

# database
DB_HOST=localhost
DB_PORT=3306
DB_USER=demo_user
DB_PASSWD=123456
DB_DATABASE=demo_db


4、在app.module内挂载全局配置和mysql

src/app.module.ts


import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigService, ConfigModule } from '@nestjs/config';
import envConfig from './config/env';
import { AppService } from './app.service';
import { UserModule } from './system/user/user.module';

@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // 设置为全局
envFilePath: [envConfig.path],
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
type: 'mysql',
host: configService.get('DB_HOST', 'localhost'), // 主机,默认为localhost
port: configService.get<number>('DB_PORT', 3306), // 端口号
username: configService.get('DB_USER', 'root'), // 用户名
password: configService.get('DB_PASSWORD', '123456'), // 密码
database: configService.get('DB_DATABASE', 'test_db'), //数据库名
entities: ['dist/**/*.entity{.ts,.js}'],
timezone: '+08:00', //服务器上配置的时区
synchronize: true, //根据实体自动创建数据库表, 生产环境建议关闭
autoLoadEntities: true,
}),
}),
UserModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

5、定义userEntity实体

src/system/user/entities/user.entity.ts


import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity('user_tb')
export class UserEntity {
@PrimaryGeneratedColumn()
s_id: string;

@Column({ type: 'varchar', length: 20, default: '', comment: '名称' })
s_name: string;

@Column({ type: 'int', default: 0, comment: '年龄' })
s_age: number;
}

6、user.module内引入entity实体

import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
// 引入typeorm和Enetiy实例
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserEntity } from './entities/user.entity';

@Module({
imports: [TypeOrmModule.forFeature([UserEntity])],
controllers: [UserController],
providers: [UserService],
}
)

export class UserModule {}

7、在控制器user.controller修改api地址

@Post('create')
create(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}

地址拼接为:全局前缀api+模块user+自定义create = localhost:3000/api/user/crtate


image.png


image.png


三、使用class-validato校验入参


1、安装依赖

npm i class-validator class-transformer -S

2、配置校验规则

src/system/user/dto/create-user.dto.ts


import { IsNotEmpty } from 'class-validator';

export class CreateUserDto {
@IsNotEmpty({ message: '名称不能为空' })
readonly s_name: string;
}

image.png


更多校验规则查看:git文档


四、使用filter全局错误过滤、interceptor全局成功过滤


1、使用cli自动生成过滤器


nest g filter common/http-exception
nest g interceptor common/transform

2、编写过滤器


src/common/http-exception/http-exception.filter.ts


import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
} from '@nestjs/common';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp(); // 获取请求上下文
const response = ctx.getResponse(); // 获取请求上下文中的 response对象
const status = exception.getStatus(); // 获取异常状态码

let resultMessage = exception.message;

// 拦截class-validate错误信息
try {
const exceptionResponse = exception.getResponse() as any;
if (Object.hasOwnProperty.call(exceptionResponse, 'message')) {
resultMessage = exceptionResponse.message;
}
} catch (e) {}

const errorResponse = {
data: null,
message: resultMessage,
code: '9999',
};

// 设置返回的状态码, 请求头,发送错误信息
response.status(status);
response.header('Content-Type', 'application/json; charset=utf-8');
response.send(errorResponse);
}
}

src/common/transform/transform.interceptor.ts


import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { map, Observable } from 'rxjs';

@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => {
return {
data,
code: '0000',
msg: '请求成功',
};
}),
);
}
}

3、在main.ts里挂载


import { HttpExceptionFilter } from './common/http-exception/http-exception.filter';
import { TransformInterceptor } from './common/transform/transform.interceptor';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter()); // 全局注册错误的过滤器(错误异常)
app.useGlobalInterceptors(new TransformInterceptor()); // 全局注册成功过滤器
await app.listen(3000);
}
bootstrap();

手动抛出异常错误只需在service的方法里


throw new HttpException('message', HttpStatus.BAD_REQUEST)


五、使用idredis连接redis


1、安装依赖

npm i ioredis -S

2、在.env文件添加reids配置

# redis
REDIS_HOST=localhost
REIDS_PORT=6379
REIDS_PASSWD=
REIDS_DB=3

3、在common目录下创建cache模块,连接redis

nest g mo cache common && nest g s cache common

src/common/cache/cache.service.ts


import { Injectable, Logger } from '@nestjs/common';
import { Redis } from 'ioredis';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class CacheService {
public client;
constructor(private readonly configService: ConfigService) {
this.getClient();
}

async getClient() {
const client = new Redis({
host: this.configService.get('REDIS_HOST', 'localhost'), // 主机,默认为localhost
port: this.configService.get<number>('REIDS_PORT', 6379), // 端口号
password: this.configService.get('REIDS_PASSWD', ''), // 密码
db: this.configService.get<number>('REIDS_DB', 3),
});
// 连接成功提示
client.on('connect', () =>
Logger.log(
`redis连接成功,端口${this.configService.get<number>(
'REIDS_PORT',
3306,
)}
`
,
),
);
client.on('error', (err) => Logger.error('Redis Error', err));

this.client = client;
}

public async set(key: string, val: string, second?: number) {
const res = await this.client.set(key, val, 'EX', second);
return res === 'OK';
}

public async get(key: string) {
const res = await this.client.get(key);
return res;
}
}

在cache.module内抛出service
src/common/cache/cache.module.ts


@Module({
providers: [CacheService],
exports: [CacheService],
})

4、在user.module内引入cacheModule并在user.service内使用

src/system/user/user.module.ts


import { CacheModule } from 'src/common/cache/cache.module';
@Module({
imports: [CacheModule],
controllers: [UserController],
providers: [UserService],
})

export class UserModule {}

src/system/user/user.service.ts


import { CacheService } from '@src/common/cache/cache.service';

@Injectable()
export class UserService {
constructor(
private readonly cacheService: CacheService,
) {}

async create(createUserDto: CreateUserDto) {
const redisTest = await this.cacheService.get('redisTest');

Logger.log(redisTest, 'redisTest');
if (!redisTest) {
await this.setRedis();
return this.create(createUserDto);
}

...
}
async setRedis() {
const res = await this.cacheService.set(
'redisTest',
'test_val',
12 * 60 * 60,
);
if (!res) {
Logger.log('redis保存失败');
} else {
Logger.log('redis保存成功');
}
}
}

image.png


image.png


六、使用swagger生成文档


1、安装依赖

npm i @nestjs/swagger swagger-ui-express -S

2、在main.ts引入并配置

import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 设置swaager
const options = new DocumentBuilder()
.setTitle('nest-demo example')
.setDescription('The nest demo API description')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('swagger', app, document);

...
}
bootstrap();

此时访问http://wwww.localhost:9000/swagge就可以看到文档


image.png


3、在控制器为业务模块和api打上标签

src/system/user/user.controller.ts


import { ApiTags, ApiOperation } from '@nestjs/swagger';

@ApiTags('user')
@Controller('user')
export class UserController {
constructor(private readonly userService: UserService) {}

@ApiOperation({
summary: '创建用户',
})
@Post('create')
create(@Body() createUserDto: CreateUserDto) {
return this.userService.create(createUserDto);
}
}

4、在dto内为字段设置名称

src/system/user/dto/create-user.dto.ts


import { ApiProperty } from '@nestjs/swagger';

export class CreateUserDto {
@ApiProperty({ type: 'string', example: '用户名称' })
@IsNotEmpty({ message: '名称不能为空' })
readonly s_name: string;

@ApiProperty({ type: 'number', example: '用户年龄' })
readonly s_age: number;
}

这时刷新浏览器,就能看到文档更新了


image.png


更多swaager配置查看:官方文档


七、使用docker-compose自动部署到本地docker


1、在根目录下创建docker-compose.yml

version: "3.0"

services:
# docker容器启动的redis默认是没有redis.conf的配置文件,所以用docker启动redis之前,需要先去官网下载redis.conf的配置文件
redis_demo: # 服务名称
container_name: redis_demo # 容器名称
image: daocloud.io/library/redis:6.0.3-alpine3.11 # 使用官方镜像
# 配置redis.conf方式启动
# command: redis-server /usr/local/etc/redis/redis.conf --requirepass 123456 --appendonly yes # 设置redis登录密码 123456、--appendonly yes:这个命令是用于开启redis数据持久化
# 无需配置文件方式启动
command: redis-server --appendonly yes # 开启redis数据持久化
ports:
- 6379:6379 # 本机端口:容器端口
restart: on-failure # 自动重启
volumes:
- ./deploy/redis/db:/data # 把持久化数据挂载到宿主机
- ./deploy/redis/conf/redis.conf:/usr/local/etc/redis/redis.conf # 把redis的配置文件挂载到宿主机
- ./deploy/redis/logs:/logs # 用来存放日志
environment:
- TZ=Asia/Shanghai # 解决容器 时区的问题
networks:
- my-server_demo

mysql_demo:
container_name: mysql_demo
image: daocloud.io/library/mysql:8.0.20 # 使用官方镜像
ports:
- 3306:3306 # 本机端口:容器端口
restart: on-failure
environment:
MYSQL_DATABASE: demo_db
MYSQL_ROOT_PASSWORD: 123456
MYSQL_USER: demo_user
MYSQL_PASSWORD: 123456
MYSQL_ROOT_HOST: '%'
volumes:
- ./deploy/mysql/db:/var/lib/mysql # 用来存放了数据库表文件
- ./deploy/mysql/conf/my.cnf:/etc/my.cnf # 存放自定义的配置文件
# 我们在启动MySQL容器时自动创建我们需要的数据库和表
# mysql官方镜像中提供了容器启动时自动docker-entrypoint-initdb.d下的脚本的功能
- ./deploy/mysql/init:/docker-entrypoint-initdb.d/ # 存放初始化的脚本
networks:
- my-server_demo

server_demo: # nestjs服务
container_name: server_demo
build: # 根据Dockerfile构建镜像
context: .
dockerfile: Dockerfile
ports:
- 9003:9003
restart: on-failure # 设置自动重启,这一步必须设置,主要是存在mysql还没有启动完成就启动了node服务
networks:
- my-server_demo
depends_on: # node服务依赖于mysql和redis
- redis_demo
- mysql_demo

# 声明一下网桥 my-server。
# 重要:将所有服务都挂载在同一网桥即可通过容器名来互相通信了
# 如nestjs连接mysql和redis,可以通过容器名来互相通信
networks:
my-server_demo:

2、在根目录创建Dockerfile文件

FROM daocloud.io/library/node:14.7.0

# 设置时区
ENV TZ=Asia/Shanghai \
DEBIAN_FRONTEND=noninteractive
RUN ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime && echo ${TZ} > /etc/timezone && dpkg-reconfigure --frontend noninteractive tzdata && rm -rf /var/lib/apt/lists/*

# 创建工作目录
RUN mkdir -p /app

# 指定工作目录
WORKDIR /app

# 复制当前代码到/app工作目录
COPY . ./

# npm 源,选用国内镜像源以提高下载速度
RUN npm config set registry https://registry.npm.taobao.org/

# npm 安装依赖
COPY package.json /app/package.json
RUN rm -rf /app/package-lock.json
RUN cd /app && rm -rf /app/node_modules && npm install

# 打包
RUN cd /app && rm -rf /app/dist && npm run build

# 启动服务
# "start:prod": "cross-env NODE_ENV=production node ./dist/src/main.js",
CMD npm run start:prod

EXPOSE 9003

3、修改.env.prod正式环境配置

# default
PORT=9003
HOST=localhost

# database
DB_HOST=mysql_demo #使用容器名称连接
DB_PORT=3306
DB_USER=demo_user
DB_PASSWD=123456
DB_DATABASE=demo_db

# redis
REDIS_HOST=redis_demo #使用容器名称连接
REIDS_PORT=6379
REIDS_PASSWD=
REIDS_DB=3

4、修改main.ts启动端口

import { ConfigService } from '@nestjs/config';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
const configService = app.get(ConfigService); // 获取全局配置
const PORT = configService.get<number>('PORT', 9000);
const HOST = configService.get('HOST', 'localhost');
await app.listen(PORT, () => {
Logger.log(`服务已经启动,接口请访问:http://wwww.${HOST}:${PORT}`);
});
}
bootstrap();

5、前台运行打包

docker-compose up


运行完成后大概率会报错,因为我们使用的mysql账号没有权限,所以需要进行设置


image.png


// 进入mysql容器命令
docker ecex -it mysql_demo /bin/bash
// 登录mysql
mysql -uroot -p123456
// 查询数据库后进入mysql查询数据表
show databases;
use mysql;
show tables;
// 查看user表中的数据
select User,Host from user;
// 刚创建的用户表没有我们设置连接的用户和host,所以需要创建
CREATE USER 'demo_user'@'%' IDENTIFIED BY '123456';
// 给创建的用户赋予权限
GRANT ALL ON *.* TO 'demo_user'@'%';
// 刷新权限
flush privileges;

如果还报错修改下密码即可
Pasted Graphic 1.png


ALTER USER 'demo_user'@'%' IDENTIFIED WITH mysql_native_password BY '123456';

此时项目应该能正常启动并成功访问


image.png


image.png


6、切换后台运行

// Ctrl+C 终止程序后执行后台运行命令
docker-compose up -d

八、总结


docker-compose up正常用来测试本地打包,和第一次构建redismysql容器,后续需要在本地运行开发模式只需保证redismysql容器正常运行即可,如需再次打包,删除server容器和镜像再次执行即可


docker ps -a // 查询docker容器
docker rm server_demo // 删除server容器
docker images // 查询镜像
docker rmi nest-demo_server_demo // 删除server镜像, server镜像名称:项目名称_容器名称
docker-compose up -d // 重新打包

本地开发模式只需关闭server容器,然后在项目内只需 start:dev即可


docker stop server_demo
npm run start:dev

作者:jjggddb
来源:juejin.cn/post/7215844385614528549
收起阅读 »

keepAlive模式下切换页面时缓存页面中el-select已展开的选项框无法自动关闭解决方案

web
问题描述 如下图,在keepAlive缓存的页面中使用element中的select选择器,打开弹出框后不手动关闭,直接切换页面,会出现弹出框仍然展示在页面上的现象。 问题原因 select选择器提供一个属性 popper-append-to-body 为...
继续阅读 »

问题描述


如下图,在keepAlive缓存的页面中使用element中的select选择器,打开弹出框后不手动关闭,直接切换页面,会出现弹出框仍然展示在页面上的现象。


select-bug.gif


问题原因



  1. select选择器提供一个属性 popper-append-to-body 为false时,弹出框是放置在select选择器所在层级中,为true时,允许将弹出框插入至body元素中。


image.png


image.png



  1. 本页面被keepAlive缓存后 再切出本页面时不会触发select选择器组件的blur事件


所以当弹出框被插入至body元素中时,切出缓存页面 无法触发select选择器组件的blur事件,弹出框在body中无法隐藏


解决方法1


设置属性 popper-append-to-body为false,弹出框不会直接插入至body元素中,页面切换后弹出框也会被隐藏


局限性:
某些场景需要设置select选择器上级元素超出隐藏,弹出框如果超出上级元素的范围则无法完全展示


image.png


解决方法2


elementselect选择器源码中弹出框开启关闭由变量visible控制,将elselect组件包装一下,在deactivated生命周期钩子里设置弹出框关闭,注册组件时


image.png


// SelectWrapper 组件
<script lang="ts">
import { Mixins, Component, Watch } from 'vue-property-decorator';
import { Select } from 'element-ui';

@Component({
name: 'ElSelect',
})
export default class ElSelect extends Mixins(Select) {
visible: boolean | undefined;

deactivated() {
this.visible = false;
}
}
</script>

入口文件全局注册新的SelectWrapper组件,替换掉elementselect选择器,这样可以做到在业务组件中无感使用


 app.component('el-select', SelectWrapper);

作者:Eden的前端笔记
来源:juejin.cn/post/7215855138812461115
收起阅读 »

iframe之间的通信

web
前言 iframe 想必大家都挺熟悉的了,就不多说了👍👍。写这篇文章的初衷主要是丰富自己的知识和解决遇到的问题。因为我基本上没接触过 iframe ,所以对它的通信方式不是很了解。前几天,跟我的一个朋友(在下杨公子)聊天时,他提到了 iframe 的通信方式,...
继续阅读 »

前言


iframe 想必大家都挺熟悉的了,就不多说了👍👍。写这篇文章的初衷主要是丰富自己的知识和解决遇到的问题。因为我基本上没接触过 iframe ,所以对它的通信方式不是很了解。前几天,跟我的一个朋友(在下杨公子)聊天时,他提到了 iframe 的通信方式,我觉得很有意思,就开始了解和学习。在这篇文章中,我将分享我所学到的内容,希望对大家有所帮助🤪🤪。


接下来我们就一起来学习一下关于 iframe 通信的相关知识吧😁


iframe通信的几种方式😶‍🌫️😶‍🌫️



  1. URL 传参:父窗口可以通过在 iframe 的 src 属性后添加参数来向子窗口传递数据,子窗口可以通过 location.searchlocation.hash 来获取参数✨✨。



  • 使用 ? 拼接参数,子页面使用 location.search 接收参数


// parent.html
<iframe id="iframe1" src="./child1.html?name=来自parent的消息" frameborder="0"></iframe>

// child1.html
<script>
console.log(window.decodeURIComponent(location.search)) // ?name=来自parent的消息
</script>



  • 使用 # 拼接参数,子页面使用 location.hash 接收参数,同时还可以使用 window.onhashchange 来监听参数的变化。


// parent.html
<iframe id="iframe1" src="./child1.html#name=来自parent的消息" frameborder="0"></iframe>
<script>
const iframe1 = document.getElementById('iframe1')
// 在2s后更改hash
setTimeout(() => {
iframe1.src = './child1.html#age=12'
}, 2000)
</script>


// child1.html
<script>
console.log('hash', window.decodeURIComponent(location.hash)) // #name=来自parent的消息
window.onhashchange = () => {
console.log('hashchange', window.location.hash) // #age=12
}
</script>



⚡⚡需要注意的是通过 URL 传参 的时候,传输携带中文的话,记得使用 decodeURIComponent 进行解码。




  1. window.postMessage:安全、可靠且支持跨域的 iframe 通信方式,它可以在两个窗口之间异步传递消息✨✨✨✨✨。



  • 在发送方中,使用 window.postMessage() 方法向另一个窗口发送消息。该方法接收两个参数:要发送的消息和目标窗口的源(例如,"http://127.0.0.1:5500/child.html" 或 "*")。


window.postMessage('Hello world!', 'http://127.0.0.1:5500/child.html')


  • 在接收方中,使用 window.addEventListener() 方法监听 message 事件。该事件对象包含三个属性:data 表示接收到的数据,origin 表示发送方的源,source 表示发送方窗口的引用。


window.addEventListener('message', function(event) {
// 判断消息是否来自可信任的源
if (event.origin === 'http://127.0.0.1:5500/child.html') {
console.log('message: ' + event.data)
}
})

兼容性,来自 window.postMessage | MDN


image.png



  1. window.name:可以使用一个隐藏的iframe和window.name属性在不同的窗口之间共享数据✨✨。



  • 在子页面中,将要传递给父页面的数据保存在 window.name 属性中。


例如


window.name = 'Hello Parent!';


  • 在父页面中,创建一个隐藏的 iframe 元素,并且将其源设置为子页面的 URL


const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = 'http://127.0.0.1:5500/child1.html';
document.body.appendChild(iframe);


  • 在父页面中,等待 iframe 加载完成后,通过访问 iframe.contentWindow.name 属性来获取子页面中保存的数据。


iframe.onload = function() {
const childData = iframe.contentWindow.name;
onsole.log('message:', childData); // 输出:message: Hello Parent!
};


⚡⚡注意:使用 window.name 进行跨域 iframe 通信存在安全性问题,因为所有具有相同名称的窗口都可以访问和修改 window.name




  1. 服务器端转发:可以将消息从一个iframe发送到服务器,然后再由服务器将其转发到另一个iframe。✨✨✨



博客主要记录一些学习的文章,如有不足,望大家指出,谢谢。



作者:树深遇鹿
来源:juejin.cn/post/7215854856731934781
收起阅读 »

女朋友想学webGL修图,安排!

web
前言 看完小白可以用webgl实现修图功能!我们平常生活中都使用过adobe photoshop修图,各种各样的滤镜以及特效眼花缭乱,实现高斯模糊,雕刻,曝光等这些特效看起来似乎很难,那么今天我们来手敲一个简单实现。 之前讲了简单的webgl的原理与点的绘制、...
继续阅读 »

前言


看完小白可以用webgl实现修图功能!我们平常生活中都使用过adobe photoshop修图,各种各样的滤镜以及特效眼花缭乱,实现高斯模糊,雕刻,曝光等这些特效看起来似乎很难,那么今天我们来手敲一个简单实现。


之前讲了简单的webgl的原理与点的绘制、以及webgl在vscode需要注意的点,本文将接着介绍如何做个简单的修图功能,由于篇幅有限,只讲基本的语法、多边形绘制、缓冲区、帧缓存、纹理uv等。


预览


chrome-capture-2023-2-30.gif


canvas也可以更简单的实现,getImageData可以得到点的集合,然后putImageData绘制就行了。但是一些复杂的算法,例如高斯模糊、雕刻效果,貌似就没有webgl灵活了。


createBuffer 缓冲区


缓冲区你可以理解canvas的save()保存状态,但是这里我们一般是点的集合,这里我不会讲具体的api细节,但是知道具体的代码流程就行,就是创建buffer及数据 -> 绑定数据 -> 如何加载


let bufferOrigin = gl.createBuffer()
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW)
gl.bindBuffer(gl.ARRAY_BUFFER, bufferOrigin);

gl.enableVertexAttribArray(positionAttributeLocation); // 告诉缓冲区怎么加载
gl.vertexAttribPointer(positionAttributeLocation, size, type, normalize, stride, offset);

Program 对象


const canvas = document.querySelector("#canvas");
image.width = 540
image.height = 720

canvas.style.width = 540 + 'px'
canvas.style.height = 720 + 'px'

const gl = canvas.getContext("webgl");
if (!gl) {
return;
}
const program = webglUtils.createProgramFromScripts(gl, ["vertex-shader-2d", "fragment-shader-2d"]);

image加载的dom对象,设置宽高,webglUtils是封装的方法,其实就是之前的初始化的着色器,返回program程序对象。


shader 着色器


先看看着色器源码


<script id="vertex-shader-2d" type="x-shader/x-vertex">
attribute vec2 a_position; // attribute在顶点着色器处理
attribute vec2 a_texCoord; // 纹理参数
uniform vec2 u_resolution; // 页面的坐标
attribute vec4 a_composeColor; // 纹理增强的向量
varying vec4 v_composeColor;

void main() {
// 屏幕坐标 -> 裁剪坐标
vec2 zeroToOne = a_position / u_resolution;
vec2 zeroToTwo = zeroToOne * 2.0;

vec2 clipSpace = zeroToTwo - 1.0;

gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
v_composeColor = a_composeColor;
v_texCoord = a_texCoord;
}
</script>

attribute类型用于顶点着色器的属性,一般可以在后期动态添加一些控制,但是uniform是只能静态编译的时候就决定了,所以一般用于控制材质、光照等确定的值。为了能控制到片元着色器,那么一定要使用varying这个类型,一般通过变量传递给片元着色器做动态的渲染,所以一般会配合attriubute + varying


<script id="fragment-shader-2d" type="x-shader/x-fragment">
precision mediump float;

uniform sampler2D u_image;
uniform vec2 u_textureSize;
uniform float u_kernel[9];
uniform float u_kernelWeight;
varying vec2 v_texCoord;
varying vec4 v_composeColor;

void main() {
vec2 onePixel = vec2(1.0, 1.0) / u_textureSize;
// 卷积内核的前置处理,u_kernel我们传递的核心数据
vec4 colorSum =
texture2D(u_image, v_texCoord + onePixel * vec2(-1, -1)) * u_kernel[0] +
texture2D(u_image, v_texCoord + onePixel * vec2( 0, -1)) * u_kernel[1] +
//.....
// 计算最终的颜色结果
gl_FragColor = vec4((colorSum / u_kernelWeight).rgb, 1) * v_composeColor;
}
</script>


这里返回的结果gl_FragColor就是最终绘制的颜色。注意颜色范围是0到1需要做个转换,这里的最核心的也就是卷积的算法


卷积


卷积就是一个 3×3 的矩阵, 矩阵中的每一项代表当前处理的像素和周围8个像素的乘法因子, 相乘后将结果加起来除以内核权重(内核中所有值的和或 1.0 ,取二者中较大者)


image.png


像素矩阵 * 修改矩阵 = 赋值于内核也就是中心位置


这也就是我们能处理模糊、锐化等特效的原理, 下面是简单的计算


// 将周围八个点相加用于平均数相除
function computeKernelWeight(kernel) {
const weight = kernel.reduce(function(prev, curr) {
return prev + curr;
});
return weight <= 0 ? 1 : weight;
}

 gl_FragColor = vec4((colorSum / u_kernelWeight).rgb, 1) * v_composeColor;

滤镜


 const kernelsFilter = {
sharpness: {
name: '锐度',
data: [
0, -1, 0,
-1, 5, -1,
0, -1, 0
],
},
gaussianBlur: {
name: '高斯模糊',
data: [
0, 1, 0,
1, 1, 1,
0, 1, 0
],
},
edgeDetect2: {
name: '反相',
data: [
-1, -1, -1,
-1, 8, -1,
-1, -1, -1
],
},
emboss: {
name: '浮雕效果',
data: [
-2, -1, 0,
-1, 1, 1,
0, 1, 2
],
},
};

// 向量乘积的滤镜
const composeFilter = {
light: {
name: '曝光',
data: new Float32Array([1.2, 1.2, 1.2, 1])
},
langmanmeigui: {
name: '浪漫玫瑰',
data: new Float32Array([1.1, 1, 1, 1])
},
// ....
}

将上面的参数传入对上面的着色器,然后通过卷积赋值于gl_FragColor,这样简单的修图工具就大功告成了。


texcoord 纹理


const texcoordLocation = gl.getAttribLocation(program, "a_texCoord");
// ...
gl.vertexAttribPointer(texcoordLocation, size2, type2, normalize2, stride2, offsetVal2);

这里用缓冲区处理,本质图片也就是4个点的矩形,因为每个点其实对应像素和位置, 下面是创建纹理的标准代码,将image传入到textImage2D,然后将缓冲区绑定到这样我们就可以绘制纹理了


 // webgl创建纹理,并设置基本纹理参数,载入image图片
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);

//定义纹理处理能力
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);


帧缓冲


如何给图片施加多种状态的叠加效果,也就是图片 -> 纹理一 -> 纹理一 + 纹理二 -> 画布,那么我们需要用到帧缓冲,其实就是通过不断的bindTexture来覆盖之前的状态。


// 绘制帧缓冲
function drawFrames () {
const originTexture = createAndSetupTexture(gl)
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
let textures = []
let frameBuffers = []
const kernelsFilterList = ['gaussianBlur', 'emboss', 'boxBlur',
'gaussianBlur', 'boxBlur', 'gaussianBlur', 'boxBlur', 'gaussianBlur'] //叠加效果的数组

for (let i = 0; i < kernelsFilterList.length; i++) {
let texture = createAndSetupTexture(gl)
textures.push(texture)

gl.texImage2D(
gl.TEXTURE_2D, 0, gl.RGBA, image.width, image.height, 0,
gl.RGBA, gl.UNSIGNED_BYTE, null);

var fBuffer = gl.createFramebuffer()
frameBuffers.push(fBuffer)
gl.bindFramebuffer(gl.FRAMEBUFFER, fBuffer);
// 绑定纹理到帧缓冲
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
}
gl.bindTexture(gl.TEXTURE_2D, originTexture);

for (var i = 0; i < kernelsFilterList.length; i++) {
setFramebuffer(frameBuffers[i], image.width, image.height);
drawWithKernel(kernelsFilterList[i]);
// 叠加
gl.bindTexture(gl.TEXTURE_2D, textures[i]);
}

// 绘制
setFramebuffer(null, canvas.width, canvas.height);
drawWithKernel("normal");

function setFramebuffer (fbo, width, height) {
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); // 绑定帧缓存
gl.uniform2f(resolutionLocation, width, height); // 设置到裁剪坐标
gl.viewport(0, 0, width, height); // 将裁剪坐标自适应到屏幕坐标
}
}


总结


通过基本的语法、纹理使用、帧缓存等,我们对webgl的基本的2d图形处理有了一定的认知,正常在绘制三角形,四边形,圆形,我们都可以使用缓存区,最后drawArrays绘制,在一些图形的渲染需要保存之前的状态的时候,我们可以使用帧缓存处理。关于当前页面的优化,当前的修图页面应该将各种调色分到不同的glsl文件,同样我们也可以做裁剪,上传图片编辑并下载。



如果觉得文章对你有帮助,不要忘了一键三连 👍



附录



  1. 内卷年代,是该学学WebGL了 - 掘金 (juejin.cn)

  2. 为什么我的WebGL开发这么丝滑 🌊 - 掘金 (juejin.cn)


作者:谦宇
来源:juejin.cn/post/7215977393696522299
收起阅读 »

屡次wx.getLocation申请被拒,让ChatGPT帮我编写申请理由后立马通过

微信小程序完成某些需求功能需要获取精准经纬度,需要微信小程序后台-开发管理-接口设置,申请“获取当前的地理位置、速度(wx.getLocation)”,一直审核不通过,有的人甚至提交了10多次都没有通过,如果你没有掌握方法,及时申请几个月还是一样不给通过。 包...
继续阅读 »

微信小程序完成某些需求功能需要获取精准经纬度,需要微信小程序后台-开发管理-接口设置,申请“获取当前的地理位置、速度(wx.getLocation)”,一直审核不通过,有的人甚至提交了10多次都没有通过,如果你没有掌握方法,及时申请几个月还是一样不给通过。


包括本人也是申请数次未通过,思考是否是理由不够充分,借助ChatGPT帮助我丰富申请理由,我用ChatGPT的回答来申请,立马审核通过。以下是模拟对ChatGPT的提问和回复。
1679989283893.jpg


提问


这是一个社区团购的微信小程序,由于团购服务点功能,需要精准定位到最近的服务点,因此需要wx.getLocation接口申请。请你基于我的需求,编写一个充分的申请原因,并包含操作步骤说明。


ChatGP回答


申请原因:


该社区团购微信小程序需要使用wx.getLocation接口获取用户的地理位置信息,以便精准定位用户所在地区,并显示最近的服务点。服务点根据用户所在地区的需求,提供更加贴近实际的商品和服务,确保用户的用户体验。该接口可以通过用户授权的方式获得用户所在位置信息,不会泄露用户的隐私信息。


操作步骤:


1. 首先在微信小程序中调用wx.getLocation接口进行定位,用户将会收到授权提示,需同意授权以便应用获取用户的地理位置信息。

2.定位成功后,小程序将通过得到的经纬度信息,计算用户位置与服务点之间的距离,并返回最近的服务点信息给用户。

3. 如果用户不同意授权,将无法获取用户地理位置信息,定位功能将无法使用,导致无法提供精准的服务点查找。

4.在使用wx.getLocation接口时,需要注意用户个人隐私问题,小程序应在提供提醒并遵守相关规定的前提下,妥善保护用户隐私信息。


接口申请总结


ChatGPT的回答固然帮助我们丰富了申请原因,但是申请该接口的基本要点还是要掌握。



  1. 确保小程序的服务类目可申请wx.getLocation,最好是主营类目,包含两个类目。

  2. 原因表达充分,主要以用户体验角度,及用户隐私。

  3. 提交的图片、视频,符合申请的步骤。


最后


ChatGPT是自然语言处理工具,需要充分的表达清楚,才可能接近输出我们需要的答案,且我们能够甄别验证它的输出。


其他
# 经验分享:快速通过“获取当前的地理位置、速度(wx.getLocation)接口”审核


作者:ZTrainWilliams
来源:juejin.cn/post/7215465880884674619
收起阅读 »

往往排查很久的问题,最后发现都非常简单。。。

之前线上发生了一个很诡异的异常,网上各种搜索、排查,都没有找到问题,给大家分享一下。 大概在 2 月份的时候,我们的某个应用整合了中间件的 kafka 客户端,发布到灰度和蓝节点进行观察,然后就发现线上某个 Topic 发生了大量的RetriableCommi...
继续阅读 »

之前线上发生了一个很诡异的异常,网上各种搜索、排查,都没有找到问题,给大家分享一下。


大概在 2 月份的时候,我们的某个应用整合了中间件的 kafka 客户端,发布到灰度和蓝节点进行观察,然后就发现线上某个 Topic 发生了大量的RetriableCommitException,并且集中在灰度机器上。


E20:21:59.770 RuntimeException  org.apache.kafka.clients.consumer.RetriableCommitFailedException  ERROR [Consumer clientId=xx-xx.4-0, groupId=xx-xx-consumer_[gray]] Offset commit with offsets {xx-xx-xx-callback-1=OffsetAndMetadata{offset=181894918, leaderEpoch=4, metadata=''}, xx-xx-xx-callback-0=OffsetAndMetadata{offset=181909228, leaderEpoch=5, metadata=''}} failed org.apache.kafka.clients.consumer.RetriableCommitFailedException: Offset commit failed with a retriable exception. You should retry committing the latest consumed offsets.
Caused by: org.apache.kafka.common.errors.TimeoutException: Failed to send request after 30000 ms.


排查


检查了这个 Topic 的流量流入、流出情况,发现并不是很高,至少和 QA 环境的压测流量对比,连零头都没有达到。


但是从发生异常的这个 Topic 的历史流量来看的话,发生问题的那几个时间点的流量又确实比平时高出了很多。



同时我们检查 Broker 集群的负载情况,发现那几个时间点的 CPU 负载也比平时也高出很多(也只是比平时高,整体并不算高)。



对Broker集群的日志排查,也没发现什么特殊的地方。


然后我们对这个应用在QA上进行了模拟,尝试复现,遗憾的是,尽管我们在QA上把生产流量放大到很多倍并尝试了多次,问题还是没能出现。


此时,我们把问题归于当时的网络环境,这个结论在当时其实是站不住脚的,如果那个时刻网络环境发生了抖动的话,其它应用为什么没有这类异常?


可能其它的服务实例网络情况是好的,只是发生问题的这个灰实例网络发生了问题。


那问题又来了,为什么这个实例的其它 Topic 没有报出异常,偏偏问题只出现在这个 Topic 呢?。。。。。。。。。


至此,陷入了僵局,无从下手的感觉。


从这个客户端的开发、测试到压测,如果有 bug 的话,不可能躲过前面那么多环节,偏偏爆发在了生产环境。


没办法了,我们再次进行了一次灰度发布,如果过了一夜没有事情发生,我们就把问题划分到环境问题,如果再次出现问题的话,那就只能把问题划分到我们实现的 Kafka 客户端的问题了。


果不其然,发布后的第二天凌晨1点多,又出现了大量的 RetriableCommitFailedException,只是这次换了个 Topic,并且异常的原因又多出了其它Caused by 。


org.apache.kafka.clients.consumer.RetriableCommitFailedException: Offset commit failed with a retriable exception. You should retry committing the latest consumed offsets.
Caused by: org.apache.kafka.common.errors.DisconnectException
...
...
E16:23:31.640 RuntimeException  org.apache.kafka.clients.consumer.RetriableCommitFailedException  ERROR 
...
...
org.apache.kafka.clients.consumer.RetriableCommitFailedException: Offset commit failed with a retriable exception. You should retry committing the latest consumed offsets.
Caused by: org.apache.kafka.common.errors.TimeoutException: The request timed out.

分析


这次出现的异常与之前异常的不同之处在于:



  1. 1. Topic 变了

  2. 2. 异常Cause变了


而与之前异常又有相同之处:



  1. 1. 只发生在灰度消费者组

  2. 2. 都是RetriableCommitFailedException


RetriableCommitFailedException 意思很明确了,可以重试提交的异常,网上搜了一圈后仅发现StackOverFlow上有一问题描述和我们的现象相似度很高,遗憾的是没人回复这个问题:StackOverFlow。


我们看下 RetriableCommitFailedException 这个异常和产生这个异常的调用层级关系。



除了产生异常的具体 Cause 不同,剩下的都是让我们再 retry,You should retry Commiting the lastest consumed offsets。



从调用层级上来看,我们可以得到几个关键的信息,commit 、 async。


再结合异常发生的实例,我们可以得到有用关键信息: 灰度、commit 、async。


在灰度消息的实现上,我们确实存在着管理位移和手动提交的实现。



看代码的第 62 行,如果当前批次消息经过 filter 的过滤后一条消息都不符合当前实例消费,那么我们就把当前批次进行手动异步提交位移。结合我们在生产的实际情况,在灰度实例上我们确实会把所有的消息都过滤掉,并异步提交位移。


为什么我们封装的客户端提交就会报大量的报错,而使用 spring-kafka 的没有呢?


我们看下Spring对提交位移这块的核心实现逻辑。



可以同步,也可以异步提交,具体那种提交方式就要看 this.containerProperties.isSyncCommits() 这个属性的配置了,然而我们一般也不会去配置这个东西,大部分都是在使用默认配置。



人家默认使用的是同步提交方式,而我们使用的是异步方式。


同步提交和异步提交有什么区别么?


先看下同步提交的实现:



只要遇到了不是不可恢复的异常外,在 timer 参数过期时间范围内重试到成功(这个方法的描述感觉不是很严谨的样子)。



我们在看下异步提交方式的核心实现:



我们不要被第 645 行的 RequestFuture future = sendOffsetCommitRequest(offsets) 所迷惑,它其实并不是发送位移提交的请求,它内部只是把当前请求包装好,放到 private final UnsentRequests unsent = new UnsentRequests(); 这个属性中,同时唤醒真正的发送线程来发送的。



这里不是重点,重点是如果我们的异步提交发生了异常,它只是简单的使用 RetriableCommitFailedException 给我们包装了一层。


重试呢?为什么异步发送产生了可重试异常它不给我们自动重试?


如果我们对多个异步提交进行重试的话,很大可能会导致位移覆盖,从而引发重复消费的问题。


正好,我们遇到的所有异常都是 RetriableCommitException 类型的,也就是说,我们把灰度位移提交的方式修改成同步可重试的提交方式,就可以解决我们遇到的问题了。


作者:艾小仙
来源:juejin.cn/post/7214398563023274021
收起阅读 »

既当产品又当研发,字节大哥手把手带我追求极致

在学校的时候,计算机相关专业的同学应该都或多或少都被“大作业”折磨过,没有为“大作业”熬过夜通过宵的大学生活可以说是不完整的。步入公司后才发现,校园里的“大作业”就像玩具一样,需求明确、解决方案明确、最终产品效果明确、甚至还有前人的作品可以参考,而在公司里要做...
继续阅读 »

在学校的时候,计算机相关专业的同学应该都或多或少都被“大作业”折磨过,没有为“大作业”熬过夜通过宵的大学生活可以说是不完整的。步入公司后才发现,校园里的“大作业”就像玩具一样,需求明确、解决方案明确、最终产品效果明确、甚至还有前人的作品可以参考,而在公司里要做的东西,上面说的特点至少有一个不具备,甚至通通不具备。


而我在字节实习的过程中,所经手的恰恰就是这么一个需求不明确、解决方案不明确、最终产品效果不明确的项目。整个过程中有过焦头烂额毫无进展的时刻也有过欲哭无泪的时刻,还好有我的mentor带着我一路披荆斩棘、过关斩将。


首先和大家讲一下项目背景,当时我在的组是视频会议移动端,经历了近三年大流感的洗礼,相信大家对于视频会议中可能遇到的各种问题如数家珍,包括但不限于没声了、没音了、没画面了、画面卡顿、画面不清晰、画面和语音不同步、同步屏幕时闪退等等等等。作为一个服务企业级的B端产品,出现以上问题时就可能会投诉,然后经过客户成功部门转手到运营再转手到研发这里,研发就需要判断分析一下究竟是我们产品的原因、还是客户本身设备的问题、或者是第三方环境的因素,当用户的量级上来后,这样的客诉就会很多,会严重占用oncall的研发人员的时间以及精力。


我的mentor,一个专注于解决问题、避免重复劳动的人,一个字节范我觉得有E+的人,一个虽然身处移动端但是前后端甚至网络也都会的人,觉得这样很不OK,应该有个工具,能够自动的分析出来客户究竟遇到了什么问题,分析不出来的再找研发进行排查。没有这个工具也不影响业务开发的进展,所以整个项目并不存在时间上的紧迫性,但是呢,有这个工具做出来后肯定会大大降低研发的开发时间,所以项目的必要性还是有的。于是,我作为刚入职的实习新人,这个项目就交给我来做了。


而我,一个还没有从校园中完全出来的新兵蛋子,说实话面对这样的场面是一脸懵逼的,对于要做啥、要怎么做可以说是一无所知,我的mentor在我入职后,让我先了解了解背景,第一周就带着我oncall了,让我知道都可能有样的客诉,手把手给我演示他们正常的排查问题的方式。先了解客户反馈的情况,然后捞出来客户对应时间的设备信息以及设备日志。


说实话,作为一个新人,或者说我本身对于项目有一种畏难心理,碰到一点难题就总是想着往后拖,或者摆烂先不管然后就搁置在一边不想再问津了,但是我的mentor是一个有着坚定信念的人,差不多就是见山开山,见水架桥这种,遇到问题会主动找到相关人员一起解决,可以说就是有那种主人翁,项目owner的意识。于是,我就跟在他的后面,和整个团队的不同角色沟通他们遇到问题时排查的思路,试图总结出来一种通用的流程。在过程中,难免有许多困难,我的第一反应是退缩,但是导师的第一反应是拉会拉上相关人员一起讨论,看看用什么方式可以解决。比如在如何确定设备日志和故障表现的映射关系时,先后调研了多种方式看看相关团队有没有类似的做法以及他们最后实现的效果,包括大数据机器学习、代码状态流转图、自定义规则引擎等多种方式,最后调研后决定采用自定义规则引擎的方式。在实现需求的时候,需要其他团队协作时,他总是直接向前提出自己的需求,而我向一个陌生人发消息之前总要做一些心理建设,总是在担心些什么、害怕些什么,而事实上大家都是打工人,谁也不比谁厉害多少,对方不配合那就拉+1进群一起看看,解决不了就向上暴露问题。


于是,导师披荆斩棘在前,我在后面跟着实现他的设想。我们很快就做出来了第一个版本。通过Python自动化下载设备日志,然后正则匹配筛选出含有特定标记的日志,并对他们的出现频率次数做出判断。因为Python是解释型的语言,所以可以把规则直接写成python语言,用eval或者exec函数进行执行。第一个版本做出来后,导师又积极的带着我去给其他人宣传我们的这个工具。然后根据他们的反馈继续进行相关改进,最后我离职前实现的效果就是@ 一个群里的机器人,告诉他出现问题的ID,他就能自动化的拉下来日志进行排查,然后告诉你他分析的结果,整个交互非常的方便。


一个成功的项目必须要有一个负责的owner,我的导师就向我展示了一个优秀的owner是如何一步步解决问题、排除项目中的难关,如今我也正式成为一名打工人,希望我也能早日如他一般自如的面对工作。


我是日暮与星辰之间,出道两年半的Java选手,相信时间的力量,

作者:日暮与星辰之间
来源:juejin.cn/post/7211801284709138493
一起成为更好的自己!

收起阅读 »

硬盘坏了,一气之下用 js 写了个恢复程序

web
硬盘坏了,一气之下写了个恢复程序 师傅拯救无望 硬盘已经寄过去超过一周了,一问竟然是还没开始弄??? 再过一周,上来就问我分几个区?我要恢复哪些数据?我要恢复的数据在哪个位置? 那好吧,既然给了钱师傅也都放弃了,我也没什么好寄托希望的了。况且经过这三个...
继续阅读 »

硬盘坏了,一气之下写了个恢复程序


师傅拯救无望


硬盘已经寄过去超过一周了,一问竟然是还没开始弄???


2023-03-24-14-15-16.png


再过一周,上来就问我分几个区?我要恢复哪些数据?我要恢复的数据在哪个位置?


2023-03-24-14-18-50.png


2023-03-24-14-19-30.png


2023-03-24-14-20-05.png


那好吧,既然给了钱师傅也都放弃了,我也没什么好寄托希望的了。况且经过这三个星期的缓解,心情已经平复了很多,就像时光,回不来了就是回不来了。


自救之路


在把硬盘寄过去的时间里,等待师傅的修复结果的时间里,我并没有闲着(在摸鱼)。


经过调研,数据恢复方法通常有:



  • 硬件损坏,对坏的盘进行修复

  • 误删或逻辑错误等,文件扫描修复

  • git 重置恢复


很明显,这些都不适用于我现在的场景。因为师傅能不能修好是未知的,我只是数据盘没了,系统盘还在。由于 vscode 的数据目录空间占比较小,就没有搬迁到数据盘里,这刚好可以为恢复代码提供了可能。


这是因为新版 vscode 有一个时间线功能,这个时间线数据是默认存储在用户目录下的。


我从 C:/Users/love/AppData/Roaming/Code/User/History 目录中确实找到了很多名为 entries.json 的文件,结构如下:


{
// 配置版本
"version": 1,
// 原来文件所在位置
"resource": "file:///d%3A/git2/cloudcmd/.madrun.mjs",
// 文件历史
"entries": [
{
// 历史文件存储的名称
"id": "YFRn.mjs",
"source": "工作区编辑",
// 修改的时间
"timestamp": 1656583915880
},
{
"id": "Vfen.mjs",
"timestamp": 1656585664751
},
]
}

通过上面的文件大概可以看到,每一个时间点的文件都保存在另一个随机命名的文件里。而网上的方法基本都是自己一个个手动到目录里去根据最新的 id 去找对应的文件内容,然后创建文件并把内容复制出来。


这个过程恢复一两个文件还好,但我这可是要恢复整个 git 工作区,大概有几十个项目上千个文件。


这时候当然是在网上找找有没有什么 vscode 数据恢复 相关的工具,很遗憾找了大半天都没有找到。


气死我了,一气之下就自己写个!


恢复程序开发步骤


毕竟只要数据在磁盘上,无非就是一个文件读取操作的问题,还要拿在这水文章,见谅见谅。


首先考虑需求:



  • 我要实现一个自动扫描 vscode 数据目录

  • 然后以原始的目录结构还原出来,不需要我自己去创建文件夹和文件

  • 如果还原的文件最新的那份不是我想要的,我还能根据时间线进行对比和选择

  • 扫描出来有N个项目时,我可以指定只还原某此项目

  • 我可以搜索文件、目录名或文件内容进行还原

  • 为了方便,我还要一个看起来不太丑的操作界面


大概就上面这些吧。


然后考虑实现:


我要实现一个自动扫描 vscode 数据目录


要的就是我自己连数据目录和恢复地址也不需要填写,就能自动恢复的那种。那么就让程序来自动查找数据目录。经过调研,各版本的 vscode 的数据目录一般保存在这些地方:


参考: stackoverflow.com/a/72610691


  - win -- C:\Users\Mark\AppData\Roaming\Code\User\History
- win -- C:\Users\Mark\AppData\Roaming\Code - Insiders\User\History
- /home/USER/.config/VSCodium/User/History/
- C:\Users\USER\AppData\Roaming\VSCodium\User\History

大概有上面这些路径,当然不排除使用者故意把默认位置修改掉这种边缘情况,或者使用者就只想扫描某个数据目录的情况,所以我也要支持手动输入目录:


  let { historyPath, toDir } = req.body
const homeDir = os.userInfo().homedir
const pathList = [
historyPath,
`${homeDir}/AppData/Roaming/Code/User/History/`,
`${homeDir}/AppData/Roaming/Code - Insiders/User/History/`,
`${homeDir}/AppData/Roaming/VSCodium/User/History`,
`${homeDir}/.config/VSCodium/User/History/`,
]
historyPath = (() => {
return pathList.find((path) => path && fs.existsSync(path))
})()
toDir = toDir || normalize(`${process.cwd()}/re-store/`)

然后以原始的目录结构还原出来……


这就需要解析扫描到的时间线文件 entries.json 了。我们先把解析结果放到一个 list 中,以下是一个完整的解析方法。


然后再把列表转换为树型,与硬盘上的状态对应起来,这样便于调试数据和可视化。


function scan({ historyPath, toDir } = {}) {
const gitRoot = `${historyPath}/**/entries.json`

fs.existsSync(toDir) === false && fs.mkdirSync(toDir, { recursive: true })
const globbyList = globby.sync([gitRoot], {})

let fileList = globbyList.map((file) => {
const data = require(file)
const dir = path.parse(file).dir
// entries.json 地址
data.from = file
data.fromDir = dir
// 原文件地址
data.resource = decodeURIComponent(data.resource).replace(
/.*?\/\/\/(.*$)/,
`$1`
)
// 原文件存储目录
data.resourceDir = path.parse(data.resource).dir
// 恢复后的完整地址
data.rresource = `${toDir}/${data.resource.replace(/:\//g, `/`)}`
// 恢复后的目录
data.rresourceDir = `${toDir}/${path
.parse(data.resource)
.dir.replace(/:\//g, `/`)}
`

const newItem = [...data.entries].pop()
// 创建文件所在目录
fs.mkdirSync(data.rresourceDir, { recursive: true })
const binary = fs.readFileSync(`${dir}/${newItem.id}`, {
encoding: `binary`,
})
fs.writeFileSync(data.rresource, binary, { encoding: `binary` })
return data
})

const tree = pathToTree(fileList, { key: `resource` })
return tree
}

为了方便,我还要一个看起来不太丑的操作界面


我们要把文件树的形式展示出来,还要方便切换。后面决定使用 macos 的文件管理器风格,大概如下。


image.png


如果还原的文件最新的那份不是我想要的,我还能根据时间线进行对比和选择


理论上这里应该要做一个像 vscode 对比文件那样,有代码高亮功能,并且把有差异的字符高亮出来。


实际上,这个需求得加钱。


2023-03-24-15-09-25.png


由于界面是在浏览器里的,需要自动打开,浏览器与系统交互需要一个接口,所以我们使用 opener 来自动打开浏览器。


使用 get-port 来自动生成接口服务的端口,避免使用时出现占用。


  const opener = require(`opener`)
const { portNumbers, default: getPort } = await import(`get-port`)
const port = await getPort({ port: portNumbers(3000, 3100) })
const server = express()
server.listen(port, `0.0.0.0`, () => {
const link = `http://127.0.0.1:${port}`
opener(link)
})

封装成工具,我为人人


理论上我根本不需要什么 UI 界面,也不需要配置,因为我的文件都恢复出来了我还花时间去搞毛线?


实际上,万一别人也有这个恢复文件的需要呢?那么他只要运行下面这条命令代码就能立刻恢复到当前目录啦!


npx vscode-file-recovery

这就是恢复后的文件在硬盘里的样子啦:


2023-03-24-15-22-23.png


所有代码位于:



建议收藏,以备不时之需。/手动狗头


作者:程序媛李李李李李蕾
来源:juejin.cn/post/7213994684262826040
收起阅读 »

Android开发小技巧-屏幕常亮与高亮的管理

前言 在使用我们国民应用微信和支付宝的时候,打开付款码给别人扫码的时候,那个页面简直亮瞎我的眼,做为一个Android开发者,我就想这个功能是怎么实现的呢? 问题:如何实现屏幕的常量与亮度控制呢?又有哪些方式来实现呢? 一、WakeLock机制 说起应用程序A...
继续阅读 »

前言


在使用我们国民应用微信和支付宝的时候,打开付款码给别人扫码的时候,那个页面简直亮瞎我的眼,做为一个Android开发者,我就想这个功能是怎么实现的呢?


问题:如何实现屏幕的常量与亮度控制呢?又有哪些方式来实现呢?


一、WakeLock机制


说起应用程序App的耗电,其本质就是硬件的消耗电量,硬件耗电单元分为CPU、基带、GPU、WIFI、BT、GPS、LCD/OLED等等。


耗电量的层级 基带非通话时间的能耗基本上在 5mA 左右, 而CPU只要处于非休眠状态,能耗至少在 50mA 以上,GPU执行图形运算时会更高, 另外 LCD/OLED, GPS等硬件又更高。


一般手机待机时,CPU、LCD、WIFI均进入休眠状态,这时 Android 中应用程序的代码也会停止执行,只会有基带处理器的耗电。 这也就是为什么微信比短信相比更加的耗电,答案就是短信使用基带耗电小, 而微信使用CUP耗电大。


Android 为了确保应用程序中关键代码的正确执行,提供了 WakeLock 的API,使得应用程序有权限通过代码阻止CPU进入休眠状态。


WakeLock 阻止应用处理器 CPU 的挂起,确保关键代码的运行,通过中断唤起应用处理器 CPU,可以阻止屏幕变暗。所有的 WakeLock 被释放后,系统会挂起。


例如以下音乐播放器,我们申请了 WakeLock 的情况下,就算按下电源键锁屏了,我们的音乐还是会播放。CPU不会休眠照样会处理我们的应用。


但是如果我们不释放 WakeLock,或者滥用 WakeLock 就会导致电池续航尿崩,用户查看电池消耗查看你的 App 耗电量高就会卸载了。


除了阻止 CPU 休眠,WakeLock 还可以让屏幕常亮,通过设置对应的 levelAndFlags 即可实现,常用的几个levelAndFlags:



  • PARTIAL_WAKE_LOCK:保持CPU 运转,屏幕和键盘灯有可能是关闭的。

  • SCREEN_DIM_WAKE_LOCK:保持CPU 运转,允许保持屏幕显示但有可能是灰的,允许关闭键盘灯

  • SCREEN_BRIGHT_WAKE_LOCK:保持CPU 运转,保持屏幕高亮显示,允许关闭键盘灯

  • FULL_WAKE_LOCK:保持CPU 运转,保持屏幕高亮显示,键盘灯也保持亮度

  • ACQUIRE_CAUSES_WAKEUP:不会唤醒设备,强制屏幕马上高亮显示,键盘灯开启。有一个例外,如果有notification弹出的话,会唤醒设备。

  • ON_AFTER_RELEASE:WakeLock 被释放后,维持屏幕亮度一小段时间,减少 WakeLock 循环时的闪烁情况


怎么使用呢?先声明权限


    <uses-permission android:name="android.permission.WAKE_LOCK"/>

然后直接使用:


    override fun init() {
val powerManager = commContext().getSystemService(Service.POWER_SERVICE) as PowerManager
wakeLock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "My Lock")

//是否需计算锁的数量
wakeLock.setReferenceCounted(false)

}

override fun onResume() {
super.onResume()
wakeLock.acquire()

}

override fun onStop() {
super.onStop()
wakeLock.release();
}

这样就可以实现一个简单的屏幕常亮的控制了。


虽然是可以实现逻辑,但是按照上面说的,谷歌可能也是怕我们滥用,导致手机续航尿崩,然后甩锅到Android系列上,说Android系列辣椒续航不行什么什么的,谷歌老早就标记过时了,并提供了新的 Api 实现此功能 SCREEN_ON。


二、KEEP_SCREEN_ON


有两种方式设置,一种是xml内部设置,另一种是通过Activity的window添加flag来设置


    <LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:keepScreenOn="true"
android:orientation="vertical"/>



或者oncreate方法中添加flags


    override fun init() {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}

两种方法都能实现,并且是和Activity生命周期绑定的,当我们退出这个页面就可以退出常量的状态,使用起来也是非常的方便。


那如果是这样一样场景,比如我们使用单Activity+多Fragment的方式,我们需要为其中一个Fragment设置为常亮,切换Fragment的时候动态的切换亮度,那怎么办?


我们需要动态的开关这个flag


window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);


又或者找到跟视图动态设置keepScreenOn属性 setKeepScreenOn 也是可行的,下面我们会以window的方式做一个工具封装类。


二、最大亮度的设置


最大亮度可以通过当前 Activity 的 window 对象设置 windowLayoutParams ,设置对应的 screenBrightness 值即可实现。


我们可以实现一个工具类来控制常亮的开关和最大亮度的开关,特别适用于单Activity+多Fragment的使用场景。


public class ScreenUtils {

/**
* 获取屏幕宽度
*/

public static int getScreenWidth(Context context) {
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
int width = wm.getDefaultDisplay().getWidth();
return width;
}


/**
* 获取屏幕高度
*/

public static int getScreenHeith(Context context) {
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
int height = wm.getDefaultDisplay().getHeight();
return height;
}


/**
* 是否使屏幕常亮
*
* @param activity 当前的页面的Activity
*/

public static void keepScreenLongLight(Activity activity, boolean isOpenLight, boolean maxBrightness) {

Window window = activity.getWindow();
if (isOpenLight) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}

WindowManager.LayoutParams windowLayoutParams = window.getAttributes();
windowLayoutParams.screenBrightness = maxBrightness ?
WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_FULL : WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE;
window.setAttributes(windowLayoutParams);
}
}

使用的时候在页面显示的时候常亮且最大亮度即可,然后我们可以在任意地方关闭这些设置。


    override fun onResume() {
super.onResume()

ScreenUtils.keepScreenLongLight(this, true, true)
}


override fun onStop() {
super.onStop()

ScreenUtils.keepScreenLongLight(this, false, false)
}

最大亮度打开页面简直亮瞎我的眼!



OK,完结!



作者:newki
来源:juejin.cn/post/7130424225147387935
收起阅读 »

十六进制常量还有这种玩法

前言 上一篇文章中 juejin.cn/post/715437… ,在源码解析阶段,那些判断16进制的地方,很有意思,加上我以前也写过一篇关于这个的文章 http://www.jianshu.com/p/bff2b84ca… ,所以想在这里做个分享。 状态变量...
继续阅读 »

前言


上一篇文章中 juejin.cn/post/715437… ,在源码解析阶段,那些判断16进制的地方,很有意思,加上我以前也写过一篇关于这个的文章 http://www.jianshu.com/p/bff2b84ca… ,所以想在这里做个分享。


状态变量的一般写法


可能有些朋友平时在开发时定义常量状态是这样定义的:


public static final int SEX_BOY = 0; // 男生
public static final int SEX_GIRL = 1; // 女生

然后看了某篇文章之后,某个经验丰富的说,定义常量时最好使用16进制,再去看了看Android某些类的源码,嗯,好像里面定义常量确实是使用了16进制,于是之后写代码就开始


public static final int SEX_BOY = 0x00; // 男生
public static final int SEX_GIRL = 0x01; // 女生
public static final int SEX_OTHER = 0x02; // 其它

比如说有很多个状态,就从0x01、0x02......0xff 这样列举下去。


这样写对吗?我随便找个源码来举例下,随便从View.java扣出一段代码


e60a26626531e0f7231f1858adc1518.png


为什么是这样定义呢,为什么不是像我们那种写法?


叠加状态的定义方式


其实这个直接说不好解释,跟着我去操作,就理解为什么要这么定义了。


假设我们定义状态,定义成这样


public static final int TYPE1 = 0x01;
public static final int TYPE2 = 0x02;
public static final int TYPE3 = 0x04;
public static final int TYPE4 = 0x08;
public static final int TYPE5 = 0x10;
public static final int TYPE6 = 0x20;
public static final int TYPE7 = 0x40;
public static final int TYPE8 = 0x80;

为什么这么写呢?

我们将16进制转成2进行,上面就对应成


0d39ccd1271903a3cc6f461d2b03217.png


有意思的就在这里,我先说的我想的过程中错误的一个思路 (我觉得挺有意思的,所以可以说下,因为是一个错误的思路,如果不想看可以直接跳看下面的这样定义的原因)


二进制从右往左来说

(1)我用第一位表示性别 000:女 001:男

(2)我用第二位表示角色 000:学生 010:老师

(3)我用第三位表示班级 000:A班 100:B班

那么 “A班的女老师” 我可以表示成 010 = 2

“A班的男老师” 可以表示成 011=3

“B班的女学生”可以表示成 100 .....

这样可以组成8个状态而不会冲突,但是这样的做法是只能用3个状态组合进行比较,而且单个状态下有000表示了3种,而且这种做法同一位上只能表示两种状态,假如我加个C班,那就没辙了。


然后换了一种思考的方法,假如我这样表示状态


public static final int TYPE1 = 0x01;  // 女
public static final int TYPE2 = 0x02; // 男
public static final int TYPE3 = 0x04; // 学生
public static final int TYPE4 = 0x08; // 老师
public static final int TYPE5 = 0x10; // 主任
public static final int TYPE6 = 0x20; // A班
public static final int TYPE7 = 0x40; // B班
public static final int TYPE8 = 0x80; // C班

那么 使用二进制的或运算:

“A班的女老师” 我可以表示成 TYPE6|TYPE1|TYPE4 = 0010 1001 = 41

“A班的男老师” 可以表示成 TYPE6|TYPE2|TYPE4 = 0010 1010 = 42

“B班的女主任”可以表示成 TYPE7|TYPE1|TYPE5 = 0101 0001 = 81

这样也能把多个状态组成一个状态,而且组合状态也能和单个状态进行同等级判断,并且这种做法不会产生重复的状态。


举个例子就是说你平时写


if(性别==女 && 角色 == 老师 && 班级 == A班){
......
}else if(版本 == C班){
......
}

如果用我这种方法定义状态的话,你只用写


if(type == 0x29){
......
}else if(type == 0x80){
......
}

可能有些人说就仅仅为了这样?那我写&&还好过,写成16进制转换转的我脑壳疼。我还不如多写几个&&,而且这样也更容易让别人看懂。
但这个写法不仅仅有这种好处,再举个例子,假如在很多个组合的状态中你需要去判断这个状态是“男”还是“女”等等,多状态下判断单状态多了,也不是说乱,但会写很多代码,但是现在可以直接这样写


public void switchSex(type){
if(type & 0x03 == 0x01){
// 是女生
}else{
// 是男生
}
}

就可以直接这样用二进制的与运算来实现判断。

我也仅仅是举了两个例子,我的意思是这样去定义十六进制常量,方便二进制做运算,二进制还有其他的运算呢,我仅仅举了“或”和“与”,还有什么异或啊,移位啊之类的,而且就算作用不大,按装逼来说,我直接做二进制的运算肯定比你那些乱七八糟的运算来得快吧。


总结


当然这只是我领悟的一种思路,而且我想很多人也知道这种做法,或者用16进制来定义常量不仅仅有这个好处,只是我觉得很有意思,所以想分享一下。


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

“勇敢的去走自己的夜路”——走出我的“浪浪山”

引子 2022年,经历过太多太多的故事,也发生了太多太多的事故。 这一年,迷途失措且努力,未来可期却恍惚,我错失了太多的机会,幸然遇到了大家,让我们一同努力见证Cool(小陈同学)的改变,这一年我经历过比赛失利,国奖失之交臂,也遇到了求职季的滑铁卢。但有幸的是...
继续阅读 »

引子


2022年,经历过太多太多的故事,也发生了太多太多的事故。


这一年,迷途失措且努力,未来可期却恍惚,我错失了太多的机会,幸然遇到了大家,让我们一同努力见证Cool(小陈同学)的改变,这一年我经历过比赛失利,国奖失之交臂,也遇到了求职季的滑铁卢。但有幸的是遇到了一堆可爱的掘友,利用掘金的资源也找到了一个工作。


这一年,我走了很远的路,吃了很多的苦,踩了很多的坑,才将这份年终总结交付与星球大伙。也曾有幸与掘友一起分享只属于我们的“情书”


第一节:对你,同样对自己说


今天是 2023年1月1日,这一年,半分努力,半分浑噩,忽隐忽现的理想,支撑着自己踽踽独行。几年前,他应征入伍,算不上什么好选择,也或许并没有选择的权利。北方干冷的空气,窗前停驻的麻雀,以及战友豪迈的言语曾一度让我觉得,南京或许会是我最终的归宿。


在南京的第二个年头,这一年我21周岁,报国的赤心和热血似乎都正热时,我做出了人生的第一个计划,“退役复学”。


感恩军旅生活,让我真正的热爱祖国与持续学习,在二零年上旬,新冠疫情爆发了,一个八十八线小城市的我,除了紧张的气氛外,到也没受到多大的影响,在家依旧忙碌,直到2022年2月10日,我记得非常清楚,写了一天前端(三件套的弱鸡)代码的我,结束了当天的笔记小结,打开了B站,悄然间随机看见了关注了好久的鱼皮居然真的开了学习圈子(编程导航),这让一个对编程说不上爱的萌新,从此爱上了coder与share,(一个利他的博主谁又能不爱呢?),曾经把编程视为作业的我,我发现我能用他code出一个全新的世界,我便一发不可收拾爱上了它(这里的它指的是编程)。


(一)身体是革命的本钱


but 「熬夜 + 不规律的作息 + 不健康的饮食」+ 「年轻」= 无事发生


“年轻人”,似乎总是有一种得天独厚的优势,有精力,有体力。而这对于我这个退役选手更是easy了,这些不太好的习惯也似乎在年轻一代的大水潭中泛不出多少涟漪,凭借着这份“本钱” ,自然能更加心安理得,反正:我还年轻,我还可以熬。


(2) 继续战斗,也请先照顾好自己


疫情消耗掉了大半年的时间,大学断断续续的锻炼,把熬夜换成了早起,开始按时吃早餐,解封后的日子,趁着南方冬天来的很晚,与几个战友开始了跑步的活动,这一阶段体能上确实有了很大的提升,我很享受跑步后,被风吹过的感觉(皮一下:我也曾吹过未来女朋友曾吹过的风)。


说来也很神奇,每次当我没什么精神,只要去跑步,回来冲个澡就会精神百倍,所以我一般傍晚的时候有空就会去跑跑,然后就可以再晚上全身心的写代码,整理笔记(当然最后就是发到星球上面,感受大家阅读后的“指责与指导,哈哈哈”)。


运动本不应被当做一种应该做的任务,而应被看作一种休闲的方式,没必要与别人比较强度,组别,只有自己舒服就是最好的标准。


所以,不管是真的热爱也罢,苦于生计也罢,即使继续战斗也请先照顾好自己


(二) 随波逐流只会让你接近平均值


普通人的危机感总来自他人,而想要成为一个优秀的人,危机感必须来自自身,随波逐流只会让你靠近平均值,总有一种恍惚感,懂得越多,越觉得自己像这个世界的孤儿,与同龄人格格不入,总是自负的认为他人幼稚,就像鲁迅先生说过:“人类的悲欢并不相通......”。听着他们谈论着我 “早就走过的路”,“早就见过的风光”,我也只觉得他们吵闹。


可惜,我在某些时候,总是小气的,心中惰于学习,更不愿将自己的 “财富” 与他人分享,总忧虑别人以己为石,跳向远方,患得患失的一种矛盾,让自己无奈又颓靡。
后来我遇到了鱼皮,我发现分享的乐趣后我便不再随波逐流,持续性努力,以下是我在星球这一年输出的笔记



(ps:请大佬过目,记得留赞)如下:



大数据笔记:wx.zsxq.com/dweb2/index…


运维Devops笔记:wx.zsxq.com/dweb2/index…


低代码Lowcode笔记:wx.zsxq.com/dweb2/index…


yarn的学习:wx.zsxq.com/dweb2/index…


软件设计师:wx.zsxq.com/dweb2/index…


NodeJS笔记:wx.zsxq.com/dweb2/index…


机器学习方面:wx.zsxq.com/dweb2/index…


Vue+pinpa笔记:wx.zsxq.com/dweb2/index…


MySQL笔记:wx.zsxq.com/dweb2/index…


华为鸿蒙认证:wx.zsxq.com/dweb2/index…


软件工程笔记:wx.zsxq.com/dweb2/index…


力扣刷题攻略:wx.zsxq.com/dweb2/index…


ES6模块暴露笔记:wx.zsxq.com/dweb2/index…


ACM算法思维导图:wx.zsxq.com/dweb2/index…


Bootstrap笔记:wx.zsxq.com/dweb2/index…


网络安全资源贴:wx.zsxq.com/dweb2/index…


(三) 不被枯井遮住双眼,保持谦虚及自尊


目光短浅带来的问题是致命的,当你有一天觉得自己好像还不错,好像已经登到峰顶了。那就需要反思一下自己或许已经陷入了“枯井”中,你会这样想,那大概率是被枯井遮住了双眼,你看不到枯井之外的世界,为了一点点成就就沾沾自喜,虽然阶段性的成功也很值得高兴,但千万不要走进这份舒适区中,温水煮青蛙的例子也不少见,走出枯井后,你就会发现外面的世界还是在一个枯井中,你要做的就是不断的往上爬。


永远不要看不起任何人,即使一位在你眼中普普通通的人,他的技术或许逊色你不少,但是他在思想和创造性上总能给你意想不到的惊喜。即使我的学校很普通,但是我的身边仍然有着一批充满韧劲的朋友,希望能通过考研,亦或者对于技术的钻研,弥补自己高考的遗憾,我记得大二那年,我常常在凌晨一点半两点收到微信弹窗大家一起交流一些问题。备战比赛的三点一线生活,学技术的通宵达旦,为了目标不断努力,这样的人仍然值得我尊重与学习,我认为他们拥有了一名大学生应该有的“灵魂”


除此之外,请千万保持自尊,自尊并非别人给的,而是自己给的,如果遇到比自己弱的人就有“自尊”,遇到比自己强的人就畏畏惧惧,没有“自尊”,那么这种自尊就没有任何意义了,闻道有先后,术业有专攻,应当尊重任何在某个方向的前辈,但是也没必要过于拘束,见贤思齐,见不贤而内自省即可。


(四) 远离总是给你负面情绪的人


但是如果你遇到了一些人,总时时刻刻,在学习以及生活上给你一些负面的观点,这种人会严重影响你坚定往枯井上爬的信念,不管你们是什么关系,我给你一个建议——赶快跑(这里现代化的叫法喊:润),有多快,跑多快,如果你们不幸要发生必要的交互,请将这段关系限制在最小范围内,切勿投入感情


(五) 传道授业:若要学知识,必得为人师


这一年我很喜欢读一本书,那就是《软技能:代码之外的生存指南》(下面我会提到)其中有一个章节给我印象很深,即第33章,传道授业:若要学知识,必得为人师,下面我摘了一段:



在你传道授业的时候都会发生什么 当我们初次接触某个课题的时候,我们对于自己对此了解多少往往都会高估。我们很容易自欺欺人,以为已经对某样东西 了如指掌,直到我们试着去教会别人的时候,才能发现事实并非如此。你有没有被别人问过一个非常简单的问题,却震惊地发现自己不能清晰地解答。你刚开始会说:“这个,很明显……”,接下来只有“哦……”。这种情况在我身上屡屡发生。我们自认为已经透彻理解了这个话题,实际上我们只是掌握了表面知识。这就是传道授业的价值。在你的知识集合里面,总有一部分知识你并没有理解透彻到可以向别人解释,而“教”的过程能够迫使你面对这一部分。作为人类,我们的大脑善于模式识别。我们能够识别模式,并且套用这些固定的模式去解决许多问题, 而没有做到“知其然”也“知其所以然”。这种肤浅的理解力无碍于我们完成工作,因而不易被察觉。然而一旦我们试着向别人解释某件事情的运作原理或背后的原因的时候,我们在认知上的漏洞就会暴露出来。不过这并非坏事。我们需要知道自己的弱点,然后才能对症下药。在教别人的时候,你迫使自己面对课题中的难点,深入 探索,从只知皮毛变成完全理解。学习是暂时的,而理解是永久性的。我可以背诵九九乘法表,但是一旦理解了乘法的运算原理,即使突然记性不好,我也可以重做一张乘法表。



我已经记不清很多年前我初中亦还是我高中的一位任课老师曾经说过这么一句话:能教会别人,自己也就没问题了。可惜那个时候的自己压根没提起学习的欲望,当然了,也或许与我自己根本不喜欢枯燥的应试教育有点关系。我也没理解这句话的意思。大学这几年,我很喜欢与朋友交流技术方面的事情,每个人都有很多我意想不到的理解与想法。还有更多时候我更加喜欢帮助朋友解决一些问题,当你什么时候可以将别人的一个问题,用通俗的解释说明 + 简洁却又富有代表意义的实例 + 补充一些自己的理解与看法,说给别人听得时候,最起码,我认为你对于这块内容就真的入门了。当你能够滔滔不绝的讲解给别人一块内容,能合理的安排讲解的引子与顺序的时候,这说明这一块的知识已经在你脑海中有了一条清晰的体系。同时你通过与别人交流的时候,再根据别人对你提供的一些方向好好反思斟酌一下,不断的修改。相信我,当你成功与他人讲解/交流你的知识后,你会爱上这种感觉的。


但是老板和老师可不会等你,很多时候我们都不得不 “填鸭子” 式的学习一些内容,例如根据老师的要求,强制使用一些指定的框架或者技术,或者根据业务/项目组长的需要和安排,你需要快速的学习一些你并不熟悉的内容,凭借我们多年 “应试” 的本事,大家总能很快的就找到这种套路,例如怎么快速搭建环境,怎么配置,如何快速的用起来。但是千万别止步于此,不然终究只是一个CRUD工程师,这也不一定是坏事,当你熟悉如何用一款框架或技术后,再去看一些源码,或许会事半功倍。



作为人类,我们的大脑善于模式识别。我们能够识别模式,并且套用这些固定的模式去解决许多问题, 而没有做到“知其然”也“知其所以然”。



(六) 别让情绪扼杀你状态


(1) 所谓迷茫,都不过是想的太多


总在独处时,开始怀疑自己,我是谁,我在干什么,以后该怎么办......在我理智的那两天,我都会把这种状态归咎于闲的蛋疼。但是确确实实在那种状态下,什么事情都没法下手,最严重的的一种状态,就是会有一种深深的无力感,感觉距离目标实在太远了。这种无力感,会瞬间摧毁你的勇气,让你不敢下手去做些什么。就像是一场噩梦,你明知道应该醒来,却无法挣脱。



鼠鼠我啊是家里唯一的大学生,大学入了党,工作也没有让家里操心,家里人都认为我有出息了,只有鼠鼠觉得鼠鼠是个废物,鼠鼠以前也会想着让妈妈为自己骄傲,让家里人可以开心的生活,可是浪浪山不如鼠鼠所愿,鼠鼠在浪浪山清楚的认识到了阶层的差距,身边的人正活着曾经难以想象的生活,鼠鼠也才知道人生可以那么精彩,它就在我眼前,又好像远在天边。鼠鼠在家里是最强天赋,在浪浪山却是擦锅布,我好像永远走不出浪浪山了,鼠鼠想回下水道,鼠鼠下辈子不想做鼠鼠。



这种状态,都不过是因为想的太多,我们总是在刚起步的时候,就想着终点在哪里;总是在刚学习一项技能的时候,就想着攻克技术难题;总是在与人初次见面之后,就想推心置腹;总是在今天都没有过好的时候,就想着明天该怎么办。我就是这样一个人,常常纠结于各种各样的学习路线上,每次在学习不同的技术的选择上,进行纠结,但其实这两者我明明是有足够的时间兼学的,还有时候明明知道基础要一步一步走扎实,但是却想到后面还有各种各样的新式技术,高级技术等着我,就会又开始所谓的迷茫。


其实这种所谓的迷茫,很多时候都是源自于我们想的太多了,路要一步一步走,饭要一口一口吃,想的太多,就会感到迷茫和焦虑。最好的办法就是,立足当下,安于寂寞,不要太着急看到极大的成果,放平心态,只有你的心里想通了,你的状态就会迅速回归,重振旗鼓


(2) 你总需要一个人走一段路


孤独伴随着,几年前来到几百公里外上学的我,亦或是年后即将开始找实习,找工作的我。


我想我总会有一段时间感觉到莫名的孤独,想找个人聊聊天,却又不想去找,自己戴着耳机,漫无目的的走在路上。以及每次晚上或者凌晨写完东西,躺在床上有一种说不出的感觉,特别的是,我并不感到忧伤,只是感觉空落落的,也不想认识新的朋友,也不想联系家人,却也不知道有些话该和谁说。


即使你人缘很好,常常有三五好友一起相伴,但是总会有一些空隙感到孤独,这源自于你的内心还是不够强大,有的人独行却乐在其中,有的人三无成伴却又内心孤独,因为孤独的人心中并无足乐者,灵魂还是被空虚填满。


所以,请充实自己的生活,多出去走走,多与人交往,给自己多找点自己感兴趣的事情去做,即使感到孤独,也没必要太过沮丧,只需要告诉自己,沮丧,孤独,都是正常的,我们要在自己走的这段路上,让自己成为一个更加闪亮的人。走过自己的一段夜路,终将会有柳暗花明又一村的“闹市”。


(七) 恋爱的本质是「撞」而不是「寻」


(1) 你真的想要谈恋爱吗?


有时候总会想,谈恋爱是「一定要」还是「可以要」亦或者 「没必要」。


总有那么几天,好似陷入了爱情的怪圈。让你平淡无奇的生活荡起了阵阵涟漪,打破了你安稳的生活轨迹。


大部分时候,或许只是你想要摆脱这种“孤零零”的状态,又或者看着别人的“幸福”与“快乐”,激起了你的那份欲望,而欲望总会在你的忍耐中冲昏你的头脑,让你开始憧憬爱情,并且费力的去「寻」去「找」,试图去接触不同的人,试图找出哪一个是适合自己的,或许你会觉得,主动去寻找自己的幸福是一件很美好的事情,不过于我而言,这并不是爱情,我只能把它叫做权衡利弊后的一个选择。


或许有的时候,你只是想找一个人陪你,那也或许并不是爱情。你结束了一天的忙碌,合上了笔记本电脑,关掉了手机,疲惫的倚靠在椅子上,狭小的房间中,只有那盏台灯在一片死寂中发出微弱的光。连点一只烟的动作都觉得多余,他只想一个人安静的待一会,也不知道在想什么。但如果无由头的想起了一些事情,一些人,这个引火线,就会瞬间将情绪点燃,无尽的孤独涌上心头,这个时候,你渴望有一个人陪在你身边,陪你说说话,哪怕陪着你坐一坐,起码让你知道你并不是一个人。自此以后,你开始标榜自己「需要人陪」,看似高尚的理由,其实只是你害怕寂寞的一种借口,就算你真的找到了一个陪着你的人,那你真的爱她吗,可能你只是在你漆黑的房间中又添置了一盏台灯,这样能让你的眼中看起来更加明亮。


(2) 三观一致真的很重要!


这几年也接触过一些异性,或许也有动心过,但是你会发现,不同人看待,处理事物的方式会有截然不同的结果,你认为简直不可理解的事情,在其眼中似乎也稀松平常,或许你不懂她,也许她不懂你,三观这个词的定义实在太模糊,最简单的方法就是看你们在一起的感觉,给你的感觉如果是很舒服的,那么可以进一步了解一下,害,没什么好说的了, 希望你可以找一位能符合你心中期望的另一半。


(3) 顺着人生轨迹走吧,别为了一个人停下来


千万不要陷入单恋的漩涡中,这是致命的,对的人是不需要主动找的,你只需要顺着人生轨迹走,在合适的年纪做合适的 “正事” ,自然而然就会遇到那个人了,如果等到七老八十,也没有遇到,或许这也就是命。或许说的太悲观了,但我仍认为,与其让自己为了追求一个不确定,也或许没有回应的爱情,不如自己欣赏自己孤岛中的美丽。但话也不能太绝对,或许有一天我就会因为所谓的爱情,陷入盲目。爱情这东西,谁说的好呢。但我只要不断告诉自己,一定不要停下来


第二节 这一年我都做了些什么


(一) 学习 + 技术输出


(1) 行百里者半九十


按照原来的计划,从 Java --> JavaWeb --> SSM --> SpringBoot 这个主线就算结束了,其中夹杂着 SQL,Linux,Web,小程序,设计模式等等的一些支线。不过,根据自己的情况和具体需要吗,其实我已经做出了一些重点的调整,我会在后面的目标中去提到。


(2) 一年和球友一起 输出了一百多万字的笔记



先放地址【Java全栈方向】:http://www.yuque.com/icu0/wevy7f


欢迎大佬们关注一下小弟。



一年中,一边学习,一边做总结,做整理,陆陆续续一年也写了200来篇笔记(也可能是文章或者感悟)(不一定纯后端/前端,还有 Linux ,计网等等)记得某个大佬说过写博客和笔记不一定能做到对别人有帮助,但起码对自己有帮助。但是我一直通过大白话概括 + 做图 + 简单示例 + 官方说明的方式写文章,也在努力希望能对别人也有帮助。



(二) 超爱买书的购物狂


这一年买了不少书(买了 == 看了),还有好多想买的都在我的购物车里吃灰,再买我真怕自己变成一个光收藏的 “读书人” 了,来盘点盘点这一年我看了比较有感觉的书(没感觉的和没怎么读的就不提了,如果给我多一点时间,我争取出一篇介绍自己读的书籍的文章)




一件恐怖的事情:我利用一年时间看过了这些书



第三节 明年今日,记得要回头看看



We already walked too far, down to we had forgotten why embarked. ——纪伯伦《先知》


译文:我们已经走得太远,以至于忘记了为什么而出发。



2022年度回顾



2023年新的目标


技术上:


只有写1-2月的,所以我放一个链接,欢迎大家监督我学习



http://www.yuque.com/icu0/qeowns… 《Cool的三两事》



生活上:


  1. 孝敬父母

  2. 勤运动

  3. 照顾好自己的身体

  4. 不要熬夜

  5. 与人交谈,沉稳思虑而后动

  6. 多读书,多出去走走,善待他人


学业上:


  1. 英语四级

  2. 拉取开源项目,为开源项目提PR

  3. 持续输出技术型文章

  4. 专升本上岸


总而言之,2022喜忧参半,有“春风得意马蹄疾,一日看尽长安花”的喜悦,也有“停杯投箸不能食,拔剑四顾心茫然”的忧愁,但我希望我的2023能有“长风破浪会有时,直挂云帆济沧海”。



个人独白:


以上内容皆是一名专科生的自白,感谢自己在大专三年没有一天是“浑浑噩噩式”学习,也没有一天因为当前的荣誉而骄傲满足,同时感谢部队两年的栽培,让我站在低谷依旧能仰望天空,扎根大地,心有猛虎,细嗅蔷薇。


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

让ChatGPT帮我写一篇博客

前言 昨天还是前天在公众号看到美国的一个设计师兼作家的Jackson,在用ChatGPT创业,他让ChatGPT做“老板”,自己充当一个人工联络员,这样的一个故事让他火了,不仅推特粉丝大涨,公司也做得有模有样。这就给我一些启发,今天早上突然想到为什么不能让Ch...
继续阅读 »

前言


昨天还是前天在公众号看到美国的一个设计师兼作家的Jackson,在用ChatGPT创业,他让ChatGPT做“老板”,自己充当一个人工联络员,这样的一个故事让他火了,不仅推特粉丝大涨,公司也做得有模有样。这就给我一些启发,今天早上突然想到为什么不能让ChatGPT也帮我干活呢,所以我让他写了一篇我感兴趣的博客。


什么是交互艺术?



  • 交互艺术的定义和历史


交互艺术(Interactive Art)是指通过观众与作品互动来创造艺术作品的一种艺术形式。观众可以通过各种方式来参与艺术作品的创作过程,例如通过触摸、声音、身体动作、传感器等方式与作品互动,从而影响作品的展示效果和呈现形式。


交互艺术的历史可以追溯到20世纪60年代,当时的科技进步促使艺术家开始尝试使用电子设备和计算机来创建艺术作品。随着计算机技术的不断发展,交互艺术逐渐成为一个独立的艺术领域,许多艺术家开始探索如何将新兴技术与传统艺术形式结合起来,创造出更具互动性、多媒体性和参与性的艺术作品。


在当代艺术中,交互艺术已经成为一个重要的艺术形式,不断涌现出许多富有创意和思想性的作品。交互艺术的发展也受到了数字技术和科技的快速进步的影响,艺术家们可以使用越来越先进的技术来创造更加复杂、多样化的交互艺术作品。



  • 交互艺术与传统艺术形式的区别和相似之处


交互艺术与传统艺术形式在表现形式、创作方式和观赏方式上存在一些区别和相似之处。


区别:



  1. 表现形式不同:传统艺术形式通常是静态的,比如绘画、雕塑等,而交互艺术则常常是动态的,利用科技和互动方式与观众进行互动。

  2. 创作方式不同:传统艺术形式往往是由一个或几个艺术家单独创作完成,而交互艺术则通常需要一个团队来完成,包括设计师、程序员、工程师等多个专业人员。

  3. 观众参与度不同:传统艺术形式通常是观众被动地欣赏,而交互艺术则需要观众的积极参与,观众不再是简单的观看者,而是成为作品的一部分,与作品进行互动。


相似之处:



  1. 创作目的相似:传统艺术和交互艺术都有一个共同的目的,就是通过艺术表达和传递情感、思想和价值观念等。

  2. 创作需要创意:无论是传统艺术还是交互艺术,都需要创意,才能产生具有艺术价值的作品。

  3. 艺术的审美标准类似:无论是传统艺术还是交互艺术,都需要具有一定的审美标准,才能得到观众的认可。


总的来说,交互艺术是一种创新的艺术形式,与传统艺术形式相比具有很多不同的地方,但它们都是为了表达和传递情感、思想和价值观念等而存在的。


交互艺术的表现形式



  • 交互装置


互动装置是一种创新的展示形式,是通过各种技术手段实现人机交互,让观众能够积极参与其中,与艺术品互动,甚至改变艺术品的形态。例如,光线与声音互动的装置、投影与运动互动的装置、使用虚拟现实技术的装置等。



  • 互动性媒体


数字媒体是指以计算机技术为基础,通过数字技术手段,将文本、音频、视频等多种媒体形式进行集成和处理的一种新兴的艺术形式。数字媒体广泛应用于网络、游戏、互动展览、艺术品创作等领域。数字媒体可以通过人机交互来实现观众参与。



  • 跨媒介艺术


跨媒介艺术是指将不同的媒介进行融合,从而创造出全新的艺术形式。在交互艺术中,跨媒介艺术得以大量运用,以实现更为复杂和多样化的表现。主要包括:融合音乐,舞蹈,戏剧,美术,文字等等。


交互艺术的设计过程



  • 设计理念和目标


交互艺术的设计理念和目标可以根据具体项目的不同而有所不同,比如以互动性为主:交互艺术的设计目标是与观众进行互动,让观众成为艺术作品的一部分。互动可以是双向的,也可以是多向的,观众和作品之间可以有各种形式的交流和反馈。以参与性为主:交互艺术作品的设计目的是让观众成为作品的参与者,观众不仅是作品的被动观看者,还可以通过各种方式主动参与到作品中,体验艺术的过程。以创新性为主:交互艺术通常借助科技手段来实现艺术形式的创新,例如虚拟现实、增强现实、人工智能等技术,让观众体验到新颖的艺术形式和感官体验。以实验性为主:交互艺术通常具有实验性质,设计者会尝试各种不同的技术和形式,不断探索和发掘新的艺术表现方式和可能性。以社交性为主:交互艺术作品通常可以带来社交体验,让多个观众之间产生互动和交流,增加观众之间的沟通和共同体验。以可持续性为主:交互艺术的设计也需要考虑作品的可持续性,包括对环境的影响、对观众的健康和安全等方面的考虑。同时还需要考虑作品的维护和管理,确保作品的长期运行和展示。



  • 技术实现和选择


交互艺术的技术实现有多种选择,以下是一些常见的技术实现:



  1. 传感器技术:通过感应器获取观众的运动、声音、触摸等行为,以此来激发或控制艺术作品的变化。

  2. 虚拟现实技术:使用计算机技术和虚拟现实设备(如头戴式显示器、手套式控制器等)创造虚拟空间,使观众可以沉浸在其中与作品进行交互。

  3. 增强现实技术:使用手机、平板电脑等设备,将虚拟图像叠加在现实场景中,使观众可以在真实场景中进行虚拟的交互体验。

  4. 数据可视化技术:使用数据可视化软件和技术将数据转化为图形、动画、声音等形式,让观众可以与数据进行交互并得到更深入的理解。

  5. 互动音乐技术:使用计算机技术和音乐软件,将观众的声音、运动等行为转化为音乐,并与音乐作品进行互动。

  6. 智能机器人技术:使用机器人技术和人工智能技术,创造能够与观众进行交互的智能机器人艺术作品。


除此之外,还有许多其他的技术可以被应用于交互艺术的实现,这取决于艺术家的创造力和技术能力。



  • 用户参与和反馈


用户参与和反馈在交互艺术中起着至关重要的作用,这是因为交互艺术强调观众参与、互动和沟通,与传统艺术形式相比,用户的参与和反馈更能够影响交互艺术的展现和效果。用户参与和反馈可以创造更丰富、更具有互动性的艺术体验。通过参与和反馈,用户可以主动探索和发现艺术作品中的细节,与艺术家进行更深入的互动和交流。用户参与和反馈可以增加用户对交互艺术的参与度,使观众更加融入艺术作品之中,感受到艺术作品所传达的情感和信息。用户反馈可以帮助艺术家改善艺术作品的表现,及时发现并解决问题,让作品更加完善和符合观众的期望。通过参与和反馈,用户可以更好地理解和体验艺术作品,从而对作品产生更深刻的印象和理解,提高作品的艺术价值和影响力。


交互艺术的影响和意义



  • 对艺术和文化的影响


首先,交互艺术提供了新的观看方式和体验方式,通过参与和互动,观众成为了作品的一部分,与作品发生了联系和互动,这种体验方式比传统艺术观看更加身临其境,更能够引起观众的共鸣和情感共鸣。


其次,交互艺术扩展了艺术形式和创作方式的范围,使得艺术家可以使用更多的媒介和技术手段来表现自己的创意和思想,创作出更加复杂、多样化的作品。同时,交互艺术还促进了跨学科的合作和交流,让不同领域的人们汇聚在一起,共同探索艺术的新领域和可能性。


最后,交互艺术也在一定程度上挑战了传统艺术的观念和价值体系,它更加强调观众的参与和互动,追求创意和表达的多样性和自由性,让艺术更加民主化和开放化,更加贴近生活和人们的需求。



  • 对科技和创新的影响


交互艺术与科技、创新密切相关,因为交互艺术往往需要运用先进的科技和技术手段来实现。因此,交互艺术对科技和创新的影响主要表现在以下几个方面:



  1. 推动科技进步和应用:交互艺术在探索人机交互的过程中,往往需要运用先进的科技和技术手段,例如虚拟现实、增强现实、智能算法、传感器技术等等。这些技术的研究和应用,可以推动科技的进步和应用,也可以为其他领域的技术创新提供借鉴和参考。

  2. 催生新兴产业:随着交互艺术的不断发展和普及,一些新兴产业也应运而生,例如虚拟现实、增强现实、智能穿戴等等。这些产业的发展,也为科技和创新提供了新的发展机遇。

  3. 拓展创新思维:交互艺术强调观众参与和互动,鼓励观众从不同的角度去思考和理解作品。这种互动式的艺术形式,不仅可以拓展观众的视野和想象力,也可以激发人们的创新思维,从而为科技和创新带来新的灵感和方向。

  4. 促进科技与文化的融合:交互艺术将科技和文化相结合,探索科技与文化之间的互动和融合。这种融合不仅可以为文化艺术注入新的活力和创新,也可以促进科技和文化之间的相互理解和交流,为科技和创新带来新的思路和方向。



  • 对社会和人类的影响


首先,交互艺术的出现丰富了人们的文化生活,为人们带来了全新的艺术体验。交互艺术将观众从被动的接受者转变为积极的参与者和创造者,让人们更深入地体验艺术,对个人的审美和文化素养的提高有积极的推动作用。


其次,交互艺术对科技和创新的发展也有很大的促进作用。在交互艺术的设计中,常常会运用到各种前沿的科技手段,如人工智能、虚拟现实、增强现实等,这些技术的应用不仅提高了艺术表现的多样性和创新性,也促进了科技的发展和推广。


另外,交互艺术也推动了跨学科的合作与交流。交互艺术的创作需要艺术家、设计师、工程师等多个领域的专业人才进行合作,这种跨学科的合作有助于促进不同领域之间的交流与合作,进一步推动科技和艺术的发展。


最后,交互艺术也对社会产生了深远的影响。交互艺术作为一种探索艺术与科技、人与自然等关系的艺术形式,常常会引起人们对社会和人类的思考和反思。同时,交互艺术还可以作为一种公共艺术形式,为城市文化建设和社区文化发展做出贡献。


一些优秀的交互艺术作品的介绍和分析


《雨林声音之旅》(The Rainforest):这是一个由音乐家、工程师和艺术家合作制作的多媒体艺术作品,旨在通过视听交互体验向人们展示热带雨林生态系统的美丽和脆弱。在这个作品中,观众通过穿戴智能耳机,能够听到热带雨林中各种生物的声音,并随着观众的行动而改变。这个作品既展示了交互艺术对生态保护的关注,也通过技术手段提供了一个沉浸式的交互体验。


《万花筒之舞》(Kaleidoscope Dance):这个作品由艺术家和编程人员合作创作,是一个通过跟随舞者动作变幻图形的互动舞蹈。在这个作品中,观众通过观看跳舞者的身影,看到身影在投影上变幻出不同的几何图形,并随着舞者的动作而变化。这个作品的互动性和美学效果非常出色,展示了交互艺术的创造性和能力。


《未来自然》(Future Natural):这个作品由美国纽约市的艺术家托尼·瑞戈(tony oursler)制作,展示了未来科技与自然环境的融合。这个作品是一个互动装置,观众通过操纵屏幕上的自然元素,比如云、火、风和水,来创造出自己的自然景观。这个作品展示了交互艺术的潜力,让观众在艺术作品中自由探索和创造。


《印象派的视觉音乐》(Visual Music of Impressionism):这是一个通过数字技术还原19世纪法国印象派画家的绘画作品的互动展览。这个展览展示了通过数字技术将视觉艺术与音乐相结合的能力,让观众可以通过触摸屏幕、移动手势和声音互动等方式来探索印象派绘画作品的美学和音乐性。


未来展望:交互艺术的发展和趋势


交互艺术从20世纪末期开始兴起,并在21世纪初逐渐得到了广泛的关注和发展。最初的交互艺术作品主要依赖计算机技术,随着移动设备、传感器、物联网等技术的发展,交互艺术的形式也越来越多样化和复杂化。


近年来,交互艺术已经从传统的展览空间向公共空间和虚拟空间延伸,例如城市中的互动艺术装置和游戏、虚拟现实艺术作品等。同时,交互艺术也更多地关注社会和环境问题,例如气候变化、人类生活和工作的影响等。


未来的趋势将继续突破传统的艺术形式,更多地与科技、社会、文化等领域交叉融合,例如增强现实、人工智能、机器人技术等。同时,交互艺术也将更加强调观众的参与和反馈,更加注重体验和互动的情感性和反思性。


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

ChatGPT正在杀死程序员,讲讲我的看法——AI春晚,Boom!

AI春晚,Boom! 这两周,简直是AI春晚,ChatGPT4、ChatGPT Plugin、Microsoft 365 copilot、Github copilotX、MidJourney V5、Adobe Firefly、NVIDIA一堆新产品... 多到...
继续阅读 »

AI春晚,Boom!


这两周,简直是AI春晚,ChatGPT4、ChatGPT Plugin、Microsoft 365 copilot、Github copilotX、MidJourney V5、Adobe Firefly、NVIDIA一堆新产品... 多到数不过来了,几乎是数不过来了,大产品发布的速度可以用小时来计算,小产品更是多如牛毛,github trends、Hacker News差不多半数都被ChatGPT相关霸榜。


可谓科技圈的“大变局”,上一次大家这么激动,还是iPhone诞生。


接下来,我来说说看我的看法和一些个人预测。


我的几点看法




  1. ChatGPT的诞生,将带来新的革命,第一次信息技术革命主要是加速了信息传递,而此次第二次信息技术革命,则是让信息传递进一步加速的同时,让信息有了自生产能力。例如:你给AI一个标题,AI可以根据过往学习经验,联想一个新的故事。又比如:你写了一段新的代码,可以让AI帮你解释,也许未来帮你写文档。AI可以根据少量的信息,进行信息生产,这是替代人类一部分能力的关键点。




  2. 我认为大语言模型的能力并未被全部释放,AI可以做的还有更多,更有创造力,只不过暂时被封印了。未来的前景空间无限,随便上升一点点都是big boom.




  3. 这只是这一种模型而已... 相信AI行业的投入的资金接下来会是爆炸式的增长。其他模型大概率很快在资金加持下,进行多轮验证。指不定再来个别的超级AI。




  4. 自然语言(提示语)编程诞生,这将是最难的一门语言,短期来看,必然是英语,掌握英语的人未来拥有更大的竞争力。但是自然语言有较大的缺陷,就是难以构建复杂结构和海量参数的提示语,而人类的需求往往是精准的,只用自然语言效率比较低。所以我预测,未来会有更加结构化的语言诞生,它将综合人类的自然语言和高级编程语言,成长为新的热门语言。




  5. 毫无疑问,随着内容创作方面的效率急速提升,大多数白领的工作将会受到极大影响,被调整优化。不进步就会被淘汰,但是也意味着会出现新的机会。




  6. AI程序员不仅正在杀死白领,还无情地杀死另外一些程序员,底层如前端程序员和测试工程师将会受到巨大影响,优化。这其实是因为,相当一部分程序员的工作其实更接近于内容创作(cv工程师),而不是严密的逻辑思考和创造,所以更容易被AI取代。不得不说,程序员的差距就像人与狗的差距一样大。




  7. 新的繁荣即将到来?毫无疑问。纺织机夺走了大部分手工纺织者的工作,但是催生了更庞大的市场,例如服装设计,人的需求促使人朝着更加高级的方向发展了。那新的工作机会呢?可能是自然语言(提示语)工程、AI训练师等等。




  8. 恭喜前端程序员,终于不用写大量页面和CSS了,暂时也许可以考虑往全栈卷了?




  9. 如果你是一个简单的内容工作者或者初级程序员,那么赶紧学吧,否则将面临失业。




  10. 如果你是中高级技术工程师,也赶紧学吧,新的机会正在到来。本来吧,互联网遇冷,就业困难,大家都不想学了,学不动了,现在好了,局势逼着你学。




  11. 好消息是,我们有墙,也可能是坏消息。




  12. 可以大概率肯定的是,就业市场应该很快会回暖,现在就等巨头公司找找方向,开启新市场。


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

23年ChatGPT平替方案和开源案例

作为一个老掉牙的全栈程序员,不愿意参与职场的人情世故,只愿意埋头研究技术。是不是大家也有这种想法?这种想法,目前都是扯淡的,那些人情世故会把你挤走~ 想要纯粹的技术环境,可能或许只有大厂了~ 现实是:ChatGPT每天都在疯狂地学习,疯狂的进步,也在被开发人...
继续阅读 »

作为一个老掉牙的全栈程序员,不愿意参与职场的人情世故,只愿意埋头研究技术。是不是大家也有这种想法?这种想法,目前都是扯淡的,那些人情世故会把你挤走~ 想要纯粹的技术环境,可能或许只有大厂了~



现实是:ChatGPT每天都在疯狂地学习,疯狂的进步,也在被开发人员疯狂的使用。至少,我们团队每天总有一个窗口留给了它。由于最近OpenAI服务器频发故障,进入官网的速度越来越慢,难度越来越大。平替或许不是最好的方案,但有时候却是不可或缺的工具。


据悉:OpenAI团队目前正在努力改善模型的性能和速度,想让模型变得更快、更高效。无独有偶,百度也在近期推出了企业专版产品:文心千烦,网友回复:百度果然没有辜负大家,第一时间没有投入技术研究,而是研究出付费方式


平替案例


以下是群友整理的地址,请勿填写自己的key,以防止被调用。


anzorq-chatgpt-demo.hf.space

chat.openai1s.com [荐]

chat.aifks001.online

gptocean.com

chatgpt.ai [荐]

ai-chat.scholarcn.com

http://www.x5.chat
builtbyjesse.com/lab
http://www.scifmat.work/
aichat.momen.vip/home


开源代码:


github.com/waylaidwand…

github.com/dirk1983/ch…

github.com/Chanzhaoyu/…

github.com/869413421/c…


推荐插件


语音交互插件


Voice Control for ChatGPT:实现语音与chagpt交互,支持多国语言


image.png


下载地址:chrome.google.com/webstore/de…


角色提示市场


AIPRM for ChatGPT,可以内嵌角色,输入风格, 输出风格等多种标签支持(貌似开始收费了)


image.png

下载地址:chrome.google.com/webstore/de…


聊天工具库-彩蛋


聊天工具,可以支持复制,下载聊天记录,下载图片,转成pdf多种工具


image.png


下载地址


Google浏览器插件


ChatGPT 谷歌助手和高亮显示工具,玩的太嗨了


image.png


下载地址:chrome.google.com/webstore/de…


角色脚本插件-彩蛋


可以通过/触发脚本指令,收录近300个专业的角色插件指令,目前尚未发布市场。


image.png


下载地址


接入外网的ChatGPT


可以访问互联网的ChatGPT,数据将不会再停留在2021年9月,而是实时数据检索,通过GPT的语言模型将关键信息提取出来喂养后,给出更加精准的答案


image.png


技术研究


image.png


案例比较多,就提取一个自己玩的比较好的,用node启动的服务,可以在seveless中快速启动。
设置环境变量:OPENAI_API_KEY为自己key即可


import cloud from '@lafjs/cloud'//这个不用管
import axios from 'axios'//这个测试的
import { ChatGPTAPI } from 'chatgpt'
export async function main(ctx: FunctionContext) {
// body, query 为请求参数, auth 是授权对象
const { auth, body, query } = ctx;
const prompt = body.text.content;
const api = new ChatGPTAPI({ apiKey: cloud.env.OPENAI_API_KEY })
let res = await api.sendMessage('你好')
console.log(res.text)
}

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

Android 实现计时器

这周接到个新需求,统计用户在线时长,累积到一定时长后上报,可以通过计时器来实现。本篇文章介绍下安卓端实现计时器的三种方式。 Timer、TimerTask 通过Timer和TimerTask实现计时,代码如下: class TimeChangeExample ...
继续阅读 »

这周接到个新需求,统计用户在线时长,累积到一定时长后上报,可以通过计时器来实现。本篇文章介绍下安卓端实现计时器的三种方式。


Timer、TimerTask


通过TimerTimerTask实现计时,代码如下:


class TimeChangeExample : BaseGestureDetectorActivity() {

private lateinit var binding: LayoutTimeChangeExampleActivityBinding

private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")

private var timerHandler = object : Handler(Looper.myLooper() ?: Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
if (msg.what == 0) {
setCountdownTimeText(msg.obj as Long)
}
}
}
private var timer: Timer? = null
private var timerTask: TimerTask? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.layout_time_change_example_activity)
binding.btnCountdownByTimer.setOnClickListener {
clearText()
binding.tvCountdownText.text = "countdown by timer\n"
startCountdownByTime()
}
binding.btnStopTimer.setOnClickListener {
stopTimer()
}
}

private fun startCountdownByTime() {
stopTimer()
timerTask = object : TimerTask() {
override fun run() {
timerHandler.sendMessage(timerHandler.obtainMessage(0, System.currentTimeMillis()))
}
}
timer = Timer()
timer?.schedule(timerTask, 0, 1000)
}

private fun stopTimer() {
timer?.cancel()
timer = null
timerTask = null
}

private fun setCountdownTimeText(time: Long) {
binding.tvCountdownText.run {
post {
text = text.toString() + "${dateFormat.format(Date(time))}\n"
}
}
}

private fun clearText() {
binding.tvCountdownText.text = ""
}

override fun onDestroy() {
super.onDestroy()
stopTimer()
}
}

效果如图:


两次计时之间的误差都是毫秒级的。


timer -original-original.gif

BroadCastReceiver


通过注册广播,监听系统时间变化实现计时,但是广播回调触发的间隔固定为一分钟,代码如下:


class TimeChangeExample : BaseGestureDetectorActivity() {

private lateinit var binding: LayoutTimeChangeExampleActivityBinding

private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")

private val timeChangeBroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == Intent.ACTION_TIME_TICK) {
setCountdownTimeText(System.currentTimeMillis())
}
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.layout_time_change_example_activity)
binding.btnCountdownByBroadcast.setOnClickListener {
clearText()
binding.tvCountdownText.text = "countdown by broadcast\n"
startCountdownByBroadcast()
}
binding.btnStopBroadcast.setOnClickListener {
stopBroadcast()
}
}

private fun startCountdownByBroadcast() {
registerReceiver(timeChangeBroadcastReceiver, IntentFilter().apply {
addAction(Intent.ACTION_TIME_TICK)
})
}

private fun stopBroadcast() {
unregisterReceiver(timeChangeBroadcastReceiver)
}

private fun setCountdownTimeText(time: Long) {
binding.tvCountdownText.run {
post {
text = text.toString() + "${dateFormat.format(Date(time))}\n"
}
}
}

private fun clearText() {
binding.tvCountdownText.text = ""
}

override fun onDestroy() {
super.onDestroy()
stopBroadcast()
}
}

效果如图:


两次计时之间的误差都是毫秒级的。


boardcast.png

Handler


通过HandlerRunnable来实现计时,代码如下:


class TimeChangeExample : BaseGestureDetectorActivity() {

private lateinit var binding: LayoutTimeChangeExampleActivityBinding

private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.layout_time_change_example_activity)
binding.btnCountdownByHandler.setOnClickListener {
clearText()
binding.tvCountdownText.text = "countdown by handler\n"
startCountdownByHandler()
}
binding.btnStopHandler.setOnClickListener {
stopHandler()
}
}

private val handler = Handler(Looper.myLooper() ?: Looper.getMainLooper())
private val countdownRunnable = object : Runnable {
override fun run() {
setCountdownTimeText(System.currentTimeMillis())
val currentTime = SystemClock.uptimeMillis()
val nextTime = currentTime + (1000 - currentTime % 1000)
handler.postAtTime(this, nextTime)
}
}

private fun startCountdownByHandler() {
val currentTime = SystemClock.uptimeMillis()
val nextTime = currentTime + (1000 - currentTime % 1000)
handler.postAtTime(countdownRunnable, nextTime)
}

private fun stopHandler() {
handler.removeCallbacks(countdownRunnable)
}

private fun setCountdownTimeText(time: Long) {
binding.tvCountdownText.run {
post {
text = text.toString() + "${dateFormat.format(Date(time))}\n"
}
}
}

private fun clearText() {
binding.tvCountdownText.text = ""
}

override fun onDestroy() {
super.onDestroy()
stopHandler()
}
}

效果如图:


两次计时之间的误差都是毫秒级的。


handler -original-original.gif

示例


在示例Demo中添加了相关的演示代码。


ExampleDemo github


ExampleDemo gitee


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

android自定义View: 九宫格解锁

本系列自定义View全部采用kt 系统:mac android studio: 4.1.3 kotlin version1.5.0 gradle: gradle-6.5-bin.zip 废话不多说,先来看今天要完成的效果: 3X3 (样式1)4*4(样式2)...
继续阅读 »

本系列自定义View全部采用kt



系统:mac


android studio: 4.1.3


kotlin version1.5.0


gradle: gradle-6.5-bin.zip


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


3X3 (样式1)4*4(样式2)5*5(样式3)
68003856905943AF9D5C44066EC4E13128A4F0086BD411F14D83F0B04E14893C6826652AEDDA18C295974B9A54BC55C6

Tips:不止3X3 或者 5X5 ,如果你想,甚至可以设置10*10


画圆


先以3*3的九宫格来介绍!


image-20220914105128040


我们要画成这样的效果, 画的是有一点丑,但是没关系.


首先来分析一下怎么花,这9个点的位置如何确定:



  • 我们为了平均分, 单个圆的外层矩形 宽 = view.width / 3

  • 高 = 宽

  • 1号圆的圆心位置 = 0个矩形的宽度 = view.width / (3 * 2) + ( view.width / 3 ) * 0

  • 2号圆的圆心位置 = 1号圆的圆心位置 + 1个矩形的宽度 = view.width / (3 * 2) + (view.width / 3) * 1

  • 3号圆的圆心位置 = 1号圆的圆心位置 + 2个矩形的宽度 = view.width / (3 * 2) + (view.width / 3) * 2


高坐标的计算也是如此


来看看目前的代码:


class BlogUnLockView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
//
strokeJoin = Paint.Join.BEVEL
}

// 大圆半径
private val bigRadius by lazy { width / (NUMBER * 2) * 0.7f }

// 小圆半径
private val smallRadius by lazy { bigRadius * 0.2f }

companion object {
const val NUMBER = 3
}

private val unLockPoints = arrayListOf<ArrayList<UnLockBean>>()

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
// 矩形直径
val diameter = width / NUMBER

//
val ratio = (NUMBER * 2f)
var index = 1

// 循环每一行行
for (i in 0 until NUMBER) {
val list = arrayListOf<UnLockBean>()

// 循环每一列
for (j in 0 until NUMBER) {
list.add(
UnLockBean(
width / ratio + diameter * j,
height / ratio + diameter * i,
index++
)
)
}
unLockPoints.add(list)
}
}

override fun onDraw(canvas: Canvas) {
canvas.drawColor(Color.YELLOW)

unLockPoints.forEach {
it.forEach { data ->
// 绘制大圆
paint.alpha = (255 * 0.6).toInt()
canvas.drawCircle(data.x, data.y, bigRadius, paint)

// 绘制小圆
paint.alpha = 255
canvas.drawCircle(data.x, data.y, smallRadius, paint)
}
}
}
}

当前效果:


image-20220914110551658


目前问题:



  • 整个view占满了屏幕,需要测量


测量代码比较简单,就是让宽和高一样即可


image-20220914111142598


此时改变number变量,就可以设置几行几列:


例如这样:


5*510*10
image-20220914111416881image-20220914111450264

接下来我们就处理手势事件,按下滑动,抬起等,来改变选中


onTouchEvent事件处理


在事件处理之前先来分析一下需要几种事件,对于解锁功能来说:



  • ORIGIN 刚开始,还没有触摸

  • DOWN 正在触摸中(输入密码)

  • UP 触摸结束 (输入密码正确)

  • ERROR 触摸结束 (输入密码错误)


那么就先定义4种颜色,来表示这4种状态:


companion object {

// 原始颜色
private var ORIGIN_COLOR = Color.parseColor("#D8D9D8")

// 按下颜色
private var DOWN_COLOR = Color.parseColor("#3AD94E")

// 抬起颜色
private var UP_COLOR = Color.parseColor("#57D900")

// 错误颜色
private var ERROR_COLOR = Color.parseColor("#D9251E")
}

接下来挨个处理事件


DOWN(按下)


首先需要思考,在按下的时候要做什么事情:



  • 判断是否选中


/*
* TODO 判断是否选中某个圆
* @param x,y: 点击坐标位置
*/

private fun isContains(x: Float, y: Float) = let {
unLockPoints.forEach {
it.forEach { data ->
// 循环所有坐标 判断两个位置是否相同
if (PointF(x, y).contains(PointF(data.x, data.y), bigRadius)) {
return@let data
}
}
}
return@let null
}

// 判断一个点是否在另一个点范围内
fun PointF.contains(b: PointF, bPadding: Float = 0f): Boolean {
val isX = this.x <= b.x + bPadding && this.x >= b.x - bPadding

val isY = this.y <= b.y + bPadding && this.y >= b.y - bPadding
return isX && isY
}

思路: 通过比较 按下位置和所有位置,判断是否有相同的



  • 如果有相同的,那么就返回对应坐标

  • 如果没有相同的,那么就返回null



@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
// 判断是否选中
val pointF = isContains(event.x, event.y)
pointF?.let {
// 将当前类型变为按下类型
it.type = JiuGonGeUnLockView.Type.DOWN
}
}
...
}
invalidate()
return true
}

override fun onDraw(canvas: Canvas) {
// canvas.drawColor(Color.YELLOW)

unLockPoints.forEach {
it.forEach { data ->
// 根据类型设置颜色
paint.color = getTypeColor(data.type)

// 绘制大圆
paint.alpha = (255 * 0.6).toInt()
canvas.drawCircle(data.x, data.y, bigRadius, paint)

// 绘制小圆
paint.alpha = 255
canvas.drawCircle(data.x, data.y, smallRadius, paint)
}
}
}

/// TODO 获取类型对应颜色
private fun getTypeColor(type: JiuGonGeUnLockView.Type): Int {
return when (type) {
JiuGonGeUnLockView.Type.ORIGIN -> ORIGIN_COLOR
JiuGonGeUnLockView.Type.DOWN -> DOWN_COLOR
JiuGonGeUnLockView.Type.UP -> UP_COLOR
JiuGonGeUnLockView.Type.ERROR -> ERROR_COLOR
}
}

当前效果:


B6B94BC2B7487B5894E6840C1F783F7A

MOVE(移动)


move事件和down事件的逻辑是一样的,滑动的过程中判断点是否选中,然后绘制点


@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
val pointF = isContains(event.x, event.y)
pointF?.let {
// 将当前类型改变为按下类型
it.type = JiuGonGeUnLockView.Type.DOWN
}
}
MotionEvent.ACTION_MOVE -> {
val pointF = isContains(event.x, event.y)
pointF?.let {
// 将当前类型改变为按下类型
it.type = JiuGonGeUnLockView.Type.DOWN
}
}

....
}

invalidate()
return true
}

当前效果:


1800F1D0441C219F4F2735B706DFFB9B

可以看出,效果是基本完成了,但是还有一个小错误


通常我们在九宫格的时候,一般都是先按下一个点才能滑动, 否则是不能滑动的,


现在的问题是,直接就可以滑动,所以还需要调整一下


那么我们就需要在down事件中标记一下是否按下,然后在move事件中判断一下


// 是否按下
private var isDOWN = false

@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
val pointF = isContains(event.x, event.y)
pointF?.let {
// 将当前类型改变为按下类型
it.type = JiuGonGeUnLockView.Type.DOWN
isDOWN = true // 表示按下
}
}
MotionEvent.ACTION_MOVE -> {
if (!isDOWN) {
return super.onTouchEvent(event)
}
val pointF = isContains(event.x, event.y)
pointF?.let {
// 将当前类型改变为按下类型
it.type = JiuGonGeUnLockView.Type.DOWN
}
}

MotionEvent.ACTION_CANCEL,
MotionEvent.ACTION_UP -> {
isDOWN = false // 标记没有按下
}
}

invalidate()
return true
}

此时效果:


980BE4943A8EBF10516BAA27E023151B

UP(抬起)


思路分析:


抬起的时候要做很多事情




  • 判断输入密码是否正确



    • 密码输入正确,那么就改变为深绿色

    • 密码输入错误,就改变为红色




  • 完成之后,还需要吧所有的状态清空




在这里的时候,先不判断密码是否成功, 默认都是成功的,



  • 先吧输入的密码toast出来

  • 并且吧状态清空


等结尾的时候再来判断密码.


那么此时肯定是需要将所有选中的都记录下来, 然后在up事件中操作即可


// 记录选中的坐标
private val recordList = arrayListOf<UnLockBean>()

@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
val pointF = isContains(event.x, event.y)
pointF?.let {
// 将当前类型改变为按下类型
it.type = JiuGonGeUnLockView.Type.DOWN
isDOWN = true

recordList.add(it)
}
}
MotionEvent.ACTION_MOVE -> {
if (!isDOWN) {
return super.onTouchEvent(event)
}
val pointF = isContains(event.x, event.y)
pointF?.let {
// 将当前类型改变为按下类型
it.type = JiuGonGeUnLockView.Type.DOWN

// 这里会重复调用,所以需要判断是否包含,如果不包含才添加
if (!recordList.contains(it)) {
recordList.add(it)
}
}
}

MotionEvent.ACTION_CANCEL,
MotionEvent.ACTION_UP -> {
// 将结果打印
recordList.map {
it.index
}.toList() toast context

clear()
}
}

invalidate()
return true
}

/// 清空所有状态
private fun clear() {
recordList.forEach {
// 将所有选中状态还原
it.type = JiuGonGeUnLockView.Type.ORIGIN
}
recordList.clear()
isDOWN = false // 标记没有按下

invalidate()
}

当前效果:


C1A1C9AA5362879D8EB870BC953FFAD9

画连接线


还是以这张图来说:


image-20220914105128040


假设现在需要连接 1,5,6,9


那么可以通过Path()来画线


在DOWN事件中,通过moveTo()移动到1的位置


在MOVE事件中,通过lineTo()画5,6,9的位置 即可


private val path = Path()

@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
val pointF = isContains(event.x, event.y)
pointF?.let {
/// 隐藏部分代码

path.moveTo(it.x, it.y)
}
}
MotionEvent.ACTION_MOVE -> {
val pointF = isContains(event.x, event.y)
pointF?.let {
/// 隐藏部分代码

// 这里会重复调用,所以需要判断是否包含,如果不包含才添加
if (!recordList.contains(it)) {
recordList.add(it)
path.lineTo(it.x, it.y) // 连接到移动的位置
}
}
}

MotionEvent.ACTION_CANCEL,
MotionEvent.ACTION_UP -> {
// 将结果打印
recordList.map {
it.index
}.toList() toast context


clear()
}
}

invalidate()
return true
}

/*
* 作者:史大拿
* 创建时间: 9/14/22 1:38 PM
* TODO 用来清空标记
*/

private fun clear() {
path.reset() // 重置

recordList.forEach {
// 将所有选中状态还原
it.type = JiuGonGeUnLockView.Type.ORIGIN
}
recordList.clear()
isDOWN = false // 标记没有按下
}

override fun onDraw(canvas: Canvas) {
paint.style = Paint.Style.FILL
unLockPoints.forEach {
/// 隐藏部分代码
}

paint.style = Paint.Style.STROKE
paint.strokeWidth = 4.dp
paint.color = DOWN_COLOR // 默认按下颜色
canvas.drawPath(path, paint)
}

当前效果:


93DE90804F77B93312D8547F84F4609B

可以看出,已经完成了画连接线,但是还缺少一条指示当前手指位置的线,


我叫他移动线,,, (好土的名字)


移动线就2个坐标



  • 开始位置 (最后一个选中的位置)

  • 结束位置 (当前手指按下的位置)


private val line = Pair(PointF(), PointF())

@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
val pointF = isContains(event.x, event.y)
pointF?.let {
/// 隐藏代码

line.first.x = it.x
line.first.y = it.y
}
}
MotionEvent.ACTION_MOVE -> {
val pointF = isContains(event.x, event.y)
pointF?.let {
if (!recordList.contains(it)) {
//// 隐藏代码

// 最后一个选中的位置
line.first.x = it.x
line.first.y = it.y
}
}

// 手指的位置
line.second.x = event.x
line.second.y = event.y
}
....
}

invalidate()
return true
}

override fun onDraw(canvas: Canvas) {

paint.style = Paint.Style.FILL
unLockPoints.forEach {
/// 隐藏代码
}

// 绘制连接线
paint.style = Paint.Style.STROKE
paint.strokeWidth = 4.dp
paint.color = DOWN_COLOR // 默认按下颜色
canvas.drawPath(path, paint)

// 绘制移动线
if (line.first.x != 0f && line.second.x != 0f
) {
canvas.drawLine(
line.first.x,
line.first.y,
line.second.x,
line.second.y,
paint
)
}
}

当前效果:


2C05F7D7EB4E102778B87581AA183E79

此时效果就差不多了,画笔默认是实心圆, 来看看空心效果


空心效果


空心效果很简单,只需要调整画笔的style即可


 override fun onDraw(canvas: Canvas) {
// 实心效果
// paint.style = Paint.Style.FILL

// 空心效果
paint.style = Paint.Style.STROKE
paint.strokeWidth = 4.dp

// canvas.drawXXX()
}

当前效果


2F8ECA7B3AE2F9DCAE2FD46F846B66C9

可以看出,此时的效果和我们想的一样,但是画线的时候从小圆圆心穿过了,不太好看


有没有一种办法,让线不从圆心穿过


那么就先来分析一下:


image-20220914144550029


假设现在是从7移动到2


那么就需要连接C点和F点,只需要计算出C点和F点的坐标即可


先来分析现在的已知条件:



  • dx = end.x - start.x

  • dy = end.y - start.y

  • d = (dx平方 + dy平方) 开根号

  • 小圆半径 = smallRadius


那么就可以算出当前的偏移量:



  • offsetX = dx * (smallRadius / d)

  • offsetY = dy * (smallRadius / d)


知道偏移量,就可以算出C和F的坐标:


那么C的坐标为:



  • C.x = start.x + offsetX

  • C.y = start.y + offsetY


那么F的坐标为:



  • F.x = end.x + offsetX

  • F.y = end.y + offsetY


只要C和F的坐标之后


只需要通过path.moveTo() 移动到C的位置


通过path.lineTo() 移动到F的位置即可


@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
/// ...
}
MotionEvent.ACTION_MOVE -> {
val pointF = isContains(event.x, event.y)
pointF?.let {
// 将当前类型改变为按下类型
it.type = JiuGonGeUnLockView.Type.DOWN

// 这里会重复调用,所以需要判断是否包含,如果不包含才添加
if (!recordList.contains(it)) {
recordList.add(it)
if (recordList.size >= 2) {
// TODO 不穿过圆心
val start = recordList[recordList.size - 2]
val end = recordList[recordList.size - 1]

val d = PointF(start.x, start.y).distance(PointF(end.x, end.y))
val dx = (end.x - start.x)
val dy = (end.y - start.y)
val offsetX = dx * smallRadius / d
val offsetY = dy * smallRadius / d

val cX = start.x + offsetX
val cY = start.y + offsetY
path.moveTo(cX, cY)

val fX = end.x - offsetX
val fY = end.y - offsetY
path.lineTo(fX, fY)

// line
line.first.x = it.x + offsetX
line.first.y = it.y + offsetY
}
}
}

// 手指的位置
line.second.x = event.x
line.second.y = event.y
}

/// 隐藏UP代码
}

invalidate()
return true
}


// 计算两点之间的距离
fun PointF.distance(b: PointF): Float = let {
val a = this

// 这里 * 1.0 是为了转Double
val dx = b.x - a.x * 1.0
val dy = b.y - a.y * 1.0
return@let sqrt(dx.pow(2) + dy.pow(2)).toFloat()
}

当前的效果:


18478E736B00DAB45797EC8BC2164F9F

所有的效果基本就差不多了,接下来来比较密码


密码比较


思路分析:



  • 先将正确密码集合传过来,然后和输入的密码做比较

  • 首先先判断两个集合的长度

    • 如果长度不一样,那么密码肯定是不同的,直接标记为错误即可

    • 如果长度一样,只需要比较每一个值是否相同

      • 相同则输入成功,将正确结果回调回去

      • 有一个不相同,则输入失败,标记为错误即可






// 密码
open var password = listOf<Int>()

MotionEvent.ACTION_UP -> {
// 清空移动线
line.first.x = 0f
line.first.y = 0f
line.second.x = 0f
line.second.y = 0f


// 标记是否成功
val isSuccess =
// 先比较长度是否相同
if (recordList.size == password.size) {
val list = recordList.zip(password).filter {
// 通过判断每一个值
it.first.index == it.second
}.toList()

// 如果每一个值都相同,那么就成功
list.size == password.size
} else {
false
}

// 密码错误,将标记改变成成错误
if (!isSuccess) {
recordList.forEach {
it.type = JiuGonGeUnLockView.Type.ERROR
}
"输入失败" toast context
} else {
"输入成功" toast context
}

// 延迟1秒清空
postDelayed({
clear()
}, 1000)
}

23B8401108604115F972F00855280E1C

现在已经可以完成输入密码了,


但是状态还不对,我们希望连接线的颜色和圆的颜色一致,


当然我们可以这样:


override fun onDraw(canvas: Canvas) 
// paint.style = Paint.Style.FILL
paint.style = Paint.Style.STROKE
paint.strokeWidth = 4.dp

unLockPoints.forEach {
it.forEach { data ->

// 根据类型设置颜色
paint.color = getTypeColor(data.type)

// 绘制大圆
paint.alpha = (255 * 0.6).toInt()
canvas.drawCircle(data.x, data.y, bigRadius, paint)

// 绘制小圆
paint.alpha = 255
canvas.drawCircle(data.x, data.y, smallRadius, paint)

// 绘制连接线
canvas.drawPath(path, paint)

// 绘制移动线
if (line.first.x != 0f && line.second.x != 0f
) {
canvas.drawLine(
line.first.x,
line.first.y,
line.second.x,
line.second.y,
paint
)
}
}
}
}

但是我还是选择了通过一个全局变量,来记录当前的状态,然后给连接线和移动线设置颜色


代码很简单,就不展示了,直接看效果:


9D20C8CE4024A396D9AE75D7607F739E

到此时,效果就基本完成了,


但是,写完发现,代码真的太乱了,而且有很多设置的东西,


比如说:



  • 默认颜色

  • 移动颜色

  • 输入成功颜色

  • 输入失败颜色

  • 解锁的大小

    • 例如3,就是3 X 3 5就是5 X 5



  • 样式

    • 空心 or 实心




一般遇到这种情况我认为有2种方式



  • 自定义属性

  • 设计模式


自定义属性用的很多,这里我就通过Adapter模式来优化一下


先来定义规范


abstract class UnLockBaseAdapter {
// 设置宫格个数
// 例如输入3: 表示3*3
abstract fun getNumber(): Int

// 设置样式
abstract fun getStyle(): JiuGonGeUnLockView.Style

/*
* 作者:史大拿
* 创建时间: 9/14/22 10:24 AM
* TODO 画连接线时,是否穿过圆心
*/

open fun lineCenterCircle() = false

// 设置原始颜色
open fun getOriginColor(): Int = let {
return Color.parseColor("#D8D9D8")
}

// 设置按下颜色
open fun getDownColor(): Int = let {
return Color.parseColor("#3AD94E")
}

// 设置抬起颜色
open fun getUpColor(): Int = let {
return Color.parseColor("#57D900")
}

// 设置错误颜色
open fun getErrorColor(): Int = let {
return Color.parseColor("#D9251E")
}
}

实现:


class UnLockAdapter : UnLockBaseAdapter() {
override fun getNumber(): Int = 5

override fun getStyle(): JiuGonGeUnLockView.Style = JiuGonGeUnLockView.Style.STROKE

override fun getOriginColor(): Int {
return Color.YELLOW
}
}

读取数据:


open var adapter: UnLockBaseAdapter? = null

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)

if (adapter == null) {
throw AndroidRuntimeException("请设置Adapter")
}
adapter?.also {
NUMBER = it.getNumber()
ORIGIN_COLOR = it.getOriginColor()
DOWN_COLOR = it.getDownColor()
UP_COLOR = it.getUpColor()
ERROR_COLOR = it.getErrorColor()
}
}

来看看最终效果:


71F2901C6F6BE9DB19BBF98B95CE3FA0

思路参考自


完整代码


原创不易,您的点赞就是对我最大的帮助


其他自定义文章:



作者:史大拿
来源:juejin.cn/post/7143137578080796686
收起阅读 »

Android开发仿掘金Web端登录界面(Kotlin)

Android开发仿掘金Web端登录界面(Kotlin) 前言 各位大佬好,给大家分享一下用Android原生实现掘金Web端的登录界面效果,有哪些可以优化希望大佬们可以指正,那我们开始吧 最终效果图 前期准备 我们需要先把需要的资源给download下来,...
继续阅读 »

Android开发仿掘金Web端登录界面(Kotlin)


前言


各位大佬好,给大家分享一下用Android原生实现掘金Web端的登录界面效果,有哪些可以优化希望大佬们可以指正,那我们开始吧


最终效果图


LPDS_GIF_20220905_182520.gif


前期准备


我们需要先把需要的资源给download下来,我用Chrome来进行这一步



  • 开启Chrmoe的调试模式: 按F12开启或者在设置->更多工具->开发工具


1662367049960.jpg



  • 开是网络抓包:网络->图片


1662367135318.jpg


这样我们就看到了所需要的图片资料了,我们另存一下放入我们的项目


代码


配置Gradle



  • 我们来配置ViewBinding。在build.gradle中的android添加如下代码:


viewBinding {
enabled = true
}


  • 我们需要添加一些依赖


//glide库
implementation 'com.github.bumptech.glide:glide:4.13.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.13.0'

LoginDialog


我们创建一个LoginDialog.kt文件,并且继承与DialogFragment用于展示登录的UI,具体操作如下



  • dailog_login.xml


layout目录下创建dailog_login.xml文件,用于显示登录的布局,具体代码如下:


<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
//设置这个布局的高度是自适应的
android:layout_height="wrap_content">

//CardView来优化布局(可以快速设置圆角、阴影等操作)
<androidx.cardview.widget.CardView
android:layout_width="0dp"
android:layout_height="wrap_content"
//这里设置88dp是因为最上面的图片高度是96dp,我们这是88dp就可以实现完成重叠效果
android:layout_marginTop="88dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/dialog_top_img">

//为了方便布局在CardView里面添加一个约束布局
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">

<ImageView
android:id="@+id/imageView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="@+id/textView2"
app:layout_constraintEnd_toEndOf="@+id/edit_user"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_baseline_close_24" />

<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="手机登录"
android:textColor="@color/black"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<EditText
android:id="@+id/edit_user"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:background="@drawable/bg_edit"
android:ems="11"
android:hint="请输入手机号码"
android:inputType="number"
android:maxLength="11"
android:paddingStart="100dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView2" />

<EditText
android:id="@+id/edit_pwd"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:background="@drawable/bg_edit"
android:ems="11"
android:hint="请输入密码"
android:inputType="number"
android:maxLength="4"
android:paddingStart="10dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/edit_user" />

<TextView
android:id="@+id/tv_code"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="获取验证码"
android:textColor="#007fff"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="@+id/edit_pwd"
app:layout_constraintEnd_toEndOf="@+id/edit_pwd"
app:layout_constraintTop_toTopOf="@+id/edit_pwd" />

<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="80dp"
android:layout_height="0dp"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="@+id/edit_user"
app:layout_constraintStart_toStartOf="@+id/edit_user"
app:layout_constraintTop_toTopOf="@+id/edit_user">

<TextView
android:id="@+id/textView5"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:text="+86"
android:textColor="#000000" />

<ImageView
android:id="@+id/imageView2"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="1"
android:scaleType="center"
app:srcCompat="@drawable/ic_down" />
</LinearLayout>

<TextView
android:id="@+id/btn_login"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_marginTop="16dp"
android:background="@drawable/bg_btn"
android:gravity="center"
android:text="登录"
android:textColor="@color/white"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="@+id/edit_pwd"
app:layout_constraintStart_toStartOf="@+id/edit_pwd"
app:layout_constraintTop_toBottomOf="@+id/edit_pwd" />

<TextView
android:id="@+id/textView6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="其他登录方式"
android:textColor="#007fff"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="@+id/btn_login"
app:layout_constraintTop_toBottomOf="@+id/btn_login" />

<TextView
android:id="@+id/textView7"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="登录即表示同意"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="@+id/btn_login"
app:layout_constraintTop_toBottomOf="@+id/textView6" />

<TextView
android:id="@+id/textView8"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="用户协议"
android:textColor="#007fff"
android:textSize="16sp"
app:layout_constraintStart_toEndOf="@+id/textView7"
app:layout_constraintTop_toTopOf="@+id/textView7" />

<TextView
android:id="@+id/textView10"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="2dp"
android:text="、"
android:textSize="16sp"
app:layout_constraintStart_toEndOf="@+id/textView8"
app:layout_constraintTop_toTopOf="@+id/textView8" />

<TextView
android:id="@+id/textView9"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="隐私政策"
android:textColor="#007fff"
android:textSize="16sp"
app:layout_constraintStart_toEndOf="@+id/textView10"
app:layout_constraintTop_toTopOf="@+id/textView7" />

</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>

<ImageView
android:id="@+id/dialog_top_img"
android:layout_width="142dp"
android:layout_height="96dp"
android:elevation="2dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_login_2" />

</androidx.constraintlayout.widget.ConstraintLayout>


  • bg_edit.xml


drawable目录下创建bg_edit.xml的资源文件,设置EditText的样式,代码如下:


<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
//不对焦的样式
<item android:state_window_focused="false"
android:drawable="@drawable/bg_edit_nofocused"/>

//对焦的样式
<item android:state_focused="true"
android:drawable="@drawable/bg_edit_focused" />

</selector>


  • bg_edit_nofocused


<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="2px"/>
<solid android:color="@color/white"/>
<stroke android:color="#e4e6eb" android:width="1dp"/>
</shape>


  • bg_edit_focused


<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/white"/>
<stroke android:color="#007fff" android:width="1dp"/>
</shape>


  • bg_btn.xml


drawable目录下创建bg_btn.xml的资源文件,设置TextView的样式,不用Button是因为设置background较为麻烦,代码如下


<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">

<solid android:color="#007fff"/>
<corners android:radius="2px"/>
</shape>


  • LoginDialo


LoginDialog.kt中代码具体如下:


class LoginDialog : DialogFragment() {
//使用viewBinding
lateinit var mBinding: DialogLoginBinding

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
)
: View {
//创建布局
mBinding = DialogLoginBinding.inflate(layoutInflater)
return mBinding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//初始化Dialog的相关配置
initDialog()
}

/**
* 初始化dialog相关配置
*
*/

private fun initDialog() {
//设置Dialog的显示大小
setDialogSize()

//设置window的背景为透明色
dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))

//设置点击空白和返回键不消失
dialog?.setCanceledOnTouchOutside(false)

//设置dialog的动画
dialog?.window?.setWindowAnimations(R.style.dialog_base_anim)
}

/**
* 设置dialog的大小
*
*/

private fun setDialogSize(){
val window = dialog?.window
window?.let {

//获取屏幕信息
val wm = requireContext().getSystemService(Context.WINDOW_SERVICE) as? WindowManager
val display = wm?.defaultDisplay
val point = Point();
display?.getSize(point);


val layoutParams = it.attributes;

//设置宽度为屏幕的百分之90
layoutParams.width = (point.x * 0.9).toInt()
//设置高度为自适应
layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT
it.attributes = layoutParams
}
}
}

MainActivity



  1. 我们修改一下MainActivity,实现展示一个登录Button,点击后弹出登录界面,具体代码如下:


class MainActivity : AppCompatActivity() {

lateinit var mBinding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(mBinding.root)

mBinding.button.setOnClickListener {
LoginDialog().show(supportFragmentManager, "")
}
}
}

运行一下


这个时候我们的UI大致就完成了,我们运行看一下,是不是我们所有期望的那样


Screenshot_20220905_171401_com.juejin.login.jpg


登录逻辑


我们完成了UI相关的功能,接下来我们需要开始写,登录相关的逻辑了



  • 点击不同输入框显示不同UI


在web端中,当我们点击输入手机号和请输入密码时,最上面的UI是显示不同,我们先把这个一部分功能实现以下:


我们添加一个initView()方法,专门初始化View相关操作,具体代码如下:


private fun initView() {
//设置焦点变化监听
mBinding.editUser.onFocusChangeListener =
View.OnFocusChangeListener { v, hasFocus ->
//该控件获取了焦点
if(hasFocus){
//设置获取焦点后的UI
Glide.with(this).load(R.drawable.ic_login_2).into(mBinding.dialogTopImg)
}
}

mBinding.editPwd.onFocusChangeListener =
View.OnFocusChangeListener { v, hasFocus ->
//该控件获取了焦点
if(hasFocus){
//设置获取焦点后的UI
Glide.with(this).load(R.drawable.ic_login_1).into(mBinding.dialogTopImg)
}
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//让输入框获取焦点
mBinding.editUser.requestFocus()
}
}

获取验证码


我们知道点击获取验证码会出现一个验证是否为人为操作,当操作完成后发送验证码,并且会有一个60s间隔,并且需要显示出实际秒数,我们使用Captcha库来完成验证,使用CountDownTimer来实现倒计时的效果


添加验证拼图



  • 添加倒计时


val timeDown = object : CountDownTimer(60 * 1000, 1000) {
override fun onTick(millisUntilFinished: Long) {
mBinding.tvCode.text = "${millisUntilFinished / 1000}s"
}

override fun onFinish() {
//设置验证码可点击
mBinding.tvCode.isEnabled = true
//恢复text
mBinding.tvCode.text = "获取验证码"
}

}


  • 添加依赖


implementation 'com.luozm.captcha:captcha:1.1.2'


  • 添加布局


<com.luozm.captcha.Captcha
android:id="@+id/capt_cha"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="2dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/cardView"
app:layout_constraintStart_toStartOf="@+id/cardView"
app:layout_constraintTop_toTopOf="@+id/cardView"
//随便找一个图片就行了
app:src="@drawable/ic_captcha" />


  • 添加事件监听


mBinding.captCha.setCaptchaListener(object : Captcha.CaptchaListener {
/**
* 验证通过回调
*
* @param time
* @return
*/

override fun onAccess(time: Long): String {
//设置验证码不可点击
mBinding.tvCode.isEnabled = false
//开始倒计时
timeDown.start()
//关闭图片验证
mBinding.captCha.visibility = View.GONE
return "验证通过,耗时" + time + "毫秒";
}

/**
* 验证失败回调
*
* @param failCount
* @return
*/

override fun onFailed(failCount: Int): String {
return "验证失败,已失败" + failCount + "次";
}

override fun onMaxFailed(): String {
Toast.makeText(
this@LoginDialog.requireContext(),
"验证超过次数,你的帐号被封锁",
Toast.LENGTH_SHORT
).show();
return "验证失败,帐号已封锁";
}

})


  • 点击发送验证码


mBinding.tvCode.setOnClickListener {
//显示图片验证
mBinding.captCha.visibility = View.VISIBLE
}

登录



  • 添加登录判断逻辑


我们还是在initView方法中添加代码:


mBinding.btnLogin.setOnClickListener {
//登录按钮不可交互
mBinding.btnLogin.isEnabled =false

//修改UI
mBinding.btnLogin.text = "登录中..."

//开始验证
//判断手机号格式是否正确,这里只做了长度的按断,其实可以用正则来判断,我这里知识简单判断
if(mBinding.editUser.text.toString().length < 11){
Toast.makeText(
this@LoginDialog.requireContext(),
"账号格式错误",
Toast.LENGTH_SHORT
).show();
//登录按钮可交互
mBinding.btnLogin.isEnabled =true
//修改UI
mBinding.btnLogin.text = "登录"
return@setOnClickListener
}
if(mBinding.editPwd.text.toString().length < 4){
Toast.makeText(
this@LoginDialog.requireContext(),
"验证码错误",
Toast.LENGTH_SHORT
).show();
//登录按钮可交互
mBinding.btnLogin.isEnabled =true
//修改UI
mBinding.btnLogin.text = "登录"
return@setOnClickListener
}

Toast.makeText(
this@LoginDialog.requireContext(),
"登录成功",
Toast.LENGTH_SHORT
).show();

dismiss()

}

总结


到这里我们模仿掘金Web端登录就成功了,如果想看源码在这里传送门


作者:zuoz
来源:juejin.cn/post/7139841541350588447
收起阅读 »

Android 隐私合规检测

目前应用市场的隐私合规检查越来越严格,各大手机厂商的检测标准也不一致,经常有这个平台过审了那个平台还有问题出现,按照工信部的要求,工信部隐私合规说明。隐私合规是个不可不重视的点。 我们通常遇到的主要问题: 在用户同意隐私协议之前,不能有收集用户隐私数据的行...
继续阅读 »

目前应用市场的隐私合规检查越来越严格,各大手机厂商的检测标准也不一致,经常有这个平台过审了那个平台还有问题出现,按照工信部的要求,工信部隐私合规说明。隐私合规是个不可不重视的点。


我们通常遇到的主要问题:



  • 在用户同意隐私协议之前,不能有收集用户隐私数据的行为。例如:在用户同意协议之前不能去获取 Android ID、Device ID、MAC 等隐私数据。




  • 在用户同意隐私协议之后,获取权限时必须要符合当前使用场景,例如:我们需要获取手机读写,相机权限,这种需要在真正的读写,打开相机等页面时才能去请求权限。




如上问题处理可分为两种:权限 和 隐私 


  • 权限 需要在对应页面即 app内获取权限时主动设置弹窗等方式给予app相应的权限


'如电话权限,定位权限,相机权限,浮窗权限,读写权限等。在每个申请危险权限前,都需要弹窗说明权限解释说明。'


  • 隐私 为app使用过程中与用户个人相关的个人信息


'如位置,Mac地址,设备id等。就Android端而言,多数隐私信息需要对应授权后才能获取,但目前仍存在部分隐私信息无需授权就可以拿到的'

如何检测


一、第三方检测

京数安扫描平台
国舜
网易云盾


二、静态检测



  • Lint 检查项目


    Lint用于检测静态代码和资源,找到其中不符合预定义规则的地方。可参考网易云隐私合规静态检查




  • 反编译查找对应方法


    反编译主要是为了找出第三方的一些不合规方法调用,但是比较麻烦,全局搜索很不方便




三、动态检测(开源)



  • 1、Xposed


    优点 :Xposed 是比较早的做hook的框架, Xposed框架可以在不修改APK文件的情况下影响程序运行(修改系统)的框架服务,基于它可以制作出许多功能强大的模块,且在功能不冲突的情况下同时运作。Android中一般存在两种hook:sdk hook和ndk hook。native hook的难点在于理解ELF文件与学习ELF文件,Java层Hook则需要了解虚拟机的特性与java上的反射使用。另外还存在全局hook,即结合sdk hook和ndk hook,xposed就是一种典型的全局hook框架。


    缺点:需要手机ROOT




  • 2、VirtualXposed


    优点 :VirtualXposed 是基于VirtualApp 和 epic 实现的,能在非ROOT环境下直接运行Xposed模块 (目前支持5.0~10.0)。其实VirtualXposed就是一个支持Xposed的虚拟机,我们把开发好的Xposed模块和对应需要hook的App安装上去就能实现hook功能。


    缺点:步骤相对麻烦,de.robv.android.xposed 的依赖需要翻墙。




  • 3、epic


    优点 :配置简单,属于运行时hook,说明在动态加载dex也能检测到,也是我目前再用的,可以自定义配置hook 对应的类和方法,并找出当前调用线程堆栈,直接定位到调用的方法。


    缺点:兼容问题,Android 11及以上只能支持 64位,不过这个不影响11以下的使用;只检测java类代码,native没有hook 。




  • 4、PrivacySentry


    接入相对复杂,基于自定义transform , 编译期注解+hook方案,第一个transform收集需要拦截的敏感函数,第二个transform替换敏感函数,运行期收集日志,同时支持游客模式。


    有java.util.zip.ZipException: duplicate entry: META-INF/INDEX.LIST 冲突风险。




  • 5、camille


    使用 python Frida 等工具命令,做hook 模块,手机需要Root,功能强大但相对复杂




  • 6、自定义Asm插件,做代码插入检测


    可以在class->dex时,对相应的类、调用方法,做检测。添加我们的拦截代码




四、epic落地


  • 我这里使用的时 epic 检测,直接依赖:


implementation 'me.weishu:epic:1.0.0'
implementation 'me.weishu.exposed:exposed-xposedapi:0.4.5'

主要核心是 DexposedBridge.findAndHookMethod 方法


//targetClass: 传入 需要hook 的类,如:TelephonyManager.class
//targetMethod:类对应的方法,如:getDeviceId
DexposedBridge.findAndHookMethod(targetClass, targetMethod, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);

//被调用的类名
String className = param.method.getDeclaringClass().getName();
//被调用的函数名
String methodName = param.method.getName();
LogAction.log("检测到 " + className + " 被调用: methodName=" + methodName);
//这里可以搜集当前的线程信息,堆栈等,将调用关系打印出来,例如:

//Thread thread = Thread.currentThread();
//StringBuilder stringBuilder = new StringBuilder();
//获取线程信息
//String threadInfo = getThreadInfo(thread);
//stringBuilder.append(threadInfo);
// 返回表示此线程的堆栈转储的堆栈跟踪元素数组。
// 如果这个线程还没有启动,已经启动但还没有被系统计划运行,或者已经终止,这个方法将返回一个零长度的 数组。
//StackTraceElement[] stackTraceElements = thread.getStackTrace();
//String print = printToString2(stackTraceElements);
//stringBuilder.append("线程堆栈日志:").append(print);

//LogAction.log(stringBuilder);
}

@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);

}
});

例如,我这里用了 leakcanary 做检测时会提示的


image.png


因为我对 android.app.ApplicationPackageManager 这个类做了检测,queryIntentActivities 方法被调用时即触发了beforeHookedMethod


五、集成优化处理



  • 我们可以自己定义一个module模块,单独处理合规检测,利用 debugImplementation 的方式集成,不会影响到线上




  • 可以使用 ContentProvider 做初始化入口,debugImplementation 集成进来即可,在 ContentProvider onCreate 的时候去 start启用 需要hook 的集合类。




  • 可以使用企业微信提供 API  Token,在收到 隐私限制方法被调用时,触发消息发送,方便测试和提示,不需要去看log日志。


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

乡愁

立秋了,你是否开始感到有一丝凉爽了呢?我还没有,关中平原的闷热已经让我生无可恋了,不过此刻故乡的天气甚是宜人,稻田里面的稻子再过一段时间应该就熟了吧,你是否会因为秋而感到一丝乡愁呢! 乡愁/书文 盛夏,路边的绿茵总让人有一丝的惬意,城市里绿化生长出来的木棉总...
继续阅读 »

立秋了,你是否开始感到有一丝凉爽了呢?我还没有,关中平原的闷热已经让我生无可恋了,不过此刻故乡的天气甚是宜人,稻田里面的稻子再过一段时间应该就熟了吧,你是否会因为秋而感到一丝乡愁呢!



乡愁/书文


盛夏,路边的绿茵总让人有一丝的惬意,城市里绿化生长出来的木棉总让人想起它二三月的木棉花,那样的繁花似锦,花开又花落。
花开有时,诚然我们都会淡忘,好比吾日三省吾身,今天你业绩够了么?意向客户呢?客户流失原因是什么?你来说说原因?谁会关心花开花落呢?现实的情况可容不得你沾花惹草,或者让你在池塘里摸鱼?写这些不是写给谁,也不是取悦别人或者迎合别人,纯粹写给自己,说真的闲暇之余好多时间净泡在短视频的世界里了,我想:除了短视频里你所看到的世界还有另外一个你自己导演的短视频。


短视频里面的大佬说着动不动月入上万是一件多么轻松的事。(根据国家统计局全国有6亿人民的月收入是1000)
我曾想所谓的大佬是不是都飘了,真实的世界是真的限制我贫穷的想象了么,其实不然,财富的本身是对物质的追求,当欲望达不到本身的需求时都会变本加厉,但当一个企业也为之生存而不断革新寻求人最终求生的欲望时,那这便叫财富。财富也可以指精神上的财富,心里有了归属才会有家,有了家才会有感情,有了感情才会有了乡愁。


我前些天悄悄把头像改成猫的头像,甚至动摇我发过的毒誓:自此我家再也不养猫
说到猫我些许年前我写的《猫》着实是摘抄了余光中老先生的嫌疑,我要想一想当时写的是不是这样:



小时候

母亲挑着扁担

一头是菜篮子里面有我们爱吃的酸菜豆米

另一头是菜篮子里面有小喵咪爱吃的猪肺肉

长大后

母亲仍然挑着扁担

一头是菜篮子里的秤砣

另一头还是猪肺肉

提到酸菜豆米

至此便有了乡愁



说到吃的我又来劲了,但这不包括我们老家的荞凉粉、水晶凉粉、肠旺面、竹荪、酸菜豆米这些,当然什么北京的刷羊肉,厦门的沙茶面,兰州的拉面,长沙的臭豆腐,山西的刀削面,广东的肠粉,陕西的肉夹馍,东北的猪肉炖粉条,这些我都吃过,除了台湾的担仔面…


此时脑海里瞅见一位歌手在台上说着:哎呦,不错哦!
这让我想起儿时的儿歌:天上的星星不说话,地上的娃娃想妈妈…
说到歌,我着实喜欢蔡琴的《恰是你的温柔》和《被遗忘的时光》跟李宗盛的《爱的代价》。
“走吧!走吧!人总要学着自己长到…”曾经何时,我是那么的颠沛流离呀,涛声依旧,海风冷飕飕把我脸颊吹得发福的望着年三十的夜景,自己第一次在岛上过年,第一次在外有了乡愁。


冒昧问一下,我今年刚回去的家叫不叫乡愁呢,去的时候老家还在下着雪,来的时候已是木棉花盛开的季节


驿站/雷


同事去我的家乡出差,偶然遇到一个路牌,上面写着:“离乡的游子不会明白,当他离开的那一刻,故乡就成了驿站”。同事将这段话发给了我,那一瞬间,我不知道自己在想什么,整个人就像存在一种空明的状态中,我自己也感觉不到我的的存在,我好像已经不是我了!



“起床喽!太阳晒屁股,唱哈不起勒,赶紧起来吃饭了”。




“嬢,你着菜唱卖嘛”“咦!唱会着贵嘛,便宜的,天天来你家买起勒”。




“老板,来份烙锅,再来碗荞凉粉,记到加皮蛋哦”。




“不晓得你着大勒人了,一日三餐都不会按时吃,不按时吃对身体不好勒”。



看到那句话后,这些发生在家乡的画面总是在脑海中浮现,父母的叮嘱、生活的琐碎、好友的相聚,至今让人一直想去捕捉、想去寻找,想去看清那些细节,却又是那么触手可及而又模糊不清。于是只能强忍内心的好奇和冲动,默默地行走在不属于自己的道路上。


乡愁是什么?是飘向远方的蒲公英无法回到母亲身边的无奈,风往哪吹,它便往哪走!是茫茫戈壁中胡杨找不到归属的感慨,光往哪散,它便往哪长,是漂泊他乡的游子最简单、纯粹的情绪,如一缕清水绵绵不绝!母校后上的白马塔,乡村茶山顶的瞭望台,小城中心处的织金阁以及歪头山上的那一片松树林,他们共同眺望着那走在高速上逐渐拉长的影子,呢喃着:“过年你要回家吗?”


离乡一年有余,中途虽回过两次家,但也如同匆匆过客一般,草草和亲人交谈两句,同好友小酌两杯,还不待仔细观赏一下家乡的风景,又急忙踏上离开的路途,去为生活、为理想而奔波,来不及去细细体会回家的那种温馨和愉悦。或许正如那句话一样故乡已然成为了游子的驿站,它仅仅只是一个游子小憩片刻的场所,只能为短暂归乡的人们提供临时的心灵庇护,走在车水马龙的闹市中,眺望着络绎不绝的人群,自己是那么的显眼,那么的格格不入,仿佛这不是自己的家。


漫街的落叶无不在诉说着,风已经有了秋的味道,而我却只能低头细语:“我也不知道过年回不回家”。


同一片星空下,此时的他乡,寡情薄意的乌云将游子唯一寄托思念的月光遮挡,而故乡也是否如此呢?离乡的游子明白了,但此时他已经在异乡了。


时光的表/小四


”年轻的游子不会明白,在离开的那一刻,故乡永远成为了驿站“,他坐的一列列动车无数次路过故乡的田野,一次次飞机穿过故乡顶上的大气层,无数次的故乡金曲掠过耳旁,泪花一次次地投射出门前的几颗杉树,时针一次次地滴答着心灵!


父亲年轻的时候是个修表的工匠,不过从我记事起,他那堆修表工具和一堆破烂的手表已经在一个破旧的麻袋里面沾满了灰尘,于是就过上了背井离乡的打工生活,四处奔波,只求一家人的生活,他能够修复时间,却修复不了满脸的皱纹,父亲可能现在还会记得他年少时在集市上修表的时光吧,就像现在的我时不时的会想起那个夕阳落山的午后,父亲在屋檐下抽着烟,母亲带着眼镜织着毛线拖鞋,我拿着那把破木吉他唱着许巍的《故乡》。


十四岁时,情窦初开,喜欢上一个女孩子,在懵懂无知的年纪里,在百度上搜了表白话语,学会了表白,于是有了一段两小无猜的时光,于是你满眼都是她,送她糖,送她巧克力,也会舞文弄墨的送她画,送她诗,后来,你选了两块”廉价“表,她戴一块,你也戴了一块,她的表秒针比你慢了一秒,你的比她快了一秒,你都会将其调节到零误差,不过,就算两块表的时间零误差,懵懂的开始也会以懵懂结束,过了很多年,你们都没有怎么见过面,你们曾经一起跑步的那片区域已经立起一座又一座亭阁,夜夜灯火通明,游人不断,又过了很多年,你结婚生子,我还在求学,每次回到故乡,我都会去那边走一走,看一看,我们作为独立的个体在一直在各自的世界里生活,寻找,看到你那么开心,我为你开心,我想告诉你,你的选择是对的,我的选择也是对的,你最喜欢听的那首《羞答答的玫瑰静悄悄的开》,也在慢慢的绽放它留给你的情怀!


那一年,你刚进入大学不久,假期你回到了家中,你和好友二人吃着烧烤,喝着高度白酒,他问你学习如何,你问他生活怎么样,一直聊到夜深人静,你们俩醉醺醺的,一个搀扶着一个,在飘着毛毛雨的街道走着,你看见他手上依然戴着高二时你送他的那块表,只不过你的那块在高中毕业就丢了,你只感觉心里一酸,他说这两年我都戴着这块表呢,此后的四五年时光里,我们的见面次数越来越少,他辗转了无数的地方,换了无数的职业,我也为了工作和生活四处行走,你给我过你在北戴河的列车上无比的绝望,在浙江的苦楚生活,我知道你现在的压力很大,日子很不好过,但是我不会去安慰你,你既然选择出来了,那么就应该承担一切后果。


时光稍纵即逝,父亲的修表生涯早在二十世纪就结束了,懵懂年龄慢一秒,快一秒的美好时光早已褪色,在北戴河的绝望,杭州的等待依然要继续,我们手里都有一块表,戴着的时候是有形的表,不戴的时候是无形的表,不论秒针是否跳动,时间都在跳动,这块表是时

作者:刘牌
来源:juejin.cn/post/7129129354096803848
间,更是滴答的乡愁!

收起阅读 »

Android进程间大数据通信:LocalSocket

前言 说起Android进行间通信,大家第一时间会想到AIDL,但是由于Binder机制的限制,AIDL无法传输超大数据。 那么我们如何在进程间传输大数据呢? Android中给我们提供了另外一个机制:LocalSocket 它会在本地创建一个socket通道...
继续阅读 »

前言


说起Android进行间通信,大家第一时间会想到AIDL,但是由于Binder机制的限制,AIDL无法传输超大数据。


那么我们如何在进程间传输大数据呢?


Android中给我们提供了另外一个机制:LocalSocket


它会在本地创建一个socket通道来进行数据传输。


那么它怎么使用?


首先我们需要两个应用:客户端和服务端


服务端初始化


override fun run() {
server = LocalServerSocket("xxxx")
remoteSocket = server?.accept()
...
}

先创建一个LocalServerSocket服务,参数是服务名,注意这个服务名需要唯一,这是两端连接的依据。


然后调用accept函数进行等待客户端连接,这个函数是block线程的,所以例子中另起线程。


当客户端发起连接后,accept就会返回LocalSocket对象,然后就可以进行传输数据了。


客户端初始化


var localSocket = LocalSocket()
localSocket.connect(LocalSocketAddress("xxxx"))

首先创建一个LocalSocket对象


然后创建一个LocalSocketAddress对象,参数是服务名


然后调用connect函数连接到该服务即可。就可以使用这个socket传输数据了。


数据传输


两端的socket对象是一个类,所以两端的发送和接受代码逻辑一致。


通过localSocket.inputStreamlocalSocket.outputStream可以获取到输入输出流,通过对流的读写进行数据传输。


注意,读写流的时候一定要新开线程处理。


因为socket是双向的,所以两端都可以进行收发,即读写


发送数据


var pool = Executors.newSingleThreadExecutor()
var runnable = Runnable {
try {
var out = xxxxSocket.outputStream
out.write(data)
out.flush()
} catch (e: Throwable) {
Log.e("xxx", "xxx", e)
}
}
pool.execute(runnable)

发送数据是主动动作,每次发送都需要另开线程,所以如果是多次,我们需要使用一个线程池来进行管理


如果需要多次发送数据,可以将其进行封装成一个函数


接收数据


接收数据实际上是进行while循环,循环进行读取数据,这个最好在连接成功后就开始,比如客户端


localSocket.connect(LocalSocketAddress("xxx"))
var runnable = Runnable {
while (localSocket.isConnected){
var input = localSocket.inputStream
input.read(data)
...
}
}
Thread(runnable).start()

接收数据实际上是一个while循环不停的进行读取,未读到数据就继续循环,读到数据就进行处理再循环,所以这里只另开一个线程即可,不需要线程池。


传输复杂数据


上面只是简单事例,无法传输复杂数据,如果要传输复杂数据,就需要使用DataInputStreamDataOutputStream


首先需要定义一套协议。


比如定义一个简单的协议:传输的数据分两部分,第一部分是一个int值,表示后面byte数据的长度;第二部分就是byte数据。这样就知道如何进行读写


写数据


var pool = Executors.newSingleThreadExecutor()
var out = DataOutputStream(xxxSocket.outputStream)
var runnable = Runnable {
try {
out.writeInt(data.size)
out.write(data)
out.flush()
} catch (e: Throwable) {
Log.e("xxx", "xxx", e)
}
}
pool.execute(runnable)

读数据


var runnable = Runnable {
var input = DataInputStream(xxxSocket.inputStream)
var outArray = ByteArrayOutputStream()
while (true) {
outArray.reset()
var length = input.readInt()
if(length > 0) {
var buffer = ByteArray(length)
input.read(buffer)
...
}
}

}
Thread(runnable).start()

这样就可以传输复杂数据,不会导致数据错乱。


传输超大数据


上面虽然可以传输复杂数据,但是当我们的数据过大的时候,也会出现问题。


比如传输图片或视频,假设byte数据长度达到1228800,这时我们通过


var buffer = ByteArray(1228800)
input.read(buffer)

无法读取到所有数据,只能读到一部分。而且会造成后面数据的混乱,因为读取位置错位了。


读取的长度大约是65535个字节,这是因为TCP被IP包包着,也会有包大小限制65535。


但是注意!写数据的时候如果数据过大就会自动进行分包,但是读数据的时候如果一次读取貌似无法跨包,这样就导致了上面的结果,只能读一个包,后面的就错乱了。


那么这种超大数据该如何传输呢,我们用循环将其一点点写入,也一点点读出,并根据结果不断的修正偏移。代码:


写入


var pool = Executors.newSingleThreadExecutor()
var out = DataOutputStream(xxxSocket.outputStream)
var runnable = Runnable {
try {
out.writeInt(data.size)
var offset = 0
while ((offset + 1024) <= data.size) {
out.write(data, offset, 1024)
offset += 1024
}
out.write(data, offset, data.size - offset)
out.flush()
} catch (e: Throwable) {
Log.e("xxxx", "xxxx", e)
}

}

pool.execute(runnable)

读取


var input = DataInputStream(xxxSocket.inputStream)
var runnable = Runnable {
var outArray = ByteArrayOutputStream()
while (true) {
outArray.reset()
var length = input.readInt()
if(length > 0) {
var buffer = ByteArray(1024)
var total = 0
while (total + 1024 <= length) {
var count = input.read(buffer)
outArray.write(buffer, 0, count)
total += count
}
var buffer2 = ByteArray(length - total)
input.read(buffer2)
outArray.write(buffer2)
var result = outArray.toByteArray()
...
}
}
}
Thread(runnable).start()

这样可以避免因为分包而导致读取的长度不匹配的问题


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

用ChatGPT提高开发效率(Andoid)

一、前言 我们问了10个问题,开发的、生活中有疑问的,ChartGPT都给了我们答案。这些答案怎么样,请往下看吧。 ChatGPT-1、写一个车牌号的正则表达式 ChatGPT-2、写一个中国大陆车牌号的正则表达式 ChatGPT-3、用kotlin写一个车...
继续阅读 »

一、前言


我们问了10个问题,开发的、生活中有疑问的,ChartGPT都给了我们答案。这些答案怎么样,请往下看吧。



  • ChatGPT-1、写一个车牌号的正则表达式

  • ChatGPT-2、写一个中国大陆车牌号的正则表达式

  • ChatGPT-3、用kotlin写一个车牌号的校验函数

  • ChatGPT-4、用Kotlin写一个String装换成金额的扩展函数

  • ChatGPT-5、用Kotlin写一个String装换成人民币的扩展函数

  • ChatGPT-6、用Kotlin写一个String转换成人民币的扩展函数

  • ChatGPT-7、用Kotlin写一个将TextView的文本内容复制到粘贴板的扩展函数

  • ChatGPT-8、美食探店怎么做?

  • ChatGPT-9、台湾卤肉饭?

  • ChatGPT-10、牛皮弃面馆(柏庐南路店)


二、ChatGPT使用初体验


ChatGPT-1、写一个车牌号的正则表达式
企业微信截图_16796251027919.png


ChatGPT-2、写一个中国大陆车牌号的正则表达式
企业微信截图_1679625500776.png
^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼][A-HJ-NP-Z](?!DD)[A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳]$


ChatGPT-3、用kotlin写一个车牌号的校验函数
企业微信截图_16796251209911.png


三、ChatGPT PK 5年+年程序员


ChatGPT-4、用Kotlin写一个String装换成金额的扩展函数
企业微信截图_16796251209911.png


ChatGPT-5、用Kotlin写一个String装换成人民币的扩展函数
企业微信截图_16796251209911.png


ChatGPT-6、用Kotlin写一个String转换成人民币的扩展函数
1111.png
PK
image2023-3-24_11-2-10.png


ChatGPT-7、用Kotlin写一个将TextView的文本内容复制到粘贴板的扩展函数
image2023-3-24_11-2-10.png
PK
image2023-3-24_11-3-0.png


四、总结


ChatGPT-1、写一个车牌号的正则表达式

ChatGPT-2、写一个中国大陆车牌号的正则表达式

ChatGPT-3、用kotlin写一个车牌号的校验函数


以上问题,明确的告诉了我们答案,并且配上了讲解和使用方法。


极大的减少了我们自己写正则的错误概率,或者网上查,答案质量参差不齐的筛选验证的烦恼。


对这种重复劳动、通用的功能函数,ChatGPT很好用


ChatGPT-4、用Kotlin写一个String装换成金额的扩展函数

ChatGPT-5、用Kotlin写一个String装换成人民币的扩展函数

ChatGPT-6、用Kotlin写一个String转换成人民币的扩展函数

ChatGPT-7、用Kotlin写一个将TextView的文本内容复制到粘贴板的扩展函数


以上问题,实现了在开发过程中实际的问题,直接Copy就可以使用


请注意我的问题,用Kotlin写一个String转换成人民币的扩展函数,只要你问题问的明确,ChatGPT就会给你想要的答案


ChatGPT-4、ChatGPT-5转换写错了,写成了装换,ChatGPT理解了我的意思,并且他自我纠正了


ChatGPT-6、ChatGPT-7,ChatGPT与5年+程序员的PK,功能都实现了,实现思路基本相同。但是5+程序员写的更简洁,自由 (程序员的个人素质了,Lewis对个人要求标准较高,这就是通往大佬的之路)。


你在通往IT大佬的路上,不要轻视ChatGPT哦,我们使用的ChatGPT是通用版,训练出来的模型是面向所有用户的。


一个假设,如果使用Githut上所有的代码训练ChatGPT,训练出来的模型还会比不过5年的程序员吗?Githut+ChatGPT好像都为微软的,这个假设可能已经在实验室阶段了。


以上假设已经有了,GitHub Copilot X GitHub + GPT-4联手的产品


五、ChatGPT最后的胡说八道


ChatGPT-8、美食探店怎么做?
企业微信截图_16796255651978.png


ChatGPT-9、台湾卤肉饭?
企业微信截图_16796256304717.png


ChatGPT-10、牛皮弃面馆(柏庐南路店)
企业微信截图_1679625678718.png


ChatGPT-8、美食探店怎么做?

ChatGPT-9、台湾卤肉饭?

ChatGPT-10、牛皮弃面馆(柏庐南路店)


以上问题,初一看ChatGPT回答的很专业,以下详细说一下


ChatGPT-8、ChatGPT-9,很多人都有疑问的问题,网上资料很多,回答的很专业,也很有条理,挺好。作为一个技术就不都说了,可以看看这个「打不过,就加入」,我和ChatGPT的故事


ChatGPT-10,昆山一家小店,没有名气,主打台湾牛肉面和台湾小吃,ChatGPT就开始胡说八道了,但是显得很专业,他谈的模板就是按照逻辑去介绍一家店。如果你问他一家有名的店或者连锁店(例如:海底捞),他会回答的应该很专业。


其实还有很多疑问的,移动开发未来前景怎么样?ETH今天会不会大跌,短线做空可以吗?,但是毕竟今天周五了,让自己过一个愉快的周末吧^_^。


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

面试官:判断图是否有环

web
面试官让我写一个判断图是否有环,我没写出来,心想又是“面试造火箭,入职拧螺丝”。我把面试官pass了。没想到开发中真的遇到了判断有向图是否有环。 图是一种常见的数据结构,分为有向图和无向图。图是由边和节点组成的。 在前端开发中,接触到图的场景不算多。常见的有流...
继续阅读 »

  • 面试官让我写一个判断图是否有环,我没写出来,心想又是“面试造火箭,入职拧螺丝”。我把面试官pass了。没想到开发中真的遇到了判断有向图是否有环。

  • 图是一种常见的数据结构,分为有向图和无向图。图是由边和节点组成的。

  • 在前端开发中,接触到图的场景不算多。常见的有流程、图形可视化等场景。

  • 我们在配置题目流程时遇到了需要判断图是否有环的需求。


背景



  • 简单介绍需求,通过可视化流程配置答题流程,题目与题目之间用线连接,箭头的方向代表下一个题目。回答完当前题目,根据不同的条件,跳到下一题;如果题目流程中有循环,会导致答题流程无法结束,所以需要校验题目的流程中不能有循环。

  • 下面的是有循环,不符合条件


image.png



  • 下面的是无循环,符合条件


image.png
image.png
image.png
image.png


技术方案



  • 根据需求,我们把题目的流程配置抽象成有向图,题目是节点,题目之间的连线是边。

  • 需求里的有无循环,最终可以转换成图是否有环的问题。从图的某个节点作为起点,根据边的方向出发跳到下一个节点,最终是否回到起点。如果回到起点,就是有循环、有环,否则是无循环、无环。

  • 去除题目和各种条件等无关的结构,数据结构如下。


//边
export interface Edge {
id: string;
source: {
cell: string; //这条边的起点的id
[x: string]: any;
};
target: {
cell: string; //这条边的终点的id
[x: string]: any;
};
data: {
type: 'EDGE',
[x: string]: any;
}
[x: string]: any;
};
//节点
export interface Node {
id: string;
data: {
type: 'NODE';
name: string;
[x: string]: any;
};
[x: string]: any;
};
export type Data = Node | Edge;


  • 测试数据如下


const data: Data[] = [
{
id: '1',
data: {
type: 'NODE',
name: '节点1'
}
},
{
id: '2',
data: {
type: 'NODE',
name: '节点2'
}
},
{
id: '3',
data: {
type: 'NODE',
name: '节点3'
}
},
{
id: '4',
source: {
cell: '1'
},
target: {
cell: '2'
},
data: {
type: 'EDGE'
}
},
{
id: '5',
source: {
cell: '1'
},
target: {
cell: '3'
},
data: {
type: 'EDGE'
}
}
];


  • 根据数据结构和测试数据data:Data[],分为以下几个步骤:

    1. 获得边的集合和节点的集合。

    2. 根据边的集合和节点的集合,获得每个节点的有向邻居节点的集合。即以每个节点的为起点,通过边连接的下一个节点的集合。例如测试数据节点1,通过边id4和边id5,可以连接节点2节点3,所以节点1的邻居节点是节点2节点3,而节点2节点3无有向邻居节点。

    3. 最后根据有向邻居节点的集合,判断是否有环。




具体实现



  • 获得边的集合和节点的集合


const edges: Map<string, Edge> = new Map(), nodes: Map<string, Node> = new Map();
const idMapTargetNodes: Map<string, Node[]> = new Map();
const initGraph = () => {
for (const item of data) {
const { id } = item;
if (item.data.type === 'EDGE') {
edges.set(id, item as Edge);
} else {
nodes.set(id, item as Node);
}
}
};


  • 获取有向邻居节点的集合,这里的集合,可以优化成id。我为了方便处理,存储了节点


const idMapTargetNodes: Map<string, Node[]> = new Map();
const initTargetNodes = () => {
for (const [id, edge] of edges) {
const { source, target } = edge;
const sourceId = source.cell, targetId = target.cell;
if (nodes.has(sourceId) && nodes.has(targetId)) { //防止有空的边,即边的起点和终点不在节点的集合里
const targetNodes = idMapTargetNodes.get(sourceId);
if (Array.isArray(targetNodes)) {
targetNodes.push(nodes.get(targetId) as Node);
} else {
idMapTargetNodes.set(sourceId, [nodes.get(targetId) as Node]);
}
}
}
};


  • 最后判断是否有环,有两种方式:递归和循环。都是深度优先遍历。execute是遍历所有节点,hasCycle是把图的某个节点做为起点,判断是否有环。如果以所有节点为起点,都没有环,说明这个图没有环。

    1. 递归。hasCycle判断当前节点是否有环;checked是做优化,防止某些节点多次检查,回溯阶段,把当前节点加入checkedvisited记录当前执行的hasCycle里是否访问过,如果访问过,就是有环。需要注意的是,每次执行hasCycle时,visited用的是一个变量,所以在回溯阶段需要把当前节点从visited里删除。


    const checked: Set<string> = new Set();
    const hasCycle = (node: Node, visited: Set<Node>) => {
    if (checked.has(node.id)) return false;
    if (visited.has(node)) return true;
    visited.add(node);
    const { id } = node;
    const targetNodes = idMapTargetNodes.get(id);
    if (Array.isArray(targetNodes)) {
    for (const item of targetNodes) {
    if (hasCycle(item, visited)) return true;
    }
    }
    checked.add(node.id);
    visited.delete(node);
    return false;
    };
    const execute = () => {
    const visited: Set<Node> = new Set();
    for (const [id, node] of nodes) {
    if (hasCycle(node, visited)) return true;
    checked.add(id);
    }
    return false;
    };


    1. 循环。checked和递归时,作用一样,这里不做说明。visited是用来判断当前的节点是否遍历过,如果遍历过,就是有环。用循环实现深度优先遍历时,需要用来存储当前链路上的节点,即当前节点已经后代节点。并且从里面获取最后一个节点,作为当前遍历的节点。如果当前节点有向邻居节点不为空,就把有向邻居节点的最后一个节点拿出来压栈;如果有向邻居节点为空,就把当前的节点出栈。在压栈时,如果当前节点在visited里,就说明有环,如果没有就要把这个节点加入到visited。在出栈时,把当前节点从visited里删除掉,因为如果不删掉,当一个节点的多个邻居节点最终指向同一个节点时,会判断为有环。


    const checked: Set<string> = new Set();
    const hasCycle = (node: Node) => {
    const { id } = node;
    if (checked.has(id)) return false;
    const stack = [id];
    const visited: Set<string> = new Set();
    visited.add(id);
    while (stack.length > 0) {
    const lastId = stack[stack.length - 1];
    const targetNodes = idMapTargetNodes.get(lastId) || [];
    if (targetNodes.length > 0) {
    const { id } = targetNodes.pop() as Node;
    if (visited.has(id)) return true;
    stack.push(id);
    visited.add(id);
    } else {
    stack.pop();
    visited.delete(lastId);
    }
    }
    return false;
    };
    const execute = () => {
    for (const [id, node] of nodes) {
    if (hasCycle(node)) return true;
    checked.add(id);
    }
    return false;
    };



总结



  • 要掌握常见的数据结构与算法,本例中用到了图、深度优先遍历。


源码



作者:PlayerWho
来源:juejin.cn/post/7213945427853443131
收起阅读 »

给自己编写一个批量填写日报的工具

web
背景 公司要求我们每天填写工时,每天的时间都花在了哪些地方,干了什么。平时没顾得上填,欠下了一屁股工时债。收到邮件催填通知后,发现要补两个月的工时,填了一会儿,感觉变化的只是日期和工作内容,其它的内容项都是固定内容。一天一天填着实费劲,于是决定写一个填写日报的...
继续阅读 »

背景


公司要求我们每天填写工时,每天的时间都花在了哪些地方,干了什么。平时没顾得上填,欠下了一屁股工时债。收到邮件催填通知后,发现要补两个月的工时,填了一会儿,感觉变化的只是日期和工作内容,其它的内容项都是固定内容。一天一天填着实费劲,于是决定写一个填写日报的小工具,只要在js文件中,补充一下每天的工作内容,然后执行node命令,批量完成工时的填写。


思路



  1. 先根据设置的起始结束时间,查询一下当月有多少个工作日,要补多少天的工时。要排除当月每周周末,法定节假日的日期,加上调休补班的日期。

  2. 根据计算出来的需要补充工时的天数,编辑好要补填的工作内容条数,然后批量发送网络请求,完成工时的填写。


工作日查询实现


发现了一个叫蛙蛙工具的网站,免费提供接口给第三方使用,可以用来查询工作日。每分钟限制查10次。下图是抓取的响应数据:重点说一下要用到的weekend_date_listholiday_date_list字段;



  • weekend_date_list 周末日期

  • holiday_date_list 法定节假日


只要排除这两个数组中的日期,剩下的就是查询日期时间段工作日。


image.png


思路有了,来看看实现。首先要写好发送查询请求的逻辑,要知道请求地址,请求参数,请求参数格式,响应数据内容。其次,拿到响应结果后,生成一个从开始日期到结束日期,格式为YYYY-MM-DD的数组,从这个数组中剔除周末和法定节假日,剩余的日期就是工作日,知道工作日的天数后,就知道要写几天的工作日报。代码如下:


import axios from "axios";
import dayjs from "dayjs";
import { startDate,endDate } from "./config.js";


// 查询从本月的工作日
export const queryWorkingDay = () => {
return new Promise((resolve, reject) => {
const url = "https://www.iamwawa.cn/home/workingday/ajax";
const params = {
start_date: startDate.format("YYYY-MM-DD"),
end_date: endDate.format("YYYY-MM-DD"),
};

axios
.post(url, params, { headers: { "Content-Type": "application/x-www-form-urlencoded" } })
.then(({ data: res }) => {
const { status, data, info } = res;
const {
// 平常的周末
weekend_date_list = [],
// 法定节假日
holiday_date_list = [],
// 工作日天数
working_date_count,
} = data;

// console.log(data);

// 生成设置的当月起始结束日期数组
const dayOfMonth = genNumArr(startDate.date(), endDate.date()).map((day) =>
dayjs().date(day).format("YYYY-MM-DD")
);

// 需要排除的法定节假日和周末日期
const excludeDays = [
weekend_date_list.map((item) => item.date),
holiday_date_list.map((item) => item.date),
].flat();

// 工作日
const workDays = dayOfMonth.filter((day) => !excludeDays.includes(day));

// console.log(status,data,info);
console.log(`本月你需要补充${working_date_count}天日报`);
console.log(`需要填写的日期:`);
workDays.forEach((day) => {
console.log(day);
});

console.log(`需要排除的日期:`);
excludeDays.forEach((day) => {
console.log(day);
});

resolve(workDays);
});
});
};

// 生成连续数字数组
function genNumArr(start, end) {
return Array.from(new Array(end + 1).keys()).slice(start);
}


提交工时实现


先登录填报工时网站, 手动填写一条,在调试模式下查看一下请求地址和请求参数。
1679728451342.png
请求地址我就不贴出来了,这里只提供思路,请求数据为:


{
"workDate": "2023-03-14",
"tapdId": null,
"groupId": 12,
"projectId": 159,
"lineId": 2,
"taskId": 16,
"workContent": "xxxxxx",
"workHours": 8
}

如法炮制查询工作日的方法,发起工时提交请求,结果吃了闭门羹。提示没有权限。


image.png


后面经过排查,发现网络请求的请求头,需要带一个authorization的参数,服务器根据这个参数判断有没有提交权限。这个参数你必须登录原网站才能拿到,把这个参数复制出来,配置到代码中,再发请求,这次很顺利的提交了。


image.png


提交数据跑通之后,要实现批量提交数据就很Easy了,循环调用提交单条数据的接口就可以了。有个细节需要注意一下,提交请求太快,服务器会返回错误,所以每个请求之间加了一个500ms的延时。提交工时的代码如下:


import axios from "axios";
import { queryWorkingDay } from "./queryDay.js";
import { authorization, workContentList } from "./config.js";

// 提交每月的工时
export const submitMonthWorkHour = async () => {
const workDays = await queryWorkingDay();

for (let index = 0, len = workContentList.length; index < len; index++) {
await submitEachDayData(workDays[index], workContentList[index]);
}
};

/**
* 提交每天的工时数据
* @param {*} workDate 工作日期
* @param {*} workContent 工作内容
*/

const submitEachDayData = (workDate, workContent) => {
return new Promise((resolve, reject) => {
const url = "https://xxx/xxx",
authorization,
"Content-Type": "application/json",
};

const params = {
workDate,
workContent,
tapdId: null,
groupId: 12,
projectId: 159,
lineId: 2,
taskId: 16,
workHours: 8,
};

setTimeout(() => {
axios
.post(url, params, { headers })
.then(({ data }) => {
const { ret, retdata, retmsg } = data;
// if (ret === 0) {
console.log(`${workDate}--${retmsg}`);
resolve("ok");
// }
})
.catch((err) => reject(err));
}, 500);
});
};

主流程实现


在package.json中配置两条指令,一条用于查询设置的起始结束时间有多少个工作日,需要补充多少天的工作日报,接着在上面的submitMonthWorkHour方法中,手动编辑,给工作内容列表workContentList填充数据,一条数据对应一天的工作日报。填写完之后,执行提交命令。


{
"license":"MIT",
"scripts": {
"query": "node main.js query",
"submit": "node main.js submit"
},
"dependencies": {
"axios": "^1.3.4",
"dayjs": "^1.11.7"
},
"type": "module",
"devDependencies": {}
}


顺便说一下,node v9+版本,若要使用import/export语法, 需要在package.json中指定 "type": "module"


在主函数中, 根据不同的指令执行不同的操作。实际使用时, 肯定是要先调用yarn query查询补充多少天日报才行。


import { queryWorkingDay } from "./queryDay.js";
import { submitMonthWorkHour } from "./submitData.js";

main();
// 主流程
function main() {
const argv = process.argv;
// 先查询需要补充多少天日报
if (argv.includes("query")) {
queryWorkingDay();
} else if (argv.includes("submit")) {
submitMonthWorkHour();
} else {
console.log('指令错误');
process.exit(1);
}
}


把配置数据放到config.js中, 这里要说一下dayjs().date()dayjs().daysInMonth(), 它们的执行结果都是一个数字,代表的含义是这个月的日期,默认开始时间是当天日期,结束时间是月底日期。可手动修改。


import dayjs from "dayjs";
// 设置查询工作日的开始时间
export const startDate = dayjs().set("date", dayjs().date());
export const endDate = dayjs().set("date", dayjs().daysInMonth());
// 每次先登录一下填报工时的网站,把http请求头中的authorization复制出来
export const authorization = "";
// 手动填写需要补充的工时
export const workContentList = [""];

结语


至此,批量提交日报的小工具就开发完了。爱因斯坦说, 比知识更重要的是想象力。文中列举的知识点大家可能都懂,但是要把这些知识串接起来,开发一个有实用价值的工具,是需要一点灵动和想象力的。而灵动来源于优化意识,需要一个善于发现问题的心灵,洞悉生活中,工作中的痛点,寻找改进之法。 这个小工具已上传至码云,感兴趣的朋友可点击这里下载


作者:去伪存真
来源:juejin.cn/post/7214349925064802362
收起阅读 »

GPT-4都来了,我们还需要刷算法题和背八股文吗?

温馨提示:有点标题党了,本文并不是正常技术分享,而是表达自己的一些观点,如有冒犯,请多包含。 2023年的面试寒冬 从去年到今年各个大厂都在不断裁员,导致今年IT互联网行情很差,很多朋友都缺乏面试机会或者面试不通过。 程序员一旦要开始面试,很多朋友都开始循环...
继续阅读 »

温馨提示:有点标题党了,本文并不是正常技术分享,而是表达自己的一些观点,如有冒犯,请多包含。



2023年的面试寒冬


从去年到今年各个大厂都在不断裁员,导致今年IT互联网行情很差,很多朋友都缺乏面试机会或者面试不通过。


程序员一旦要开始面试,很多朋友都开始循环以下几个步骤:



  • 刷算法题,如:LeetCode各种困难程度的题目

  • 背诵各种八股文,如:浏览器请求一个URL的完整过程

  • 复习各种冷门知识,如:Promise.race(谁先返回就获取谁的结果,后面直接不处理)或Promise.allSettled(结果为Iterator对象,实现next())


也许以往这种复习模式还算不错,因为目前大多数面试流程基本上都是会问上述问题,而面试官问这些问题的最终目的是什么:



  • 算法题,主要考的是逻辑思维能力

  • 八股文,主要考的是基础知识是否足够扎实,知识深度是否足够

  • 冷门知识,主要考的是知识广度,你除了目前使用知识点外还能拥有其他知识面


后续一些项目经验讲述,主要目的在于测试你是否真的参与到项目中,但是这个往往都是容易被忽略的点,这个放到后面再讲。


当下最火的IT技术,ChatGPT智能机器人出来后,它不像普通搜索引擎一样,需要去大量的结果去找到自己的答案。而是直接将答案提供给到你,让你可以更加快速实现功能。


如果ChatGPT普及开来,那么面试是否还需要考算法题和八股文吗?


或者换个问题,当AI技术可以代替绝大部分基础开发工作的时候,面对这种技术变更,作为普通开发者,我们应该如何做技术成长规划呢?


怎么办


看清楚问题


作为普通的开发人员,我们平时大部分开发工作说的不好听一点,其实都是在复制粘贴,简单点说就是在搬砖,从A地方搬到B地方,举几个例子:



  • 利用框架封装好的东西去开发项目,是否有尝试过自己去实现一个框架,如:Vue、React

  • 实现某一个功能的时候,要嘛从现有项目中拷贝,要嘛从网上找对应答案,比如:需要从一个url获取参数,你会自己实现一个,还是拷贝呢?

  • 遇到问题,第一反应基本上先从网上找答案,找到答案看看能否解决,如果可以就不会再往下深入学习


以上基本上就是我们普通开发的发展路线——尽量不去开发轮子,也没有时间和兴趣去开发轮子。


当chatGPT智能机器人出现之后,你会发现你会的机器人都会,而且它还能实现你不能实现的功能,那么我们可以大胆猜测未来会有这么一个产品:



一个低代码智能化平台,只需要资深开发者去输入一些关键性的业务代码,同时优化生成出来的代码逻辑。



举个更简单的例子,原本你手动搬的砖,现在有机器人可以实现自动搬了,或者说当马车被汽车替代了,那么马夫的工作自然也就被司机所替代。


那么问题就很简单了,现状的问题是:


大厂或小厂都在裁员,不需要那么多搬砖工了,而你还在为了更快搬砖,去提高自己的搬砖能力,如:刷算法题、基础知识等,不就等于在锻炼自己臂力让自己能搬更多的砖吗?


解决方案


既然清楚问题了,针对这个问题能有什么解决方案呢?其实是有的,就在于自己的选择。


成为少数人


怎么理解成为少数人呢?主要有两种方式:


第一种方式,无可替代或者很难替代。


你的工作只有你能做,或者你做的工作很难。还是以搬砖为例:



  • 可以在高楼层搬砖

  • 可以在深海里搬砖


简单的说,除了开发业务功能外,你还掌握了其他的技能,说出来可能大家会骂人,如:



  • 写一手漂亮的PPT,能把PPT做堪比艺术品

  • 项目管理能力强,能把握项目进度

  • 有产品思维,你实现的功能比产品经理想得还完整

  • 其他软技能...


第二种方式,成为专家


这种方式需要天赋+机会+努力,缺一不可,简单来说,就是大家搬砖用的工具都是你制作的,如:



  • Vue/React的作者,将很难被淘汰

  • 公司内部的架构师们,项目的整体架构和轮子都由他搭建的,或者公司需要他们去新搭建一套轮子


那么如何成为专家呢?不管是从网上还是现实中,都有很多实现路线,我这里简单总结一下:



  • 努力学习,扩展技术栈,日复一日的坚持学习知识

  • 深入业务领域,将技术与业务结合,不断创造一些新的方案或架构

  • 扩大自己的影响范围,等待机会,创造一套属于自己的架构体系


离开搬砖


如果真的继续干这一行,吃技术饭很难混下来,那么只有选择离开,但是离开也分不同的分开,下面简单说几句。


第一种离开,改变自己,拥抱新方向
当汽车来临的时候,马夫无法继续工作了,那么为何不直接投入汽车司机的行业呢?


既然AI技术已经开始成熟投入使用了,那么作为开发者有什么理由不提前进入AI领域,去尝试一种新的方向呢?


比如:



  • 彻底转行成为AI开发,这里门槛有点高,但是可以去尝试

  • 利用AI开发工具,成为AI的使用者是不是更加合适呢?


第二种离开,永远离开,投入新领域
AI的到来,会实现很多基础开发工作,那么我不做开发了,是不是就不用被淘汰了呢?


正如所说,天涯何处无芳草,哪里都可以混口饭吃。


不要做21世纪的孔乙己,脱不下身上的长袍。


举几个例子:



  • 转技术培训,前提是你在IT开发待的足够久

  • 创业当老板,从打工思维变老板思维,前提是得有钱有资源

  • 早餐摊,卖粥,足以养活一家人,前提是身体健康


总之,心态不要奔溃,被裁员也好,面试不通过也好,这只是当前整个大背景下的一个波浪,更何况目前AI的技术并没有那么发达。


如果以时间为维度,你这次的失败,只是你几十年人中的一个小片段。


如果以空间为维度,你我皆是蝼蚁,宇宙何其庞大,你抬头看片星空,是否心情会更加放松一点。


最后,推荐大家去看一本书《百万富翁的快车道》,名字很俗,但是却能给我们带来一种新的思想模式:



  • 财富不是金钱等物质,而是你所能控制的时间+你的健康+人脉

  • 人生的每个选择,都是你的信念系统做出的,如果你要财富,则需要不断优化自己本身的信念系统


免责声明


本文是个人一些想法,仅供参考。



做一个有温度的技术分享作者 —— Qborfy


作者:QBorfy
来源:juejin.cn/post/7211120847787098171

收起阅读 »

线程池也会导致OOM的原因

1. 前言 我这边从一个问题引出这次的话题,我们可能会在开中碰到一种OOM问题,java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Try again 相信很多人碰到过这个错误,很...
继续阅读 »

1. 前言


我这边从一个问题引出这次的话题,我们可能会在开中碰到一种OOM问题,java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Try again


相信很多人碰到过这个错误,很容易从网上搜索到出现这个问题的原因是线程过多,那线程过多为什么会导致OOM?线程什么情况下会释放资源?你又能如何做到让它不释放资源?


有的人可能会想到,那既然创建线程过多会导致OOM,那我用线程池不就行了。但是有没有想过,线程池,也可能会造成OOM。其实这里有个很经典的场景,你使用OkHttp的时候不注意,每次请求都创建OkHttpClient,导致线程池过多出现OOM


2. 简单了解线程池


如何去了解线程池,看源码,直接去看是很难看得懂的,要先了解线程池的原理,对它的设计思想有个大概的掌握之后,再去看源码,就会轻松很多,当然这里只了解基础的原理还不够,还需要有一些多线程相关的基础知识。


本篇文章只从部分源码的角度去分析,线程池如何导致OOM的,而不会全部去看所有线程池的源码细节,因为太多了


首先,要了解线程池,首先需要从它的参数入手:



  • corePoolSize:核心线程数量

  • maximumPoolSize:最大线程数量

  • keepAliveTime,unit:非核心线程的存活时间和单位

  • workQueue:阻塞队列

  • ThreadFactory:线程工厂

  • RejectedExecutionHandler:饱和策略


然后你从网上任何一个地方搜都能知道它大致的工作流程是,当一个任务开始执行时,先判断当前线程池数量是否达到核心线程数,没达到则创建一个核心线程来执行任务,如果超过,放到阻塞队列中等待,如果阻塞队列满了,未达到最大线程数,创建一条非核心线程执行任务,如果达到最大线程数,执行饱和策略。在这个过程中,核心线程不会回收,非核心线程会根据keepAliveTime和unit进行回收。


**这里可以多提一嘴,这个过程用了工厂模式ThreadFactory和策略模式RejectedExecutionHandler,关于策略模式可以看我这篇文章 ** juejin.cn/post/719502…


其实从这里就可以看出为什么线程池也会导致OOM了:核心线程不会回收,非核心线程使用完之后会根据keepAliveTime和unit进行回收 ,那核心线程就会一直存活(我这不考虑shutdown()和shutdownNow()这些情况),一直存活就会占用内存,那你如果创建很多线程池,就会OOM。


所以我这篇文章要分析:核心线程不会释放资源的过程,它内部怎么做到的。 只从这部分的源码去进行分析,不会全部都详细讲。


先别急,为了照顾一些基础不太好的朋友,涉及一些基础知识感觉还是要多讲一下。上面提到的线程回收和shutdown方法这些是什么意思?线程执行完它内部的代码后会主动释放资源吗?


我们都知道开发中有个概念叫生命周期,当然线程池和线程也有生命周期(这很重要),在开发中,我们称之为lifecycle。


生命周期当然是设计这个东西的开发者所定义的,我们先看线程池的生命周期,在ThreadPoolExecutor的注释中有写:


*
* The runState provides the main lifecycle control, taking on values:
*
* RUNNING: Accept new tasks and process queued tasks
* SHUTDOWN: Don't accept new tasks, but process queued tasks
* STOP: Don't accept new tasks, don't process queued tasks,
* and interrupt in-progress tasks
* TIDYING: All tasks have terminated, workerCount is zero,
* the thread transitioning to state TIDYING
* will run the terminated() hook method
* TERMINATED: terminated() has completed
*

看得出它的生命周期有RUNNING,SHUTDOWN,STOP,TIDYING和TERMINATED。而shutdown()和shutdownNow()方法会改变生命周期,这里不是对线程池做全面解析,所以先有个大概了解就行,可以暂时理解成这篇文章的所有分析都是针对RUNNING状态下的。


看完线程池的,再看看线程的生命周期。线程的生命周期有:



  • NEW:创建,简单来说就是new出来没start

  • RUNNABLE:运行,简单来说就是start后执行run方法

  • TERMINATED:中止,简单来说就是执行完run方法或者进行中断操作之后会变成这个状态

  • BLOCKED:阻塞,就是加锁之后竞争锁会进入到这个状态

  • WAITING、TIMED_WAITING:休眠,比如sleep方法


这个很重要,需要了解,你要学会线程这块相关的知识点的话,这些生命周期要深刻理解 。比如BLOCKED和WAITING有什么不同?然后学这块又会涉及到锁那一块的知识。以后有时间可以单独写几篇这类的文章,这里先大概有个概念,只需要能先看懂后面的源码就行。


从生命周期的概念你就能知道线程执行完它内部的代码后会主动释放资源,因为它run执行完之后生命周期会到TERMINATED,那这又涉及到了一个知识点,为什么主线程(ActivityThread),执行完run的代码后不会生命周期变成TERMINATED,这又涉及到Looper,就得了解Handler机制,可以看我这篇文章 juejin.cn/post/715882…


扯远了,现在进入正题,先想想,如果是你,你怎么做让核心线程执行完run之后不释放资源,很明显,只要让它不执行到TERMINATED生命周期就行,如何让它不变成TERMINATED状态,只需要让它进入BLOCKED或者WAITING状态就行。所以我的想法是这样的,当这个核心线程执行完这个任务之后,我让它WAITING,等到有新的任务进来的时候我再唤醒它进入RUNNABLE状态。 这是我从理论这个角度去分析的做法,那看看实际ThreadPoolExecutor是怎么做的


3. 线程池部分源码分析


前面说了,不会全部都讲,这里涉及到文章相关内容的流程就是核心线程的任务执行过程,所以这里主要分析核心线程。


当我们使用线程池执行一个任务时,会调用ThreadPoolExecutor的execute方法


public void execute(Runnable command) {
......

int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}

// 我们只看核心线程的流程,所以后面的代码不用管
......
}

这个ctl是一个状态相关的代码,可以先不用管,我后面会简单统一做个解释,这里不去管它会比较容易理解,我们现在主要是为了看核心线程的流程。从这里可以看出,当前线程的数量小于核心线程的话执行addWorker方法


private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);

// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;

for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}

boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());

if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}

这个addWorker分为上下两部分,我们分别来做解析


private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);

// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;

for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}

// 下半部分
......
}

这里主要是做了状态判断的一些操作,我说过状态相关的我们可以先不管,但是这里的写法我觉得要单独讲一下为什么会这么写。不然它内部很多代码是这样的,我怕劝退很多人。


首先retry: ...... break retry; 这个语法糖,平常我们开发很少用到,可以去了解一下,这里就是为了跳出循环。 其次,这里的compareAndIncrementWorkerCount内部的代码是AtomicInteger ctl.compareAndSet(expect, expect + 1) ,Atomic的compareAndSet操作搭配死循环,这叫自旋,所以说要看懂这个需要一定的java多线程相关的基础。自旋的目的是为了什么?这就又涉及到了锁的分类中有乐观锁,有悲观锁。不清楚的可以去学一下这些知识,你就知道为什么它要这么做了,这里就不一一解释。包括你看它的源码,能看到,它会很多地方用自旋,很多地方用ReentrantLock,但它就是不用synchronized ,这些都是多线程这块基础的知识,这里不多说了。


看看下半部分


private boolean addWorker(Runnable firstTask, boolean core) {

// 上半部分
......



boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
......
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
......
}
return workerStarted;
}

看到它先创建一个Worker对象,再调用Worker对象内部的线程的start方法,我们看看Worker


private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{

private static final long serialVersionUID = 6138294804551838833L;

final Thread thread;
Runnable firstTask;

Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}

public void run() {
runWorker(this);
}

// 其它方法
......
}

看到它内部主要有两个对象firstTask就是任务,thread就是执行这个任务的线程,而这个线程是通过getThreadFactory().newThread(this)创建出来的,这个就是我们创建ThreadPoolExecutor时传的“线程工厂”

外部调t.start();之后就会执行这里的run方法,因为newThread传了this进去,你可以先简单理解调这个线程start会执行到这个run,然后run中调用runWorker(this);


注意,你想想runWorker(this)方法,包括之后的流程,都是执行在哪个线程中?都是执行在子线程中,因为这个run方法中的代码,都是执行在这个线程中。你一定要理解这一步,不然你自己看源码会可能看懵。 因为有些人长期不接触多线程环境的情况下,你会习惯单线程的思维去看问题,那就很容易出现理解上的错误。


我们继续看看runWorker,时刻提醒你自己,之后的流程都是在子线程中进行,这条子线程的生命周期变为RUNNABLE


final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {s
w.lock();

// 中断相关的操作
......

try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
......
} finally {
afterExecute(task, thrown);
}
} finally {
......
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}

先讲讲这里的一个开发技巧,task.run()就是执行任务,它前面的beforeExecute和afterExecute就是模板方法设计模式,方便扩展用。

执行完任务后,最后执行processWorkerExit方法


private void processWorkerExit(Worker w, boolean completedAbruptly) {
if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
decrementWorkerCount();

final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
completedTaskCount += w.completedTasks;
workers.remove(w);
} finally {
mainLock.unlock();
}

tryTerminate();

......
}

workers.remove(w)后执行tryTerminate方法尝试将线程池的生命周期变为TERMINATED


final void tryTerminate() {
for (;;) {
int c = ctl.get();
if (isRunning(c) ||
runStateAtLeast(c, TIDYING) ||
(runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
return;
if (workerCountOf(c) != 0) { // Eligible to terminate
interruptIdleWorkers(ONLY_ONE);
return;
}

final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
try {
terminated();
} finally {
ctl.set(ctlOf(TERMINATED, 0));
termination.signalAll();
}
return;
}
} finally {
mainLock.unlock();
}
// else retry on failed CAS
}
}

先不用管状态的变化,一般一眼都能看得出这里是结束的操作了,我们追踪的核心线程正常在RUNNING状态下是不会执行到这里的。 那我们期望的没任务情况下让线程休眠的操作在哪里?

看回runWorker方法


final void runWorker(Worker w) {
......
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {s
......
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}

看到它的while中有个getTask()方法,认真看runWorker方法其实能看出,核心线程执行完一个任务之后会getTask()拿下一个任务去执行,这就是当核心线程满的时候任务会放到阻塞队列中,核心线程执行完任务之后会从阻塞队列中拿下一个任务执行。 getTask()从抽象上来看,就是从队列中拿任务。


private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?

for (;;) {
......

try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}

先把timed当成正常情况下为false,然后会执行workQueue.take(),这个workQueue是阻塞队列BlockingQueue, 注意,这里又需要有点基础了。正常有点基础的人看到这里,已经知道这里就是当没有任务会让核心线程休眠的操作,看不懂的,可以先了解下什么是AQS,可以看看我这篇文章 juejin.cn/post/716801…


如果你说你懒得看,行吧,我随便拿个ArrayBlockingQueue给你举例


public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}

notEmpty是Condition,这里调用了Condition的await()方法,然后想想执行这步操作的是在哪条线程上?线程进入WAITING状态了吧,不会进入TERMINATED了吧。


然后当有任务添加之后会唤醒它,它继续在循环中去执行任务。


这就验证了我们的猜想,通过让核心线程进入WAITING状态以此来达到执行完run方法中的任务也不会主动TERMINATED而释放线程。所以核心线程一直占用资源,这里说的资源指的是空间,而cpu的时间片是会让出的。


4. 部分线程池的操作解读


为什么线程池也会导致OOM,上面已经通过源码告诉你,核心线程不会释放内存空间,导致线程池多的情况下也会导致OOM。这里为了方便新手阅读ThreadPoolExecutor相关的代码,还是觉得写一些它内部的设计思想,不然没点基础的话确实很难看懂。


首先就是状态,上面源码中都有关线程池的生命中周期状态(ctl字段),可以看看它怎么设计的


private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3; // Integer.SIZE是32
private static final int CAPACITY = (1 << COUNT_BITS) - 1;

// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;

它这里用了两个设计思想,第一个就是用位来表示状态,关于这类型的设计,可以看我这2篇文章 juejin.cn/post/715547…juejin.cn/post/720550…


另外一个设计思想是:用一个变量的高位置表示状态,低位表示数量。 这里就是用高3位来表示生命周期,剩下的低位表示线程的数量。和这个类似的操作有view中的MeasureSpec,也是一个变量表示两个状态。


然后关于设计模式,可以看到它这里最经典的就是用了策略模式,如果你看饱和策略那块的源码,可以好好看看它是怎么设计的。其它的还有工厂、模板之类的,这些也不难,就是策略还是建议学下它怎么去设计的。


然后多线程相关的基础,这个还是比较重要的,这块的基础不好,看ThreadPoolExecutor的源码会相对吃力。比如我上面提过的,线程的生命周期,锁相关的知识,还有AQS等等。如果你熟悉这些,再看这个源码就会轻松很多。


对于总体的设计,你第一看会觉得它的源码很绕,为什么会这样?因为有中断操作+自旋锁+状态的设计 ,它的这种设计就基本可以说是优化代码到极致,比如说状态的设计,就比普通的能省内存,能更方便通过CAS操作。用自旋就是乐观锁,能节省资源等。有中断操作,能让整个系统更灵活。相对的缺点就是不安全,什么意思呢?已是就是这样写代码很容易出BUG,所以这里的让人觉得很绕的代码,就是很多的状态的判断,这些都是为了保证这个流程的安全。


5. 总结


从部分源码的角度去分析,得到的结论是线程池也可能会导致OOM


那再思考一个问题:不断的创建线程池,“一定”会导致OOM吗? 如果你对线程池已经有一定的了解,相信你也知

作者:流浪汉kylin
来源:juejin.cn/post/7210691957790572601
道这个问题的答案。


收起阅读 »

从Flutter到Compose,为什么都在推崇声明式UI?

Compose推出之初,就曾引发广泛的讨论,其中一个比较普遍的声音就是——“🤨这跟Flutter也长得太像了吧?!” 这里说的长得像,实际更多指的是UI编码的风格相似,而关于这种风格有一个专门的术语,叫做声明式UI。 对于那些已经习惯了命令式UI的Androi...
继续阅读 »

Compose推出之初,就曾引发广泛的讨论,其中一个比较普遍的声音就是——“🤨这跟Flutter也长得太像了吧?!”


这里说的长得像,实际更多指的是UI编码的风格相似,而关于这种风格有一个专门的术语,叫做声明式UI


对于那些已经习惯了命令式UI的Android或iOS开发人员来说,刚开始确实很难理解什么是声明式UI。就像当初刚踏入编程领域的我们,同样也很难理解面向过程编程面向对象编程的区别一样。


为了帮助这部分原生开发人员完成从命令式UI到声明式UI的思维转变,本文将结合示例代码编写、动画演示以及生活例子类比等形式,详细介绍声明式UI的概念、优点及其应用。


照例,先奉上思维导图一张,方便复习:





命令式UI的特点


既然命令式UI与声明式UI是相对的,那就让我们先来回顾一下,在一个常规的视图更新流程中,如果采用的是命令式UI,会是怎样的一个操作方式。


以Android为例,首先我们都知道,Android所采用的界面布局,是基于View与ViewGroup对象、以树状结构来进行构建的视图层级。



当我们需要对某个节点的视图进行更新时,通常需要执行以下两个操作步骤:



  1. 使用findViewById()等方法遍历树节点以找到对应的视图。

  2. 通过调用视图对象公开的setter方法更新视图的UI状态


我们以一个最简单的计数器应用为例:



这个应用唯一的逻辑就是“当用户点击"+"号按钮时数字加1”。在传统的Android实现方式下,代码应该是这样子的:


class CounterActivity : AppCompatActivity() {

var count: Int = 0

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_counter)

val countTv = findViewById<TextView>(R.id.count_tv)
countTv.text = count.toString()

val plusBtn = findViewById<Button>(R.id.plus_btn)
plusBtn.setOnClickListener {
count += 1
countTv.text = count.toString()
}

}
}

这段代码看起来没有任何难度,也没有明显的问题。但是,假设我们在下一个版本中添加了更多的需求:




  • 当用户点击"+"号按钮,数字加1的同时在下方容器中添加一个方块。

  • 当用户点击"-"号按钮,数字减1的同时在下方容器中移除一个方块。

  • 当数字为0时,下方容器的背景色变为透明。


现在,我们的代码变成了这样:


class CounterActivity : AppCompatActivity() {

var count: Int = 0

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_counter)

// 数字
val countTv = findViewById<TextView>(R.id.count_tv)
countTv.text = count.toString()

// 方块容器
val blockContainer = findViewById<LinearLayout>(R.id.block_container)

// "+"号按钮
val plusBtn = findViewById<Button>(R.id.plus_btn)
plusBtn.setOnClickListener {
count += 1
countTv.text = count.toString()
// 方块
val block = View(this).apply {
setBackgroundColor(Color.WHITE)
layoutParams = LinearLayout.LayoutParams(40.dp, 40.dp).apply {
bottomMargin = 20.dp
}
}
blockContainer.addView(block)
when {
count > 0 -> {
blockContainer.setBackgroundColor(Color.parseColor("#FF6200EE"))
}
count == 0 -> {
blockContainer.setBackgroundColor(Color.TRANSPARENT)
}
}
}

// "-"号按钮
val minusBtn = findViewById<Button>(R.id.minus_btn)
minusBtn.setOnClickListener {
if(count <= 0) return@setOnClickListener
count -= 1
countTv.text = count.toString()
blockContainer.removeViewAt(0)
when {
count > 0 -> {
blockContainer.setBackgroundColor(Color.parseColor("#FF6200EE"))
}
count == 0 -> {
blockContainer.setBackgroundColor(Color.TRANSPARENT)
}
}
}

}

}

已经开始看得有点难受了吧?这正是命令式UI的特点,侧重于描述怎么做,我们需要像下达命令一样,手动处理每一项UI的更新,如果UI的复杂度足够高的话,就会引发一系列问题,诸如:



  • 可维护性差:需要编写大量的代码逻辑来处理UI变化,这会使代码变得臃肿、复杂、难以维护。

  • 可复用性差:UI的设计与更新逻辑耦合在一起,导致只能在当前程序使用,难以复用。

  • 健壮性差:UI元素之间的关联度高,每个细微的改动都可能一系列未知的连锁反应。


声明式UI的特点


而同样的功能,假如采用的是声明式UI,则代码应该是这样子的:


class _CounterPageState extends State<CounterPage> {
int _count = 0;

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
children: [
// 数字
Text(
_count.toString(),
style: const TextStyle(fontSize: 48),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
// +"号按钮
ElevatedButton(
onPressed: () {
setState(() {
_count++;
});
},
child: const Text("+")),
// "-"号按钮
ElevatedButton(
onPressed: () {
setState(() {
if (_count == 0) return;
_count--;
});
},
child: const Text("-"))
],
),
Expanded(
// 方块容器
child: Container(
width: 60,
padding: const EdgeInsets.all(10),
color: _count > 0 ? const Color(0xFF6200EE) : Colors.transparent,

child: ListView.separated(
itemCount: _count,
itemBuilder: (BuildContext context, int index) {
// 方块
return Container(width: 40, height: 40, color: Colors.white);
},
separatorBuilder: (BuildContext context, int index) {
return const Divider(color: Colors.transparent, height: 10);
},
),
))
],
),
);
}
}


在这样的代码中,我们几乎看不到任何操作UI更新的代码,而这正是声明式UI的特点,它侧重于描述做什么,而不是怎么做,开发者只需要关注UI应该如何呈现,而不需要关心UI的具体实现过程。


开发者要做的,就只是提供不同UI与不同状态之间的映射关系,而无需编写如何在不同UI之间进行切换的代码。


所谓状态,指的是构建用户界面时所需要的数据,例如一个文本框要显示的内容,一个进度条要显示的进度等。Flutter框架允许我们仅描述当前状态,而转换的工作则由框架完成,当我们改变状态时,用户界面将自动重新构建


下面我们将按照通常情况下,用声明式UI实现一个Flutter应用所需要经历的几个步骤,来详细解析前面计数器应用的代码:



  1. 分析应用可能存在的各种状态


根据我们前面对于“状态”的定义,我们可以很容易地得出,在本例中,数字(_count值)本身即为计数器应用的状态,其中还包括数字为0时的一个特殊状态。



  1. 提供每个不同状态所对应要展示的UI


build方法是将状态转换为UI的方法,它可以在任何需要的时候被框架调用。我们通过重写该方法来声明UI的构造:


对于顶部的文本,只需声明每次都使用最新返回的状态(数字)即可:


Text(
_count.toString(),
...
),

对于方块容器,只需声明当_count的值为0时,容器的背景颜色为透明色,否则为特定颜色:


Container(
color: _count > 0 ? const Color(0xFF6200EE) : Colors.transparent,
...
)

对于方块,只需声明返回的方块个数由_count的值决定:


ListView.separated(
itemCount: _count,
itemBuilder: (BuildContext context, int index) {
// 方块
return Container(width: 40, height: 40, color: Colors.white);
},
...
),


  1. 根据用户交互或数据查询结果更改状态


当由于用户的点击数字发生变化,而我们需要刷新页面时,就可以调用setState方法。setState方法将会驱动build方法生成新的UI:


// "+"号按钮
ElevatedButton(
onPressed: () {
setState(() {
_count++;
});
},
child: const Text("+")),
// "-"号按钮
ElevatedButton(
onPressed: () {
setState(() {
if (_count == 0) return;
_count--;
});
},
child: const Text("-"))
],

可以结合动画演示来回顾这整个过程:



最后,用一个公式来总结一下UI、状态与build方法三者的关系,那就是:



以命令式和声明式分别点一杯奶茶


现在,你能了解命令式UI与声明式UI的区别了吗?如果还是有些抽象,我们可以用一个点奶茶的例子来做个比喻:


当我们用命令式UI的思维方式去点一杯奶茶,相当于我们需要告诉制作者,冲一杯奶茶必须按照煮水、冲茶、加牛奶、加糖这几个步骤,一步步来完成,也即我们需要明确每一个步骤,从而使得我们的想法具体而可操作。


而当我们用声明式UI的思维方式去点一杯奶茶,则相当于我们只需要告诉制作者,我需要一杯“温度适中、口感浓郁、有一点点甜味”的奶茶,而不必关心具体的制作步骤和操作细节。


声明式编程的优点


综合以上内容,我们可以得出声明式UI有以下几个优点:




  • 简化开发:开发者只需要维护状态->UI的映射关系,而不需要关注具体的实现细节,大量的UI实现逻辑被转移到了框架中。




  • 可维护性强:通过函数式编程的方式构建和组合UI组件,使代码更加简洁、清晰、易懂,便于维护。




  • 可复用性强:将UI的设计和实现分离开来,使得同样的UI组件可以在不同的应用程序中使用,提高了代码的可复用性。




总结与展望


总而言之,声明式UI是一种更加高层次、更加抽象的编程方式,其最大的优点在于能极大地简化现有的开发模式,因此在现代应用程序中得到广泛的应用,随着更多框架的采用与更多开发者的加入,声明式UI必将继续发展壮大,成为以后构建用户界面的首选方式。


作者:星际码仔
来源:juejin.cn/post/7212622837063811109
收起阅读 »

Android 带你重新认知属性动画

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第5篇文章,点击查看活动详情 前言 之前写过一篇关于属性动画简单使用的文章juejin.cn/post/714417… 虽然官方直接提供的属性动画只有4个效果:透明度、位移、旋转、缩放,然后用Set实现组合...
继续阅读 »

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第5篇文章,点击查看活动详情


前言


之前写过一篇关于属性动画简单使用的文章juejin.cn/post/714417…


虽然官方直接提供的属性动画只有4个效果:透明度、位移、旋转、缩放,然后用Set实现组合,用插值器加一些效果。但其实属性动画能做的超越你的想象,他能做到anything。你可以实现各种你所想象的效果,改图片形状、路径的动画、颜色的变化等(当然这得是矢量图)。而插值器,除了系统提供的那些插值器之外,你还能进行自定义实现你想要的运动效果。


实现的效果


我这里拿个形变的效果来举例。可以先看看实现的效果:


sp.gif


实现要点


要点主要有两点:(1)要去想象,到了这种程度包括更复杂的效果,没有人能教你的,只能靠自己凭借经验和想象力去规划怎么实现。 (2)要计算,一般做这种自定义的往往会涉及计算的成分,所以你要实现的效果越高端,需要计算的操作就越复杂。


思路


我做这个播放矢量图和暂停矢量图之间的形变,这个思路是这样的: 其实那个三角形是由两部分组成,左边是一个矩形(转90度的梯形),右边是一个三角形。然后把两个图形再分别变成长方形。具体计算方式是我把width分成4份,然后配合一个偏移量offset去进行调整(计算的部分没必要太纠结,都是要调整的)


步骤:



  1. 绘制圆底和两个图形

  2. 属性动画

  3. 页面退出后移除动画


1. 绘制圆底和两个图形


一共三个Paint


init {
paint = Paint()
paint2 = Paint()
paint3 = Paint()

paint?.color = context.resources.getColor(R.color.kylin_main_color)
paint?.isAntiAlias = true
paint2?.color = context.resources.getColor(R.color.kylin_white)
paint2?.style = Paint.Style.FILL
paint2?.isAntiAlias = true
paint3?.color = context.resources.getColor(R.color.kylin_white)
paint3?.isAntiAlias = true
}

绘制圆底就比较简单


paint?.let {
canvas?.drawCircle((width/2).toFloat(), (height/2).toFloat(), (width/2).toFloat(),
it
)
}

然后先看看我的一个参考距离的计算(有这个参考距离,才能让图形大小跟着宽高而定,而不是写死)


override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
if (baseDim == 0f){
baseDim = (0.25 * width).toFloat()
}
}

另外两个图用路径实现


if (path1 == null || path2 == null){
path1 = Path()
path2 = Path()
// 设置初始状态
startToStopAnim(0f)
}
paint2?.let { canvas?.drawPath(path1!!, it) }
paint3?.let { canvas?.drawPath(path2!!, it) }

看具体的绘制实现


private fun startToStopAnim(currentValue : Float){
val offset : Int = (baseDim * 0.25 * (1-currentValue)).toInt()

path1?.reset()
path1?.fillType = Path.FillType.WINDING
path1?.moveTo(baseDim + offset, baseDim) // 点1不变
path1?.lineTo(2 * baseDim+ offset - baseDim/3*currentValue,
baseDim + (0.5 * baseDim).toInt() * (1-currentValue))
path1?.lineTo(2 * baseDim+ offset - baseDim/3*currentValue,
2 * baseDim +(0.5 * baseDim).toInt() + (0.5 * baseDim).toInt() * currentValue)
path1?.lineTo(baseDim+ offset, 3 * baseDim) // 点4不变
path1?.close()


path2?.reset()
path2?.fillType = Path.FillType.WINDING
if (currentValue <= 0f) {
path2?.moveTo(2 * baseDim + offset, baseDim + (0.5 * baseDim).toInt())
path2?.lineTo(3 * baseDim + offset, 2 * baseDim)
path2?.lineTo(2 * baseDim + offset, 2 * baseDim + (0.5 * baseDim).toInt())
}else {
path2?.moveTo(2 * baseDim+ offset + baseDim/3*currentValue,
baseDim + (0.5 * baseDim).toInt() * (1-currentValue))
path2?.lineTo(3 * baseDim + offset, baseDim + baseDim * (1-currentValue))
path2?.lineTo(3 * baseDim + offset, 2 * baseDim + baseDim * currentValue)
path2?.lineTo(2 * baseDim+ offset + baseDim/3*currentValue,
2 * baseDim +(0.5 * baseDim).toInt() + (0.5 * baseDim).toInt() * currentValue)
}
path2?.close()
}

这个计算的过程不好解释,加偏移量就是一个调整的过程,可以去掉偏移量offset看看效果就知道为什么要加了。path1代表左边的路径,左边的路径是4个点,path2是右边的路径,右边的路径会根据情况去决定是3个点还是4个点,默认情况是3个。


2、属性动画


fun startToStopChange(){
isRecordingStart = true
if (mValueAnimator1 == null) {
mValueAnimator1 = ValueAnimator.ofFloat(0f, 1f)
mValueAnimator1?.addUpdateListener {
val currentValue: Float = it.animatedValue as Float
startToStopAnim(currentValue)
postInvalidate()
}
mValueAnimator1?.interpolator = AccelerateInterpolator()
}
mValueAnimator1?.setDuration(500)?.start()
}

float类型0到1其实就是实现一个百分比的效果。变过去能实现后,变回来就也就很方便


fun stopToStartChange(){
isRecordingStart = false
if (mValueAnimator2 == null) {
mValueAnimator2 = ValueAnimator.ofFloat(1f, 0f)
mValueAnimator2?.addUpdateListener {
val currentValue: Float = it.animatedValue as Float
startToStopAnim(currentValue)
postInvalidate()
}
mValueAnimator2?.interpolator = AccelerateInterpolator()
}
mValueAnimator2?.setDuration(500)?.start()
}

3.移除动画


view移除后要移除动画


fun close(){
try {
if (mValueAnimator1?.isStarted == true){
mValueAnimator1?.cancel()
}
if (mValueAnimator2?.isStarted == true){
mValueAnimator2?.cancel()
}
}catch (e : Exception){
e.printStackTrace()
}finally {
mValueAnimator1 = null
mValueAnimator2 = null
}
}

然后还要注意,这个动画是耗时操作,所以要做防快速点击。


总结


从代码可以看出,其实并实现起来并不难,难的在于自己要有想象力,要能想出这样的一个过程,比较花费时间的可能就是一个调整的过程,其它也基本没什么技术难度。


我这个也只是简单做了个Demo来演示,你要问能不能实现其它效果,of course,你甚至可以先把三角形变成一个正方形,再变成两个长方形等等,你甚至可以用上贝塞尔来实现带曲线的效果。属性动画就是那么的强大,对于矢量图,它能实现几乎所有的你想要的效果,只有你想不到,没有它做不到。


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

有些东西你要是早点懂,也不至于走那么多冤枉路

最近在阅读一些书籍和学习一些技术的时候,有一些心得,再和过去自己在不同阶段的一些经历进行反思,总结一些个人的想法和看法,也希望自己在很多年后再回头来看的时候,不像今天回头去看很多年前一样感到有一丝悔意和不甘。 在大学二年级下学期之前,我是处于一种“无头苍蝇”的...
继续阅读 »

最近在阅读一些书籍和学习一些技术的时候,有一些心得,再和过去自己在不同阶段的一些经历进行反思,总结一些个人的想法和看法,也希望自己在很多年后再回头来看的时候,不像今天回头去看很多年前一样感到有一丝悔意和不甘。


在大学二年级下学期之前,我是处于一种“无头苍蝇”的状态,并不是说自己自甘堕落,破罐子破摔,不是的,相反,我是渴求改变自己的,想学东西的,但是,对于我这种普通本科的学生,虽然学校图书馆有你看不完的书,但是,你总得知道你该看什么书,什么样的对你有帮助,知识的海洋是没有边际的,但是一个人的精力是有限的,当把过多的时间花费在一些对自己没有成长,但是自己却在自嗨的事情上的时候是很可怕的。


就拿读书这件事来说,那时候因为我是“无头苍蝇”,所以就“病急乱投医”,总觉得要去看一点书来充实自己,于是我就看了一些历史,三皇五帝,春秋战国,秦汉三国南北朝都去看了,后面又去看了王阳明,曾国藩,后面是越看越觉得不行,我是学软件工程的,怎么研究起历史来了,然后又去寻求内心的安慰,又去听电台,我记得那时候我最爱听的就是《饮鸩不止渴》和《十点读书》,然后里面的一些鸡汤就把自己灌饱了,就觉得“未来可期”,其实后来才发现是“未来可欺”,不过其实对于像我这样的人,整个社会太多太多,他们想改变自己,想未来有一份不错的事业,他们有梦想,有激情,但是,他们却不知道怎么做,他们的父母不懂这个专业,他们的身边也没啥人懂这个专业,他们从小没见过大的世界,所以导致他们“浪费”了很多时光。


当然,读书是一件十分好的事,听电台也很好,但是,在生命的每一个阶段,你自己应该把时间主要花费在什么上面,这是一个很有智慧的问题,读历史,读人物传记能够让我们有更多的思考空间,有宽广的胸怀,让人遇事从容淡定,因为随着时间的推移,枭雄豪杰不过是一堆白骨,但是眼下我们依旧在生活着,是避不开生计,避不开七情六欲的,所以在头顶星辰大海的同时也要看好眼前的路,在自己没有方向的时候,就看别人怎么做的,如果没有目标参考系,那就做自己专业该做的,然后极力去了解相关资讯和技术,这样即使种不出南瓜,但是也绝对不会少了豆子,最主要的是,这个过程它会去锻炼人,提高自己的思考能力。


不过在自己有了目标以后,怎么去实现这个目标更是一个问题,如果没有条理,没有规划,没有结合社会情况,那么,努力就会显得无力苍白,学生时代的时候,说白了,对于普通本科的学校,大家都差不多,自然没有人有更高的论调,所以都沉迷于表面的浮华,而不去关注其核心原理,所以在学习技术的时候,也会显得毛毛躁躁,不去认真专研,而是沉迷于“多”,“炫”上面,其实着是不对的,不论是计算机专业还是其他专业,知识框架是很多的,但是大多数人少了一种刨根问底的精神,当然,刨根问底也并不一定是一种值得称赞的精神,但是在学生时代如果有刨根问底的精神,那么事必成,因为学生时代正是种子发芽的时候,但是进入社会以后,刨根问底未必就是好事了,它和我们的职业规划,个人性格等有很大的关联,比如你觉得你不可能成为技术专家的条件,你的优势也不在哪里,你还去刨根问底技术,那么这不是明智的选择,但是如果暂时还在一个过渡期,很多东西还不确定,那么是有必要去刨根问底的,而且刨得越深越好。


个人定位无论在那个阶段都是很重要的,它是一种判断力,更是一种智慧,你让韩信去管钱粮他肯定不如萧何,你让萧何去管兵马打仗,他肯定不如韩信,所以,没有那个方向是做好的,没有那个岗位是最值得深耕的,这完全要根据自己的情况来,如果你是一个技术控,对技术有无尽的热爱,加上脑子转得也快,那么,从事技术发展肯定是一个很不错的选择,但是如果对技术不敏感,别人一遍就会,而我十遍都不会,那么可能我真的不适合,但是我对商业有洞察力,对客户有一套打法,那么就没必要去技术哪里死磕,虽然你能花11个小时弄懂,但是硬生生花11小时去弄,因为人最宝贵的就是时间,这样就显得不理智,因为其他它从某种意义上已经证明你确实不适合干这个。


主要就说这些了,虽然都是能懂的道理,但是还是要时刻记录下来,给自己看的同时也希望对别人有启示,往往很多时候是环境和认知限制了我们,所以这时候思考和学习就是最好的解法,在资源不充足的情况下,一定会踩很多坑,滩很多浑水,所以寻求资源是一件十分有必要的事情,多向优秀的人请教,学习,多了解这个社会的运转,不要被关在狭小的信息茧房里。


今天的分享就到这里,感谢你

作者:刘牌
来源:juejin.cn/post/7214858677174452281
的观看,我们下期见。

收起阅读 »