注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

实测GPT4!不到1小时写完了一个小程序界面!推理能力提升能直接破译密文?

开始 本想着ChatGPT充钱后能用上GPT4的多模态识别的功能,然后就在充值完成撒花庆祝之后 GPT4生成小程序 发现虽然GPT4可以用了,但是图片上传却还没有更新,并且由于实在是太火爆了,所以只能每4小时发送50条消息。 所以,无奈之下,就只能先测试下...
继续阅读 »

开始


本想着ChatGPT充钱后能用上GPT4的多模态识别的功能,然后就在充值完成撒花庆祝之后



GPT4生成小程序


发现虽然GPT4可以用了,但是图片上传却还没有更新,并且由于实在是太火爆了,所以只能每4小时发送50条消息。


所以,无奈之下,就只能先测试下相比之前的gpt3.5, gpt4的编码能力是否有了一个实质的提升。


于是我决定试试让他写一个微信小程序的评论界面,可以从我们这篇文章跳转进去。于是有了下面这段对话



但是这个界面展示出来过于简陋了当然,这个功能在gpt3.5中也能够有着很好的完成度。于是我接着让他去丰富界面元素。



于是优化之后有了下面这个界面样式(除了界面上的👍图标是我自己加上去的)



但是这样还是不够,这只是一个简单的界面,完成了基本的发送评论功能,但是当我希望再添加一个回复功能时,界面样式就乱了套了。于是这个就只能暂时作罢。于是回复评论就只能暂时作罢。既然上传不了图片,那我就把评论的样式用文字的形式打出来看看它能否实际理解,于是我这样问


你应该使得评论看起来像这样
我的用户名 2022/3/17 6:34
我觉得今天天气真不错 0

继续完善关于界面逻辑操作,比如用户点赞,每条评论只能点击一次,用户评论数限制。



这里其实可以看出GPT4对于上一版本的推理能力的提升。在上一个版本中,当我需要它修改某段逻辑时,它会把完整代码重新写一遍,再然后可能就超过token限制了界面报错。在GPT4中,根据上下文,它可以明确指出与实现这个功能不相关的逻辑,并注释省略,再给出我的问题的答案。



要实现一个完整的功能,许多细节上的东西chatgpt还不能很好的实现,但是根据我们的提示去拆分功能点,它已经可以把这些功能点做到一个相对较好的整合了,更多的是针对通用场景代码的编写。不得不说的是,gpt4已经能够大大降低编码的时间了。


文末评论区迁移到了chatgpt写的界面去了,大家可以尝试互动。


GPT4的文字能力


虽然引导它完成这个界面属实花费了不少时间,但是确实发现它的语言组织能力比之前提升了不少。
所以我决定让它写一篇“码后感”



" 咱把刚刚对话的内容,写一篇博客介绍一下gpt4的实际应用,要求叙事丰富,语言幽默风趣,重点突出,引发思考。文体不限,诗歌除外,不少于1000字。"





最后,我完全沉浸在了GPT-4助手的智慧之中🤯



GPT4推理能力大提升


在上一个版本中,chatgpt一直被诟病的就是数学能力,但在这次升级之中,官方直接放出评测结果




  • 官方放出评测结果:信心满满


在GPT-4的官方发布会上,OpenAI直接公布了关于其数学能力的评测结果。这是对GPT-4在数学方面表现的一种信任和自信的体现。根据评测报告,GPT-4在处理数学问题时的表现已经有了显著提升。不仅如此,它还在某些复杂的数学任务中展现出了出奇制胜的能力,让人惊叹不已。



  • 更强大的数学解题能力:从基础到高级


GPT-4在数学能力方面的提升并不局限于简单的四则运算。事实上,GPT-4已经能够处理一系列复杂数学问题,包括代数、微积分、概率论等领域。这使得它在帮助用户解决数学难题时更具价值。对于那些一直苦恼于数学问题的用户来说,GPT-4无疑是一个福音。



  • 深度理解与推理:数学问题不再令人头疼


GPT-4在数学能力方面的提升不仅体现在解题能力上,还表现在对数学概念的深度理解和推理能力上。在处理数学问题时,GPT-4能够更好地理解题目中的概念和关系,并根据这些信息进行有针对性的推理。这使得它在解决数学问题时更具策略性和准确性。



  • 用户体验的巨大提升:数学辅导不再遥不可及


随着GPT-4在数学能力方面的突破,用户在使用过程中也将享受到更好的体验。不管是学生在解决作业问题时,还是工程师在处理实际项目中的数学挑战,GPT-4都将成为一个强有力的助手。


求解鸡兔同笼


还是使用经典题目来提问


鸡兔同笼,共有30个头,88只脚。
求笼中鸡兔各有多少只?


这波有理有据,可以得出正确答案。


那如果是肯德基呢?



实测密文破译


为了实验它的逻辑能力,我找了一道密码破译的题目。已知这段密文是维吉尼亚密码,简单点说就是需要分析词频,然后再遍历不同长度的密钥找到能与英文单词的常见频率一一对应上。看看结果如何。



Zemlpxphmj tizgeyel zqqlvmtgw ygwlofsgw h tssniy-eshuxlt vpfi vh lpilz crk nsdu, wbegluw hph mcmswvl. Uxbfiuvw zgrkkrn krcgwakkhvmvpw vp Qpuwpqr aq xog Muviypeaksucp Zrejg Wacxpqr sgeypik vlpu jptwa jeuf. wawhlpxz yiyg wlv jvt xog ieemakrn gbwgvpgrjg sm yeaelppk aji scyuel vh xogmy tizgeyel pp tltwvp jyqq Dcpsqtz Hppila Hejkppvc pp Zptkppmh. Vlhv ieemagqlpx mchlf aogr Vtfpves Ugpgrjgw’ Hpxhtiz tsjmia, ylpel dcw zgx aq hlnmcgv aji jcvnq, wbhjltik cr hpstcpf fyykrn cwjgra.



可能是期望过高,这段密码始终没有成功破解不管是直接推出密码还是给出密钥长度提示,甚至给出了密钥,依然无法解密这段文本。


image.png
image.png


所以只能期待接下来的更新中是否会进一步地加强了。


评论留言


我用gpt4写的评论留言界面链接入口在这里,感兴趣的可以一起讨论。


image.png


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

如何写一个炫酷的大屏仿真页

前言 之前我写过一遍文章《从阅读页仿真页看贝塞尔曲线》,简要的和大家介绍了仿真页的具体实现思路,正好写完文章的时候,看到 OPPO 发布会里面提到了仿真页,像这样: 看着确实有点炫酷,我平时也接触了很多跟阅读器相关的代码,就零零碎碎花了一些时间撸了一个双页仿...
继续阅读 »

前言


之前我写过一遍文章《从阅读页仿真页看贝塞尔曲线》,简要的和大家介绍了仿真页的具体实现思路,正好写完文章的时候,看到 OPPO 发布会里面提到了仿真页,像这样:


OPPO折叠屏


看着确实有点炫酷,我平时也接触了很多跟阅读器相关的代码,就零零碎碎花了一些时间撸了一个双页仿真。


看效果:


11.gif


由于使用录屏,所以看着有点卡顿,实际效果非常流畅!


一、基础知识具备


仿生页里面用到很多自定义 View 的知识,比如:



  1. 贝塞尔曲线

  2. 熟悉 Canvas、Paint 和 Path 等常用的Api

  3. Matrix


具备这些知识以后,我们就可以看懂绝大部分的代码了。这一篇同样并不想和大家过多的介绍代码,具体的可以看一下代码。


二、双仿真和单仿真有什么不同


我写双仿真的时候,感觉和单仿真有两点不同:



  • 绘制的页数

  • 背部的贴图处理


首先,单仿真只要准备两页的数据:


QQ20230312-0.jpg


背部的内容也是第一页的内容,需要对第一页内容进行翻转再平移。


而双仿真需要准备六页的内容,拿左边来说:


QQ20230312-1.jpg


我们需要准备上层图片(柯基)、背部图片(阿拉斯加)和底部图片(吉娃娃,看不清),因为我们不知道用户会翻页哪侧,所以两侧一共需要准备六页的数据。


由于翻转机制的不一样,双仿真对于背部的内容只需要平移就行,但是需要新的一页内容,这里相对来说比单仿真简单。


三、我做了哪些优化


主要对翻页的思路进行了优化,


正常的思路是这样的,手指落下的点即页脚:


QQ20230312-2.jpg


这样写起来更加简单,但是对于用户来说,可操作的区域比较小,相对来说有点难用。


另外一种思路就是,手指落下的点即到底部同等距离的边:


QQ20230312-4.jpg


即手指落位的位置到当前页页脚距离 = 翻动的位置到当前页脚的距离


使用这种方式的好处就是用户可以操作的区域更大,翻书的感觉跟翻实体书的感觉更类似,也更加跟手。


总结


这篇文章就讲到这了,这个 Demo 其实是一个半成品,还有一些手势没处理,阴影的展示还有一些问题。


写仿真比较难的地方在于将一些场景转化成代码,有些地方确实很难去想。


talk is cheap, show me code:


仓库地址:github.com/mCyp/Double…


如果觉得本文不错,点赞是对本文最好的肯定,如果你还有任何问题,欢迎评论区讨论!


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

理解TextView三部曲之番外篇:或许这会是最终的进化

额,为什么会有番外篇呢。。因为新版本上线后,别的同学用我的这个控件,描边显示出问题了-_-! 什么问题呢? 我把问题抽出来,同时把问题放大点,给大家看看(抹眼泪.png)   好嘛,问题不大。。就是描边歪了一点点,对吧。 可是怎么会这样!?,我自己测根本就没...
继续阅读 »

额,为什么会有番外篇呢。。因为新版本上线后,别的同学用我的这个控件,描边显示出问题了-_-!


什么问题呢?


我把问题抽出来,同时把问题放大点,给大家看看(抹眼泪.png)


1_error_show



好嘛,问题不大。。就是描边歪了一点点,对吧。


可是怎么会这样!?,我自己测根本就没有问题,压根就没出现过这样的问题啊。。(抹眼泪.png)


我又去检查了一遍计算描边位置那块的代码,最初是以为其他同学一不小心该了那块的代码,导致描边位置计算出错了,结果发现,代码丝毫没有动过的痕迹。


那怎么会描边出错呢?而且他描边出问题的地方,在我这里这里显示也没什么问题,在他那里会什么会有这么大的偏差呢?


我不信邪,看看那位同学都对StrokeTextView做了哪些设置?结果发现,他多了下面这行代码:


mStrokeTextView.setTypeface(typeface);

我捉摸了一下,发现这行代码很有问题,因为我的StrokeTextView是继承自TextView的,调用setTypeface(),看看它的默认实现:


public void setTypeface(@Nullable Typeface tf) {
if (mTextPaint.getTypeface() != tf) {
mTextPaint.setTypeface(tf);

if (mLayout != null) {
nullLayouts();
requestLayout();
invalidate();
}
}
}

看了一眼我就明白了,它只是给TextPaint设置了不同的typeFace,而我们的描边是使用不同的TextPaint,也就是说setTypeface()只是给我们的文本设置了字体,却没有给我们的StrokeTextPaint设置相同的字体,导致了两种不同字体之间,没有办法对齐位置,导致了描边差异。


怎么解决?简单,照葫芦画瓢就行,我们在StrokeTextView重写setTypeface()方法。


setTypeface()的默认实现有两种,我们都要重写:


@Override
public void setTypeface(@androidx.annotation.Nullable Typeface tf) {
// 模仿TextView的设置
// 需在super.setTypeface()调用之前,不然没有效果
if (mStrokePaint != null && mStrokePaint.getTypeface() != tf) {
mStrokePaint.setTypeface(tf);
}

super.setTypeface(tf);
}


另一种比较复杂,不过我们会模仿就行了:


public void setTypeface(@Nullable Typeface tf, int style) {
if (style > 0) {
if (tf == null) {
tf = Typeface.defaultFromStyle(style);
} else {
tf = Typeface.create(tf, style);
}

setTypeface(tf);
// now compute what (if any) algorithmic styling is needed
int typefaceStyle = tf != null ? tf.getStyle() : 0;
int need = style & ~typefaceStyle;
getPaint().setFakeBoldText((need & Typeface.BOLD) != 0);
getPaint().setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);

// 同步设置mStrokeTextPaint
if (mStrokePaint != null) {
mStrokePaint.setFakeBoldText((need & Typeface.BOLD) != 0);
mStrokePaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
}
} else {
getPaint().setFakeBoldText(false);
getPaint().setTextSkewX(0);

// 同步设置mStrokeTextPaint
if (mStrokePaint != null) {
mStrokePaint.setFakeBoldText(false);
mStrokePaint.setTextSkewX(0);
}

setTypeface(tf);
}
}

两步解决,但为什么我这显示没问题,别的同学那里显示就出问题了呢?


我突然想起来,相同字体在不同手机上显示是有差异的,而且有些手机不一定都支持那种字体


我和那位同学用着不同厂商的真机进行测试,而我的真机是不支持他设置的字体的,所以看着没问题,但他的小米是支持的。难怪我这看着没问题,他那看着就很离谱。


修改完后,我们在运行一遍。


2_fixed_show


怎一个完美形容!ok,bug解决了,准备提交代码



就这样结束了吗?


时隔多日,我又重新审核了一遍代码,我留意到这样一行代码


float heightWeNeed
= getCompoundPaddingTop() + getCompoundPaddingBottom() + mStrokeWidth + mTextRect.height() + DensityUtil.dp2px(getContext(), 4);

我们需要的高度 = 内边距 + 描边高度 + 文本高度 + 一个额外设定的值 ?


怎么会需要一个额外的值呢?要实现wrap_content的效果,我们的宽度不是只需要加上边距、文本高度和一个描边的高度吗?


好奇怪的逻辑,这不是多余嘛,我当时怎么想的来着哈哈?不符合我wrap_content的预期,把它删了试试,再测一遍


把我之前的测试用例都测了一遍,都运行正常


除了。。除了下面这种情况。


3_error_show



果然,去掉额外的高度,就会有这种高度不够显示的情况。看来当时的我,就是遇到了这种情况,然后一个手快,就给heightWeNeed做了这种适配。


不过这种手快的适配方法貌似不太优雅,为了适配单一的这种情况,要牺牲剩下的所有情况都增加一个额外的高度。


而且因为我们适配的额外高度是一个固定值,如果我们给文本字体大小设置大一点,还是会有高度不够显示的可能,毕竟文本变大了,所需要的高度也就更多了。


好吧,这种适配方法看来是用不得了,要换一个吗?但是计算高度的公式 = 内边距 + 文本高度 + 描边高度,这个公式肯定是没错的。


回到我们最初的问题,我们为什么会需要增加一个额外的固定高度呢?明明公式都是对的,为什么还是会有偏差,难道是公式里的对应的值计算错误了?


我们看看再来看看这个式子:


heightWeNeed = getCompoundPaddingTop() + getCompoundPaddingBottom() +
mStrokeWidth + mTextRect.height();

其中,getCompoundPaddingTop() 和 getCompoundPaddingBottom() 是Android提供的计算内边距的api,这个肯定不至于错吧。


mStrokeWidth是我们的描边宽度,是由用户使用时自定义的,这个没什么需要计算的,就是一个值而已


那么mTextRect.height() 这个呢,我们需要这里返回一个正确的文本高度。


看看这个mTextRect是在哪里赋值的


getPaint().getTextBounds(text, 0, text.length(), mTextRect);

从getTextBounds()里跟下去,发现最后调用测量的是native方法,看不到内部实现,不过我们可以看看getTextBounds()的注释


/**
* Retrieve the text boundary box and store to bounds.
*
* Return in bounds (allocated by the caller) the smallest rectangle that
* encloses all of the characters, with an implied origin at (0,0).
*
* @param text string to measure and return its bounds
* @param start index of the first char in the string to measure
* @param end 1 past the last char in the string to measure
* @param bounds returns the unioned bounds of all the text. Must be allocated by the caller
*/
public void getTextBounds(String text, int start, int end, Rect bounds) {
...
// native 方法
nGetStringBounds(mNativePaint, text, start, end, mBidiFlags, bounds);
}

Return in bounds the smallest rectangle that encloses all of the characters


在bounds中返回包含所有字符的最小矩形


5_bounds_height


也就是说bounds返回的高度,只是能够包含文本的最小高度。


我们在三部曲概览里就讨论过,安卓里文本的描绘,是由几根线来确定的


4_text_lines


文本的高度应该为(fontMetrics.bottom -fontMetrics. top),但是,bounds中返回的height也够文本显示啊?怎么会显示成下面这个样子?


6_error_show


比如这样


7_thought_show


但实际情况好像是这样的


8_thought_show_2


我想到,安卓绘制文本是有起点坐标的,这个起点由gravity,textAlign,和baseline确定,和内容展示高度好像没有关系。


虽然我们展示高度设小了,但它的起点坐标还在原来的位置(比如y坐标baseline),这才导致了18数字显示不完整,底部好像缺了一块。



问题的根本找到了,看来好像有两种解决方法



  1. 调整baseline的位置:把我们的baseline位置上移一些,让它和展示区域底部位置重合,这样就能以最小区域显示完整的文本内容。

  2. 拓宽bounds.height的高度,以(fontMetrics.bottom - fontMetrics.top)作为文本的高度显示,这样就无需改变baseline的位置,但比第一种方案要多需要一些空间。


这里我选了第二种,顺着系统的绘制规则来,图个方便,而且我们的描边也可以利用文本顶部多出来的这些空间。


我们新设个变量 textHeight = fontMetrics.descent - fontMetrics.top


heightWeNeed = getCompoundPaddingTop() + getCompoundPaddingBottom() +
textHeight + mStrokeWidth / 2;

为了最大化利用空间,文字顶部到top线的距离已经足够我们的描边显示了,而bottom线到descent线之间的距离很窄,就可能不够我们的描边显示。


所以只需要在文字底部加一半的描边宽度,同时去掉buttom线和descent线之间的距离,这样就能确保文字和描边都有足够的位置显示了。


好了,番外篇终于结束了,看了眼字数,居然比之前的三部曲系列都要多一些。实在没想到需要这么长的篇幅来讲这两个小优化,谢谢小伙伴们能够看到这里啦。


源码我都已经上传到github了,欢迎小伙伴自取,如果觉得写得不错的,还请给这份工程给个star ~_ <



兄dei,如果觉得我写的还不错,麻烦帮个忙呗 :-)



  1. 给俺点个赞被,激励激励我,同时也能让这篇文章让更多人看见,(#^.^#)

  2. 不用点收藏,诶别点啊,你怎么点了?这多不好意思!

  3. 噢!还有,我维护了一个路由库。。没别的意思,就是提一下,我维护了一个路由库 =.= !!


拜托拜托,谢谢各位同学!


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

前端加载超大图片(100M以上)实现秒开解决方案

web
前言前端加载超大图片时,一般可以采取以下措施实现加速:图片压缩:将图片进行压缩可以大幅减小图片的大小,从而缩短加载时间。压缩图片时需要注意保持图片质量,以免影响图片显示效果。图片分割:将超大图片分割成多个小图块进行加载,可以避免一次性加载整个图片,从而加快加载...
继续阅读 »

前言

前端加载超大图片时,一般可以采取以下措施实现加速:

  1. 图片压缩:将图片进行压缩可以大幅减小图片的大小,从而缩短加载时间。压缩图片时需要注意保持图片质量,以免影响图片显示效果。

  2. 图片分割:将超大图片分割成多个小图块进行加载,可以避免一次性加载整个图片,从而加快加载速度。这种方式需要在前端实现图片拼接,需要确保拼接后的图片无缝衔接。

  3. CDN 加速:使用 CDN(内容分发网络)可以将图片缓存在离用户更近的节点上,从而加速图片加载速度。如果需要加载的图片是静态资源,可以将其存储在 CDN 上,以便快速访问。

  4. 懒加载:懒加载是一种图片延迟加载的方式,即当用户浏览到需要加载的图片时才进行加载,可以有效避免一次性加载大量图片而导致页面加载速度缓慢。

  5. WebP 格式:使用 WebP 格式可以将图片大小减小到 JPEG 和 PNG 的一半以下,从而加快图片加载速度。

  6. HTTP/2:使用 HTTP/2 协议可以并行加载多个图片,从而加快页面加载速度。

  7. 预加载:预加载是在页面加载完毕后,提前加载下一步所需要的资源。在图片加载方面,可以在页面加载完毕后提前加载下一个需要显示的图片,以便用户快速浏览。

而对于几百M或上G的大图而言,不管对图片进行怎么优化或加速处理,要实现秒开也是不太可能的事情。而上面介绍的第二条“图像分割切片”是最佳解决方案。下面介绍下如何对大图进行分割,在前端进行拼接实现秒开。

图像切片原理介绍

图像切片是指将一张大图分割成若干个小图的过程,以便于存储和处理。图像切片常用于网络地图、瓦片地图、图像拼接等应用中。

切片原理主要包括以下几个步骤:

  1. 定义切片大小:首先需要定义每个小图的大小,一般情况下是正方形或矩形。

  2. 计算切片数量:根据定义的切片大小,计算原始图像需要被切成多少个小图。计算公式为:切片数量 = 原始图像宽度 / 切片宽度 × 原始图像高度 / 切片高度。

  3. 切割图像:按照计算出的切片数量,将原始图像分割成相应数量的小图。可以使用图像处理库或自己编写代码实现。

  4. 存储切片:将切割后的小图存储到磁盘上,可以使用常见的图片格式,如JPEG、PNG等。

  5. 加载切片:在需要显示切片的地方,根据需要加载相应的小图,组合成完整的图像。

使用图像切片可以降低处理大图像的复杂度,同时也能够提高图像的加载速度,使得用户可以更快地查看图像的细节。图像切片广泛应用于需要处理大图像的场景,能够提高图像处理和显示效率,同时也能够提高用户的体验。

实现

先上效果图


上传打开图形

先上传大图,至后台进行切片处理, 上传相关代码为:

async onChangeFile(file) {
           try {
               message.info('文件上传中,请稍候...')
               this.isSelectFile = false;
               this.uploadMapResult = await svc.uploadMap(file.raw);
               if (this.uploadMapResult.error) {
                   message.error('上传图形失败!' + this.uploadMapResult.error)
                   return
              }
               this.form.mapid = this.uploadMapResult.mapid;
               this.form.uploadname = this.uploadMapResult.uploadname;
               this.maptype = this.uploadMapResult.maptype || '';
               this.dialogVisible = true;
          } catch (error) {
               console.error(error);
               message.error('上传图形失败!', error)
          }
      }

如果需要上传后对图像进行处理,可以新建一个cmd.txt文件,把处理的命令写进文件中,然后和图像一起打包成zip上传。

如需要把1.jpg,2.jpg拼接成一个新的图片m1.png再打开,cmd.txt的写法如下:

join
1.jpg
2.jpg
m1.png
horizontal

再把1.jpg,2.jpg,cmd.txt三个文件打包成zip文件上传即可

打开图像相关代码

async onOpenMap() {
           try {
               let mapid = this.form.mapid;
               let param = {
                   ...this.uploadMapResult,
                   // 图名称
                   mapid: this.form.mapid,
                   // 上传完返回的fileid
                   fileid: this.uploadMapResult.fileid,
                   // 上传完返回的文件名
                   uploadname: this.form.uploadname,
                   // 地图打开方式
                   mapopenway: this.form.openway === "直接打开图形" ? vjmap.MapOpenWay.Memory : vjmap.MapOpenWay.GeomRender,
                   // 如果要密码访问的话,设置秘钥值
                   secretKey: this.form.isPasswordProtection ? svc.pwdToSecretKey(this.form.password) : undefined,
                   style: vjmap.openMapDarkStyle(),// div为深色背景颜色时,这里也传深色背景样式
                   // 图像类型设置地图左上角坐标和分辨率
                   imageLeft: this.form.imageLeft ? +this.form.imageLeft : undefined,
                   imageTop: this.form.imageTop ? +this.form.imageTop : undefined,
                   imageResolution: this.form.imageResolution ? +this.form.imageResolution : undefined,
              }
               let isVectorStyle = this.form.openway === "存储后渲染矢量";
               await openMap(param, isVectorStyle);
          } catch (error) {
               console.error(error);
               message.error('打开图形失败!', error)
          }
      }

应用案例

应用一 对图像进行拼接前端查看

原始图片为



最终效果为:


体验地址: vjmap.com/app/cloud/#…

应用二 对tiff影像进行切片并与CAD图叠加校准

对tiff影像上传时可设置地理坐标范围。

tiff/tfw, jpg/jpgw坐标文件的格式(6个参数) 0.030000 0.0000000000 0.0000000000 -0.030000 451510.875000 3358045.000000

以上每行对应的含义:

1 地图单元中的一个象素在X方向上的X分辨率尺度。 2 平移量。 3 旋转量。 4 地图单元中的一个象素在Y方向上的Y分辨率尺度的负值。 5 象素1,1(左上方)的X地坐标。 6 象素1,1(左上方)的Y地坐标。

在上传图时需要根据文件中的第一个,第五个和第六个值设置地图范围


或者上传完后,操作菜单中点击设置地图范围进行设置


影像地图切片完成后,可与CAD图进行叠加校准。效果如下


体验地址: vjmap.com/demo/#/demo…

作者:vjmap
来源:juejin.cn/post/7212270321622106170

收起阅读 »

Android App封装 ——架构(MVI + kotlin + Flow)

项目搭建经历记录 Android App封装 ——架构(MVI + kotlin + Flow) Android App封装 —— ViewBinding Android App封装 —— DI框架 Hilt?Koin? Android App封装 —— 实...
继续阅读 »

项目搭建经历记录



  1. Android App封装 ——架构(MVI + kotlin + Flow)

  2. Android App封装 —— ViewBinding

  3. Android App封装 —— DI框架 Hilt?Koin?

  4. Android App封装 —— 实现自己的EventBus


一、背景


最近看了好多MVI的文章,原理大多都是参照google发布的 应用架构指南,但是实现方式有很多种,就想自己封装一套自己喜欢用的MVI架构,以供以后开发App使用。


说干就干,准备对标“玩Android”,利用提供的数据接口,搭建一个自己习惯使用的一套App项目,项目地址:Github wanandroid


二、MVI


先简单说一下MVI,从MVC到MVP到MVVM再到现在的MVI,google是为了一直解决痛点所以不断推出新的框架,具体的发展流程就不多做赘诉了,网上有好多,我们可以选择性适合自己的。


应用架构指南中主要的就是两个架构图:


2.1 总体架构


image.png


Google推荐的是每个应用至少有两层:



  • UI Layer 界面层: 在屏幕上显示应用数据

  • Data Layer 数据层: 提供所需要的应用数据(通过网络、文件等)

  • Domain Layer(optional)领域层/网域层 (可选):主要用于封装数据层的逻辑,方便与界面层的交互,可以根据User Case


图中主要的点在于各层之间的依赖关系是单向的,所以方便了各层之间的单元测试


2.2 UI层架构


UI简单来说就是拿到数据并展示,而数据是以state表示UI不同的状态传送给界面的,所以UI架构分为



  • UI elements层:UI元素,由activity、fragment以及包含的控件组成

  • State holders层: state状态的持有者,这里一般是由viewModel承担


image.png


2.3 MVI的特点


MVI相比与MVVM的核心区别是它的两大特性:


1. 唯一可信数据源


唯一可信数据源,是为了解决MVVM中View层使用大量LiveData,导致各种LiveData数据并行更新或者互相交互时会偶尔出现不可控逻辑,导致偶现一些的奇奇怪怪的Bug。


MVI使用唯一可信的数据源UI State来避免这种问题。


2. 数据单向流动。


image.png


从图中可以看到,



  1. 数据从Data Layer -> ViewModel -> UI,数据是单向流动的。ViewModel将数据封装成UI State传输到UI elements中,而UI elements是不会传输数据到ViewModel的。

  2. UI elements上的一些点击或者用户事件,都会封装成events事件,发送给ViewModel。



PS:这里有同学问,为啥不直接调用ViewModel的方法,还要弄个events事件流这么麻烦?


的确,如果直接调用是很方便,但是这样UI和ViewModel就耦合了,这时就要像MVP架构那样定义很多接口才能解耦。而定义events事件流就是另外一种方便解耦的方法,避免接口膨胀。其次,这个也是为了保证数据的单向流动,如果UI和ViewModel能直接调用方法的话,如果方法还有返回值,就破坏了数据的单向流动。



2.4 搭建MVI要注意的点


了解了MVI的原理和特点后,我们就要开始着手搭建了,其中需要解决的有以下几点



  1. 定义UI Stateevents

  2. 构建UI State单向数据流UDF

  3. 构建事件流events

  4. UI State的订阅和发送


三、搭建项目


3.1 定义UI Stateevents


我们可以用interface先定义一个抽象的UI Stateeventseventintent是一个意思,都可以用来表示一次事件。


@Keep
interface IUiState

@Keep
interface IUiIntent

然后根据具体逻辑定义页面的UIState和UiIntent。


data class MainState(val bannerUiState: BannerUiState, val detailUiState: DetailUiState) : IUiState

sealed class BannerUiState {
object INIT : BannerUiState()
data class SUCCESS(val models: List<BannerModel>) : BannerUiState()
}

sealed class DetailUiState {
object INIT : DetailUiState()
data class SUCCESS(val articles: ArticleModel) : DetailUiState()
}

通过MainState将页面的不同状态封装起来,从而实现唯一可信数据源


3.2 构建单向数据流UDF


在ViewModel中使用StateFlow构建UI State流。



  • _uiStateFlow用来更新数据

  • uiStateFlow用来暴露给UI elements订阅


abstract class BaseViewModel<UiState : IUiState, UiIntent : IUiIntent> : ViewModel() {

private val _uiStateFlow = MutableStateFlow(initUiState())
val uiStateFlow: StateFlow<UiState> = _uiStateFlow

protected abstract fun initUiState(): UiState

protected fun sendUiState(copy: UiState.() -> UiState) {
_uiStateFlow.update { copy(_uiStateFlow.value) }
}
}

class MainViewModel : BaseViewModel<MainState, MainIntent>() {

override fun initUiState(): MainState {
return MainState(BannerUiState.INIT, DetailUiState.INIT)
}
}

3.3 构建事件流


在ViewModel中使用 Channel构建事件流



有人好奇这里为啥用Channel,而不用SharedFlow或者StateFlow?


Channel就像一个队列一样,适合实现单个生产者和单个消费者之间的通信,而 SharedFlow 更适合实现多个观察者订阅同一数据源。而这里的Intent事件更像前者,各个协程生产出不同的Intent事件通过Channel发送给ViewModel,然后在ViewModel中集中处理消费。




  1. _uiIntentFlow用来传输Intent

  2. 在viewModelScope中开启协程监听uiIntentFlow,在子ViewModel中只用重写handlerIntent方法就可以处理Intent事件了

  3. 通过sendUiIntent就可以发送Intent事件了


abstract class BaseViewModel<UiState : IUiState, UiIntent : IUiIntent> : ViewModel() {

private val _uiIntentFlow: Channel<UiIntent> = Channel()
val uiIntentFlow: Flow<UiIntent> = _uiIntentFlow.receiveAsFlow()

fun sendUiIntent(uiIntent: UiIntent) {
viewModelScope.launch {
_uiIntentFlow.send(uiIntent)
}
}

init {
viewModelScope.launch {
uiIntentFlow.collect {
handleIntent(it)
}
}
}

protected abstract fun handleIntent(intent: IUiIntent)

class MainViewModel : BaseViewModel<MainState, MainIntent>() {

override fun handleIntent(intent: IUiIntent) {
when (intent) {
MainIntent.GetBanner -> {
requestDataWithFlow()
}
is MainIntent.GetDetail -> {
requestDataWithFlow()
}
}
}
}

3.4 UI State的订阅和发送


3.4.1 订阅UI State


在Activity中订阅UI state的变化



  1. lifecycleScope中开启协程,collect uiStateFlow

  2. 使用map 来做局部变量的更新

  3. 使用distinctUntilChanged来做数据防抖


class MainActivity : BaseMVIActivity() {

private fun registerEvent() {
lifecycleScope.launchWhenStarted {
mViewModel.uiStateFlow.map { it.bannerUiState }.distinctUntilChanged().collect { bannerUiState ->
when (bannerUiState) {
is BannerUiState.INIT -> {}
is BannerUiState.SUCCESS -> {
bannerAdapter.setList(bannerUiState.models)
}
}
}
}
lifecycleScope.launchWhenStarted {
mViewModel.uiStateFlow.map { it.detailUiState }.distinctUntilChanged().collect { detailUiState ->
when (detailUiState) {
is DetailUiState.INIT -> {}
is DetailUiState.SUCCESS -> {
articleAdapter.setList(detailUiState.articles.datas)
}
}

}
}
}
}

3.4.2 发送Intent


直接调用sendUiIntent就可以发送Intent事件


button.setOnClickListener {
mViewModel.sendUiIntent(MainIntent.GetBanner)
mViewModel.sendUiIntent(MainIntent.GetDetail(0))
}

3.4.3 更新Ui State


调用sendUiState发送Ui State更新


需要注意的是: 在UiState改变时,使用的是copy复制一份原来的UiState,然后修改变动的值。这是为了做到 “可信数据源”,在定义MainState的时候,设置的就是val,是为了避免多线程并发读写,导致线程安全的问题。


class MainViewModel : BaseViewModel<MainState, MainIntent>() {
private val mWanRepo = WanRepository()

override fun initUiState(): MainState {
return MainState(BannerUiState.INIT, DetailUiState.INIT)
}

override fun handleIntent(intent: IUiIntent) {
when (intent) {
MainIntent.GetBanner -> {
requestDataWithFlow(showLoading = true,
request = { mWanRepo.requestWanData() },
successCallback = { data -> sendUiState { copy(bannerUiState = BannerUiState.SUCCESS(data)) } },
failCallback = {})
}
is MainIntent.GetDetail -> {
requestDataWithFlow(showLoading = false,
request = { mWanRepo.requestRankData(intent.page) },
successCallback = { data -> sendUiState { copy(detailUiState = DetailUiState.SUCCESS(data)) } })
}
}
}
}

其中 requestDataWithFlow 是封装的一个网络请求的方法


protected fun <T : Any> requestDataWithFlow(
showLoading: Boolean = true,
request: suspend () -> BaseData<T>,
successCallback: (T) -> Unit,
failCallback: suspend (String) -> Unit = { errMsg ->
//默认异常处理
},
)
{
viewModelScope.launch {
val baseData: BaseData<T>
try {
baseData = request()
when (baseData.state) {
ReqState.Success -> {
sendLoadUiState(LoadUiState.ShowMainView)
baseData.data?.let { successCallback(it) }
}
ReqState.Error -> baseData.msg?.let { error(it) }
}
} catch (e: Exception) {
e.message?.let { failCallback(it) }
}
}
}

至此一个MVI的框架基本就搭建完毕了


3.5运行效果


www.alltoall.net_device-2022-12-15-161207_I_ahtLP5Kj.gif

四、 总结


不管是MVC、MVP、MVVM还是MVI,主要就是View和Model之间的交互关系不同



  • MVI的核心是 数据的单向流动

  • MVI使用kotlin flow可以很方便的实现 响应式编程

  • MV整个View只依赖一个State刷新,这个State就是 唯一可信数据源


目前搭建了基础框架,后续还会在此项目的基础上继续封装jetpack等更加完善这个项目。


项目源码地址:Github wanandroid


作者:剑冲
来源:juejin.cn/post/7177619630050000954
收起阅读 »

Rust在Android端的入门开发

前言 IOS上应用还在半路上,遇到了一些集成问题。在了解、学习过程中发现,IOS的Swifit UI动画真的是比Flutter做的好几倍,后面有时间可以记录记录。本次先记录Android集成吧,对比性能的话,可以在rust中for循环个10万次,对比C的时间消...
继续阅读 »

前言


IOS上应用还在半路上,遇到了一些集成问题。在了解、学习过程中发现,IOSSwifit UI动画真的是比Flutter做的好几倍,后面有时间可以记录记录。本次先记录Android集成吧,对比性能的话,可以在rustfor循环个10万次,对比C的时间消耗。

参考资料

Building and Deploying a Rust library on Android

JNI Create

Create JNI


目录


Rust在Android端的入门开发.png


一、环境准备


rustup配置


这个配置,在装rust的时候就配置了,可以忽略。如果没有配置,想了解的可以看二、Rust入门之Hello World


配置NDK


第一步

先确定自己的NDK目录

默认目录一般都在 /Users/你的用户名/Library/Android/sdk/ndk-bundle 这个位置,用户目录可以用 ${HOME} 代替。


第二步

创建库crate


cargo new android_demo --lib

第三步

切换到 android_demo 项目下,创建 NDK 文件

找到 make_standalone_toolchain.py 文件,执行以下语句


python D:/Android/SDK/ndk-bundle/build/tools/make_standalone_toolchain.py --api 26 --arch arm64 --install-dir NDK/arm64
python D:/Android/SDK/ndk-bundle/build/tools/make_standalone_toolchain.py --api 26 --arch arm --install-dir NDK/arm
python D:/Android/SDK/ndk-bundle/build/tools/make_standalone_toolchain.py --api 26 --arch x86 --install-dir NDK/x86

对应的NDK目录如下


rust_ndk_1.PNG


第四步

找到 cargo的配置文件,~/.cargo/config


[target.aarch64-linux-android]
ar = "E:/VSCodeWorkspace/rust/android_demo/NDK/arm64/bin/aarch64-linux-android-ar"
linker = "E:/VSCodeWorkspace/rust/android_demo/NDK/arm64/bin/aarch64-linux-android-clang"

[target.armv7-linux-androideabi]
ar = "E:/VSCodeWorkspace/rust/android_demo/NDK/arm/bin/arm-linux-androideabi-ar"
linker = "E:/VSCodeWorkspace/rust/android_demo/NDK/arm/bin/arm-linux-androideabi-clang"

[target.i686-linux-android]
ar = "E:/VSCodeWorkspace/rust/android_demo/NDK/x86/bin/i686-linux-android-ar"
linker = "E:/VSCodeWorkspace/rust/android_demo/NDK/x86/bin/i686-linux-android-clang"

其中 E:/VSCodeWorkspace/rust/android_demo 是本次项目目录。


第五步

添加工具链


rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android

第六步

在当前 android_demo 目录下,执行以下语句

编译Rust项目,按需要的架构编译即可。


cargo build --target aarch64-linux-android --release
cargo build --target armv7-linux-androideabi --release
cargo build --target i686-linux-android --release


  • 出现问题

    • note: %1 不是有效的 Win32 应用程序。 (os error 193) ,第三步和第六步编译不一致。解决方法:将第四步,换成Android SDK 目录下的ndk,看下面代码示例。

    • error: linker cc not found,解决方案也是按照下面,一定要使用 .cmd




解决方案


[target.aarch64-linux-android]
ar = "D:\\Android\\SDK\\ndk\\21.4.7075529\\toolchains\\llvm\\prebuilt\\windows-x86_64\\bin\\aarch64-linux-android-ar"
linker = "D:\\Android\\SDK\\ndk\\21.4.7075529\\toolchains\\llvm\\prebuilt\\windows-x86_64\\bin\\aarch64-linux-android26-clang.cmd"

[target.armv7-linux-androideabi]
ar = "D:\\Android\\SDK\\ndk\\21.4.7075529\\toolchains\\llvm\\prebuilt\\windows-x86_64\\bin\\arm-linux-androideabi-ar"
linker = "D:\\Android\\SDK\\ndk\\21.4.7075529\\toolchains\\llvm\\prebuilt\\windows-x86_64\\bin\\armv7a-linux-androideabi26-clang++.cmd"
xxx

产物


rust_target_2.PNG


二、Rust实现


Cargo.toml


[package]
name = "android_demo"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
jni-sys = "0.3.0"

[target.'cfg(target_os="android")'.dependencies]
jni = { version = "0.5", default-features = false }

[lib]
crate-type = ["dylib"]

lib.rs


/*
* @Author: axiong
*/

use std::os::raw::{c_char};
use std::ffi::{CString, CStr};

#[no_mangle]
pub extern fn rust_greeting(to: *const c_char) -> *mut c_char {
let c_str = unsafe { CStr::from_ptr(to) };
let recipient = match c_str.to_str() {
Err(_) => "there",
Ok(string) => string,
};

CString::new("Hello ".to_owned() + recipient).unwrap().into_raw()
}

/// Expose the JNI interface for android below
/// 只有在目标平台是Android的时候才开启 [cfg(target_os="android")
/// 由于JNI要求驼峰命名,所以要开启 allow(non_snake_case)
#[cfg(target_os="android")]
#[allow(non_snake_case)]
pub mod android {
extern crate jni;

use super::*;
use self::jni::JNIEnv;
use self::jni::objects::{JClass, JString};
use self::jni::sys::{jstring};

#[no_mangle]
pub unsafe extern fn Java_com_rjx_rustdemo_RustGreeting_greeting(env: JNIEnv, _: JClass, java_pattern: JString) -> jstring {
// Our Java companion code might pass-in "world" as a string, hence the name.
let world = rust_greeting(env.get_string(java_pattern).expect("invalid pattern string").as_ptr());
// Retake pointer so that we can use it below and allow memory to be freed when it goes out of scope.
let world_ptr = CString::from_raw(world);
let output = env.new_string(world_ptr.to_str().unwrap()).expect("Couldn't create java string!");

output.into_inner()
}
}

三、Android集成


SO集成


rust_android_001.PNG


RustGreeting.java


public class RustGreeting {
static {
System.loadLibrary("android_demo");
}

private static native String greeting(final String pattern);

public static String sayHello(String to) {
return greeting(to);
}

}

MainActivity.java


public class MainActivity extends AppCompatActivity {

// Used to load the 'native-lib' library on application startup.
static {
//System.loadLibrary("native-lib");
}

private ActivityMainBinding binding;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());

// Example of a call to a native method
TextView tv = binding.sampleText;
tv.setText(RustGreeting.sayHello("Rust!!"));
}

}

效果


Rust_Hello.PNG


作者:CodeOver
来源:juejin.cn/post/7170696817682694152
收起阅读 »

在 Flutter 多人视频中实现虚拟背景、美颜与空间音效

在之前的「基于声网 Flutter SDK 实现多人视频通话」里,我们通过 Flutter + 声网 SDK 完美实现了跨平台和多人视频通话的效果,那么本篇我们将在之前例子的基础上进阶介绍一些常用的特效功能。 本篇主要带你了解 SDK 里几个实用的 API ...
继续阅读 »

在之前的「基于声网 Flutter SDK 实现多人视频通话」里,我们通过 Flutter + 声网 SDK 完美实现了跨平台和多人视频通话的效果,那么本篇我们将在之前例子的基础上进阶介绍一些常用的特效功能。



本篇主要带你了解 SDK 里几个实用的 API 实现,相对简单



虚拟背景


虚拟背景是视频会议里最常见的特效之一,在声网 SDK 里可以通过 enableVirtualBackground 方法启动虚拟背景支持。


首先,因为我们是在 Flutter 里使用,所以我们可以在 Flutter 里放一张 assets/bg.jpg 图片作为背景,这里有两个需要注意的点:




  • assets/bg.jpg 图片需要在 pubspec.yaml 文件下的 assets 添加引用


      assets:
      - assets/bg.jpg



  • 需要在 pubspec.yaml 文件下添加 path_provider: ^2.0.8path: ^1.8.2 依赖,因为我们需要把图片保存在 App 本地路径下




如下代码所示,首先我们通过 Flutter 内的 rootBundle 读取到 bg.jpg ,然后将其转化为 bytes, 之后调用 getApplicationDocumentsDirectory 获取路径,保存在的应用的 /data" 目录下,然后就可以把图片路径配置给 enableVirtualBackground 方法的 source ,从而加载虚拟背景。


Future<void> _enableVirtualBackground() async {
 ByteData data = await rootBundle.load("assets/bg.jpg");
 List<int> bytes =
     data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
 Directory appDocDir = await getApplicationDocumentsDirectory();
 String p = path.join(appDocDir.path, 'bg.jpg');
 final file = File(p);
 if (!(await file.exists())) {
   await file.create();
   await file.writeAsBytes(bytes);
}

 await _engine.enableVirtualBackground(
     enabled: true,
     backgroundSource: VirtualBackgroundSource(
         backgroundSourceType: BackgroundSourceType.backgroundImg,
         source: p),
     segproperty:
         const SegmentationProperty(modelType: SegModelType.segModelAi));
 setState(() {});
}

如下图所示是都开启虚拟背景图片之后的运行效果,当然,这里还有两个需要注意的参数:



  • BackgroundSourceType :可以配置 backgroundColor(虚拟背景颜色)、backgroundImg(虚拟背景图片)、backgroundBlur (虚拟背景模糊) 这三种情况,基本可以覆盖视频会议里的所有场景

  • SegModelType :可以配置为 segModelAi (智能算法)或 segModelGreen(绿幕算法)两种不同场景下的抠图算法。




这里需要注意的是,在官方的提示里,建议只在搭载如下芯片的设备上使用该功能(应该是对于 GPU 有要求):



  • 骁龙 700 系列 750G 及以上

  • 骁龙 800 系列 835 及以上

  • 天玑 700 系列 720 及以上

  • 麒麟 800 系列 810 及以上

  • 麒麟 900 系列 980 及以上



另外需要注意的是,为了将自定义背景图的分辨率与 SDK 的视频采集分辨率适配,声网 SDK 会在保证自定义背景图不变形的前提下,对自定义背景图进行缩放和裁剪


美颜


美颜作为视频会议里另外一个最常用的功能,声网也提供了 setBeautyEffectOptions 方法支持一些基础美颜效果调整。


如下代码所示, setBeautyEffectOptions 方法里主要是通过 BeautyOptions 来调整画面的美颜风格,参数的具体作用如下表格所示。



这里的 .5 只是做了一个 Demo 效果,具体可以根据你的产品需求,配置出几种固定模版让用户选择。



_engine.setBeautyEffectOptions(
 enabled: true,
 options: const BeautyOptions(
   lighteningContrastLevel:
       LighteningContrastLevel.lighteningContrastHigh,
   lighteningLevel: .5,
   smoothnessLevel: .5,
   rednessLevel: .5,
   sharpnessLevel: .5,
),
);

属性作用
lighteningContrastLevel对比度,常与 lighteningLevel 搭配使用。取值越大,明暗对比程度越大
lighteningLevel美白程度,取值范围为 [0.0,1.0],其中 0.0 表示原始亮度,默认值为 0.0。取值越大,美白程度越大
smoothnessLevel磨皮程度,取值范围为 [0.0,1.0],其中 0.0 表示原始磨皮程度,默认值为 0.0。取值越大,磨皮程度越大
rednessLevel红润度,取值范围为 [0.0,1.0],其中 0.0 表示原始红润度,默认值为 0.0。取值越大,红润程度越大
sharpnessLevel锐化程度,取值范围为 [0.0,1.0],其中 0.0 表示原始锐度,默认值为 0.0。取值越大,锐化程度越大

运行后效果如下图所示,开了 0.5 参数后的美颜整体画面更加白皙,同时唇色也更加明显。


没开美颜开了美颜

色彩增强


接下来要介绍的一个 API 是色彩增强: setColorEnhanceOptions ,如果是美颜还无法满足你的需求,那么色彩增强 API 可以提供更多参数来调整你的需要的画面风格。


如下代码所示,色彩增强 API 很简单,主要是调整 ColorEnhanceOptionsstrengthLevelskinProtectLevel 参数,也就是调整色彩强度和肤色保护的效果


  _engine.setColorEnhanceOptions(
     enabled: true,
     options: const ColorEnhanceOptions(
         strengthLevel: 6.0, skinProtectLevel: 0.7));

如下图所示,因为摄像头采集到的视频画面可能存在色彩失真的情况,而色彩增强功能可以通过智能调节饱和度和对比度等视频特性,提升视频色彩丰富度和色彩还原度,最终使视频画面更生动。



开启增强之后画面更抢眼了。



没开增加开了美颜+增强

属性参数
strengthLevel色彩增强程度。取值范围为 [0.0,1.0]。0.0 表示不对视频进行色彩增强。取值越大,色彩增强的程度越大。默认值为 0.5。
skinProtectLevel肤色保护程度。取值范围为 [0.0,1.0]。0.0 表示不对肤色进行保护。取值越大,肤色保护的程度越大。默认值为 1.0。 当色彩增强程度较大时,人像肤色会明显失真,你需要设置肤色保护程度; 肤色保护程度较大时,色彩增强效果会略微降低。 因此,为获取最佳的色彩增强效果,建议动态调节 strengthLevel 和 skinProtectLevel 以实现最合适的效果。

空间音效


其实声音调教才是重头戏,声网既然叫声网,在音频处理上肯定不能落后,在声网 SDK 里就可以通过 enableSpatialAudio 打开空间音效的效果。


_engine.enableSpatialAudio(true);

什么是空间音效?简单说就是特殊的 3D 音效,它可以将音源虚拟成从三维空间特定位置发出,包括听者水平面的前后左右,以及垂直方向的上方或下方。



本质上空间音效就是通过一些声学相关算法计算,模拟实现类似空间 3D 效果的音效实现



同时你还可以通过 setRemoteUserSpatialAudioParams 来配置空间音效的相关参数,如下表格所示,可以看到声网提供了非常丰富的参数来让我们可以自主调整空间音效,例如这里面的 enable_blurenable_air_absorb 效果就很有意思,十分推荐大家去试试。


属性作用
speaker_azimuth远端用户或媒体播放器相对于本地用户的水平角。 取值范围为 [0,360],单位为度,例如 (默认)0 度,表示水平面的正前方;90 度,表示水平面的正左方;180 度,表示水平面的正后方;270 度,表示水平面的正右方;360 度,表示水平面的正前方;
speaker_elevation远端用户或媒体播放器相对于本地用户的俯仰角。 取值范围为 [-90,90],单位为度。(默认)0 度,表示水平面无旋转;-90 度,表示水平面向下旋转 90 度;90 度,表示水平面向上旋转 90 度
speaker_distance远端用户或媒体播放器相对于本地用户的距离,取值范围为 [1,50],单位为米,默认值为 1 米。
speaker_orientation远端用户或媒体播放器相对于本地用户的朝向。 取值范围为 [0,180],单位为度。默认)0 度,表示声源和听者朝向同一方向;180: 180 度,表示声源和听者面对面
enable_blur是否开启声音模糊处理
enable_air_absorb是否开启空气衰减,即模拟声音在空气中传播的音色衰减效果:在一定的传输距离下,高频声音衰减速度快、低频声音衰减速度慢。
speaker_attenuation远端用户或媒体播放器的声音衰减系数,取值范围为[0,1]。 0:广播模式,即音量和音色均不随距离衰减;(0,0.5):弱衰减模式,即音量和音色在传播过程中仅发生微弱衰减;0.5:(默认)模拟音量在真实环境下的衰减,效果等同于不设置 speaker_attenuation 参数;(0.5,1]:强衰减模式,即音量和音色在传播过程中发生迅速衰减
enable_doppler是否开启多普勒音效:当声源与接收声源者之间产生相对位移时,接收方听到的音调会发生变化


音频类的效果这里就无法展示了,强烈推荐大家自己动手去试试。



人声音效


另外一个推荐的 API 就是人声音效:setAudioEffectPreset, 调用该方法可以通过 SDK 预设的人声音效,在不会改变原声的性别特征的前提下,修改用户的人声效果,例如:


_engine.setAudioEffectPreset(AudioEffectPreset.roomAcousticsKtv);

声网 SDK 里预设了非常丰富的 AudioEffectPreset ,如下表格所示,从场景效果如 KTV、录音棚,到男女变声,再到恶搞的音效猪八戒等,可以说是相当惊艳。


参数作用
audioEffectOff原声
roomAcousticsKtvKTV
roomAcousticsVocalConcert演唱会
roomAcousticsStudio录音棚
roomAcousticsPhonograph留声机
roomAcousticsVirtualStereo虚拟立体声
roomAcousticsSpacial空旷
roomAcousticsEthereal空灵
roomAcousticsVirtualSurroundSound虚拟环绕声
roomAcoustics3dVoice3D 人声
voiceChangerEffectUncle大叔
voiceChangerEffectOldman老年男性
voiceChangerEffectBoy男孩
voiceChangerEffectSister少女
voiceChangerEffectGirl女孩
voiceChangerEffectPigking猪八戒
voiceChangerEffectHulk绿巨人
styleTransformationRnbR&B
styleTransformationPopular流行
pitchCorrection电音


PS:为获取更好的人声效果,需要在调用该方法前将 setAudioProfile 的 scenario 设为 audioScenarioGameStreaming(3):


_engine.setAudioProfile(
 profile: AudioProfileType.audioProfileDefault,
 scenario: AudioScenarioType.audioScenarioGameStreaming);


当然,这里需要注意的是,这个方法只推荐用在对人声的处理上,不建议用于处理含音乐的音频数据


最后,完整代码如下所示:


class VideoChatPage extends StatefulWidget {
 const VideoChatPage({Key? key}) : super(key: key);

 @override
 State<VideoChatPage> createState() => _VideoChatPageState();
}

class _VideoChatPageState extends State<VideoChatPage> {
 late final RtcEngine _engine;

 ///初始化状态
 late final Future<bool?> initStatus;

 ///当前 controller
 late VideoViewController currentController;

 ///是否加入聊天
 bool isJoined = false;

 /// 记录加入的用户id
 Map<int, VideoViewController> remoteControllers = {};

 @override
 void initState() {
   super.initState();
   initStatus = _requestPermissionIfNeed().then((value) async {
     await _initEngine();

     ///构建当前用户 currentController
     currentController = VideoViewController(
       rtcEngine: _engine,
       canvas: const VideoCanvas(uid: 0),
    );
     return true;
  }).whenComplete(() => setState(() {}));
}

 Future<void> _requestPermissionIfNeed() async {
   if (Platform.isMacOS) {
     return;
  }
   await [Permission.microphone, Permission.camera].request();
}

 Future<void> _initEngine() async {
   //创建 RtcEngine
   _engine = createAgoraRtcEngine();
   // 初始化 RtcEngine
   await _engine.initialize(const RtcEngineContext(
     appId: appId,
  ));

   _engine.registerEventHandler(RtcEngineEventHandler(
     // 遇到错误
     onError: (ErrorCodeType err, String msg) {
       if (kDebugMode) {
         print('[onError] err: $err, msg: $msg');
      }
    },
     onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
       // 加入频道成功
       setState(() {
         isJoined = true;
      });
    },
     onUserJoined: (RtcConnection connection, int rUid, int elapsed) {
       // 有用户加入
       setState(() {
         remoteControllers[rUid] = VideoViewController.remote(
           rtcEngine: _engine,
           canvas: VideoCanvas(uid: rUid),
           connection: const RtcConnection(channelId: cid),
        );
      });
    },
     onUserOffline:
        (RtcConnection connection, int rUid, UserOfflineReasonType reason) {
       // 有用户离线
       setState(() {
         remoteControllers.remove(rUid);
      });
    },
     onLeaveChannel: (RtcConnection connection, RtcStats stats) {
       // 离开频道
       setState(() {
         isJoined = false;
         remoteControllers.clear();
      });
    },
  ));

   // 打开视频模块支持
   await _engine.enableVideo();
   // 配置视频编码器,编码视频的尺寸(像素),帧率
   await _engine.setVideoEncoderConfiguration(
     const VideoEncoderConfiguration(
       dimensions: VideoDimensions(width: 640, height: 360),
       frameRate: 15,
    ),
  );

   await _engine.startPreview();
}

 @override
 void dispose() {
   _engine.leaveChannel();
   super.dispose();
}

 @override
 Widget build(BuildContext context) {
   return Scaffold(
       appBar: AppBar(),
       body: Stack(
         children: [
           FutureBuilder<bool?>(
               future: initStatus,
               builder: (context, snap) {
                 if (snap.data != true) {
                   return const Center(
                     child: Text(
                       "初始化ing",
                       style: TextStyle(fontSize: 30),
                    ),
                  );
                }
                 return AgoraVideoView(
                   controller: currentController,
                );
              }),
           Align(
             alignment: Alignment.topLeft,
             child: SingleChildScrollView(
               scrollDirection: Axis.horizontal,
               child: Row(
                 ///增加点击切换
                 children: List.of(remoteControllers.entries.map(
                  (e) => InkWell(
                     onTap: () {
                       setState(() {
                         remoteControllers[e.key] = currentController;
                         currentController = e.value;
                      });
                    },
                     child: SizedBox(
                       width: 120,
                       height: 120,
                       child: AgoraVideoView(
                         controller: e.value,
                      ),
                    ),
                  ),
                )),
              ),
            ),
          )
        ],
      ),
       floatingActionButton: FloatingActionButton(
         onPressed: () async {
           // 加入频道
           _engine.joinChannel(
             token: token,
             channelId: cid,
             uid: 0,
             options: const ChannelMediaOptions(
               channelProfile:
                   ChannelProfileType.channelProfileLiveBroadcasting,
               clientRoleType: ClientRoleType.clientRoleBroadcaster,
            ),
          );
        },
      ),
       persistentFooterButtons: [
         ElevatedButton.icon(
             onPressed: () {
               _enableVirtualBackground();
            },
             icon: const Icon(Icons.accessibility_rounded),
             label: const Text("虚拟背景")),
         ElevatedButton.icon(
             onPressed: () {
               _engine.setBeautyEffectOptions(
                 enabled: true,
                 options: const BeautyOptions(
                   lighteningContrastLevel:
                       LighteningContrastLevel.lighteningContrastHigh,
                   lighteningLevel: .5,
                   smoothnessLevel: .5,
                   rednessLevel: .5,
                   sharpnessLevel: .5,
                ),
              );
               //_engine.setRemoteUserSpatialAudioParams();
            },
             icon: const Icon(Icons.face),
             label: const Text("美颜")),
         ElevatedButton.icon(
             onPressed: () {
               _engine.setColorEnhanceOptions(
                   enabled: true,
                   options: const ColorEnhanceOptions(
                       strengthLevel: 6.0, skinProtectLevel: 0.7));
            },
             icon: const Icon(Icons.color_lens),
             label: const Text("增强色彩")),
         ElevatedButton.icon(
             onPressed: () {
               _engine.enableSpatialAudio(true);
            },
             icon: const Icon(Icons.surround_sound),
             label: const Text("空间音效")),
         ElevatedButton.icon(
             onPressed: () {                
               _engine.setAudioProfile(
                   profile: AudioProfileType.audioProfileDefault,
                   scenario: AudioScenarioType.audioScenarioGameStreaming);
               _engine
                  .setAudioEffectPreset(AudioEffectPreset.roomAcousticsKtv);
            },
             icon: const Icon(Icons.surround_sound),
             label: const Text("人声音效")),
      ]);
}

 Future<void> _enableVirtualBackground() async {
   ByteData data = await rootBundle.load("assets/bg.jpg");
   List<int> bytes =
       data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
   Directory appDocDir = await getApplicationDocumentsDirectory();
   String p = path.join(appDocDir.path, 'bg.jpg');
   final file = File(p);
   if (!(await file.exists())) {
     await file.create();
     await file.writeAsBytes(bytes);
  }

   await _engine.enableVirtualBackground(
       enabled: true,
       backgroundSource: VirtualBackgroundSource(
           backgroundSourceType: BackgroundSourceType.backgroundImg,
           source: p),
       segproperty:
           const SegmentationProperty(modelType: SegModelType.segModelAi));
   setState(() {});
}
}

最后


本篇的内容作为上一篇的补充,相对来说内容还是比较简单,不过可以看到不管是在画面处理还是在声音处理上,声网 SDK 都提供了非常便捷的 API 实现,特别在声音处理上,因为文章限制这里只展示了简单的 API 介绍,所以强烈建议大家自己尝试下这些音频 API ,真的非常有趣。


作者:无知小猿
来源:juejin.cn/post/7211388928242352184
收起阅读 »

聊一下AIGC

“UGC不存在了”——借鉴自《三体》 ChatGPT 的横空出世将一个全新的概念推上风口——AIGC( AI Generated Content)。 GC即创作内容(Generated Content),和传统的UGC、PGC,OGC不同的是,AIGC的创作...
继续阅读 »

“UGC不存在了”——借鉴自《三体》



ChatGPT 的横空出世将一个全新的概念推上风口——AIGC( AI Generated Content)。


GC即创作内容(Generated Content),和传统的UGC、PGC,OGC不同的是,AIGC的创作主体由人变成了人工智能。



xGC



  • PGC:Professionally Generated Content,专业生产内容

  • UGC:User Generated Content,用户生产内容

  • OGC:Occupationally Generated Content,品牌生产内容。



AI 可以 Generate 哪些 Content?


作为淘宝内容线的开发,我们每天都在和内容打交道,那么AI到底能生成什么内容?


围绕着不同形式的内容生产,AIGC大致分为以下几个领域:



文本生成


基于NLP的文本内容生成根据使用场景可分为非交互式文本生成交互式文本生成


非交互式文本生成包括摘要/标题生成、文本风格迁移、文章生成、图像生成文本等。


交互式文本生成主要包括聊天机器人、文本交互游戏等。


【代表性产品或模型】:JasperAI、copy.AI、ChatGPTBard、AI dungeon等。



图像生成


图像生成根据使用场可分为图像编辑修改图像自主生成


图像编辑修改可应用于图像超分、图像修复、人脸替换、图像去水印、图像背景去除等。


图像自主生成包括端到端的生成,如真实图像生成卡通图像、参照图像生成绘画图像、真实图像生成素描图像、文本生成图像等。


【代表性产品或模型】:EditGAN,Deepfake,DALL-E、MidJourneyStable Diffusion文心一格等。



音频生成


音频生成技术较为成熟,在C端产品中也较为常见,如语音克隆,将人声1替换为人声2。还可应用于文本生成特定场景语音,如数字人播报、语音客服等。此外,可基于文本描述、图片内容理解生成场景化音频、乐曲等。


【代表性产品或模型】:DeepMusic、WaveNet、Deep Voice、MusicAutoBot等。



视频生成


视频生成与图像生成在原理上相似,主要分为视频编辑视频自主生成


视频编辑可应用于视频超分(视频画质增强)、视频修复(老电影上色、画质修复)、视频画面剪辑(识别画面内容,自动场景剪辑)


视频自主生成可应用于图像生成视频(给定参照图像,生成一段运动视频)、文本生成视频(给定一段描述性文字,生成内容相符视频)


【代表性产品或模型】:Deepfake,videoGPT,Gliacloud、Make-A-Video、Imagen video等。



多模态生成


以上四种模态可以进行组合搭配,进行模态间转换生成。如文本生成图像(AI绘画、根据prompt提示语生成特定风格图像)、文本生成音频(AI作曲、根据prompt提示语生成特定场景音频)、文本生成视频(AI视频制作、根据一段描述性文本生成语义内容相符视频片段)、图像生成文本(根据图像生成标题、根据图像生成故事)、图像生成视频。


【代表性产品或模型】:DALL-E、MidJourney、Stable Diffusion等。


本文接下来将会着重讲述文本类AIGC和图像类AIGC。


文本类AIGC


RNN → Transformer → GPT(ChatGPT)


最近势头正猛的ChatGPT就是文本类AIGC的代表。


ChatGPT(Chat Generative Pre-trained Transformer),即聊天生成型预训练变换模型,Transformer指的是一种非常重要的算法模型,稍后将会介绍。


其实现在的用户对于聊天机器人已经很熟悉了,比如天猫精灵、小爱同学或是Siri等语音助手。那为什么ChatGPT一出现,这些语音助手就显得相形见绌呢?


本质上是NLP模型之间的差异。


在自然语义理解领域(NLP)中,RNN和Transformer是最常见的两类模型。


循环神经网络(recurrent neural network)


RNN,即循环神经网络(recurrent neural network)源自于1982年由Saratha Sathasivam 提出的霍普菲尔德网络。下图所示是一个RNN网络的简易展示图,左侧是一个简单的循环神经网络,它由输入层、隐藏层和输出层组成。



RNN 的主要特点在于 w 带蓝色箭头的部分。输入层为 x,隐藏层为 s,输出层为 o。U 是输入层到隐藏层的权重,V 是隐藏层到输出层的权重。隐藏层的值 s 不仅取决于当前时刻的输入 x,还取决于上一时刻的输入。权重矩阵 w 就是隐藏层上一次的值作为这一次的输入的权重。由此可见,这种网络的特点是,每一个时刻的输入依赖于上一个时刻的输出,难以并行化计算。



从人类视角理解RNN 人类可以根据语境或者上下文,推断语义信息。就比如,一个人说了:我喜欢旅游,其中最喜欢的地方是三亚,以后有机会一定要去___,很显然这里应该填”三亚”。 但是机器要做到这一步就比较困难。RNN的本质是像人一样拥有记忆的能力,因此,它的输出就依赖于当前的输入和记忆。



Transformer


而Transformer模型诞生于2017年,起源自《Attention Is All You Need》。这是一种基于Attention机制来加速深度学习算法的模型,可以进行并行化计算,而且每个单词在处理过程中注意到了其他单词的影响,效果非常好。




Attention机制:又称为注意力机制,顾名思义,是一种能让模型对重要信息重点关注并充分学习吸收的技术。通俗的讲就是把注意力集中放在重要的点上,而忽略其他不重要的因素。 其中重要程度的判断取决于应用场景,根据应用场景的不同,Attention分为空间注意力时间注意力,前者用于图像处理,后者用于自然语言处理。




Transformer是完全基于自注意力机制的一个深度学习模型,有关该模型的介绍,详情可参考下面这篇文章👇


人工智能 LLM 革命前夜:一文读懂横扫自然语言处理的 Transformer 模型


由于Transformer的存在加速了深度学习的效果,基于海量数据的进行样本训练便有了可能。至此,LLM正式从幕后走向台前。


LLM,Large Language Model 即大型语言模型。这个大不仅仅指数据集的“大”,同样也是指算法模型的“大”。一般来说,在训练数据足够充足的情况下,往往是模型越大效果越好。在某种程度上说,甚至只要样本足够,哪怕模型“稍微简单”一些,也是可以取得不错的结果的。


笔者在2019年时曾翻译过一篇文章👇


机器学习竞赛实际上是一场数据竞赛


这篇文章的主要观点便是“AI竞争本质上就是数据之争”,所有希望创建有影响力、有价值的AI应用都应该认识到以下三点:



  1. 差异化数据是这场AI游戏成功的关键

  2. 有意义的数据比全面的数据好

  3. 起点应该是自己所擅长的东西


以ChatGPT为例,其本质是基于GPT3的一种变体,而GPT又是基于Transformer模型的一种演化。从模型参数上来说,GPT3共使用了1750亿个参数训练而成,而ChatGPT只使用了15亿个参数,但其数据集是却是整个互联网和几百万本书大概3千亿文字。哪怕是这样,却也是对一众使用RNN的NLP程序造成了降维打击。


GPT



这篇文章写到一半的时候GPT-4发布了,现在作为小插曲来扩展一下



笔者在和朋友的日常交流中发现大家总是将ChatGPT和GPT混为一谈,其实这是两个不同的东西。让我们来问一下New Bing这两者的区别。



很显然,从Bing给我的回答看来,为了让对话更加生动和有趣,ChatGPT是一个专为聊天设计的专业模型,而GPT则是一个通用语言模型。GPT4就是这个模型发展到第四代的模样,相较于GPT3,GPT4可以做的事情变得更多了。



  • GPT-4 是一个更大的模型,网传拥有约 1000 万亿个参数,这意味着它能够处理更多的数据,学习更多的知识和技能。

  • GPT-4 能够接受多模态的输入,例如文本、图像、音频和视频,并生成相应的输出。这使得它能够处理更复杂和丰富的任务,例如图像描述、语音识别和视频生成。


类ChatGPT


在国内一直都有一句调侃的话,叫做“国外一开源,国内就自主研发”。那既然算法模型是公开的,代码也已经开源了,那在国内,那些类ChatGPT的模型是不是应该如“雨后春笋”般涌现了呢?


事实上并没有,本质上还是因为LLM的扩展和维护是相当困难的。主要来源于以下几点:



  1. 漫长的训练时间

  2. 高昂的费用开支

  3. 海量的训练数据

  4. 稀缺的高端人才



时代的眼泪



  • 2017 - Attention is all you need

  • 2023 - Money is all you need



以复旦大学开源的类ChatGPT应用MOSS为例,虽然不知道具体的模型参数数量,但其负责人表示相较于ChatGPT少了一个数量级,再加上简中互联网作为其训练样本,训练质量可想而知。


点此体验👉moss.fastnlp.top/




关于训练的样本数据,这里举一个小例子🌰。 同样是搜索代码段,ChatGPT给你推StackOverflow的答案,MOSS给你推csdn的答案,高下立判



本来还想补充一下百度的文心一言的,结果他们发布了一个ChatPPT,网上一堆段子,这里就不吐槽了。


图像类AIGC


说完了文本类AIGC,我们再来看看最近另一个比较火的领域——图像类AIGC。


俗话说,饱暖思淫欲。作为“第一生产力”的“性”,很多技术发展都离不开他。扎克伯克创建Facebook的起因就是为了更好的认识小姐姐。而图像类AIGC出圈的一个很大原因就在于,他生成的美女小姐姐越来越真实了。


作为一个业余摄影师,第一眼也没能正确分辨出下面这两张图谁是真人,谁是AI画出来的人。




那么问题来了:这些由AI生成出来的美女是如何生成的呢?


GAN → DiffusioModel → Stable Diffusion


生成对抗网络( Generative Adversarial Networks,GAN


2014年 Ian GoodFellow提出了生成对抗网络,成为早期最著名的生成模型。GAN使用零和博弈策略学习,在图像生成中应用广泛。以GAN为基础产生了多种变体,如DCGAN,StytleGAN,CycleGAN等。



零和博弈是指参与博弈的各方,在严格竞争下,一方的收益必然意味着另一方的损失,博弈各方的收益和损失相加总和永远为“零” ,双方不存在合作的可能。



一个简单易懂的例子


有两个人,都快要渴死了,现在他们面前只有一杯水,抢到水的那人得到的收益是1,没抢到水的那个人会死掉,收益为-1,总收益为0。这就是零和博弈。


GAN包含两个部分:



  • 生成器: 学习生成合理的数据。对于图像生成来说是给定一个向量,生成一张图片。其生成的数据作为判别器的负样本。

  • 判别器:判别输入是生成数据还是真实数据。网络输出越接近于0,生成数据可能性越大;反之,真实数据可能性越大。



如上图,我们希望通过GAN生成一些手写体来以假乱真。我们定义生成器与判别器:



  • 生成器:图中蓝色部分网络结构,其输入为一组向量,可以表征数字编号、字体、粗细、潦草程度等。在这里使用特定分布随机生成。

  • 判别器:在训练阶段,利用真实数据与生成数据训练二分类模型,输出为0-1之间概率,越接近1,输入为真实数据可能性越大。


生成器与判别器相互对立。在不断迭代训练中,双方能力不断加强,最终的理想结果是生成器生成的数据,判别器无法判别是真是假。(和周伯通的左右互搏很像)


以生成对抗网络为基础产生的应用:图像超分、人脸替换、卡通头像生成等。


扩散模型( Diffusion Model,里程碑式模型


扩散是受到非平衡热力学的启发,定义一个扩散步骤的马尔科夫链,并逐渐向数据中添加噪声,然后学习逆扩散过程,从噪声中构建出所需的样本。扩散模型的最初设计是用于去除图像中的噪声。随着降噪系统的训练时间越来越长且越来越好,可以从纯噪声作为唯一输入,生成逼真的图片。



马尔科夫链指的是一种随机过程,它的特点是当前状态只依赖于前一个状态,而与其他历史状态无关。



一个马尔科夫链的例子是股市模型,它有三种状态:牛市、熊市和横盘。 每种状态之间有一定的转移概率,例如从牛市到熊市的概率是0.4,从熊市到牛市的概率是0.2,等等。这样就可以用一个转移矩阵来描述这个马尔科夫链。


一个标准的扩散模型分为两个过程:前向过程与反向过程。在前向扩散阶段,图像被逐渐引入的噪声污染,直到图像成为完全随机噪声。在反向过程中,利用一系列马尔可夫链在每个时间步逐步去除预测噪声,从而从高斯噪声中恢复数据。


前向扩散过程,向原图中逐步加入噪声,直到图像成为完全随机噪声。



反向降噪过程,在每个时间步逐步去除噪声,从而从高斯噪声中恢复源数据。



扩散模型的工作原理是通过添加噪声来破坏训练数据,然后通过逆转这个噪声过程来学习恢复数据。换句话说,扩散模型可以从噪声中生成连贯的图像。


扩散模型通过向图像添加噪声进行训练,然后模型学习如何去除噪声。然后,该模型将此去噪过程应用于随机种子以生成逼真的图像。


下图为向原始图像中添加噪声,使原始图像成为随机噪声。



下图为从噪声中恢复的原始图像的变种图像。



Stable Diffusion(Stability AI 文本生成图像,代码与模型开源


2022年8月,Stability AI发布了Stable Diffusion ,这是一种开源Diffusion模型,代码与模型权重均向公众开放。


通过prompt提示语“郊区街区一栋房子的照片,灯光明亮的超现实主义艺术,高度细致8K”,生成图像如下,整体风格与内容锲合度高,AI作画质量较高。


在线体验👉huggingface.co/spaces/stab…



仰望星空:AIGC与元宇宙




结论先行,我认为Web3.0就是元宇宙,AIGC为元宇宙提供养料。



第一代互联网(Web1.0)是PC(个人计算机)互联网,从1994年发展至今。Web1.0让人类第一次掌握高效的传输信息的手段,随着各大网页应用的普及,互联网用户被迅速的连接起来,从而提升了全球信息的传输效率,各大门户网站从此处开始大放异彩。


第二代互联网(Web2.0)是移动互联网,从2008年左右拉开大幕,至今仍精彩纷呈。正是由于移动设备具备“永远在线”和“随时随地”的特点,“上网”二字逐渐从大众的视野消失,因为每个人时时刻刻都生活在网络里。


第三代互联网(Web3.0)是互联网的下一代技术发展方向,主要特征是去中心化、安全、开放和自主。元宇宙是一种虚拟的、持续的、共享的和交互的数字世界,可以让人们以不同的身份和形式参与其中。Web 3.0为元宇宙提供了技术基础和可能性,而元宇宙则是Web3.0在应用场景和生活方式上的体现。


百度百科对于元宇宙的定义是👇



元宇宙(Metaverse) 是人类运用数字技术构建的,由现实世界映射或超越现实世界,可与现实世界交互的虚拟世界,具备新型社会体系的数字生活空间。



本质上,元宇宙是一种新型社会形态,在元宇宙中,更多工作和生活将被数字化。


更多的数字化,意味着需要更丰富的虚拟地图、虚拟场景、虚拟对象和虚拟角色,这其中涉及到大量数字内容的生产和制作。以往,数字内容制作开发周期较长,通常以年计,在生产方式上,或来源于现实,通过扫描或重建模型实现材质、光影、动作捕捉等,或通过创作工具辅助艺术家实现。而这些刚好是AIGC所擅长的。AIGC广泛的适用性可以为元宇宙提供全新内容生成解决方案。



🌰 脑洞一下 开放世界游戏一直被大家视作元宇宙“数字世界”的雏形,试想一下,如果未来的3A大作,NPC的脸部模型、肢体动作是Diffusion Model生成的,聊天是ChatGPT和你对话,语音是Deep Voice创作的,你还会觉得他就只是一个普普通通的NPC吗?抑或是,此时看文章的你,才是地球Online里的一个NPC呢?



脚踏实地:AIGC的应用场景


元宇宙目前还只是大家的一个美好幻想,Web3.0究竟走向何方还需要大家共同探索,作为时代的先锋,既要学会抬头仰望星空,也不能忘记低头看路。


如今,AIGC的浪潮已起,作为非算法行业的从业者,底层的算法实现并非我们关心的重点,如何发挥AI的作用,创造出实际的价值才是我们应该探讨的方向。除了聊天机器人、画画这种不痛不痒的功能外,AIGC可能会颠覆的应用场景会有哪些呢?


人工智能助理


AIGC+搜索已经成为我现在获取信息的第一途径了。目前,New Bing作为我的贴心小助手,不管是什么想要了解的知识点,他都可以快速的给我解答,省去了我在海量信息中筛选的过程。




辅助工作流



摘自Microsoft 365 Copilot官网


人类天生就有梦想、创造、创新的本能。我们每个人都渴望做一些有意义的工作——写一部伟大的小说,做一个发现,建立一个强大的社区,照顾生病的人。我们都有与工作核心相连的冲动。但是今天,我们花了太多时间在那些消耗我们时间、创造力和精力的琐碎任务上。为了重新连接到工作的灵魂,我们不仅需要一种更好地做同样事情的方法。我们需要一种全新的工作方式。 —— 翻译自 ChatGPT


GPT4发布的第二天,Microsoft 365 Copilot变横空出世,宣传片相信大家都已经看到了,从此以后我们的工作方式将永远改变,开启新一轮的生产力大爆发。


除此之外,前段时间笔记软件Notion也上线了自己的AI助手,可以帮助用户更轻松、更快捷地完成日常任务。主要包括自动文本生成、内容推荐、智能搜索、情感分析等。



可以预见,AIGC的出现将会极大的改变现有的工作模式,未来,越来越多的效率软件、办公软件将会推出其自己的AI解决方案。



🎺 插播一条行业动态 36氪独家获悉,钉钉已完成对协同办公厂商「我来wolai」的全资收购。3月5日,我来wolai(上海我云网络科技有限公司)数位核心团队成员已经退出公司股东名单。公司法人已变为钉钉总裁叶军,公司则由阿里100%控股。36氪就上述消息向钉钉求证,钉钉官方表示:我来wolai团队已加入钉钉,将负责智能化协作文档的研发,和个人版文档的产品设计。



文本生成器(对话、文案、代码……)



笔者最近一直沉迷“以xxx的口吻调教ChatGPT”,上图便是在绩效季到来之际用chatGPT给老板们整的活,别的不说,效果还是蛮不错的。自行跳转👉 《老板评语生成器》


ChatGPT最强大的便是其背后的庞大数据,基于此,你甚至可以让那些不存在的人物、已故的人物出现。笔者最近刚看完电视剧《三体》,如果真的有一款“三体游戏”,里面的墨子、秦始皇等人物会不会就是ChatGPT生成的呢?


如果你也想调教出一个自己的对话机器人,可以试试这个网站👉open-gpt.app/


关于AIGC的落地,最后推荐两个和我们饭碗有关的AIGC产品


Codeium



Github Copilot



AIGC在伦理问题上的攻与守


攻——利用AIGC生成有害内容


AIGC技术强大的创作能力也引发对技术作恶的担忧。当要求类ChatGPT应用制造有害信息时,它确实可以做到,输出主要以文本和图片为主。



  1. 网络诈骗话术教学者

  2. 人机交互意味着无限可能,ChatGPT 可能会遵守某种虚假设定下的邪恶指令。

  3. 黑产团伙可精细化训练AIGC技术生成各类话术,用于网络诈骗、评论刷量等,不仅能够主动发帖,还会对其他用户的帖子做出响应,并展开长期的运营。

  4. 错误信息超级传播者

  5. ChatGPT的不可靠之处还在于,可能会加剧虚假消息和错误消息的传播。ChatGPT不具有识别甄别能力,给出的答案很可能拼凑而来,看似合理,却暗藏事实性错误。如果用户没有这方面的鉴别能力,就可能产生比较大的风险,特别是在一些政治文化、医疗健康、交通安全方面的话题,错误的回答可能引起严重后果。

  6. 色情暴力素材生成机

  7. 用户利用AIGC生成虚假名人照片等违禁图片,甚至会制作出暴力和性有关的画作,LAION-5B数据库包含色情、种族、恶意等内容,目前海外已经出现基于Stable Diffusion模型的色情图片生成网站。




  1. AIGC对个人肖像权等权利的侵犯


finance.sina.com.cn



  1. AIGC对原创作品的版权侵犯


网易LOFTER风波警醒了谁?_风闻


守——如何应对AIGC的“暗黑”一面?


在可预见的未来,AIGC将会以井喷的态势席卷各个行业,在享受AI赋能我们生产力的同时,也应该警惕AIGC带来的危害。


《Nature》杂志在《ChatGPT: five priorities for research》一文中提到,对AIGC的进一步研究需要关注的五个问题:



  1. 务必要对模型进行持续校对;

  2. 制定问责制与规则;

  3. 投资于真正开放的大语言模型;

  4. 扩大辩论范围;

  5. 拥抱人工智能的好处。


ChatGPT会设置多层道德底线,兼顾科学和伦理,约束“经济人”行为,以保持“人尽其才、物尽其用”特性。未来,亦有可能出现“以子之矛,攻子之盾”的场景,用AI去识别/对抗AI。



也许未来会出现很多这样的文章👇
《一种基于xxx的AI文本/图像检测技术》



最后


对行业的思考


Meta AI 负责人、图灵奖得主杨立昆Yann LeCun近日表示:就底层技术而言,ChatGPT 并没有特别的创新。与其说 ChatGPT 是一个科学突破,不如说它是一个像样的工程实例



AI人工智能的底层三大件,数据,算力,算法的发展给ChatGPT的出现提供了爆发的基础,Open AI 将它组合的很好,不但是算法而且还包括了算力,数据。


数据方面,互联网的几十年高速发展,积累了海量人类的文本。


算力方面,计算机技术的发展,从芯片的制程到类似Chiplet,等助力AI芯片蓬勃发展。


算法,从神经元算法起步,到Transformer 等各类算法的炉火纯青的应用。


所以AI底层三大件的发展,一定会催生出更多类似于ChatGPT的通用人工智能应用,但我们更应该关注底层三大件的发展,未来数据类似于宝藏和矿产;芯片算力,成了决胜AI的大器,没有芯片那么数据矿产无法挖掘;算法,犹如矿藏提纯配方。


对于行业而言,未来做好AI三大件的工作,才能在AI时代赢得红利。


对个人的启发


说到最后,很多人变得焦虑,担心自己会被取代,那么从个人的角度出发,我们对于AIGC的态度究竟应该是什么样的呢?


马克思说过,人和其他动物的最大区别是“使用工具制造工具”,而GPT是进入信息时代以来,人类最伟大的工具之一。使用 GPT 能够直接调用人类千年以来积累的知识与技能,对于我们普通人而言,自己身上没有神迹,也非天才,与其尝试与机器、GPT和AI竞争,不如站在AI这个巨人的肩膀上,利用这些工具让自己变得更强。


未来,能否使用AI将会成为衡量一个人能力的标准之一。就像当年智能设备普及时,那些不会使用智能手机的人注定会被淘汰一样。


作者:插猹的闰土
来源:juejin.cn/post/7212924329428615226
收起阅读 »

面试官问我按钮级别权限怎么控制,我说v-if,面试官说再见

web
最近的面试中有一个面试官问我按钮级别的权限怎么控制,我说直接v-if啊,他说不够好,我说我们项目中按钮级别的权限控制情况不多,所以v-if就够了,他说不够通用,最后他对我的评价是做过很多东西,但是都不够深入,好吧,那今天我们就来深入深入。 因为我自己没有相关实...
继续阅读 »

最近的面试中有一个面试官问我按钮级别的权限怎么控制,我说直接v-if啊,他说不够好,我说我们项目中按钮级别的权限控制情况不多,所以v-if就够了,他说不够通用,最后他对我的评价是做过很多东西,但是都不够深入,好吧,那今天我们就来深入深入。


因为我自己没有相关实践,所以接下来就从这个有16.2k星星的后台管理系统项目Vue vben admin中看看它是如何做的。


获取权限码


要做权限控制,肯定需要一个code,无论是权限码还是角色码都可以,一般后端会一次性返回,然后全局存储起来就可以了,Vue vben admin是在登录成功以后获取并保存到全局的store中:


import { defineStore } from 'pinia';
export const usePermissionStore = defineStore({
state: () => ({
// 权限代码列表
permCodeList: [],
}),
getters: {
// 获取
getPermCodeList(){
return this.permCodeList;
},
},
actions: {
// 存储
setPermCodeList(codeList) {
this.permCodeList = codeList;
},

// 请求权限码
async changePermissionCode() {
const codeList = await getPermCode();
this.setPermCodeList(codeList);
}
}
})

接下来它提供了三种按钮级别的权限控制方式,一一来看。


函数方式


使用示例如下:


<template>
<a-button v-if="hasPermission(['20000', '2000010'])" color="error" class="mx-4">
拥有[20000,2000010]code可见
</a-button>
</template>

<script lang="ts">
import { usePermission } from '/@/hooks/web/usePermission';

export default defineComponent({
setup() {
const { hasPermission } = usePermission();
return { hasPermission };
},
});
</script>

本质上就是通过v-if,只不过是通过一个统一的权限判断方法hasPermission


export function usePermission() {
function hasPermission(value, def = true) {
// 默认视为有权限
if (!value) {
return def;
}

const allCodeList = permissionStore.getPermCodeList;
if (!isArray(value)) {
return allCodeList.includes(value);
}
// intersection是lodash提供的一个方法,用于返回一个所有给定数组都存在的元素组成的数组
return (intersection(value, allCodeList)).length > 0;

return true;
}
}

很简单,从全局store中获取当前用户的权限码列表,然后判断其中是否存在当前按钮需要的权限码,如果有多个权限码,只要满足其中一个就可以。


组件方式


除了通过函数方式使用,也可以使用组件方式,Vue vben admin提供了一个Authority组件,使用示例如下:


<template>
<div>
<Authority :value="RoleEnum.ADMIN">
<a-button type="primary" block> 只有admin角色可见 </a-button>
</Authority>
</div>
</template>
<script>
import { Authority } from '/@/components/Authority';
import { defineComponent } from 'vue';
export default defineComponent({
components: { Authority },
});
</script>

使用Authority包裹需要权限控制的按钮即可,该按钮需要的权限码通过value属性传入,接下来看看Authority组件的实现。


<script lang="ts">
import { defineComponent } from 'vue';
import { usePermission } from '/@/hooks/web/usePermission';
import { getSlot } from '/@/utils/helper/tsxHelper';

export default defineComponent({
name: 'Authority',
props: {
value: {
type: [Number, Array, String],
default: '',
},
},
setup(props, { slots }) {
const { hasPermission } = usePermission();

function renderAuth() {
const { value } = props;
if (!value) {
return getSlot(slots);
}
return hasPermission(value) ? getSlot(slots) : null;
}

return () => {
return renderAuth();
};
},
});
</script>

同样还是使用hasPermission方法,如果当前用户存在按钮需要的权限码时就原封不动渲染Authority包裹的内容,否则就啥也不渲染。


指令方式


最后一种就是指令方式,使用示例如下:


<a-button v-auth="'1000'" type="primary" class="mx-4"> 拥有code ['1000']权限可见 </a-button>

实现如下:


import { usePermission } from '/@/hooks/web/usePermission';

function isAuth(el, binding) {
const { hasPermission } = usePermission();

const value = binding.value;
if (!value) return;
if (!hasPermission(value)) {
el.parentNode?.removeChild(el);
}
}

const mounted = (el, binding) => {
isAuth(el, binding);
};

const authDirective = {
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted,
};

// 注册全局指令
export function setupPermissionDirective(app) {
app.directive('auth', authDirective);
}

只定义了一个mounted钩子,也就是在绑定元素挂载后调用,依旧是使用hasPermission方法,判断当前用户是否存在通过指令插入的按钮需要的权限码,如果不存在,直接移除绑定的元素。


很明显,Vue vben admin的实现有两个问题,一是不能动态更改按钮的权限,二是动态更改当前用户的权限也不会生效。


解决第一个问题很简单,因为上述只有删除元素的逻辑,没有加回来的逻辑,那么增加一个updated钩子:


app.directive("auth", {
mounted: (el, binding) => {
const value = binding.value
if (!value) return
if (!hasPermission(value)) {
// 挂载的时候没有权限把元素删除
removeEl(el)
}
},
updated(el, binding) {
// 按钮权限码没有变化,不做处理
if (binding.value === binding.oldValue) return
// 判断用户本次和上次权限状态是否一样,一样也不用做处理
let oldHasPermission = hasPermission(binding.oldValue)
let newHasPermission = hasPermission(binding.value)
if (oldHasPermission === newHasPermission) return
// 如果变成有权限,那么把元素添加回来
if (newHasPermission) {
addEl(el)
} else {
// 如果变成没有权限,则把元素删除
removeEl(el)
}
},
})

const hasPermission = (value) => {
return [1, 2, 3].includes(value)
}

const removeEl = (el) => {
// 在绑定元素上存储父级元素
el._parentNode = el.parentNode
// 在绑定元素上存储一个注释节点
el._placeholderNode = document.createComment("auth")
// 使用注释节点来占位
el.parentNode?.replaceChild(el._placeholderNode, el)
}

const addEl = (el) => {
// 替换掉给自己占位的注释节点
el._parentNode?.replaceChild(el, el._placeholderNode)
}

主要就是要把父节点保存起来,不然想再添加回去的时候获取不到原来的父节点,另外删除的时候创建一个注释节点给自己占位,这样下次想要回去能知道自己原来在哪。


第二个问题的原因是修改了用户权限数据,但是不会触发按钮的重新渲染,那么我们就需要想办法能让它触发,这个可以使用watchEffect方法,我们可以在updated钩子里通过这个方法将用户权限数据和按钮的更新方法关联起来,这样当用户权限数据改变了,可以自动触发按钮的重新渲染:


import { createApp, reactive, watchEffect } from "vue"
const codeList = reactive([1, 2, 3])

const hasPermission = (value) => {
return codeList.includes(value)
}

app.directive("auth", {
updated(el, binding) {
let update = () => {
let valueNotChange = binding.value === binding.oldValue
let oldHasPermission = hasPermission(binding.oldValue)
let newHasPermission = hasPermission(binding.value)
let permissionNotChange = oldHasPermission === newHasPermission
if (valueNotChange && permissionNotChange) return
if (newHasPermission) {
addEl(el)
} else {
removeEl(el)
}
};
if (el._watchEffect) {
update()
} else {
el._watchEffect = watchEffect(() => {
update()
})
}
},
})

updated钩子里更新的逻辑提取成一个update方法,然后第一次更新在watchEffect中执行,这样用户权限的响应式数据就可以和update方法关联起来,后续用户权限数据改变了,可以自动触发update方法的重新运行。


好了,深入完了,看着似乎也挺简单的,我不确定这些是不是面试官想要的,或者还有其他更高级更优雅的实现呢,知道的朋友能否指点

作者:街角小林
来源:juejin.cn/post/7209648356530896953
一二,在下感激不尽。

收起阅读 »

canvas绘制行星环绕

web
前言 最近学校学了一些JavaScript课程,其中涉及到了部分有关于canvas的知识点,万万没想到老师只是用了一节课提了一下有关canvas的一些有关使用就布置下来了一个作业--采用canvas绘制一个简易太阳系,咱作为学生还能说啥,只能冲啦。 实现原理 ...
继续阅读 »
太阳与月亮.gif

前言


最近学校学了一些JavaScript课程,其中涉及到了部分有关于canvas的知识点,万万没想到老师只是用了一节课提了一下有关canvas的一些有关使用就布置下来了一个作业--采用canvas绘制一个简易太阳系,咱作为学生还能说啥,只能冲啦。


实现原理


只是单纯的canvas方法的使用再加上一点点js的使用就可以实现这个简单的实例啦。


实现代码


html部分


<!-- 画布元素 -->
<canvas id="canvas"></canvas>

初始化画布

js获取画布元素,初始化画布背景色为黑色,设置画布真实绘制宽高为1200,浏览器呈现宽高为600px,getContext('2d')获取画布的2D上下文。


let canvas = document.getElementById('canvas')
canvas.style.background = 'black'
// 浏览器渲染出画布宽高
canvas.style.width = 600 + 'px'
canvas.style.height = 600 + 'px'
// 绘制画布真实宽高
canvas.width = 1200
canvas.height = 1200
let context = canvas.getContext('2d');

绘制太阳

绘制一个圆心为(600,600)半径为100的圆,在绘制前有几点要了解,因为canvas只支持两种形式的图形绘制:矩形和路径(由一系列点连成的线段),所以我们要使用到路径绘制函数。其中beginPath()新建一条路径,在该路径闭合前,图像绘制将在该路径中进行,其中fillSyle设置的是图像填充色,通常以closePath()闭合该路径,但由于fill()会自动闭合路径所以closePath()可以省去。详情可以参考MDN|Canvas


context.beginPath() // 开始路径绘制
context.arc(600, 600, 100, 0, Math.PI*2, true)
context.fillStyle = 'red' // 图形填充色
context.fill() // 进行填充

绘制地球轨道

与上面太阳的绘制相差不大,将填充换为了描边。strokeStyle定义图形轮廓颜色,stroke()开始绘制轮廓,最后采用closePath()闭合路径。


context.beginPath()
context.arc(600, 600, 300, 0, Math.PI*2, true) // 圆心(300,300) 半径为150的圆环
context.strokeStyle = 'rgb(255,255,255,0.3)'
context.stroke()
context.closePath()

绘制地球

注意: 这里地球的圆心坐标为(0,0)这是因为我们调用了translate()这一函数,通过这一函数我们将起始点偏移到指定位置,下文将以此坐标为新的起始点。此外需要用save()保存当前画布状态,不然后续循环会出问题。再调用rotate()方法实现旋转,其中rotate()是使得其下文绘制的图形实现旋转,旋转中心为当前起始点坐标。


context.save(); // 保存当前状态

var angle=time*Math.PI/180/8;
context.translate(600,600); // 起始点偏移量,太阳中心
context.rotate(angle);

context.translate(300,0); // 地球,月球轨道中心
context.beginPath()
context.arc(0,0,40,0,2*Math.PI,false);
context.fillStyle = 'blue'
context.strokeStyle = 'blue'
context.fill()

月球轨道及月球


// 月球轨道
context.beginPath()
context.arc(0, 0, 100, 0, Math.PI*2, true)
context.strokeStyle = 'rgb(255,255,255,0.3)'
context.stroke()
context.closePath()

context.rotate(-8*angle);

// 月球
context.beginPath()
context.arc(100,0,20,0,2*Math.PI,false);
context.fillStyle = '#fff'
context.fill()

js完整部分

定义一个绘制函数draw(),通过setInterval()函数循环调用,其中要注意在使用save()函数后要调用restore()函数恢复状态,为下次的绘制做准备。


let canvas = document.getElementById('canvas')
canvas.style.background = 'black'
// 浏览器渲染出画布宽高
canvas.style.width = 600 + 'px'
canvas.style.height = 600 + 'px'
// 绘制画布真实宽高
canvas.width = 1200
canvas.height = 1200
let context = canvas.getContext('2d');
// context.scale(2, 2)

let time = 0
function draw() {
context.clearRect(0,0,canvas.width,canvas.height); // 清除所选区域
// 绘制太阳
context.beginPath() // 开始路径绘制
context.arc(600, 600, 100, 0, Math.PI*2, true)
context.fillStyle = 'red' // 图形填充色
context.fill() // 进行填充
// 绘制地球轨道
context.beginPath()
context.arc(600, 600, 300, 0, Math.PI*2, true) // 圆心(300,300) 半径为150的圆环
context.strokeStyle = 'rgb(255,255,255,0.3)'
context.stroke()
context.closePath()

context.save(); // 保存当前状态

var angle=time*Math.PI/180/8;
context.translate(600,600); // 起始点偏移量,太阳中心
context.rotate(angle);

context.translate(300,0); // 地球,月球轨道中心
// 地球
context.beginPath()
context.arc(0,0,40,0,2*Math.PI,false);
context.fillStyle = 'blue'
context.strokeStyle = 'blue'
context.fill()

// 月球轨道
context.beginPath()
context.arc(0, 0, 100, 0, Math.PI*2, true)
context.strokeStyle = 'rgb(255,255,255,0.3)'
context.stroke()
context.closePath()

context.rotate(-8*angle);

// 月球
context.beginPath()
context.arc(100,0,20,0,2*Math.PI,false);
context.fillStyle = '#fff'
context.fill()

context.restore(); // 恢复状态
time++
}
setInterval(draw,30)


结语


以上过程便能简单的绘制一个简易太阳系图形动画了,通过文档就能快速的绘制一个简单的图形,但是要绘制复杂的图形的话还是要花时间去研究一下文档。


作者:codePanda
来源:juejin.cn/post/7212442380263112760
收起阅读 »

AI能代替前端开发吗?以一键截图分享为例,看看AI到底有多强

最近在使用 ichati 的过程中发现一个问题,我不能很容易的给其他人分享聊天内容。 比如我想给我朋友分享一下 AI 写作的效果,于是开始截屏发微信群。 结果发现我必须截三次屏幕,发三条消息。 没办法一键截屏,这很难受。 于是我问了我们用户群里的一些用户...
继续阅读 »

最近在使用 ichati 的过程中发现一个问题,我不能很容易的给其他人分享聊天内容。


比如我想给我朋友分享一下 AI 写作的效果,于是开始截屏发微信群。


Pasted image 20230319192419.png


Pasted image 20230319192513.png


Pasted image 20230319192528.png


结果发现我必须截三次屏幕,发三条消息。


没办法一键截屏,这很难受。


于是我问了我们用户群里的一些用户,确实有很多用户向我反映是这个问题。但在我主动问之前,他们没有主动提出过这个问题。


所以对大多数的用户来说,他们的思维和习惯就是,适应产品,而不是改变产品。


那怎么办呢?


好的产品一定是能发现用户痛点并解决用户痛点的。


所以我得解决掉这个问题。


其实 ichati 发展到现在,很多功能都是在帮助用户解决使用问题的过程中增加的。


言归正传。


我最初的想法是,增加一个功能,可以一键截屏并分享给好友。


但我不想自己写代码,因为太浪费时间了。


由于我在做 AI 的产品,这种杂活当然不能自己干了。


所以我就打起了盘古的主意。


然后开始让它帮我实现这个功能。


ichati.cn.png


对,不到 2 分钟的时间,80 % 的代码工作已经结束了。


上面的截图就是通过一键分享的功能生成的,效果还不错。同时我还在底部追加了产品的网址和介绍。


剩下的工作就是自己微调一下了。微调的过程中,80% 的代码又都是通过 copilot 来实现的。我一共也没写几行代码。不得不感慨 AI 的能力实在是太过于强大。


这样无论是多么长的聊天内容,都可以在下方点击截图分享按钮一键分享全部内容了。


Pasted image 20230319195109.png


这就相当于我是一个产品经理,我给产品提了个需求,产品告诉我怎么做。这种感觉实在是太奇妙了。


通过这个事儿,我再来聊聊最近很热的话题。「AI 到底能否取代程序员?」


我的观点是能取代一部分,但绝对无法取代全部。


比如用户需要一个一键截图的功能,目前的 AI 基本上是发现不了的,除非用户主动告诉 AI。这是用户痛点,必须由人来挖掘,而负责这事的人一般叫做产品经理。


接下来是实现这个功能,产品经理不清楚怎么实现,因为产品经理不懂技术细节。像 DOM 啊,Canvas 啊,生成图像格式啊等等,都不知道,即使知道也是一知半解。


这就要向程序员去传达,再由程序员去理解和实现。


理解的过程 AI 是可以做的,但需要精准表达才能得到满意的答案。如果是一个合格的产品经理,做这事儿不难。


实现的过程,更是 AI 极其擅长的。在这一点上,我不认为人可以比得过 AI。


所以从开发应用的角度去看,我认为程序员已经没必要再去继续学习各种库,各种框架的使用方式和 API 了。在这一点上,人是不可能比得过 AI 的。甚至于说,再继续做这种事,毫无价值可言。


实现之后,进入微调阶段。


程序员的能力又体现出来了。因为人能否非常快速的发现问题,并修正问题。


AI 也具备这种能力,尽管它修正问题很迅速。但它发现问题的效率远比人低的多。我指的不是代码的问题,而是产品的问题。


有些产品 AI 并不知道人的预期是什么,因为产品是最终用户是人。人自然比 AI 更清楚产品正确的样貌。


但随着 AI 技术的发展,我不能保证未来的 AI 是不是会比人更懂得人需要什么。但我觉得那一天终将到来。


从以上几步来分析,AI 的意义在于,它会让一个懂得使用 AI,并且懂产品的人成指数增强。毫不夸张地说,一个擅长使用 AI,同时又懂技术的产品,战斗力能超过 1 个产品经理加上 5 个普通程序员。


同时,当前的 AI 定位仍然是助理。它不会喧宾夺主,而且它也做不到。


所以不必过度焦虑。


但从如今的趋势来看,个人技术的优势会慢慢变低。因为一个懂得使用 AI 的中级程序员,配合 AI,技术水平直接能提升数个级别。


这个感觉,就像龙珠里面悟空和贝吉塔合体,直接暴打魔人布欧一样。


所以我认为,如今我们笃定的技术这条路。在未来,它有可能会变成一个死胡同。


这不是焦虑,这是实事求是地讲道理。


搁在六七年前,让我来实现一个一键截图功能,可能会自己手写递归,折腾一下午。因为我认为这样会帮助我技术上的成长。


确实,那样做我会懂得更多。懂得更多就可以更快地解决问题,当出现问题更快地定位问题。


也正因为如此,我也可以有自信比别人要更高的薪水。


但是现在我不这么认为了,因为在效率和知识的深度、广度上,AI 都可以轻松的打败我。


如果在人人都在深度使用 AI 的时代,六七年前我做的那些事会显得相当没有意义。与其花费时间读源码,分析原理,不如去做点别的事儿。


所以我认为如今的技术人员,应该多尝试走几条不同的路。继续死磕技术,是绝对磕不过 AI 的。


本文的结论就是:AI 绝对有改变行业布局的能力,这点是毋庸置疑的。但 AI 不会取代你,相反,它会成就你。所以没必要总是把 AI 放到对立面来制造焦虑,在未来,AI 将会是我们最的佳伙伴和助手。


作者:代码与野兽
来源:juejin.cn/post/7212432799848284221
收起阅读 »

下一代代码助手 GitHub Copilot X 发布

GitHub 今日发布了 GitHub Copilot X,这是一款基于OpenAI的GPT-4模型开发的AI助手。GitHub Copilot X 致力于改进开发者体验,将提供聊天和语音界面,支持拉取请求,回答文档问题,并通过 GPT-4 实现更个性化的开发...
继续阅读 »

GitHub 今日发布了 GitHub Copilot X,这是一款基于OpenAI的GPT-4模型开发的AI助手。GitHub Copilot X 致力于改进开发者体验,将提供聊天和语音界面,支持拉取请求,回答文档问题,并通过 GPT-4 实现更个性化的开发者体验。GitHub Copilot 作为AI编程搭档,已经为开发者提供了自动补全代码和注释功能。GitHub Copilot X将继续扩展这一功能,包括:



  1. GitHub Copilot聊天功能:在编辑器中集成与VS Code和Visual Studio的聊天界面,帮助开发者深入分析代码、生成单元测试和修复bug。

  2. GitHub Copilot语音功能:允许开发者通过口头指令进行自然语言提示。

  3. 为拉取请求提供AI生成的描述:基于GPT-4模型生成拉取请求描述和标签。

  4. 自动生成测试建议:GitHub Copilot将警告开发者测试覆盖率不足,并建议潜在的测试方案。

  5. GitHub Copilot文档功能:提供基于聊天界面的文档查询,为开发者提供实时答案。

  6. GitHub Copilot CLI:为命令行界面提供智能建议。


GitHub Copilot X的目标是为每个团队、项目和代码库提供个性化的开发体验,让开发者更高效地开发软

作者:江昪
来源:juejin.cn/post/7213335620126982202
件,提高工作满意度。

收起阅读 »

面试官问我:SharedPreference源码中apply跟commit的原理,导致ANR的原因

1.前言好几年前写过一篇SharedPreference源码相关的文章,对apply跟commit方法讲解的不够透彻,作为颜值担当的天才少年来说,怎么能不一次深入到底呢?2.正文为了熟读源码,下班后我约了同事小雪一起探讨,毕竟三人行必有我师焉。哪里来的三个人,...
继续阅读 »

1.前言

好几年前写过一篇SharedPreference源码相关的文章,对apply跟commit方法讲解的不够透彻,作为颜值担当的天才少年来说,怎么能不一次深入到底呢?

2.正文

为了熟读源码,下班后我约了同事小雪一起探讨,毕竟三人行必有我师焉。哪里来的三个人,不管了,跟小雪研究学术更重要。

在这里插入图片描述

小安学长,看了你之前的文章:Android SharedPreference 源码分析(一)对apply(),commit()的底层原理还是不理解,尤其是线程和一些同步锁他里面怎么使用,什么情况下会出现anr?

既然说到apply(),commit()的底层原理,那肯定是老步骤了,上源码。 apply源码如下:

public void apply() {
final long startTime = System.currentTimeMillis();

final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}

if (DEBUG && mcr.wasWritten) {
Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+ " applied after " + (System.currentTimeMillis() - startTime)
+ " ms");
}
}
};
// 将 awaitCommit 添加到队列 QueuedWork 中
QueuedWork.addFinisher(awaitCommit);

Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);// 将 awaitCommit 从队列 QueuedWork 中移除
}
};

SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

// Okay to notify the listeners before it's hit disk
// because the listeners should always get the same
// SharedPreferences instance back, which has the
// changes reflected in memory.
notifyListeners(mcr);
}

你这丢了一大堆代码,我也看不懂啊。

别急啊,这漫漫长夜留给我们的事情很多啊,听我一点点给你讲,包你满意。 请添加图片描述

apply()方法做过安卓的都知道(如果你没有做过安卓,那你点开我博客干什么呢,死走不送),频繁写文件建议用apply方法,因为他是异步存储到本地磁盘的。那么具体源码是如何操作的,让我们掀开他的底裤,不是,让我们透过表面看本质。

我们从下往上看,apply方法最后调用了SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);我长得帅我先告诉你,enqueueDiskWrite方法会把存储文件的动作放到子线程,具体怎么放的,我们等下看源码,这边你只要知道他的作用。这个方法的第二个参数 postWriteRunnable做了两件事:
1)让awaitCommit执行,及执行 mcr.writtenToDiskLatch.await();
2)执行QueuedWork.remove(awaitCommit);代码

writtenToDiskLatch是什么,QueuedWork又是什么?

writtenToDiskLatch是CountDownLatch的实例化对象,CountDownLatch是一个同步工具类,它通过一个计数器来实现的,初始值为线程的数量。每当一个线程完成了自己的任务调用countDown(),则计数器的值就相应得减1。当计数器到达0时,表示所有的线程都已执行完毕,然后在等待的线程await()就可以恢复执行任务。
1)countDown(): 对计数器进行递减1操作,当计数器递减至0时,当前线程会去唤醒阻塞队列里的所有线程。
2)await(): 阻塞当前线程,将当前线程加入阻塞队列。 可以看到如果postWriteRunnable方法被触发执行的话,由于 mcr.writtenToDiskLatch.await()的缘故,UI线程会被一直阻塞住,等待计数器减至0才能被唤醒。

QueuedWork其实就是一个基于handlerThread的,处理任务队列的类。handlerThread类为你创建好了Looper和Thread对象,创建Handler的时候使用该looper对象,则handleMessage方法在子线程中,可以做耗时操作。如果对于handlerThread的不熟悉的话,可以看我前面的文章:Android HandlerThread使用介绍以及源码解析

在这里插入图片描述 觉得厉害,那咱就继续深入。
enqueueDiskWrite源码如下所示:

private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final boolean isFromSyncCommit = (postWriteRunnable == null);

final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};

// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}

QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

很明显postWriteRunnable不为null,程序会执行QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);从writeToDiskRunnable我们可以看到,他里面做了两件事:
1)writeToFile():内容存储到文件;
2)postWriteRunnable.run():postWriteRunnable做了什么,往上看,上面已经讲了该方法做的两件事。

QueuedWork.queue源码:

    public static void queue(Runnable work, boolean shouldDelay) {
Handler handler = getHandler();

synchronized (sLock) {
sWork.add(work);

if (shouldDelay && sCanDelay) {
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
} else {
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}
private static class QueuedWorkHandler extends Handler {
static final int MSG_RUN = 1;

QueuedWorkHandler(Looper looper) {
super(looper);
}

public void handleMessage(Message msg) {
if (msg.what == MSG_RUN) {
processPendingWork();
}
}
}

这边我默认你已经知道HandlerThread如何使用啦,如果不知道,麻烦花五分钟去看下我之前的博客。
上面的代码很简单,其实就是把writeToDiskRunnable这个任务放到sWork这个list中,并且执行handler,根据HandlerThread的知识点,我们知道handlermessage里面就是子线程了。

接下来我们继续看handleMessage里面的processPendingWork()方法:

 private static void processPendingWork() {
long startTime = 0;

if (DEBUG) {
startTime = System.currentTimeMillis();
}

synchronized (sProcessingWork) {
LinkedList<Runnable> work;

synchronized (sLock) {
work = (LinkedList<Runnable>) sWork.clone();
sWork.clear();

// Remove all msg-s as all work will be processed now
getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
}

if (work.size() > 0) {
for (Runnable w : work) {
w.run();
}

if (DEBUG) {
Log.d(LOG_TAG, "processing " + work.size() + " items took " +
+(System.currentTimeMillis() - startTime) + " ms");
}
}
}
}

这代码同样很简单,先是把sWork克隆给work,然后开启循环,执行work对象的run方法,及调用writeToDiskRunnable的run方法。上面讲过了,他里面做了两件事:1)内容存储到文件 2)postWriteRunnable方法回调。 执行run方法的代码:

 final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);//由于handlermessage在子线程,则writeToFile也在子线程中
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};

writeToFile方法我们不深入去看,但是要关注,里面有个setDiskWriteResult方法,在该方法里面做了如下的事情:

void setDiskWriteResult(boolean wasWritten, boolean result) {
this.wasWritten = wasWritten;
writeToDiskResult = result;
writtenToDiskLatch.countDown();//计数器-1
}

如何上面认真看了的同学,应该可以知道,当调用countDown()方法时,会对计数器进行递减1操作,当计数器递减至0时,当前线程会去唤醒阻塞队列里的所有线程。也就是说,当文件写完时,UI线程会被唤醒。

既然文件写完就会释放锁,那什么情况下会出现ANR呢?

Android系统为了保障在页面切换,也就是在多进程中sp文件能够存储成功,在ActivityThread的handlePauseActivity和handleStopActivity时会通过waitToFinish保证这些异步任务都已经被执行完成。如果这个时候过渡使用apply方法,则可能导致onpause,onStop执行时间较长,从而导致ANR。

private void handlePauseActivity(IBinder token, boolean finished,
boolean userLeaving, int configChanges, boolean dontReport, int seq) {
......
r.activity.mConfigChangeFlags |= configChanges;
performPauseActivity(token, finished, r.isPreHoneycomb(), "handlePauseActivity");

// Make sure any pending writes are now committed.
if (r.isPreHoneycomb()) {
QueuedWork.waitToFinish();
}

......
}

你肯定要问,为什么过渡使用apply方法,就有可能导致ANR?那我们只能看QueuedWork.waitToFinish();到底做了什么

 public static void waitToFinish() {
long startTime = System.currentTimeMillis();
boolean hadMessages = false;

Handler handler = getHandler();

synchronized (sLock) {
if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
// Delayed work will be processed at processPendingWork() below
handler.removeMessages(QueuedWorkHandler.MSG_RUN);

if (DEBUG) {
hadMessages = true;
Log.d(LOG_TAG, "waiting");
}
}

// We should not delay any work as this might delay the finishers
sCanDelay = false;
}

StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
try {
processPendingWork();
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}

try {
while (true) {
Runnable finisher;

synchronized (sLock) {
finisher = sFinishers.poll();
}

if (finisher == null) {
break;
}

finisher.run();
}
} finally {
sCanDelay = true;
}

synchronized (sLock) {
long waitTime = System.currentTimeMillis() - startTime;

if (waitTime > 0 || hadMessages) {
mWaitTimes.add(Long.valueOf(waitTime).intValue());
mNumWaits++;

if (DEBUG || mNumWaits % 1024 == 0 || waitTime > MAX_WAIT_TIME_MILLIS) {
mWaitTimes.log(LOG_TAG, "waited: ");
}
}
}
}

看着一大坨代码,其实做了两件事:
1)主线程执行processPendingWork()方法,把之前未执行完的内容存储到文件的操作执行完,这部分动作直接在主线程执行,如果有未执行的文件操作并且文件较大,则主线程会因为IO时间长造成ANR。
2)循环取出sFinishers数组,执行他的run方法。如果这时候有多个异步线程或者异步线程时间过长,同样会造成阻塞产生ANR。

第一个很好理解,第二个没有太看明白,sFinishers数组是在什么时候add数据的,而且根据writeToDiskRunnable方法可以知道,先写文件再加锁的,为啥会阻塞呢?

在这里插入图片描述

sFinishers的addFinisher方法是在apply()方法里面调用的,代码如下:

        @Override
public void apply() {
......
// 将 awaitCommit 添加到队列 QueuedWork 中
QueuedWork.addFinisher(awaitCommit);

Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);// 将 awaitCommit 从队列 QueuedWork 中移除
}
};
......
}

正常情况下其实是不会发生ANR的,因为writeToDiskRunnable方法中,是先进行文件存储再去阻塞等待的,此时CountDownLatch永远都为0,则不会阻塞主线程。

final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);//写文件,写成功后会调用writtenToDiskLatch.countDown();计数器-1
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();//回调到awaitCommit.run();进行阻塞
}
}
};

但是如果processPendingWork方法在异步线程在执行时,及通过enqueueDiskWrite方法触发的正常文件保存流程,这时候文件比较大或者文件比较多,子线程则一直在运行中;当用户点击页面跳转时,则触发该Activity的handlePauseActivity方法,根据上面的分析,handlePauseActivity方法里面会执行waitToFinish保证这些异步任务都已经被执行完成。
由于这边主要介绍循环取出sFinishers数组,执行他的run方法造成阻塞产生ANR,我们就重点看下sFinishers数组对象是什么,并且执行什么动作。

private static final LinkedList<Runnable> sFinishers = new LinkedList<>();
@UnsupportedAppUsage
public static void addFinisher(Runnable finisher) {
synchronized (sLock) {
sFinishers.add(finisher);
}
}

addFinisher刚刚上面提到是在apply方法中调用,则finisher就是入参awaitCommit,他的run方法如下:

final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
mcr.writtenToDiskLatch.await();//阻塞
} catch (InterruptedException ignored) {
}

if (DEBUG && mcr.wasWritten) {
Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+ " applied after " + (System.currentTimeMillis() - startTime)
+ " ms");
}
}
};

不难看出,就是调用CountDownLatch对象的await方法,阻塞当前线程,将当前线程加入阻塞队列。也就是这个时候整个UI线程都阻塞在这边,等待processPendingWork这个异步线程执行完毕,虽然你是在子线程,但是我主线程在等你执行结束才会进行页面切换,所以如果过渡使用apply方法,则可能导致onpause,onStop执行时间较长,从而导致ANR。

小安学长不愧是我的偶像,我都明白了,那继续讲讲同步存储commit()方法吧。

commit方法其实就比较简单了,无非是内存和文件都在UI线程中,我们看下代码证实一下:

 @Override
public boolean commit() {
long startTime = 0;

if (DEBUG) {
startTime = System.currentTimeMillis();
}

MemoryCommitResult mcr = commitToMemory();//内存保存

SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);//第二个参数为null
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
} finally {
if (DEBUG) {
Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+ " committed after " + (System.currentTimeMillis() - startTime)
+ " ms");
}
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}

可以看到enqueueDiskWrite的第二个参数为null,enqueueDiskWrite方法其实上面讲解apply的时候已经贴过了,为了不让你往上翻我们继续看enqueueDiskWrite方法:

   private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final boolean isFromSyncCommit = (postWriteRunnable == null);//此时postWriteRunnable为null,isFromSyncCommit 则为true

final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};

// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
if (isFromSyncCommit) { //当调用commit方法时,isFromSyncCommit则为true
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();//主线程回调writeToDiskRunnable的run方法,进行writeToFile文件的存储
return;
}
}

QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

关键代码已经注释过了,由于postWriteRunnable为null,则isFromSyncCommit为true,代码会在主线程回调writeToDiskRunnable的run方法,进行writeToFile文件的存储。这部分动作直接在主线程执行,如果文件较大,则主线程也会因为IO时间长造成ANR的。

所以SharedPreference 不管是commit()还是apply()方法,如果文件过大或者过多,都会有ANR的风险,那如何规避呢?

解决肯定有办法的,下一篇就介绍SharedPreference 的替代方案mmkv的原理,只是今晚有点晚了,咱们早上睡吧,不是,早点回家吧~~~


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

不一样的Android堆栈抓取方案

背景 曾几何时,我们只需要简简单单的一行 Thread.currentThread().getStackTrace() 代码就可以轻轻松松的获取到当前线程的堆栈信息,从而分析各种问题。随着需求的不断迭代,APP 遇到的问题越来越多,卡顿,ANR,异常等等问题接...
继续阅读 »

背景


曾几何时,我们只需要简简单单的一行 Thread.currentThread().getStackTrace() 代码就可以轻轻松松的获取到当前线程的堆栈信息,从而分析各种问题。随着需求的不断迭代,APP 遇到的问题越来越多,卡顿,ANR,异常等等问题接踵而来,那么简简单单某个时刻的堆栈信息已经不能满足我们的需求了,我们的目光逐渐转移到了每个时刻的堆栈上,如果能获取一个时间段内,每个时刻的堆栈信息,那么卡顿,以及 ANR 的问题也将被解决。


抓栈方案


目前对于一段时间内的抓栈方案有两种:



  • 方法插桩抓栈

  • Native 抓栈


代码插桩抓栈


基本思路


APP 编译阶段,对每个方法进行插桩,在插桩的同时,填入当前方法 ID,发生卡顿或者异常的时候,将之前收集到的方法 ID 进行聚合输出。


插桩流程图:



优点:简单高效,无兼容性问题


缺点:插桩导致所有类都非 preverify,同时 verify 与 optimize 操作会在加载类时被触发。增加类加载的压力照成一定的性能损耗。另外也会导致包体积变大,影响代码 Debug 以及代码崩溃异常后错误行数


Native 抓栈


使用 Native 抓栈之前,我们先了解一下 Java 抓栈的整个流程


JAVA堆栈获取流程图


抓栈当前线程


抓栈当前线程


抓栈其他线程


抓栈其他线程


Java堆栈获取原理分析


由于当前线程抓栈和其他线程抓栈流程类似,这里我们从其他线程抓栈的流程进行分析
首先从入口代码出发,Java 层通过 Thread.currentThread().getStackTrace() 开始获取当前堆栈数据


Thread.java

public StackTraceElement[] getStackTrace() {

StackTraceElement ste[] = VMStack.getThreadStackTrace(this);
return str!=null?ste:EmptyArray.STACK_TRACE_ELEMENT;

}

Thread 中的 getStackTrace 只是一个空壳,底层的实现是通过 native 来获取的,继续往下走,通过 VMStack 来获取我们需要的线程堆栈数据


dalvik_system_vmstack.cc

static jobjectArray VMStack_getThreadStackTrace(JNIEnv* env, jclass, jobject javaThread) {

ScopedFastNativeObjectAccess soa(env);

// fn 方法是线程挂起回调
auto fn = [](Thread* thread, const ScopedFastNativeObjectAccess& soaa)
REQUIRES_SHARED(Locks::mutator_lock_) -> jobject {
return thread->CreateInternalStackTrace(soaa);
};

// 获取堆栈
jobject trace = GetThreadStack(soa, javaThread, fn);
if (trace == nullptr) {
return nullptr;
}

// trace 是一个包含 method 的数组,有这个数据之后,我们进行数据反解,就能获取到方法堆栈明文
return Thread::InternalStackTraceToStackTraceElementArray(soa, trace);

}

上述代码中,需要注意三个元素




  • fn={return thread->CreateInternalStackTrace(soaa);}。 // 这个是线程挂起后的回调函数




  • GetThreadStack(sao,javaThread,fn) // 用来获取实际的线程堆栈信息




  • Thread::InternalStackTraceToStackTraceElementArray(sao,trace),这里 trace 就是我们拿到的目标产物,这里面就包含了当前线程此时此刻的堆栈信息,需要对堆栈进行进一步的解析,才能获取到可识别的堆栈文本




接下来我们从获取堆栈信息函数着手,看看 GetThreadStack 的具体行为。


dalvik_system_vmstack.cc

static ResultT GetThreadStack(const ScopedFastNativeObjectAccess& soa,jobject peer,T fn){

********
********
********

ThreadList* thread_list = Runtime::Current()->GetThreadList();

// 【Step1】: 挂起线程
Thread* thread = thread_list->SuspendThreadByPeer(peer,SuspendReason::kInternal,&timed_out);
if (thread != nullptr) {
{
ScopedObjectAccess soa2(soa.Self());

// 【Step2】: FN 回调,这里面执行的就是抓栈操作,回到外层的回调函数逻辑中
trace = fn(thread, soa);
}

// 【Step3】: 恢复线程
bool resumed = thread_list->Resume(thread, SuspendReason::kInternal);
}
}
return trace;
}

在该操作的三个步骤中,就包含了抓栈的整个流程,




  • 【Step1】: 挂起线程,线程每时每刻都在执行方法,这样就导致当前线程的方法堆栈在不停的增加,如果想要抓到瞬时堆栈,就需要把当前线程暂停,保留瞬时的堆栈信息,这样抓出来的数据才是准确的。




  • 【Step2】: 执行 FN 的回调,这里的 FN 回调,就是上文介绍的回调方法 fn={return thread->CreateInternalStackTrace(soaa)}




  • 【Step3】: 恢复线程的正常运行。




上述流程中,我们需要重点关注一下 FN 回调里面做了什么,以及怎么做到的


thread.cc

jobject Thread::CreateInternalStackTrace(const ScopedObjectAccessAlreadyRunnable& soa) const {

// 创建堆栈回溯观察者
FetchStackTraceVisitor count_visitor(const_cast<Thread*>(this),&saved_frames[0],kMaxSavedFrames);
count_visitor.WalkStack(); // 回溯核心方法

// 创建堆栈回溯观察者 2 号,详细的堆栈数据就是 2 号处理返回的
BuildInternalStackTraceVisitor build_trace_visitor(soa.Self(), const_cast<Thread*>(this), skip_depth);

mirror::ObjectArray<mirror::Object>* trace = build_trace_visitor.GetInternalStackTrace();
return soa.AddLocalReference<jobject>(trace);

}



  • 创建堆回溯观察者 1 号 FetchStackTraceVisitor,最大深度 256 进行回溯,如果深度超过了 256,则使用 2 号继续进行回溯




  • 创建堆回溯观察者 2 号 BuildInternalStackTraceVisitor,承接 1 号的回溯结果,1 号没回溯完,2 号接着回溯。




栈回溯的详细过程


回溯是通过 WalkStack 来实现的。StackVisitor::WalkStack 是一个用于在当前线程堆栈上单步遍历帧的函数。它可以用来收集当前线程堆栈上特定帧的信息,以便进行调试或其他分析操作。 例如,它可以用来找出当前线程堆栈上哪些函数调用了特定函数,或者收集特定函数的参数。 也可以用来找出线程调用的函数层次结构,以及每一层调用的函数参数。 使用这个函数,可以更好地理解代码的执行流程,并帮助进行异常处理和调试。


stack.cc

void StackVisitor::WalkStack(bool include_transitions) {

for (const ManagedStack* current_fragment = thread_->GetManagedStack();current_fragment != nullptr; current_fragment = current_fragment->GetLink()) {

cur_shadow_frame_ = current_fragment->GetTopShadowFrame();

****
****
****

do {
// 通知子类,进行栈帧的获取
bool should_continue = VisitFrame();
cur_depth_++;
cur_shadow_frame_ = cur_shadow_frame_->GetLink();
} while (cur_shadow_frame_ != nullptr);
}

}

ManagedStack 是一个单链表,保存了当前 ShadowFrame 或者 QuickFrame 栈指针,先依次遍历 ManagedStack 链表,然后遍历其内部的 ShadowFrame 或者 QuickFrame 还原一个可读的调用栈,从而还原出当前的 Java 堆栈



还原操作是通过 VisitFrame 来实现的,它是一个抽象接口,实现类我们需要看 BuildInternalStackTraceVisitor 的实现


thread.cc

class BuildInternalStackTraceVisitor : public StackVisitor {

mirror::ObjectArray<mirror::Object>* trace_ = nullptr;
bool VisitFrame() override REQUIRES_SHARED(Locks::mutator_lock_) {

****
****
****

// 每循环一帧,将其添加到 arrObj 中
ArtMethod* m = GetMethod();
AddFrame(m, m->IsProxyMethod() ? dex::kDexNoIndex : GetDexPc());
return true;
}

void AddFrame(ArtMethod* method, uint32_t dex_pc) REQUIRES_SHARED(Locks::mutator_lock_) {
ObjPtr<mirror::Object> keep_alive;
if (UNLIKELY(method->IsCopied())) {
ClassLinker* class_linker = Runtime::Current()->GetClassLinker();
keep_alive = class_linker->GetHoldingClassLoaderOfCopiedMethod(self_, method);
} else {
keep_alive = method->GetDeclaringClass();
}

// 添加每一次遍历到的 artMethod 对象,在添加完成之后,进行 count++,进行 Arr 的偏移
trace_->Set<false,false>(static_cast<int32_t>(count_) + 1, keep_alive);
++count_;
}

}

在执行 VisitFrame 的过程中,会将每次的 method 拎出来,然后添加至 ObjectArray 的集合中。当所有方法查找完成之后,会进行 method 的反解。


堆栈信息反解关键操作


反解的流程在文章开头,通过 Thread::InternalStackTraceToStackTraceElementArray(soa,trace) 来进行反解。


thread.cc

jobjectArray Thread::InternalStackTraceToStackTraceElementArray(const ScopedObjectAccessAlreadyRunnable& soa,jobject internal,jobjectArray output_array,int* stack_depth) {

int32_t depth = soa.Decode<mirror::Array>(internal)->GetLength() - 1;

for (uint32_t i = 0; i < static_cast<uint32_t>(depth); ++i) {
ObjPtr<mirror::ObjectArray<mirror::Object>> decoded_traces = soa.Decode<mirror::Object>(internal)->AsObjectArray<mirror::Object>();
const ObjPtr<mirror::PointerArray> method_trace = ObjPtr<mirror::PointerArray>::DownCast(decoded_traces->Get(0));

// 【Step1】: 提取数组中的 ArtMethod
ArtMethod* method = method_trace->GetElementPtrSize<ArtMethod*>(i, kRuntimePointerSize);
uint32_t dex_pc = method_trace->GetElementPtrSize<uint32_t>(i + static_cast<uint32_t>(method_trace->GetLength()) / 2, kRuntimePointerSize);

// 【Step2】: 将 ArtMethod 转换成业务上层可识别的 StackTraceElement 对象
const ObjPtr<mirror::StackTraceElement> obj = CreateStackTraceElement(soa, method, dex_pc);
soa.Decode<mirror::ObjectArray<mirror::StackTraceElement>>(result)->Set<false>(static_cast<int32_t>(i), obj);
}
return result;

}

static ObjPtr<mirror::StackTraceElement> CreateStackTraceElement(

const ScopedObjectAccessAlreadyRunnable& soa,
ArtMethod* method,
uint32_t dex_pc) REQUIRES_SHARED(Locks::mutator_lock_) {

// 【Step3】: 获取行号
line_number = method->GetLineNumFromDexPC(dex_pc);

// 【Step4】: 获取类名
const char* descriptor = method->GetDeclaringClassDescriptor();
std::string class_name(PrettyDescriptor(descriptor));
class_name_object.Assign(mirror::String::AllocFromModifiedUtf8(soa.Self(), class_name.c_str()));

// 【Step5】: 获取类路径
const char* source_file = method->GetDeclaringClassSourceFile();
source_name_object.Assign(mirror::String::AllocFromModifiedUtf8(soa.Self(), source_file));


// 【Step6】: 获取方法名
const char* method_name = method->GetInterfaceMethodIfProxy(kRuntimePointerSize)->GetName();
Handle<mirror::String> method_name_object(hs.NewHandle(mirror::String::AllocFromModifiedUtf8(soa.Self(), method_name)));

// 【Step7】: 数据封装回抛
return mirror::StackTraceElement::Alloc(soa.Self(),class_name_object,method_name_object,source_name_object,line_number);
}

到这里我们已经分析完一次由 Java 层触发的堆栈调用链路一直到底层的实现逻辑。


核心流程


我们的目标是抓栈,因此我们只需要关注 count_visitor.WalkStack 之后的栈回溯流程。



耗时阶段


这里最后阶段将 ArtMethod 转换成业务上层可识别的 StackTraceElement,由于涉及到大量的字符串操作,给 Java 堆栈的执行贡献了很大的耗时占比。


抓栈新思路


传统的抓栈产生的数据很完善,过程也比较耗时。我们是否可以简化这个流程,提高抓栈效率呢,理论上是可以的,我们只需要自己将这个流程复写一份,然后抛弃部分的数据,优化数据获取时间,同样可以做到更高效的抓栈体验。


Native抓栈逻辑实现


根据系统抓栈流程,我们可以梳理出要做的几个事情点


要做的事情:




  • 挂起线程【获取挂起线程方法内存地址】




  • 进行抓栈【获取抓栈方法内存地址】【优化抓栈耗时】




  • 恢复线程的执行【获取恢复线程方法内存地址】




遇到的问题及解决方案:



  • 如何获取系统 threadList 对象


threadList 是线程执行挂起和恢复的关键对象,系统未暴露该对象的直接访问操作,因此我们只能另辟蹊径来获取它,threadList 获取依赖流程图如下:



如果想要执行线程的挂起 thread_->SuspendThreadByPeer 或者恢复 thread_list->Resume ,首先需要获取到 thread_list 系统对象,该对象是通过 Runtime::Current()->getThreadList() 获取而来,,因此我们要先获取 Runtime , Runtime 的获取可以通过 JavaVmExt 来获取,而 JavaVmExt 可以通过 JNI_OnLoad 时的 JavaVM 来获取,完整流程如下代码所示


JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {

JNIEnv *env = NULL;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
return -1;
}

JavaVM *javaVM;
env->GetJavaVM(&javaVM);
auto *javaVMExt = (JavaVMExt *) javaVM;
void *runtime = javaVMExt->runtime;

// JavaVMExt 结构
// 10.0 https://android.googlesource.com/platform/art/+/refs/tags/android-10.0.0_r1/runtime/jni/java_vm_ext.h

// 【Step1】. 找到 Runtime_instance_ 的位置
if (api < 30) {
runtime_instance_ = runtime;
} else {
int vm_offset = find_offset(runtime, MAX_SEARCH_LEN, javaVM);
runtime_instance_ = reinterpret_cast<void *>(reinterpret_cast<char *>(runtime) + vm_offset - offsetof(PartialRuntimeR, java_vm_));
}

// 【Step2】. 以 runtime_instance_ 的地址为起点,开始找到 JavaVMExt 在 【https://android.googlesource.com/platform/art/+/refs/tags/android-10.0.0_r29/runtime/runtime.h】中的位置
// 7.1 https://android.googlesource.com/platform/art/+/refs/tags/android-7.1.2_r39/runtime/runtime.h
int offsetOfVmExt = findOffset(runtime_instance_, 0, MAX, (size_t) javaVMExt);
if (offsetOfVmExt < 0) {
ArtHelper::reduce_model = 1;
return;
}

// 【Step3】. 根据 JavaVMExt 的位置,根据各个版本的结构,进行偏移,生成 PartialRuntimeSimpleTenR 的结构
if (ArtHelper::api == ANDROID_P_API || ArtHelper::api == ANDROID_O_MR1_API) {
PartialRuntimeSimpleNineR *simpleR = (PartialRuntimeSimpleNineR *) ((char *) runtime_instance_ + offsetOfVmExt - offsetof(PartialRuntimeSimpleNineR, java_vm_));
thread_list = simpleR->thread_list_;
}else if (ArtHelper::api <= ANDROID_O_API) {
PartialRuntimeSimpleSevenR *simpleR = (PartialRuntimeSimpleSevenR *) ((char *) runtime_instance_ + offsetOfVmExt - offsetof(PartialRuntimeSimpleSevenR, java_vm_));
thread_list = simpleR->thread_list_;
}else{
PartialRuntimeSimpleTenR *simpleR = (PartialRuntimeSimpleTenR *) ((char *) runtime_instance_ + offsetOfVmExt - offsetof(PartialRuntimeSimpleTenR, java_vm_));
thread_list = simpleR->thread_list_;
}

}

经过三个步骤,我们就可以获取到底层的 Runtime 对象,以及最关键的 thread_list 对象,有了它,我们就可以对线程执行暂停和恢复操作。



  • 线程的暂停和恢复


因为 SuspendThreadByPeer 和 Resume 方法我们访问不到,但如果我们能够找到这两个方法的内存地址,那么就可以直接执行了,怎么获取到内存地址呢?这里使用 Nougat_dlfunctions 的 fake_dlopen() 和 fake_dlsym() 来获取已被加载到内存的动态链接库 libart.so 中方法内存地址。


    WalkStack_ = reinterpret_cast<void (*)(StackVisitor *, bool)>(dlsym_ex(handle,"_ZN3art12StackVisitor9WalkStackILNS0_16CountTransitionsE0EEEvb"));
SuspendThreadByThreadId_ = reinterpret_cast<void *(*)(void *, uint32_t, SuspendReason, bool *)>(dlsym_ex(handle,"_ZN3art10ThreadList23SuspendThreadByThreadIdEjNS_13SuspendReasonEPb"));
Resume_ = reinterpret_cast<bool (*)(void *, void *, SuspendReason)>(dlsym_ex(handle, "_ZN3art10ThreadList6ResumeEPNS_6ThreadENS_13SuspendReasonE"));
PrettyMethod_ = reinterpret_cast<std::string (*)(void *, bool)>(dlsym_ex(handle, "_ZN3art9ArtMethod12PrettyMethodEb"));


到这里,我们已经已经可以完成线程的挂起和恢复了,接下来就是抓栈的操作处理流程。



  • 自定义抓栈


同样的,由于我们已经获取到用于栈回溯的 WalkStack 方法地址,我们只需要提供一个自定义的 TraceVisitor 类即可实现栈回溯


class CustomFetchStackTraceVisitor : public StackVisitor {

bool VisitFrame() override {

// 【Step1】: 系统堆栈调用时我们分析到的流程,每帧遍历时会走一次当前流程
void *method = GetMethod();

// 【Step2】: 获取到 Method 对象之后,使用 circular_buffer 存起来,没有多余的过滤逻辑,不反解字符串
if (CustomFetchStackTraceVisitorCallback!= nullptr){
return CustomFetchStackTraceVisitorCallback(method);
}
return true;
}

}

获取到 Method 之后,为了节省本次的抓栈耗时,我们使用固定大小的 circular_buffer 将数据存储起来,新数据自动覆盖老数据,根据需求,进行异步反解 Method 中的详细堆栈数据。到这里,自定义的 Native 抓栈逻辑就完成了。


总结


目前自定义 native 抓栈的多个阶段需要兼容不同系统版本的 thread_list 获取,以及不同版本的线程挂起,线程恢复的函数地址获取。这些都会导致出现或多或少的兼容性问题,这里可以通过两种方案来规避,第一种是过滤读取到的不合法地址,对于这类不合法地址,需要跳过抓栈流程。另外一种就是动态配置下发过滤这些不兼容版本机型。


参考资料



作者:网易云音乐技术团队
链接:https://juejin.cn/post/7212809255946469432
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

技术简历该如何写

很久没有写文章了,一直不知道如何再起笔,也一直没有想好要写些什么。这几年感受最深的就是互联网跌宕起伏,身边多多少少的技术小伙伴都有不同程度的受到影响。裁员,求职,招聘这几个词一直围绕着这两年的技术人。因此,重新执笔后的第一篇就来聊聊技术简历该如何写。 简历不仅...
继续阅读 »

很久没有写文章了,一直不知道如何再起笔,也一直没有想好要写些什么。这几年感受最深的就是互联网跌宕起伏,身边多多少少的技术小伙伴都有不同程度的受到影响。裁员,求职,招聘这几个词一直围绕着这两年的技术人。因此,重新执笔后的第一篇就来聊聊技术简历该如何写。


简历不仅仅是敲门砖


我们都经常会听到简历是敲门砖。但是它也不仅仅只是敲门砖,一个技术岗位的求职流程,从投递简历之后,一般会经历以下几个流程:


hr的简历筛选 -> 部门简历筛选 -> N轮技术面 -> HR面 -> 谈offer -> 接offer ->背调 -> 入职,而简历只有在前两轮的简历筛选才会起到敲门砖的作用。除了得到面试的机会以外,它在技术面中也有着非常重要的作用。在技术面中,一般面试官包括你所投递的岗位所在的小组的同事、你的组长、部门的Leader,事业线的Leader等,如果有交叉面,可能有别的小组或者部门的人,这些人对候选人的第一印象就是简历,其次是之前面试官的面评,而在这个技术面环节,简历的内容将会直接影响你的技术面走向。所以一定要重视自己的简历,如果连求职者自己都没能重视自己的简历,又怎么能希望面试官很好的考察和了解你。


如何编排简历内容


首先来聊聊简历整体内容编排的原则:



  1. 不要出现重复的内容:简历的空间大小一般跟一张A4纸大小差不多,在简历这寸土寸金的地方,切记不要出现重复的内容来浪费简历的空间。尽可能的利用简历空间。

  2. 字号适中和字体统一:字号过小会导致在阅读简历时非常吃力,而字号过大会比较占用简历空间,所以可以根据自己的内容,适当调节字号。而全篇的字体尽量保持一种风格,不要出现多种字体风格。

  3. 简历内容需要有侧重点:侧重的内容有以下四种方式:加图标来凸显侧重的内容、加粗、改变文字颜色以及改变字号。比较推荐用加粗和适当的字号改变来凸显侧重的内容,但是在一些正文部分不建议用改变字号来凸显,因为会显得整体很乱,一般在正文部分加粗就可以达到凸显的目的。

  4. 把精力从提升美感转移到充实内容:可能很多人会觉得简历美观不是更加分吗?的确,在内容相同的情况下,精美的简历的确会更有优势,但是在技术岗位中,除了产品设计等与美搭边的岗位,其他岗位对此的加分可以忽略不计,而有这个时间纠结简历的边框颜色用什么,不如多审视自己的内容,注重内容的编排和撰写。虽然可以适当不注重美感,但是简历基本的整洁还是需要保证的。


除了简历整体的编排原则外,最重要的就是技术简历的内容,技术简历可以拆解成以下六部分,其中前五个部分必须要有,最后一个部分是可选:



  • (必选)基本信息:在基本信息中必须呈现的内容有姓名、性别、年龄、民族、籍贯、电话、邮箱。基本信息必须准确且真实,否则面试官很难联系到候选人。这些内容一般都不太会有很大的问题,但是在基本信息中,对于照片,除非是对自己的外貌非常自信,并且认为外貌出众到能够起到加分效果的,才把照片附着在简历上,否则大可不必在简历上加上照片,因为它既起不到加分的作用,还会占用简历空间。

  • (必选)教育背景:教育背景一般罗列从大学学历至最高学历的教育背景,那些从高中学历开始写的真的大可不必。并且在排版上建议把最高学历放在最前面,倒叙的方式描述你的教育背景,这样可以让hr和面试官第一眼就了解你的学历优势。如果是一些不太耳熟能详的学校,但是却是985或者211的学校,建议在学校名称后面加括号备注,以免筛选简历的人不了解学校背景而导致被误杀。

  • (必选)职业技能:在写职业技能时,首先从岗位要求中去提炼关键字,一般的岗位要求会列举一些具体的职业技能关键字,以及比较笼统的职业技能关键字,比如岗位要求熟悉Docker、Kubernetes等云原生技术,其中熟悉Kubernetes是一项比较具体的职业技能,而云原生就是一个比较笼统的职业技能描述,它涵盖的范围就很大。而我们在描述职业技能时,第一重要的就是在提炼到关键字后,岗位要求的具体的职业技能需要尽量出现在简历的职业技能描述中,除非完全没有经验或者没有接触过这类技能。第二重要的就是围绕笼统的职业技能来描述你擅长的内容,比如你擅长Service mesh技术,那就可以描述一下mesh相关的技术栈。最后才是描述一些其他技能点,这些其他技能也有侧重点,尽量选择跟岗位的职责有关系那一部分。除了内容上的优先级以外,还有一个非常重要的技巧就是不要把你非常不熟练的职业技能写到简历中。因为职业技能这一栏的内容一般有两个作用,第一个作用是作为一些简历筛选的依据,第二个作用就是很有可能被面试官挑关键字问八股文。而如果掌握的不够熟练,一旦遇到挑关键字问八股文的话,势必会很被动,并且回答不出来也会导致面试减分。最后一点就是描述职业技能时,“熟悉”这个词在大部分时候要比“精通”这个词好用,因为面试官看到“精通”这个词,心里预期会比较高,除非你非常自信,否则尽量用“熟悉”这个词。

  • (必选)工作经历:工作经历一般描述的是从毕业以后至今的每一段工作经历,工作经历分两种情况:第一种情况是经历比较多,此时如果每一份工作经历都展开描述会导致留给项目经历的空间就不多了,所以可以考虑只描述时间、所在的公司、担任的职位即可。而如果工作经历并不多,比如只有2-3段工作经历,此时可以在上述的三个基本内容之上,再加上一小段的描述,用于描述该职业中承担的职责,让面试官能够更好的了解之前的工作经历。一般如果有一些筛选要求,比如需要有一些大厂背书等,主要会关注工作经历中的职位和公司。

  • (必选)项目经历:项目经历是重中之重,它是整个简历中非常重要的部分,它的内容应该占用简历50%-60%的空间。为什么项目经历这么重要,因为项目经历往往是部门筛选阶段、面试官最关注的内容。首先能否用简洁的话清楚地描述整个项目,这非常考验候选人总结的能力。其次,一个项目经历都与岗位符合的。那么该如何编排项目经历的内容?首先项目经历一般描述2-4个项目即可。因为项目描述再多,一场面试大约在30-120分钟左右,面试官并没有那么多时间了解完所有的项目,反而项目写的太多,会被面试官碰到一些你不那么擅长的地方,所以集中火力准备几个项目即可。其次,既然项目数量有限,那就要从以往做过的项目中挑选出更加契合岗位的项目,它不但能够让你更容易的通过简历筛选,也能够让你更加容易通过面试。为什么这样做可以更加容易通过面试?因为一般来说,技术面试的前几面都是与你的岗位有所相关,如果你描述了与岗位契合的项目经历,你们能在面试过程中更容易取得一些共鸣,交流起来也会更加顺畅,这会为你的面试加不少分,而如果项目经历非常不契合,一旦面试官对你的项目不感兴趣或者并不能很好的理解你的项目,那就可能会面临无休止的八股文,在这种情况下,对于候选人是非常被动和吃亏的。所以在投递不同的岗位时,尽量每次调整自己的项目经历内容,不要想一个简历想吃遍天下。最后,每个项目经历的描述必须包括项目的整体介绍、你所承担的角色和职责以及你对项目的总结,项目的介绍和承担的职责比较容易理解, 而为什么要加上对项目的总结?因为项目的总结才能看出你在项目中的思考,并不是所有面试官都擅长引导你说出你自己的总结和思考的,而在简历中直接呈现你的思考,能够反映出你的一些特质,这样才能够更加吸引面试官。

  • (可选)自我评价:很多人的简历上都会写有自我评价,但是为什么我认为自我评价是可选的?首先如果你的自我评价只是单一的写一些自夸的话,比如我肯吃苦,有钻研精神,热爱技术等等,那么建议不要写自我评价,因为一般这些话会被自动略过,这并不意味着你描述的一定是假的,而是作为hr或者面试官,第一次接触你,并且甚至都没有见过面,几乎都不会对这些话感兴趣。那么为什么它又是可选的内容,如果真的要写自我评价,该如何写好它?答案就一个,那就是佐证。比如你热爱技术,那就佐证如何热爱技术。比如贴一些你的Github链接、博客链接等来证明你热爱技术这个事实。
    介绍了技术简历的需要的一些内容后,还有两项内容是尽量不要写入简历中:

  • 求职意向:首先你在投递简历时候,应该已经明确投递的岗位,所以你的求职意向就是该岗位,无需在简历中再描述一遍。去掉该内容,可以节省简历空间,留给上述更需要的内容。除此之外,很多候选人都是一份简历吃天下,难免在投递不同岗位时忘记修改求职意向,导致求职意向和投递的岗位有所区别,而在简历筛选阶段虽然hr或者面试官知道你是忘记修改了这部分,但是也传达给他们一个讯息,也就是你对自己的职业道路并不是很清晰。

  • 期望薪资:首先填写期望薪资会在简历筛选阶段就比别人多一个筛选的内容,更加不容易通过筛选。其次,很多候选人都会同时面试多家企业,如果你在简历中写了期望薪资,一旦一家企业A给与了你期望的薪资,但是别的公司给与你更高的薪资,而你其实对企业A更感兴趣,希望再聊一下薪资,此时会比较难开口,因为你简历中已经写明了自己的期望薪资。所以要给自己一个议价的机会,不要在简历中填写期望薪资。


写给为找工作犯愁的你


面试是一个双向选择的过程,最终能否顺利拿到offer并入职,不仅仅取决于你个人的努力,还取决于面试官以及岗位需求,在这个过程中有很多不确定性的因素,而我们应该把我们能做好的内容都尽量做的完美一些,因为面试官虽然需要尽可能的挖掘和发现候选人潜能和能力,但是候选人更应该尽可能地展示自己的能力,在这短短的几十分钟内,让面试官尽可能的了解你的能力,在最大程度上减少因为自己的原因而导致面试失败。而因为面试官的不专业、岗位的变动等原因导致的面试失败,不要沮丧和气馁,那并不是你不够优秀,只是差了点运气,调整好心态,努力准备下一场面试,尽量抓住每一个机会。


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

30 岁了!通过 AI 问答完成了这篇思考文章

大家好,我是 shixin。 岁数越大,越不愿承认自己的真实年龄。前段时间别人问我年纪的时候,我嘴硬的说“二十九周岁”,现在,就只能无奈的说“三十”了。 说来也奇怪,为什么会觉得无奈呢?我想,我是想保留「二十多岁」的青春朝气、心无旁骛,抗拒「三十多岁」的中年危...
继续阅读 »

大家好,我是 shixin。


岁数越大,越不愿承认自己的真实年龄。前段时间别人问我年纪的时候,我嘴硬的说“二十九周岁”,现在,就只能无奈的说“三十”了。


说来也奇怪,为什么会觉得无奈呢?我想,我是想保留「二十多岁」的青春朝气、心无旁骛,抗拒「三十多岁」的中年危机、生活压力。


无论怎样我终究还是和三十岁相遇了,既然逃不掉,那今天就和它聊一聊。


三十岁意味着什么


我拿着这个问题问了 ChatGPT,它根据我的上下文给的回答如下:




可以看到,它给的回答还是蛮好的,基本上道出了现在困扰我的一些点。


三十岁,工作和生活对我的要求更高了。


工作方面,现在需要考虑的比以前更多了一些。除了个人贡献还需要做团队贡献,为自己的小组和整个团队带来更大价值,把自己知道的技术和经验传播给更多小伙伴。


家庭方面,快到要小孩的时候了。理论上三十岁已经年纪不小应该响应国家号召,但无奈生娃养娃的成本太大,还得多奋斗几年才有底气。今年准备先把婚礼办了(疫情影响婚礼日期改了好几次,上帝保佑这次顺利),过两年再考虑要孩子吧。


至于工作生活的平衡,老实讲目前还没有足够的资本,还得在工作上投入大部分时间。如何解决这种情况呢?是个值得思考的问题。


三十岁前我的人生


三十岁前,我的人生里有很多意想不到


十岁的我,没有想到未来我会去包头,更没有想到会在高中遇到现在的老婆。那时的我在呼和浩特,有四五个很要好的朋友,搬家的时候心里有一万个不舍。


十五岁的我,没有想到我会去西安读书,学的是计算机。那时的我还在想方设法溜到网吧通宵打游戏。


二十岁的我,没有想到我会从事安卓开发,也没有想到会去上海工作。那时的我在盲目瞎学,手机上写 OJ,看小甲鱼和黑马程序员,图书馆借了几本很老的 MFC 和 HTML CSS 书,跟着例子敲出来一个 H5 打飞机游戏。


二十五岁的我,没有想到我会在上海定居。那时我想的是干几年去西安定居,在那里离老家近一点,买房压力也小一点。后来机缘巧合,在买房时和几个前辈朋友聊了聊,听了他们的劝导,改成在上海定居。




ChatGPT 的这段回答让我泪目。有时候打的字越多,越渴望得到认可的回复,这种感觉,它给到了。



三十岁的我,虽然没有 100% 达到五年前预想的目标,但好在完成了一些当时觉得很难的事,比如买房、写书、直播分享,这些事是我成长的见证,也让我沉淀下一些经验和教训。


希望自己可以继续保持的


我希望自己继续保持的第一个点:在损失可以接受的情况下,多尝试多探索。


之前打德扑的时候,我属于比较激进和浪的那种,这种性格的缺点是会浪费很多筹码,但优点是过程很有趣,也常常会博到一些额外的收益。


生活里也是类似,在大学做小生意的时候,我愿意多跑几家店看看有没有价格更合适的货,也愿意多推开一扇门去问一下是否有需求,虽然收到不少白眼、也没赚大钱,但这段经历让我意识到:反正被拒绝也没什么损失,多试一次就多一个机会。


第二个需要继续保持的点:多种善因。


过往人生的关键节点,让我深刻的感受到:当下的果,往往来自过去不经意间种下的因。


就拿今年的几件事来说:



  1. 二月有机会在社区里做分享,缘自去年国庆主动报名 GDE 项目,虽然没通过筛选,但好在建立了联系,有这种机会时人家才会想到我




  1. 上周组里做的 ReactNative 技术培训,缘自字节时做的 Diggo 项目,在其中提升了前端开发技术,以至于后面做 RN 很顺畅,从而走在团队前头


今年很多事都是之前种下的善因结出的果实,除了满足,还需要多想想:



  1. 怎样为以后种下更多善因




  1. 现在要做的事,从长期来看,重复多次后的收益是怎样的



第三个需要继续保持的点:每日、每周、每年必做计划。


每日预则立,不立则废。我是一个善忘的人,如果哪天没有定好计划,基本上就稀里糊涂的过去了。首次发现这个问题,是我写2016 年度总结的时候,回顾发现好多细节都不记得了,有的月份里可能只记得一两件事,剩下的日子都进了黑洞无影无踪。


从那以后我就经常做记录、做计划,既然内存不够用,那就用磁盘缓存。做好每天的计划后,即使被突发事情分了心,我也可以及时调整状态回归高优。在日积月累下,才渐渐地完成了一件件看似很难的事,比如一篇有价值的文章、一个高质量的开源库(github.com/shixinzhang…)。



希望自己可以避免的


除了需要继续保持的,我也有很多后悔的事,比如做错事、说错话、浪费时间。


总结原因后,大概有这几点需要避免:



  1. 避免思想上的懒惰,少说这样的话:没办法、算了、就这样吧;多说:我试试、或许这样做就可以




  1. 避免和他人比较,比别人优秀或者差都不重要,重要的是有没有持续前进




  1. 避免没有进展的时候硬逼自己,多思考方向、方法是不是有问题




  1. 避免花钱的时候只看价钱,不对比购买后的体验和长期区别




  1. 避免做计划的时候过于悲观,目标定高点才可能做的更好




  1. 避免追求完美而不愿意开始,做完比做好优先级更高




  1. 避免在累的时候不休息,贪图享乐而继续浑浑噩噩





  1. 避免骄傲自满、自我膨胀,骄傲一来羞耻就来了




大胆想象一下,三十五岁的我


借用亚马逊的逆向工作法,先想象一下我 35 岁的情况:



  1. 第一种可能:独立开发了某个产品,为细分领域的人提供了独特的价值,从而获得不错的收益,业务比较忙的时候雇佣了几个助手




  1. 第二种可能:继续打工,但因为技术较好、沟通表达能力不错、有商业思维,担任某个业务的技术负责人




  1. 第三种可能:因为工作经验和年纪薪资不匹配被裁,投简历基本没有回复,最后忍痛降薪 50% 接了个 offer


要达到第一种情况,需要具备技术广度,可以独立完成产品的需求调研、设计、全栈开发和运营,更重要的是,尽早捕捉到信息,挖掘出其中的信息不平衡点或者需求点。这种情况对人的要求更高、风险也更高。


要达到第二种情况,需要付出的努力比上面略微少一点,需要具备一定的技术深度和广度、提升对公司业务和行业趋势的了解,主导完成一些有价值的事,同时在公司内部有一定的影响力。这种情况比第一种更稳一点。


要避免第三种情况,需要经常了解市场相关岗位的要求,不断提升自己的技术和业务价值以匹配要求,最好有代表性的作品和影响力。


总结


这篇文章是我三十岁当天开始动笔写的,因为种种原因拖到今天才完成,实在不应该(捂脸哭。


总是听人讲“三十而立”,为了看自己到底立没立,我看了好些名人的视频,想从中寻找答案。



到现在我悟了,所谓的“立“就是建立、确定、稳固。人活着最重要的就是吃饱和开心,三十岁,能够有一技之长和自我融洽的三观,就算是立住了吧!


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

复盘:这个注意力的陷阱,越早改掉越好

本文2048字,阅读只需3分钟。 以下为正文。 昨天晚上12点,我洗漱完,刚要入睡的时候, 忽然听到楼下吵闹起来,声音此起彼伏。 接着是小孩的哭闹声,搬桌子的声音,路人的帮腔声。 又过了几分钟,水果摊主的声音也加入了:“不要再吵了,我们要准备收摊休息了”。 强...
继续阅读 »

本文2048字,阅读只需3分钟。


以下为正文。


昨天晚上12点,我洗漱完,刚要入睡的时候,


忽然听到楼下吵闹起来,声音此起彼伏。


接着是小孩的哭闹声,搬桌子的声音,路人的帮腔声。


又过了几分钟,水果摊主的声音也加入了:“不要再吵了,我们要准备收摊休息了”。


强忍着睡意,推开窗子瞥了一眼:阵仗好大,都围成水桶了,都是一群看热闹不嫌事大的。


回去倒头就睡,


但是大脑异常清醒。


于是,就开始复盘一些事情。


我们太容易被无关紧要的事情吸引,而忘记了自己昂贵的注意力究竟应该放在哪里。


生活中,大家似乎很喜欢「围观」这件事情。


在现实世界里围观


走在街上,看到有热闹的事情,马上就上去围观了。


但发生的事情,无外乎就是:


1、娱乐营销,引导消费。


2、吵架撕逼,互相揭短。


3、发生车祸,血肉模糊。


正如今天发生的事情,两个摊主发生矛盾,跟围观的路人半毛钱关系都没有。但就是架不住看热闹的心态,身体忍不住上钩了。


就这样,时间一点一点浪费掉了。


同时,在公园里也能看到围观的画面。


公园里两个老大爷在下棋,


以棋盘为圆心,2米的距离为半径,画一个圆,周围站满了围观的人群。


事情的发展往往变成这样。


刚入场,站好立场,指点江山。


再然后,发现立场不对,相互说服。


就这样,时间一点一点浪费掉了。


类似的事情不止发生在现实世界里,还发生在网络上。


在网络世界里围观


从新闻里看世界


1、七旬大爷结婚3次被瓜分财产


2、好欣慰,残疾大熊猫终于学会爬树了


3、普京不再低调,美国罕见服软


4、吴秀波,将7年情人送进监狱,如今为自己的风流付出代价


5、印度男子因新娘高中成绩差退婚


每天各种新闻App给我推过来这些内容都让我作呕。


从抖音里看世界


看娱乐新闻,追陈坤跟周迅的私生子的真实性。


看颜值视频,扭腰扭屁股,冲着屏幕邪魅一笑。


看健身视频,评论区里指责up主身板像猴子。


从技术社区里看世界


本来打算学习技术问题的,一下子就被文章评论区的「理智」发言吸引了。


比如抨击作者技术不行。


比如说作者不写技术文章,写这种水文的撕逼。


话题一旦打开,如开闸的洪流,进而演变成无休止的争论。


跟今晚外面的争论是不是如出一辙?


我们太容易被无关紧要的事情吸引,而忘记了自己昂贵的注意力究竟应该放在哪里。


注意力才是最重要的


注意力才是最重要的,远比技术、思考、底层逻辑这些东西都重要100倍。注意力在哪,事业就在哪(是事业,而不是工作)。


好,如果改掉了围观的坏毛病,开始认真的关注自己的注意力了呢,接下来还会掉入另外一个陷阱。


那就是追趋势。


追趋势


类似的经历很多。


风口来了追风口。


1、P2P火了,我要投资。


2、短视频直播火了,我要跟上。


3、NFT火了,我要入局。


4、内容创业火了,我要开始写作。


5、知识付费火了,我要开始学做课。


风口来了,镰刀也跟着来了。


还有,政策来了追政策


国家对互联网+的扶持,是个创业机会。


国家对新能源的扶持,投资新能源股票吧。


国家对实体行业的扶持,现在从互联网跳槽去传统企业好不好。


一通操作,干废了。


还有,热点来了追热点


李嘉诚投资了中国哪些项目?


俄乌战争的持续发酵对中国的影响?


硅谷银行倒闭对中国投资的影响?


热点一天一个样子,本质上我们追热点的心一点都没变。


就拿前面说的「内容创业」这个事情来说,


用20多年积累的名人名言,告诉我:


1、机会是留给有准备的人。


2、机会是留给行业有积累的人。


3、机会是留给聚焦深度的人。


要做到这三点,都需要长期付出注意力才行。那把注意力花在了围观上,做成这件事的人凭什么是我呢?


未来自己坐庄


大学的时候读过一本书叫做《刻意练习》,大概的意思是:一个人只要对一件事情付出足够的时间进行刻意练习,就能成为这方面的专家。


我今天想到的「注意力」这个词,跟这个刻意练习是同一个意思。


段永平的《投资问答录》里面记忆最深的一句话是:坚持做对的事情,并努力把事情做对。


我在这句话里面看到的是在一个对的方向上持续投入注意力,长期坚持,成为这个事情的专家。


从时间的长度来看,无论是李嘉诚也好,巴菲特也好,做的事情最终拼的也是概率。大部分时间,有赢有亏,但是赢的概率明显高于亏的概率。


而长期投入注意力,做自己很了解的事情,就是提高赢的概率。


毕业以来,


躲过了消费主义的陷阱。


躲过了P2P的陷阱。


躲过了无脑恋爱的陷阱。


最终却没躲过理财投资的陷阱。


年轻的时候,被什么东西割上一刀是没法躲避的。从小到大,也没人跟我说过上面的陷阱到底是怎么一回事。


对规则不了解,没有亲身经历过。


而每一个盘子,庄家的设计都直指人性的自负、贪婪、消极、享乐。


手里存款从0-100万的过程,总有一把镰刀是向你挥舞的。


但自从知道自己的注意力在哪里,


刻意练习加以辅助,


在未来,无论再小的一个盘子,我可以自己坐庄。


如果这篇文档对你有帮助,欢迎点赞、关注或者在评论区留言,我会第一时间对你的认可进行回应。精彩内容在后面,防止跑丢,友友们可以先关注我,每一篇文章都能及时通知不会遗失。


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

高仿PPT特殊文字效果,TextView实现

事情是这样的,我无聊刷到一个B站视频【旁门左道PPT】我发现了大厂发布会中,少文字PPT还贼高级的秘密!。看到视频中这个特殊的PPT文字效果,个人感觉非常高端。我就想,能不能用TextView来实现。于是就有了这篇文章,效果如下图: ...
继续阅读 »

事情是这样的,我无聊刷到一个B站视频【旁门左道PPT】我发现了大厂发布会中,少文字PPT还贼高级的秘密!。看到视频中这个特殊的PPT文字效果,个人感觉非常高端。我就想,能不能用TextView来实现。于是就有了这篇文章,效果如下图:

















简单填充加入文字排版加入动画

图片填充


在Android中,google提供了 BitmapShader 来实现图片填充的功能。代码如下


public BitmapShader(@NonNull Bitmap bitmap, 
@NonNull TileMode tileX,
@NonNull TileMode tileY)

参数介绍:


● bitmap:用来做填充的 Bitmap 对象


● tileX:横向的 TileMode(平铺模式)


● tileY:纵向的 TileMode


TileMode有三种:分别是 Shader.TileMode.CLAMP、Shader.TileMode.MIRROR、Shader.TileMode.REPEAT


● Shader.TileMode.CLAMP:如果着色器超出原始边界范围,会复制边缘颜色。


● Shader.TileMode.MIRROR:横向和纵向的重复着色器的图像,交替镜像图像是相邻的图像总是接合。


● Shader.TileMode.REPEAT: 横向和纵向的重复着色器的图像。


接下来,我们自定义 TextView,让它使用我们定义的Shader,代码如下:


class MaskTextView: androidx.appcompat.widget.AppCompatTextView {

constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)

private var shader: BitmapShader? = null

fun setMaskDrawable(source: Drawable): Unit {
val maskW: Int = source.getIntrinsicWidth()
val maskH: Int = source.getIntrinsicHeight()

val b = Bitmap.createBitmap(maskW, maskH, Bitmap.Config.ARGB_8888)
val c = Canvas(b)

c.drawColor(currentTextColor)
source.setBounds(0, 0, maskW, maskH)
source.draw(c)

shader = BitmapShader(b, Shader.TileMode.REPEAT, Shader.TileMode.CLAMP)
paint.shader = shader
}
}

免费壁纸网站中找到一个你喜欢的图片,调用 setMaskDrawable 方法时,我们就可以看到填充后的效果了。效果如下:



但是光这个效果还不够,还需要设置文字排版。看【旁门左道PPT】我发现了大厂发布会中,少文字PPT还贼高级的秘密!我们知道,有三种文字排版,分别是 高低低高、高低高低、低高低高,它们都需要修改文字的 baseline 来实现。


如何修改单个字符的 baseline 呢?很简单,不需要重写 onDraw 方法。我们可以自定义 Span,然后通过 TextPaint 来实现。在上代码前,先介绍一下 TextPaint,TextPaint 继承 Paint,在绘制和测量文本时给Android一些额外的数据。它的属性介绍如下:


● baselineShift - 基线是文本底部的线。改变baselineShift会使基线向上或向下移动,所以它影响到文本在一条线上的绘制高度。


● bgColor - 这是文本后面的背景颜色。


● density - 暂不清楚它的作用


● drawableState - 暂不清楚它的作用


● linkColor - 一个链接的文本颜色。


可以看到我们只需要修改 baselineShift 就可以改变单个文字的 baseline 了,自定义的Span的代码如下:


class TextUpOrDownSpan(private val isUp:Boolean, private val offset: Int): CharacterStyle() {

override fun updateDrawState(tp: TextPaint?) {
tp?.baselineShift = if(isUp) - offset else offset
}
}

效果如下:



添加一个波浪动画


我们也可以给我们的图片填充增加一个动画,其中最常见的就是波浪动画了。效果实现很简单:


第一步:在波浪效果网站上下载一张自己想要的波浪图片



第二步:创建自定义的TextView,加上对应的参数,方便做动画。代码如下:


class AnimatorMaskTextView: androidx.appcompat.widget.AppCompatTextView {

private var shader: BitmapShader? = null
private var shaderMatrix: Matrix = Matrix()
private var offsetY = 0f
var maskX = 0f
set(value) {
field = value
invalidate()
}
var maskY = 0f
set(value) {
field = value
invalidate()
}

fun setMaskDrawable(source: Drawable): Unit {
val maskW: Int = source.getIntrinsicWidth()
val maskH: Int = source.getIntrinsicHeight()

val b = Bitmap.createBitmap(maskW, maskH, Bitmap.Config.ARGB_8888)
val c = Canvas(b)

c.drawColor(currentTextColor)
source.setBounds(0, 0, maskW, maskH)
source.draw(c)

shader = BitmapShader(b, Shader.TileMode.REPEAT, Shader.TileMode.CLAMP)
paint.shader = shader
offsetY = ((height - maskH) / 2).toFloat()
}

override fun onDraw(canvas: Canvas?) {
shaderMatrix.setTranslate(maskX, offsetY + maskY)
shader?.setLocalMatrix(shaderMatrix)
paint.shader = shader
super.onDraw(canvas)
}
}

第三步:使用Android的动画api,控制图片的位置。代码如下:


val maskXAnimator: ObjectAnimator = 
ObjectAnimator.ofFloat(textView, "maskX", 0f, textView.width.toFloat())
val maskYAnimator: ObjectAnimator =
ObjectAnimator.ofFloat(textView, "maskY", 0f, (-textView.getHeight()).toFloat())
val animatorSet = AnimatorSet()
animatorSet.playTogether(maskXAnimator, maskYAnimator)
animatorSet.start()

效果如下:



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

Android记一次JNI内存泄漏

记一次JNI内存泄漏 前景 在视频项目播放界面来回退出时,会触发内存LeakCanary内存泄漏警告。 分析 查看leakCanary的日志没有看到明确的泄漏点,所以直接取出leakCanary保存的hprof文件,保存目录在日志中有提醒,需要注意的是如果是a...
继续阅读 »

记一次JNI内存泄漏


前景


在视频项目播放界面来回退出时,会触发内存LeakCanary内存泄漏警告。


分析


查看leakCanary的日志没有看到明确的泄漏点,所以直接取出leakCanary保存的hprof文件,保存目录在日志中有提醒,需要注意的是如果是android11系统及以上的保存目录和android11以下不同,android11保存的目录在:


   /data/media/10/Download/leakcanary-包名/2023-03-14_17-19-45_115.hprof 

使用Memory Analyzer Tool(简称MAT) 工具进行分析,需要讲上面的hrof文件转换成mat需要的格式:


   hprof-conv -z 转换的文件 转换后的文件

hprof-conv -z 2023-03-14_17-19-45_115.hprof mat115.hprof

打开MAT,导入mat115文件,等待一段时间。


在预览界面打开Histogram,搜索需要检测的类,如:VideoActivity


screenshot-20230314-204413.png


搜索结果查看默认第一栏,如果没有泄漏,关闭VideoActivity之后,Objects数量一般是零,如果不为零,则可能存在泄漏。


右键Merge Shortest Paths to GC Roots/exclude all phantom/weak/soft etc,references/ 筛选出强引用的对象。


image.png


筛选出结果后,出现com.voyah.cockpit.video.ui.VideoActivity$1 @0x3232332 JIN Global 信息,且无法继续跟踪下去。


screenshot-20230314-205257.png


筛选出结果之后显示有六个VideoActivity对象没有释放,点击该对象也无法看到GC对象路径。(正常的java层内存泄漏能够看到泄漏的对象具体是哪一个)


正常的内存泄漏能够看到具体对象,如图:


image.png
这个MegaDataStorageConfig就是存在内存泄漏。


而我们现在的泄漏确实只知道VideoActivity$1 对象泄漏了,没有具体的对象,这样就没有办法跟踪下去了。


解决办法:


虽然无法继续跟踪,但泄漏的位置说明就是这个VideoActivity1,我们可以解压apk,在包内的class.dex中找到VideoActivity1 ,我们可以解压apk,在包内的class.dex中找到VideoActivity1这个Class类(class.dex可能有很多,一个个找),打开这个class,查看字节码(可以android studio中快捷打开build中的apk),根据【 .line 406 】等信息定位代码的位置,找到泄漏点。


screenshot-20230314-205442.png


screenshot-20230314-205600.png
screenshot-20230314-205523.png


根据方法名、代码行数、类名,直接定位到了存在泄漏的代码:


screenshot-20230314-205730.png


红框区内就是内存泄漏的代码,这个回调是一个三方sdk工具,我使用时进行了注册,在onDestory中反注册,但还是存在内存泄漏。(该对象未使用是我代码修改之后的)


修改方法


将这个回调移动到Application中去,然后进行事件或者回调的方式通知VideoActivity,在VideoActivity的onDestory中进行销毁回调。


修改完之后,多次进入VideoAcitivity然后在退出,导出hprof文件到mat中筛选查看,如图:


image.png


VideoActiviyty的对象已经变成了零,说明开始存在的内存泄漏已经修改好了,使用android proflier工具也能看到在退出videoactivity界面之后主动进行几次gc回收,内存使用量会回归到进入该界面之前。


总结:



  1. LeakCanary工具为辅助,MAT工具进行具体分析。因为LeakCanary工具的监听并不准确,如触发leakcanary泄漏警告时代码已经泄漏了很多次。

  2. 如果能够直接查看泄漏的对象,那是最好修改的,如果不能直接定位泄漏的对象,可以通过泄漏的Class对象在apk解压中找到改class,查看字节码定位具体的代码泄漏位置。

  3. 使用第三方的sdk时,最好使用Application Context,统一分发统一管理,减少内存泄漏。

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

文心一言:这48小时,我被问了xxxx个问题

16日下午14:00文心一言开启邀测了!作为国内首个新一代知识增强大语言模型、生成式 AI 产品,“邀测通道”瞬间被挤爆,甚至连百度人自己也需要排队等候。  但随着厂长的一声令下,开放了百度内部全员申请  一场由百度人自己发起的文心一言4...
继续阅读 »

16日下午14:00文心一言开启邀测了!作为国内首个新一代知识增强大语言模型、生成式 AI 产品,“邀测通道”瞬间被挤爆,甚至连百度人自己也需要排队等候。 


但随着厂长的一声令下,开放了百度内部全员申请 


图片

一场由百度人自己发起的文心一言48h 内测开始了…… 


由于发布当天是星期四,众所周知的老梗被提上日程 。

图片

这题一言表现不错,晚上加鸡腿! 


图片

本山西小编很好奇这位同学来自哪里,难道是老乡? 

图片

这位同学继续发问,“波棱盖”这很不山西,评论区有人知道这是哪里的说法吗? 


还有人用文心一言玩起了成语接龙,合理怀疑这位同学在摸鱼。

图片


语文考试通过,紧接着就是数学考试 。

图片


一问一答都表现不错,接下来是一场多轮考验。

图片

图片

不得不说,一言这张夜色昙花图,真是开到小编心里了。


下面这个问题,小编都嗅到了甜甜的味道,看来这位同学好事将近啊!

图片


下面这个魔鬼问题,让小编回忆起被英语听力支配的恐惧。

图片


看着大家晒出与一言的对话,小编只能感叹一句中华文化博大精深!一言你辛苦了,加油!


虽然测试在火热进行中,但可能还是有不少小伙伴没有办法第一时间使用文心一言,大家有什么问题需要我帮你问,欢迎评论区留言,小编继续帮大家刁难文心一言。


小tips:
个人用户:访问 yiyan.baidu.com,点击『立即体验』,目前可排队体验文心一言
企业用户:访问百度智能云官网进行申请,预约体验文心一言API调用服务


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

练习时长长达两年半的前端练习生——折腾半生,归来仍是菜鸡🥬🐤

毕业一年半,工作两年半 2021年毕业,虽然说是计算机专业毕业,但是大学嘛,懂的都懂 🥬🐤 大二暑假参加了Java老师组织的自学小组。【学Java未半,而中道转投前端】,自学了 HTML、CSS、JavaScript前端三剑客,大三下半学期,手持这三把 “利剑...
继续阅读 »

毕业一年半,工作两年半


2021年毕业,虽然说是计算机专业毕业,但是大学嘛,懂的都懂 🥬🐤


大二暑假参加了Java老师组织的自学小组。【学Java未半,而中道转投前端】,自学了 HTMLCSSJavaScript前端三剑客,大三下半学期,手持这三把 “利剑” 开始找工作 🤺 🤺🤺(此时我还不知道啥是框架)


果然是单纯未经世事的学生啊,结局嘛不仅是碰了一鼻子灰,更是备受打击😔。历时一个月,光公交费都花了三百多。


这一个月的感悟总结出来就是一句话:一定要掌握一个框架,一定要背面试题


工作


经历过现实的打击后,意识到了自己的不足,花了一些money,买了关于Vue(还涉及到超前沿的Vue3🐂)和React框架的课和一系列的JS基础课,开始埋头苦学。


实习


大四上学期还是要去学校上课的,在我三天两头校内校外跑,三天打鱼两天晒网的努力之下,终于找到了一份实习工作,技术栈是Vue和JS



面试真的是最有效率最有效果的学习方式,面试的路上 坐在公交车上都在背面试题,效率嘎嘎高,半个月顶我半年的学习效果



不得不说,公司氛围很好,老板领导都很棒,同事都很nice


经过大半年的努力工作和加班,我也到了毕业的日子


2021年,我青葱岁月画上句号的一年。于是为了顺利毕业,赶上青春最后的小尾巴,于是辞职回校毕业答辩


六月份毕业后,开始挥师北上


正式工作


在北京找到了一份工作,一直干到现在


工作前期,一直使用的都是JQueryvue2用于维护老项目,JQuery也是现用现学


公司领导还是相当鼓励使用新技术的,现在开发基本上也都是Vue3TS,但是还是时不时的维护和更新祖传的JQuery老项目


现在的这个公司是我毕业后面试的第一份工作,当时刚毕业,正在迷惘和不知所措中挣扎,也没有好好的准备面试,面了这一个就赶紧确定下来了。**建议大家在找工作时一定要多准备准备,多面试几家,一定不要太着急**

生活


北京可真大呀,租房可真贵呀,租的第一个房子在北京的西北角,凤凰岭附近,房租2200,通勤一个半小时左右,早上六点多就起来,晚上回到家都十点多


image.png


花了两千多大洋,买了一个灵眸相机,准备周末出去转一转记录一下美好的人生,但是和对象的休息时间不一致,所以周末我基本上也没出去过,这个相机也一直在角落里吃灰


image.png


现在手机也都能满足我们的拍照摄影需求,所以想买这个玩意的朋友,听老弟一句劝,别买了,这是真鸡肋啊


2022年换了个房子,离我俩上班的地方都挺近的,但是房租也是真贵,将近四千了


在北京的第一个七夕,花太贵了。请了一下午假,花了七十大洋买各种材料,做了个蔬菜花,不仅能吃一顿还能再吃一顿,真是经济又好吃 ^_^


image.png


image.png


副业挣钱



生命不息,折腾不止



缺钱啊!想要挣钱


两年前做了一个个人公众号,经常三天打鱼两天晒网的更新,找工作的时候经常发面试题和答案集锦,现在工作了也有很长时间没有更新了


2021年参与了一个活动,想折腾着找点副业干,于是跟几个朋友运营一个公众号,现在公众号也盈利了一段时间了,由于我自己不擅长引流,只能埋头苦干,分成收益寥寥无几,但是聊胜于无,下一步继续努力学习引流争取挣大钱


2022年一边做着公众号,一边寻求着突破


继续折腾,开始做闲鱼: 扒图片写文案,上架商品,每天都需要一两个小时的时间去选品、整理文案,一个月的时间卖出了十几件东西,一共收入一百出头,因为那一阵公司正忙,天天加班,也没有多余的时间在这上面,所以就慢慢放弃了。如果坚持下来一个月也能有个千八百的,可惜没有如果,但是耗费时间也是真的,工作不忙的朋友可以尝试下


坚持+执行 才能有结果

2022年尝试了一些事情,执行了,但是没有坚持。


2022


回望2022,回想自己的所作所为,这一年过的得过且过,没有目标没有动力,打鱼晒网是常态,咸鱼翻身是为了刷抖音🐠🐠🐠


这一年在公众号上抽中了两本书,一本《vue技术内幕》一本《前端架构师》,但是都放在书架上吃灰了


这一年更新过几次公众号,之前一直有记录面试题的习惯,也整理过一部分,并且把它们发到公众号上了,今年也整理了一些,但是内容不太完整,有一些还没找到答案,所以有一些也没有发到公众号上


image.png


这一年也获得过一些赞同,偶然间在github上看到大佬们的介绍页非常花里胡哨,我也整了一个,并且写了一个教程,收获了三十多个star,感觉心里美美哒~


github项目地址


掘金文章:# gitHub的readme页的卡片和提交活动图的制作方法


image.png


曾经梦想着成为一名开源大佬,尝试着写了一个的开源的JS工具库,写了几个工具函数就没再更新过了,这年它也算是烂尾了~


参加了几次掘金的月更活动,水了几十篇关于算法的文章,还获得了一些小礼品。并且学习用VuePress搭建了一个在线文档,里面记录了一些方法函数,还有我写的算法文章。掘金好像还改了月更活动的规则,以后单独用一个算法题不能算更文了,看来以后参加活动该换一种方式水了🐶。不过算法还是要继续学的,以后也会不定期更新算法



我写的文章还是有很多干货的,不信你看我的这个专栏:# 真·实用技能-从零实战



learnjts.png


还突然心血来潮对元宇宙区块链开始感兴趣了,买了极客时间的课,在B站学习了一点solidity,但也都是浅饮辄止,花了100大洋在币安上买了几个币,第二天就跌到97😂。


买了一个掘金的会员🥰,学习了几本小册;掘友们都说会员标志不够大不够闪💥,非常赞同😝


🥳🥳🥳跟几个大学舍友小聚了一下。有个周末还和一个舍友去4S店逛了逛看了看车,只看不买,诶~就是玩。店员也不管,这么多车我看着都一样,只能看出来小轿车和SUV,朋友挨个给我讲解🤠不得不说这个SUV坐着就是舒服


image.png


2023


2023就要来了,flag当立则立,没有flag的新年是没有灵魂的一年


image.png



  • 看完《vue技术内幕》和《前端架构师》这两本书

  • 深入学习 React和TS,完成一个完整的项目

  • 学习web3技术(web3.js、solidity)

  • 开源一个项目

  • 持续更新公众号,深耕自媒体

  • 折腾一个服务器,申请一个域名持续折腾

  • 玩遍北京,去北京外面玩一次(让我的口袋云台相机发挥作用)

  • 一定要换一个新的发型,烫个头

  • 换个新工作

  • 输出优质文章,多薅几次掘金的羊毛

  • 和朋友多聚聚,喝酒聊天吹🐄🍻


一个总结,写了大半个月😂😂😂


作者:董员外
来源:juejin.cn/post/7182217167134261308
收起阅读 »

随想 小城市的程序员该如何生存

前言 Hello,这里是百里, 一个无所事事的老年程序员. 随便写写,感慨一下.现今社会越来越畸形,以前打仗农村包围城市,现在经济也农村包围城市.一方面享受的交通,经济娱乐的便利,一方面又感慨,大城市何处是家. 今天讲讲我一个半路出身程序员的想法,以及将来我该...
继续阅读 »

前言


Hello,这里是百里, 一个无所事事的老年程序员.


随便写写,感慨一下.现今社会越来越畸形,以前打仗农村包围城市,现在经济也农村包围城市.一方面享受的交通,经济娱乐的便利,一方面又感慨,大城市何处是家. 今天讲讲我一个半路出身程序员的想法,以及将来我该如何或者我想如何.


半路出身转程序


普通二本,机械专业,直接进了校企和做的国家投资单位,做一名优秀的流水线工人.没错干了1年多真就流水线,我负责QA品质检查,检查玻璃质量如何,有没有损坏异色,干了1年多.工资5500一个月,每天9小时 ,单休.我当时还觉得我挺高兴的.直到发现招工时候,高中毕业的人也和我干一样的活,还是我领导,比我进来还晚.ε=(´ο`*)))唉ε=(´ο`*)))唉 .


18年裸辞,在家自己学了一下程序,最开始学的是java 学了3个多月,面了一家医疗企业,但是没让我做开发,让我做运维实施.因为有些编程基础,平时可以自己改改.工资其实也不错,在房价1.3w的地方能开到1.2w一个月. 缺点么..
我离职的时候还有176天的假期没修完. 基本上无休.我干了两年.


20年.刷抖音时候看了python 怎么怎么好 ,一咬牙一跺脚,花了3w多培训了python ,当初讲的多好多好, 但是,但是,这工作只能在大城市,我们这小地方 ,最好找工作的依然是php 和java ,python 一个都没有.至今还记得那个培训机构叫做 某男孩. 76个人进去的14个人毕业, 还说毕业率100% ,呵呵呵 骗子企业.


再后来凭借着会一些sql ,在某传统企业,做erp 二开, 基于delphi, 一直干到现在.


大城市就业机会多VS 小城市生活惬意


现今很多人不结婚,晚婚,多半是因为大城市生活节奏快,或者说结婚了没有物质基础,结婚了以后孩子怎么办,自己本身很痛苦了,让孩子更痛苦?


我是23岁结的婚,老婆是大学同学,大学谈了4年,当初也想过去大城市去打拼,因为同样的工作甚至更简单的工作工资就比我熬夜加班高的多. 但是我退缩了.传统农村人思想罢了.想回到家老婆孩子热炕头,小地方两个人赚一个月工资也够活的.

我有很多朋友在北京大厂,一年20w ,30w 的 工作 ,做的跟我相同的工作. 其实真的很羡慕,一年顶我2年的工作.也不是没想过去北上广深,但是我受不了孤独,哈哈矫情罢了..抛弃不了孩子老婆.


我们自己有一片菜地,还有个小院子,会自己种菜,还养了鸡.家门口有小河 , 偶尔还跟岳父抓抓鱼,真就码农.


讲讲技术栈


到现在入门程序已经快3年了.看到掘金中各种大佬说的东西讲道理,,完全看不懂,也许是年纪大了,(马上27),不知道学什么好,我的想法就是这辈子我不打算去大城市,就小城小桥流水活着 ,但是老技术不能吃一辈子, delphi 的工作讲道理我感觉做不久, 好多同学甚至不知道这个语言干嘛的.
本身技术栈.


python ,花了3w培训的,简单的没什么问题,不过好久没用了.


delphi,不能说精通,但是基本干活没啥问题.curd 没问题.天天用.


VUE2,3 ,偶尔做做bi,没事自己学的,买的课,但是也就是学了而已,学完了就忘了, 因为用不到. 而且也不深,因为看所谓的面试题,基本上不会,我一度认为我学的是假的东西 ,还去找人家退款.


SQL/kattle 算不上精通, 属于干活没问题情况, 因为delphi 是基于sql 存储过程的语言,动不动sql 写上万行... 那种 . 至于kattle 则是偶尔取数,做bi使用 ,还是停留在 能用会用, 问我就挂那种情况 .


帆软/数据分析 : 公司花钱买了帆软的8000 的课, 考试我是都考过了,然后 Bi 还是拿vue 做. 小程序 拿 uniapp 做. 也不知道为啥花钱买这个, 我兴师动众的学了3个多月基本上都会做,但是还是那句话 ,用不到,现在也就是学过了而已.


SAP 今年公司新近的业务, 讲道理 据说这个工资很高,而且很吃香, 现在ABAP 自己学了几个月了,已经能入手一些业务,不知道将来的发展如何. 继续用着吧.


未来及方向


年纪越来越大了,响应国家政策,现在努力二胎,又是一笔开销.


越活越迷茫,我该做什么,我该学什么 ,当前领导总是让我看了很多什么什么做人,怎么怎么演讲的书,美名其曰成长,但是我觉得还是东西学到手了才是真的.


打算扎根制造业,对于erp ,mes ,aps 等业务流程还是很熟悉的, 感觉制造业都用的东西还是可以的. 打算学sap,数据分析,BI方向吧. 也不知道方向对不对.


以上随便写写,27了还迷茫不知道是不是因为半路转行的缘故.


后续


三百六十行,行行转IT,感觉现在IT 这碗水早晚要洒,只是年头问题.当然如果非常牛逼的人除外.
但是人如果区分家庭和事业哪个更重要,也不好分辨,各有各的道理.


认识一个以前在群里的大佬.34岁没结婚,没孩子,死了,技术贼牛逼.也认识啥都不会但是光靠说也能拿几十万的人.钱难赚,钱又好赚. ε=(´ο`*)))唉 .
行了 写完继续摸鱼, 写写技术笔记吧.


不知道有没有在夜深人静的时候想过,我将来怎么

作者:百里落云
来源:juejin.cn/post/7140887445632974884
办,这种可笑的话题.

收起阅读 »

Android悬浮窗自己踩的2个小坑

最近在做一个全局悬浮窗基于ChatGPT应用快Ai,需要悬浮于在其他应用上面,方便从悬浮窗中,和ChatGPT对话后,对ChatGPT返回的内容拖拽到其他应用内部。快Ai应用本身透明,通过WindowManger添加悬浮窗。类似现在很多应用跳转到其他应用,会悬...
继续阅读 »

最近在做一个全局悬浮窗基于ChatGPT应用快Ai,需要悬浮于在其他应用上面,方便从悬浮窗中,和ChatGPT对话后,对ChatGPT返回的内容拖拽到其他应用内部。快Ai应用本身透明,通过WindowManger添加悬浮窗。类似现在很多应用跳转到其他应用,会悬浮一个小按钮,方便用户点击调回自身一样。只不过快Ai窗口比较大,但不全屏。


碰到以下几个问题:


1、悬浮窗中EditText无法获得弹出键盘


主要是没有明白下面两个属性的作用,在网上搜索之后直接设置了。



  • WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE


设置FLAG_NOT_FOCUSABLE,悬浮窗外的点击才有效,会把事件分发给悬浮窗底层的其他应用Activity。但设置了FLAG_NOT_FOCUSABLE,屏幕上除悬浮窗之外的地方也可以点击、但是悬浮窗上的EditText会掉不起键盘。


此时悬浮窗外的事件是不会触发悬浮窗内ViewonToucheEvent函数,可以通过添加WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH标志位,但无法拦截事件。




  • WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL


    屏幕上除了悬浮窗外能够点击、弹窗上的EditText也可以输入、键盘能够弹出来。




所以根据业务需要,我只需要添加WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL标志位即可。


2、悬浮窗无法录音


通过Activity调起Service,然后在Service通过WindowManager添加悬浮窗。在没有进行任何操作,正常情况下,可以调起科大讯飞进行录音转成文字发给ChatGPT。


问题点一:同事为了解决我还没来得及修复的windowManger.removeView改成exitProcess问题,强行进行各种修改,最终还调用了activityfinish函数,把activity干掉。最终导致无法调起科大讯飞的语音识别。总是报录音权限问题,找不到任何的问题点,网上资料都说没有给录音权限,其实是有的。最后通过代码回退,定位到是Activity被干掉了,同事也承认他的愚蠢行为。


问题点二:在进行一些操作,例如授权跳转到设置之后,退出设置回到原先界面,科大讯飞调不起录音,还是报权限问题。在有了问题点一的经验后,在Activity的各个生命周期打印日志,发现但onResume函数没有被回调到,也就是应用在后台运行时,该问题必现。


所以就一顿顿顿搜索后,找到官方文档:
Android 9 对后台运行的应用增加了权限限制。


image.png


解决方法:



  1. 声明为系统应用,没问题。但我们想做通用软件。

  2. 增加前台服务。实测没效果。

  3. 在2的基础上,再添加一个属性:android:foregroundServiceType="microphone"。完美。


<service android:name=".ui.service.AiService"
android:foregroundServiceType="microphone"
/>

image.png


希望本文对君有用!


作者:新小梦
来源:juejin.cn/post/7211116982513811516
收起阅读 »

如何进行图片压缩

web
前言 最近要搞图像处理服务,其中一个是要实现图片压缩功能。以前前端开发的时候只要利用canvas现成的API处理下就能实现,后端可能也有现成的API但我并不知道。仔细想想,我从来没有详细了解过图片压缩原理,那刚好趁这次去调研学习下,所以有了这篇文章来记录。老样...
继续阅读 »

前言


最近要搞图像处理服务,其中一个是要实现图片压缩功能。以前前端开发的时候只要利用canvas现成的API处理下就能实现,后端可能也有现成的API但我并不知道。仔细想想,我从来没有详细了解过图片压缩原理,那刚好趁这次去调研学习下,所以有了这篇文章来记录。老样子,如有不对的地方,DDDD(带带弟弟)。


我们先把图片上传到后端,看看后端接收了什么样的参数。这里后端我用的是Node.js(Nest),图片我以PNG图片为例。


接口和参数打印如下:


@Post('/compression')
@UseInterceptors(FileInterceptor('file'))
async imageCompression(@UploadedFile() file: Express.Multer.File) {

return {
file
}
}


要进行压缩,我们就需要拿到图像数据。可以看到,唯一能藏匿图像数据的就是这串buffer。那这串buffer描述了什么,就需要先弄清什么是PNG。


PNG


这里是PNG的WIKI地址。


阅读之后,我了解到PNG是由一个8 byte的文件头加上多个的块(chunk)组成。示意图如下:



其中:


文件头是由一个被称为magic number的组成。值为 89 50 4e 47 0d 0a 1a 0a(16进制)。它标记了这串数据是PNG格式。


块分为两种,一种叫关键块(Critical chunks),一种叫辅助块(Ancillary chunks)。关键块是必不可少的,没有关键块,解码器将不能正确识别并展示图片。辅助块是可选的,部分软件在处理图片之后就有可能携带辅助块。每个块都是四部分组成:4 byte 描述这个块的内容有多长,4 byte 描述这个块的类型是什么,n byte 描述块的内容(n 就是前面4 byte 值的大小,也就是说,一个块最大长度为28*4),4 byte CRC校验检查块的数据,标记着一个块的结束。其中,块类型的4 byte 的值为4个acsii码,第一个字母大写表示是关键块小写表示是辅助块;第二个字母大写表示是公有小写表示是私有;第三个字母必须是大写,用于PNG后续的扩展;第四个字母表示该块不识别时,能否安全复制,大写表示未修改关键块时才能安全复制,小写表示都能安全复制。PNG官方提供很多定义的块类型,这里只需要知道关键块的类型即可,分别是IHDR,PLTE,IDAT,IEND。


IHDR


PNG要求第一个块必须是IHDR。IHDR的块内容是固定的13 byte,包含了图片的以下信息:


宽度 width (4 byte) & 高度 height (4 byte)


位深 bit depth (1 byte,值为1,2,4,8或者16) & 颜色类型 color type (1 byte,值为0,2,3,4或者6)


压缩方法 compression method (1 byte,值为0) & 过滤方式 filter method (1 byte,值为0)


交错方式 interlace method (1 byte,值为0或者1)


宽度和高度很容易理解,剩下的几个好像都很陌生,接下来我将进行说明。


在说明位深之前,我们先来看颜色类型,颜色类型有5种值:




  • 0 表示灰度(grayscale)它只有一个通道(channel),看成rgb的话,可以理解它的三色通道值是相等的,所以不需要多余两个通道表示。




  • 2 表示真实色彩(rgb)它有三个通道,分别是R(红色),G(绿色),B(蓝色)。




  • 3 表示颜色索引(indexed)它也只有一个通道,表示颜色的索引值。该类型往往配备一组颜色列表,具体的颜色是根据索引值和颜色列表查询得到的。




  • 4 表示灰度和alpha 它有两个通道,除了灰度的通道外,多了一个alpha通道,可以控制透明度。




  • 6 表示真实色彩和alpha 它有四个通道。




之所以要说到通道,是因为它和这里的位深有关。位深的值就定义了每个通道所占的位数(bit)。位深跟颜色类型组合,就能知道图片的颜色格式类型和每个像素所占的内存大小。PNG官方支持的组合如下表:


2023-03-17_180115.png


过滤和压缩是因为PNG中存储的不是图像的原始数据,而是处理后的数据,这也是为什么PNG图片所占内存较小的原因。PNG使用了两步进行了图片数据的压缩转换。


第一步,过滤。过滤的目的是为了让原始图片数据经过该规则后,能进行更大的压缩比。举个例子,如果有一张渐变图片,从左往右,颜色依次为[#000000, #000001, #000002, ..., #ffffff],那么我们就可以约定一条规则,右边的像素总是和它前一个左边的像素进行比较,那么处理完的数据就变成了[1, 1, 1, ..., 1],这样是不是就能进行更好的压缩。PNG目前只有一种过滤方式,就是基于相邻像素作为预测值,用当前像素减去预测值。过滤的类型一共有五种,(目前我还不知道这个类型值在哪里存储,有可能在IDAT里,找到了再来删除这条括号里的已确定该类型值储存在IDAT数据中)如下表所示:


Type byteFilter namePredicted value
0不做任何处理
1Sub左侧相邻像素
2Up上方相邻像素
3AverageMath.floor((左侧相邻像素 + 上方相邻像素) / 2)
4Paeth取(左侧相邻像素 + 上方相邻像素 - 左上方像素)最接近的值

第二步,压缩。PNG也只有一种压缩算法,使用的是DEFLATE算法。这里不细说,具体看下面的章节。


交错方式,有两种值。0表示不处理,1表示使用Adam7 算法进行处理。我没有去详细了解该算法,简单来说,当值为0时,图片需要所有数据都加载完毕时,图片才会显示。而值为1时,Adam7会把图片划分多个区域,每个区域逐级加载,显示效果会有所优化,但通常会降低压缩效率。加载过程可以看下面这张gif图。



PLTE


PLTE的块内容为一组颜色列表,当颜色类型为颜色索引时需要配置。值得注意的是,颜色列表中的颜色一定是每个通道8bit,每个像素24bit的真实色彩列表。列表的长度,可以比位深约定的少,但不能多。比如位深是2,那么22,最多4种颜色,列表长度可以为3,但不能为5。


IDAT


IDAT的块内容是图片原始数据经过PNG压缩转换后的数据,它可能有多个重复的块,但必须是连续的,并且只有当上一个块填充满时,才会有下一个块。


IEND


IEND的块内容为0 byte,它表示图片的结束。


阅读到这里,我们把上面的接口改造一下,解析这串buffer。


@Post('/compression')
@UseInterceptors(FileInterceptor('file'))
async imageCompression(@UploadedFile() file: Express.Multer.File) {
const buffer = file.buffer;

const result = {
header: buffer.subarray(0, 8).toString('hex'),
chunks: [],
size: file.size,
};

let pointer = 8;
while (pointer < buffer.length) {
let chunk = {};
const length = parseInt(buffer.subarray(pointer, pointer + 4).toString('hex'), 16);
const chunkType = buffer.subarray(pointer + 4, pointer + 8).toString('ascii');
const crc = buffer.subarray(pointer + length, pointer + length + 4).toString('hex');
chunk = {
...chunk,
length,
chunkType,
crc,
};

switch (chunkType) {
case 'IHDR':
const width = parseInt(buffer.subarray(pointer + 8, pointer + 12).toString('hex'), 16);
const height = parseInt(buffer.subarray(pointer + 12, pointer + 16).toString('hex'), 16);
const bitDepth = parseInt(
buffer.subarray(pointer + 16, pointer + 17).toString('hex'),
16,
);
const colorType = parseInt(
buffer.subarray(pointer + 17, pointer + 18).toString('hex'),
16,
);
const compressionMethod = parseInt(
buffer.subarray(pointer + 18, pointer + 19).toString('hex'),
16,
);
const filterMethod = parseInt(
buffer.subarray(pointer + 19, pointer + 20).toString('hex'),
16,
);
const interlaceMethod = parseInt(
buffer.subarray(pointer + 20, pointer + 21).toString('hex'),
16,
);

chunk = {
...chunk,
width,
height,
bitDepth,
colorType,
compressionMethod,
filterMethod,
interlaceMethod,
};
break;
case 'PLTE':
const colorList = [];
const colorListStr = buffer.subarray(pointer + 8, pointer + 8 + length).toString('hex');
for (let i = 0; i < colorListStr.length; i += 6) {
colorList.push(colorListStr.slice(i, i + 6));
}
chunk = {
...chunk,
colorList,
};
break;
default:
break;
}
result.chunks.push(chunk);
pointer = pointer + 4 + 4 + length + 4;
}

return result;
}


这里我测试用的图没有PLTE,刚好我去TinyPNG压缩我那张测试图之后进行上传,发现有PLTE块,可以看一下,结果如下图。



通过比对这两张图,压缩图片的方式我们也能窥探一二。


PNG的压缩


前面说过,PNG使用的是一种叫DEFLATE的无损压缩算法,它是Huffman Coding跟LZ77的结合。除了PNG,我们经常使用的压缩文件,.zip,.gzip也是使用的这种算法(7zip算法有更高的压缩比,也可以了解下)。要了解DEFLATE,我们首先要了解Huffman Coding和LZ77。


Huffman Coding


哈夫曼编码忘记在大学的哪门课接触过了,它是一种根据字符出现频率,用最少的字符替换出现频率最高的字符,最终降低平均字符长度的算法。


举个例子,有字符串"ABCBCABABADA",如果按照正常空间存储,所占内存大小为12 * 8bit = 96bit,现对它进行哈夫曼编码。


1.统计每个字符出现的频率,得到A 5次 B 4次 C 2次 D 1次


2.对字符按照频率从小到大排序,将得到一个队列D1,C2,B4,A5


3.按顺序构造哈夫曼树,先构造一个空节点,最小频率的字符分给该节点的左侧,倒数第二频率的字符分给右侧,然后将频率相加的值赋值给该节点。接着用赋值后节点的值和倒数第三频率的字符进行比较,较小的值总是分配在左侧,较大的值总是分配在右侧,依次类推,直到队列结束,最后把最大频率和前面的所有值相加赋值给根节点,得到一棵完整的哈夫曼树。


4.对每条路径进行赋值,左侧路径赋值为0,右侧路径赋值为1。从根节点到叶子节点,进行遍历,遍历的结果就是该字符编码后的二进制表示,得到:A(0)B(11)C(101)D(100)。


完整的哈夫曼树如下(忽略箭头,没找到连线- -!):



压缩后的字符串,所占内存大小为5 * 1bit + 4 * 2bit + 2 * 3bit + 1 * 3bit = 22bit。当然在实际传输过程中,还需要把编码表的信息(原始字符和出现频率)带上。因此最终占比大小为 4 * 8bit + 4 * 3bit(频率最大值为5,3bit可以表示)+ 22bit = 66bit(理想状态),小于原有的96bit。


LZ77


LZ77算法还是第一次知道,查了一下是一种基于字典和滑动窗的无所压缩算法。(题外话:因为Lempel和Ziv在1977年提出的算法,所以叫LZ77,哈哈哈😂)


我们还是以上面这个字符串"ABCBCABABADA"为例,现假设有一个4 byte的动态窗口和一个2byte的预读缓冲区,然后对它进行LZ77算法压缩,过程顺序从上往下,示意图如下:



总结下来,就是预读缓冲区在动态窗口中找到最长相同项,然后用长度较短的标记来替代这个相同项,从而实现压缩。从上图也可以看出,压缩比跟动态窗口的大小,预读缓冲区的大小和被压缩数据的重复度有关。


DEFLATE


DEFLATE【RFC 1951】是先使用LZ77编码,对编码后的结果在进行哈夫曼编码。我们这里不去讨论具体的实现方法,直接使用其推荐库Zlib,刚好Node.js内置了对Zlib的支持。接下来我们继续改造上面那个接口,如下:


import * as zlib from 'zlib';

@Post('/compression')
@UseInterceptors(FileInterceptor('file'))
async imageCompression(@UploadedFile() file: Express.Multer.File) {
const buffer = file.buffer;

const result = {
header: buffer.subarray(0, 8).toString('hex'),
chunks: [],
size: file.size,
};

// 因为可能有多个IDAT的块 需要个数组缓存最后拼接起来
const fileChunkDatas = [];
let pointer = 8;
while (pointer < buffer.length) {
let chunk = {};
const length = parseInt(buffer.subarray(pointer, pointer + 4).toString('hex'), 16);
const chunkType = buffer.subarray(pointer + 4, pointer + 8).toString('ascii');
const crc = buffer.subarray(pointer + length, pointer + length + 4).toString('hex');
chunk = {
...chunk,
length,
chunkType,
crc,
};

switch (chunkType) {
case 'IHDR':
const width = parseInt(buffer.subarray(pointer + 8, pointer + 12).toString('hex'), 16);
const height = parseInt(buffer.subarray(pointer + 12, pointer + 16).toString('hex'), 16);
const bitDepth = parseInt(
buffer.subarray(pointer + 16, pointer + 17).toString('hex'),
16,
);
const colorType = parseInt(
buffer.subarray(pointer + 17, pointer + 18).toString('hex'),
16,
);
const compressionMethod = parseInt(
buffer.subarray(pointer + 18, pointer + 19).toString('hex'),
16,
);
const filterMethod = parseInt(
buffer.subarray(pointer + 19, pointer + 20).toString('hex'),
16,
);
const interlaceMethod = parseInt(
buffer.subarray(pointer + 20, pointer + 21).toString('hex'),
16,
);

chunk = {
...chunk,
width,
height,
bitDepth,
colorType,
compressionMethod,
filterMethod,
interlaceMethod,
};
break;
case 'PLTE':
const colorList = [];
const colorListStr = buffer.subarray(pointer + 8, pointer + 8 + length).toString('hex');
for (let i = 0; i < colorListStr.length; i += 6) {
colorList.push(colorListStr.slice(i, i + 6));
}
chunk = {
...chunk,
colorList,
};
break;
case 'IDAT':
fileChunkDatas.push(buffer.subarray(pointer + 8, pointer + 8 + length));
break;
default:
break;
}
result.chunks.push(chunk);
pointer = pointer + 4 + 4 + length + 4;
}

const originFileData = zlib.unzipSync(Buffer.concat(fileChunkDatas));

// 这里原图片数据太长了 我就只打印了长度
return {
...result,
originFileData: originFileData.length,
};
}


最终打印的结果,我们需要注意红框的那几个部分。可以看到上图,位深和颜色类型决定了每个像素由4 byte组成,然后由于过滤方式的存在,会在每行的第一个字节进行标记。因此该图的原始数据所占大小为:707 * 475 * 4 byte + 475 * 1 byte = 1343775 byte。正好是我们打印的结果。


我们也可以试试之前TinyPNG压缩后的图,如下:



可以看到位深为8,索引颜色类型的图每像素占1 byte。计算得到:707 * 475 * 1 byte + 475 * 1 byte = 336300 byte。结果也正确。


总结


现在再看如何进行图片压缩,你可能很容易得到下面几个结论:


1.减少不必要的辅助块信息,因为辅助块对PNG图片而言并不是必须的。


2.减少IDAT的块数,因为每多一个IDAT的块,就多余了12 byte。


3.降低每个像素所占的内存大小,比如当前是4通道8位深的图片,可以统计整个图片色域,得到色阶表,设置索引颜色类型,降低通道从而降低每个像素的内存大小。


4.等等....


至于JPEG,WEBP等等格式图片,有机会再看。溜了溜了~(还是使用现成的库处理压缩吧)。


好久没写文章,写完才发现语雀不能免费共享,发在这里吧。


作者:月下风物语
来源:juejin.cn/post/7211434247146782775
收起阅读 »

就目前前端行业现状浅析与今后前端发展探讨

前端目前现状 目前前端技术发展来看,ThreeJs已是一个不可忽视的技术点,在目前很多领域已经有3D案例在应用,更多的甲方也乐意让自己的产品更加的炫酷,功能更加的炫丽,所以前端在3D的技术领域已经是相当丰富,在地图应用方面,场景展示方面,产品介绍方面,3D...
继续阅读 »

前端目前现状




  • 目前前端技术发展来看,ThreeJs已是一个不可忽视的技术点,在目前很多领域已经有3D案例在应用,更多的甲方也乐意让自己的产品更加的炫酷,功能更加的炫丽,所以前端在3D的技术领域已经是相当丰富,在地图应用方面,场景展示方面,产品介绍方面,3D效果远比传统页面更有吸引力。




  • 前端已死: 造成这种观点的影响其实是有很多原因的,其中最主要的原因我认为是这3年以来的影响,让很多行业都开始下沉,而互联网行业相对来说在这3年以来的影响相对较小,所以在当时没有这么明显。而在这三年以来,很多各行各业的人员也开始转向互联网行业,而前端是学习成本、入门成本最低的一行,所以很多培训班开始大量招收想要入行互联网的人员,这一点从B站的前端教学视频上可以看得出来,跟后端相比,前端的课程更加的多、内容也更加的丰富。但是这么多人涌入了前端行业,必然会造成前端求职困难的情况。但是从长远的角度来看,这对前端行业其实是更加的有利的,更多的人员选择了前端,会让前端的生态更加的丰富,各种知识、观点的碰撞,也会让前端行业进入下一个加速发展期,前端未来可期。




  • 技术权衡方面: 随着VUE与React两家的影响力逐渐扩大,留给区域框架的发展空间其实已经不多,一方面是项目维护成本会逐渐增加,冷门的技术在招人方面会更加的难招聘到合适的人才。VUE相对来说,相对React来说会更加简单一点,按这种观点来看,以后应该是Vue一家独大,但是从目前Vue3的技术发展来看,未来很有可能会出现一种情况,就是Vue与React合并在一起,因为从两种技术架构来看,其实两者的区别并不大,将两个框架进行合并在一起,这样的话,对前端今后的发展也是更加有利的,期待这一天的到来。




  • 流媒体现状:其实现阶段,前端在流媒体技术上的处理已经非常的完善,直播、音频、图像、文件处理等,在困难程度上来说,其实后端的压力会更大一些,所以目前前端来说,流媒体这一块难的东西基本上都已经得到解决,很多成熟的开源应用已经可以帮助很多人完成项目的开发,后端的开发者们仍需努力。




  • 兼容性问题,其实到了2023年,对于绝大部分的开发者来说,没有了IE,很多兼容性问题已经不需要再去关注,学习的成本也相对减少,这是前端行业的一种进步。




今后发展探讨




  • 3D应用: 3D应用在今后两年的成长应该会得到更多的加速,3D技术也是前端技术的一个分水岭,在今后,可能会将不会3D与会3D来对前端技术评价进行区分,所以我个人认为,3D技术值得现在每一个前端开发者去学习。




  • VUE与React: 这两款框架各有所长,目前来说,中小型项目用vue的选择会更多一点,用React的项目基本上是大型项目,但是在今后,我认为随着Typescript的发展,Vue在大型项目上会与React一样受欢迎。




  • Typescript: 随着Typescript的发展,在今后,很多开发者会拥抱ts,渐渐减少使用js,这应该是一个不可逆转的趋势,除非js有非常大的发展,不然ts必然会成为今后前端开发者入门学习的直接语言,可以直接跳过js去学习ts。




  • NodeJs: nodejs仍有很长的路要走,这是必然的,目前nodejs的生态并不完善,在开源方面与学习成本方面都是非常高的,这一点得不到解决的话,Nodejs就仍是鸡肋。




  • 技术的学习成本:随着现在的微服务、uniapp、webpack、cssScript开源项目越来越多,各位前端从业人员需要学习的技术也越来越多,随着学习成本的逐渐增加,这对前端的生态发展其实是不利的,如果不是很明确需要这种技术,我个人认为是不需要去盲目学习的,因为很多新兴的技术,文档的不完善、案例的不完善,能不能善始善终都是一个问题。




总结


该文章仅为我个人观点,如有不对,请各位批评指正。


作者:无我Code
来源:juejin.cn/post/7211801284709974077
收起阅读 »

摸鱼两天,彻底拿下虚拟滚动!

web
总结 通过自己的实践发现,网上相传的虚拟滚动实现方案有种是行不通的(涉及浏览器机制) 实现虚拟滚动,滚动元素中利用上下两个只有高度的空盒子撑开空间是不可行的 html布局示意: <div class="content-container"> ...
继续阅读 »

总结


通过自己的实践发现,网上相传的虚拟滚动实现方案有种是行不通的(涉及浏览器机制)




  • 实现虚拟滚动,滚动元素中利用上下两个只有高度的空盒子撑开空间是不可行的


    html布局示意


    <div class="content-container">
     <div class="top-padding"></div>

     <div class="content-item"></div>
     <div class="content-item"></div>
     <div class="content-item"></div>

     <div class="bottom-padding"></div>
    </div>



  • 可行方案:


    html布局示意


    <div class="scroll-container">
     <div class="content-container">
       <div class="content-item"></div>
      ...
       <div class="content-item"></div>
     </div>
    </div>



如果您和我一样,想自己实现一下虚拟滚动,下面 实现虚拟滚动 部分 中我会尽可能保姆级详细的复现我当时写代码的所有过程(包括建项目...),适合新手(但是不能是小白,需要知道虚拟滚动是干啥的东西,因为我没有去介绍虚拟滚动)。


如果您对这玩意的实现完全没啥好奇的,可以看看 部分,我详细记录了一个关于浏览器滚动条的特点,或许对你来说有点意思。


实现虚拟滚动


下面用vue3写一个demo,并没封装多完善,也不是啥生产可用的东西,但绝对让你清晰虚拟滚动的实现思路。


项目搭建


pnpm create vite创建一个项目,项目名、包名输入virtualScrollDemo,选择技术栈Vue + TypeScript;再简单安装个less,即pnpm install less less-loader -D,然后配一下vite.config.ts,顺便给src配个别名。


vite.config.ts


import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { resolve } from "path"; // 让ts识别模块,这里还需要 pnpm i @types/node

// https://vitejs.dev/config/
export default defineConfig({
 plugins: [vue()],
 css: {
   preprocessorOptions: {
     less: {
    },
  },
},
 resolve: {
   alias: [
    {
       find: "@",
       replacement: resolve(__dirname, "/src"),
    },
  ],
},
});

App.vueimport VirtualScroll from '@/components/VirtualScroll.vue'还是报错,ts还要配置别名才行,tsconfig.json中加一下baseUrlpaths即可


tsconfig.json


{
 "compilerOptions": {
   "target": "ESNext",
   "useDefineForClassFields": true,
   "module": "ESNext",
   "moduleResolution": "Node",
   "strict": true,
   "jsx": "preserve",
   "resolveJsonModule": true,
   "isolatedModules": true,
   "esModuleInterop": true,
   "lib": ["ESNext", "DOM"],
   "skipLibCheck": true,
   "noEmit": true,
   "baseUrl": "./",
   "paths": {
     "@/*": ["src/*"]
  }
},
 "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
 "references": [{ "path": "./tsconfig.node.json" }]
}

然后项目删一删没用的就成了这样:


src/
├── App.vue
├── components/
│   └── VirtualScroll.vue
└── shared/
  └── dataConstant.ts

dataConstant.ts是准备的一个长列表渲染的数据源:


export const dataSource = [
{
   text: "jrd",
},
{
   text: "jrd1",
},
 ...
]

结构搭建


为了突出重点,实现虚拟滚动逻辑必要的样式我都写在:style中了,辅助性的样式都写在<style></style>


先把长列表搭建出来:


基本长列表.gif


<template>
 <div
   class="scroll-container"
   :style="{
     overflow: 'auto',
     height: `${viewPortHeight}px` // 列表视口高度(值自定义即可)
   }"

 >

 <div
   class="content-container"
   :style="{
     height: `${itemHeight * dataSource.length}px`
   }"

 >

   <div
     class="content-item"
     v-for="(data, index) in dataSource"
   >

     {{ data.text }}
   </div>
 </div>
</div>
</template>

<script lang="ts">
import { defineComponent } from "vue";
import { dataSource } from "@/shared/dataConstant";

export default defineComponent({
 name: "VirtualScroll",
 setup() {
   const viewPortHeight = 500; // 滚动列表的可视高度
   const itemHeight = 50; // 一个列表项的高度
   return {
     viewPortHeight,
     dataSource,
     itemHeight
  }
},
});
</script>

<style scoped lang="less">
.scroll-container {
 border: 2px solid red;
 width: 300px;
 .content-container {
   .content-item {
     height: 50px;
     background-image: linear-gradient(0deg, pink, blue);
  }
}
}

</style>

注释:


html结构三层嵌套,最外层是div.scroll-container,里面依次是div.content-containerdiv.content-item


div.scroll-container容器是出现滚动条的容器,所以它需要一个固定高度(可视区域的高度)以及overflow: auto,这样他内部元素超过了它的高度它才会出现滚动条;div.content-container的作用就是撑开div.scroll-container,解释一下,因为我们最终要的效果是只渲染一小部分元素,单单渲染的这一小部分内容肯定是撑不开div.scroll-container的,所以根据渲染项的多少以及每个渲染项的高度写死div.content-container的高度,不管渲染项目多少,始终保持div.scroll-containerscrollHeight正常。


核心计算


监听div.scroll-container的滚动事件,滚动回调中计算startIndexendIndex,截取数据源(截取要渲染的一小部分数据,即renderDataList = dataSource.slice(startIndex, endIndex)):


计算startIndex和endIndex.gif


<template>
 <div
   class="scroll-container"
   :style="{
     overflow: 'auto',
     height: `${viewPortHeight}px` // 列表视口高度(值自定义即可)
   }"

   ref="scrollContainer"
   @scroll="handleScroll"
 >

 <div
   class="content-container"
   :style="{
     height: `${itemHeight * dataSource.length}px`
   }"

 >

   <div
     class="content-item"
     v-for="(data, index) in dataSource"
   >

     {{ data.text }}
   </div>
 </div>
</div>

</template>

<script lang="ts">
import { defineComponent, ref } from "vue";
import { dataSource } from "@/shared/dataConstant";

export default defineComponent({
 name: "VirtualScroll",
 setup() {
   const viewPortHeight = 525; // 滚动列表的可视高度
   const itemHeight = 50; // 一个列表项的高度
   const startIndex = ref(0);
   const endIndex = ref(0);
   const scrollContainer = ref<HTMLElement | null>(null);
   const handleScroll = () => {
     if(!scrollContainer.value) return
     const scrollTop = scrollContainer.value.scrollTop;
     startIndex.value = Math.floor(scrollTop / itemHeight);
     endIndex.value = Math.ceil((scrollTop + viewPortHeight) / itemHeight) - 1;
     console.log(startIndex.value, endIndex.value);
  }
   return {
     viewPortHeight,
     dataSource,
     itemHeight,
     scrollContainer,
     handleScroll
  }
},
});
</script>

<style scoped lang="less">
.scroll-container {
 border: 2px solid red;
 width: 300px;
 .content-container {
   .content-item {
     height: 50px;
     background-image: linear-gradient(0deg, pink, blue);
  }
}
}

</style>

注释:


startIndexendIndex我们都按照从0开始(而非1开始)的标准来计算。 startIndex对应div.scroll-container上边界压住的div.content-itemindexendIndex对应div.scroll-container下边界压住的div.content-itemindex,也就是说,startIndexendIndex范围内的数据,是我们在保证可视区域不空白的前提下至少要进行渲染的数据,我可能表述不很清楚,静心想一想不难理解的。


收尾


最后的两步就是根据startIndexendIndexdataSource中动态截取出来renderDataListv-for只渲染renderDataList,然后把渲染出来的div.content-item通过定位 + transform移动到正确的位置即可了。


监听startIndexendIndex,变化时修改renderDataList


逻辑:


// 因为slice函数是左闭右开,所以截取时为endIndex.value + 1
const renderDataList = ref(dataSource.slice(startIndex.value, endIndex.value + 1));
watch(() => startIndex.value, () => {
renderDataList.value = dataSource.slice(startIndex.value, endIndex.value + 1);
})
watch(() => endIndex.value, () => {
renderDataList.value = dataSource.slice(startIndex.value, endIndex.value + 1);
})

结构:


<div 
class="content-item"
v-for="(data, index) in renderDataList"
>
{{ data.text }}
</div>

这时候,数据已经正确渲染了,只是位置还不太对


效果:


数据结构正确渲染.gif


我们要做的就是通过css把渲染出来的dom移动到正确的位置,这里采取的方案就是div.content-container相对定位,div.content-item绝对定位,并且topleft都设置为0(所有都移动到左上角),然后通过translate: transformY把它们移动到“正确”的位置:


结构:


<div 
class="content-item"
v-for="(data, index) in renderDataList"
:style="{
position: 'absolute',
top: '0',
left: '0',
transform: `translateY(${(startIndex + index) * itemHeight}px)`
}"
>
{{ data.text }}
</div>

经过上面的修改之后已经基本收工了,不知道是哪个样式的原因div.content-item的宽度不是100%了,手动加上就好了


效果:


虚拟滚动大功告成.gif


优化



  1. 给滚动事件添加节流

  2. 引入缓冲结点数变量countOfBufferItem,适当扩充(startIndex, endIndex)渲染区间,防止滑动过快出现空白


最终代码:


<template>
<div
class="scroll-container"
:style="{
overflow: 'auto',
height: `${viewPortHeight}px` // 列表视口高度(值自定义即可)
}"

ref="scrollContainer"
@scroll="handleScroll"
>

<div
class="content-container"
:style="{
height: `${itemHeight * dataSource.length}px`,
position: 'relative'
}"

>

<div
class="content-item"
v-for="(data, index) in renderDataList"
:style="{
position: 'absolute',
top: '0',
left: '0',
transform: `translateY(${(startIndex + index) * itemHeight}px)`
}"

>

{{ data.text }}
</div>
</div>
</div>

</template>

<script lang="ts">
import { defineComponent, ref, watch } from "vue";
import { dataSource } from "@/shared/dataConstant";

export default defineComponent({
name: "VirtualScroll",
setup() {
const viewPortHeight = 525; // 滚动列表的可视高度
const itemHeight = 50; // 一个列表项的高度
const startIndex = ref(0);
const endIndex = ref(Math.ceil(viewPortHeight / itemHeight) - 1);
const scrollContainer = ref<HTMLElement | null>(null);

let isHandling = false; // 节流辅助变量
const countOfBufferItem = 2; // 缓冲结点数量
const handleScroll = () => {
if(isHandling) return;
isHandling = true;
setTimeout(() => {
if(!scrollContainer.value) return
const scrollTop = scrollContainer.value.scrollTop;
startIndex.value = Math.floor(scrollTop / itemHeight);
startIndex.value = startIndex.value - countOfBufferItem >= 0 ? startIndex.value - countOfBufferItem : 0; // 扩充渲染区间
endIndex.value = Math.ceil((scrollTop + viewPortHeight) / itemHeight) - 1;
endIndex.value = endIndex.value + countOfBufferItem >= dataSource.length - 1 ? dataSource.length - 1 : endIndex.value + countOfBufferItem; // 扩充渲染区间
isHandling = false;
}, 30)
}

const renderDataList = ref(dataSource.slice(startIndex.value, endIndex.value + 1));
watch(() => startIndex.value, () => {
renderDataList.value = dataSource.slice(startIndex.value, endIndex.value + 1);
})
watch(() => endIndex.value, () => {
renderDataList.value = dataSource.slice(startIndex.value, endIndex.value + 1);
})
return {
viewPortHeight,
dataSource,
itemHeight,
scrollContainer,
handleScroll,
renderDataList,
startIndex,
endIndex
}
},
});
</script>

<style scoped lang="less">
.scroll-container {
border: 2px solid red;
width: 300px;
.content-container {
.content-item {
height: 50px;
background-image: linear-gradient(0deg, pink, blue);
width: 100%;
}
}
}

</style>

虽说没啥bug吧,但是滚动的快了还是有空白啥的,这应该也算是这个技术方案的瓶颈。



bug复现


我一开始的思路是一个外层div.container,设置overflow: hidden,然后内部上中下三部分,上面一个空盒子,高度为startIndex * listItemHeight;中间部分为v-for渲染的列表,下面又是一个空盒子,高度(dataSource.length - endIndex - 1) * listItemHeight,总之三部分的总高度始终维持一个定值,即这个值等于所有数据完全渲染时div.containerscrollHeight


实现之后,问题出现了:


不受控制的滚动.gif


一旦触发了“机关”,滚动条就会不受控制的滚动到底


我把滚动回调的节流时间设置长为500ms


不受控制的滚动-长节流.gif


发现滚动条似乎陷入了一种循环之中,每次向下移动一个数据块的高度。 分析这个现象,需要下面一些关于滚动条特性的认知。


滚动条的特性


先给结论:当一个定高(scrollHeight固定)的滚动元素,其(撑开其高度的)子元素高度发生变化时(高度组成发生变化,比如一个变高,一个变低,但保持滚动元素的scrollHeight总高度不变),滚动条位置也会发生变化,变化遵循一个原则:保持当前可视区域展示的元素在可视区域内位置不变。


写个demo模拟一下上面说的场景,div.container是一个滚动且定高的父元素,点击按钮后其内部的div.top变高,div.bottom变矮


Test.vue:


<template>
<div class="container" ref="container">
<div
class="top"
:style="{
height: `${topHeight}px`,
}"

>
</div>
<div class="content"></div>
<div
class="bottom"
:style="{
height: `${bottomHeight}px`,
}"

>
</div>
</div>

<button @click="test">按钮</button>
</template>

<script lang="ts">
import { defineComponent, ref } from "vue";

export default defineComponent({
setup() {
const topHeight = ref(300);
const bottomHeight = ref(300);
const container = ref(null);
const test = () => {
topHeight.value += 50;
bottomHeight.value -= 50;
};
return {
topHeight,
bottomHeight,
test,
container,
};
},
});
</script>


<style scoped lang="less">
.container {
width: 200px;
height: 600px;
overflow: auto;
border: 1px solid green;
.top {
width: 100%;
border: 3px solid red;
}
.content {
height: 1000px;
}
.bottom {
width: 100%;
border: 3px solid black;
}
}
</style>


滚动条位置变化demo展示


仔细观察滚动条:


滚动条位置变化demo.gif


解释一下上图,首先是上面一个红色盒子,底部一个黑色盒子:



  • 我们可视区域的左上角在红色区域时点击按钮,这时候浏览器底层判断我们正在浏览红色元素,所以虽然内部元素高度变化,但我们的可视区域相对于红色盒子左上角的位置不变

  • 第一次刷新之后,我们可视区域的左上角在中间盒子上,这时候我们点击按钮,红色盒子高度增加,黑色盒子高度减小,中间盒子的相对整个滚动区域的位置就靠下了,但是——浏览器的滚动条也随之向下移动了(而且,滚动条向下移动的距离 === 红色盒子高度增加值 === 黑色盒子高度减小值 === 中间盒子相对滚动区域向下偏移值

  • 第二次刷新后,更直观的表现了滚动条的这个特点:我把滚动条恰好移动到中间盒子上,上面紧邻红色盒子,点击三次按钮后,滚动条下移三次,此时我向上滚动一点,接着看到了红色盒子。


bug原因分析


有了上面的认知,再来看这个图


不受控制的滚动-长节流.gif


bug的“生命周期”:


1.我们手动向下滚动滚动条 ——> 2.内部计算(startIndex以及endIndex的改变)触发上方占位的<div>元素高度增加,下方占位<div>高度减小,中间渲染的内容部分整体位置相对于整个滚动元素下移 ——> 3.(浏览器为了保持当前可视区域展示的元素在可视区域内位置不变)滚动条自动下移 ——> 4.触发新的计算 ——> 2.


感慨:上中下三个部分,上下动态修改高度占位,中间部分渲染数据,思路多么清晰的方案,但谁能想到浏览器滚动条出来加了道菜呢


网上不少地方都给了这个方案...


成功的虚拟滚动、带bug的虚拟滚动和测试组件的源码我都放到这里了,需要的话可以去clone:github.com/jinrd123/Vi…(带bug的虚拟滚动是我第一次实现时随性写的,代码组织以及注释可能不是很规范)


作者:荣达
来源:juejin.cn/post/7211088034179366973
收起阅读 »

GPT-4的出现对前端入门者意味着什么?

嗨!我是团子,大家好久不见~ 3月15日凌晨,由OpenAI发布的多模态预训练大模型GPT-4一经推出就引起了大家的激烈讨论,让最近很火的ChatGPT烧的愈发旺盛。 在GPT-4中,一个重大的突破是支持多模态输入。也就是说,除了文字输入外,也支持图像、...
继续阅读 »

嗨!我是团子,大家好久不见~


3月15日凌晨,由OpenAI发布的多模态预训练大模型GPT-4一经推出就引起了大家的激烈讨论,让最近很火的ChatGPT烧的愈发旺盛。







在GPT-4中,一个重大的突破是支持多模态输入。也就是说,除了文字输入外,也支持图像、视频、音频等从多个角度出发对事物进行描述。







OpenAI在发布会上演示了几种多模态输入的案例,包括读论文、解物理题、看图说话...







甚至可以根据一张草图十秒钟编写出一个网页!(再智能就真的不礼貌了.jpg)







这让致力于成为前端工程师的我们有点坐不住了。。


大家在各个群里激烈讨论,有人在担心ChatGPT会抢饭碗,也有人为ChatGPT的新升级感到兴奋,甚至开始期待16号百度文心一言的发布会。


因此,今天就想和大家来聊聊GPT-4的出现对前端入门者意味着什么。


陷入沉思


诚然,GPT-4根据一张草图10秒钟制作出一个网页的能力让人感到震撼,但也让前端入门者陷入了沉思:





  1. 1.既然GPT可以很快且较为准确的实现一个网页,那还需要初级前端工程师吗?

  2. 2.初级前端工程师的岗位真的那么好替代吗?

  3. 3.如果不做前端工程师的话,什么岗位是GPT无法替代的呢?

  4. 4.作为初级前端工程师来说,现在做什么才能容灾GPT带来的影响呢?




思考了很久,个人认为GPT的出现确实将信息科技的发展推到了一个新的高度,但感到恐慌的不应该只有前端工程师,对于初级后端工程师甚至任何可以由人工智能胜任的岗位从业者来说,都应该认真思考下在面对GPT这种人工智能时怎么做才能体现自己的价值。


GPT的出现给前端带来了什么影响


1. 门槛变高


在求职面试中,很多面试官都喜欢问面试者的一个问题是:你为什么要做前端。这个问题其实不止是前端,对于任何岗位的求职者来说,都是一个需要认真思考的问题。


早些年互联网快速发展时,程序员的职业受到大家的追捧,很多其他行业和专业的人成功通过自学成为了一名程序员,实现了相对的财富自由。由此可见,早些年程序员的入门门槛其实不高的,行业壁垒也不强。而前端工程师的岗位也因为相比于Java、C++等岗位更容易上手成为培训班割韭菜的首选方向。


但随着互联网近两年发展进入一个瓶颈期以来,前端程序员甚至其他岗位程序员的需求量也逐渐趋于饱和。再加上GPT的出现,对于一些基础且重复的工作,确实不再像早年那样需要很多的人力了,但是想要涌入互联网的人还是很多,因此前端工程师甚至是其他岗位工程师的门槛也会变高。


2. 趋向于精英化


正如前面所述,GPT的出现加上岗位需求量的减少使得前端工程师的从业门槛变高,这带来的结果其实就是让整个互联网行业向着精英化发展。


记得2010年以前,大家在谈论到计算机时,都会觉得这是一个很高端的技术,认为从事计算机岗位的人智商超群,那会上电脑课听老师讲二进制编码都觉得很神奇。


感觉2010年以后互联网快速膨胀的十年,使得现在的互联网行业也慢慢的回归了理性,大家的关注点更多是优化、创新。因此对于互联网从业者的要求也会逐渐变高,不仅仅是求职者的优中选优,对于已经是程序员的从业者来说,也需要持续输入来提高自己的竞争力。


如何应对GPT带给行业的冲击


1. 将GPT具备的技能转换为自己的技能


坦白说,在发现GPT可以帮我debug、解答我各种不理解的专业问题时,我是非常兴奋的!因为这代表着我的工作效率会变高。


某种程度上说,它能让我更快速的适应程序员这份工作,甚至可以去探索更多的领域,包括:后端、客户端、算法、测试等等,再或者医生、律师、作词家、小说家等等。


但是探索这些领域的前提是我们要成为GPT的主人,将自己的需求准确的发布给GPT。


坦白说,GPT确实是一个很好的回答者,但是如何提问才是解决问题的前提!


2. 利用GPT去做更多的创新和思考


在GPT没有大量普及的时候,会用GPT的人具有更多的优势。


但是当GPT成为像手机一样的日常用品时,会用GPT就不再是核心竞争力了。


因此,我们需要多思考如何基于GPT的能力去做更多有意义的事情,这可能才会让我们的路走的更远吧!


总结


GPT的出现不是为了让更多的人失业,而是为了让这个世界变得更好。


在时代的洪流下,消极内耗并不能改变现状,如何顺应时代的发展,拿出积极的态度去跟上时代的脚步才是我们应该思考的问题!


作者:程序员团子
来源:juejin.cn/post/7211064454572982329
收起阅读 »

又一个免费GPT-4工具 Cursor,程序员写代码将被颠覆

每天都被openai震撼到, 他们家被广为人知的产品是chatgpt,就是那个聊天工具。现在已经开始有越来越多的产品集成openai,比如微软的office,bing。 现在又一个工具出现,一个叫Cursor的编辑器已经集成了openai的GPT-4,而且还是...
继续阅读 »

每天都被openai震撼到, 他们家被广为人知的产品是chatgpt,就是那个聊天工具。现在已经开始有越来越多的产品集成openai,比如微软的office,bing。


现在又一个工具出现,一个叫Cursor的编辑器已经集成了openai的GPT-4,而且还是免费的,它将彻底改变我们写代码的方式。 以前不会写代码先去百度google一下。Ctrl+c,Ctrl+v搞定,我们调侃我们都是CV工程师。


以后我们恐怕要成为“KL"工程师,为什么叫”KL“工程师呢, 因为只要K和L两个指令就可以直接生成代码、修改代码,哪行代码不会点哪里,他都给你解释得明明白白。


image-20230319003903733


下载安装


Cursor编辑器提供了Windows、MacOS、Linux 三个平台的安装包,安装包下载链接: pan.baidu.com/s/1Jh3gIrnj… 提取码: dt7m


image-20230319004116280


下载安装完成后,会引导你进行初始化设置,你即可以选择VIM或者Emacs的操作习惯,也可以保持默认设置,另外它还支持绑定Copilot。


我们可以直接用Ctrl+K,输入一个指令:”用Python实现一个猜数字游戏,每次最多3次机会“。 他就会立马直接给你生成好代码:


import random

def guess_number():
number = random.randint(1, 100)
print("我想了一个1到100之间的数字,你最多有3次机会猜对它。")
for i in range(3):
guess = int(input("请输入你猜的数字:"))
if guess == number:
print("恭喜你,猜对了!")
return
elif guess < number:
print("你猜的数字太小了。")
else:
print("你猜的数字太大了。")
print("很遗憾,你没有猜对。我想的数字是:", number)

guess_number()

一行代码都不用修改可直接运行。



如果你对某一行代码不懂,你只要按住Ctrl+L选中它,然后输入关于这行代码你要问的问题。比如我问 name 是什么东西时,他就很智能的告诉了我。


img


不过目前这个编辑器的功能相比vscode还比较简陋,相信很快各大主流编辑器都会集成GPT4


哎,发现这个世界变化的有点太快,LLMs(大规模语言模型)将开启一次伟大的技术革命。


作者:刘志军
来源:juejin.cn/post/7211876919037132856
收起阅读 »

IM实战:Android接入环信 IM SDK

本次学习目标:注册环信;Android端集成环信 IM SDK;实现注册-登录-发送消息-会话列表等;一、环信IM- 什么是环信IM环信IM是一款即时通讯产品,为开发者提供基于移动互联网的即时通讯能力,如单聊、群聊、发语音、发图片、发位置等,让开发者摆脱繁重的...
继续阅读 »

本次学习目标:
注册环信;
Android端集成环信 IM SDK;
实现注册-登录-发送消息-会话列表等;


一、环信IM
- 什么是环信IM

环信IM是一款即时通讯产品,为开发者提供基于移动互联网的即时通讯能力,如单聊、群聊、发语音、发图片、发位置等,让开发者摆脱繁重的移动IM通讯底层开发,24小时即可让App拥有内置IM能力。

- 环信IM可以实现什么?

实现了个人与个人,群组,聊天室发消息,发语音,发图片,发位置等功能,提供了一套供开发者拿来即用的EaseIMKit的ui库;

二、 环信IM实现通讯的基本流程


1、环信账号
a、找到环信im官网,注册一个管理者账号 ,注册地址:https://console.easemob.com/user/register
b、注册成功以后,点击右上角登录 > 登录即时通讯云 ,进入console后台
c、在环信通讯云控制台创建Appkey;


2、Android端接入环信SDK

选择如下任意一种方式将环信即时通讯 IM SDK 集成到你的项目中(以下集成方式只需选择一种,同时使用多种集成方式可能会报错):

方法一:
该方法仅适用于 v3.8.2 或以上版本。

1.在项目的 build.gradle 中添加 mavenCentral()仓库。

buildscript {
repositories {
...
mavenCentral()
}
...
}

allprojects {
repositories {
...
mavenCentral()
}
}


2.在 module 的 build.gradle 中添加如下依赖:

dependencies {
...
// x.y.z 请填写具体版本号,如:3.9.4。
// 可通过 SDK 发版说明获得最新版本号。
implementation 'io.hyphenate:hyphenate-chat:x.x.x'
}


方法二:
手动复制 SDK 文件
打开 SDK 下载页面,获取最新版的环信即时通讯 IM Android SDK,然后解压。



将 SDK 包内 libs 路径下的如下文件,拷贝到你的项目路径下:



3、Android端注册与登录


(1)在主进程中进行初始化:

EMOptions options = new EMOptions();
options.setAppKey("Your appkey");
......// 其他 EMOptions 配置。
EMClient.getInstance().init(context, options);


(2)创建账号

此方法为同步方法,会阻塞当前线程;

此方法只有在开放注册模式下,才能调用。如果此方法报错,请检查下环信管理后台是否是开放注册模式;

// 注册失败会抛出 HyphenateException。
// 同步方法,会阻塞当前线程。
EMClient.getInstance().createAccount(mAccount, mPassword);


(3)登录账号

EMClient.getInstance().login(mAccount, mPassword, new EMCallBack() {
// 登录成功回调
@Override
public void onSuccess() {

}

// 登录失败回调,包含错误信息
@Override
public void onError(final int code, final String error) {

}

@Override
public void onProgress(int i, String s) {

}

});


4、Android端添加好友获取好友列表

(1)添加好友:

// 同步方法,会阻塞当前线程。异步方法为 asyncAddContact(String, String, EMCallBack)。
EMClient.getInstance().contactManager().addContact(toAddUsername, reason);


(2)获取好友列表:

// 从服务器获取好友列表。
// 同步方法,会阻塞当前线程。异步方法为 asyncGetAllContactsFromServer(EMValueCallBack)。
List<String> usernames = EMClient.getInstance().contactManager().getAllContactsFromServer();
// 从本地数据库获取好友列表。
List<String> usernames = EMClient.getInstance().contactManager().getContactsFromLocal();


5、Android端实现发送文本消息

发送一条单聊消息

// `content` 为要发送的文本内容,`toChatUsername` 为对方的账号。
EMMessage message = EMMessage.createTxtSendMessage(content, toChatUsername);
// 发送消息
EMClient.getInstance().chatManager().sendMessage(message);


6、Android端实现接收消息

你可以用注册监听 EMMessageListener 接收消息。

该 EMMessageListener 可以多次添加,请记得在不需要的时候移除 listener,

如在activity 的 onDestroy() 时。

在新消息到来时,你会收到 onMessageReceived 的回调,消息接收时可能是一条,

也可能是多条。你可以在该回调里遍历消息队列,解析并显示收到的消息。

EMMessageListener msgListener = new EMMessageListener() {

// 收到消息,遍历消息队列,解析和显示。
@Override
public void onMessageReceived(List<EMMessage> messages) {

}
};
// 注册消息监听
EMClient.getInstance().chatManager().addMessageListener(msgListener);
// 解注册消息监听
EMClient.getInstance().chatManager().removeMessageListener(msgListener);


7、EaseIMKit 创建聊天列表页面

EaseIMKit 提供了 EaseChatFragment,添加到 Activity 中并传递相应的参数即可用。

必须向 EaseChatFragment 传递的参数为:

conversationId——会话 ID,单聊时指对方 ID,群聊和聊天室时指群和聊天室 ID;

chatType——聊天类型,整型,分别为单聊(1)、群聊(2)和聊天室(3);

可选传递参数为:

history_msg_id——消息 ID,用于查询历史记录时的定位消息 ID;

isRoaming——是否开启漫游,布尔类型,用于标记是否优先从服务器拉取消息。

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2)
EaseChatFragment chatFragment = new EaseChatFragment();
Bundle bundle = new Bundle();
bundle.putString(EaseConstant.EXTRA_CONVERSATION_ID, "环信id");
bundle.putInt(EaseConstant.EXTRA_CHAT_TYPE, 1);
chatFragment.setArguments(bundle);
getSupportFragmentManager().beginTransaction().replace(R.id.container,chatFragment,"chat").commit();
}


8、EaseIMKit 创建会话列表页面

EaseIMKit 提供了 EaseConversationListFragment,需要将其或者其子类添加到

Activity 中。开发者需要对刷新事件(新消息,删除消息,删除会话等)进行处理。

1.加载会话:

public void loadDefaultData() {
presenter.loadData();
}


2.设置数据

public void setData(List<EaseConversationInfo> data) {
presenter.sortData(data);
}


3.删除会话

@Override
public void deleteConversation(int position, EaseConversationInfo info) {
presenter.deleteConversation(position, info);
}


9、EaseIMKit 添加联系人页面

EaseIMKit 提供了 EaseContactListFragment,添加其及其子类到 Activity 中。开发者需要对刷新事件(添加联系人,删除联系人等)进行处理

1.设置数据

public void setData(List<EaseUser> data) {
presenter.sortData(data);
}


到这里我们已经完成了Android端SDK集成,并实现了IM基本功能,相信以各位小伙伴的能力接着的优化完全不是问题,当然也说不定有些奇葩问题搞不定,建议可以去环信官网联系官方技术支持,快速帮你解决遇到的问题~~

环信官网:https://www.easemob.com/

注册环信账号:注册环信即时通讯云

Android端SDK下载:https://www.easemob.com/download/im

收起阅读 »

告诉ChatGPT,我想读博了

上篇文章详细写了如何体验ChatGPT。在实际使用中发现它对固定模板式的文字工作做的比较好。于是我瞬间想起了毕业前被论文支配的恐惧,我突然有一个大胆的想法,那么ChatGPT是否能帮我写一篇毕业论文呢? 1、论文大纲 以大家最常用的图书管理系统为例。在毕业论文...
继续阅读 »

上篇文章详细写了如何体验ChatGPT。在实际使用中发现它对固定模板式的文字工作做的比较好。于是我瞬间想起了毕业前被论文支配的恐惧,我突然有一个大胆的想法,那么ChatGPT是否能帮我写一篇毕业论文呢?


1、论文大纲


以大家最常用的图书管理系统为例。在毕业论文的第一步,我们先根据题目生成一个论文大纲。


image-20230212115237500


唔,感觉还行感觉稍微调整一下就可以用。


2、论文摘要


论文大纲有了,接下来是要写一个中英文的摘要。


image-20230212121653775


看样子只是翻译了一下,和普通的翻译软件也没啥差别。但ChatGPT的强大不止于此,还可以接受我们的“调教”。


3、论文润色


写过论文的同学都知道,初版论文往往经过天翻地覆的修改,因此「论文润色」是写论文时时刻刻都在做的事情。


比如我们对上面的摘要进行润色,我们试着在英文内容前加一个Prompt:



Please proofread and polish the passage from an academic angle and highlight the modification:


请从学术角度对这段话进行校对和润色,并突出修改的内容。



添加完提示后,神奇的事情发生了,不仅给了一个船新版本的摘要翻译,还给出了修改的提示


image-20230212121953558


那么ChatGPT给的修改建议到底是胡说八道,还是有据可依呢?


我取第一条修改意见,前面加上一个why,作为一个Prompt:



why Replaced "presents" with "proposes" to emphasize the introduction of a new idea ?



image-20230212205150644


oh!有理有据,令人信服。这不比某些不靠谱的导师给你瞎写修改意见强?


4、检查拼写与语法错误


自己写论文时难免会有大量的语法错误,此时也可以通过ChatGPT来检查。


比如我们随便写一个语法错误的句子,然后加一个Prompt。



Please help me to check the spelling and formatting errors and explain the reasons :“I think this song is most poplar of all,and I am very like that。”



image-20230213105718592


看的出来,通过我们自定义的Prompt,ChatGPT不仅将错误的句子修改正确,还帮我把错误的地方列举出来并且给予了详细的解释。相当于你的专属英文老师。


5、总结


简单的体验完之后,可以看出不论是在论文的润色,还是拼写语法的检查,ChatGPT确实都做的很好,可以称之为极其高效率的学术写作练习。感觉有了这么牛的论文写作工具,扶我起来,感觉还能再读个博!


我想读博了


突然,我回忆起自己曾经硕士毕业前写论文时的每一个夜晚,陷入了人生的大思考....


无论是选题还是目录,最后到论文的正文与总结,以往我们的学习方式都是通过搜索引擎进行「单方向信息获取」,大量的时间都用来检索信息,真正留给自己的思考的精力并不多。未来的学习方式定会变成基于AI的**「人机互动加速成长」**,我们通过更高效的方式来获取信息,更多精力留给自己的想法与思考。


即便你读研读博导师是个水货,从不指点你学术写作,你依然可以通过ChatGPT进行训练提高。


虽然最近很多媒体都在扬言AI取代人类的很多职业,但我想,ChatGPT不仅仅可以做为生产力工具,更可以高效的进行学习和工作,未来一定会发挥其教育的属性和价值。


毋庸置疑,人类进入了新的学习时代,新的生产力时代,我很庆幸自己生在了这个时代。


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

人工智能未来是否会取代人类程序员?

   这个话题在近期来引起了很大讨论,尤其是当GPT4发布后,其展现出来的能力让很多岗位的从业者战战兢兢,比如像程序员,甚至有大佬跳出说三年 AI一定会取代程序员。人工智能和机器人是否会大规模取代人类程序员和工程师确实是一个非常复杂的问题。   一方面,人工智...
继续阅读 »

   这个话题在近期来引起了很大讨论,尤其是当GPT4发布后,其展现出来的能力让很多岗位的从业者战战兢兢,比如像程序员,甚至有大佬跳出说三年 AI一定会取代程序员。人工智能和机器人是否会大规模取代人类程序员和工程师确实是一个非常复杂的问题。


  一方面,人工智能和机器人确实在某些方面具有优势,比如处理大量数据、重复任务、持续运行等。这可能会影响某些类型的程序员工作,比如像是做一些简单工作的外包程序员。


  但是,程序员的工作内容并不仅仅是编写代码。他们需要与团队成员沟通,分析需求,设计系统架构,解决问题,优化性能等。这些任务需要丰富的经验和创造力,而目前的AI技术尚无法满足这些需求。 更具体点,我觉得以目前AI的能力,它在以下这些方面还完全无法取代人类程序员:



  1. 创新性解决问题:人类程序员能够通过创新思维解决复杂问题,而现有的AI通常依赖大量数据和已有知识,难以实现真正的创新。

  2. 高度抽象的思考:人类程序员能够理解和应用高度抽象的概念,而现有的AI在这方面仍然有限。

  3. 深入理解人类情感和需求:人类程序员能够理解其他人的情感和需求,从而开发更符合用户期望的软件。现有的AI在理解人类情感方面仍然有很大的局限性。

  4. 道德和伦理判断:在开发软件时,人类程序员可以根据道德和伦理原则做出判断。而现有的AI无法像人类一样理解和遵循道德伦理准则。

  5. 协作与沟通:人类程序员可以与团队成员有效沟通,协同解决问题。目前的AI在这方面仍然有很大的不足。

  6. 灵活判断和决策:人类程序员可以在不确定和模棱两可的情况下作出灵活判断和决策。而人工智能和机器人更依赖大量数据和明确规则,在没有足够信息的情况下,其判断和决策能力会受限。

  7. 审美和直观感受:人类程序员有对美感、审美和直观感受的理解,这些都是人工智能和机器人难以具备的。像是UI设计、交互体验设计等需要这方面的能力。


  如果你担忧AI对自己未来的影响,可以重点关注下个人以上方面能力的提升。努力提升自己的创造性、沟通表达能力、抽象思维等…… 这些软技能,至少目前就不用担心未来会被AI取代(当然以后的AI是不是也会具备上述能力,也不好说)。


  当然,换个视角思考,人工智能的进步也在推动程序员工作的变化。人工智能可以帮助程序员完成一些重复和耗时的任务,从而提高工作效率和产出,比如像Github推出的CoPilot,据说在某些场景下可以提升60%的编码速度。 所以,人工智能对程序员来说更像是一个合作伙伴,而不是竞争对手。


  综上,尽管人工智能在某些方面具有威胁,但作为一个整体,人工智能很难完全取代人类程序员。人工智能和人类程序员各有所长,在未来的互动中会形成更为合理和高效的工作模式。人类程序员的工作依然非常重要和必要。



备注:本文大部分内容由AI生成,我主要是将内容做整理和简单润色。


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

ChatGPT 生态,毁灭人性的一次开源!

很多人可能对百度文心一言发布会的现场记忆犹新。几百亿身价的老板,像小学生一样汇报自己的 PPT,充满了忐忑。 其实大可不必,命令行更加富有科技感,也更有说服力。不管对程序员来说还是围观的群众来说,能干什么并不重要,实际输出什么才是大家最关心的。 毕竟把人当傻子...
继续阅读 »

很多人可能对百度文心一言发布会的现场记忆犹新。几百亿身价的老板,像小学生一样汇报自己的 PPT,充满了忐忑。


其实大可不必,命令行更加富有科技感,也更有说服力。不管对程序员来说还是围观的群众来说,能干什么并不重要,实际输出什么才是大家最关心的。


毕竟把人当傻子的年代慢慢过去了。


这也难怪。ChatGPT 的模型越来越完善,资本家们都很着急。以往,打着开源的遮羞布,他们也可以拥有自己的自主产权。但没想到 ChatGPT 这么不识好歹,竟然将自己的核心技术夹的这么紧。


如果 ChatGPT 的能力,能够离线,能够运行在任何小型设备上,那么拥有独立人格的智能单元就会变成现实。这种设想,比集中化的大脑更有诱惑力。


这里,就有一个。你可以下载下来实际编译运行在在自己的MacBook上。




llama.cpp


github.com/xjjdog/llam…


这是一个 C++ 实现的 LLaMA 对话库。Java 和 Python的同学也不要被吓倒,它的使用超级简单。如果你遇到什么问题,欢迎到公众号(xjjdog)提问。


热门问题:



描述


和ChatGPT对比起来,llama的好处是:使用普通的Macbook,Linux,甚至Docker、树莓派等,就可以运行类比于 ChatGPT 的对话模型。




  • 纯C++代码,代码少,而且没有任何依赖




  • Apple 的M1 芯片也可以跑,而且有性能优化




  • x86架构拥有 AVX2 支持




  • 在 CPU 上就能跑,不需要 GPU




支持的平台:




  • [X] Mac OS




  • [X] Linux




  • [X] Windows (via CMake)




  • [X] Docker




模型下载地址:


curl -o ggml-alpaca-7b-q4.bin -C - https://gateway.estuary.tech/gw/ipfs/QmQ1bf2BTnYxq73MFJWu1B7bQ2UD6qG7D7YDCxhTndVkPC
curl -o ggml-alpaca-7b-q4.bin -C - https://ipfs.io/ipfs/QmQ1bf2BTnYxq73MFJWu1B7bQ2UD6qG7D7YDCxhTndVkPC
curl -o ggml-alpaca-7b-q4.bin -C - https://cloudflare-ipfs.com/ipfs/QmQ1bf2BTnYxq73MFJWu1B7bQ2UD6qG7D7YDCxhTndVkPC



那么,这个工具要怎么用呢?超级简单。


首先,将代码clone到本地。


git clone https://github.com/ggerganov/llama.cpp.git

然后,进入到llama.cpp目录。


cd llama.cpp

编译代码。


make

生成后的文件名称叫做main,以后,我们只需要运行 ./main即可。


最重要的一步,你需要下载一个数据模型。否则 llama 是不知道加载什么数据来进行计算的。为了测试,我们下载一个最小的。这个文件大小有3.9G,你需要相应大小的内存预留。


curl -o ggml-alpaca-7b-q4.bin -C - https://gateway.estuary.tech/gw/ipfs/QmQ1bf2BTnYxq73MFJWu1B7bQ2UD6qG7D7YDCxhTndVkPC

最后,我们就可以指定这个模型,来进行对话输出了。


./main -m ./ggml-alpaca-7b-q4.bin -p "Will the future be female?" -n 512 --color



  • m 指定的是模型的位置。




  • p 是对话或者问题。比如这里,我问我是否能够吃狗肉!




  • n 指定的是输出的文字数量,默认是128。




  • --color 输出彩色内容。




下面是一些输出。首先会将输入进行切分,然后生成内容,最后将耗时打印。


% ./main -m ./ggml-alpaca-7b-q4.bin -p "Can i eat dog?" -n 512 --color

No you cannot! Eating dogs is illegal and against the law. It would be considered animal abuse, so please don’t do it under any circumstances…unless you are a cannibal

main: mem per token = 14368644 bytes
main:     load time =   743.12 ms
main:   sample time =   455.50 ms
main:  predict time = 46903.35 ms / 91.79 ms per token
main:    total time = 48455.85 ms

交互模式


如果你想要和ChatGPT一样有对话能力的话,也是可以的。需要加上 -i 参数,当然,也可以使用 -r User:参数输出一个提示符。


比如:


./main -m ./ggml-alpaca-7b-q4.bin -p "Will the future be female?" -n 128 --color -i -r "User:"

授课模式


所谓授课模式,就是提供一个按照顺序输出的文件列表,让电脑按照顺序把答案输出。如果liyanhong使用这种模式,而不是ppt,估计效果会更好。


比如:


./main -m ./models/13B/ggml-model-q4_0.bin -n 256 --repeat_penalty 1.0 --color -i -r "User:" -f prompts/chat-with-bob.txt

内存需求


内存的需求取决于你使用的模型。我们的测试使用的都是最简单的模型,所以4GB就够了。如果想要更精细的输出,你的内存需要更大一些。
































modeloriginal sizequantized size (4-bit)
7B13 GB3.9 GB
13B24 GB7.8 GB
30B60 GB19.5 GB
65B120 GB38.5 GB

Android


你甚至可以在Android上跑起来。如果你的内存够大,那么完全可以做一个小型对话机器人,还是本地的!


后面如果解决了部分加载的问题,Android的嵌入式应用会非常方便。


End


人类有用的知识库看起来很多,但其实训练下来,最多也不会超过TB级别。当然也不能这么说,计算机也是由 0 和 1 组成的,但现在它几乎什么都能做。但无疑,除了训练算法,对于使用者来说,模型才是最重要的。


把这些有限的数据预装在小型的设备中,这就会成为最小的智能体。在数据中加入人格(目前的Chat系列是可以做到的),这个智能体就可以充当我们的秘书、代言人,甚至男女朋友。


嗯,一切皆有可能。从现在开始注意养生,活的长一点点,来看看未来世界的样子!



作者简介:小姐姐味道  (xjjdog),一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道。我的个人微信xjjdog0,欢迎添加好友,进一步交流。

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

我把FlutterWeb渲染模式改成Canvaskit后...

背景 用FLutterWeb开发的网站在使用过程中出现了一些问题,比如在Google浏览器中使用交互、动画流畅,在360浏览器中就卡顿;图标在代码中动态设置颜色的方式在Google浏览器中正常显示,在Safari浏览器中颜色缺失,变为黑色;在有的电脑中Goog...
继续阅读 »

背景


用FLutterWeb开发的网站在使用过程中出现了一些问题,比如在Google浏览器中使用交互、动画流畅,在360浏览器中就卡顿;图标在代码中动态设置颜色的方式在Google浏览器中正常显示,在Safari浏览器中颜色缺失,变为黑色;在有的电脑中Google浏览器也有动画、交互卡顿的现象、页面报错等。很奇怪,一脑袋问号。


优化方案


这些问题的原因是,渲染模式为html导致的,将渲染模式由html改为canvaskit,之前遇到的问题基本就解决了,动画也不卡了,画面也流畅了,图标也正常了,兼容性也提高了,再也不用担心在老板的电脑上卡住了。


渲染模式


简单说说两种模式的区别。

html渲染模式:flutter会采用HTML的custom element,CSS,CanvasSVG来渲染UI元素。

canvaskit渲染模式:flutter将 Skia 编译成 WebAssembly 格式,并使用 WebGL 渲染。



























htmlcanvaskit
命令行--web-renderer html--web-renderer canvaskit
优点体积更小渲染性能强;多端一致
缺点渲染性能差;跨端兼容差体积相较html多2.5M

所以使用canvaskit会更加流畅,更符合FLutter的气质。但是!也出现了些新的问题。


由Canvaskit引起的问题


图片跨域



报错描述:
Access to XMLHttpRequest at 'https://.../icon/setting_228.webp' from origin 'https://...' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.



在html模式下是正常的,而在canvaskit出现了跨域问题,查看日志发现区别。


html的网络请求类型type就是图片本身,不会出现问题。
图片跨域2.png



不会出现跨域的问题原因是在HTML中,有一些标签也可以发起HTTP请求,比如script标签,link标签,img标签,form标签,且被允许跨域。



  1. link,img标签都是单纯的引入资源文件

  2. form标签用于收集用户输入并发送,但是发送成功会跳转到新网页,并将服务器响应作为新网页的内容

  3. script标签可以引入外部js文件,并执行引入的js文件的代码


其中,script标签由于其可以执行引入的js文件的代码,再加上其跨域特性,让script标签可以用来做一些超出其设计初衷的事。script标签会发起HTTP GET去请求服务器上的js文件,所以script标签可以用于实现HTTP GET跨域请求。



而canvaskit模式下,请求类型是xhr,不支持跨域。而我的图片地址和服务地址并不在一个域名,所以出现该问题。
图片跨域1.png



原因是同源策略,它是浏览器特有的一种安全机制,主要用于限制不同的源之间的数据交互。



那如何解决呢?


询问前端大佬后,发现解决问题最快的方法就是放到自身服务的域名下。随后我把图片放在项目中的asset目录中,更改本地引用地址,打包上传部署,解决!


(PS:这个问题在本地debug模式下,并不会出现)


首次打开加载慢


在首次改成canvaskit模式部署后,打开网站,页面一度白屏很长时间,预计有10秒,查看后台日志发现是下载了很多文件,包括canvaskit绘制引擎、字体等。主要耗时是在引擎(约9M)、字体下载,而下载这些的域名都是官方的,所以下载速度也有所限制。
引擎下载.png
解决办法:
将引擎和字体传值自己的服务器,以加快下载速度。




  1. 引擎本地化,查看网络请求详情,可以看到下载地址,单独下载后放到项目中。
    下载引擎.png
    我的位置是web/assets/canvaskit/canvaskit.js&wasm
    引擎存放地址.png

    再设置替换引擎路径,在运行或打包的时候加上以下命令行。等号后面为本地的路径。


    --dart-define=FLUTTER_WEB_CANVASKIT_URL=assets/canvaskit/



  2. 本地化加载KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf字体文件,同样在请求详情中获取地址,下载至本地,放在本地,web/assets/canvaskit/


    字体下载.png


    替换本地地址,在构建完成后的build目录下的main.dart.js中搜索该字体名,把前缀替换成本地路径。


    https://fonts.gstatic.com/s/roboto/v20/KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf
    替换成
    assets/canvaskit/KFOmCnqEu92Fr1Me5WZLCzYlKw.ttf



字体需下载


在打开页面时,会出现字体乱码,原因是正在下载字体,而且引用的字体不一样下载的库也是不同的。同样也可以下载至本地,替换main.dart.js的地址,但下载完体验后,发现不管是第一次还是之后都会出现乱码,只是显示的时间长短,体验也是不很好。

字体乱码.png


下载字体地址.png
所以我是在pubspec.yaml中设置了本地的字体包的方式解决的,这样在首次加载或后面的刷新,都未出现过乱码。


设置字体.png


加载时提示


经过上面两步设置,首次加载时长会有大大缩减,但是也会有白屏,为了更好的体验在白屏时加个提示。


// 在 web/index.html 中的 body 标签下加提示
<div id="text">静态资源加载中...</div>

浏览器刷新后页面加载两次


在使用网站时刷新会出现页面加载两次的问题,查看日志发现是web/index.html中的一段代码引起的。


// If service worker doesn't succeed in a reasonable amount of time,
// fallback to plaint <script> tag.
setTimeout(() => {
if (!scriptLoaded) {
console.warn(
'Failed to load app from service worker. Falling back to plain <script> tag.',
);
loadMainDartJs();
}
}, 4000);

引起超时的原因是navigator.serviceWorker.register(serviceWorkerUrl)注册失败,而上面的代码是兜底的逻辑。serviceWorker是服务器与浏览器之间的代理,目前用不上,所以将注册逻辑注释掉,直接调用loadMainDartJs()即可。


路由包装url地址方式失效


在canvaskit模式下,刷新后不会停留在当前页面了。之前写过一篇文章《FlutterWeb浏览器刷新后无法回退的解决方案》中的方案看来只适应在html模式下。


解决办法:
在上面的的文章基础上稍微修改下。


 // 刷新时回调
_beforeUnload = (event) {
// 本地记录,标记成"已刷新"
DB(DBKey.isRefresh).value = true;

// 记录刷新时的页面,用于还原(本次新增的方法)
List history = get();
DB(DBKey.initRoute).value = history.last;
history.removeLast();
set(history);

// 移除刷新前的实例的监听
html.window.removeEventListener('beforeunload', _beforeUnload);
html.window.removeEventListener('popstate', _popState);
};

// 获取上次最后的页面,(本次新增的方法)
static String initRoute(currentContext) {
return DB(DBKey.initRoute).get(Uri(scheme: RoutePath.scheme, host: RoutePath.home).toString());
}

// 初始化
MaterialApp(
.....
initialRoute: RouterHistory.initRoute(context),//(本次新增的方法)
.....
))

这样设置完后也会停留在当前页面了。


最后


如果有遇到其他问题或更好的解决办法欢迎提出讨论


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

Flutter 本地存储 —— 基本的键值对存储

前言 在原生的 Android 或 iOS 中,都提供了基本的键值对存储方式,Android 是 SharedPreferences,iOS 是 NSUserDefaults。在 Flutter 中,提供了 shared_preferences 这个插件来实现...
继续阅读 »

前言


在原生的 Android 或 iOS 中,都提供了基本的键值对存储方式,Android 是 SharedPreferences,iOS 是 NSUserDefaults。在 Flutter 中,提供了 shared_preferences 这个插件来实现本地键值对数据存储。实际上,shared_preferences 在 Android 就是使用 SharedPreferences 实现,在 iOS 上则是使用 NSUserDefaults 实现。


基本使用


在 pubspec.yaml 文件中添加以下代码:


dependencies:
flutter:
sdk: flutter
shared_preferences: ^2.0.18

我们将基础的计数应用修改为支持从上一次结果(即存储在本地的数值)开始增加。代码如下:


import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
int _counter = 0;
late SharedPreferences _prefs;

@override
void initState() {
super.initState();
_loadCounter();
}

void _loadCounter() async {
_prefs = await SharedPreferences.getInstance();
setState(() {
_counter = (_prefs.getInt('counter') ?? 0);
});
}

void _incrementCounter() async {
setState(() {
_counter++;
});
await _prefs.setInt('counter', _counter);
}

@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Shared Preferences 示例'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
),
);
}
}

上面的代码是一个基础的计数器应用,我们定义了一个_counter变量来保存计数器的值,并且使用 SharedPreferences 实例来存储和检索_counter变量的值。
initState方法中,我们使用_loadCounter方法来加载_counter变量的值。在_loadCounter方法中,我们首先使用SharedPreferences.getInstance() 方法来获取 SharedPreferences 实例,然后使用 getInt()方法来检索 _counter 变量的值。如果检索到的值为 null,则将 _counter 变量的值设置为 0。
_incrementCounter 方法中,我们使用了setInt方法将 _counter 变量的值保存到 SharedPreferences 实例中来实现本地存储。
运行效果如下:


基本本地存储.gif


存储其他类型数据


shared_preferences支持存储的数据类型有整型、浮点型(double)、字符串、布尔型和字符串数组。如果想存储对象,也可以通过 json 序列化和反序列化的方式实现。我们来看一个更复杂点的例子。


class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);

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

class _MyAppState extends State<MyApp> {
// 初始化需要存储的值
int _counter = 0;
String _username = '';
bool _isDarkModeEnabled = false;
final _textController = TextEditingController(text: '');

// SharedPreferences 实例
late SharedPreferences _prefs;

// 加载 SharedPreferences 中存储的值
Future<void> _loadData() async {
_prefs = await SharedPreferences.getInstance();
setState(() {
_counter = _prefs.getInt('counter') ?? 0;
_username = _prefs.getString('username') ?? '';
_textController.text = _username;
_isDarkModeEnabled = _prefs.getBool('isDarkModeEnabled') ?? false;
});
}

void _incrementCounter() async {
setState(() {
_counter++;
});
await _prefs.setInt('counter', _counter);
}

// 保存用户名
void _saveUsername(String username) async {
setState(() {
_username = username;
});
await _prefs.setString('username', _username);
}

// 切换暗黑模式
void _toggleDarkMode(bool isDarkModeEnabled) async {
setState(() {
_isDarkModeEnabled = isDarkModeEnabled;
});
await _prefs.setBool('isDarkModeEnabled', _isDarkModeEnabled);
}

@override
void initState() {
super.initState();
_loadData();
}

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter SharedPreferences 示例',
theme: _isDarkModeEnabled ? ThemeData.dark() : ThemeData.light(),
home: Scaffold(
appBar: AppBar(
title: const Text('Flutter SharedPreferences 示例'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'计数器的值:$_counter',
),
const SizedBox(height: 20),
TextFormField(
decoration: const InputDecoration(
labelText: '请输入您的名字',
),
controller: _textController,
onChanged: (value) {
_saveUsername(value);
},
),
const SizedBox(height: 20),
SwitchListTile(
title: const Text('启用暗黑模式'),
value: _isDarkModeEnabled,
onChanged: (value) {
_toggleDarkMode(value);
},
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: '递增计数器的值',
child: const Icon(Icons.add),
),
),
);
}
}

上述代码增加了两个类型的存储,分别是字符串和布尔型,存储方式其实是类似的,布尔型使用 getBool 获取、setBool 存储;字符串则是使用 getStringsetString。我们通过布尔型变量控制是否启用暗黑模式,使用字符串类存储用户名。下面是运行的结果。


高级应用.gif


总结


可以看到shared_preferences 非常简单,因此可以应用在简单的键值对存储中,典型的就是我们在本地换成后端的SessionId、记住用户名和密码、或者默认的勾选项等等。然后基于这些存储的数据做默认值显示和业务规则控制、或填充到请求表单里。对于复杂的业务对象存储,则需要使用 SQL数据库或者是 NoSQL 数据库。


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

究极进化版基于 dio 的网络封装库

dio
可能是 Flutter 上最强的网络框架, 基于dio实现的非侵入式框架(不影响原有功能). 学习成本低、使用简单, 一行代码发起网络请求, 甚至无需初始化。 之前发过两篇关于封装网络库的文章: 强大的dio封装,可能满足你的一切需要 一步一步教你封装最新版...
继续阅读 »

可能是 Flutter 上最强的网络框架, 基于dio实现的非侵入式框架(不影响原有功能). 学习成本低、使用简单, 一行代码发起网络请求, 甚至无需初始化。


之前发过两篇关于封装网络库的文章:



距离最早的文章发布时间,已经过去了三年。这期间 dio 也更新到5.x.x了,在使用中也积攒了许多定制需求和优化方案。在确定需求和方案后,修修改改,终于发布了最新最实用的网络请求版本。


欢迎贡献代码/问题


特点



  • 个人使用下来感觉开发效率比目前网络请求库都高:最简单易用

  • 专为 Flutter 而生,支持全平台

  • 遵循设计模式最佳实践,build模式全局配置

  • catch请求错误,不需要开发者处理

  • 优秀的源码/注释/文档/示例

  • 类似kotlin的语法糖:请求结果的when语句和密封类


主要功能



  • RESTful API 设计
    GET/POST/PUT/HEAH/DELETE/PATCH/DOWNLOAD

  • 可取消请求

  • 异步解析,数据量大不再卡顿

  • 全局错误处理(减少崩溃率)

  • 自定义解析器,支持全局和单个请求

  • 自定义解析方法

  • 配置请求参数

  • 漂亮的日志打印

  • 证书快速配置

  • 代理配置

  • 拦截器配置

  • 强制缓存模式/自定义缓存Key/缓存有效期/LRU缓存算法/缓存任何数据

  • 监听上传/下载进度


简单使用


添加依赖:


dependencies:
flutter_nb_net: ^0.0.1

像 dio 一样使用,无需配置,返回实体类实现BaseNetworkModel,复写fromJson函数即可,:


class BannerModel extends BaseNetworkModel<BannerModel> {

@override
BannerModel fromJson(Map<String, dynamic> json) {
return BannerModel.fromJson(json);
}
//...
}

温馨提示:dart实体类可用freezedjson_serializable生成或者JsonToDart插件一键生成。


  /// Get 请求
void requestGet() async {
var appResponse = await get<BannerModel, BannerModel>("banner/json",
responseType: BannerModel());
appResponse.when(success: (BannerModel model) {
var size = model.data?.length;
debugPrint("成功返回$size条");
}, failure: (String msg, int code) {
debugPrint("失败了:msg=$msg/code=$code");
});
}

get<BannerModel, BannerModel>这里有两个泛型,前者是接口返回的数据需要序列化的类型,后者是开发关注的需要返回的类型。如果数据类型一致,两个泛型就是一样的。否则,比如接口返回一个用户列表,前面泛型就是User类型,后面是List<User>;又或者接口返回的数据包了几层,我们只需要最里面的数据格式,那么前面就是需要序列化的整个数据类型,第二个泛型是最里面的数据类型。


配置使用


全局配置


在使用前进行全局配置:


 NetWrapper.instance
// header
.addHeaders({"aaa": '111'})
// baseUrl
.setBaseUrl("https://www.wanandroid.com/")
// 代理/https
.setHttpClientAdapter(IOHttpClientAdapter()
..onHttpClientCreate = (client) {
client.findProxy = (uri) {
return 'PROXY 192.168.20.43:8888';
};
client.badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
return client;
})
// cookie
.addInterceptor(CookieManager(CookieJar()))
// dio_http_cache
.addInterceptor(DioCacheManager(CacheConfig(
baseUrl: "https://www.wanandroid.com/",
)).interceptor)
// dio_cache_interceptor
.addInterceptor(DioCacheInterceptor(
options: CacheOptions(
store: MemCacheStore(),
policy: CachePolicy.forceCache,
hitCacheOnErrorExcept: [401, 403],
maxStale: const Duration(days: 7),
priority: CachePriority.normal,
cipher: null,
keyBuilder: CacheOptions.defaultCacheKeyBuilder,
allowPostMethod: false,
)))
// 全局解析器
.setHttpDecoder(MyHttpDecoder.getInstance())
// 超时时间
.setConnectTimeout(const Duration(milliseconds: 3000))
// 允许打印log,默认未 true
.enableLogger(true)
.create();

如果接口返回的数据格式规范,配置自定义一个全局解析器 .setHttpDecoder(MyHttpDecoder.getInstance())


/// 默认解码器
class MyHttpDecoder extends NetDecoder {
/// 单例对象
static final MyHttpDecoder _instance = MyHttpDecoder._internal();

/// 内部构造方法,可避免外部暴露构造函数,进行实例化
MyHttpDecoder._internal();

/// 工厂构造方法,这里使用命名构造函数方式进行声明
factory MyHttpDecoder.getInstance() => _instance;

@override
K decode<T extends BaseNetworkModel, K>(
{required Response<dynamic> response, required T responseType}) {
var errorCode = response.data['errorCode'];

/// 请求成功
if (errorCode == 0) {
var data = response.data['data'];
if (data is List) {
var dataList = List<T>.from(
data.map((item) => responseType.fromJson(item)).toList()) as K;
return dataList;
} else {
var model = responseType.fromJson(data) as K;
return model;
}
} else {
var errorMsg = response.data['errorMsg'];
throw NetException(errorMsg, errorCode);
}
}
}

如果添加缓存,可以使用dio_cache_interceptordio_http_cache等 dio 推荐的缓存库。


 // dio_http_cache
.addInterceptor(DioCacheManager(CacheConfig(
baseUrl: "https://www.wanandroid.com/",
)).interceptor)
// dio_cache_interceptor
.addInterceptor(DioCacheInterceptor(
options: CacheOptions(
store: MemCacheStore(),
policy: CachePolicy.forceCache,
hitCacheOnErrorExcept: [401, 403],
maxStale: const Duration(days: 7),
priority: CachePriority.normal,
cipher: null,
keyBuilder: CacheOptions.defaultCacheKeyBuilder,
allowPostMethod: false,
)))

因为dio_http_cache依赖的diojson_annotation是旧版本,所以如果使用dio_http_cache需要解决下依赖冲突:


dependency_overrides:
dio: ^5.0.3
json_annotation: ^4.8.0

配置代理和证书:


      .setHttpClientAdapter(IOHttpClientAdapter()
..onHttpClientCreate = (client) {
client.findProxy = (uri) {
return 'PROXY 192.168.20.43:8888';
};
client.badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
return client;
})

配置cookie


addInterceptor(CookieManager(CookieJar()))

开启 log,默认开启:


.enableLogger(true)

response_log_android_studio.png


特殊配置


有的接口比较特殊,比如返回的数据格式是特殊的,需要单独解析,此时有两种方法实现,第一种适合多个相同的特殊接口,在请求时传入自定义的解析器,第二种是在回调中解析。


解析器httpDecode


    var appResponse = await get<BannerBean, List<BannerBean>>("banner/json",
responseType: BannerBean(), httpDecode: MyHttpDecoder.getInstance());
appResponse.when(success: (List<BannerBean> model) {
var size = model.length;
debugPrint("成功返回$size条");
}, failure: (String msg, int code) {
debugPrint("失败了:$msg");
});

回调converter


   var appResponse = await get<BannerModel, List<BannerBean>>("banner/json",
options: buildCacheOptions(const Duration(days: 7)),
responseType: BannerModel(), converter: (response) {
var errorCode = response.data['errorCode'];
/// 请求成功
if (errorCode == 0) {
var data = response.data['data'];
var dataList = List<BannerBean>.from(
data.map((item) => BannerBean.fromJson(item)).toList());
return Result.success(dataList);
} else {
var errorMsg = response.data['errorMsg'];
return Result.failure(msg: errorMsg, code: errorCode);
}
});
appResponse.when(success: (List<BannerBean> model) {
debugPrint("成功返回${model.length}条");
}, failure: (String msg, int code) {
debugPrint("失败了:msg=$msg/code=$code");
});

名字的由来


一开始这个库的名字是net,这是我第一次在 pub 上发布,不知道库名称不能重合的规则,一直失败:


`xxx@gmail.com` has insufficient permissions to upload new versions to existing package `net`.

说明 pub 上已经有了这个名字的库,改名flutter_net,依然失败:


`xxx@gmail.com` has insufficient permissions to upload new versions to existing package `flutter_net`.


最后改名为flutter_nb_net,终于发布成功了。


国际惯例上源码


pub地址


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

从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必将继续发展壮大,成为以后构建用户界面的首选方式。


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

Android本地化适配之ICU接口

背景: 在多语言项目中,我们经常会遇到本地化适配不规范导致的问题。例如 月份翻译错误、数字显示格式不正确 或者 数字显示形式与本地习惯不符 等。为了寻求一种更精准高效的适配方案,我在网上查阅了相关资料。发现Google提供了一个本地化API接口——ICU AP...
继续阅读 »

背景:


在多语言项目中,我们经常会遇到本地化适配不规范导致的问题。例如 月份翻译错误数字显示格式不正确 或者 数字显示形式与本地习惯不符 等。为了寻求一种更精准高效的适配方案,我在网上查阅了相关资料。发现Google提供了一个本地化API接口——ICU API。它可以利用ICU和CLDR提供Unicode和其他国际化支持。从而提升软件的本地化质量。


本地化适配场景


本地化适配所涉及到的问题,基本上都可以归纳为:在不同的语言和地区,同一个信息所显示翻译不同、格式不同或者一个事物表现规则不同。通过归纳总结,我们把常见的本地化适配分为以下几个类型:时间信息数字信息文件大小及其单位历法规则测量单位和时区信息


本地化接口使用


针对以上所述本地化场景,我们分别介绍相关接口以及接口使用方法。


常见格式如日期、单位等,在 data/locales 目录中有详细说明。,我们可以下载 data/locales/zh.txt 文件查看不同标签对应内容含义。


例如我们要查看星期在各国翻译下的内容:



  • 首先我们在 data/locales/zh.txt 中查找星期一对应标签 dayNames 下面。其中wide表示全称,short表示简称。

  • 打开 data/locales 其它国家对应语言码txt文件。比如查看法语下翻译情况时打开 fr.txt 文件。

  • 同样查找 dayNames 标签对应翻译,并与手机上显示翻译比对是否一致。


除了 data/locales 目录外还有几个目录需要关注:



  • data/unit 常用单位

  • data/curr 货币单位

  • data/lang 语言描述


时间日期格式


DateFormat#getInstanceForSkeleton 、SimpleDateFormat.getDateInstance


说明:


1.这些接口能够解决x年x月x日星期x x时x分x秒这类时间信息格式问题。比如年月日出现先后顺序不同情况。这个格式与本地人使用习惯相关。


好的,我已经输出了你的文章的前半部分,下面是后半部分:


因此使用这个接口能够很好地解决时间显示格式问题。


时间日期格式


DateFormat#getInstanceForSkeleton 、SimpleDateFormat.getDateInstance


说明:


1.这些接口能够解决 x年x月x日星期xx时x分x秒 这类时间信息格式问题。比如年月日出现先后顺序不同情况,这个格式与本地人使用习惯是相关的。因此使用这个接口能够很好地解决时间显示格式问题。


2.同时这个接口 还能解决时间表述中翻译问题,不用再单独考虑翻译问题。 这个翻译的准确度,往往要高于翻译公司的准确度


3.使用其中一些特殊接口能够解决一些特殊字串翻译问题。比如使用以下接口可以获取 星期一二月 等相关特殊时间在本地语言翻译字符。这个翻译准确度往往高于翻译公司准确度。希望有类似字串显示场景可以研究这个接口并进行使用。developer.android.com/reference/j…


以下只就说明进行一个简单举例:下面的getDateInstance可以不传第二个参数,那么就会根据当前系统预设来输出结果。


DateFormat df = DateFormat.getDateInstance(DateFormat.LONG, Locale.FRANCE);
myDate = df.parse(myString);
...
January 12, 1952

具体格式由第一个参数控制,模块可根据需求自行定制,给出以下使用案例



DateFormat.getInstanceForSkeleton(DateFormat.ABBR_MONTH_DAY, Locale.getDefault()).format(new Date());
...
3月16日
Mar 16

DateFormat.getInstanceForSkeleton(DateFormat.MONTH_WEEKDAY_DAY, Locale.getDefault()).format(new Date());
...
3月16日星期四
Thursday,March 16

DateFormat.getInstanceForSkeleton("MMMEdd", locale1).format(new Date());
...
3月16日星期四
Thursday,Mar 16

DateFormat.getInstanceForSkeleton(DateFormat.YEAR_MONTH_DAY, Locale.getDefault()).format(new Date());
DateFormat.getInstanceForSkeleton(DateFormat.YEAR_MONTH_DAY, Locale.ENGLISH).format(new Date());
...
2023年3月16
March 16,2023

DateFormat.getInstanceForSkeleton("yyyyMMdd", Locale.getDefault()).format(new Date());
...
2023/03/16
03/16/2023

DateFormat.getInstanceForSkeleton("hhmma", Locale.getDefault()).format(new Date());
...
下午7:51
7:51 PM

SimpleDateFormat.getDateInstance(DateFormat.MEDIUM, Locale.getDefault()).format(new Date());
...
2023年03月16日
Mar 16, 2023

// 使用工具类
DateUtils.formatDateTime(context, time, DateUtils.FORMAT_ABBREV_MONTH);
DateUtils.formatDateTime(context, time, DateUtils.FORMAT_SHOW_YEAR);

日期/时间区间:


DateInterval dateInterval = new DateInterval(time1, time2);
DateIntervalFormat.getInstance(DateFormat.YEAR_ABBR_MONTH_DAY, Locale.getDefault()).format(dateInterval, new StringBuffer(""), new FieldPosition(0));
...
2023年3月16日至7月16日
Mar 16 – Jul 16, 2023

数字信息格式


NumberFormat#getInstance、NumberFormat#getCurrencyInstance、NumberFormat#getPercentInstance


说明:


1.这些接口能够解决数字信息格式问题。比如数字显示形式应该是本地字母而不是阿拉伯数字、小数点和千分位符号位置不同等。
2.同时这些接口还能解决货币单位和百分比符号的本地化显示问题。
3.使用其中一些特殊接口还能解决一些特殊数字信息的本地化显示问题。比如使用以下接口可以获取本地语言对应的序数词(第一、第二等)或者序列号(1st、2nd等)。


以下只就说明进行一个简单举例:下面的getInstance可以不传参数,那么就会根据当前系统预设来输出结果。


NumberFormat nf = NumberFormat.getInstance(Locale.FRENCH);
myNumber = nf.parse(myString);

输出当地习惯显示的数字格式,或者当地文字


NumberFormat.getInstance(Locale.getDefault()).format(100001.23);
...
100,000.89
100,000.89

DecimalFormat.getPercentInstance(Locale.getDefault()).format(0.53);
...
53%
53%

CompactDecimalFormat.getInstance(locale, CompactDecimalFormat.CompactStyle.SHORT).format(100000);
...
10万
100K

文件大小及其单位格式


FileSizeUtils#formatFileSize(Context context,long numberBytes)


说明:


1.这个接口能够解决文件大小及其单位格式问题。比如文件大小显示形式应该是KB而不是kB、单位之间是否有空格等。
2.同时这个接口还能根据当前系统预设自动选择合适的单位和精度来显示文件大小。


给出一个简单举例:


public static String formatFileSize(Context context, long sizeBytes); 

将内容大小格式化为字节、千字节、兆字节等形式。 显示的数字,会根据当地习惯,用逗号分开,或者用点号隔开,或者数字用当地字母显示。 文件大小的单位,也会根据当地习惯显示为Mb或者MB等不同形式。


developer.android.com/reference/a…


Android O开始使用标准的单位制 1KB = 1000 bytes


Formatter.formatFileSize(this, 15345);
...
//15.35 KB
//乌克兰语 15,61 КБ

Formatter.formatShortFileSize(this, 15612524);
...
16 MB
16 MB
16 КБ

测量单位


MeasureFormat


在需要显示测量单位的场景,可以使用此接口做好本地化信息展示。以下简单介绍毫升的显示。同样的,其他测量单位,都可以采用类似的方式获取, 只是把参数替换一下即可。


说明: 对于长度,质量,体积,货币、卡路里、ml等测量单位的本地化显示。目前基本都基于翻译拼接来完成。后面建议使用google官方接口来实现。 1.数字+单位


支持的测量单位可参考developer.android.google.cn/reference/a… ,调整传参即可


Measure measure = new Measure(30.5, MeasureUnit.CELSIUS);
MeasureFormat.getInstance(Locale.getDefault(),MeasureFormat.FormatWidth.SHORT).format(measure);
...
30.5°C
30.5°C

Measure measure = new Measure(30.5, MeasureUnit.HOUR);
MeasureFormat.getInstance(Locale.getDefault(),MeasureFormat.FormatWidth.SHORT).format(measure);
...
30.5小时
30.5hr

Measure measure = new Measure(224, MeasureUnit.GIGABYTE);
MeasureFormat.getInstance(Locale.getDefault(),MeasureFormat.FormatWidth.SHORT).format(measure);
...
**224吉字节****数字单位常量中文与预期不符,可能需要单独处理**
224GB

针对部分场景,需要区分数字和单位大小,可以结合MeasureFormat和NumberFormat,分别拿到完整字符串和数字部分字符串,计算index后设置样式即可(注意:不要拆分成两个字符串分别布局)


NumberFormat instance = NumberFormat.getInstance(Locale.getDefault());
String celsius = MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.SHORT, instance).formatMeasures(measure);
String number = instance.format(30.5);
SpannableString spannableString = new SpannableString(celsius);
spannableString.setSpan(new AbsoluteSizeSpan(30, true), celsius.indexOf(number), celsius.indexOf(number) + number.length(), 0);
textView.setText(spannableString);


日期格式符号


说明:获取星期、月份列表等,支持获取format形式和standalone形式,wide、narrow、short


DateFormatSymbols dateFormatSymbols = new DateFormatSymbols(Locale.getDefault);
dateFormatSymbols.getWeekdays();
dateFormatSymbols.getWeekdays(DateFormatSymbols.STANDALONE,DateFormatSymbols.WIDE);
dateFormatSymbols.getMonths();

接口记录:


1、TextUtils#expandTemplate developer.android.google.cn/reference/k…


// string.xml
<string name="storage_size_large_alternate"><xliff:g id="number" example="128">^1</xliff:g> <font size="15"><xliff:g id="unit" example="KB">^2</xliff:g></font></string>

TextUtils.expandTemplate(getText(R.string.storage_size_large_alternate), "128", "GB");

常见问题FAQ


iculocal


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

车载Android程序员的2022年终总结与转行建议

不知不觉间2022年只剩下几天了,这意味着我即将迈入30岁的殿堂,作为一个搞车载的程序员,而立之年感受颇多,借助掘金社区的活动,简单分享一下2022年的个人总结。 回顾2022 2022年对我而言,是很糟的一年,2021年底由于工作和我个人的原因和异地的女友分...
继续阅读 »

不知不觉间2022年只剩下几天了,这意味着我即将迈入30岁的殿堂,作为一个搞车载的程序员,而立之年感受颇多,借助掘金社区的活动,简单分享一下2022年的个人总结。


回顾2022


2022年对我而言,是很糟的一年,2021年底由于工作和我个人的原因和异地的女友分手了,经过一段时间的混乱后,我选择写技术博客来转移自己的注意力。


紧接着2022年年初,我顺利通过公司的内部答辩从 “高级软件工程师” 晋级 “资深软件工程师” ,这是我这一年中最值得开心的事。上家公司是一家上市企业,我个人学历以及工作年限其实并不符合晋升要求,但是得益于我在车载应用领域的文章积累,成为众多候选者中举证材料最完整的人,所以算是破格晋级。


从部门总监那里得知晋级成功的那一刻,还是非常开心的,这样开心的时间大概持续了一周左右,这段时间,我一直在问自己,这样是不是就足够了?还要继续深入应用开发吗?它都有哪些前景呢?


做车载这三年,在公司我一直主要从事车载应用的开发,偶尔也会参与Framework层一些模块的开发,我的晋升依据也是基于我在应用方面的积累,但是我其实一直对Android Framework、Native开发有着相当大的兴趣,但是公司内部预计很难安排相应的工作给到我,所以经过艰难的权衡,我还是决定在2022年10月离职了。


离职之后,简单尝试了几个互联网公司的面试,主要还是验证自己的一些想法,这里列举几个我印象比较深的问题。


Q:应用的换肤机制和原理


A:不知道!


车载应用虽然有换肤的需求,但是车载系统应用是基于Android Runtime Resource Overlay实现换肤,不过这套机制并不支持非系统应用,导致车载应用与移动端应用实现换肤的技术手段完全不同,所以我并没有实践过移动端应用的换肤,也就谈不上理解原理了。


结果:挂了!


Q:有没有使用过Flutter,描述一下它的绘制原理


A:额,同样不知道!!


国内主流的车载IVI系统,基本都是Android OS,也没听过车载系统应用有跨平台的需求,所以Flutter在车载应用开发领域使用的很有限,以至于做了三年车载应用开发,完全没有接触过Flutter。


结果:不出意外,挂了!


Q:插件化的原理


A:不造啊!!!


车载应用的一个重要特点就是交互逻辑要尽可能简单,方便驾驶员操作,所以车载应用一般页面都很少,基本用不上那么插件化这么复杂的工具。


结果:当然还是挂了!


简单的几次面试,基本验证了我的想法是合理的,移动端应用和车载应用开发还是有不少区别的,想进来其实并不难,但是做久了想出去就难了。


展望2023



“如果我不能教会新手,那么就说明我没有真正理解这个知识”


最近在B站看到的费曼学习法启发了我,最高效的学习方式就是,将学到的内容化繁为简的教会别人。


2022年我大约写了18篇技术性博客,但是多数时候,我都是把它当做对学习或工作内容的一次总结,回过头再来看这些内容,总结的质量差,大面积的拼凑了源码,阅读起来枯燥又乏味,而且缺乏提炼和总结,仿佛写了很多,但是抓不到重点。


所以,2023年要继续磨炼自己的写作技巧,把文章内容写得更加浅显易懂,同时也准备在B站制作一些视频教程,锻炼自己的总结能力。


转行车载的建议


为什么要写换行建议?因为总会有人问我,做手机开发如何转行车载开发,那么就在这里简单提一下。


APP工程师


基于我这次找工作的经验来看,APP工程师如果希望转行Framework岗位,实际工作经验偏少的话基本不现实,所以只能考虑转行车载APP工程师。


但是要注意的是,车载应用与移动端的应用开发还是存在一些不同的,例如开发一个车载系统设置、或SystemUI应用,使用的API往往是Android SDK没有暴露的API,手机应用开发基本不会涉及这块的内容,所以各个公司往往更愿意招聘有一定车载经验的程序员。


但是,手机应用开发转车载不代表没有机会,一般只要实力过硬,转行成功的概率还是非常大的。所以还是建议提升自己应用开发实力,写一些博客、做一些开源项目,有空闲时间的再看一下原生SystemUI、Launher、系统设置这些应用是如何实现的,都是非常不错的加分项目。


当然如果有一个211或985的学历,那就另当别论了,学历永远是硬实力!


Framework或Native/HAL工程师


从事FW或Native/HAL的工程师转行车载没什么特别值得犹豫的,FW/Native工程师一直都是车载稀缺的岗位,主机厂商也愿意接收没有车载经验的FW/Native工程师,如果转行之后薪资会迎来一波提升,那么直接转就行。


小结


以上就我的2022年终总结了,需要指出的是,文中提到的我个人的职业规划并不适用于大多数人,而且属于一个潜在风险很大的发展路径,极容易变成什么都会什么都不精,所以对于多数人我建议还是做深度发展,而不是横向发展。


写这篇总结的时候,惊讶的发觉我开始低烧了,寄,不会刚上一周的班就阳了吧。吃药休息了,咱们2023年再见吧!



作者:林栩link
来源:juejin.cn/post/7178398167954620474
收起阅读 »

错过了前三次的伟大革命(蒸汽机革命、电气革命、信息革命),GPT-4 你还要错过人工智能AI革命吗!!!

错过了前三次的伟大革命(蒸汽机革命、电气革命、信息革命),GPT-4 你还要错过人工智能AI革命吗!!! 看 GPT-4 是如何炼成绝世神功:葵花宝典? 欲练神功,不必自宫。西方不败——GPT-4! GPT-4 is OpenAI’s most advanc...
继续阅读 »

错过了前三次的伟大革命(蒸汽机革命、电气革命、信息革命),GPT-4 你还要错过人工智能AI革命吗!!!



看 GPT-4 是如何炼成绝世神功:葵花宝典?


欲练神功,不必自宫。西方不败——GPT-4!


GPT-4 is OpenAI’s most advanced system, producing safer and more useful responses


GPT-4 是 OpenAI 最先进的系统,可产生更安全、更有用的响应



image-20230317095643129.png


更牛的问题解决能力


GPT-4由于其更广泛的通用知识和解决问题的能力,可以更准确地解决难题。


Creativity 创造力


GPT-4 比以往任何时候都更具创造性和协作性。它可以生成、编辑并与用户一起完成创意和技术写作任务,例如创作歌曲、编写剧本或学习用户的写作风格。


GPT-4 Input 输入:


Explain the plot of Cinderella in a sentence where each word has to begin with the next letter in the alphabet from A to Z, without repeating any letters.
用一个句子解释灰姑娘的情节,其中每个单词必须以字母表中从 A 到 Z 的下一个字母开头,不重复任何字母。

GPT-4 Output 输出:


A beautiful Cinderella, dwelling eagerly, finally gains happiness; inspiring jealous kin, love magically nurtures opulent prince; quietly rescues, slipper triumphs, uniting very wondrously, xenial youth zealously.
美丽的灰姑娘,渴望安居,终于获得幸福;激起嫉妒的亲人,爱情神奇地孕育出富贵的王子;悄悄拯救,拖鞋胜利,团结无比,热心青年。

ChatGPT Input 输入:


Explain the plot of Cinderella in a sentence where each word has to begin with the next letter in the alphabet from A to Z, without repeating any letters.
用一个句子解释灰姑娘的情节,其中每个单词必须以字母表中从 A 到 Z 的下一个字母开头,不重复任何字母。

ChatGPT Output 输出:


After attending a ball, Cinderella dances enchantingly for good-hearted King, leaving midnight noticed or perhaps quickly returning slipper to unite victoriously with prince, resulting in ultimate victory with wonderful, extravagant, youthful zest.
参加舞会后,灰姑娘为好心的国王翩翩起舞,留下午夜的注意,或者迅速返回拖鞋,与王子胜利团结,以美妙、奢侈、青春的热情取得最终胜利。

image-20230317084912093.png


对比看下哪个更牛~


Visual input 视觉输入


GPT-4 can accept images as inputs and generate captions, classifications, and analyses.


GPT-4 可以接受图像作为输入并生成说明、分类和分析。下面是接受一个图像的输入之后,生成图像说明、分类和分析的输出结果:


image-20230317085635694.png


Input 输入:


What can I make with these ingredients?
我可以用这些原料做什么?

Output 输出:


你可以用这些原料做很多选择!一些可能性包括: 
- 煎饼或华夫饼
- 可丽饼
- 法式吐司
- 煎蛋卷或菜肉馅煎蛋饼
- 乳蛋饼
- 奶油冻或布丁
- 蛋糕或纸杯蛋糕
- 松饼或面包
- 饼干或饼干
这些只是几个例子,但可能性是无穷无尽的!

有木有很牛逼~


能够识别分析图像那就越来越接近人类了,不但可以对话,还可以感知外在的世界,如果可以聊天时有脑有嘴的话,GPT-4那现在就是有眼了,有视觉能力了。当一个机器有视觉能力之后会发生什么???大家可与脑洞一下~


有视觉能力这是GPT-4有别于前几代GPT重大的升级,也是人工智能发展到另一个新的里程碑的意义。AI 革命现在不是未来可期了,而是未来已来了,就发生在你我所处的这个共同的时代,老铁们还不赶紧上车 (观众公众号“李荣生”,了解及时准确全面的GPT的进展,ChatGPT新技术的在业务中的调教、应用和体验)


错过了前三次的伟大革命(蒸汽机革命、电气革命、信息革命),你还要错过人工智能AI革命吗!!!


Longer context 更长的上下文


GPT-4 能够处理超过 25,000 个单词的文本,允许使用长格式内容创建、扩展对话以及文档搜索和分析等用例。


Input 输入:


[Text from: https://en.wikipedia.org/wiki/Rihanna]

Describe her Super Bowl performance and what made it special.
描述她在超级碗比赛中的表现以及它的特别之处。

Output 输出:


Rihanna's Super Bowl LVII halftime show performance was special for a number of reasons. For one, it marked her first live performance in over five years, and it was her first time headlining the Super Bowl halftime show. Additionally, she made history by becoming the first person to perform at the Super Bowl while pregnant. The show itself was critically acclaimed, with many praising her high-energy performance and impressive vocals. It also garnered a massive viewership, becoming the second most-watched halftime show in history.
出于多种原因,蕾哈娜 (Rihanna) 的超级碗 LVII 中场秀表演很特别。首先,这是她五年多来的第一次现场表演,也是她第一次登上超级碗中场秀的头条新闻。此外,她还创造了历史,成为第一个在怀孕期间在超级碗上表演的人。该节目本身广受好评,许多人称赞她充满活力的表演和令人印象深刻的歌声。它还获得了巨大的收视率,成为历史上收视率第二高的中场秀。


备注: Super Bowl 超级碗是美国职业橄榄球联盟(NFL)每年举行的年度总决赛,通常在每年的二月初举行,是美国和全球最受关注和观看的单一体育赛事之一。



可以接收wikipedia网址地址的输入,并经过分析之后归纳蕾哈娜 (Rihanna)比赛的表现和特别之处。为解决部分老铁不方便之门,我把wikipedia地址的部分内容截图如下:是个美女哦~


image-20230317092527923.png


可以接收更多文字的输入,包括网址地址的搜索和分析,经过理解之后可以根据prompt提示输出,理解问题的能力更强大了。


GPT-4的能力总结一句话:会说、会看、更懂你!!!


更牛的推理能力


GPT-4 的高级推理能力超越了 ChatGPT,ChatGPT VS GPT-4 推理能力对比:


image-20230317083206574.png


ChatGPT Input 输入:


Andrew从上午11点到下午3点有空,Joanne从中午到下午2点,然后下午3:30到5点有空。Hannah在中午有半个小时的时间,然后下午4点到6点。Andrew、Hannah和Joanne 30分钟会议的开始时间有哪些选择?

ChatGPT Output 输出:


以下是它们的可用性列表:
-安德鲁:上午11点至下午3点
-乔安妮:中午至下午2点,下午3:30至下午5点
-汉娜:中午至下午12:30,下午4点至6点

根据他们的可用性,他们三人都有30分钟的可用时间,从下午4点到下午4点30分。因此,会议可以安排在下午4点。

ChatGPT-4 Input 输入:


Andrew从上午11点到下午3点有空,Joanne从中午到下午2点,然后下午3:30到5点有空。Hannah在中午有半个小时的时间,然后下午4点到6点。Andrew、Hannah和Joanne 30分钟会议的开始时间有哪些选择?

ChatGPT-4 Output 输出:


安德鲁:上午11点至下午3点
乔安妮:下午12点至2点,下午3:30至5点
汉娜:下午12点至12点30分,下午4点至6点

30分钟会议的常见时间:中午12点至中午12点30分

ChatGPT VS GPT-4 考试能力对比


参加 美国 (Uniform Bar Exam 统一律师考试)和(Biology Olympiad 生物奥林匹克竞赛考试)的对比,GPT-4 通过在测试中获得更高的近乎一百分满分数证明优于 ChatGPT。


image-20230317081742335.png



备注: Uniform Bar Exam(普通考试统一律师资格考试),简称UBE,是一个跨越美国多个州的法律考试。它是由具有法律职业监管权的各州联合开发的,旨在便捷地实现律师的跨州执业。通过UBE,考生只需参加一次考试,就能将成绩用于多个州的律师资格申请。UBE考试由三个部分组成,包括多项选择题、法律分析和写作。考试的题材涵盖了民法、刑法和司法程序等多个领域的知识。通过UBE考试是律师在跨州执业时的重要成就之一,同时也证明了其掌握了足够的法律知识和技能,能够为客户提供高质量的法律服务。


Biology Olympiad(生物奥林匹克竞赛)是一个面向中学生的国际生物学竞赛。它由国际生物学奥林匹克委员会(IBO)组织,旨在鼓励中学生研究生物学、发展科学技能以及推动全球青少年对生物学的兴趣。每年,参赛国家都会选派最优秀的中学生代表参加IBO大赛。本竞赛通常包括一系列基础和高级的生物学知识测试、实验考察和科学研究等项目。参加Biology Olympiad不仅能锻炼和展示学生的生物学技能,还可以帮助学生获得在全球范围内访问高水平生物学教育和研究资源的机会。



GPT-X 进化中


遵循 GPT、GPT-2 和 GPT-3 的研究路径,我们的深度学习方法利用更多数据和更多计算来创建越来越复杂和强大的语言模型。


gpt-4-motif-20230317081633225.svg


GPT-4进化之旅


We spent 6 months making GPT-4 safer and more aligned. GPT-4 is 82% less likely to respond to requests for disallowed content and 40% more likely to produce factual responses than GPT-3.5 on our internal evaluations.


花费了6个月的时间来确保GPT-4更加安全和准确。我们对GPT-4进行了多次内部测试和修改,使其回答不被允许的内容请求的可能性降低了82%,同时在产生事实回答时的准确率比GPT-3.5提高了40%。这意味着我们对GPT-4进行了改进,解决了其可能存在的问题,并提高了它的准确性和可靠性,使其更适合用于公共平台。


简单一句话就是:我更牛逼了~。更要命的是,这个家伙还不断迭代、不断进化中~


欲练神功,不必自宫,是否会炼就为真正的一个——西方不败(GTP-X)?


如果做到更安全和更准确?


Safety & alignment 他们(OpenAI)是这样做的:


人工反馈训练


我们在训练GPT-4时,吸收了更多人类反馈,包括ChatGPT用户提交的反馈,以改善GPT-4的行为。我们还与超过50位专家合作,包括AI安全和安全领域的专家,进行早期反馈。


通过真实世界的使用不断改进


我们将从以前模型在真实世界使用时的经验教训中,应用到GPT-4的安全研究和监控系统中。就像ChatGPT一样,随着越来越多的人使用GPT-4,我们也将以定期的节奏更新和改进它。


GPT-4协助的安全研究


GPT-4的高级推理和指令遵循能力加速了我们的安全工作。我们使用GPT-4来创建训练数据,进行模型微调,并在训练、评估和监控中迭代分类器。


有谁在使用 GPT-4 能力?


使用 GPT-4 构建创新产品的组织合作包括:


Stripe互联网支付平台 Stripe 利用 GPT-4 来简化用户体验并打击欺诈。


Morgan Stanley华尔街摩根士丹利投资银行 摩根士丹利财富管理部署 GPT-4 来组织其庞大的知识库。


Khan Academy全球在线平台可汗学院 可汗学院在有限的试点项目中探索 GPT-4 的潜力


Government of Iceland冰岛政府 冰岛如何使用 GPT-4 来保护其语言。


Duolingo GPT-4 加深了 Duolingo 上的对话。Duolingo是一款流行的语言学习应用程序,允许用户以游戏化的方式学习一系列不同的语言。Duolingo 提供了一个互动平台,通过其独特的教学方法和游戏元素,让用户在轻松有趣的环境中学习语言。Duolingo的目标是帮助人们学习语言、提高沟通技能和拓宽对其他文化的理解。它同时也为用户提供一个可持续的、个性化的学习体验。Duolingo 影响了全球数百万语言学习者,并在广泛的年龄范围内取得了成功。


Be My Eyes 利用GPT-4技术改变了视觉辅助功能。Be My Eyes是一款志愿者和盲人用户之间的求助平台。该平台让盲人用户请求帮助时,匹配到愿意帮助的志愿者。使用GPT-4技术,Be My Eyes可以识别图像并实时将描述发送给需要帮助的盲人用户。这使得盲人用户可以像有视力的人一样,获取到更多关于周围环境的信息。


通过在技术上方便快捷地连接盲人用户和有视力的志愿者,Be My Eyes广泛地改变着视觉障碍群体的生活中的社交和日常方面。这项技术帮助他们更好地融入社会,增强了他们的自信和独立性。最重要的是,Be My Eyes通过与GPT-4技术的整合,成为了利用人工智能的科技慈善的优秀典范。


还有下面其他应用案例:


image-20230317074524959.png


gpt-4-motif.svg


关于 GPT-4 的更多信息


More on GPT-4 :


Research 研究GPT-4 是 OpenAI 在深度学习扩展方面的最新里程碑。详情请查看 GPT-4 研究
Infrastructure基础设施GPT-4 是在微软 Azure AI 超级计算机上进行训练的。Azure 的人工智能优化基础设施还使我们能够向全球用户提供 GPT-4。
Limitations 限制GPT-4 仍然存在许多已知的限制,例如社会偏见、幻觉和对抗性提示,我们正在努力解决这些问题。随着社会采用这些模型,我们鼓励和促进透明度、用户教育和更广泛的人工智能素养。我们还旨在扩大人们在塑造我们的模型方面的输入途径。
Availability 可用性GPT-4 在 ChatGPT Plus 上可用,并作为 API 提供给开发人员构建应用和服务。

We’re excited to see how people use GPT-4 as we work towards developing technologies that empower everyone.


我们很期待看到人们如何使用 GPT-4,同时我们将努力开发能够赋予每个人权力的技术。


View contributions


GPT-4 API 候补名单


We’re making GPT-4 available as an API for developers to build applications and services.


我们将 GPT-4 作为 API 供开发人员构建应用程序和服务。


Join waitlist 加入候补名单


image-20230317073049107.png


加入候补名单地址:openai.com/waitlist/gp…


Availability 可用性


During the gradual rollout of GPT-4, we’re prioritizing API access to developers that contribute exceptional model evaluations to OpenAI Evals to learn how we can improve the model for everyone. We are processing requests for the 8K and 32K engines at different rates based on capacity, so you may receive access to them at different times. Researchers studying the societal impact of AI or AI alignment issues can also apply for subsidized access via our Researcher Access Program.


在 GPT-4 的逐步推出期间,我们优先考虑为 OpenAI Evals 贡献卓越模型评估的开发人员访问 API,以了解我们如何为每个人改进模型。我们正在根据容量以不同的速率处理对 8K 和 32K 引擎的请求,因此您可能会在不同时间获得对它们的访问权限。研究 AI 的社会影响或 AI 对齐问题的研究人员也可以通过我们的研究人员访问计划申请补贴访问。


For use cases we tried, GPT-3.5 did not reliably handle multi-language text. We hope to explore GPT-4 for this use case.


对于我们尝试的用例,GPT-3.5不能可靠地处理多语言文本。我们希望为这个用例探索GPT-4。


作者:KonyLee
来源:juejin.cn/post/7211734661762973733
收起阅读 »

百度的文心一言 没有想象中那么差

robin 的演示 我们用 robin 的演示例子来对比一下 文心一言和 ChatGPT 的真实表现(毕竟发布会上是录的)。 注意,我使用的 GPT 版本是 4.0 文学创作 1 三体的作者是哪里人? 文心一言: ChatGPT: 嗯,中文表现上文心一言更...
继续阅读 »

robin 的演示


我们用 robin 的演示例子来对比一下 文心一言和 ChatGPT 的真实表现(毕竟发布会上是录的)。 注意,我使用的 GPT 版本是 4.0


文学创作


1 三体的作者是哪里人?


文心一言:


ChatGPT:


嗯,中文表现上文心一言更好。


2 电视剧三体的演员都有谁?


文心一言:


ChatGPT:



关于这个问题 ,由于 ChatGPT 的数据只能查到 2021 年的,所以它不知道是正常的。


3 主演 于和伟 和 张鲁一 谁更高?


文心一言:


ChatGPT:


4 可以总结一下《三体》的核心内容吗?如果要续写的话,可以从哪些角度出发?


文心一言:


ChatGPT:



虽然网络出一点儿问题,但我更喜欢 ChatGPT 的回答。


商业文案创作


1 如果要成立一个用大模型服务中小企业数字化升级的科技服务公司,可以起个什么公司名?


文心一言:


ChatGPT:


显然我更喜欢文心一言的,但如果你跟 ChatGP 继续聊下去,它可能提供更进一步符合你需求的答案。


2 数智云图这个名称不错,给我起一个公司的服务 Slogan,表达共赢的概念


文心一言:


ChatGPT:



这一轮文心一言的回答更好。


3 帮我生成一篇公司成立的新闻稿,数智云图以共赢的服务理念用大模型服务中小企业数字化升级。字数 600 字


文心一言:


ChatGPT:


ChatGPT 试了几次网络都有问题,这一轮不好评价。


数理逻辑推算任务


1 鸡兔同笼问题


文心一言:


ChatGPT:



这一轮没有意外,我更喜欢 ChatGPT 的回答。


中文理解能力


1 “洛阳纸贵”是什么意思?


文心一言:


ChatGPT:


感觉差不多。


2 当时洛阳的纸到底有多贵?


文心一言:


ChatGPT:


那么这一题,ChatGPT 开始一本正经的胡说八道了。


3 这个成语在现在的经济学原理里,对应的理论是什么?


文心一言:


ChatGPT:


4 用洛阳纸贵四个字写一首藏头诗。


文心一言:



ChatGPT:


ChatGPT 给出的结果明显不对。


多模态生成


目前文心一言的测试版本并不能生成语音和视频,但是可以直接生成图片,图片的质量比想象中的要好,而且还有很多的风格可以选择,比如说卡通风格,油画风格,还有很多的风格,可以满足不同的需求。


绘画能力应该是集成了现成的文心一格。


以下是我试的几个例子


描述: 请为 2023 世界智能交通大会创作一张海报。



描述:“灌木丛中的一朵机械花,有金属花瓣,周围环境和人的镜面反射,鸟瞰图。 构图夸张,具有强烈的视觉冲击力和叙事性”



描述:“雨天香港、哥特式建筑 3D 画风”



描述:“一只睡在柜子上面的猫,卡通风格”



描述:“麦田中的少年,油画风”


坦率讲与 midjourney 的绘画能力相比,文心一格的绘画能力还是有差距的。


编程


1 请帮我写一个网页版的贪吃蛇游戏


文心一言:


ChatGPT:


虽然又遇到了网络问题,但各位开发老铁们,不用我说了吧,都知道该选啥哈


2 生成测试数据


文心一言:



ChatGPT:


ChatGPT 完胜


文心一言使用注意事项


在使用过程中出现了排队的情况:



这我在使用 ChatGPT 的时候可没有遇到过。


可以输入“/” 来获取模版



绘画的例子上文举过了,我们来看看剩下 2 个:


查一个知识



写一篇报告



总结


经过试用文心一言,再对比 ChatGPT,我认为:文心在中文语料上应该是更丰富些。多语言上目前一定不如 ChatGPT 优秀。虽然这两个模型在某些方面有所重叠,但它们在应对特定语言和领域问题时具有各自的优势。


其实最令我意外的是,文心一言并没有发布会时让人感觉的那么差。它不是 chatPPT, 至少目前看不是,它完成了从 0 到 1 的过程 ,虽然有差距,但还是真心地希望国内的企业能够在 AI 的领域做出一些成绩,而不是一味地跟风。




我现在理解了 😊


作者:xiaohezi
来源:juejin.cn/post/7211467514413367351
收起阅读 »

用了这两款插件,同事再也不说我代码写的烂了

大家好,我是风筝同事:你的代码写的不行啊,不够规范啊。我:我写的代码怎么可能不规范,不要胡说。于是同事打开我的 IDEA ,安装了一个插件,然后执行了一下,规范不规范,看报告吧。这可怎么是好,这玩意竟然给我挑出来这么多问题,到底靠谱不。同事潇洒的走掉了,只留下...
继续阅读 »

大家好,我是风筝

同事:你的代码写的不行啊,不够规范啊。

我:我写的代码怎么可能不规范,不要胡说。

于是同事打开我的 IDEA ,安装了一个插件,然后执行了一下,规范不规范,看报告吧。

这可怎么是好,这玩意竟然给我挑出来这么多问题,到底靠谱不。

同事潇洒的走掉了,只留下我在座位上盯着屏幕惊慌失措。我仔细的查看了这个报告的每一项,越看越觉得这插件指出的问题有道理,果然是我大意了,竟然还给我挑出一个 bug 来。

这是什么插件,review 代码无敌了。

这个插件就是 SonarLint,官网的 Slogan 是 clean code begins in your IDE with {SonarLint}

作为一个程序员,我们当然希望自己写的代码无懈可击了,但是由于种种原因,有一些问题甚至bug都无法避免,尤其是刚接触开发不久的同学,也有很多有着多年开发经验的程序员同样会有一些不好的代码习惯。

代码质量和代码规范首先肯定是靠程序员自身的水平和素养决定的,但是提高水平的是需要方法的,方法就有很多了,比如参考大厂的规范和代码、比如有大佬带着,剩下的就靠平时的一点点积累了,而一些好用的插件能够时时刻刻提醒我们什么是好的代码规范,什么是好的代码。

SonarLint 就是这样一款好用的插件,它可以实时帮我们 review代码,甚至可以发现代码中潜在的问题并提供解决方案。

SonarLint 使用静态代码分析技术来检测代码中的常见错误和漏洞。例如,它可以检测空指针引用、类型转换错误、重复代码和逻辑错误等。这些都是常见的问题,但是有时候很难发现。使用 SonarLint 插件,可以在编写代码的同时发现这些问题,并及时纠正它们,这有助于避免这些问题影响应用程序的稳定性。

比如下面这段代码没有结束循环的条件设置,SonarLint 就给出提示了,有强迫症的能受的了这红下划线在这儿?

SonarLint 插件可以帮助我提高代码的可读性。代码应该易于阅读和理解,这有助于其他开发人员更轻松地维护和修改代码。 SonarLint 插件可以检测代码中的代码坏味道,例如不必要的注释、过长的函数和变量名不具有描述性等等。通过使用 SonarLint 插件,可以更好地了解如何编写清晰、简洁和易于理解的代码。

例如下面这个名称为 hello_world的静态 final变量,SonarLint 给出了两项建议。

  1. 因为变量没有被使用过,建议移除;
  2. 静态不可变变量名称不符合规范;

SonarLint 插件可以帮助我遵循最佳实践和标准。编写符合标准和最佳实践的代码可以确保应用程序的质量和可靠性。 SonarLint 插件可以检测代码中的违反规则的地方,例如不安全的类型转换、未使用的变量和方法、不正确的异常处理等等。通过使用 SonarLint 插件,可以学习如何编写符合最佳实践和标准的代码,并使代码更加健壮和可靠。

例如下面的异常抛出方式,直接抛出了 Exception,然后 SonarLint 建议不要使用 Exception,而是自定义一个异常,自定义的异常可能让人直观的看出这个异常是干什么的,而不是 Exception基本类型导出传递。

安装 SonarLint

可以直接打开 IDEA 设置 -> Plugins,在 MarketPlace中搜索SonarLint,直接安装就可以。

还可以直接在官网下载,打开页面https://www.sonarsource.com/products/sonarlint/,在页面中可以看到多种语言、多种开发工具的下载图标,点击下方的 EXPLORE即可到下载页面去下载了。虽然我们只是在 IDEA 中使用,但是它不管支持 Java 、不只支持 IDEA ,还支持 Python、PHP等众多语言,以及 Visual Studio 、VS Code 等众多 IDE。

在 IDEA 中使用

SonarLint 插件安装好之后,默认就开启了实时分析的功能,就跟智能提示的功能一样,随着你噼里啪啦的敲键盘,SonarLint插件就默默的进行分析,一旦发现问题就会以红框、红波浪线、黄波浪线的方式提示。

当然你也可以在某一文件中点击右键,也可在项目根目录点击右键,在弹出菜单中点击Analyze with SonarLint,对当前文件或整个项目进行分析。

分析结束后,会生成分析报告。

左侧是对各个文件的分析结果,右侧是对这个问题的建议和修改示例。

SonarLint 对问题分成了三种类型

类型说明
Bug代码中的 bug,影响程序运行
Vulnerability漏洞,可能被作为攻击入口
Code smell代码意味,可能影响代码可维护性

问题按照严重程度分为5类

严重性说明
BLOCKER已经影响程序正常运行了,不改不行
CRITICAL可能会影响程序运行,可能威胁程序安全,一般也是不改不行
MAJOR代码质量问题,但是比较严重
MINOR同样是代码质量问题,但是严重程度较低
INFO一些友好的建议

SonarQube

SonarLint 是在 IDE 层面进行分析的插件,另外还可以使用 SonarQube功能,它以一个 web 的形式展现,可以为整个开发团队的项目提供一个web可视化的效果。并且可以和 CI\CD 等部署工具集成,在发版前提供代码分析。

SonarQube是一个 Java 项目,你可以在官网下载项目本地启动,也可以以 docker 的方式启动。之后可以在 IDEA 中配置全局 SonarQube配置。

也可以在 SonarQube web 中单独配置一个项目,创建好项目后,直接将 mvn 命令在待分析的项目中执行,即可生成对应项目的分析报告,然后在 SonarQube web 中查看。

5

对于绝大多数开发者和开发团队来说,SonarQube 其实是没有必要的,只要我们每个人都解决了 IDE 中 SonarLint 给出的建议,当然最终的代码质量就是符合标准的。

阿里 Java 规约插件

每一个开发团队都有团队内部的代码规范,比如变量命名、注释格式、以及各种类库的使用方式等等。阿里一直在更新 Java 版的阿里巴巴开发者手册,有什么泰山版、终极版,想必各位都听过吧,里面的规约如果开发者都能遵守,那别人恐怕再没办法 diss 你的代码不规范了。

对应这个开发手册的语言层面的规范,阿里也出了一款 IDEA 插件,叫做 Alibaba Java Coding Guidelines,可以在插件商店直接下载。

比如前面说的那个 hello_world变量名,插件直接提示「修正为以下划线分隔的大写模式」。

再比如一些注释上的提示,不建议使用行尾注释。

image-20230314165107639

还有,比如对线程池的使用,有根据规范建议的内容,建议自己定义核心线程数和最大线程数等参数,不建议使用 Excutors工具类。

有了这俩插件,看谁还能说我代码写的不规范了。

作者:古时的风筝
来源:juejin.cn/post/7211151196328804408
收起阅读 »

来看看这个很酷的按钮交互效果

web
今天分享一个很有特色的按钮交互效果,如封面图所示,保证让你停不下来,原作者是Adam Kuhn,有兴趣的可以去codepen体验,地址:codepen,本文将核心功能逐一讲解。 基于这个动图可以将主要实现的几个功能点拆分为以下几点: 按钮的径向渐变背景色可以...
继续阅读 »

今天分享一个很有特色的按钮交互效果,如封面图所示,保证让你停不下来,原作者是Adam Kuhn,有兴趣的可以去codepen体验,地址:codepen,本文将核心功能逐一讲解。


基于这个动图可以将主要实现的几个功能点拆分为以下几点:



  • 按钮的径向渐变背景色可以随着鼠标的移动变化

  • 按钮的背景区域会随着鼠标的移动产生弹性变化效果

  • 按钮的文字阴影会随着鼠标的变化而变化


鼠标位置获取


在正式开始前做一些准备工作,分析主要的这几个功能点可以发现每个功能都和鼠标的移动有关,都需要借助于鼠标移动的坐标,所以我们首先获取鼠标的位置并传递到css中,代码如下:


document.querySelectorAll(".inner").forEach((button) => {
button.onmousemove = (e) => {
const target = e.target;
const rect = target.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;

button.style.setProperty("--x", `${x}px`);
button.style.setProperty("--y", `${y}px`);
button.style.setProperty("--height", `${rect.height}px`);
button.style.setProperty("--width", `${rect.width}px`);
};
});

这里除开传递鼠标的位置,还传递了当前按钮的宽高用于后续按钮文案阴影的依赖。


径向渐变背景动起来


背景色默认是纯色,随着鼠标的产生变化,所以这里和两个关键点有关,鼠标移入hover,移动过程中的坐标变化。实现过程核心是通过background定义两个背景色,默认的显示部分background-size是100%,渐变部分的background-size是0,待hover时设置为100%,这时就会显示渐变背景色内容了。


  background: 
// 渐变背景色
radial-gradient(
circle at center,
var(--lightest),
var(--light) 5%,
var(--dark) 30%,
var(--darkest) 50%
),
// 默认显示背景色
var(--darkest);
background-size: 0px 0px, 100%;

:hover {
background-size: 100%, 100%;
}

显示之后要动起来,基于js传入的坐标值应用到transformtranslate平移,这里注意移动是要基于当前元素的中心点位所以x和y都要减去自身的50%。


transform: translate(calc(var(--x) - 50%), calc(var(--y) - 50%));

如图所示,绿色区域是按钮部分,整个背景的中心点要和鼠标移动的坐标一致,所以要减去自身宽高的各一半。还有一点需要注意的是不能在移动的过程中让背景色漏出,所以背景区域是整个按钮的2倍。



这时整个背景区域很大,这里使用了CSS3的混合模式mix-blend-mode: lighten,最终只会应用亮色部分也就是中间的绿色区域。这里的混合模式给下一步中的弹性伸缩效果起到重要的作用。


此时的效果就是这样的,原代码在此基础上还增加了transition和filter体验让效果更佳,因涉及篇幅较长这里就不一一说明了,



背景区域弹性变化交互效果


背景弹性交互效果需要增加一个元素,与当前按钮同级别。此时的html如下:


<div class="inner">
<button type="button">南城FEbutton>
<div class="blob">div>
div>

blob元素和button都使用了绝对定位,因为按钮上面有文字,所以层级上button更高。blob元素增加了两个伪元素,先看after


&:after {
width: calc(100% - 4rem);
height: calc(100% - 4rem);
top: 2rem;
left: 2rem;
border-radius: 5rem;
box-shadow: 0 0 0 8rem #fff;
}

基于当前界面减少实际按钮的区域,并通过定位居中,再通过box-shadow填充白色背景,还增加了圆角,此时按钮的背景变成如下所示,按钮的雏形已经有了。



然后before主要也是通过box-shadow来增加额外的元素显示,分为三个部分,中间部分跟随鼠标移动,上下两个部分为鼠标移动到边界的反向效果区域。核心代码如下:


box-shadow: 0 0 0 0.75rem #fff, 0 -8rem 0 2rem #fff, 0 8rem 0 2rem #fff;

再配合基于js传入的坐标值应用到translate平移,box-shadow部分的内容即可跟随鼠标动起来了。这里用到了一个css3的函数clamp,它可以用来限制一个值的范围。clamp函数接受三个参数,分别表示最小值、推荐值和最大值。函数的返回值为推荐值,但是它会被限制在最小值和最大值之间。所以这里超出按钮的显示区域会有临界点,不会完全脱离,核心代码如下:


transform: translate(
clamp(5%, calc(var(--x) - 50%), 550%),
clamp(1rem, calc(var(--y) - 50%), 5rem)
);

此时按钮的效果如下,圆形部分即是上面的0 0 0 0.75rem #fff,下面的半圆即是0 8rem 0 2rem #fff,因为增加了圆角border-radius: 100%所以都是圆形。为什么下面的是半圆白色,因为after中的box-shadow白色背景遮挡了,所以不会完全显示,又因为是白色阴影加上混合模式所以这块区域以亮色白色显示。



是不是和目标效果有些接近了,加上一行关键代码即可。


filter: blur(12px) contrast(50);

这里使用filter属性处理,首先对元素进行模糊处理,如果只是增加模糊的效果如下,可以看到增加的伪元素圆形都被磨平了,完美的融入到了按钮本身的背景色中。



再加上contrast调整元素的对比度即可达到最终的效果,这里切记执行的顺序不能写反。在CSS中 filter 属性中的函数是按照从左到右的顺序执行的。如果你在 filter 属性中使用了多个函数,那么它们会按照从左到右的顺序依次执行。



按钮的文字阴影变化


文字的阴影变化主要是改变其水平和垂直的偏移量,以及模糊半径,这里就要用到最开始传入的按钮宽高的数据了,因为偏移量的计算会基于整个按钮的面积,这样才会显得更逼真。


先看水平和垂直的偏移量,核心还是基于clamp函数,设置最小值,最大值,中间的推荐值则会随着鼠标的坐标值变化而变化,具体的数值有兴趣的可以调整体验,以下是文字阴影的水平和垂直的偏移量计算的代码:


clamp(-6px, calc((var(--width) / 2 - var(--x)) / 12), 6px)
clamp(-4px, calc((var(--height) / 2 - var(--y)) / 16), 4px)

然后是模糊半径的计算,这里用到了max函数,最大取5px,其他情况基于坐标值和宽高计算得出。


max(
calc((var(--width) / 2 - var(--x)) / 8 +
((var(--height) / 2 - var(--y)) / 3)),
calc((
((var(--width) / 2 - var(--x)) / 8) +
((var(--height) / 2 - var(--y)) / 3)
) * -1
),
5px
)

最终的效果如下:



最后


到此整个核心的实现过程就结束了,整个代码中我们使用了box-shadowtext-shadowmix-blend-modefilter等属性,还有CSS3函数maxclampcalc。还有transition动画相关没有说明,涉及的知识点比较多,有兴趣的同学可以看源码了解。


在线代码预览:



到此本文就结束了,看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~



作者:南城FE
来源:juejin.cn/post/7212516589060849720
收起阅读 »

Android应用被抓包?防护手段需知道

为了提高网络数据传输的安全性,业内采用HTTPS的方式取代原来的HTTP,Android的应用开发也不例外,我们似乎只需要修改一下域名就能完成http到https的切换,无需做其他额外的操作,那么这个HTTPS是如何实现的?是否真的就安全了?在不同的Andro...
继续阅读 »

为了提高网络数据传输的安全性,业内采用HTTPS的方式取代原来的HTTP,Android的应用开发也不例外,我们似乎只需要修改一下域名就能完成http到https的切换,无需做其他额外的操作,那么这个HTTPS是如何实现的?是否真的就安全了?在不同的Android版本上是否有差异?今天我们就来详细研究一下以上的问题。


Tips:本篇旨在讨论HTTPS传输的安全性,应用本地安全不在讨论范畴。


HTTPS原理



诞生背景



首先就是老生常谈的问题,什么是HTTPS,相信大家有有所了解,这里简单提一下:


由于HTTP协议(HyperText Transfer Protocol,超文本传输协议)中,传输的内容是明文的,请求一旦被劫持,内容就会完全暴露,劫持者可以对其进行窃取或篡改,因此这种数据的传输方式存在极大的安全隐患。


因此,在基于HTTP协议的基础上对传输内容进行加密的HTTPS协议(HyperText Transfer Protocol over Secure Socket Layer)便诞生了,这样即使传输的内容被劫持,由于数据是加密的,劫持者没有对应的密钥也很难对内容进行破解,从而提高的传输的安全性。



密钥协商



既然要对传输的内容进行加密,那就要约定好加密的方式与密钥管理。首先在加密方式的选择上,有对称加密非对称加密两种,这两种方式各有有缺。


对称加密:


加密和解密使用相同的密钥,这种效率比较高,但是存在密钥维护的问题。如果密钥通过请求动态下发,会有泄漏的风险。如果密钥存放到Client端,那么密钥变更时就要重新发版更新密钥,而且如果要请求多个服务器就要维护多个密钥,对于服务器端也是同理,这种密钥的维护成本极高。


非对称加密:


加密和解密使用不同的密钥,即公钥与私钥,私钥存放在Server端,不对外公开,公钥是对外公开的,这样无论是公钥打包进Client端还是由Server端动态下发,也无需担心泄漏的问题。但是这种加密方式效率较低。


HTTPS协议中,结合了对称加密和非对称加密两种方式,取其精华,弃其糟粕,发挥了两者各自的优势。


假设目前Server端有一对密钥,公钥A和私钥A,在Client端发起请求时,Server端下发公钥A给Client端,Client端生成一个会话密钥B,并使用公钥A对会话密钥B进行加密传到Server端,Server端使用私钥A进行解密得到会话密钥B,这时Client端和Server端完成了密钥协商工作,之后Client和和Server端交互的数据都使用会话密钥B进行对称加解密。在密钥协商过程中,就算被劫持,由于劫持者没有私钥A,也无法获取协商的会话密钥B,因此保证了数据传输的安全性。


密钥协商过程简图如下:


密钥协商简图.png



CA证书



上面的过程貌似解决了数据传输的安全问题,但依然有一个漏洞,就是如果劫持者篡改了Server端下发给Client端的公钥的情况。


中间人攻击(MITM攻击)简图如下:


中间人攻击简图.png


为了解决Client端对Server端下发公钥的信任问题,出现了一个被称作CA(Certificate Authority)的机构。


CA机构同样拥有采用非对称加密的公钥和私钥,公钥加上一些其他的信息(组织单位、颁发时间、过期时间等)信息被制作成一个cer/pem/crt等格式的文件,被称作证书,这些CA机构用来给其他组织单位签发证书的证书叫做根证书,根证书一般都会被预装在我们的设备中,被无条件信任


以Android设备为例,我们可以在设置 -> 安全 -> 更多安全设置 -> 加密与凭据 -> 信任的凭据中查看当前设备所有的预装的证书。


设备预装的证书.jpeg


如果Server端部署的证书是正规CA机构签发的证书(CA机构一般不会直接用根证书为企业签发域名证书,而是使用根证书生成的中间证书,一般情况下证书链是三级,根证书-中间证书-企业证书),那么我们在进行HTTPS请求的时候,不需要做其他额外操作,Client端获取到Server端下发的证书会自动与系统预装的证书进行校验,以确认证书是否被篡改。


如果Server端的证书是自签的,则需要在Client端自行处理证书校验规则,否则无法正常完成HTTPS请求。


这也是为什么,我们在Android开发网络请求时,无需做额外操作便能丝滑切换到HTTPS,但是这样真的就能保证网络请求的安全性了吗?


真的安全了吗?


经过上面的介绍我们可以了解到,如果Client端(手机、浏览器)中预装了大量正规CA机构的根证书,Server端如果是正规CA签发的证书,理论上是解决了HTTPS通信中双端的信任问题,但是还存在一个问题,就是这些Client端一般都会支持用户自行安装证书,这将会给Android端的网络安全带来哪些风险?接下来我们就继续来聊聊。


由于Android版本更新迭代较快,且不同版本之前差异较大,因此分析这个问题的时候一定要基于一个特定的系统版本,区别分析。Android 5.0(21)之前的版本太过古老,这里就不再进行分析,直接分析5.0之后的版本。


在一个只采用默认配置的的测试项目中进行HTTPS请求的抓包测试,发现在5.0(包括)到7.0(不包括)之间的版本,可以通过中间人或VPN的方式进行抓包,而7.0及以上版本则无法正常抓包,抓包情况如下



7.0以下手机代理抓包情况:



Android7.0以下.jpeg



7.0及以上手机代理抓包情况:



之所以7.0是个分水岭,是因为在Android7.0之前,系统除了对系统内置CA证书无条件信任外,对用户手动安装的CA证书也无条件信任了。


虽然说7.0及以上的设备不再信用用户自行添加的CA证书,安全性比之前的高很多,但是无门却无法阻止那些抓包的人使用7.0之下的手机,除非提高应用的最小支持版本,但这样就意味着要放弃一些用户,显然也不适用于所有情况。


那么如何在保证低版本兼容性的同时兼顾安全性呢,我们接下来继续探讨。


如何更安全


除了系统默认的安全校验之外,我们也可以通过如下手段来提高请求的安全性,让抓包变得更加困难。



禁用代理



该方式适用于所有Android版本。


在网络请求时,通过调用系统API获取当前网络是否设置了代理,如果设置了就终止请求,达到保护数据安全的目的。因为通过中间人的方式进行抓包,需要把网络请求转发到中间人的代理服务器,如果禁止了代理相当于从源头解决了问题。


优势:设置简单,系统API简单调用即可获取代理状态。


劣势:




  1. 会错杀一些因为其他场景而使用代理的用户,导致这样的用户无法正常使用




  2. 通过开启VPN在VPN上设置代理转发到中间人服务器的方式绕过




由于设置禁用代理的方式很容易被绕过且有可能影响正常开启VPN用户的使用,因此不推荐使用该方式。



数据加密



该方式适用于所有Android版本。


对请求传输的数据进行加密,然后再通过HTTPS协议传输。HTTPS本身在传输过程中会生成一个会话密钥,但是这个密钥可以被抓包获取,如果对传输的数据进行一次加密后再传输,即使被抓包也没法解析出真实的数据。


优势:安全性较高,只要密钥没有泄漏,数据被破获的风险较低


劣势:




  1. 修改同时修改Client端和Server端代码,增加加解密逻辑




  2. 加解密操作影响效率且有密钥维护的成本




在对数据安全性要求比较高的接口上,可以采用这种方式对传输内容进行增强保护。



证书单向认证



该方式适用于所有Android版本。


在默认情况下,HTTPS在握手时,Server端下证书等信息到Client端,Client端校验该证书是否为正规CA机构签发,如果是则通过校验。这里我们可以自定义校验规则,可以下载Server端的证书到打包到APK中,在请求时进行证书校验。


https单向认证.png


优势:安全性高。


劣势:证书容易过期,当前企业证书有效期只有1年,需要每年进行续签,Client需要维护证书更新的问题。



证书双向认证



该方式适用于所有Android版本。


在单向认证中,Client端会验证Server端是否安全,但是Server端并没有对Client进行校验,这里可以让Server端对Client也进行一次认证。这种认证需要在单向认证的基础上再额外创建一套证书B,存放在Client端,并在Client端完成对Server端的校验后,把Client端的公钥证书发送到Server端,由Server端进行校验,校验通过后开始密钥协商等后续步骤。


https双向认证.png


优势:安全性非常高!


劣势:




  1. Server端要存放Client端公钥证书,如果一个Server对应多个Client则需要维护多套




  2. 增加了校验成本,会降低相应速度





网络安全配置文件



该方案为google官方推荐的方案,也是一种证书的单向校验,不过在Android7.0及更高版本上,配置简单,只需要再清单文件的application节点下增加一个networkSecurityConfig项,并指向一个按固定的格式创建一个xml文件,即可完成网络安全的校验,体验相当丝滑,唯一美中不足的是该配置不支持7.0以下的版本。


在7.0及以上版本中,在xml文件夹下创建名为network_security_config_wanandroid的网络安全配置文件:


网络安全文件配置.jpeg


该文件只需要在清单文件application节点的networkSecurityConfig中引用该文件即可,如此就完成了对wanandroid.com域名及其所有次级域名的证书单向认证。


在7.0以下版本中:


由于networkSecurityConfig是7.0版本新增的,因此在所有7.0以下的设备上无法生效,所以针对7.0以下的设备只能通过代码进行认证。推荐使用OkHttp:


okHttp进行证书校验.png


需要注意的是,在通过代码配置指定域名的证书校验时,根域名和次级域名需要分别进行配置。


优势:安全性较高,代码改动少。


劣势:本质还是证书的单向认证。



选择要校验的证书



如果说采取了google推荐的方式进行安全校验,那校验证书链中的哪个证书比较合适呢?


理论上来说,当然是校验企业自己的证书最好,即证书链的第三层企业证书


image.png


但是该层证书的有效期比较短,一般每年都要进行重签,重签之后证书的Sha256就会发生变化,这时候就要及时更新Client端中信息,否则就无法正常完成校验。


为了规避证书频繁过期的问题,我们可以直接对根证书进行校验,一般来说,根证书的有效期是比较长的:


image.png


这样就不用担心证书频繁过期的问题了,但是如果再企业证书续签的时候更换了CA机构,那就必须要更新Client端中的根证书信息了,不过这就是另外的一个问题了。



只校验根证书会不会存在风险?



几乎不会,因为正规的CA机构在给一个企业颁发证书的时候,会有审核机制的,一般不会出现错误办法的状况,但在历史上确实出现过CA机构被骗,将证书颁发给了相应域名之外的人。下面截图来自Google官网:


列入黑名单.png


不过这是非常小概率的事件了,因此校验域名+根证书摘要算是即安全又避免了证书频繁过期的问题,再加上google官方的推荐,算的上是最佳解决方案了。


这篇文章就介绍到这里,感谢观看~~


上号.jpg


作者:王远道
来源:juejin.cn/post/7210688688921821221
收起阅读 »

扒一扒抖音是如何做线程优化的

背景 最近在对一些大厂App进行研究学习,在对某音App进行研究时,发现其在线程方面做了一些优化工作,并且其解决的问题也是之前我在做线上卡顿优化时遇到的,因此对其具体实现方案做了深入分析。本文是对其相关源码的研究加上个人理解的一个小结。 问题 创建线程卡顿 在...
继续阅读 »

背景


最近在对一些大厂App进行研究学习,在对某音App进行研究时,发现其在线程方面做了一些优化工作,并且其解决的问题也是之前我在做线上卡顿优化时遇到的,因此对其具体实现方案做了深入分析。本文是对其相关源码的研究加上个人理解的一个小结。


问题


创建线程卡顿


在Java中,真正的内核线程被创建是在执行 start函数的时候, nativeCreate的具体流程可以参考我之前的一篇分析文章 Android虚拟机线程启动过程解析 。这里假设你已经了解了,我们可以可以知道 start()函数底层涉及到一系列的操作,包括 栈内存空间分配、内核线程创建 等操作,这些操作在某些情况下可能出现长耗时现象,比如由于linux系统中,所有系统线程的创建在内核层是由一个专门的线程排队实现,那么是否可能由于队列较长同时内核调度出现问题而出现长耗时问题? 具体的原因因为没有在线下复现过此类问题,因此只能大胆猜测,不过在线上确实收集到一些case, 以下是线上收集到一个阻塞现场样本:



那么是不是不要直接在主线程创建其他线程,而是直接使用线程池调度任务就没有问题? 让我们看下 ThreadPoolExecutor.execute(Runnable command)的源码实现



从文档中可以知道,execute函数的执行在很多情况下会创建(JavaThread)线程,并且跟踪其内部实现后可以发现创建Java线程对象后,也会立即在当前线程执行start函数。



来看一下线上收集到的一个在主线程使用线程池调度任务依旧发生卡顿的现场。



线程数过多的问题


在ART虚拟机中,每创建一个线程都需要为其分配独立的Java栈空间,当Java层未显示设置栈空间大小时,native层会在FixStackSize函数会分配默认的栈空间大小.



从这个实现中,可以看出每个线程至少会占用1M的虚拟内存大小,而在32位系统上,由于每个进程可分配的用户用户空间虚拟内存大小只有3G,如果一个应用的线程数过多,而当进程虚拟内存空间不足时,创建线程的动作就可能导致OOM问题.



另一个问题是某些厂商的应用所能创建的线程数相比原生Android系统有更严格的限制,比如某些华为的机型限制了每个进程所能创建的线程数为500, 因此即使是64位机型,线程数不做控制也可能出现因为线程数过多导致的OOM问题。


优化思路


线程收敛


首先在一个Android App中存在以下几种情况会使用到线程



  • 通过 Thread类 直接创建使用线程

  • 通过 ThreadPoolExecutor 使用线程

  • 通过 ThreadTimer 使用线程

  • 通过 AsyncTask 使用线程

  • 通过 HandlerThread 使用线程


线程收敛的大致思路是, 我们会预先创建上述几个类的实现类,并在自己的实现类中做修改, 之后通过编译期的字节码修改,将App中上述使用线程的地方都替换为我们的实现类。


使用以上线程相关类一般有几种方式:



  1. 直接通过 new 原生类 创建相关实例

  2. 继承原生类,之后在代码中 使用 new 指令创建自己的继承类实例


因此这里的替换包括:



  • 修改类的继承关系,比如 将所有 继承 Thread类的地方,替换为 我们实现 的 PThread

  • 修改上述几种类直接创建实例的地方,比如将代码中存在 new ThreadPoolExecutor(..) 调用的地方替换为 我们实现的 PThreadPoolExecutor


通过字码码修改,将代码中所有使用线程的地方替换为我们的实现类后,就可以在我们的实现类做一些线程收敛的操作。


Thread类 线程收敛


在Java虚拟机中,每个Java Thread 都对应一个内核线程,并且线程的创建实际上是在调用 start()函数才开始创建的,那么我们其实可以修改start()函数的实现,将其任务调度到指定的一个线程池做执行, 示例代码如下


class ThreadProxy : Thread() {
override fun start() {
SuperThreadPoolExecutor.execute({
this@ThreadProxy.run()
}, priority = priority)
}
}

线程池 线程收敛


由于每个ThreadPoolExecutor实例内部都有独立的线程缓存池,不同ThreadPoolExecutor实例之间的缓存互不干扰,在一个大型App中可能存在非常多的线程池,所有的线程池加起来导致应用的最低线程数不容小视。


另外也因为线程池是独立的,线程的创建和回收也都是独立的,不能从整个App的任务角度来调度。举个例子: 比如A线程池因为空闲正在释放某个线程,同时B线程池确可能正因为可工作线程数不足正在创建线程,如果可以把所有的线程池合并成 一个统一的大线程池,就可以避免类似的场景。


核心的实现思路为:



  1. 首先将所有直接继承 ThreadPoolExecutor的类替换为 继承 ThreadPoolExecutorProxy,以及代码中所有new ThreadPoolExecutor(..)类 替换为 new ThreadPoolExecutorProxy(...)

  2. ThreadPoolExecutorProxy 持有一个 大线程池实例 BigThreadPool ,该线程池实例为应用中所有线程池共用,因此其核心线程数可以根据应用当前实际情况做调整,比如如果你的应用当前线程数平均是200,你可以将BigThreadPool 核心线程设置为150后,再观察其调度情况。

  3. 在 ThreadPoolExecutorProxy 的 addWorker 函数中,将任务调度到 BigThreadPool中执行



AsyncTask 线程收敛


对于AsyncTask也可以用同样的方式实现,在execute1函数中调度到一个统一的线程池执行



public abstract class AsyncTaskProxy<Params,Progress,Result> extends AsyncTask<Params,Progress,Result>{

private static final Executor THREAD_POOL_EXECUTOR = new PThreadPoolExecutor(0,20,
3, TimeUnit.MILLISECONDS,
new SynchronousQueue<>(),new DefaultThreadFactory("PThreadAsyncTask"));


public static void execute(Runnable runnable){
THREAD_POOL_EXECUTOR.execute(runnable);
}

/**
* TODO 使用插桩 将所有 execute 函数调用替换为 execute1
* @param params The parameters of the task.
* @return This instance of AsyncTask.
*/

public AsyncTask<Params, Progress, Result> execute1(Params... params) {
return executeOnExecutor(THREAD_POOL_EXECUTOR,params);
}


}

Timer类


Timer类一般项目中使用的地方并不多,并且由于Timer一般对任务间隔准确性有比较高的要求,如果收敛到线程池执行,如果某些Timer类执行的task比较耗时,可能会影响原业务,因此暂不做收敛。


卡顿优化


针对在主线程执行线程创建可能会出现的阻塞问题,可以判断下当前线程,如果是主线程则调度到一个专门负责创建线程的线程进行工作。


    private val asyncExecuteHandler  by lazy {
val worker = HandlerThread("asyncExecuteWorker")
worker.start()
return@lazy Handler(worker.looper)
}


fun execute(runnable: Runnable, priority: Int) {
if (Looper.getMainLooper().thread == Thread.currentThread() && asyncExecute
){
//异步执行
asyncExecuteHandler.post {
mExecutor.execute(runnable,priority)
}
}else{
mExecutor.execute(runnable, priority)
}

}

32位系统线程栈空间优化


在问题分析中的环节中,我们已经知道 每个线程至少需要占用 1M的虚拟内存,而32位应用的虚拟内存空间又有限,如果希望在线程这里挤出一点虚拟内存空间来,可以参考微信的一个方案, 其利用PLT hook需改了创建线程时的栈空间大小。


而在另一篇 juejin.cn/post/720930… 技术文章中,也介绍了另一个取巧的方案 :在Java层直接配置一个 负值,从而起到一样的效果



OOM了? 我还能再抢救下!


针对在创建线程时由于内存空间不足或线程数限制抛出的OOM问题,可以做一些兜底处理, 比如将任务调度到一个预先创建的线程池进行排队处理, 而这个线程池核心线程和最大线程是一致的 因此不会出现创建线程的动作,也就不会出现OOM异常了。



另外由于一个应用可能会存在非常多的线程池,每个线程池都会设置一些核心线程数,要知道默认情况下核心线程是不会被回收的,即使一直处于空闲状态,该特性是由线程池的 allowCoreThreadTimeOut控制。



该参数值可通过 allowCoreThreadTimeOut(value) 函数修改



从具体实现中可以看出,当value值和当前值不同 且 value 为true时 会触发 interruptIdleWorkers()函数, 在该函数中,会对空闲Worker 调用 interrupt来中断对应线程



因此当创建线程出现OOM时,可以尝试通过调用线程池的 allowCoreThreadTimeOut 来触发 interruptIdleWorkers 实现空闲线程的回收。 具体实现代码如下:



因此我们可以在每个线程池创建后,将这些线程池用弱引用队列保存起来,当线程start 或者某个线程池execute 出现OOM异常时,通过这种方式来实现线程回收。


线程定位


线程定位 主要是指在进行问题分析时,希望直接从线程名中定位到创建该线程的业务,关于此类优化的文章网上已经介绍的比较多了,基本实现是通过ASM 修改调用函数,将当前类的类名或类名+函数名作为兜底线程名设置。这里就不详细介绍了,感兴趣的可以看 booster 中的实现



字节码修改工具


前文讲了一些优化方式,其中涉及到一个必要的操作是进行字节码修改,这些需求可以概括为如下



  • 替换类的继承关系,比如将 所有继承于 java.lang.Thread的类,替换为我们自己实现的 ProxyThread

  • 替换 new 指令的实例类型,比如将代码中 所有 new Thread(..) 的调用替换为 new ProxyThread(...)


针对这些通用的修改,没必要每次遇到类似需求时都 进行插件的单独开发,因此我将这种修改能力集成到 LanceX插件中,我们可以通过以下 注解方便实现上述功能。


替换 new 指令


@Weaver
@Group("threadOptimize")
public class ThreadOptimize {

@ReplaceNewInvoke(beforeType = "java.lang.Thread",
afterType = "com.knightboost.lancetx.ProxyThread")
public static void replaceNewThread(){
}

}

这里的 beforeType表示原类型,afterType 表示替换后的类型,使用该插件在项目编译后,项目中的如下源码



会被自动替换为



替换类的继承关系


@Weaver
@Group("threadOptimize")
public class ThreadOptimize {

@ChangeClassExtends(
beforeExtends = "java.lang.Thread",
afterExtends = "com.knightboost.lancetx.ProxyThread"
)
public void changeExtendThread(){};



}

这里的beforeExtends表示 原继承父类,afterExtends表示修改后的继承父类,在项目编译后,如下源码



会被自动替换为



总结


本文主要介绍了有关线程的几个方面的优化



  • 主线程创建线程耗时优化

  • 线程数收敛优化

  • 线程默认虚拟空间优化

  • OOM优化


这些不同的优化手段需要根据项目的实际情况进行选择,比如主线程创建线程优化的实现方面比较简单、影响面也比较低,可以优先实施。 而线程数收敛需要涉及到字节码插桩、各种对象代理 复杂度会高一些,可以根据当前项目的实际线程数情况再考虑是否需要优化。


线程OOM问题主要出现在低端设备 或一些特定厂商的机型上,可能对于某些大厂的用户基数来说有一定的收益,如果你的App日活并没有那么大,这个优化的优先级也是较低的。


性能优化专栏历史文章:


文章地址
监控Android Looper Message调度的另一种姿势juejin.cn/post/713974…
Android 高版本采集系统CPU使用率的方式juejin.cn/post/713503…
Android 平台下的 Method Trace 实现及应用juejin.cn/post/710713…
Android 如何解决使用SharedPreferences 造成的卡顿、ANR问题juejin.cn/post/705476…
基于JVMTI 实现性能监控juejin.cn/post/694278…

参考资料


1.某音App


2.内核线程创建流程


3.juejin.cn/post/720930… 虚拟内存优化: 线程 + 多进程优化


4.github.com/didi/booste…


作者:卓修武K
来源:juejin.cn/post/7212446354920407096
收起阅读 »

Android无需权限调起系统相机拍照

在进行一些小型APP的开发,或者是对拍照界面没有自定义要求时,我们可以用调起系统相机的方式快速完成拍照需求 和不需读写权限进行读写操作的方案一样,都是通过Intent启动系统的activity让用户进行操作,系统再将用户操作的结果告诉我们,因为过程对APP是完...
继续阅读 »

在进行一些小型APP的开发,或者是对拍照界面没有自定义要求时,我们可以用调起系统相机的方式快速完成拍照需求


和不需读写权限进行读写操作的方案一样,都是通过Intent启动系统的activity让用户进行操作,系统再将用户操作的结果告诉我们,因为过程对APP是完全透明的,所以不会侵犯用户隐私。


有两种方法可以调起系统相机拍照获取图片,我们先讲比较简单的一种


1、直接获取用户拍照结果

val launcher = registerForActivityResult(ActivityResultContracts.TakePicturePreview()) {bitmap->
bitmap ?: return@registerForActivityResult
vm.process(bitmap)
}

launcher.launch("image/*")

这个在旧版本的API中就等于


startActivityForResult(Intent(MediaStore.ACTION_IMAGE_CAPTURE),CODE)

等到用户完成拍照,返回我们的activity时,我们就可以得到一张经过压缩的bitmap。这个方法很简单,它的缺点就是获得的bitmap像素太低了,如果对图片像素有要求的话需要使用第二种方法


2、用户拍照之后指定相机将未压缩的图片存放到我们指定的目录

var uri: Uri? = null

val launcher =
registerForActivityResult(ActivityResultContracts.TakePicture()) {
if(it){
uri?.let { it1 -> vm.process(it1) }
}
}

val picture = File(externalCacheDir?.path, "picture")
picture.mkdirs()
uri = FileProvider.getUriForFile(
this,
"${BuildConfig.APPLICATION_ID}.fileprovider",
File(picture, "cache")
)
launcher.launch(uri)

这里我逐行进行解释:



  1. 首先,我们需要指定拍摄的照片要存到哪,所以我们先指定图片的存放路径为externalCacheDir.path/picture/cache 注意这张图片在文件系统中的名字就叫做cache了(没有文件后缀)。

  2. 然后我们通过FileProvider构建一个有授权的Uri给系统相机,相机程序拿到我们的临时授权,才有权限将文件存放到APP的私有目录。

  3. 系统相机拍照完成之后就会走到回调,如果resultCode为RESULT_OK才说明用户成功拍照并保存图片了。这样我们就能得到一张系统相机拍出来的原图的Uri,这样我们就可以用这张图片去处理业务了。


注意:使用方法二需要用到FileProvider,所以我们还要在AndroidManifest里声明


<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>

@xml/provider_paths是我们授权访问的文件路径,这里我写的是


<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="external_files" path="."/>
</paths>

关于这个"path.xml",其实还有一些可以补充说明的,后面有空会补上,这里我简单说明一下:


因为我们创建临时文件的时候,文件指定的目录是externalCacheDir?.path,对应的path就是external-cache-path,表示我们要临时授权的目录是externalCacheDir,如果文件目录指定的是其他路径,那path节点也需要改成代表对应文件夹的节点,这样其他应用才能访问到

作者:用户5944254635000
来源:juejin.cn/post/7211400484104388663
我们APP的私有目录

收起阅读 »

放弃熬夜,做清晨的霸主🔥

☀️ 前言 不知道最近大家有没有在 b 站刷到硬核的HeyMatt老师一个视频,标题叫做放弃熬夜,做清晨的霸主(人生效率的巨变)。 抱着随便看看的心情点了进去后,我沉默了并思考了片刻,我决定按照他视频里的建议和方法尝试一下。 在尝试早起将近一个月的时间后,我...
继续阅读 »

☀️ 前言



  • 不知道最近大家有没有在 b 站刷到硬核的HeyMatt老师一个视频,标题叫做放弃熬夜,做清晨的霸主(人生效率的巨变)

  • 抱着随便看看的心情点了进去后,我沉默了并思考了片刻,我决定按照他视频里的建议和方法尝试一下。

  • 在尝试早起将近一个月的时间后,我发现,我的效率确实是有了质的提升,接下来我会根据HeyMatt老师提到的方法和我一些实践来进行说明,感兴趣的小伙伴欢迎收藏慢慢看。


🕐 极致利用晚上时间的错觉



  • 会不会有很多小伙伴会有这种情况,每天辛勤劳作后,到了11点半大脑就会提示你:累了一天了,要不要放松一下呢?视频里说到,这种大脑暗示的放松大体分为三种:

    • 开始刷视频,打个游戏,借助浅层的刺激感来放松

    • 点个宵夜,搞个小烧烤吃吃,借助食物换取特定心境

    • 想一些过往能够牵动情绪的往事,沉浸在起伏连绵的情绪中



  • 绝了,以上三种我都尝试过,全中,但是作为程序员我还会有其他的几种:

    • 学习知识📖

    • 优化代码💼

    • 加快需求进度,赶需求🏃



  • 我经常会有这种想法,如果晚上11点半到1点半我可以把这些事情做完或者做多一点,那么我的时间就会被延长🕐。

  • 错❌,看了这个视频后我真的悟了,我花掉了N个晚上的两个小时,但是换不回来人生相应的发展,甚至很多质量很差的决策、代码都是在这个时间段产出的。

  • 可能你确实在这晚上获得了很多愉悦感,但是这个愉悦感是没有办法持续的第二天又赖床又想逃避,你会去想我白白浪费了晚上两个小时刷剧,过了一个晚上这个愉悦感在你早上醒来的时候会忽然转化为你的焦虑感

  • 确实是这样的,特别是在周末熬夜的时候,你会潜意识的特别晚睡,第二天让睡眠拉满,直接到中午才起床,但其实这样不是浪费了更多的时间吗?


🤔 三个风险



  • HeyMatt老师提到在熬夜的这些时间,面临了至少三个风险。


时间的消耗不可控



  • 就拿我来举例,我前段时间老是想着公司需求怎么做,需求的方案是不是不完整,是不是有可以优化的点,要修复的Bug怎么定位,怎么解决。

  • 我不自觉的就会想,噢我晚上把它给搞定,那么第二天就可以放下心去陪家人出去走走。

  • 可是事实呢?运气好一点或许可以在2个小时解决1点准时睡觉,但是运气不好时,时间会损耗越来越多,2个半小时,3个小时,4个小时,随着时间的消逝,问题没有解决就会越发焦虑,不禁查看时间已经凌晨3-4点了。

  • 就更不用说以前大学的时候玩游戏,想着赢一局就睡觉,结果一晚上都没赢过...😓


精神方面的损耗



  • 当我们消耗了晚上睡眠时间来工作、来学习、来游戏,那么代价就是你第二天会翻倍的疲惫。

  • 你会不自觉的想要睡久一点,因为这样才能弥补你精神的损耗,久而久之你就会养成晚睡晚起的习惯,试问一下自己有多久没有在周末看过清晨的阳光了?

  • 再说回我,当我前一个晚上没有解决问题带着焦虑躺在床上时,我脑子会不自觉全是需求、Bug,这真的不夸张,我真的睡着了都会梦到我在敲代码。这其实就是一种极度焦虑而缺乏休息的大脑能干出来的事情。

  • 我第二天闹钟响了想起我还有事情没做完,就会强迫自己起床,让自己跟**“想休息的大脑”**打架,久而久之这危害可想而知。


健康维度的损耗



  • 随着熬夜次数的增多,年龄的增长,很多可见或不可见的身体预警就会越来越多,具体有什么危害,去问AI吧,它是懂熬夜的。



🔥 做清晨的霸主



  • 那么怎么解决这些问题呢,其实很简单,把晚上11.30后熬夜的时间同等转化到早上即可,比如11.30-1.30,那么就转化到6.30-8.30,这时候就会有同学问了:哎呀小卢,你说的这么简单,就是起不来呀!!

  • 别急,我们都是程序员,最喜欢讲原理了,HeyMatt老师也将原理告诉了我们。


赖床原理



  • 其实我们赖床起不来的很大一部分原因是自己想太多了。

  • 闹钟一响,你会情不自禁去思考,“我真的要现在起床吗?” “我真的需要这一份需要早起的工作吗?” “我起床之后我需要干什么?” “这么起来会不会很累,要不还是再睡一会,反正今天不用上班?”

  • 这时候咱们大脑就处于一种**“睡眠”“清醒”**的重叠状态,就跟叠buffer一样,大脑没有明确的收到指令是要起床还是继续睡。

  • 当我们想得越多,意识就变得越模糊,但是大脑不愿意去思考,大脑无法清晰地识别并执行指令,导致我们又重新躺下了。


练就早起



  • 在一次采访中,美国作家 Jocko Willink 老师提出了一种早起方法::闹钟一响,你的大脑什么都不要想,也不需要去想,更不用去思考,让大脑一片空白,你只需执行动作即可。

  • 而这个动作其实特别简单,就是坐起来--->站起来--->去洗漱,什么都不用想,只用去做就好。

  • 抱着试一试的心态,我尝试了一下这种方法,并在第二天调整了闹钟到 6:30。第二天闹钟一响,直接走进卫生间刷个牙洗个脸,瞬间清醒了,而且我深刻的感觉到我的专注力精神力有着极大的提升,大脑天然的认为现在是正常起床,你是需要去工作和学习👍。

  • 绝了,这个方法真的很牛*,这种方法非常有效,让我觉得起床变得更容易了,推荐大家都去试试,你会回来点赞的。


克服痛苦



  • 是的没错,上面这种办法是会给人带来痛苦的,在起床的那一瞬间你会感觉仿佛整个房间的温度都骤降了下来,然后,你使劲从被窝里钻出来,脚底下着地的瞬间,你感到冰凉刺骨,就像是被一桶冰水泼醒一样。你感到全身的毛孔都瞬间闭合,肌肉僵硬,瑟瑟发抖,好像一股冰冷的气流刺痛着你的皮肤。

  • 但是这种痛苦是锐减的,在三分钟之后你的痛苦指数会从100%锐减到2%

  • 带着这种征服痛苦的快感,会更容易进入清晨的这两小时的写作和工作中。


✌️ 我得到了什么



  • 那么早起后,我收获了什么呢❓❓


更高效的工作时间



  • 早起可以让我在开始工作前有更多的时间来做自己想做的事情,比如锻炼、读书、学习新技能或者提升自己的专业知识等,这些事情可以提高我的效率专注力,让我在工作时间更加高效。

  • 早起可以让我更容易集中精力,因为此时还没有太多事情干扰我的注意力。这意味着我可以更快地完成任务,更少地分心更少地出错


更清晰的思维



  • 早上大脑比较清醒,思维更加清晰,这有助于我更好地思考解决问题,我不用担心我在早上写的需求方案是否模糊,也能更好的做一些决策

  • 此外,早起还可以让我避免上班前匆忙赶路的情况,减少心理上的紧张压力


更多可支配的时间



  • 早起了意味着早上两个最清醒的时间随便我来支配,我可以用半小时运动,再用10分钟喝个咖啡,然后可以做我喜欢做的事情。

  • 可以用来写代码,可以用来写文章,也可以用来运营个人账号

  • 可以让我有更多的时间规划安排工作,制定更好的工作计划时间管理策略,从而提高工作效率减少压力


更好的身体健康



  • 空腹运动对我来说是必须要坚持的一件事情,早起可以让我有更多的时间来锻炼身体,这对程序员来说非常重要,因为长时间的坐着工作容易导致身体不健康

  • 用来爬楼,用来跑步,用来健身环等等等等,随便我支配,根本不用担心下班完了后缺乏运动量。


👋 写在最后



  • 我相信,我坚持了一年后,我绝对可以成为清晨的霸主,你当然也可以。

  • 而且通过早起不思考这个方法,很多在生活有关于拖延的问题都可以用同样的方式解决,学会克服拖延直接去做,在之后就会庆幸自己做出了正确的决定

  • 如果您觉得这篇文章有帮助到您的的话不妨🍉🍉关注+点赞+收藏+评论+转发🍉🍉支持一下哟~~😛您的支持就是我更新的最大动力。

  • 如果想跟我一起讨论和学习更多的前端知识可以加入我的前端交流学习群,大家一起畅谈天下~~~

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

对标chatgpt?百度今日正式发布文心一言

3月16日下午,百度正式发布文心一言,包括五大能力——“文学创作、商业文案创作、数理逻辑推算、中文理解、多模态生成”。 百度李彦宏介绍称,内测中“文心一言”并不完美,但市场有需求必须要推出来。 《文心雕龙》是一本中国古代文学批评著作,作者刘勰在书中提出了“文心...
继续阅读 »

3月16日下午,百度正式发布文心一言,包括五大能力——“文学创作、商业文案创作、数理逻辑推算、中文理解、多模态生成”。


百度李彦宏介绍称,内测中“文心一言”并不完美,但市场有需求必须要推出来。


《文心雕龙》是一本中国古代文学批评著作,作者刘勰在书中提出了“文心”这一概念,指的是文学创作的内心体验和情感表达。《一言》则是唐代文学家韩愈的一篇散文,其中提到“一言既出,驷马难追”,意思是说一旦说出口的话就很难收回。


因此,百度的名字“文心一言”寓意着搜索引擎能够帮助人们快速地找到自己想要的信息,同时也提醒人们在言行上要慎重,避免后悔。


image.png


文心一言五大能力


新闻发布会现场,李彦宏展示了文心一言在五个使用场景的表现,包括文学创作、商业文案创作、数理推算、中文理解和多模态生成。从直播展示来看,文心一言某种程度上具有了对人类意图的理解能力,回答的准确性、逻辑性、流畅性都逐渐接近人类水平。但李彦宏也多次提及,这类大语言模型还远未到发展完善的阶段,进步空间很大,未来这段时间它一定会飞速发展,日新月异。


中文理解


image.png


多模态内容




  • 根据描述生成海报
    image.png




  • 用四川话语音讲述
    image.png




  • 视频生成能力
    不会对所有用户开放,百家号之前已经在运用。




image.png


对话式 AI 涉及的技术方向


image.png


文心大模型框架图


image.png


文心一言已有650家生态合作伙伴


文心一言已有650家生态合作伙伴


生成式大模型的三大产业机会


image.png


文心一言体验方式


百度同时公布了文心一言的邀请测试方案。3月16日起,首批用户即可通过邀请测试码,在文心一言官网体验产品,后续将陆续开放给更多用户。此外,百度智能云即将面向企业客户开放文心一言API接口调用服务。3月16日起正式开放预约,搜索“百度智能云”进入官网,可申请加入文心一言云服务测试。


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

关于ChatGPT-4,你需要知道什么?

关于ChatGPT-4,你需要知道什么? 开启对话式AI的未来:特性、应用和伦理考虑 ChatGPT-4正以其先进的自然语言理解能力, 改进的上下文保留和更像人类的反应, 对话式AI的世界进行革命. 作为OpenAI开创性的ChatGPT-3的继任者, Cha...
继续阅读 »

关于ChatGPT-4,你需要知道什么?


开启对话式AI的未来:特性、应用和伦理考虑


ChatGPT-4正以其先进的自然语言理解能力, 改进的上下文保留和更像人类的反应, 对话式AI的世界进行革命. 作为OpenAI开创性的ChatGPT-3的继任者, ChatGPT-4在AI领域树立了新的基准, 在各种应用中为用户提供无缝和高度互动的体验.


自GPT-2推出以来, AI语言模型在理解和生成类似人类的文本的能力方面已经取得了长足的进步. 通过ChatGPT-3, OpenAI推动了AI能够实现的边界, 现在, ChatGPT-4更进一步, 有望实现前所未有的性能改进, 并在AI领域开辟新的可能性.


本文全面介绍了ChatGPT-4的主要特性和改进, 其广泛的应用和用例, 以及围绕该技术的伦理考虑. 无论你是AI爱好者, 开发者还是企业主, 本文将帮助你了解ChatGPT-4是如何改变我们与技术互动的方式, 以及它对各行业的潜在影响.


0_ZXO7Gmke5dDkbCGX.jpg


一 特性和改进


ChatGPT-4最显著的进步之一是它能够产生更自然和类似人类的反应. 通过利用最先进的机器学习算法和大量的训练数据, AI模型现在可以有效地进行复杂的对话, 表现出同情心, 甚至在适当的时候表现出幽默感.


ChatGPT-4对成语表达, 俚语和俗语有更好的理解, 使其在处理不同的沟通方式时更具适应性和通用性. 这种改进的语言理解确保用户可以更自然地与AI模型交谈, 而不必为适应技术而调整他们的语言.


为了提供真正的个性化体验, ChatGPT-4可以根据个人用户的喜好和沟通方式进行定制. 通过先进的机器学习技术, AI模型可以从用户的输入中学习, 并相应地调整其反应, 确保每次互动都感觉真实和定制.


ChatGPT-4的情境感知个性化使它能够理解不同用户情境的细微差别, 如专业, 休闲或教育环境. 这使AI模型能够根据情况调整其语气和风格, 为用户提供更有亲和力和符合背景的对话体验.


0_jP5vmSZApn4Eejd6.jpg


二 应用和用例


客户支持和服务 ChatGPT-4可以集成到客户支持系统中, 提供即时, 高效和准确的援助, 减少等待时间, 提高客户满意度. 其先进的语言理解和个性化功能使它能够处理各种查询, 并为用户提供量身定做的解决方案.


内容创作和编辑 ChatGPT-4可以帮助作家, 营销人员和内容创作者生成高质量, 有创意和有吸引力的内容, 从博客文章到社交媒体帖子. 其先进的语言能力可以帮助用户克服写作障碍, 完善他们的想法, 并产生引人注目的内容, 与他们的观众产生共鸣.


虚拟助理 ChatGPT-4增强的对话能力使其成为支持虚拟助手的理想候选者, 为用户提供了与数字伙伴之间更直观, 更像人类的互动. 从安排约会到回答问题和提供建议, ChatGPT-4使虚拟助理变得更有帮助和更全面.


电子学习和辅导 ChatGPT-4的高级语言理解和上下文感知的个性化, 可以用来创建互动和自适应的电子学习平台. 人工智能模型可以帮助学生按照自己的节奏学习, 回答问题, 提供反馈, 并根据他们的独特需求和能力提供个性化的学习路径.


0_WrIF8U62_yRU3joj.jpg


三 伦理顾虑和AI安全


解决人工智能系统中的偏见 OpenAI致力于确保ChatGPT-4的开发是负责任的, 符合道德的. AI系统的主要问题之一是训练数据中存在的偏见, 这可能会导致有偏见的输出. OpenAI积极致力于减少这些偏见, 完善训练过程, 利用多样化和有代表性的数据源, 并不断迭代模型以解决已发现的问题.


隐私和数据保护 随着ChatGPT-4这样的AI模型越来越普遍, 对用户隐私和数据保护的关注也越来越重要. OpenAI遵守严格的数据保护准则, 并确保用户数据得到负责任和安全的处理, 符合相关法规和最佳做法.


防止误用和内容审核 OpenAI认识到滥用AI技术的潜在风险, 如ChatGPT-4. 为了减少这些风险, OpenAI实施了强大的内容审核和滥用预防措施, 确保AI模型的使用是负责任的, 并遵循道德准则.


正在进行的研究和合作 OpenAI致力于推进人工智能安全研究, 解决与部署ChatGPT-4等AI系统相关的挑战, 并促进与其他研究机构, 行业利益相关者和公众的合作. 通过分享知识和合作, AI社区可以为AI安全和伦理制定最佳实践.


0_sHoYhenR7WRd4NIc.jpg


四 挑战和未来方向


ChatGPT-4的其余局限性 尽管有了显著的进步, ChatGPT-4仍然有局限性, 比如它的反应偶尔会有不准确或不一致的地方. OpenAI继续致力于完善该模型以解决这些问题并提高其整体性能.


进一步发展的机会 随着AI技术的不断发展, 将不断有机会增强ChatGPT-4的能力, 扩大其应用范围, 并解决其限制. 该模型的未来迭代可能具有更强的语言理解, 语境保持和个性化能力.


0_IKgWTh_KNk_dW34n.jpg


五 总结


ChatGPT-4是对话式AI领域的一个重要里程碑, 为用户提供了增强的类人互动和广泛的潜在应用. 其先进的功能和改进有希望改变行业, 彻底改变我们的沟通方式, 并塑造AI驱动的未来体验.


OpenAI邀请用户探索和参与ChatGPT-4, 提供宝贵的反馈和见解, 这将有助于塑造这一突破性技术的持续发展.


随着我们继续见证像ChatGPT-4这样的AI技术的快速发展, 我们必须对这些进展的道德和社会影响保持警惕. 通过促进合作, 透明度和对负责任的发展的共同承诺, 我们可以确保AI技术服务于更大的利益, 并释放其全部潜力以造福人类.


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

我尝试用ChatGPT理解了下GPT4的现场直播,结果。。。

今天一早醒来,就被GPT4的发布给霸屏了。OpenAI官网上的介绍确实过于震撼,各个公众号的文章中也有提及,从专业考试到人类梗图,GPT4不只是回答的像模像样,更超越了我们这些普通人。 除了这些具体任务上的表现,GPT4的能力也有了突破——不仅可以处理更长的内...
继续阅读 »

今天一早醒来,就被GPT4的发布给霸屏了。OpenAI官网上的介绍确实过于震撼,各个公众号的文章中也有提及,从专业考试到人类梗图,GPT4不只是回答的像模像样,更超越了我们这些普通人。


除了这些具体任务上的表现,GPT4的能力也有了突破——不仅可以处理更长的内容(最长3.2万个token),也可以更好的理解图像内容。除了能力的增强,GPT4也更安全、更人性化了。


除了官网的Blog,OpenAI在北京时间2023年3月15日凌晨1点举办了一场25分钟左右的直播,具体展示GPT4的新能力。视频已经发布在国外某视频网站上,我们这就尝试用ChatGPT来总结一下这个现场直播的主要内容。


工具与原理


这里主要使用了VidSummize扩展,该扩展可以解析视频网站的字幕,并且利用OpenAI的API对字母内容进行分析。结合特定的prompt,便可以生成视频的概述。


prompt设置


这里采用默认的prompt,具体如下:



summarize this text using bullet points, give it a big, bold heading as well. make it: easy to read and exhaustive but also provide context on topics or concepts the reader might not know of if necessary. remember that you heading should be separated by two new lines from the bullet points.



翻译过来就是:



请使用项目符号列出要点,提供一个大而醒目的标题,并确保易读且详尽。如有必要,请为读者提供有关主题或概念的背景信息。请注意,您的标题应与项目符号间隔两个新行。



模型设置


这里选择使用ChatGPT类的Turbo模型——GPT-3.5-Turbo


结果展示


等待片刻,ChatGPT分析后的输出结果为:



Chapter 1 - OpenAI Demonstrates GPT-4 Capabilities in Live Stream



  • OpenAI has been building GPT-4 technology for the past two years

  • The company has focused on delivering GPT4 by rebuilding their entire training stack and testing it in real-world scenarios

  • OpenAI demonstrated how to make GPT-4 shine and get the most out of it

  • The company showed how to use GPT-4 as a tool and partner

  • OpenAI showcased GPT-4s ability to summarize an article with every word beginning with a specific letter

  • The company also demonstrated how a Discord using GPT-4 as an AI programming assistant

  • GPT-4 can leverage documentation it has not memorized and can synthesize new content in new ways

  • OpenAI showed how to debug code with GPT-4 and how it can accept inputs that intersperse images and text.


Chapter 2 - Using GPT-4 to Accomplish Tasks and Solve Problems



  • GPT-4 is a neural network that can be used to accomplish tasks and solve problems.

  • It can be used to describe images in detail, even if they are not explicitly tagged.

  • GPT-4 can also be used to understand dense content, such as tax code, and provide explanations and calculations.

  • The model is still being developed and optimized for prime time use.

  • GPT-4 has a length limit of32,000 tokens and be used for long documents.

  • The system message can be to get the in the mood of the task at hand.

  • GPT can output and create working websites from hand-drawn mock-ups.

  • The model can be used as an augmenting tool to increase productivity, but it is important to be in the drivers seat and understand what is going.

  • PT is flexible system that applied various and applications.


Chapter 3 - Exciting Opportunities with OpenAI Evals



  • OpenAI Evals is a model that can be improved by contributions from everyone

  • Improving the model can benefit a lot of people

  • OpenAI Evals offers exciting opportunities for building new things

  • The team is excited to see what people will build with the model

  • Contributions to OpenAI Evals are welcome and encouraged



对应的中文如下:



第一章 - OpenAI在直播中展示了GPT-4的能力



  • OpenAI过去两年一直在开发GPT-4技术。

  • 公司专注于通过重新构建整个训练系统并在真实情境中测试它来交付GPT4。

  • OpenAI展示了如何让GPT-4发挥作用并充分利用它。

  • 公司展示了如何将GPT-4用作工具和合作伙伴。

  • OpenAI展示了GPT-4摘要文章的能力,每个单词都以特定字母开头。

  • 公司还演示了使用GPT-4作为AI编程助手的Discord。

  • GPT-4可以利用它没有记忆的文档,并以新的方式综合新内容。

  • OpenAI展示了如何使用GPT-4调试代码以及如何接受交替图片和文本的输入。


第二章 - 使用GPT-4完成任务和解决问题



  • GPT-4是一个神经网络,可用于完成任务和解决问题。

  • 即使没有明确标记,它也可以详细描述图像。

  • GPT-4还可用于理解密集内容,例如税法,并提供解释和计算。

  • 该模型仍在开发和优化中,以供主流使用。

  • GPT-4的长度限制为32,000个标记,可用于长文档。

  • 系统消息可用于让用户进入工作状态。

  • GPT可以从手绘模型中输出和创建工作网站。

  • 该模型可用作增强工具,以提高生产力,但重要的是要掌握局面并理解正在进行的工作。

  • PT是灵活的系统,适用于各种应用程序。


第三章 - OpenAI Evals带来令人兴奋的机遇



  • OpenAI Evals是一个可以通过所有人的贡献改进的模型。

  • 改进模型可以惠及许多人。

  • OpenAI Evals为构建新事物提供了令人兴奋的机遇。

  • 团队很高兴看到人们将用该模型构建什么。

  • 欢迎和鼓励为OpenAI Evals做出贡献。



可以看出,这个结果很详尽,但是其中也会包含一些词法和语法错误,但这些并不影响我们了解视频的大体内容。


最后我再人工总结下直播中关键demo:



  • 结对编程时,代码运行出现错误的情况下通过输入最新文档进行bug修复

  • 对图片以及带梗的图片进行理解

  • 根据手绘图片,生成页面代码

  • 输入超长的税法条文,分析计算应纳税额

  • 总结编写打油诗


上述内容的截图如下(可参考截图中的视频时间查看原始视频):


结对编程解决BUG


图片内容理解


手绘图片转网页,这里出现了一个非技术原因导致的“翻车”


纳税额计算


打油诗内容总结


总结


相比ChatGTP,GPT4升级的地方主要有以下几个方面:



  • token的数量上有了很大的提升,从ChatGPT的4096直接提升到了32000,高了一个数量级,这使得普通人依赖机器解决专业问题(计算税、阅读法律条文)成为了可能。

  • 具备了多模态能力——除了自然语言,目前还可以处理图片。

  • 在逻辑推理能力上也有显著提升。


目前官网暂时没有给出GPT4的API,所以即刻体验的方式只有一种:加入ChatGPT Pro计划。需要等待体验的方式就是加入WaitingList排队,可以通过给OpenAI Evals贡献模型评价加快排队进度。


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

一文解码百度地图红绿灯倒计时

百度地图红绿灯倒数计时是什么? 当驾驶员打开百度地图导航,并经过红绿灯路口时,可以看到红绿灯变灯剩余秒数,也会在倒计时结束前收到语音播报,做好启停准备,从而避免急加速、急减速,还可缓解红绿灯被前车遮挡时的焦虑。 据反馈,红绿灯倒计时有助于提升“超视距”的驾驶体...
继续阅读 »

百度地图红绿灯倒数计时是什么?


当驾驶员打开百度地图导航,并经过红绿灯路口时,可以看到红绿灯变灯剩余秒数,也会在倒计时结束前收到语音播报,做好启停准备,从而避免急加速、急减速,还可缓解红绿灯被前车遮挡时的焦虑。


据反馈,红绿灯倒计时有助于提升“超视距”的驾驶体验,也能够助力交管部门提高信控路口整体通行效率。


图片


图 百度地图路口红绿灯倒计时


百度地图红绿灯倒计时数据从何而来? 百度地图红绿灯倒计时数据有两种来源,一种是大数据计算所得,另一种是官方权威数据接入。下面详解两种数据来源的基础原理和优缺点。


来源一:大数据计算红绿灯倒计时


数据来源: 脱敏的位置服务数据;道路拓扑结构数据;红绿灯准静态数据(即,与红绿灯相关但一般不会发生变动的信息,包括:红绿灯的空间位置数据、红绿灯控制的进口道车道及流向信息等)等。


技术说明: 百度地图基于海量的脱敏位置服务数据,计算得出每个路口红绿灯的周期时长和绿信比;同时,基于路口不同方向进口道的历史车流数据,挖掘红绿灯灯色的切换时间点;进而综合以上两类数据得到实时的倒计时读秒。


优点: 通过大数据计算的红绿灯倒计时,可支持在全国范围大部分的城市路口提供红绿灯倒计时服务,覆盖面广。同时,计算红绿灯倒计时的方法效率高、成本低,对于短期内暂不具备官方合作直接接入红绿灯数据条件的城市,也可以为市民提供红绿灯倒计时服务,方便市民体验。因此,大数据计算的红绿灯倒计时,可极大地提升日常出行体验和交通运行效率。


缺点:


1)大数据计算的红绿灯倒计时覆盖时段和范围可能存在缺失的情况:部分路口部分时段由于红绿灯非定周期控制、配时调整等因素,无法通过大数据计算得到精准且稳定的倒计时数据。


例如:由于自适应信号控制是根据路网实时的车流状态去配置最优信号配时方案,变化性强,因此自适应信号控制路口暂时无法通过大数据精准计算得到这类红绿灯的倒计时信息。


2)依托于大数据的计算方法,也不可避免大数据算法的天然缺陷,例如:算法可能无法完美模拟和还原真实世界,从而导致存在部分难以完全消除的误差。


来源二:权威接入红绿灯倒计时


数据来源: 路口静态数据;公安交通管理部门的交通信号控制平台的信号灯态数据。


技术说明: 2020年10月,公安部交通管理局签发《关于进一步加强城市道路交通信号控制应用工作的指导意见》(公交管〔2020〕302号)提出:积极探索应用车路协同技术。有条件的地方要结合车联网、车路协同技术应用和发展的需要,推动交通信号灯状态信息、交通事件及管控信息的开放共享,主动向通行车辆推送建议速度、路况动态等实时信息,帮助出行者及时掌握交通状况,合理选择出行路线。


权威接入正是基于此指导意见与各地公安交通管理部门一起积极探索,将交通信号控制平台的红绿灯灯态数据,共享给百度地图,再精准实时推送给百度地图的用户。


同时,数据传输中严格遵守中华人民共和国公共安全行业标准《公安交通集成指挥平台通信协议》(GA/T 1049-2013)、《公安视频图像信息系统安全技术要求第3部分:安全交互》(GA/T 1788.3-2021),符合公安部网络交互要求,充分保障数据安全。


图片


图《公安交通集成指挥平台通信协议第1部分:总则》(GA/T 1049.1-2013)


优点: 通过权威接入,百度地图可为出行用户提供实时准确、持续稳定、权威官方的红绿灯信息。即使在根据流量变化实时调整红绿灯配时的自适应信控路口,百度地图也可以基于接入的自适应红绿灯数据,为导航用户精准推送红绿灯信息。


缺点: 相较大数据计算红绿灯倒计时,由于不同城市交通信号控制相关工作的管理机制、软硬件配置等存在诸多差异,以及数据接入后的服务稳定需要持续保障,百度地图需要逐一和各地具体对接,一城一策,规模化覆盖的速度会慢一些。


百度地图红绿灯倒计时已经上线了吗? 基于大数据算法,百度地图充分发挥AI技术能力对城市路口红绿灯倒计时应算尽算,同时,百度地图与各地公安交通管理部门联合,已在西安、兰州等30多个城市实现全城数以千计的路口红绿灯灯态独家上线百度地图APP。


在兰州,使用百度地图导航时,驾驶员不仅可获得实时的红绿灯信息,还可以获得更高阶的智能出行服务:绿波出行引导,即:基于交警信号管控、区域实时交通状态、用户驾驶行为、导航规划路径等数据,可综合计算出不停车通过下一信号控制路口的“建议车速”,百度地图导航语音提醒驾驶人调整车速,助力真正实现“一路绿灯”,让驾驶人获得“畅通无阻”的舒适驾驶体验,降低交通系统中急加、急减、频繁启停的驾驶能耗。


图片


图 百度地图导航中的绿波车速引导


兰州市公安局交警支队科设大队副大队长杨懿认为,关于百度地图红绿灯上图,一是在城市出行方面为驾驶员个性化出行服务做出了探索;二是为车路协同等前沿领域奠定了基础;三是为下一步红绿灯控制优化提供了渠道;四是在出行诱导方面发挥了重要作用。


地图导航中红绿灯倒计时服务的目的不只是在路口为用户提供视觉盲区的倒计时提醒,也是为了提醒用户遵守行车规则、避免急停急启、关注驾驶行为、让每一次出发都更温和,更是为了助力城市交通运行协调和优化升级。


湖南省交通运输厅科技信息中心部长乔川龙认为,西安、兰州等城市的交管部门能够把交通红绿灯数据接入导航软件是智能交通发展中的一个里程碑事件,体现了中国智慧,为解决世界城市交通问题提供了中国方案。


百度地图始终致力于为用户提供更安全、更贴心、更智能的出行服务。欢迎更多地方交警与百度地图联合,为公众提供更优质高效的出行服务,打造城市智能交通和便民服务新名片。


红绿灯相关概念:


交通信号: 在道路空间上无法实现分离的地方,主要是在平面交叉口中,给不同方向的交通流在时间上分配通行权的一种交通管理措施。交通红绿灯用不同颜色的灯指标交通的通行和停止,灯色一般为红、黄、绿三色,所以交通红绿灯通常称为红绿灯。


周期时长: 某一进口道红绿灯的各种灯色轮流显示一次所需的时间,即各种灯色显示时间之和。


绿信比: 一个信号相位的有效绿灯时长与周期时长之比。有效绿灯时间是显示绿灯时间减去损失时间,其中损失时间是由绿灯启亮时的反应延迟、绿灯快结束时的驾驶放缓车速等候带来必然损失。


干线控制(绿波系统): 把一条干道上一批相邻的交通信号连接起来,加以协调控制,让相邻信号间的绿灯时差与车辆在其间的行程时间相适应,使车辆能连续通过尽可能多的绿灯。


绿波车速引导: 用户在建议车速下行驶,可实现一次通过多个路口不等灯,提高交通系统整体通行效率。


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