注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Java实现KgCaptcha短信验证码

背景Java是一种流行的编程语言,验证码是一种常用的网络安全技术。Java发展至今,网上也出现了各种各样的验证码,本人初学Java,下面是我用Java实现短信验证码的总结。截图展示实现代码后台接收前台的kgCaptchaToken进行验证,验证成功执行成功处理...
继续阅读 »

背景

Java是一种流行的编程语言,验证码是一种常用的网络安全技术。Java发展至今,网上也出现了各种各样的验证码,本人初学Java,下面是我用Java实现短信验证码的总结。

截图展示



实现代码

后台接收前台的kgCaptchaToken进行验证,验证成功执行成功处理,验证失败返回错误代码及信息。

package com.kyger;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;

public class demo extends HttpServlet {
private static final long serialVersionUID = 1L;

public demo() {
super();
}

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

// 编码
request.setCharacterEncoding("utf-8");
response.setCharacterEncoding("utf-8");;
response.setContentType("text/html; charset=utf-8");

// 后台处理
if (request.getMethod().equals("POST")){
String html, appId, appSecret, Token;

// 设置 AppId 及 AppSecret,在应用管理中获取
appId = "appId";
appSecret = "appSecret";

// 填写你的 AppId 和 AppSecret,在应用管理中获取
KgCaptchaSDK KgRequest = new KgCaptchaSDK(appId, appSecret);


// 前端验证成功后颁发的 token,有效期为两分钟
KgRequest.token = request.getParameter("kgCaptchaToken");
// System.out.print(KgRequest.token);

// 填写应用服务域名,在应用管理中获取
KgRequest.appCdn = "https://cdn.kgcaptcha.com";

// 请求超时时间,秒
KgRequest.connectTimeout = 5;

// 用户登录或尝试帐号,当安全策略中的防控等级为3时必须填写,一般情况下可以忽略
// 可以填写用户输入的登录帐号(如:request.getParameter("username"),可拦截同一帐号多次尝试等行为
KgRequest.userId = "kgCaptchaDemo";

// request 对象,当安全策略中的防控等级为3时必须填写,一般情况下可以忽略
KgRequest.request = request;
// java 环境中无法提供 request 对象,请分别定义:clientIp|clientBrowser|domain 参数,即:
// KgRequest.clientIp = "127.0.0.1"; // 填写客户端IP
// KgRequest.clientBrowser = ""; // 客户端浏览器信息
// KgRequest.domain = "http://localhost"; // 你的授权域名或服务IP

// 发送验证请求
Map requestResult = KgRequest.sendRequest();
if("0".toString().equals(requestResult.get("code"))) {
// 验签成功逻辑处理 ***

// 这里做验证通过后的数据处理
// 如登录/注册场景,这里通常查询数据库、校验密码、进行登录或注册等动作处理
// 如短信场景,这里可以开始向用户发送短信等动作处理
// ...

html = "";
} else {
// 验签失败逻辑处理
html = "";
}

response.getWriter().append(html);
} else {
response.sendRedirect("index.html");
}
}

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}

}


后端检测

后台接收数据,同时对来源及应用进行检测。

# 服务器黑名单检测
if self.auth.client_blacklist():
return self.r_code(20017) # 服务器黑名单

# 验签次数限制检测
excess = self.auth.excess(2)
if excess:
return self.r_code(code=[20020, 20021, 20022][excess - 1])

# 来路域名检测
if not self.kg["HTTP_REFERER"]: return self.r_code(20004) # 域名不合法,无法获取来路域名
if not self.auth.domain_auth(): return self.r_code(20005) # 来源域名未授权

# 应用有效时间检测
validity = self.auth.app_validity()
if validity[0] == 1: return self.r_code(20006) # 授权未开始
if validity[0] == 2: return self.r_code(20007) # 授权已结束

if self.auth.app_state(): return self.r_code(20008) # 当前应用/域名被禁用


结尾

SDK开源地址:KgCaptcha (KgCaptcha) · GitHub,顺便做了一个演示:凯格行为验证码在线体验

收起阅读 »

两个故事:送给面试失败的你

全文一共3084字,读完预计需要5分钟 故事1:从世界五百强高管手中拿下项目 那年我大二。 在北五环外的宏福校区上满一年课后,我们正式搬回了位于市中心的海淀区本部,终于算是进城了。 进城的第一个好处,就是做项目的机会变多了! 当年为了尽早的实现经济独立,课...
继续阅读 »



全文一共3084字,读完预计需要5分钟



故事1:从世界五百强高管手中拿下项目


图片


那年我大二。


在北五环外的宏福校区上满一年课后,我们正式搬回了位于市中心的海淀区本部,终于算是进城了。


进城的第一个好处,就是做项目的机会变多了!


当年为了尽早的实现经济独立,课没怎么好好上,兼职项目和实习倒是干了不少。


北邮有一个著名的BBS,上面啥都有,极其热闹。其中最热门的板块有俩,一个是“缘来如此”,听名字就知道是用来谈情说爱的。另一个则是就业机会交流板块,各种招聘内推资讯满天飞。


我当时天天刷帖子找实习和兼职机会,自然也收到不少面试邀约,而我要聊的这一次,格外的与众不同。


面试约在了晚上8点,北师大东门外的一个安静的咖啡店里,二楼。


我面试从来不迟到,一般都会掐着点到场,可那次我到的时候,面试官已经落座了。是一个四十出头的中年男人,高高瘦瘦,戴银边方框眼镜。桌上摆了一壶明亮的金丝皇菊花茶,两个小巧精致的玻璃茶杯。


我入座后,他客气的给我倒了一杯茶,面试便开始了。说是面试,其实更像是一场闲聊,整个谈话氛围非常轻松自在,咖啡店是文艺范儿的,灯光幽暗,空气里飘荡着舒缓的爵士乐和淡淡的松香,木质的楼板被过往的客人踩出咯咯吱吱的响声。


我们大约是聊了半个多小时的样子,从他的表情可以看得出他挺满意。结束谈话的时候他说,我稳重的样子不太像90后。我羞涩的笑了笑,带着他给我布置的第一个项目走出咖啡店。


没错,这个项目就这么轻而易举的被我拿到手了,也许是出于对我母校的信任,又或许是当晚的聊天氛围实在舒服,总之我获得了他的信任。


可好戏才刚刚开始。


我收到的第一个任务,要求我在一周内根据需求文档写出一份静态网站Demo出来。


这个任务现在看来很easy,但是当时的我,连最基本的网页怎么写都不会,我只用过Dreamweaver拖拖拽拽搞过一个蹩脚且丑陋的页面。而这一次,我面对的是一个商业任务,并且限定了交付日期,一周!这可把我愁坏了。甚至都不是时间紧不紧的问题,是我根本不知道从哪里下手啊。


事情的突破口出现在我大学舍友身上。


这小子是个富二代,家境优渥,从小就接触计算机,自然是见多识广,啥都会一点。我撺掇着让他给我演示“如何使用现成框架搭建网页”,当时用的是Bootstrap 1.0。我俩在通信原理的课堂上,坐在教室的后排,偷偷摸摸的教学着。我目不转睛的盯着他一点点用现成的类名,逐渐拼出了一个漂亮的页面,那种感觉真是兴奋极了!


其实现在回头看,我们当时的编码方法非常拙劣,完全上不了台面的那种,还真是一个敢教,一个敢学哈哈哈。


一节课偷偷摸摸下来,我基本上算是知道了一个静态页面如何从0到1的写出来了,剩下的事情就只是时间问题了。一下课我就飞快地跑回宿舍,埋头学习Bootstrap的API文档,一点点一点点,小心翼翼的拼凑着页面。


一周后,我如期把Demo交给了老板,保住了这次兼职机会。这份工作给我带来了每个月2000块的收入,整整持续了一年(当时我一个月生活费也就不到800)。


我是在后来的一年的交往中才知道,这个老板是某世界五百强的集团高管,空闲时间和朋友一起做了个创业公司,出于竞业关系,不方便公开招募团队,于是私下找学生兼职做点项目。


而我,就成了那个幸运的学生。


故事2:屡败屡战拿下百度Offer


图片


转眼到了大三。


杂七杂八的项目做了不少,钱也倒是有赚一些,但基本上都是小打小闹,尤其是技术层面,苦于一直没有接触过企业级项目,我并不知道一个成熟的商业项目的代码长什么样。


抱着这样一个期望,我开始争取大公司的实习机会。


还是从北邮人论坛,我拿到了一个百度的面试机会,具体哪个部门我已经记不太清了。


面试官安排我在茶水间坐下,丢给我一份试题和一支笔。


那份题目我做得稀烂,面试官看了看卷子,当场跟我说,你可能不太行。不过没等他赶我走,我迅速拿出电脑,打开作品集,指着我画的一些设计图对他说:那么……你们招UI设计师么?这是我的作品。


面试官先是一愣(心想这小子不是码农么,怎么还会设计),然后让我等一下,便拿着我的作品,跑去找他们团队的UI设计师帮忙过目。


是不是觉得我的行为很奇怪?没错,我自己也没想明白其实。当时我还没想好毕业后从事什么工作,由于从小就喜欢画点东西,平时也会看一些用户体验设计相关的书,大学期间在学生社团里做了不少有意思的图,所以我天真的以为自己毕业后也是可以从事UI设计工作的。


面试官很快就回来了,跟我摇摇头,结束了这场面试。


这次经历给我打击是比较大的,我原本自豪的以为我既能写代码,又能做设计,一定是个抢手货,但事实上,我的两种技能在学校里勉强能秀一下,到了职场专业人士眼里,就是些玩具级别的东西,根本不够看的。


回去后我继续寻找实习机会,中间拿到了亚信联创的Offer,在亚联实习了三个月,学了些CSS基础知识。但我一直不甘心,还是一心想去大公司。


不久后,我等来了百度的另一个面试机会。这次是核心部门:网页搜索部,百度的立足之本。


经过亚联的短暂实习,我对自己的前端技能颇有信心,感觉这次应该是稳了。


然而,是我天真了。


网页搜索部的前端是重逻辑的,需要写大量的JS代码。而彼时的我,才刚把CSS入门,JS的部分只会使用jQuery写一些简单的页面交互效果。


面试官跟我反馈说没过的时候,我赶紧叫住他,说,能否指点一下我,如果想要在百度做前端的话,我应该学习些什么知识?


他也愣了一下(心想这小子不按套路出牌啊),然后拿过笔,在我笔试卷子的背面空白处,一边说一边写下一些零零散散的知识点。


拿过这张画得乱七八糟的A4纸,我如获至宝,连声感谢之后便离开了百度。


参考着这份凌乱的前端知识图谱,我窝在宿舍学习了一个月。一个月后,我给这个面试官发了条短信,请求说能不能再给我一次面试机会,我感觉我准备好了。


他同意了。于是我第3次到百度面试。


面试很快就结束了。面试官跟我说,看得出来这一个月你确实学到不少东西,进步很大,但是距离录用标准还是有点距离的,抱歉。


我又又又一次叫住面试官,和上次一样,我又跟他要了一份更加进阶的前端知识图谱。我说我回去再学习下,还会再来的。


他尴尬而不失礼貌的跟我笑了笑,便起身离开了。


这次的白纸更加密密麻麻,而且看得出来知识点比上一次又更加专业和精深了许多,学起来自然也吃力了不少。


两个月后,我再次联系到他,请求再一次面试。


他又同意了。于是我第4次到百度面试。


这次面试更快的结束了,可能我们彼此已经相对比较熟悉了吧,前几次考察过的知识点这次也就直接跳过了。


看得出来他有些犹豫,可能是他已经不太好意思当面拒绝我了,于是让我等等,找来了他的同事,给我加了一次交叉技术面。


这位新面试官说话也很直肠子,聊了二十分钟后,他直接跟我说:“技术的话勉勉强强吧,但是你这小子身上有一股抑制不住的热情和活力,学习能力也不错,感觉可以培养一下试试,我这里就给你过了吧,我再跟同事商量下,你稍等”。


过了几分钟,先前那个被我屡次骚扰的面试官来了,通知我,面试过了!


这可把我高兴坏了,经过半年多,4次5轮面试,我终于凭自己本事(厚脸皮)拿到了大公司的实习机会!(当年的百度在技术领域可是BAT之首)


这份实习工作给我带来了4000块每月的收入,让我实现了彻底的经济独立。不过这都不是最重要的,最最最重要的是,我终于知道大公司的前端开发是什么样的了!


后来我入职百度后,当我面对着屏幕上漂亮的,模块化的面向对象风格的JS代码的时候,我终于知道了业余和专业的差距。


Anyway,幸运再一次光顾了我这个小子。




在我十多年的工作经历中,诸如此类的幸运不胜枚举。旁人看到的,是我的一路顺风顺水,但只有我自己明白,生活没有奇迹,所有的幸运,都是一次次暗中努力换来的福报




全文完。


作者:沐洒
来源:juejin.cn/post/7220997060136140858
收起阅读 »

🚀 我用一小时实现的娃娃机,你敢信?

web
生活不止眼前的苟且,还有诗和远方 掘友们,大家好我是前端奶爸,入行5年的前端小学生🥜~ 工作八小时摸鱼四小时,喜欢跑步但不是为了让自己更瘦,而是为了让自己活得更久~ 活到九十九,卷到九十九~ 前言 前段时间去商场吃饭的时候看到一个有趣的娃娃机,一个密封的机...
继续阅读 »

生活不止眼前的苟且,还有诗和远方



掘友们,大家好我是前端奶爸,入行5年的前端小学生🥜~

工作八小时摸鱼四小时,喜欢跑步但不是为了让自己更瘦,而是为了让自己活得更久~

活到九十九,卷到九十九~



前言


前段时间去商场吃饭的时候看到一个有趣的娃娃机,一个密封的机器里底部放着一些被捆绑好的龙虾,可以买币去抓龙虾,抓到以后可以初加工费找附近的商家给做成龙虾大餐,感觉很有意思,把抓抓玩出了一个新的高度~


主要是抓到以后还可以出手工费进行烹饪,很吸引人,周边围观的人也很多,观察了一会发现。爪子的抓力不够,龙虾在水里还能移动,而且感觉每一个个头都不小,那小爪感觉根本抓不起来~~


到家后孩子就说爸爸你可不可以做一个娃娃机呢?


身为一个程序员,这点要求我感觉还是难不倒我,然后就突发奇想,给孩子在手机上做一个简易娃娃机。起初的想法是哄她开心,看到掘金最近有小游戏的活动,顺便分享给大家~~


效果


简易娃娃机.gif


如上图,一个移动的抓手,以及几个礼物样品,还有左右移动,抓起按钮,素材很简单,但是做出来的效果还是有娃娃机的感觉的~


地址


代码托管地址在:github在线预览地址资源路径不对无法访问,如果有需要源码的同学可以自行去git仓库获取~


布局


布局部分比较简单,直接贴代码了。可以根据自己的需求不同自定义即可~


<div class="page-portrait" id="page-portrait">
<div id="pageContainer" class="page-container game-box">
<div class="poster-main">
<ul class="poster-list">
<li class="item lw1"><img src="images/dx-lw1.png" alt=""></li>
<li class="item lw2"><img src="images/dx-lw2.png" alt=""></li>
<li class="item lw3"><img src="images/dx-lw3.png" alt=""></li>
<li class="item lw4"><img src="images/dx-lw4.png" alt=""></li>
<li class="item lw5"><img src="images/dx-lw5.png" alt=""></li>
<li class="item lw6"><img src="images/dx-lw6.png" alt=""></li>
</ul>
</div>
<div id="stop" class="button"></div>
<div id="left" class="left-btn"></div>
<div id="right" class="right-btn"></div>
<div class="zhua-top">
<span class="zhua-zuo"></span>
<span class="zhua-zhu"></span>
<div class="zhua zhuamove"></div>
</div>
</div>
</div>

css用到了几个运动处理了爪子的动效,如下方代码所示


@keyframes run {
0% {
background-image: url(../images/dx-zhua3.png);
}
25% {
background-image: url(../images/dx-zhua2.png);
}
50% {
background-image: url(../images/dx-zhua1.png);
}
75% {
background-image: url(../images/dx-zhua2.png);
}
100% {
background-image: url(../images/dx-zhua3.png);
}
}
@keyframes zhuashou {
0% {
top: 360px;
background-image: url(../images/dx-zhua1.png);
}
100% {
top: 360px;
background-image: url(../images/dx-zhua2.png);
}
}
@keyframes zhuadown {
0% {
top: 138px;
background-image: url(../images/dx-zhua1.png);
}
100% {
top: 360px;
background-image: url(../images/dx-zhua1.png);
}
}
@keyframes zhua-slideUp {
0% {
top: 360px;
background-image: url(../images/dx-zhua2.png);
}
100% {
top: 138px;
background-image: url(../images/dx-zhua2.png);
}
}
@keyframes img-slideUp {
0% {
top: 23px;
}
100% {
top: -200px;
}
}

js代码创建了一个控制器类,处理事件以及动画效果的交替等。


var Carousel = {
data: {
result: 1
},
init: function () {
Carousel.control();
},
stop: function () {
$(".zhua").removeClass("zhuamove").addClass("zhuadown");
$(".zhua-zhu").addClass("zhudown");
var timer01 = setTimeout(function () {
$(".zhua").removeClass("zhuadown").addClass("zhuashou");
var timer03 = setTimeout(function () {
$(".zhua").removeClass("zhuashou").addClass("zhuaup");
$(".zhua-zhu").removeClass("zhudown").addClass("zhuup");
$(".poster-list .lw" + (Carousel.data.result + 1)).addClass("img-slideUp");
clearTimeout(timer03);
timer03 = null;
}, 800);
var timer02 = setTimeout(function () {
$(".zhua").removeClass("zhuaup").removeClass("zhuaup1");
$(".zhua-zhu").removeClass("zhuup");
clearTimeout(timer02);
timer02 = null;
alert("恭喜您抽中一等奖~");
Carousel.start();
}, 2500);
clearTimeout(timer01);
timer01 = null;
}, 1000);
},
start: function () {
$(".zhua").addClass("zhuamove");
$(".zhua").removeClass("zhuadown").removeClass("zhuaup1").removeClass("zhuaup");
$(".poster-list .item").removeClass("img-slideUp").removeClass("img-slideOutUp");
},
zhuaMove: function (num) {
switch (num) {
case 0:
$(".zhua-top").animate({
left: -145,
},300);
break;
case 1:
$(".zhua-top").animate({
left: 0,
},300);
break;
case 2:
$(".zhua-top").animate({
left: 145,
},300);
break;
}
},
control: function () {
$("#left").on("click", function () {
Carousel.data.result--;
if (Carousel.data.result <= 0) {
Carousel.data.result = 0;
}
Carousel.zhuaMove(Carousel.data.result);
});
$("#stop").click(Carousel.stop);
$("#right").on("click", function () {
Carousel.data.result++;
if (Carousel.data.result >= 2) {
Carousel.data.result = 2;
}
Carousel.zhuaMove(Carousel.data.result);
});
},
};

总结


css现在有很多的新的特性可以解决我们工作中遇到的动效以及兼容问题,有心的同学可以多多查阅文档,写一写自己感兴趣的小demo,或者给孩子做一个小游戏来玩,何尝不是一件有成就的事呢~


我是奶爸,喜欢我的可以关注我,有什么新的想法或者意见也可以在评论区留言,我们共同学习,共同进步~



最后希望疫情早早结束,微风袭来,春暖花开~~~



作者:前端奶爸
来源:juejin.cn/post/7089371535588196366
收起阅读 »

【404】你访问的页面需要关灯后查看!

web
前言 今天在掘金首页刷到一篇文章,就是那种文字根据不同的色块显示不同的颜色,我想着能不能做一个探照灯似的 404 页面呢。毕竟也可以根据不同的白色光照来改变文字颜色的。 为了酷炫一点,先来个背景 👉 背景相对来说比较简单了,就是一些纯粹的漂浮点 <div...
继续阅读 »

前言


今天在掘金首页刷到一篇文章,就是那种文字根据不同的色块显示不同的颜色,我想着能不能做一个探照灯似的 404 页面呢。毕竟也可以根据不同的白色光照来改变文字颜色的。


为了酷炫一点,先来个背景


👉 背景相对来说比较简单了,就是一些纯粹的漂浮点


<div>
<div class="starsec"></div>
<div class="starthird"></div>
<div class="starfourth"></div>
<div class="starfifth"></div>
</div>

👉 为了显得与众不同,我们就用四个不同的 div 元素来写样式


.starsec {
content: " ";
position: absolute;
width: 3px;
height: 3px;
background: transparent;
box-shadow: 571px 173px #00BCD4, 1732px 143px #00BCD4, 1745px 454px #FF5722, 234px 784px #00BCD4, 1793px 1123px #FF9800, 1076px 504px #03A9F4, 633px 601px #FF5722, 350px 630px #FFEB3B, 1164px 782px #00BCD4, 76px 690px #3F51B5, 1825px 701px #CDDC39, 1646px 578px #FFEB3B, 544px 293px #2196F3, 445px 1061px #673AB7, 928px 47px #00BCD4, 168px 1410px #8BC34A, 777px 782px #9C27B0, 1235px 1941px #9C27B0, 104px 1690px #8BC34A, 1167px 1338px #E91E63, 345px 1652px #009688, 1682px 1196px #F44336, 1995px 494px #8BC34A, 428px 798px #FF5722, 340px 1623px #F44336, 605px 349px #9C27B0, 1339px 1344px #673AB7, 1102px 1745px #3F51B5, 1592px 1676px #2196F3, 419px 1024px #FF9800, 630px 1033px #4CAF50, 1995px 1644px #00BCD4, 1092px 712px #9C27B0, 1355px 606px #F44336, 622px 1881px #CDDC39, 1481px 621px #9E9E9E, 19px 1348px #8BC34A, 864px 1780px #E91E63, 442px 1136px #2196F3, 67px 712px #FF5722, 89px 1406px #F44336, 275px 321px #009688, 592px 630px #E91E63, 1012px 1690px #9C27B0, 1749px 23px #673AB7, 94px 1542px #FFEB3B, 1201px 1657px #3F51B5, 1505px 692px #2196F3, 1799px 601px #03A9F4, 656px 811px #00BCD4, 701px 597px #00BCD4, 1202px 46px #FF5722, 890px 569px #FF5722, 1613px 813px #2196F3, 223px 252px #FF9800, 983px 1093px #F44336, 726px 1029px #FFC107, 1764px 778px #CDDC39, 622px 1643px #F44336, 174px 1559px #673AB7, 212px 517px #00BCD4, 340px 505px #FFF, 1700px 39px #FFF, 1768px 516px #F44336, 849px 391px #FF9800, 228px 1824px #FFF, 1119px 1680px #FFC107, 812px 1480px #3F51B5, 1438px 1585px #CDDC39, 137px 1397px #FFF, 1080px 456px #673AB7, 1208px 1437px #03A9F4, 857px 281px #F44336, 1254px 1306px #CDDC39, 987px 990px #4CAF50, 1655px 911px #00BCD4, 1102px 1216px #FF5722, 1807px 1044px #FFF, 660px 435px #03A9F4, 299px 678px #4CAF50, 1193px 115px #FF9800, 918px 290px #CDDC39, 1447px 1422px #FFEB3B, 91px 1273px #9C27B0, 108px 223px #FFEB3B, 146px 754px #00BCD4, 461px 1446px #FF5722, 1004px 391px #673AB7, 1529px 516px #F44336, 1206px 845px #CDDC39, 347px 583px #009688, 1102px 1332px #F44336, 709px 1756px #00BCD4, 1972px 248px #FFF, 1669px 1344px #FF5722, 1132px 406px #F44336, 320px 1076px #CDDC39, 126px 943px #FFEB3B, 263px 604px #FF5722, 1546px 692px #F44336;
animation: animStar 150s linear infinite;
}

👉 颜色阴影部分都是一样的,不一样的地方就在于宽高和动画时长。


👉 大家可以根据自己的想法去修改不同的宽高和时长哦


👉 动画效果需要额外写一下的哦


@keyframes animStar {
0% {
transform: translateY(0px);
}

100% {
transform: translateY(-2000px);
}
}

screenshots.gif


画灯杆(电线)


👉 一般探照灯都是在顶上的,所以就需要用一根电线连接在顶部


<div class="lamp__wrap">
<div class="lamp">
<div class="cable"></div>
</div>
</div>


  • 后面的灯元素相关内容都会在 lamp 样式标签下面哦!


.lamp__wrap {
max-height: 100vh;
overflow: hidden;
max-width: 100vw;
}
.lamp {
position: absolute;
left: 0px;
right: 0px;
top: 0px;
margin: 0px auto;
width: 300px;
display: flex;
flex-direction: column;
align-items: center;
transform-origin: center top;
animation-timing-function: cubic-bezier(0.6, 0, 0.38, 1);
animation: move 5.1s infinite;
}


  • 在处理动画的时候,使用了一个 cubic-bezier 方法,它是用来定义贝塞尔曲线的


@keyframes move {
0% {
transform: rotate(40deg);
}

50% {
transform: rotate(-40deg);
}

100% {
transform: rotate(40deg);
}
}


  • 动画效果就是将灯杆旋转不同的角度



注意一下,动画效果是在整个灯的样式中完成的,所以后面的都只需要写各自的样式就行了,不需要补充动画效果。



.cable {
width: 8px;
height: 248px;
background-image: linear-gradient(rgb(32 148 218 / 70%), rgb(193 65 25)), linear-gradient(rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7)), linear-gradient(rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7));
}


  • 灯杆给了一个渐变色的样式效果


screenshots.gif


画灯罩


👉 灯杆已经有了,那就加一个灯罩就行了


<div class="cover"></div>

.cover {
width: 200px;
height: 80px;
background: #0bd5e8;
border-top-left-radius: 50%;
border-top-right-radius: 50%;
position: relative;
z-index: 200;
}


  • 灯罩是通过不同的 border-radius 的效果画出来的


screenshots.gif


画灯泡


👉 灯泡也是比较简单的样式,一个半圆加一部分阴影即可


<div class="in-cover">
<div class="bulb"></div>
</div>

.in-cover {
width: 100%;
max-width: 200px;
height: 20px;
border-radius: 100%;
background: #08ffff;
position: absolute;
left: 0px;
right: 0px;
margin: 0px auto;
bottom: -9px;
z-index: 100;
}

.in-cover .bulb {
width: 50px;
height: 50px;
background-color: #08fffa;
border-radius: 50%;
position: absolute;
left: 0px;
right: 0px;
bottom: -20px;
margin: 0px auto;
-webkit-box-shadow: 0 0 15px 7px rgba(0, 255, 255, 0.8), 0 0 40px 25px rgba(0, 255, 255, 0.5), -75px 0 30px 15px rgba(0, 255, 255, 0.2);
box-shadow: 0 0 25px 7px rgb(127 255 255 / 80%), 0 0 64px 47px rgba(0, 255, 255, 0.5), 0px 0 30px 15px rgba(0, 255, 255, 0.2);
}

screenshots.gif


来一束追光效果吧


👉 追光就是通过一个边框线画出来的


<div class="light"></div>

.light {
width: 200px;
height: 0px;
border-bottom: 900px solid rgb(44 255 255 / 24%);
border-left: 50px solid transparent;
border-right: 50px solid transparent;
position: absolute;
left: 0px;
right: 0px;
top: 270px;
margin: 0px auto;
z-index: 1;
border-radius: 90px 90px 0px 0px;
}


  • 给边框的宽度和背景透明色就可以看出追光的效果了。


screenshots.gif


文字


👉 文字通过定位居中之后,刚好显示在灯光动画效果范围之内


<section class="error">
<div class="error__content">
<div class="error__message message">
<h1 class="message__title">掘金错误页面</h1>
<p class="message__text">不好意思,你访问的页面不存在,请关灯后重新尝试</p>
</div>
</div>
</section>

👉 文字颜色和背景色一致之后,通过灯光的透明度效果就可以实现文字显隐了。


.error {
min-height: 100vh;
position: relative;
padding: 240px 0;
box-sizing: border-box;
width: 100%;
height: 100%;
text-align: center;
margin-top: 70px;
}

.error__overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;
}

.error__content {
position: absolute;
top: 50%;
left: 50%;
width: 100%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}

.error__message {
text-align: center;
color: #181828;
}

.message__title {
font-family: 'Montserrat', sans-serif;
font-weight: 900;
text-transform: uppercase;
letter-spacing: 5px;
font-size: 5.6rem;
padding-bottom: 40px;
max-width: 960px;
margin: 0 auto;
}

.message__text {
font-family: 'Montserrat', sans-serif;
line-height: 42px;
font-size: 18px;
padding: 0 60px;
max-width: 680px;
margin: auto;
}

screenshots.gif


码上掘金查看效果



作者:蜡笔小心_
来源:juejin.cn/post/7150950812489875469
收起阅读 »

记录一次机器学习模型部署

web
简介:做了一个语音识别心脏病的机器学习模型,想要实现前后端简单的部署 用到的技术栈:Python、Flask、uni-app 前端 使用uni-app做一个小程序。需要具有语音采集、录音回放、录音上传等功能。使用uni.getRecorderManager()...
继续阅读 »

简介:做了一个语音识别心脏病的机器学习模型,想要实现前后端简单的部署


用到的技术栈:Python、Flask、uni-app


前端


使用uni-app做一个小程序。需要具有语音采集、录音回放、录音上传等功能。使用uni.getRecorderManager()实现对录音全局的控制。


屏幕截图 2023-04-04 155350.png


这里给出实现上传录音并接收请求结果的主要代码


upload() {
console.log(this.voicePath)
uni.uploadFile({
url: 'http://202.115.52.33:9500/process_data',
filePath: this.voicePath,
name: 'file',
fileType: "audio", //文件类型
success: (res) => {
console.log('success',res.data)
uni.showToast({
title: '上传成功',
icon: 'success',
});
if(res.data*1 < 0.35){
uni.showModal({
title: '检测结果',
content: '您患心脏病概率为:'+ Number(res.data*100).toFixed(2) + '%'+',心脏健康请继续保持',
success: function (res) {
if (res.confirm) {
console.log('用户点击确定');
} else if (res.cancel) {
console.log('用户点击取消');
}
}
});
}else{
uni.showModal({
title: '检测结果',
content: '您患心脏病概率为:'+ Number(res.data*100).toFixed(2) + '%'+',请及时到医院检查',
success: function (res) {
if (res.confirm) {
console.log('用户点击确定');
} else if (res.cancel) {
console.log('用户点击取消');
}
}
});
}

},
fail: (err) => {
console.log((err))
uni.showToast({
title: '上传失败',
icon: 'none',
});
},
});
}

这里有个坑需要注意,微信开发者工具模拟器录音上传到服务器,服务器无法正常使用录音(一直以为是前端上传语音的问题)。开发者工具录音文件为silk格式,说是silk其实是base64加密后的webm格式,不是普通的wav格式(貌似只能用chrome浏览器打开)。可以参考这篇文章微信小程序-录音文件无法播放问题 - 知乎 (zhihu.com),真机调试则不会出现这个问题。


后端


采用Flask来进行机器学习或者深度学习模型的部署。


# app.py
from flask import Flask, request
from predict import predict

app = Flask(__name__)

@app.route('/process_data', methods=['POST'])
def process_data():
    # 从前端接收音频文件
    fileStorage = request.files['file']  # 视频文件
    buffer_data = fileStorage.read()
    filename = request.files['file'].filename
    temp_path = 'upload/'+filename
    with open(temp_path, 'wb+') as f:
        f.write(buffer_data)  # 二进制转为音频文件
    # 模型推理
    predict_outcome = round(predict(temp_path), 4)
    # 返回结果
    return str(predict_outcome)


if __name__ == "__main__":
    app.run()

部署


使用宝塔面板实现Flask项目快速部署。



  1. 在宝塔面板中安装Python项目管理器软件


屏幕截图 2023-04-04 162932.png



  1. 上传Flask项目到服务器相应目录

  2. 在Python项目管理选择Flask框架,安装Flask项目中需要的第三方包
    这里有个需要注意的问题,我修改了第三方包的源码,下载的第三方包存放目录:上传项目文件夹/一串数字_venv/lib/python3.7/site-packages,在这里修改源码重启Python服务才能生效。

  3. Python项目管理器配置参考


bind = '0.0.0.0:5000'
user = 'scu611'
workers = 1
threads = 2
backlog = 512
daemon = True
chdir = '/www/server/phpmyadmin/heartbroken'
access_log_format = '%(t)s %(p)s %(h)s "%(r)s" %(s)s %(L)s %(b)s %(f)s" "%(a)s"'
loglevel = 'info'
worker_class = 'geventwebsocket.gunicorn.workers.GeventWebSocketWorker'
errorlog = chdir + '/logs/error.log'
accesslog = chdir + '/logs/access.log'
pidfile = chdir + '/logs/heartbroken.pid'

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

Android 应用架构指南

一 简介 遵循摩尔定律,手机终端随着每年的更新换代,其性能也飞速增长。依附于此的 Android 应用规模也愈发复杂。截止 2023 年 4 月,最新版本 8.0.32 微信 apk 大小为 238MB,而对比 2011 年微信 1.0 版本 apk 包大小仅...
继续阅读 »

一 简介


遵循摩尔定律,手机终端随着每年的更新换代,其性能也飞速增长。依附于此的 Android 应用规模也愈发复杂。截止 2023 年 4 月,最新版本 8.0.32 微信 apk 大小为 238MB,而对比 2011 年微信 1.0 版本 apk 包大小仅为 457KB,短短 12 年增长了 533 倍。


image.png


随着应用规模增大,功能扩展困难、测试规模大及并行开发难等问题愈发突出。为了从根本上解决这些问题,就需要对应用进行重构,此时应用架构设计就显得尤为重要。


Android 应用架构设计三步走:



  • 现象: 程序代码和资源越来越多,代码耦合度高,扩展、维护及测试困难

  • 手段: 分离代码,提炼模式

  • 结果: 确保应用的稳健性、可测试性和可维护性


下文主要介绍三种常见的架构设计模式 MVC、MVP、MVVM


二 MVC


MVC 全称 Model View Controller,是模型(Model)-视图(View)-控制器(Controller)的缩写。


image (1).png



  • View: 负责界面数据的展示,与用户进行交互;对应于 xml布局文件和 java 代码动态 view 部分;

  • Controller: 负责逻辑业务的处理;

  • Model: 负责管理业务数据逻辑,如网络请求、数据库处理和 I/O 的操作等。


MVC 初步解决了 Activity 代码太多的问题,但 Activity 天然不可避免要处理 UI,也要处理用户交互,导致 Activity 里糅合了视图和业务的代码,分离程度不够。


优点:



  • 耦合性较低,生命周期成本低,部署快,适用于快速开发的小型项目


缺点:



  • 不适合中大型项目,View 层和 Controller 层连接过于紧密

  • View 层对 Model 层的访问效率低

  • 一般的高级 UI 页面工具和构造器不支持 MVC 模式


三 MVP


为了将 Activity 中的表现逻辑彻底分离出来,业界提出了 MVP 的设计。


MVP 全称 Model View Controller,是模型(Model)-视图(View)-呈现者(Presenter)的缩写。


image (2).png



  • View: 只负责显示 UI,只与 Presenter 层交互,与 Model 层没有耦合。对应于 ActivityXML

  • Presenter: 负责处理业务逻辑,通过接口回调 View 层;

  • Model: 负责管理业务数据逻辑,如网络请求、数据库处理和 I/O 的操作等。


在 MVP 模式中,Model 与 View 无法直接进行交互,所以 Presenter 层会从 Model 层获得数据,适当处理后交给 View 层进行显示。在 MVP 模式中,Presenter 层将 View 层和 Model 层进行隔离,使 View 和 Model 之间不存在耦合,同时将业务逻辑从 View 层剥离。


优点:



  • 逻辑结构清晰,View 层代码不再臃肿,所有的交互都发生在 Presenter 内部


缺点:



  • View 层和 Presenter 层的交互需要定义接口方法,当交互非常复杂时,需要定义很多接口方法和回调方法,增加维护复杂度

  • Presenter 层 持有 View 层的引用,当用户关闭了 View 层,但 Model 层仍然在进行耗时操作,会有内存泄漏风险


四 MVVM


MVVM 全称 Model View ViewModel,模式改动在于中间的 Presenter 改为 ViewModel,MVVM 同样将代码划分为三个部分:


image (3).png



  • View: 与 MVP 中 View 的概念相同;

  • ViewModel: 连接 View 与 Model 的中间桥梁,ViewModel 与 Model 直接交互,通过 DataBinding 将数据变化反应给 View;

  • Model: 负责管理业务数据逻辑,如网络请求、数据库处理和 I/O 的操作等。


在实现细节上,View 和 Presenter 从双向依赖变成 View 可以向 ViewModel 发指令,但 ViewModel 不会直接向 View 回调,而是让 View 通过观察者的模式去监听数据的变化,有效规避了 MVP 双向依赖的缺点。


优点:



  • 模块间充分解耦,结构清晰,职责划分清晰

  • 在 MVP 的基础上,MVVM 把 View 和 ViewModel 也进行了解耦


缺点:



  • View 与 ViewModel 的交互分散,缺少唯一修改源,不易于追踪

  • 复杂的页面需要定义多个 MutableLiveData,并且都需要暴露为不可变的 LiveData


五 参考文献


Jetpack 系列(5)—— Android UI 架构演进:从 MVC 到 MVP、MVVM、MVI


MVC、MVP、MVVM,我到底该怎么选?


作者:话唠扇贝
来源:juejin.cn/post/7220985690795524156
收起阅读 »

我的前端之路,我的经验,相信对你会有帮助

前言   离我上一次写文章已经四年有余,期间虽不断有各种想法与思考记录,但总是被各种事情和借口打断,始终没有能真正动笔,颇为内疚。此次重新提笔,在展望未来之际,先回顾与总结下过去。期望这过程中能够有所自省,有所收获。七年时光,经历了一些起伏,面临或思考过一些问...
继续阅读 »

前言


  离我上一次写文章已经四年有余,期间虽不断有各种想法与思考记录,但总是被各种事情和借口打断,始终没有能真正动笔,颇为内疚。此次重新提笔,在展望未来之际,先回顾与总结下过去。期望这过程中能够有所自省,有所收获。七年时光,经历了一些起伏,面临或思考过一些问题,大概率大家也都曾忧虑过,期望能为大家带来一点启发与共鸣。



本文鸡汤浓度略高,对鸡汤过敏症患者抱歉。但相信看完应该有一点点收获。🦥



技术之路


  2016年时,我还是个生物学硕士,蹲在实验室里摆弄大肠杆菌,对互联网并无概念。一次偶然机会,帮朋友搭个官网,接触到了wordpress。几经周折虽成功,但过程略为艰辛(各位老铁别吐槽,当时我还只是个孩子啊~🐒)。但我发现我能沉浸其中,享受那种1+1=2的确定感 (可能是因为生物学实验其实是个概率学,结果充满着不确定性),并从中收获了远比实验成功更大的成就感。就一个瞬间,我就毅然决然决定了,这就是我应该去做的事。于是放弃了我学了7年的生物学,即使我周围所有人都表示:are u kidding me?


  当我迷茫与自我怀疑时,我就会回顾这段时光,找到那个最纯粹的初心。别人在实验室看论文,我在实验室上网易云课堂(大家有没有上过的,现在我们还经常在群中唠嗑,这里致敬下猪厂)。别人在做实验,我边做实验边看红宝书。其实真不是为了找工作(我当时还很天真,没有想太多找工作的事),也不是为了给谁一个交代,就像一个玩具一样,就是一种简单的热爱。



⭐️ 初心 ⭐️


  我相信 热爱是最纯粹的初心,只有你热爱一件事并从中找到乐趣,才能真正做好它,而它也能帮你度过那些精疲力尽的时刻。


  当你发现工作让你内心煎熬时,可以适当停下来,给自己点空间,回想下最初那些开心,也许它们能治愈你的内心,并让你放下那些无谓地执着。



  其实我这种野路子,职业生涯第一步就已经落后了。相比于各位大佬,我背景没优势,没有经验,没有技术,也没有任何人脉。但人生总是充满着神奇,记得是一天我在公交车上,看qq群里有人说有实习工作。我就回了一句,没想到这就成了我的第一份实习工作。公司虽小,但大家都很nice,我也终于敢称自己为前端工程师了,感谢我的启蒙老大,respect!


  接下来,在临近毕业的一次偶然机会,我无意间看到了美图公司在隔壁学校的校招宣讲会,我就抱着学习和了解的心态去参加了。没想到直接是全场笔试,又没想到我竟然有幸成为录取的唯一前端校招生。在美图的三年,是一段美好的旅程,快速增长的业务,轻松和谐的工作氛围以及美好的同事。我如鱼得水,在技术之路上快速吸收着养分,快速学习和成长。支撑着过亿用户的产品,追求着最极致的动画交互效果,沉迷于写公司的各种工具库、开源库,天天泡在公司,也写了一些文章,代表公司作为讲师在技术沙龙上演讲,带了个小团队。


  感恩美图公司,感恩相处的每一位同事,这段期间我学到了很多。



⭐️ 关于自信与机会 ⭐️


  我相信人生处处充满着惊喜和机遇,我们不需要妄自菲薄,也不需要感叹不公。每个机会就像是你命运的分叉点,具备蝴蝶效应,引领你去往不一样的人生。而我们需要做的,就是相信自己,然后时刻准备着,迎接即将到来的机会。


  但同样,人生不是单方向的,你不可能永远成功。一定也会有错过。抓住了机会,是你的成长。错过了机会,发现你的不足,亦是你成长的机会。


  所以,保持努力,保持平常心,尽人事,听天命。




⭐️ 关于起步 ⭐️


  每段时期,都会有不同的重点。在最开始的三年,请好好珍惜。在这段时期,不需要太多负担,也不需要太多顾虑,就是保持着激情与好奇心,让自己快速成长,去追求你所感兴趣的一切。


  从前端的角度来讲,就是写代码、写代码、写代码!这就是你最好的作品和证明,去思考最优雅的实现,最高效的实现。这将会是美妙且收获满满的一段经历。



  美好生活的某一天,我突然觉得我应该毕业了。我做完了我在这里想做的事,我得到了我想要的,是时候前往下一个目的地,开启一段新的征程了。当时我的目标是想要刷一轮offer,验证下自己的成长。我拿到了字节、美团、阿里、瑞幸的offer,最终选择来到了字节。


  离开美图的那天,大家流泪送我到公司门口,那一幕仍然历历在目。但我仍然义无反顾地出发,我知道这是我该做的。



⭐️ 关于目标 ⭐️


  我们需要目标与方向,想想五年后你期望的画面,你想成为一个什么样的前端?谁是你的灯塔?可以很天真,也可以很简单。时刻谨记着目的地,这会让你不迷失方向,然后朝着这个方向坚定前进。


  把目的地拆解成一个个具象化的车站,可能是一份理想中的简历,可能是一段让人信服的项目经历或者作品,每天都离它近一点。总有一天,你一定会到达目的地。


  每一段旅程都会有终点。当完成这趟旅程的使命后,到达目的地,不要留恋,勇敢地踏上下一段旅程,相信未来将会更美好。


  这就是成长。



  在这段三年的旅程中,每段旅程对我都有着其重要的意义,是下一段旅程的铺垫与基础。


  在实习阶段,我主要研究了PC开发以及一些数据可视化的尝试,这段旅程虽短,但这是我的前端之路中的第一个重要里程碑。它让我完成了一次重要转变,从学生踏入了社会职场,从生物转成了互联网,也成为了一名正式的前端工程师,奠定了后面一切的基础。


  接下来三年的美图阶段,我的重心主要在面向C端的移动端业务。这个阶段我的自驱力很强,对一切充满着好奇与动力。在H5活动开发、Hybrid混合应用架构、Web图片处理与合成等方向进行了深入的理解和研究,奠定了我的技术体系与擅长方向。



⭐️ 关于技术 ⭐️


  其实大家一直在讨论技术广度与深度的问题。我觉得这是分阶段的,职业生涯的不同阶段应该有不同的侧重点,但最重要的就是闪光点和标签,这能让你从竞争中脱颖而出。


  前五年是奠定自身技术体系基础的时候,业务经验的限制注定了一定无法在广度上有竞争力,所以去找到自己的喜欢和擅长,并深入研究,找到自己的标签。


  只有这样,在看过你的简历,听过你的自我介绍后,别人才能记住你的名字。



业务之路


  在字节不到半年时,一次偶然的机会,我决定跟朋友一起出去玩一把。虽然我还没完成在字节的旅程,还有很多事想做,但每个人也许都会有个“总有一天我会创业我会成功”的梦想吧?也许这就是个实现梦想的机会?


  但梦想与现实的距离往往比想象中的远,其实这很正常,往往我们在梦想的时候,我们并没有足够的认知和理解,当你真正朝梦想前进时,开始动手去深入时,你就会发现事情并非你想的那样。


  我们尝试了游戏与教育结合方向的一些产品研发,其实是很好的方向与产品,受到了很多用户的一致好评。但真正的创业,并非想法与产品,更重要的是商业与价值。这个阶段,我开始跳出技术的思维模式,思考产品体系,写商业计划书,参加路演、创业比赛等,在高压下快速提升自己的综合能力,也带领团队取得了一定的进展,看到了希望。但最终我决定停下脚步,这个决定很难,在团队仍然勇气满满的时候,我却率先主动刹车,甚至我的家人都不解和质疑,为什么在希望的前夕放弃?你是不是怕吃苦?我也纠结过,也犹豫过,但经过理智的自我剖析,我认识到我离成功的所需的能力还差得太远了。即使有了阶段性的进展,但我的能力是无法带领团队获得真正成功的。当中途发现前进的方向大概率并不是理想中的目标时,那勇往无前就不一定是明智的选择了,与其博那极小概率事件,不如重新思考与沉淀。


  这段经历其实不好过,时刻紧绷着,高压逼着自己前进。现在很多人都会问我,你会后悔吗?其实并不会,我确实也失去了一些,但失去的东西是在我的舒适圈中的,我可以再努力回来。而得到的是一段我从来没经历过的生活,我收获了很多认知,也很大程度改变了我的思维模型,也加深了很多我对互联网行业、产品、业务的理解。感谢这段旅程我遇到的每个人,也许我内心带着愧疚,但我真心由衷感谢 everyone!



⭐️ 关于经历 ⭐️


  每段旅程都是一种经历,不同的经历会带来不同的视角与收获。抛出一枚硬币,得到了正面,但也会失去反面,这就是平行世界,坦然去面对,很多东西我们无法左右,纠结和情绪很多只是徒劳。


  一段旅程,会有黑夜,也会有白天。一段经历,也会有得到与失去。放下情绪,好好去欣赏沿途的风景,至少以后能回忆那些美好的事物。




⭐️ 关于创业 ⭐️


  创业其实就是一场激情亢奋的战斗,它需要你真正发自内心的热爱与激情。当你想创业之前,给自己几天的时间,弄清楚这趟行程你想要得到什么,这是不是你内心中最纯粹的目标。


  在你还没想清楚,或者你觉得你创业的目标是融资或者赚钱,那这场战斗将会无比艰难。如果你已经开始,那就需要找到那个支撑你坚持下去的信念。


另外,如果你想创业,我有一些建议:



  • 创业第一重要的是人。如果各位小伙伴有创业的想法,那第一件事,就是去找到那些最正确的战友。

  • 第二重要的是机会,去深入研究这个行业这个环境,机会在哪里,成功就在那里。

  • 想法与产品并不是最重要的,相信我,这个世界比你天才,比你有远见的人多得是,不存在这个idea惊天地泣鬼神。(深夜会让人变得情绪和极端,如果你在深夜觉得你的想法能拯救地球,那要么起床洗把脸,看下镜子里的自己,要么答应我,早点睡身体好。😂)

  • 让专业的人做专业的事,不要觉得可以学习。创业就像一个罗马竞技场,没有人会等你成长,不要拿自己的劣势,去对抗敌人的优势。



  2021年,我决定结束创业再次回到字节。我相信这里有最优秀的团队与人才,这里有我需要的东西。二进字节,我早已不是三年前的愣头青了。创业的经历给我带来了认知与思维的提升,给了我两个最重要的改变:



  1. 具备更好的结构化思维,有了一些方法论的积累。



  • 我从一条业务方向进行切入,其实类似于创业,第一步最重要的事就是找到你的战友。我把过往的小伙伴拉起来,快速组建了一个初具战斗力的小组,并且与各方建立了紧密的合作关系。

  • 第二步,就是收集信息,梳理现状,进行行业调研,充分了解业务现状,与各方对齐目标,找到当前业务最重要的事情。

  • 第三步,那就是目标拆解,推进落地。基于前面的合作互信,以及明确的目标,过去这一年多其实进展得挺顺利的,业务核心数据得到了大幅增长,业务也成为部门重要方向,我也逐步站稳了脚跟。



⭐️ 关于方法论 ⭐️


  当遇到问题或者事时,可以先不急着动手,给自己点时间思考,可以找一些资料,借鉴一些经验。而所谓的经验,就是沉淀出的方法论。例如做业务的大致思路,产生想法 -> 找到资源 -> 梳理现状 -> 明确方向 -> 制定计划 -> 拆解目标 -> 逐一完成。


  但方法论只能避免踩坑,作为指导,并不能直接解决全部问题,真正的问题仍需要 case by case 去解决,再通过复盘,进一步沉淀出方法论。其实我们的能力就像开发生态一样,我们用 react 开发业务(生态中的方法论、经验),但仍然会遇到各种问题,于是进一步沉淀出各种工具库,推进 react 生态持续完善。


  所以,复盘是形成螺旋上升闭环的关键步骤,复盘的重点是沉淀方法论。即:



  1. 业务中,我们通过复盘沉淀方法论,下次便可做得更好;

  2. 技术中,我们通过抽象封装沉淀工具库,效率便可越来越高;

  3. 成长中,我们通过复盘积累经验,个人能力便可越来越好;


  因此,不要让自己疲于奔命,适时给自己一些空间,回顾下今天,这个月,这一年,你就知道该怎么做了。




  1. 我开始更关注技术所带来的价值。



  • 创业的经历,让我需要更进一步思考,考虑每件事的目的。其实你所做的每一件事,都有其动机与目标。带着明确的目标做事,就至少不会迷失方向。即使你玩游戏,你刷抖音,如果你从一开始就明确是为了消磨时间,休闲放松,那就不会有愧疚感。

  • 因此以前是技术->完成需求。而现在则会更进一步,技术->完成需求->带来价值。价值其实就是你所做的事的目标,它可以是多样的,可以是业务收益,也可以是技术收益、团队收益,甚至个人成长等。



⭐️ 关于价值 ⭐️


我们从技术出发影响业务主要分成三个递进式的阶段:




  1. 支撑好产品的需求,推进业务发展,这是最开始的阶段,也是每个工程师的基线。这个阶段,我们是协作者,可以多去关注一些业务的数据、进展和结论。




  2. 在完成好需求的前提下,基于数据与分析,思考如何做得更好,如何带来更多的技术附加值。从前端工程师的角度出发,为业务创造价值主要有三个方向:



    1. 保障线上的稳定性,例如更高效的错误排查与治理,线上错误率显著下降,对业务指标带来正向影响。

    2. 为用户带来更好的体验,例如提升性能、优化交互等。一个体验越好的产品,用户认可度越高,留存、活跃、转化等核心指标也有可能会越好。

    3. 产品迭代效率的提升,例如通过技术工具提升研发效率、平台化提升运营效率等,人效亦是价值。




  3. 当积累了对业务足够的认知后,跳出技术视角,真正从业务、数据、产品的视角出发,带来产品功能、策略上的优化与落地。这是一个顺其自然的阶段,只有当对业务的认知达到比较高后,以专业的角度去做。(我其实不太推崇,靠着用户视角的认知,就经常提出一大堆质疑,其实只会显得自己无知。🤷🏻♂️)




  这三个阶段是递进的,只有做好前者,做后者才会是加分项。否则便可能是不务正业。我们可以通过技术外能力来增强综合实力,而且往往更全面的领域融合,能带来一些惊喜,比如业务、数据分析、设计,甚至写文档、团队合作、做PPT等。但我们的本质仍是技术,如果我们失去技术的底层,而过于重视这些上层延伸,其实是本末倒置。(当然,除非你是规划转岗转行业。)



管理之路


  随着团队的成长,协同收益会逐步增大,通常当团队人数超过某个阈值,协同收益就会开始大于你的单人收益。这时候,开始将精力更多放在团队管理上,去提升团队的整体协同效应,解决效率卡点,是件更高性价比的事。但相信很多小伙伴也都会遇到:刚开始做管理工作时,每天好像都很忙的样子,工作、时间、精力开始碎片化,一会儿有个会议,一会儿有个讨论,一会儿有个流程。但睡觉前回顾,又好像什么事情都没完成。不知不觉,人就会陷入一种自我怀疑的状态中,怀疑自己做的事是不是有意义,担心自己的技术会不会就荒废了。


  我刚开始时也一样陷入其中。人在迷茫的时候,可以多去看看书。相比于在网上看资料,看书有个好处,就是会让人冷静下来,让你也有了更多的个人思考空间。很庆幸,我很快就得到了一些启发,也想明白了一些事,让我能继续往前走。


  从技术实现到技术管理,其实是一种思维方式的进阶。迷茫、怀疑的本质,在于认知没有产生进阶去匹配上当前的阶段需要。因此,我觉得从技术认知到管理认知,这可能是每个小伙伴走上管理之路的第一堂必修课。那为什么我会怀疑我做的事没有意义,而担心自己的技术会荒废?其实思维的本质是:我默认觉得技术是硬通货,而管理是外包装。这其实就是技术型认知,认为只有代码才是真正的技术,而编码以外的东西都是虚的。这也是为什么我们经常能听到抱怨文档工程师,PPT工程师等。


  我的第一个管理认知:管理同样也是一门技术,它也是一种硬通货。甚至文档书写、沟通技巧、总结演讲等能力,也同样是一门技术,也同样有非常多书籍在讲如何提升。当我把管理也同样看成一门技术的时候,我发现从本质上来看,这与 HTML、JavaScript、React 并没有差别,它们都是一种能力,一种去完成目标所需要的工具。当我这么想后,这不就进入了我们前端最擅长的领域,不就是学嘛(前端还怕学?🐒)。


  而学习的过程,就是建立一套属于自己的方法论(其实就是理解方式)。学习管理,就是沉淀出一套自己的方法论,形成自己的一套管理风格,去拆解重点,并逐一思考与实践:



  • 我理想中的团队是怎么样的?

  • 怎么做团队规划?

  • 怎么做团队沟通?

  • 如何做任务分配?

  • 怎么做团队技术建设?

  • 怎么做团队梯度建设?

  • 如何提升团队凝聚力?

  • ...


  另外还有很重要的一点,我觉得是管理自信。在技术认知中,我会觉得我就是喜欢并擅长做技术,但对于技术外的事情,刚开始就很容易出现缺乏自信的状态。比如我有一段就因为没有得到一些正向反馈,导致就一直在怀疑自己是不是做得不够好。也有一段一些小事情没处理好,就会觉得自己能力不足。我在反思后,觉得核心的原因主要有两个:



  • 经验不足,方法论欠缺,导致在管理工作中,缺乏一些思维主轴,做不好的概率变高;

  • 心理包袱,总觉得我不能辜负领导的期望,如果过程中刚好缺乏一些正向反馈,就容易多想是不是我哪里做得不好,让别人失望了;


  其实,第一点也源于上面提到的管理认知,因此只要将管理当成一门技术,给自己一点时间,循序渐进去拆解和沉淀,那自然就会越来越好的,我想没有前端会因为不会 React 就感到自卑吧。而第二点,一方面领导是很了解你的情况的,你的经验,你的优劣势,都会有一定的把握,通常并不会有不合理的期待,也同样会给你足够的空间来成长的。另一方面,我们也不需要完全围绕着别人的评价转,你可以相信自己的判断。不过这里延伸出来的一点,在人的相处中,合适的时机给与正向的反馈是十分重要的,它可能能让对方从自我怀疑中解脱出来。


  不过,相比于很多成熟的管理者,我还只是刚走出新手村,就不在这里高谈阔论了。待我再修炼修炼,再找个机会来跟大家分享更多管理上的思考与实践,也欢迎大家跟我一起探讨。



⭐️ 关于困惑 ⭐️


  我们无时无刻在困惑,而困惑就会带来烦恼与纠结。因此我们开始变得害怕面对困惑,畏难的情绪会引诱我们躲在自己的舒适区中。但这其实不就是人生的一部分吗?困惑,其实还有另外一个名称,叫做好奇。从出生那一刻起,你就在困惑这个世界,而你就是在一个一个的困惑中逐步成长的,有困惑是好事。


  所以这么想,是不是就没有必要去害怕面对了,自然而然去面对,出发去寻找那个属于自己的答案。给自己一些思考的时间,又或者与一名智者聊聊,去找到困惑的本质根源,然后再一一的解开那个心结,你便又成长了。



人生之路


  七年的时间,我从一个天真无知、怀揣梦想的少年一步步成长。我们中的大部分人的人生都是类似的,走着类似的路,做着类似的事。工作上,我们毕业求职升职加薪,生活上,我们恋爱吵架结婚生子。但过着过着,每个人的人生却都完全不一样。一个个小小的决定与机会,最终会让我们都走上自己的命运。


  回望过去的轨迹,首先我先看到的是规划。我的人生就是一场提前做好规划的旅行。每一步都是在规划中进行的,找什么工作、什么时机跳槽、怎么理财存钱、什么时候结婚、什么时候做什么事,都是在计划之中。其实有时我在想这是好的吗?好的一面,规划总是往好的方向的,它能让你时刻保持着前进的方向。提前的规划,会让你能够提前对一些意外情况进行规避,也能够尽量避免一些不好的事情。但另一面,完全的规划可能会让你变得过于理智,缺乏激情,而且规划可能会扼杀那些疯狂的想法,生活也许也会少了那一丝惊喜和精彩,有时候,没有计划,说走就走的旅行,反而会有意外的美丽。例如创业,也是因为我的规划和理智,我需要停下脚步重回字节。但我其实再也无法知道,如果我坚持下去的结果会是什么,也许有惊喜呢?



⭐️ 关于规划 ⭐️


  所以现在我的感悟是规划是必要的,它能让你保持正确的方向,减少迷茫。我经常跟组员讨论的就是想想三年、五年后你应该在做什么,这样你就知道你想要什么了。


  但我经常看到很多人非常不喜欢打破自己的计划,一旦一些事情或者别人打破了自己的计划,就会变得很焦虑,很有情绪,其实这也不好。我希望规划是开放的,它可以被打破,它可以随机应变,它也可以接受更多意外的发生。也许这样,你才不会错过那意外的美丽。



  另外一个点我想提的是我的乐观与积极。人生不可能永远一帆风顺,也必然伴随着痛苦和失落,我也有很多睡不着的夜晚。但我始终相信着事情会好起来的,我可以的,我努力下一定能够搞定的。比如工作上,我很少有什么畏难情绪,从我个人而言,我反而喜欢那种很有挑战的工作,比如我曾花很大精力在WebGL上手撸一个类IOS滚动条,各种手势、惯性回弹参数、边界情况等,我想大部分人会觉得吃力不讨好,可能会偏向于找个现成的库,然后跟产品说这部分没法实现。而我就会偏向于自己写一个来努力满足所有需求,并且我大概率会乐在其中,即使因此可能变得压力很大,排期很紧张之类的。


  最后,还有一个最最重要的点:身体健康永远排在第一位,没有什么事情比身体健康更重要,不仅是你自己,还包含着你的家人。而保持身体最重要的核心,就是重视它关注它。当你觉得疲惫了,就去休息。当你觉得哪里不舒服了,就去医院做下检查。当你觉得最近生活不太健康,那就动起来,让自己过得更好点。


  所以,生活总结起来就是:健健康康,保持着乐观与积极,做好规划,然后大步地向前走!



🐒 最后,关于我


  非常感谢看到这里的各位小伙伴,感恩!文章的鸡汤浓度有点高,有些地方可能带着一些我的主观,也不一定是正确的,仅供大家参考,如果能对大家有所启发和帮助,那我就很开心了。如果不能理解的,又或者一些我没提到的,比如晋升、绩效啥的,也许可以来跟我讨论讨论,可能也能碰撞出一些有意思的东西。


  另外,我也同样有很多的烦恼和缺点:说要健身,一直动不起来。说要学英语,一直没进展。书也看得不够多。说一定要开始写文章,也一直进展缓慢。有没有大佬能拯救下我。所以期待跟大家一起交流,一起成长,一起变得更好。共勉~





作者:郭东东
来源:juejin.cn/post/7220244993788854309

收起阅读 »

接受外包Offer前一定要清楚的4件事

Hello 我是光影少年。 最近有一些刚毕业的小朋友私信我,说工作贼难找,能不能先去一个软件外包公司先苟着,之后的事情等行情好些了再说。 去外包公司当然没什么不可以,成年人能基于实际做出判断和选择,并承受相应的结果就行。 只是我想补充一些细节,让可能有这个想法...
继续阅读 »

Hello 我是光影少年。


最近有一些刚毕业的小朋友私信我,说工作贼难找,能不能先去一个软件外包公司先苟着,之后的事情等行情好些了再说。


去外包公司当然没什么不可以,成年人能基于实际做出判断和选择,并承受相应的结果就行。


只是我想补充一些细节,让可能有这个想法的小朋友们对这种公司和岗位有一些大概的了解。


在这里先给部分不太清楚的观众扫个盲,软件开发行业有两种基本的用工方式:



  • 作为所服务公司的正式员工,和该公司主体签订劳动合同。

  • 作为所服务公司的劳务派遣员工,和所服务公司委托的第三方公司签订劳动合同。简单来说,你和所服务的公司实际上没有什么关系,是那家签合同的第三方公司的员工,只是替这家公司干活而已。这也是俗称的「外包员工」或者「顾问」


劳务派遣这种用工方式已经广泛地存在于许多的行业。事实上,有些公司中,围绕着派遣员工和正式员工之间的差异,存在着一整套完整的「潜规则」。


围绕着这两种不同类型的员工,在日常工作中往往会衍生出的所谓「戴蓝色工牌」和「戴绿色工牌」的区别对待的概念。


「戴绿色工牌」的他们的工资通常较低,没有其他福利待遇,在某些公司甚至被告知他们不能使用专门的员工通道,也不能参与公司组织的一些诸如团建,抽奖等活动。


「戴蓝色工牌」的是正式员工,有权享受所有的福利待遇。同时要想戴上蓝色工牌是很困难的。


在劳务派遣公司招募「戴绿色工牌」的员工时,往往会拿「表现良好者可以戴上蓝色工牌」这种承诺来作为吸引人的筹码,但是绝少有人能够如愿以偿。


表面上看上去,似乎完全不用选,正式员工就是比派遣员工要好。在大多数情况下这没错。不过我还是想介绍一下派遣员工的现状,和你在考虑是否接受这个岗位时可能会忽略的一些实际情况。


在这里,我只介绍狭义上「劳务派遣」所指的群体,


这种类型的岗位员工和一家代理机构(比如最常见的某软,某辉)签订劳动合同,然后这家机构会把他们派遣到客户公司的现场,和客户公司的雇员一起工作。


相对于客户公司,他们就是「劳务派遣」员工。代理机构会定期向客户收钱,然后从中抽成之后,再把「工资」发给员工。比如客户公司为一个岗位支付的成本是2w/月,很可能发到员工手里只有1w/月(随便举例,不代表实际情况)。


作为这种类型的员工,通常会要求定期填写一份工时记录表,来证明为客户工作的时间,然后根据这张表从所在的机构获得报酬。


代理机构的最根本的盈利模式就是赚差价,所以通常客户向机构支付的费用要比机构实际支付给员工的工资高出 50%~100%的情况也并不算少见。


可能有些小伙伴看到这里已经开始生气了。「存在即合理」,在国内,代理机构起码还得负责员工的五险一金等一些其他的基本保障。往往通过代理机构给大型客户工作,比直接去一家朝不保夕的初创公司还是稳定轻松许多,甚至收入也会高上不少——哪怕已经被拿走了一大部分。


当然,如果咱们已经选择接受一个派遣的岗位,你可以通过一些方式了解到客户给你付出的成本,之后不论是和机构谈判,还是更新对自己的定位都有好处。


同工不同酬


首先就要聊最重要的:

关于薪资。


在大多数情况下,在同等工作岗位上,作为一个派遣员工,只看固定薪资的话,单价是更高的。


在我之前的工作岗位上,我的固定时薪是比不上和我同样职级的顾问同学的。


但是,但是,通常能大量使用派遣员工的大型企业,他们的薪资构成不光是固定薪资。往往是:


薪资总包 = 薪资+绩效+其他(各种福利、股票期权等等)


派遣员工往往只有固定薪资,或者一些超时工作的加班补偿。


稳定性


为什么公司宁可支付更高的单价,也要大量使用派遣员工呢?其中一个很重要的因素就是控制固定成本支出


雇佣一个正式员工,除了薪资总包以外,还需要支付和固定工资匹配的五险一金,其他的各种福利,雇佣一个人付出的成本大概是纸面薪资的1.5倍甚至更高。而且如果公司遇到业务收缩,需要裁撤员工,正式员工也需要支付大量的赔偿金。


如果是派遣员工,那么大可以在项目建设期需要大量人力投入时短期购入大量的人手,在项目上线稳定进入维护期后终止合作,而遣散派遣员工所付出的成本低的可以忽略不计。


所以,如果你对于频繁地更换工作场所有所抵触的话,派遣员工可能对你来说是个很大的挑战。尤其是在有些时段没有什么客户需要,很可能要在代理机构坐冷板凳,甚至被裁撤掉。


工作环境


就像本文开头提到的,有人的地方就有江湖。在有些公司,派遣员工和正式员工之间存在巨大的鸿沟;有些公司中又不是那么明显,但就像一道空气墙,不知道在什么时间就会拦住你的去路。


为了控制成本,在扩张员工规模时的大型企业是十分谨慎的,对待正式员工的招募流程会比派遣员工的进场流程严格的多。高标准,严要求下,如果正式员工的数量还比派遣员工的数量少,那么难免的一些「精英思维」就会开始弥漫。


这是难以避免的,这也是许多在派遣员工岗位的小伙伴反复和我提及的:


一定要端正心态。


在很多你不经意的时候,甚至会有「被抛弃」的感觉。并不是每个客户公司的氛围都那么让人舒服,但这就是正常的,不要太过于纠结这个。


而且派遣员工有一个小小的优势,正式员工通常会因为卷而主动加班,而这种加班在不少地方是无偿的,派遣员工由于是代理机构提供的劳动力,所以通常加班都是有补偿的——毕竟你们的工作就是机构的收益,机构也会争取他们的利益。


长期发展


作为派遣员工,短期收益甚至可以说还不错。进入大型公司的劳务派遣基本不会比去初创企业工作难多少。而且也没有传统大企业对于正式员工所提出的高要求,压死人的KPI之类的影响。


但如果你想从工作中获得的不仅仅只有报酬,更想有一些为日后的履历增光添彩的项目经历的话,派遣员工可能就不是很合适。通常情况下,客户公司总倾向于让派遣员工去做一些相对简单的外围部分工作,核心的部分很难接触到,这没得说。


除非你愿意非常频繁地跳槽来换工作,否则涨薪在派遣员工中也是很困难的,而作为规模公司的正式员工,往往有很成熟的薪资上升机制,也有晋升的机会。


而且业内有一些传言,把进代理机构的经历称之为「弄花简历」,这段经历有可能会对后期想要进入大厂时的筛选过程有一定的影响。


至于说HR招人时所提到的「有转为正式员工的机会」,就像真爱,听过的人多,见过的人少,

作者:幸福少年
来源:juejin.cn/post/7217360688264134713
我也不过多发表意见。

收起阅读 »

四月·十字路口

"睡醒了就去打工,打完工就回去睡觉,想学坏都没时间",打工人的真实写照。即便放假我也是无所适从,乏善可陈。 本月下旬离职ing,这份工作是被迫离职,可以理解为"被开"。从入职之初就定义了这份工作:阶段性过渡。可是踏入社会的几次工作都告诉自己,不停的过渡过渡...
继续阅读 »

"睡醒了就去打工,打完工就回去睡觉,想学坏都没时间",打工人的真实写照。即便放假我也是无所适从,乏善可陈。



40ccab4ceae8ee8656db8ff5145f046.jpg
本月下旬离职ing,这份工作是被迫离职,可以理解为"被开"。从入职之初就定义了这份工作:阶段性过渡。可是踏入社会的几次工作都告诉自己,不停的过渡过渡过渡ing,似乎我完全不明白自己想要的是什么,胡乱抓瞎。


今又四月,最近作息颠倒,精神状态每况愈下,对现状及对未来的过多思考导致夜不能寐,复盘下这一年的种种。为了进行一次有效跳槽,此处稍作思考,梳理下:


1. 工作的能力问题




  • 此前作为初创公司的星麦,我是当时入职应届生中坚持的最久的员工了,很遗憾坚持了半年Pua主动出局。我的前端天花板老大,没有领取年终奖就跑了,对面一排的后端都提出了离职,又或者年后离职,恶劣的人文环境可见一斑。天花板曾打趣说,我应该去外包磨砺一番,这个公司不适合我,也让我回想起入职之初,部门的同事说我并不适合这种初创公司。




  • 现在这个甲方说我能力不足,调过一次项目组,碰到个刚入职的经理跟她发生过嘴角争执,觉得她不懂技术,可能当时对自己太过自信了,没处理好国企领导人之间微妙的关系,后面我又去接手另外一个项目,期间我觉得没有任何不适,这个项目让我明白了,背锅侠一定不要当,且负责人居然要出国深造,他说他扛不住了,人家都有预知风险的能力,我何尝不可?何况他是甲方都要跑,值得深思。不要沦为工位旁边的同事,不然后期颓丧成常态,一发不可收拾。





如果要换工作就好好换工作,不要带着上一份的疲惫和情绪去找工作,更好的割舍就是更好的出发。



2. 下一份工作的规划问题



  • 人文环境:"无政府"状态的工作,温水青蛙,搞不好会把自己干废,影响我的创造力和学习的劲头

  • 项目情况:技术老旧,Jsp写不上简历的项目,天花板很低,一年十年也差别不大

  • 个人成长:原项目无成长,也没有可以深度学习的前辈。好在缓冲了一年,也算看进去了点技术文档

  • 家人爱人:生活和工作状态失衡,心灵比较空虚

  • 职业发展:不能为技术和简历添彩,稳定程度很低,甲方说开就开

  • 离职成本:较低,住房成本到武汉缓冲一点时间,不至于那么焦虑


此次跳槽,属于发展性跳槽,更是为了给自己负责。单独生活了两年,心智逐渐稳定,但人不应该局限在"无朋友"的圈子,要构建新的生活圈,出门丢个垃圾和买瓶饮料已经是极限了。本以为我不会被"形单影只"所影响,终究是高估了自己。我的第一诉求,稳定+技术符合大众潮流(如Ts、React,而不是人人都会的Vue),薪资符合大众市场,首推React技术栈,公司规模中大型,向下扎根,向上开花(长长久久发展)


房子到期还有两个月,前提,找到工作这是一次规划,允许自己一个月找不到工作。不要自乱阵脚,城市选择:Base武汉or杭州。


4. 下一份工作的入职注意点



  • 是否属于外包(问清楚)

  • 面试谈薪

  • 薪资构成

  • 绩效考核

  • 年薪是几薪

  • 五险一金比例

  • 晋升机会

  • 试用期时间


5. 俯瞰未来


青年人,要追逐自己的梦想,立业、成家,有条件去深度探讨自己的出路,商人还是公务员需要后面去论证,需要有人指点迷津,而不是摸瞎过日子。


最后的最后,接下来的每一天都要有计划的准备面试,周末不应该这样度过,玩没玩好,计划也没计划。


作者:鼓浪屿
来源:juejin.cn/post/7219961144584290360
收起阅读 »

Flutter和Android原生通信的三种方式

简介 Flutter虽然有强大的跨平台能力,但是其跨平台主要体现在UI界框架上,对于一些Android原生的功能如:获取电池电量,访问手机蓝牙,定位等硬件信息显得有些不足心。还是需要调用Android原生方法获取。所以使用Flutter和Android原生通信...
继续阅读 »

简介


Flutter虽然有强大的跨平台能力,但是其跨平台主要体现在UI界框架上,对于一些Android原生的功能如:获取电池电量,访问手机蓝牙,定位等硬件信息显得有些不足心。还是需要调用Android原生方法获取。所以使用Flutter和Android原生通信的方式是必不可少的。 本文正在参加「金石计划」


本文主要介绍Flutter与Android原生三种通信方式的用法。


1.BasicMessageChannel


定义


双向通信,,有返回值,主要用于传递字符串和半结构化的信息。


基本使用


1)Flutter端



  1. 创建BasicMessageChannel对象并定义channel的name,必须和Android一端保持一致。


 late BasicMessageChannel<String> _messageChannel;
_messageChannel = const BasicMessageChannel<String>("baseMessageChannel",
StringCodec());


  1. 设置setMessageHandler监听Android原生发送的消息。


_messageChannel.setMessageHandler((message) async {
print("flutter :Message form Android reply:$message");
return "flutter already received reply ";
});


  1. 发送消息给Android原生。


 _messageChannel.send("Hello Android,I come form Flutter");

2)Android端



  1. 初始化BasicMessageChannel,定义Channel名称。


val messageChannel = BasicMessageChannel<String>(
flutterEngine.dartExecutor.binaryMessenger,
"baseMessageChannel",StringCodec.INSTANCE)


  1. 设置setMessageHandler监听来自Flutter的消息。


 messageChannel.setMessageHandler { message, reply ->
Log.i("flutter", "android receive message form flutter :$message")

}


  1. 主动发送消息给Flutter。


  messageChannel.send("flutter")

打印结果如下:



从用法上来看,Flutter和Android原生基本上是一样的,只不过是不同语言的不同写法。Flutter端主动调用send方法将消息传递给Android原生。然后打印log日志。


2.EventChannel


定义


基本使用


单向通信,是一种Android native向Flutter发送数据的单向通信方式,Flutter无法返回任何数据给Android native。主要用于Android native向Flutter发送手机电量变化、网络连接变化、陀螺仪、传感器等信息,主要用于数据流的通信


1)Flutter端



  1. 创建EventChannel对象,并给定channel名称。


late EventChannel _eventChannel;
_eventChannel = const EventChannel("eventChannel");


  1. 使用receiveBroadcastStream接收Android端的消息。


 _eventChannel.receiveBroadcastStream().listen( (event){
print("Flutter:Flutter receive from Android:$event");
},onDone: (){
print("完成");
},onError: (error){
print("失败:$error");
});

2)Android端



  1. 创建EventChannel对象,并定义channel的name。


 val eventChannel  = EventChannel(flutterEngine.dartExecutor.binaryMessenger,"eventChannel")


  1. 设置setStreamHandler监听。


  eventChannel.setStreamHandler(object :StreamHandler{
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
eventSink = events
}

override fun onCancel(arguments: Any?) {
}

})


  1. 发送消息给Flutter端。


 Handler(mainLooper).postDelayed({
Log.i("flutter", "android send message")
eventSink?.success("Hello Flutter")
},500)

打印结果如下:



EventChannel是单向通信,并且只能从Android原生端发送消息给Flutter,Flutter不可以主动发消息,使用有局限性。


3.MethodChannel


定义


双向异步通信,Flutter和原生端都可以主动发起,同时可以互相传递数据,用于传递方法调用。


基本使用


1)Flutter



  1. 创建MethodChannel对象,并定义通道名称。


 late MethodChannel _methodChannel;
_methodChannel = const MethodChannel('methodChannel');


  1. 设置setMethodCallHandler异步监听消息。


 _methodChannel.setMethodCallHandler((call) async {

});


  1. 调用invokeMethod方法发送消息给Android,并接收返回值。


  var map = {'name':'小明','age':18};
String result = await _methodChannel.invokeMethod('openMethodChannel',map);
print("Flutter接收Android返回的结果:$result");

2)Android端



  1. 创建MethodChannel对象,并定义通道名称。


 val methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger,"methodChannel")


  1. 设置setMethodCallHandler监听Flutter消息,并通过MethodCall.method区分方法名。


methodChannel.setMethodCallHandler { call, result ->
if (call.method == "openMethodChannel"){
val name = call.argument<String>("name")
val age = call.argument<Int>("age")
Log.i("flutter", "android receive form:$name ,$age ")
result.success("success")
}

}


  1. 发送消息给Flutter。


  messageChannel.send("Hello Flutter")

打印结果如下:



MethodChannel是Flutter和Android原生常用的通信方式,不仅可以异步通信,还能传递数据集合,Map等。通过定义不同的方法名,调用Android不同的功能。


总结


本文主要介绍Flutter与Android原生的三种通信方式,可以根据实际开发中的业务,选择合适的通信方式。熟练掌握三种通信方式可以在Flutter开发中使用Android原生提供的功能。


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

【转载】程序员不爱加班但为啥还总要加班?

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

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


关注程序员耳东,编程转码真轻松



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



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


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


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


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


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


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


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


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


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


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


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


上联是:这个需求很简单


下联是:怎么实现我不管


横批是:明天上线


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


会议太多,占用时间


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



  1. 需求评审会

  2. 技术方案评审会

  3. 需求复盘会

  4. 细节对齐会

  5. xx 项目启动会议

  6. xx 横向项目

  7. 技术分享会

  8. 周会

  9. 测试用例评审

  10. OKR 会议

  11. CodeReview 会议

  12. 等等......


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


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


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


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


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


技术需求,各种丐版轮子


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


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


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


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


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


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


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


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


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


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


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


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


总结


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


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

上海00后小伙AI「复活」奶奶,100%还原音容笑貌,引发巨大争议

**【新智元导读】**最近,00 后小伙用 AI 技术和奶奶实现「对话」,数字生命要成为现实了吗? 你有没有特别想念的人?可能 ta 是你的亲人,也可能是你的伴侣。 无论 ta 是谁,在我们的回忆中永远有他们的一席之地,他们一辈子都会在我们心底最柔软的角落驻留...
继续阅读 »

**【新智元导读】**最近,00 后小伙用 AI 技术和奶奶实现「对话」,数字生命要成为现实了吗?


你有没有特别想念的人?可能 ta 是你的亲人,也可能是你的伴侣。


无论 ta 是谁,在我们的回忆中永远有他们的一席之地,他们一辈子都会在我们心底最柔软的角落驻留。


在哈利波特与死亡圣器中,哈利手握魔法石,他已故的爸爸、妈妈、教父和老师化作幽灵围绕在他的身边。



哈利和他们聊天,攀谈,诉说衷肠,好像他们活过来了一样,紧紧地围绕着他。


而如今,随着 AI 技术和数字虚拟人技术的不断发展,让已故的亲人变成虚拟的数字人继续和我们交谈不再是停留在科幻小说中的情节。


最近,B 站 UP 主吴伍六就用 Midjounrney、ChatGPT 和 D-ID 等最新的 AI 技术「复活」了他刚刚去世的奶奶。


网友看后纷纷表示,恐怖谷。



用 AI「复活」奶奶


在最新的一期视频中,吴伍六为我们还原了「复活」奶奶的整个过程。


首先是头像。要想生成逼真的对话,那么一个栩栩如生的 AI 头像是必不可少的。


吴伍六选择了当下主流的 AI 绘画软件 Midjourney,并上传了一张自己的照片。



最终,他选择了下面这张作为头像,并继续优化。



奶奶的头像也是一样,AI 根据上传的图片与文字描述生成了一些选择,而 UP 主再根据脑海中的回忆挑选一张最逼真的。



之后再如法炮制,用文字细化某些面部特征。比如皱纹、眼眸、发型和神态。我们可以看到,生成的头像细节非常完美。



下一步是音频。这一步的素材基本来自于过去的电话录音、录像视频或者微信语音。而 UP 主将过去奶奶的电话音频上传给了 AI。



并用音频编辑软件 AU 进行调整。调整的方向主要集中在降噪、人声增强等等。


然后将更加清晰的音频样本切割成若干秒的短句,方便进行标注。


最后将处理好的音频打包放入语音合成系统中去。



利用语音合成系统,UP 主就可以尝试输入文本转语音了。


现在,基本的素材都齐全了。接下来就是最关键的一步——生成虚拟数字人。


通过 D-ID,用户可以生成数字虚拟人,并和他们展开交流。



在左侧选择头像,右侧上传文本或音频即可。



网站上奶奶的形象栩栩如生,音频也完美还原了奶奶的「乡音」。搭配上右侧拉家常一般的聊天内容,宛如和奶奶面对面视频通话一样。



有意思的是,奶奶的这些「回复」是 UP 主借助时下大热的 ChatGPT 生成的。


他告诉 ChatGPT,「我很想我的奶奶,你能不能模仿她的口吻来和我交谈?」


透着人情味的人工智能,很难不让人感动落泪。而借助对奶奶的回忆,UP 主也竭尽全力地让 ChatGPT 更像她的奶奶。



科技的最终目标是给人提供力量。这种力量不仅是物质上的,更重要的还是精神上的。虽然这些聊天内容并不完全拟人,也会有一些不贴切的表达。


但总的来说,生活中的小细节还是能给人最温柔的体验的。



数字生命,可行吗?


吴伍六的这段视频引爆了 B 站,网友们众说纷纭。


有关于把逝去的亲人变成虚拟数字人的做法还是个新鲜事物。观念上的转变并不那么容易。


UP 主亲自评论表示,这么做的目的是为了填补自己的小遗憾。在自述中他也提到,奶奶在大年初六去世,最后的几天里奶奶意识模糊,而 UP 主也没有好好的和奶奶告别。


他表示,不会过度缅怀,希望大家能通过 AI 的发展看到未来世界的多样性。



网友「水澹澹而生烟」也表示,这段视频激起了对自己爷爷的回忆。他在 AI 应用和伦理方面并没有过多想法,也不认可把数字虚拟人当作亲人生命的延续。


但他表示,如果能再看爷爷一眼,听一听爷爷的声音,自己就会倍感慰藉。



也有网友表示了不赞同的态度。铁缸哟就认为,数字虚拟人终归还是当不了缅怀之人的精神寄托的。



不过该项技术的未来发展如何,谁也不能预料。就像流浪地球 2 里提到的数字生命的概念一样,想象和现实之间总会存在鸿沟。


没有走到那一步,或者走到那一步之前,一切都是未知的。



当然,小编比较认可网友键垩家の执事的评论。无论虚拟数字人发展到什么地步,多珍藏一些自己所爱之人的照片、音频以及影像这件事,永远不会错。


让记忆停留在心里,让回忆变得更清晰。


参考资料:http://www.bilibili.com/video/BV1QM…


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

阿里正式加入ChatGPT战局,“通义千问”突然上线邀测!

阿里版ChatGPT实录 通义千问作为一个大语言模型,它的能力主要集中在文本生成上,即也能像ChatGPT一样“问啥答啥”,除此之外,它还具备一个“百宝袋”功能,功能也非常酷炫,大家普遍感兴趣的就是菜谱生成、彩虹屁生成器和免费代写情书。里面相当于一个工具箱,能...
继续阅读 »

阿里版ChatGPT实录


通义千问作为一个大语言模型,它的能力主要集中在文本生成上,即也能像ChatGPT一样“问啥答啥”,除此之外,它还具备一个“百宝袋”功能,功能也非常酷炫,大家普遍感兴趣的就是菜谱生成、彩虹屁生成器和免费代写情书。里面相当于一个工具箱,能快速生成各种指定类型的文案。


image.png


通义千问能完成哪些任务,解放哪些生产力,能达到 ChatGPT 几分功力?


从实际表现来看,通义千问与领先的GPT 4.0还有很大的差距,但是对比此前的一些AI大模型,通义千问在问答逻辑、信息检索、语句理解等方面都已经有了明显的进步,我现在对中国AI大模型的未来充满了信心。


image.png


通义千问从何而来?


通义千问的诞生并非偶然,是多年技术积累的结果。


2019年,阿里便开始了大模型的研发,当时阿里发布的语言大模型StructBERT超越谷歌、微软、Facebook,登顶了CLUE榜单。


2021年,阿里达摩院发布了国内首个超百亿参数多模态大模型M6,号称神经元达人类的10倍。


在2022年的世界人工智能大会上,阿里还发布了「通义」大模型系列,并宣布相关核心模型向全球开发者开源开放。


image.png


关于通义千问的技术细节,阿里达摩院官方没有透露详细信息。尽管官方口径低调,但懂的都懂,国内科技圈直接炸裂。实力玩家入场,大模型的竞争真正开始了。


正如ChatGPT脱胎于OpenAI的GPT系列,百度文心一言是自Ernie大模型发展而来,阿里也是国内最早开始研发大模型的技术大厂之一。


其中,M6在多次迭代之后,实现了十万亿级别的参数规模,并且M6和支付宝、淘宝的业务需求相结合,首个在国内实现了商业化落地。在去年的WAIC(世界人工智能大会)上,阿里还发布了通义大模型系列。其中核心模型均已开源开放。


image.png


人人都是低代码开发者


生成式AI产品潜力无穷,但能做出类似应用且可将其商业化的公司却凤毛麟角。不少企业认为AI能够助力企业建设,但企业内部多年积累下来的问题非常复杂,比如企业的一个人工智能平台所需要的数据需要从数十个系统获得,而这些系统的对接工作需要花费大量时间和精力,分属同一领域的不同企业的基础设施并不完全相同,简单复制粘贴是无法达到效果的。


在低代码的发展中,流行着一句话:人人都是低代码开发者。


在云计算与软件业,低代码开发,甚至无代码开发是近两年流行的热门概念。核心逻辑是利用代码库,将已有开发样例进行快速复写,整个开发过程中的人工成本趋近于0。


image.png


一套完善的底座意味着在大部分领域通用的解决方案能力。


JNPF,依托代码开发技术原理因此区别于传统开发交付周期长、二次开发难、技术门槛高的痛点。大部分的应用搭建都是通过拖拽控件实现,简单易上手,通过为开发者提供可视化的应用开发环境,降低或去除应用开发对原生代码编写的需求量,进而实现便捷构建应用程序的一种开发平台,快速助力研发人员快速搭建出一套适合企业发展的方案。


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


image.png


大模型时代,中国力量加速竞逐


相比于现在的业界标杆ChatGPT,通义千问的进步空间还很大,阿里方面也透露,根据内测反馈,这一大模型正在飞速迭代中。


同时,阿里是全球少数在算法和算力上都有领先布局的公司之一,在算力方面也具备天然的优势。ChatGPT这把火烧到如今,国内对具备足够竞争力的国产生成式大模型的需求,正在与日俱增。


image.png


世界正在改变,一个全新的AI时代正在到来,种种不确定因素,再一次凸显了技术自研的价值。所幸这次,我们的起跑线,并没有相差那么远。


游戏不会在一夜间结束,而现在,竞逐真正开始。最后,大家有什么问题想问通义千问?请在大胆留言。


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

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

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

介绍


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


自定义字体


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


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


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



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

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


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

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

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


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


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


AutoLink


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


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


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


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

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


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


对齐模式


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


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


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


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

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


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


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


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

速度与安全可兼得!改造异步布局大幅提升客户端布局性能

1. 背景介绍 随着小红书用户规模的不断增长,App 性能对用户体验的影响显得越来越重要,例如页面的打开速度、App 的启动速度等,几十毫秒的提升都能带来业务数据上比较显著的收益。今天要介绍的是对一个官方框架的实践以及优化,期间踩了不少坑,...
继续阅读 »

1. 背景介绍


随着小红书用户规模的不断增长,App 性能对用户体验的影响显得越来越重要,例如页面的打开速度、App 的启动速度等,几十毫秒的提升都能带来业务数据上比较显著的收益。今天要介绍的是对一个官方框架的实践以及优化,期间踩了不少坑,但收益也很可观。


AsyncLayoutInflater 最早于 2015 年出现在 support.v4 包中,用来异步 inflate 布局。通常来讲 inflate 需要在主线程执行,所以是一个页面初始化过程中的耗时主要部分,这个工具提供了可以在异步 inflate 的能力,进而减少主线程堵塞。本文主要介绍工具的使用以及如何改进,以及改进中遇到的一些问题。


2. 使用


AsyncLayoutInflater 的使用非常简单,只需要加入一个依赖即可。



同时在代码中的使用如下:



在异步 inflate 好之后会有回调,这时候就可以使用 view 了。


3. 源码分析


这个工具最厉害的地方就在于异步 inflate view 居然没有出现线程安全相关的一些问题,下面我们就来看看它是怎么处理线程安全的问题的。



首先,里面有一个 Thread 的单例,单例里有一个线程安全的阻塞队列和一个线程安全的对象池。



这个单例里有个方法是 enqueue 方法,会调用阻塞队列的 put,将 request 插入队列中。因为是一个线程安全的队列+线程安全的对象池,所以这一系列操作就保证了线程安全。


下面是inflate的流程,inflate的时候会通过 mInflateThread.obtainRequest 从对象池里拿到一个 request,然后再将这个 request 插入队列中。



下面是一个简化过的代码,run 中有一个死循环,通过阻塞队列的 take 元素进行 inflate 的操作。



以上这个简单的工具就分析完了。这部分基本就回答了线程间如何同步数据的一个问题,在一个典型的生产者消费者模型中加入线程安全的容器即可保证。


4. 问题与改进


在使用中还是遇到很多线程相关的问题,以下列举几点相对重要的问题进行阐述。


4.1 单线程与多线程


InflateThread 在这里的设计是一个单例单线程,当需要对线程有一些定制或者收拢的话,改动就有些麻烦了,这里可以通过开放一个设置线程池的方法来提供一些线程管理和定制的能力,默认可以内置一个单线程的线程池。


通过比较长时间的实验我们发现,在主线程比较空闲的时候,单线程的效果会好一些,因为都在大核上执行了,效率更高。主线程繁忙的时候,例如冷启阶段,多线程会有更好的效率。


4.2 ArrayMap 与线程安全


我们在实际使用中发现,在一些自定义 View 的构造函数中和 darkmode 的实现中使用了 SimpleArrayMap 或 ArrayMap,ArrayMap 是 SimpleArrayMap 的子类,本身 SimpleArrayMap 是用过两个 static 的数组来实现对象的缓存,从而起到复用的作用,在多线程的情况下会有线程安全问题,这里会出现复用对象不匹配导致的 crash。一个简单的方式就是当出现 crash 的时候讲对应的 cache 数组清空,即可避免。



4.3 inflate、锁与线程安全


LayoutInflater 的 inflate 方法中有一个锁,这个导致了如果你想多线程去调用 inflate 的时候,起不到多线程的效果,如果是单线程的情况下,还可能遇到和主线程在 inflate 时同样等待锁的问题。这里 mConstructorArgs 是一个成员变量,通过重写 LayoutInflater 中的 cloneInContext 方法,配合对象池就可以避开这里锁的问题。



同时 inflate 过程中用到的这些数组和容器类型,都不是线程安全的,如果想要去掉 inflate 方法开头的 synchronize 的限制,这些线程不安全的容器类也是需要特别注意的。



4.4 BasicInflater 改造


AsyncLayoutInflater 本身有一个 BasicInflater,根据以上的一些改进点,我们在实践中对其做了一些改造,扩展出了可以设置线程池的接口,使用了基础架构提供的线程池,做到了对线程的统一管理。实践下来,在CPU比较繁忙的时候,多线程的线程池效果要好于单线程,当 CPU 比较空闲的时候,单线程的效果会更好一些,因为可以更好的利用释放出来的CPU 大核的性能。



同时重写了 ArrayMap 中线程不安全的一些处理方式,使得在多线程使用 ArrayMap 或者使用依赖 ArrayMap 的功能时不会出现 crash,这里涉及到了我们的一些自定义 View 和我们的 darkmode 的实现。


在对于 inflate 的锁和一些线程不安全的容器处理上,重写了LayoutInflater 的 cloneInContext 方法去掉了 synchronized 的限制,同时在 onCreateView 的流程中加入了线程安全的容器来保障 inflate 过程的线程安全。



综合来说就是重写了 AsyncLayoutInflater,ArrayMap 和 LayoutInflater,以达到线程安全的目的,同时将这些融入到我们的业务框架中,使得使用成本更低。


4.5  ViewCache


另一个实践是在业务侧做了进一步的封装,通过一个 ViewCache  的单例,提前将一些模块化的 View 提前 inflate 好,存在 ViewCache 中,在后续需要使用的时候从 ViewCache 中在获取,这样就避免了用的时候再 inflate 导致的耗时问题了。这块整体的代码比较简单,就不单独展开讲了,需要注意的点是有些 View 没有被使用需要及时释放,避免内存泄漏。


5. 总结


AsyncLayoutInflater 的实践与优化,前后持续了半年左右,我们在 App 冷启动和笔记详情页的性能优化中获得了超过的 20% 的性能收益以及显著的业务收益。同时,我们也将这个能力沉淀了到了业务框架中,方便了后续的接入和使用成本,通过 ViewCache 和业务框架,基本做到了可以覆盖大部分业务需求的能力。未来,我们将会在框架的易用性以及一些场景的使用上做进一步的优化,结合其他的优化手段给业务方提供更多的选择,使其能在写业务的同时无需关注这部分的耗时与复杂度,从而提升开发效率。


六、作者信息


殇不患


小红书商业技术 Android 工程师,曾负责业务架构设计与性能优化,目前专注于交易链路的迭代与优化。


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

AndroidStudio 常用插件来提高开发效率的必备

Statistic 代码统计工具 款代码统计工具,可以用来统计当前项目中代码的行数和大小。 通过顶部菜单中的View->Tool Windows->Statistic按钮开启该功能。 此时就可以看到我们项目代码的统计情况了 Translati...
继续阅读 »

Statistic 代码统计工具


款代码统计工具,可以用来统计当前项目中代码的行数和大小。


image.png


通过顶部菜单中的View->Tool Windows->Statistic按钮开启该功能。


image.png
此时就可以看到我们项目代码的统计情况了


image.png


Translation 一款翻译插件


image.png


直接选中需要翻译的内容,点击右键即可找到翻译按钮;


image.png


Markdown


IDEA官方出品的一款Markdown插件,支持编辑Markdown文件并进行预览


image.png


Key Promoter X 快捷键提示


Key Promoter X 是一款帮助你快速学习快捷键的插件,当你在AndroidStudio中用鼠标点击某些功能时,它会自动提示你使用该功能的快捷键


image.png
如下图鼠标点击某些功能时,会有对应的快捷键提示


androidstudio快捷键提醒.gif


Restful Fast Request


是IDEA版本的Postman,它是一个功能强大的Restful API工具包插件,在AndroidStudio中也可以根据已有的方法快速生成接口调试用例


image.png
然后使用方法如下:


howToUse_en.gif


PlantUML Integration


PlantUML是一款开源的UML图绘制工具,支持通过文本来生成图形,安装如下:


image.png
时序图(Sequence Diagram),是一种UML交互图。它通过描述对象之间发送消息的时间顺序显示多个对象之间的动态协作。


image.png
如这里生成的是一个简单的时序图


image.png


当然还有更多详细的用法,大家可以查看官网https://plantuml.com/zh/


image.png


Sequence Diagram 根据代码生时时序图


根据代码生成时序图的插件,还支持在时序图上直接导航到对应代码以及导出为图片或PlantUML文件。


image.png


String Manipulation 用来处理字符串


业处理字符串的插件,支持各种格式代码命名方式的切换、支持各种语言的转义和反转义、支持字符加密、支持多个字符的排序、对齐、过滤等等。


image.png


然后在使用的时候只需要选中字符串,点击右键


image.png


Rainbow Brackets 彩虹括号


image.png
安装好后,重新启动 AndroiStudio 打开项目


image.png


Android Wifi 连接手机进行调试




使用Android Studio Dolphin | 2021.3.1 Patch 1 及以上版本选择点击 Pair Devices Using Wi-Fi 弹出扫码框。提示使用android11+ 的设备扫码连接。
这时需要手机和电脑连同一个无线网。然后在手机开发者选项里面找到 无线调试,一般在USB调试开关下面。点击 无线调试 开启 无线调试 功能。点击无线调试页面的 使用二维码配对设备 扫描AS的二维码链接调试。等待一会链接好后就可以看到设备信息了。


image.png


CodeGlance Pro


代码视图页面生成浏览目录


image.png
安装成功后重启AndroidStudio


image.png


SonarLint 代码 review 插件


Sonar是一个用于代码质量管理的开源平台,用于管理源代码的质量 通过插件形式,可以支持包括java,C#,C/C++,PL/SQL,Cobol,JavaScrip,Groovy等等二十几种编程语言的代码质量管理与检测


image.png


安装成功后重启 AndroidStudio ,打开安卓项目如下:


image.png


目前支持的代码语言如下 官网在这里


image.png


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

2023年 android 裸辞跳槽经历

前言 上家公司我呆了整整三年,趁着合同到期,不想续签了,于是想出来试试新的机会,从毕业到现在每次换工作都是裸辞的,哪怕今年行情不怎么好也是裸辞的,自我认为,一来经济上也没有太大压力,二来裸辞之后能全心全意准备面试,成功率大大提升,但也不排除运气太差,一直找不到...
继续阅读 »

前言


上家公司我呆了整整三年,趁着合同到期,不想续签了,于是想出来试试新的机会,从毕业到现在每次换工作都是裸辞的,哪怕今年行情不怎么好也是裸辞的,自我认为,一来经济上也没有太大压力,二来裸辞之后能全心全意准备面试,成功率大大提升,但也不排除运气太差,一直找不到工作的情况,所以每个人需要根据自己的实际情况做选择。


我是 2 月 1 号先找我的上级聊离职的事情,然后又找公司人事聊合同不续签的事情,然后约定在 3 月 15 号离职。提完离职之后跟想象的不一样,以为提完离职就可以放松下来了,没想到一直到 3 月 14 号还在写需求🤣


复习准备


提完离职之后就开始准备简历和投简历了,因为离职这件事从去年就开始考虑了,所以在去年 11 月份就开始有意识的准备了,刷算法,刷八股文。大概用了两个月时间,leetcode 刷了 100 道题,当然一开始刷算法肯定很受挫,第一题就不会,这很正常,我一般是先看题目,不会就看题解,然后背,每隔一周就把上周刷的算法再复习一遍。


640.jpeg


关于八股文的准备就是复习 Java,kotlin,Android 基础,计算机网络,数据结构,还有设计模式等等部分,这些东西比较耗时同时知识点比较多又杂,所以只能慢慢复习,就跟大学考研一样,第一遍就是全面复习,然后把不会的知识点记录下来,然后每天上下班地铁上快速的过一遍,就是无意识的记录,看到一个知识点就在脑子里回想跟它有关联的其他知识点,这样一直持续到 2 月份,然后还有简历,简历很重要,因为面试官基本都是根据你简历上面写的提问,当然也不排除不按套路来的面试官,我就碰到过一次,一上来就一直问我计算机网络底层相关的东西,如 socket,websocket,xmpp 以及如何处理高并发的东西,然后其他东西一点都没问,这就让我很郁闷了,总之简历一定要写好,同时简历里面的提到的知识点一定要滚瓜烂熟。


找工作途径


关于在哪里投递简历的话,我是用了拉钩,BOSS,51job,猎聘网,然后拉钩充值了一个月会员 169 元(屁用都没有,感觉拉钩已经死掉了,职位不更新,投简历没反应),BOSS 先买了一个月会员,然后又续费了一个月,每个月 69 元,最后也是通过 BOSS 面试找到工作的。大伙有需要可以了解一下,这里不是给 BOSS 打广告哈。然后 51job 这个 App,用了一圈感觉都是外企或者国企事业单位发布的职位比较多,不过整体感觉用处不大,没起什么作用。猎聘网 App,广告打得到处都是,然而没卵用。这就是我找工作的途径。


offer情况


从 2 月份开始投递简历,大概投了几十份简历,总共收到的面试机会大概有 15 家,参加的面试有 10 家左右,还有其他的就是不合适啥的就拒绝掉了,最后统计了一下,只有成功了 3 家,收到了 3 家录用通知,成功率只有 30%,因为我的目标很明确,就是想去外企,国企事业单位,初创企业,互联网的公司不大想去,所以一些大厂我都没有投简历。可以想象今年的行情很差,我三年前也就是在 20 年的时候换工作,那时候都是工作追着你跑的,今年行情的确不行,你去找工作,别人都不愿意搭理你。


关于具体的面试经验总结可以看我公众号里面具体的文章,以上便是我今年换工作的整体情况。


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

给计算机专业学生的十大建议 后悔毕业才知道

经常有人问起计算机专业怎么学习,根据多年的实践经验,总结出这十条,欢迎大家收藏学习。 1. 注重计算机基础的学习 俗话说“勿在浮沙筑高台”,计算机基础是你作为一个科班生与非科班生的主要差别,也是一个计算机专业的核心竞争力,这玩意就像一个人的内功一样,学好了,...
继续阅读 »

经常有人问起计算机专业怎么学习,根据多年的实践经验,总结出这十条,欢迎大家收藏学习。



1. 注重计算机基础的学习


俗话说“勿在浮沙筑高台”,计算机基础是你作为一个科班生与非科班生的主要差别,也是一个计算机专业的核心竞争力,这玩意就像一个人的内功一样,学好了,受益无穷。像经典的《深入理解计算机系统》、《算法》、《数据结构与算法》等,学好基础再学习其他框架绝对是事半功倍。


2. 培养独立思考的能力。


遇到问题了,第一时间不是想着去问别人,而是先通过自己的努力,利用搜索引擎去查一查,慢慢提高自己的自我问题解决能力。这点上,我们可以好好利用Google搜索,这点不必多讲,在技术方面,国内百度还有其他搜索的结果简直惨不忍睹。另外有一个小的tips,在国内有很多google镜像网站,可以解决无法直接访问的问题。


3. 参加有价值的竞赛。


多参加一些有价值的计算机竞赛,比如:ACM(含金量最大)、GPLT、蓝桥杯、Kaggle、阿里天池、百度之星、中国大学生计算机设计大赛这里面含金量最大的就是ACM。一方面能提升自己的实践能力,另一方面做的好还可以得到丰厚的奖金和直通大厂的实习和工作机会,千万不要错过!特别是对于那些只上了普通学校的计算机专业的学生来说,这个,更是可以提高你将来就业竞争力的不二法宝。


4. 刷题刷题刷题。


重要的事情说三遍,主要就是刷算法题,需要你先学习《数据结构与算法》,之后你就可以多刷题,多刷题,多刷题,至于刷题网站可以推荐Leetcode和牛客网。之所以刷题这么重要,是因为后面找工作无论是私企还是外企,基本上每轮面试都会来几道算法题,而且像字节这种公司,可能算法题的难度是Hard级别,所以早点准备绝对没有错。


5. 去找个实习。


提前实习,好处多多,尤其不准备考研,毕业就准备工作的,主要的实习其实也就是在暑假时间了。根据我过往经验来看,有实习经历绝对是找工作面试时的一大优势,另外实习也可以提前让我们熟悉工作环境和社会的实际需求,帮助我们发现自己的短板,毕竟课堂和社会还是有很大差别的。所以有机会一定要尽早实习,并且越多实习越好!


6. 尝试系统化学习。


现在是一个知识爆炸的时代,各种自媒体满天飞,大家时间也越来越碎片化。如果对学习也没有一个规划,今天看到Python很吃香就学Python,明天看到AI前景很好,就开始搞AI,最后可能时间也花费了不少,但是啥也没学到。因此系统化学习非常重要,比如我要学数据库,那我就从基本的《SQL必知必会》,接着学《高性能MySQL》,最后学习《MySQL技术内幕》,只有这样才能系统掌握好一门技术!


7. 多做项目多实战。


多折腾项目,千万不要只做一个理论派,理论固然重要,但是离开了实战,理论的作用将打折扣,所以身为一个计算机专业的学生,除了要学好学透必备的理论知识,一定要花时间多去折腾一些项目!比如可以自己尝试从前端到后端搭建一个网站,从中你就可以积累项目开发经验,提升编程能力,为将来找工作打下坚实基础。


8. 学会使用GitHub。


这个真的是一个巨大的资源,这个也号称是程序员最大的同性交友网站,我觉得,每一个计算机专业的人都应该知道它,会基本的使用它,在这上面,你可以找到很多有趣实用的项目供你学习。上面还有很多开源顶级项目,像Go、Spark等,你也可以试着给他们提交代码,如果最后能够被接收,仅凭这点,你就可以在面试时秒杀90%的人了!


9. 坚持写博客分享技术。


很多程序员都在保持着写博客的习惯,这是对程序员非常有利的一件事情。把自己的日常所学,形成文字分享出来,不仅仅能帮助自己对技术的深入理解,还可以积累影响力,拓展自己的圈子等等,好处多多!


10. 锻炼身体。


最重要而最容易被忽视,千里之行始于足下,身体才是革命的本钱,如果没有身体,上面的都是空谈!!!平时有空就打打球、跑跑步,养成良好的生活习惯,学会给自己的身体投资!!!



关注公众号【码老思】,一时间获取最通俗易懂的原创技术干货。


作者:码老思
来源:juejin.cn/post/7216319218504155191

收起阅读 »

GeoJSON:地理信息的JSON表示法

web
简介 GeoJSON 是一种使用 JSON 来编码各种地理数据结构的格式,是一种轻量级的数据交换格式,可以用来表示几何对象、属性数据、空间参考系统等信息 由两种对象组成:Geometry(几何对象)和 Feature(空间行状) 几何对象用来描述地理空间中的...
继续阅读 »

简介


GeoJSON 是一种使用 JSON 来编码各种地理数据结构的格式,是一种轻量级的数据交换格式,可以用来表示几何对象、属性数据、空间参考系统等信息


由两种对象组成:Geometry(几何对象)和 Feature(空间行状)



  • 几何对象用来描述地理空间中的点、线、面等几何形状

  • 空间行状用来描述一个有界的实体,包括几何对象和其他属性信息


几何对象类型有:



  • 点:Point

  • 多点:MultiPoint

  • 线:LineString

  • 多线:MultiLineString

  • 面:Polygon

  • 多面:MultiPolygon

  • 几何集合:GeometryCollection


空间行状类型有:



  • 空间行状:Feature

  • 空间形状集合:FeatureCollection


举例


几何对象和空间行状可以相互嵌套


const GeoJSON = {
type: "FeatureCollection",
features: [
{
type: "Feature",
geometry: { type: "Point", coordinates: [121.4737, 31.2304] },
properties: { id: 1 },
},
{
type: "Feature",
geometry: { type: "Point", coordinates: [121.4837, 31.2504] },
properties: { id: 2 },
},
],
};

空间行状


FeatureCollection


FeatureCollectionFeature 对象的集合,用来表示一组 Feature 对象


typefeatures 两个属性组成:



  • type 属性的值为 FeatureCollection

  • features 属性的值为 Feature 对象的数组


const FeatureCollectionJSON = {
type: "FeatureCollection",
features: [feature],
};

Feature


Feature 对象用来表示几何对象的属性信息


typegeometryproperties 三个属性组成:



  • type 属性的值为 Feature

  • geometry 属性的值为 Geometry 几何对象

  • properties 属性的值为属性对象(可选)


const FeatureJSON = {
type: "Feature",
geometry: { type: "Point", coordinates: [121.4737, 31.2304] },
properties: { id: 1 },
};

几何对象


Point


Point 用来表示一个点


typecoordinates 两个属性组成:



  • type 属性的值为 Point

  • coordinates 属性的值为一个数组,数组的第一个元素为经度,第二个元素为纬度


const PointJSON = {
type: "Point",
coordinates: [121.4737, 31.2304],
};

MultiPoint


MultiPoint 用来表示多个点


typecoordinates 两个属性组成:



  • type 属性的值为 MultiPoint

  • coordinates 属性的值为一个数组,数组的每个元素都是一个点的坐标


const MultiPointJSON = {
type: "MultiPoint",
coordinates: [
[121.4737, 31.2304],
[121.4837, 31.2504],
],
};

LineString


LineString 用来表示一条线


typecoordinates 两个属性组成:



  • type 属性的值为 LineString

  • coordinates 属性的值为一个数组,数组的每个元素都是一个点的坐标


const LineStringJSON = {
type: "LineString",
coordinates: [
[121.4737, 31.2304],
[121.4837, 31.2504],
],
};

MultiLineString


MultiLineString 用来表示多条线


typecoordinates 两个属性组成:



  • type 属性的值为 MultiLineString

  • coordinates 属性的值为一个数组,数组的每个元素都是一个线的坐标数组


const MultiLineStringJSON = {
type: "MultiLineString",
coordinates: [
[
[121.4737, 31.2304],
[121.4837, 31.2504],
],
[
[121.4727, 31.2314],
[121.4827, 31.2514],
],
],
};

Polygon


Polygon 用来表示一个面


typecoordinates 两个属性组成:



  • type 属性的值为 Polygon

  • coordinates 属性的值为一个数组,数组的第一个元素为外环的坐标数组,后面的元素为内环的坐标数组


polygon 的坐标数组的第一个元素和最后一个元素是相同的,表示闭合


const PolygonJSON = {
type: "Polygon",
coordinates: [
[
[121.4737, 31.2304],
[121.4837, 31.2504],
[121.4937, 31.2304],
[121.4737, 31.2304],
],
[
[121.4717, 31.2314],
[121.4827, 31.2524],
[121.4937, 31.2334],
[121.4757, 31.2344],
],
],
};

MultiPolygon


MultiPolygon 用来表示多个面


typecoordinates 两个属性组成:



  • type 属性的值为 MultiPolygon

  • coordinates 属性的值为一个数组,数组的每个元素都是一个面的坐标数组


const MultiPolygonJSON = {
type: "MultiPolygon",
coordinates: [
[
[
[121.4737, 31.2304],
[121.4837, 31.2504],
[121.4937, 31.2304],
[121.4737, 31.2304],
],
[
[121.4737, 31.2304],
[121.4837, 31.2504],
[121.4937, 31.2304],
[121.4737, 31.2304],
],
],
[
[
[121.4737, 31.2304],
[121.4837, 31.2504],
[121.4937, 31.2304],
[121.4737, 31.2304],
],
[
[121.4737, 31.2304],
[121.4837, 31.2504],
[121.4937, 31.2304],
[121.4737, 31.2304],
],
],
],
};

GeometryCollection


GeometryCollection 用来表示几何对象的集合


typegeometries 两个属性组成:



  • type 属性的值为 GeometryCollection

  • geometries 属性的值为几何对象的数组


const GeometryCollectionJSON = {
type: "GeometryCollection",
geometries: [
{ type: "Point", coordinates: [121.4737, 31.2304] },
{
type: "LineString",
coordinates: [
[121.4737, 31.2304],
[121.4837, 31.2504],
],
},
],
};

可选属性


这些属性都是 GeoJSON 的扩展属性,不是 GeoJSON 规范的一部分



  • id 属性,用来描述 FeatureCollection 的唯一标识

  • bbox 属性,用来描述 FeatureCollection 的边界框

    • 四至坐标,一般用来做数据裁剪

    • 这是一组左上角和右下角的坐标,示例:[minLon, minLat, maxLon, maxLat]



  • properties 属性,用来描述 FeatureCollection 的属性

  • crs 属性,用来描述坐标参考系


其他


coordinate


coordinate 是一个数组,表示一个点的坐标,数组的长度表示坐标的维度,一般是 2 维或 3



  • 2 维:[lon, lat]

  • 3 维:[lon, lat, height]


coordinate 的第一个元素表示经度,第二个元素表示纬度,第三个元素表示高度


坐标顺序是 [lon, lat],这个是推荐顺序,可由 crs 属性指定


coordinates 是多维数组:



  • 点:[lon, lat]

  • 线:[[lon, lat], [lon, lat]]

  • 面:[[[lon, lat], [lon, lat]]]

  • 多面:[[[[lon, lat], [lon, lat]]]]


坐标参考系


最常使用的坐标系是 EPSG:4326EPSG:3857



  • EPSG:4326WGS84(CGCS2000,大地) 坐标系,是 GeoJSON 规范的默认坐标系

  • EPSG:3857Web Mercator(墨卡托) 坐标系,是 OpenLayers 的默认坐标系


它们的区别:



  • EPSG:4326 是经纬度坐标系,EPSG:3857 是投影坐标系

  • EPSG:4326 的坐标范围是 [-180, -90, 180, 90]EPSG:3857 的坐标范围是 [-20037508.342789244, -20037508.342789244, 20037508.342789244, 20037508.342789244]

  • EPSG:4326 的坐标单位是度,EPSG:3857 的坐标单位是米

  • EPSG:4326 的坐标原点是 [0, 0]EPSG:3857 的坐标原点是 [-20037508.342789244, -20037508.342789244]

  • EPSG:4326 的坐标轴方向是 [x, y]EPSG:3857 的坐标轴方向是 [x, -y]


在 ts 中使用


为了在 ts 使用 GeoJSON 能够有类型约束,我整理整理了一些 GeoJSONts 类型定义和创建 GeoJSON 的方法:



举例:




  1. 表示一个点和多个点的 GeoJSON 集合:


    使用geojson.d.ts


    type PointType = FeatureCollection<Point | MultiPoint, GeoJsonProperties<T>>;

    const point2Geojson: PointType<{ id: string; name?: string }> = {
    type: "FeatureCollection",
    features: [
    {
    type: "Feature",
    geometry: {
    type: "Point",
    coordinates: [120.4737, 31.2304],
    },
    properties: { id: "12", name: "uccs" },
    },
    {
    type: "Feature",
    geometry: {
    type: "MultiPoint",
    coordinates: [
    [121.4737, 31.2304],
    [111.4737, 31.2204],
    ],
    },
    properties: { id: "1" },
    },
    ],
    };



  2. 创建一个几何对象


    使用geojson.helper.ts


    const pointGeometry = point<{ id: string }>([120.4737, 31.2304], {
    id: "1",
    });
    const featureGeoJSON = feature<Point>(pointGeometry);



参考


收起阅读 »

Android Watchdog 狗子到底做了啥

前言 有一定开发经验的或多或少有听过Watchdog,那什么是Watchdog呢?Watchdog又称看门狗,看门狗是育碧开发的一款游戏,目前已出到《看门狗军团》。开个玩笑,Watchdog是什么,为什么会设计出它,听到它也许能快速联想到死锁,它是一个由Sys...
继续阅读 »

前言


有一定开发经验的或多或少有听过Watchdog,那什么是Watchdog呢?Watchdog又称看门狗,看门狗是育碧开发的一款游戏,目前已出到《看门狗军团》。开个玩笑,Watchdog是什么,为什么会设计出它,听到它也许能快速联想到死锁,它是一个由SystemServer启动的服务,本质上是一个线程,这次我们就从源码的角度分析,它到底做了啥。


准备


当然看源码前还需要做一些准备,不然你可能会直接看不懂。首先,Handler机制要了解。锁和死锁的概念都要了解,但我感觉应都是了解了死锁之后才听说Watchdog的。SystemServer至少得知道是做什么的。Monitor的设计思想懂更好,不懂在这里也不会影响看主流程。


这里源码有两个重要的类HandlerChecker和Monitor,简单了解它的流程大概就是用handler发消息给监控的线程,然后计时,如果30秒内有收到消息,什么都不管,如果超过30秒没收到但60秒内有收到,就打印,如果60秒内没收到消息,就炸。


主要流程源码解析


PS:源码是29的


首先在SystemServer中创建并启动这个线程,你也可以说启动这个服务


private void startBootstrapServices() {
......
final Watchdog watchdog = Watchdog.getInstance();
watchdog.start();
......
watchdog.init(mSystemContext, mActivityManagerService);
......
}

单例,我们看看构造方法


private Watchdog() {
super("watchdog");
mMonitorChecker = new HandlerChecker(FgThread.getHandler(),
"foreground thread", DEFAULT_TIMEOUT);
mHandlerCheckers.add(mMonitorChecker);
// Add checker for main thread. We only do a quick check since there
// can be UI running on the thread.
mHandlerCheckers.add(new HandlerChecker(new Handler(Looper.getMainLooper()),
"main thread", DEFAULT_TIMEOUT));
// Add checker for shared UI thread.
mHandlerCheckers.add(new HandlerChecker(UiThread.getHandler(),
"ui thread", DEFAULT_TIMEOUT));
// And also check IO thread.
mHandlerCheckers.add(new HandlerChecker(IoThread.getHandler(),
"i/o thread", DEFAULT_TIMEOUT));
// And the display thread.
mHandlerCheckers.add(new HandlerChecker(DisplayThread.getHandler(),
"display thread", DEFAULT_TIMEOUT));
// And the animation thread.
mHandlerCheckers.add(new HandlerChecker(AnimationThread.getHandler(),
"animation thread", DEFAULT_TIMEOUT));
// And the surface animation thread.
mHandlerCheckers.add(new HandlerChecker(SurfaceAnimationThread.getHandler(),
"surface animation thread", DEFAULT_TIMEOUT));

// 看主流程的话,Binder threads可以先不用管
// Initialize monitor for Binder threads.
addMonitor(new BinderThreadMonitor());

mOpenFdMonitor = OpenFdMonitor.create();

// See the notes on DEFAULT_TIMEOUT.
assert DB ||
DEFAULT_TIMEOUT > ZygoteConnectionConstants.WRAPPED_PID_TIMEOUT_MILLIS;
}

看主流程的话,Binder threads可以先不用管,精讲。可以明显的看到这里就是把一些重要的线程的handler去创建HandlerChecker对象放到数组mHandlerCheckers中。简单理解成创建一个对象去集合这些线程的信息,并且Watchdog有个线程信息对象数组。


public final class HandlerChecker implements Runnable {
private final Handler mHandler;
private final String mName;
private final long mWaitMax;
private final ArrayList<Monitor> mMonitors = new ArrayList<Monitor>();
private final ArrayList<Monitor> mMonitorQueue = new ArrayList<Monitor>();
private boolean mCompleted;
private Monitor mCurrentMonitor;
private long mStartTime;
private int mPauseCount;

HandlerChecker(Handler handler, String name, long waitMaxMillis) {
mHandler = handler;
mName = name;
mWaitMax = waitMaxMillis;
mCompleted = true;
}

......
}

然后我们先看init方法


public void init(Context context, ActivityManagerService activity) {
mActivity = activity;
context.registerReceiver(new RebootRequestReceiver(),
new IntentFilter(Intent.ACTION_REBOOT),
android.Manifest.permission.REBOOT, null);
}

final class RebootRequestReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context c, Intent intent) {
if (intent.getIntExtra("nowait", 0) != 0) {
rebootSystem("Received ACTION_REBOOT broadcast");
return;
}
Slog.w(TAG, "Unsupported ACTION_REBOOT broadcast: " + intent);
}
}

void rebootSystem(String reason) {
Slog.i(TAG, "Rebooting system because: " + reason);
IPowerManager pms = (IPowerManager)ServiceManager.getService(Context.POWER_SERVICE);
try {
pms.reboot(false, reason, false);
} catch (RemoteException ex) {
}
}

明显能看出是重启的操作,注册广播,接收到这个广播之后重启。这个不是主流程,简单看看就行。


来了,重点来了,开始讲主流程。Watchdog是继承Thread,所以上面调start方法会执行到这里的run方法,润起来


@Override
public void run() {
boolean waitedHalf = false;
while (true) {
......
synchronized (this) {
long timeout = CHECK_INTERVAL;
for (int i=0; i<mHandlerCheckers.size(); i++) {
HandlerChecker hc = mHandlerCheckers.get(i);
hc.scheduleCheckLocked();
}

......
long start = SystemClock.uptimeMillis();
while (timeout > 0) {
if (Debug.isDebuggerConnected()) {
debuggerWasConnected = 2;
}
try {
wait(timeout);
} catch (InterruptedException e) {
Log.wtf(TAG, e);
}
......
timeout = CHECK_INTERVAL - (SystemClock.uptimeMillis() - start);
}

......

if (!fdLimitTriggered) {
// 直接先理解成正常情况下会进这里
final int waitState = evaluateCheckerCompletionLocked();
if (waitState == COMPLETED) {
// The monitors have returned; reset
waitedHalf = false;
continue;
} else if (waitState == WAITING) {
// still waiting but within their configured intervals; back off and recheck
continue;
} else if (waitState == WAITED_HALF) {
if (!waitedHalf) {
Slog.i(TAG, "WAITED_HALF");
// We've waited half the deadlock-detection interval. Pull a stack
// trace and wait another half.
ArrayList<Integer> pids = new ArrayList<Integer>();
pids.add(Process.myPid());
ActivityManagerService.dumpStackTraces(pids, null, null,
getInterestingNativePids());
waitedHalf = true;
}
continue;
}

// something is overdue!
blockedCheckers = getBlockedCheckersLocked();
subject = describeCheckersLocked(blockedCheckers);
} else {
......
}
......
}

// 扒日志然后退出
......

waitedHalf = false;
}
}

把一些代码屏蔽了,这样看会比较舒服,主要是怕代码太多劝退人。


首先死循环,然后遍历mHandlerCheckers,就是我们在构造方法那创建的HandlerCheckers数组,遍历数组调用HandlerChecker的scheduleCheckLocked方法


public void scheduleCheckLocked() {
if (mCompleted) {
// Safe to update monitors in queue, Handler is not in the middle of work
mMonitors.addAll(mMonitorQueue);
mMonitorQueue.clear();
}
if ((mMonitors.size() == 0 && mHandler.getLooper().getQueue().isPolling())
|| (mPauseCount > 0)) {
mCompleted = true;
return;
}
if (!mCompleted) {
// we already have a check in flight, so no need
return;
}

mCompleted = false;
mCurrentMonitor = null;
mStartTime = SystemClock.uptimeMillis();
mHandler.postAtFrontOfQueue(this);
}

HandlerChecker内有个Monitor数组,Monitor是一个接口,然后外部一些类实现这个接口实现monitor方法,这个后面会说。


public interface Monitor {
void monitor();
}

这个mCompleted默认是true


if (mCompleted) {
// Safe to update monitors in queue, Handler is not in the middle of work
mMonitors.addAll(mMonitorQueue);
mMonitorQueue.clear();
}

把mMonitorQueue数组中的元素移动到mMonitors中。这个什么意思呢?有点难解释,这样,你想想,Watchdog的run方法中是一个死循环不断调用scheduleCheckLocked方法吧,我这段代码的逻辑操作用到mMonitors,那我不能在我操作的同时你添加元素进来吧,那不就乱套了,所以如果有新加Monitor的话,就只能在每次循环执行这段逻辑开始的时候,添加进了。这段代码是这个意思。


if ((mMonitors.size() == 0 && mHandler.getLooper().getQueue().isPolling())
|| (mPauseCount > 0)) {
mCompleted = true;
return;
}

如果mMonitors数组不为空,并且这个handler的messageQueue正在工作,你理解这个isPolling方法是正在工作就行,把mCompleted状态设true,然后直接结束这个方法,这什么意思呢?你想想,我的目的是要判断这个线程是否卡住了,那我messageQueue正在工作说明没卡住嘛。看不懂这里的话可以再理解理解handler机制。


假如没有,我们往下走


// 先不管,先标记这里是A1点
if (!mCompleted) {
// we already have a check in flight, so no need
return;
}

这段不用管它,从上面可以看出这里mCompleted是true,往下走,我们先标记这里是A1点,后面流程会执行回来。


mCompleted = false;
mCurrentMonitor = null;
mStartTime = SystemClock.uptimeMillis();
mHandler.postAtFrontOfQueue(this);

把mCompleted状态设为false,mStartTime用来记录当前时间作为我们整个判断的起始时间,用handler发消息postAtFrontOfQueue。然后这里传this,就会调用到这个HandlerChecker自身的run方法。


好了,考验功底的地方,这个run方法是执行在哪个线程中?


@Override
public void run() {
final int size = mMonitors.size();
for (int i = 0 ; i < size ; i++) {
synchronized (Watchdog.this) {
mCurrentMonitor = mMonitors.get(i);
}
mCurrentMonitor.monitor();
}

synchronized (Watchdog.this) {
mCompleted = true;
mCurrentMonitor = null;
}
}

这里是拿mMonitors数组循环遍历然后执行monitor方法,其实这个就是判断死锁的逻辑,你先简单理解成如果发生死锁,这个mCurrentMonitor.monitor就会卡住在这里,不会往下执行mCompleted = true;


handler发消息的同时run方法其实已经是切线程了 ,所以Watchdog线程会继续往下执行,我们回到Watchdog的run方法


long start = SystemClock.uptimeMillis();
while (timeout > 0) {
if (Debug.isDebuggerConnected()) {
debuggerWasConnected = 2;
}
try {
wait(timeout);
// Note: mHandlerCheckers and mMonitorChecker may have changed after waiting
} catch (InterruptedException e) {
Log.wtf(TAG, e);
}
if (Debug.isDebuggerConnected()) {
debuggerWasConnected = 2;
}
timeout = CHECK_INTERVAL - (SystemClock.uptimeMillis() - start);
}

wait(timeout);进行线程阻塞,线线程生命周期变成TIME_WAITTING,timeout在这里是CHECK_INTERVAL,就是30秒。


30秒之后进入这个流程


final int waitState = evaluateCheckerCompletionLocked();
if (waitState == COMPLETED) {
// The monitors have returned; reset
waitedHalf = false;
continue;
} else if (waitState == WAITING) {
// still waiting but within their configured intervals; back off and recheck
continue;
} else if (waitState == WAITED_HALF) {
if (!waitedHalf) {
Slog.i(TAG, "WAITED_HALF");
// We've waited half the deadlock-detection interval. Pull a stack
// trace and wait another half.
ArrayList<Integer> pids = new ArrayList<Integer>();
pids.add(Process.myPid());
ActivityManagerService.dumpStackTraces(pids, null, null,
getInterestingNativePids());
waitedHalf = true;
}
continue;
}

private int evaluateCheckerCompletionLocked() {
int state = COMPLETED;
for (int i=0; i<mHandlerCheckers.size(); i++) {
HandlerChecker hc = mHandlerCheckers.get(i);
state = Math.max(state, hc.getCompletionStateLocked());
}
return state;
}

evaluateCheckerCompletionLocked就是轮询调用HandlerChecker的getCompletionStateLocked方法,然后根据全部的状态,返回一个最终的状态, 我后面会解释状态。 ,先看getCompletionStateLocked方法 (可以想想这个方法是在哪个线程中执行的)


public int getCompletionStateLocked() {
if (mCompleted) {
return COMPLETED;
} else {
long latency = SystemClock.uptimeMillis() - mStartTime;
if (latency < mWaitMax/2) {
return WAITING;
} else if (latency < mWaitMax) {
return WAITED_HALF;
}
}
return OVERDUE;
}

其实HandlerChecker的getCompletionStateLocked方法对应scheduleCheckLocked方法。


判断mCompleted为true的话返回COMPLETED状态。COMPLETED状态就是正常,从上面看出正常情况下都会返回true,只有在那条线程还卡住的情况下,返回false。什么叫“那条线程还卡住的情况”,我们在scheduleCheckLocked方法postAtFrontOfQueue之后有两种情况会出现卡住。


(1)这个Handler的MessageQueue的前一个Message一直在处理中,导致postAtFrontOfQueue在这30秒之后都没执行到run方法

(2)run方法中的mCurrentMonitor.monitor()一直卡住,30秒了还是卡住,准确来说是竞争锁处于BLOCKED状态,没能执行到mCompleted = true


这两种情况下mCompleted都为false,然后latency来计算这段时间,如果小于30秒,返回WAITING状态,如果大于30秒小于60秒,返回WAITED_HALF状态,如果大于60秒返回OVERDUE状态。


然后看回evaluateCheckerCompletionLocked方法state = Math.max(state, hc.getCompletionStateLocked());这句代码的意思就是因为我们是检测多条线程的嘛,这么多条线程里面,但凡有一条不正常,最终这个方法都返回最不正常的那个状态。


假如返回COMPLETED状态,说明这轮循环正常,开始下一轮循环判断,假如返回WAITING, 下一轮执行到HandlerChecker的scheduleCheckLocked方法的时候,就会走点A1的判断


if (!mCompleted) {
// we already have a check in flight, so no need
return;
}

这种情况下就不用重复发消息和记录开始时间。当返回WAITED_HALF的情况下调用dumpStackTraces收集信息,当返回OVERDUE的情况下就直接收集信息然后重启了。下面是收集信息重启的源码,不想看可以跳过。



......

// If we got here, that means that the system is most likely hung.
// First collect stack traces from all threads of the system process.
// Then kill this process so that the system will restart.
EventLog.writeEvent(EventLogTags.WATCHDOG, subject);

ArrayList<Integer> pids = new ArrayList<>();
pids.add(Process.myPid());
if (mPhonePid > 0) pids.add(mPhonePid);

final File stack = ActivityManagerService.dumpStackTraces(
pids, null, null, getInterestingNativePids());

// Give some extra time to make sure the stack traces get written.
// The system's been hanging for a minute, another second or two won't hurt much.
SystemClock.sleep(5000);

// Trigger the kernel to dump all blocked threads, and backtraces on all CPUs to the kernel log
doSysRq('w');
doSysRq('l');

// Try to add the error to the dropbox, but assuming that the ActivityManager
// itself may be deadlocked. (which has happened, causing this statement to
// deadlock and the watchdog as a whole to be ineffective)
Thread dropboxThread = new Thread("watchdogWriteToDropbox") {
public void run() {
// If a watched thread hangs before init() is called, we don't have a
// valid mActivity. So we can't log the error to dropbox.
if (mActivity != null) {
mActivity.addErrorToDropBox(
"watchdog", null, "system_server", null, null, null,
subject, null, stack, null);
}
StatsLog.write(StatsLog.SYSTEM_SERVER_WATCHDOG_OCCURRED, subject);
}
};
dropboxThread.start();
try {
dropboxThread.join(2000); // wait up to 2 seconds for it to return.
} catch (InterruptedException ignored) {}

IActivityController controller;
synchronized (this) {
controller = mController;
}
if (controller != null) {
Slog.i(TAG, "Reporting stuck state to activity controller");
try {
Binder.setDumpDisabled("Service dumps disabled due to hung system process.");
// 1 = keep waiting, -1 = kill system
int res = controller.systemNotResponding(subject);
if (res >= 0) {
Slog.i(TAG, "Activity controller requested to coninue to wait");
waitedHalf = false;
continue;
}
} catch (RemoteException e) {
}
}

// Only kill the process if the debugger is not attached.
if (Debug.isDebuggerConnected()) {
debuggerWasConnected = 2;
}
if (debuggerWasConnected >= 2) {
Slog.w(TAG, "Debugger connected: Watchdog is *not* killing the system process");
} else if (debuggerWasConnected > 0) {
Slog.w(TAG, "Debugger was connected: Watchdog is *not* killing the system process");
} else if (!allowRestart) {
Slog.w(TAG, "Restart not allowed: Watchdog is *not* killing the system process");
} else {
Slog.w(TAG, "*** WATCHDOG KILLING SYSTEM PROCESS: " + subject);
WatchdogDiagnostics.diagnoseCheckers(blockedCheckers);
Slog.w(TAG, "*** GOODBYE!");
Process.killProcess(Process.myPid());
System.exit(10);
}

waitedHalf = false;

补充


补充一下4个状态的定义


static final int COMPLETED = 0;
static final int WAITING = 1;
static final int WAITED_HALF = 2;
static final int OVERDUE = 3;

COMPLETED是正常情况,其它都是异常情况,OVERDUE直接重启。


然后关于Monitor,可以随便拿个类来举例子,我看很多人都是用AMS,那我也用AMS吧


public class ActivityManagerService extends IActivityManager.Stub
implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback {

看到AMS实现Watchdog.Monitor,然后在AMS的构造方法中


Watchdog.getInstance().addMonitor(this);
Watchdog.getInstance().addThread(mHandler);

public void addMonitor(Monitor monitor) {
synchronized (this) {
mMonitorChecker.addMonitorLocked(monitor);
}
}

public void addThread(Handler thread, long timeoutMillis) {
synchronized (this) {
final String name = thread.getLooper().getThread().getName();
mHandlerCheckers.add(new HandlerChecker(thread, name, timeoutMillis));
}
}

先看addThread方法,能看出,Watchdog除了自己构造函数中添加的那些线程之外,还能提供方法给外部进行添加。然后addMonitor就是把Monitor添加到mMonitorQueue里面


void addMonitorLocked(Monitor monitor) {
// We don't want to update mMonitors when the Handler is in the middle of checking
// all monitors. We will update mMonitors on the next schedule if it is safe
mMonitorQueue.add(monitor);
}

之后在scheduleCheckLocked方法再把mMonitorQueue内容移动到mMonitors中,这个上面有讲了。然后来看AMS实现monitor方法。


public void monitor() {
synchronized (this) { }
}

表面看什么都没做,实则这里有个加锁,如果这时候其它线程占有锁了,你这里调monitor就会BLOCKED,最终时间长就导致Watchdog那超时,这个上面也有讲了。


分析


首先看了源码之后我觉得总体来说不够其它功能设计的源码亮眼,比如我上篇写的线程池,感觉设计上比它就差点意思。当然也有好的地方,比如mMonitorQueue和mMonitors的设计这里。


然后从设计的角度去反推,为什么要定30秒,这个我是分析不出的,这里定30秒是有什么含义,随便差不多定一个时机,还是根据什么原理去设定的时间。


然后我觉得有个地方挺迷的,如果有懂的大佬可以解答一下。


就是getCompletionStateLocked,什么情况下会返回WAITING状态。 记录mStartTime -> sleep 30秒 -> getCompletionStateLocked,正常来看,getCompletionStateLocked中获取时间减去mStartTime肯定是会大于30秒,所以要么getCompletionStateLocked直接返回COMPLETED,要么就是WAITED_HALF或者OVERDUE,什么情况下会WAITING。


然后看源码的时候,有个地方挺有意思的,这个也可以分享一下,就是run方法中,收集信息重启那个流程,有一句注释


// Give some extra time to make sure the stack traces get written.
// The system's been hanging for a minute, another second or two won't hurt much.
SystemClock.sleep(5000);

我是没想到官方人员也这么调皮。


最后回顾一下标题,狗子到底做了什么?


现在其实去网上找,有很多人说Watchdog是为了检测死锁,然后相当于把Watchdog和死锁绑一起了。包括在SystemServer调用的时候官方也有一句注释。


// Start the watchdog as early as possible so we can crash the system server
// if we deadlock during early boot
traceBeginAndSlog("StartWatchdog");
final Watchdog watchdog = Watchdog.getInstance();
watchdog.start();
traceEnd();

if we deadlock during early boot,让人觉得就是专门处理死锁的。当然如果出现死锁的话mCurrentMonitor.monitor()会阻塞住所以能检测出来。但是我上面也说了,从源码的角度看,有两种情况会导致卡住。


(1)这个Handler的MessageQueue的前一个Message一直在处理中,导致postAtFrontOfQueue在这30秒之后都没执行到run方法

(2)run方法中的mCurrentMonitor.monitor()一直卡住,30秒了还是卡住,准确来说是竞争锁处于BLOCKED状态,没能执行到mCompleted = true


第一种情况,我如果上一个message是耗时操作,那这个run就不会执行,这种情况下可没走到死锁的判断。当然,这里都是监听的特殊的线程,主线程之类的做耗时操作也不切实际。第二种,mCurrentMonitor.monitor()一直卡住就一定是死锁了吗?我一直持有锁不释放也会导致这个结果。


所以我个人觉得这里Watchdog的作用不仅仅是为了监测死锁,而是监测一些线程,防止它们长时间被持有导致无法响应或者因为耗时操作导致无法及时响应。再看看看门狗的定义,看门狗的功能是定期的查看芯片内部的情况,一旦发生错误就向芯片发出重启信号 ,我觉得,如果单单只是为了监测死锁,那完全可以叫DeadlockWatchdog。


总结


Watchdog的主要流程是:开启一个死循环,不断给指定线程发送一条消息,然后休眠30秒,休眠结束后判断是否收到消息的回调,如果有,则正常进行下次循环,如果没收到,判断从发消息到现在的时机小于30秒不处理,大于30秒小于60秒收集信息,大于60秒收集信息并重启。


当然还有一些细节,比如判断时间是用SystemClock.uptimeMillis(),这些细节我这里就不单独讲了。


从整体来看,这个设计的思路还是挺好的,发消息后延迟然后判断有没有收到消息 ,其实这就是和判断ANR一样,埋炸弹拆炸弹的过程,是这样的一个思路。


个人比较有疑问的就是这个30秒的设计,是有什么讲究。还有上面说的,什么情况下会出现

作者:流浪汉kylin
来源:juejin.cn/post/7215498393429983291
小于30秒的场景。


收起阅读 »

css是你永远学不会的语言

web
在网上冲浪的时候,看到有这么一个网页效果;如下图: 分析: 从图中我们可以看出,当我们鼠标移入的时候,下划线冲左侧开始展示;当鼠标移出的时候,下划线从左侧开始消失 既然我们知道了大致的效果,那我们就得想想怎么实现这个效果了。 我是没有想出来的,我用F12查看...
继续阅读 »

在网上冲浪的时候,看到有这么一个网页效果;如下图:


20230330_10:43:33_1.gif


分析:


从图中我们可以看出,当我们鼠标移入的时候,下划线冲左侧开始展示;当鼠标移出的时候,下划线从左侧开始消失


既然我们知道了大致的效果,那我们就得想想怎么实现这个效果了。


我是没有想出来的,我用F12查看了一下;如下图代码:


未移入样式:


微信图片_20230330105126.png


移入样式(hover):


微信图片_20230330105126.png


代码分析



  • 背景色渐变以及方向(移入移出)

  • 背景大小

  • 过度时间


示列代码


解释:



  • 我们在css中写span的样式时;background需要禁止平铺,然后是靠右(right)并且是底部的;原因是以为收回去的时候需要方向是右侧(right);然后background-size需要将宽度设置为0 高度为2(你可以根据自己的需要设置);最后是给background-size一个过度效果既可以

  • 然后hover事件的时候需要将定位给到左侧(left)并且将background-size宽度百分之百;这样就会根据过度时间显示完成


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.title {
color: #333;
line-height: 2px;
}

.title span {
background: linear-gradient(to right, #ec695c,#61c454) no-repeat right bottom;
background-size: 0 2px;
transition: background-size 1300ms;
}
.title span:hover {
background-position-x: left;
background-size: 100% 2px;
}
</style>
</head>
<body>
<h2 class="title">
<span>
Migrating From MySQL to YugabyteDB Using YugabyteDB Voyager</span>
</h2>
</body>
</html>

以上就是今天的全部内容了!大家可以复制以上代码,即可展现效果。


当我做了以后发现,css真的是我的弱势;或者说大部分人都不怎么关注css;毕竟面试的时候大部分公司都不是很要求这个,从而我们就忽略了个语言;css真的我永远学不会的语言啊


后面我也就开一个专辑,我所遇到的css相关的一些东西


往期文章



作者:雾恋
来源:juejin.cn/post/7216163778059550757
收起阅读 »

纯前端就可以实现的两个需求

web
一:多文件下载并压缩成zip形式   我们一般看到这个需求,大多前端都会觉得这不就是一个接口的事嘛,咱们直接window.open()就搞定了,emm...,我之前也是这样想的,可惜现在是在三线城市的小互联网公司,人家后端说服务器不行,带宽不够,出不来接口0....
继续阅读 »

一:多文件下载并压缩成zip形式


  我们一般看到这个需求,大多前端都会觉得这不就是一个接口的事嘛,咱们直接window.open()就搞定了,emm...,我之前也是这样想的,可惜现在是在三线城市的小互联网公司,人家后端说服务器不行,带宽不够,出不来接口0.0,所以我就尝试着寻找……最终找到了解决办法——


  前端可以直接从cos资源服务器中下载多个文件,并放进压缩包中哦,这样就省去了后端在中间中转的那个环节,实现方式如下:


1.安装依赖


  我们需要用到两个第三方依赖哦,分别执行以下安装


npm i jszip
npm i file-saver

2.引入 


  在需要使用的时候,我们引入


import JSZip from "jszip";
import FileSaver from "file-saver";

3.实现过程


  我这里是在vue框架中,所以方法是在methods中哦,先在data里声明一个文件demo数据


data () {
return {
fileList: [ //这里的数据 实际中 应该是从后端接口中get { name: "test1.doc", url: "https://collaboration-pat-1253575580.cos.ap-shenzhen-fsi.myqcloud.com/baseFile/202303/bdf4303f-4e8d-4d95-977c-4fd5ef28d354/new.doc" }, { name: "test2.doc", url:"https://collaboration-pat-1253575580.cos.ap-shenzhen-fsi.myqcloud.com/baseFile/202303/bdf4303f-4e8d-4d95-977c-4fd5ef28d354/new.doc" } ],
}
}

methods中的方法:


handleClickDownAll () { //实现多文件压缩包下载
let _this = this
let zip = new JSZip()
let cache = {}
let promises = []
for(let item of this.fileList) {
const promise = _this.getFileArrBuffer(item.url).then(res => {
//下载文件, 并存成ArrayBuffer对象(blob)
zip.file(item.name,res,{binary:true}) //逐个添加文件
cache[item.name] = data
})
promises.push(promise) }
Promese.all(promises).then(() => {
zip.generateAsync({type:'blob'}).then(content => {
FileSaver.saveAs(content,"压缩包名字") //利用file-saver保存文件 自定义文件名
})
})
},
getFileArrBuffer(url) {
return new Promise((resolve) => {
let xmlhttp = new XMLHttpRequest()
xmlhttp.open('GET',url,true)
xmlhttp.responseType = 'blob'
xml.onload = function () {
resolve(this.response)
}
xmlhttp.send()
})
}

二:electron-vue中,生成二维码,并支持复制二维码图片


要实现的功能如下,就是点击这个“复制二维码”,可以直接把二维码图片复制下来



1.安装依赖


npm i qrcodejs2

2.引入


import QRCode from 'qrcodejs2';
import { clipboard, nativeImage} = require('electron')

3.实现


  要先在template中写一个这样的元素,用来显示二维码图片框


<div id="qrcodeImg" class="qrcode" style="height: 120px"></div>

然后再写一个画二维码的方法,如下:


drawQrcode() {
new QRCode("qrcodeImg",{
width:120,
height:120,
text:"http://www.baidu.com",
colorDark:"#000",
colorLight:"#fff"
})
}

复制二维码的方法如下:


copyCode() {
let src = document.getElementById("qrcodeImg").getElementsByTagName("img")[0].src
const image = nativeImage.createFromDataURL(src)
clipboard.writeImage(image)
this.$Message.success('复制成功')
}

4.使用


要先确保dom元素已经有了,所以在mounted中调用drawQrcode()这个方法,然后点击“复制二维码”时,调用 copyCode()这个方法就可以实现啦




作者:wenLi
来源:juejin.cn/post/7213983712732348474
收起阅读 »

再也不用手动改package.json的版本号

web
本文的起因是有在代码仓库发包后,同事问我“为什么package.json 里的版本还是原来的,有没有更新?”,这个时候我意识到,我们完全没有必要在每次发布的时候还特意去关注这个仓库的版本号,只要在发布打tag的时候同步一下即可,于是有了本文的实践。 node....
继续阅读 »

本文的起因是有在代码仓库发包后,同事问我“为什么package.json 里的版本还是原来的,有没有更新?”,这个时候我意识到,我们完全没有必要在每次发布的时候还特意去关注这个仓库的版本号,只要在发布打tag的时候同步一下即可,于是有了本文的实践。


node.js 部分,我们得有一个更改仓库代码的脚本留给ci执行


我们首先需要在工程目录中的 ./script/..目录下增加一个 update-version.js脚本



//update-version.js

const path = require('path');
const fs = require('fs');
const newVersion = process.argv[2].replace(/^v/, '');; // 获取命令行参数中的新版本号,并过滤v字头

if (!newVersion) {
console.log('请传入新版本号,版本号遵循semver规范 .eg: 1.0.0, 1.0.1, 1.1.0');
process.exit(1);

}

// 获取当前命令行上下文路径

const currentDirectory = process.cwd();

// 获取 package.json 文件中的版本号
const packageJsonPath = path.join(currentDirectory, 'package.json');
const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf8');
const packageJson = JSON.parse(packageJsonContent);
const currentVersion = packageJson.version;

// 更新 package.json 文件中的版本号

packageJson.version = newVersion;
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
console.log(`版本号已从 ${currentVersion} 更新为 ${newVersion}`);


接下来在 package.json script 配置后可以直接使用 npm run version <version> 中触发变更版本号脚本。当然这个前提是想要让这个脚本保留给开发者命令行使用。



{

"name": "version workflow",
"version": "1.0.0",
"description": "version update demo",
"main": "index.js",
"scripts": {
//...
"version": "node ./scripts/update-version.js"
},
//...

}


CI :如何让发布包的行为直接和代码仓库中的版本号同步?


接下来算重头戏,如何让发布包的行为直接和代码仓库中的版本号同步?这里我们使用的是github 提供的github action,具体操作和语法可以查看一下官方文档,本文就不过多展开。


我们需要在仓库根目录增加如下路径的文件 .github/workflows/update-action.yml



name: Update Package Version

on:
release:
types: [released]

jobs:
update:
runs-on: ubuntu-latest
steps:

- name: Checkout code
uses: actions/checkout@v3

- name: Update package.json
run: |
node ./scripts/update-version.js ${{ github.event.release.tag_name }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Commit changes
run: |
git config user.name "Your github name"
git config user.email "your github email"
git add .
git commit -m "Update version to ${{ github.event.release.tag_name }} for release ${{ github.ref }}"

- name: Push changes
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}


我们在 release hook 中的 released 状态下增加了一个 update job。 它会做下面几件事情(在脚本步骤中有)



  1. 【Checkout code】 切出新的代码分支;

  2. 【 Update package.json】在新分支执行 update-version.js 传入tag_name更新我们的工程版本号;

  3. 【Commit changes】以你定制的 git config user 信息创建一个新提交;

  4. 【Push changes】推送变更回到主干;


ps:正确来说应该在发布执行动作前prereleased执行我们的 job 但是没用这个的原因如下:



Note:  The prereleased type will not trigger for pre-releases published from draft releases, but the published type will trigger. If you want a workflow to run when stable and pre-releases publish, subscribe to published instead of released and prereleased.



当这个脚本推送后,执行发布后自动更新版本,不用在关注这个版本修改问题。
你会得到下面的效果。


在你的仓库发布界面填写正确tag后发布
image.png


触发update job 更改完成
image.png


你可能遇到最多的坑



  1. action 执行失败



Process completed with exit code 129." Node.js 12 actions are deprecated. Please update the following actions to use Node.js 16: actions/checkout@v2. For more information, see https://github.blog/changelog/2022-09-22-github-actions-all-actions-will-begin-running-on-node16-instead-of-node12/.



这是由于默认action job 执行环境的nodejs 版本与actions 包中执行脚本不匹配导致,所以一定要使用 checkout@v3 版本 actions/checkout@v3



  1. 各种不熟悉 action 语法取值导致的问题


可以优化的地方


我们前面提交的这个流程发布还是有个问题,你永远有个更超前的 commit hash 在你发布的 tag 之后


image.png
所以这个action 还有需要继续优化的地方,那就是同步更新tag hash


name: Update Package Version

on:
release:
types: [released]

jobs:
update:
runs-on: ubuntu-latest
steps:

- name: Checkout code
uses: actions/checkout@v3

- name: Update package.json
run: |
node ./scripts/update-version.js ${{ github.event.release.tag_name }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Commit changes
run: |
git config user.name "Your github name"
git config user.email "your github email"
git add .
git commit -m "Update version to ${{ github.event.release.tag_name }} for release ${{ github.ref }}"
git_hash=$(git rev-parse --short HEAD)

- name: Push changes
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}

- name: Tag Push changes
run: |
git tag -f ${{ github.event.release.tag_name }} $git_hash
git push --force origin ${{ github.event.release.tag_name }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


这里相比之前的版本增加了
Tag Push changes 这个步骤,在最后获取这个版本更新产生的 $git_hash强制更新到发布的 tag 上。


我们看看效果
image.png


最后我们看版本发布管理中的 tag hash
image.png
搞定!


可以再优化的地方


现在我们还有个问题,就是在执行 Commit changes 这个步骤时每次 git config user.name "Your github name" git config user.email "your github email" 这里是写死的,我们可以根据 GitHub Actions 中有一些预设的环境变量可以读取到当前用户的账号和邮箱信息。通过 ${{ env.GITHUB_ACTOR }} 获取到当前执行的 Actions 的用户账号,通过 ${{ env.GITHUB_ACTOR }}@users.noreply.github.com 获取到当前执行的 Actions 的用户邮箱(该邮箱为 noreply 邮箱,用于 GitHub 的通知,无法发送邮件)。注意,该邮箱不一定是用户本身的真实邮箱,可能是 GitHub 默认的邮箱。



如果需要获取当前 GitHub 账号的真实邮箱地址,可以通过 GitHub REST API 进行查询,具体可以参考官方文档:



这样我们就需要在Commit Changes之前再加一个Set Git user步骤


- name: Set Git user
env:
GITHUB_ACTOR: ${{ github.actor }}
GITHUB_EMAIL: ${{ github.actor }}@users.noreply.github.com
run: |
git config --global user.name "${{ env.GITHUB_ACTOR }}"
git config --global user.email "${{ env.GITHUB_EMAIL }}"

这样我们最终的 Github action 脚本长这样



name: Update Package Version

on:
release:
types: [released]

jobs:
update:
runs-on: ubuntu-latest
steps:

- name: Checkout code
uses: actions/checkout@v3

- name: Update package.json
run: |
node ./scripts/update-version.js ${{ github.event.release.tag_name }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Set Git user
env:
GITHUB_ACTOR: ${{ github.actor }}
GITHUB_EMAIL: ${{ github.actor }}@users.noreply.github.com
run: |
git config --global user.name "${{ env.GITHUB_ACTOR }}"
git config --global user.email "${{ env.GITHUB_EMAIL }}"

- name: Commit changes
run: |
git add .
git commit -m "Update version to ${{ github.event.release.tag_name }} for release ${{ github.ref }}"
git_hash=$(git rev-parse --short HEAD)

- name: Push changes
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}

- name: Tag Push changes
run: |
git tag -f ${{ github.event.release.tag_name }} $git_hash
git push --force origin ${{ github.event.release.tag_name }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


最后


如果我的文章对你有帮助欢迎点赞+收藏支持


作者:Jervis_cen
来源:juejin.cn/post/7220164534316433467
收起阅读 »

Flutter应用如何打包发版

Flutter应用程序的打包和发布可以通过Flutter命令行工具完成。以下是具体步骤: 确保你已经安装了Flutter SDK,并且在终端中配置了Flutter环境变量。 在终端中进入Flutter项目的根目录,运行flutter build apk命令生...
继续阅读 »

Flutter应用程序的打包和发布可以通过Flutter命令行工具完成。以下是具体步骤:



  1. 确保你已经安装了Flutter SDK,并且在终端中配置了Flutter环境变量。

  2. 在终端中进入Flutter项目的根目录,运行flutter build apk命令生成APK文件(Android)或flutter build ios命令生成ipa文件(iOS)。

  3. 如果需要对APK或ipa进行签名,则需要使用相应的签名工具,例如jarsignerFastlane。签名后的文件可以直接上传至Google Play Store、Apple App Store等进行发布。

  4. 对于Android,可以使用Flutter gradle插件构建和打包APK文件,并自动签名。在build.gradle文件中添加Flutter插件依赖,然后运行flutter build apk命令即可生成签名过的APK文件。

  5. 对于单独渠道号等,有需要的可以单独配置。此文不做具体详解。


以下是Flutter代码示例:


# pubspec.yaml
name: my_flutter_app
version: 1.0.0
dependencies:
flutter:
sdk: flutter

# build.gradle
buildscript {
repositories {
google()
jcenter()
}

dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'

// 添加Flutter插件依赖
classpath 'com.android.tools.build:gradle:4.1.0'
classpath 'com.google.gms:google-services:4.3.8'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.6.1'
classpath 'com.google.firebase:perf-plugin:1.4.0'
}
}

allprojects {
repositories {
google()
jcenter()
}
}

rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}

task clean(type: Delete) {
delete rootProject.buildDir
}

# 运行打包命令
flutter build apk --release
# 如果有空安全,运行打包命令
# 可选debug,release,profile
flutter build apk --no-sound-null-safety --release

需要注意的是,要发布到应用商店(如Google Play Store或Apple App Store),需要对应用进行签名。在Flutter应用程序构建后,可以使用相应的签名工具对APK或ipa文件进行签名。例如,在Android上,可以使用jarsignerzipalign工具对APK文件进行签名和优化。而在iOS上,则需要使用Xcode中的证书签署和打包工具进行签名和打包。


Flutter支持通过构建发布包来支持多种CPU架构(如armeabi-v7a,arm64-v8a和x86_64等),以最大程度地提高应用程序的兼容性和性能。以下是具体步骤:



  1. 在Flutter项目的根目录下运行flutter build apk --target-platform android-arm,android-arm64,android-x64命令以生成支持多架构的APK文件。

  2. 运行上述命令后,Flutter会自动构建三个不同CPU架构的版本,并将它们打包到一个单独的APK文件中。

  3. 如果需要对APK进行签名,则可以使用常规的签名工具,例如jarsignerFastlane。需要对每个CPU架构分别进行签名,然后使用zipalign工具对打包后的文件进行优化。

  4. 最后,可以将签名过的APK文件上传至Google Play Store或其他应用商店进行发布。


注意,如果使用的是Flutter 2.5或更高版本,则无需手动指定目标平台,因为Flutter会自动为你构建多架构版本的应用程序。


以下是Flutter代码示例:


# 运行打包命令
flutter build apk --release --target-platform android-arm,android-arm64,android-x64

需要注意的是,在打包发布之前,应该先通过模拟器或真机设备进行测试,确保应用程序在所有CPU架

作者:IT编程学习栈
来源:juejin.cn/post/7220657346543435831
构上都可以正常运行。

收起阅读 »

我把我的简历丢给chatGPT,他问了我这些问题

前言 chatGPT是openAI于2022年11月推出的人工智能聊天程序,chatGTP一经推出便火爆全网,通过一问一答且结合上下文的方式自动生成问题答案,作为前端开发工程师,我们可以使用chatGTP进行代码纠错,代码优化等一系列的代码调试,当然,作为一个...
继续阅读 »

前言


chatGPT是openAI于2022年11月推出的人工智能聊天程序,chatGTP一经推出便火爆全网,通过一问一答且结合上下文的方式自动生成问题答案,作为前端开发工程师,我们可以使用chatGTP进行代码纠错,代码优化等一系列的代码调试,当然,作为一个已经失业一个月的前端开发,我们还可以让chatGTP作为面试官,来对我们进行一次模拟面试。


向chatGPT提问


提问内容需要对它进行一个身份设定,它是一名前端开发的面试官。


image.png


然后将自己的简历复制一份,最好先整理一下格式,然后抛给它。


chatGPT返回的问题


根据我的简历他问了我以下几个问题:


面试官:尊敬的面试者,您的简历显示您具备丰富的前端开发经验,我们非常高兴能邀请您来参加面试。现在让我们开始探讨一些与您工作经历和技能相关的问题,期待您的精彩回答。



  1. 请详细阐述您在使用 HTML5、CSS3 和 ES6 新特性中的心得体会,以及如何在实际项目中应用这些新特性提升工作效率与用户体验。

  2. 闭包、原型和作用域等 JavaScript 基础知识在您的项目中发挥了什么作用?请结合实际项目案例,谈谈您如何运用这些知识解决实际问题。

  3. 在您的项目经历中,如何运用 Vue 全家桶以及 MVVM 思想来提升开发效率?请结合具体项目案例谈谈您的实践经验。

  4. 您曾尝试写过一个简单的 MVVM 框架,可以简要介绍下您的实现思路和遇到的挑战吗?

  5. 您在xxx问题的经验和方法值得我们借鉴,请详细介绍这个过程以及您对于类似问题的解决方案。

  6. 您如何运用 three.js 进行 3D 可视化开发?请举例说明您在项目中的具体应用。

  7. 您在项目中遇到过哪些让您印象深刻的技术挑战?您是如何克服这些挑战的?

  8. 请谈谈您对于前端开发中代码规范和项目管理的理解,以及您在实际工作中如何保证高质量的代码输出。

  9. 在您看来,一个优秀的前端开发者应具备哪些品质和技能?请结合您的实际经历分享一下。


期待您对这些问题的精彩回答。祝您面试顺利,前程似锦!


image.png


可以看出chatGTP的模拟面试其实更偏向于对项目方面进行模拟面试,而我要求面试中包含大量八股文,这个是没有给到的,我意识到它可能是无法理解“八股文”在前端开发面试中代表的意思,于是我又换了个说法提问它。


提示chatGPT提问八股文


你是一名前端开发工程师,请根据我的简历对我进行面试,面试主要面试常见的基础知识?


image.png


chatGPT面试官对于八股文的提问


面试官:你好,根据你的简历,我了解到你具备扎实的前端开发基础。在接下来的面试环节中,我将针对一些常见的前端基础知识进行提问。请放轻松,尽量展示你的技能和经验。



  1. 请谈谈你对HTML5和CSS3新特性的理解。你在实际项目中如何运用这些新特性?

  2. 请解释一下JavaScript中的闭包(closure),并给出一个闭包的应用场景。

  3. 请简述原型链(prototype chain)的概念,以及它在JavaScript中的应用。

  4. 在你的项目中,你是如何处理跨域请求的?

  5. 请谈谈你对前端性能优化的理解,以及在项目中采取了哪些措施来提升性能?

  6. 请解释一下Event Loop(事件循环)机制以及它在JavaScript中的作用。

  7. 你在实际项目中如何使用Vue全家桶(Vue-router、Vuex等)?请分享一下你的经验。

  8. 在你的项目中,你是如何处理浏览器兼容性问题的?

  9. 请解释一下HTTP缓存机制,以及如何在项目中利用HTTP缓存提高性能?

  10. 请谈谈你对响应式设计(Responsive Design)的理解以及如何在项目中实现响应式设计。


请根据这些问题,尽量详细地展示你的知识和实际经验。如果对某个问题不太了解,也可以跳过,我们会综合评估你的技能。祝你面试顺利!


或许这些八股文还不够,可以要求它提问更多八股文


你是一名前端开发工程师,请根据我的简历对我进行面试,面试请提供更多面试常见的基础知识问题


image.png


chatGPT再次提问八股文


面试官:你好,根据你的简历,我了解到你具备扎实的前端开发基础。在接下来的面试环节中,我将针对一些更多的前端基础知识进行提问。请放轻松,尽量展示你的技能和经验。



  1. 请解释一下HTML语义化的概念及其优点。

  2. 请描述CSS盒模型(Box Model)的组成部分以及在布局中的作用。

  3. 请简要介绍一下Flex布局和Grid布局,以及它们各自的应用场景。

  4. 在JavaScript中,谈谈你对作用域和作用域链的理解。

  5. 请解释一下JavaScript的同步和异步,以及如何使用Promise、async/await处理异步操作。

  6. 请谈谈你对Vue的生命周期钩子函数的理解以及常见的应用场景。

  7. 请描述一下Vue中的计算属性(computed property)和侦听属性(watch property)以及它们的区别和应用场景。

  8. 在你的项目中,你是如何使用Webpack进行模块化打包和优化的?

  9. 请解释一下浏览器的渲染机制以及如何利用这一知识进行页面性能优化。

  10. 在项目中,你是如何进行前端安全防范,例如防止XSS攻击和CSRF攻击的?

  11. 请谈谈你对前端单元测试的理解,以及在项目中如何进行单元测试。

  12. 请解释一下前端MVC和MVVM设计模式的概念及其区别。

  13. 请谈谈你对前端性能监控和数据埋点的理解以及在项目中的应用。

  14. 请简要介绍一下如何在项目中实现前后端分离以及它带来的好处。

  15. 请谈谈你对Web Accessibility(网络无障碍)的理解以及如何在项目中实现无障碍访问。


请根据这些问题,尽量详细地展示你的知识和实际经验。如果对某个问题不太了解,也可以跳过,我们会综合评估你的技能。祝你面试顺利!


image.png


关于此次模拟面试提问的总结


这样我们此次模拟面试就获得了9条关于项目的问题,和25条关于基础的问题,细看这几个问题都是明显贴合我的简历所问的问题,大部分面试官都是会针对简历进行面试,所以根据自己的简历整理可能提到的问题是非常重要的,而关于项目方面提出的问题,与我进行大厂模拟面试所遇到的问题更为相像。通过chatGPT进行模拟面试,算得上是一场挺有质量的面试了。


尝试回答部分chatGPT模拟面试提出的问题


1. 请谈谈你对HTML5和CSS3新特性的理解。你在实际项目中如何运用这些新特性?


HTML5新出了许多语义化标签(nav,header,main等)和API(canvas,webGL),语义化标签优点在于提高代码可读性和可维护性,有利于搜索引擎更容易读懂(seo),有些特殊的屏幕阅读器可以通过分析语义化标签进行展示(比如盲人阅读器)。CSS3新出了圆角,阴影,过渡,动画,弹性布局等使创造复杂样式和交互变得更简单,也使我们可以更快的去完成页面样式。


2. 请解释一下JavaScript中的闭包(closure),并给出一个闭包的应用场景。


闭包是指函数有权访问其作用域以外的变量,这就叫闭包,闭包有两种表现形式,一种是函数作为参数传递,一种是函数作为返回值,闭包的应用场景有防抖节流函数、setTimeout、封装私有变量。闭包的缺点在于,其访问的变量不会被回收(会一直占用内存),所以会造成内存泄漏。


3. 请简述原型链(prototype chain)的概念,以及它在JavaScript中的应用。


原型链是js的一种继承方式,每个对象都有自己的__proto__属性指向其构造函数的prototype属性,当对象调用某个方法且对象里并没有定义这个方法时就会通过__proto__去寻找这个方法,如果其__proto__没有就会顺着其__proto__的__proto__就是顺着其原型链去寻找方法直到null。


4. 在你的项目中,你是如何处理跨域请求的?


因为浏览器的同源策略(ip,端口,协议需要一致),我们跨域请求的时候会出现跨域问题,
在开发环境中,我使用代理服务器(如vue.config.js中的proxy配置)解决跨域问题。在生产环境中,我是用nginx的代理解决跨域问题。
当然也可以让后端在服务器端设置响应头,允许跨域请求。或者是用websocket,websocket没有跨域问题。


5. 请谈谈你对前端性能优化的理解,以及在项目中采取了哪些措施来提升性能?


前端性能优化分为两类,一种是让文件加载更快,另一种是让文件渲染更快。


加载更快的方法



  • 让传输的数据包更小(压缩文件/图片):图片压缩和文件压缩

  • 减少网络请求的次数:雪碧图/精灵图、节流防抖

  • 减少渲染的次数:缓存(HTTP缓存、本地缓存、Vue的keep-alive缓存等)

  • 使用CDN:利用内容分发网络(Content Delivery Network)加速静态资源的加载速度,将资源部署到离用户更近的服务器


文件渲染更快的方法



  • 提前渲染:ssr服务器端渲染

  • 避免渲染阻塞:CSS放在HTML的head中 JS放在HTML的body底部

  • 避免无用渲染:懒加载

  • 减少渲染次数:对dom查询进行缓存、将dom操作合并、减少重排重绘


6. 请解释一下Event Loop(事件循环)机制以及它在JavaScript中的作用。


Event Loop是js实现异步的一种机制,它让js这个单线程语言可以实现并发操作。JavaScript引擎在执行栈为空时,会从任务队列中取出任务执行;任务队列有两类。



  • 宏任务:包括script(整个代码)、setTimeout、setInterval、setImmediate和I/O等。

  • 微任务:Promise、process.nextTick等


先执行宏任务再执行微任务


7. 你在实际项目中如何使用Vue全家桶(Vue-router、Vuex等)?请分享一下你的经验


我通过vue-router进行路由管理,通过Vuex管理全局状态,我会对路由进行懒加载,通过ES6的import,当然也可以用vue的动态组件,Vuex的核心包括state、getter、mutation和action,通过mutation去更改state的值,要注意异步操作只能放在action里。


8. 在你的项目中,你是如何处理浏览器兼容性问题的?


处理浏览器兼容性问题,我会使用autoprefixer自动添加CSS前缀,使用Babel转译新语法,使用Polyfill补充缺失功能,并针对特定浏览器进行特殊处理。


9. 请解释一下HTTP缓存机制,以及如何在项目中利用HTTP缓存提高性能?


http缓存包括强缓存和协商缓存



  • 强缓存:浏览器自己的缓存策略


强缓存是通过Cache-Control字段来控制的,值有max-age(缓存的最大时间)、no-cache(无需强制缓存)、no-store(服务端直接返回)



  • 协商缓存: 由服务器判断资源是否一样,一致则返回304,否则返回200和最新资源


判断资源是否一致,这主要通过Last-Modified/If-Modified-Since和ETag/If-None-Match头部字段实现。
Last-Modified:资源最后修改时间。
If-Modified-Since:客户端下次请求相同资源时,会发送该字段,值为上次收到的Last-Modified的值。
ETag: 资源的唯一标识
If-None-Match: 客户端下次请求相同资源时,会发送该字段,值为上次收到的ETag值。


10. 请谈谈你对响应式设计(Responsive Design)的理解以及如何在项目中实现响应式设计。


响应式设计就是指让网站在不同设备下实现自适应展示,实现响应式设计有以下几种方法



  • 不使用固定单位使用相对单位,如百分比,rem,vw/vh等

  • 尽可能的使用弹性布局

  • 媒体查询:针对不同的屏幕尺寸,修改对应的css


11. 在JavaScript中,谈谈你对作用域和作用域链的理解。


作用域是指变量和函数的可访问的上下文,就是其作用域,作用域链是由指变量或函数查找时先从当前的执行上下文往其夫级的上下文寻找,直到最外层的window。


12. 请解释一下JavaScript的同步和异步,以及如何使用Promise、async/await处理异步操作。


js是一个单线程语言,所以会同步执行代码,为了防止代码阻塞,通过时间循环机制实现了代码异步处理,当同步代码都执行完毕之后,再去执行异步代码,常见的异步代码有网络请求、alert、setTimeout等,promise是异步的解决方案,它解决了之前通过回调函数实现异步而产生的回调地狱的问题,promise有三种状态,pendding、reject、fulfilled,只能从pedding到其他状态,且过程不可逆,async和await是基于promise实现的,它是为了让异步代码看起来像同步代码,使代码更容易阅读和维护。


13. 请谈谈你对Vue的生命周期钩子函数的理解以及常见的应用场景。


vue的组件的生命周期包括组件创建、组件挂载、组件更新、组件销毁,从中产生了生命周期钩子函数。
vue2中包括 beforeCreate、created、beforeMounte、Mouted、beforeUpadte、updated、beforeDestory、destroyed,如果组件使用了keep-alive进行缓存的话,还会有active、deactive。


vue3中的生命周期包括setup,onBeforeMount、onMounted、onBeforeUpdate,onUpdatedon、onBeforeUnmount、onUnmounted。


mouted(onMounted)钩子函数可以做一些关于dom的操作,beforeDestroy(BeforeUnmount)在此阶段可以执行清理工作,如移除事件监听器、取消计时器等。


14. 请描述一下Vue中的计算属性(computed property)和侦听属性(watch property)以及它们的区别和应用场景。


计算属性是依赖其他属性变化得出结果,而侦听属性是针对数据变化而触发操作,计算属性具有缓存机制。


15. 请谈谈您对于前端开发中代码规范和项目管理的理解,以及您在实际工作中如何保证高质量的代码输出。


前端代码规范和项目管理在前端开发中非常重要,他可以保证代码风格一致,提高代码可读性,提高项目可维护性和团队协作效率。


在实际工作中我使用如下内容保证高质量的代码输出。



  • 统一的编码风格:用代码风格指南和自动化工具(如ESLint、Prettier等)

  • 使用版本控制系统: 通过git来管理代码

  • 注释和文档: 编写清晰明了的readme.md

  • Code Review: 团队成员对代码进行代码审查


总结


本次面试,各位面试官满意吗?如有什么错误欢迎大佬们评论区反馈纠正。


作者:三三两两的小久
来源:juejin.cn/post/7220274775914037285
收起阅读 »

基于 ChatGPT 的AI 社交App 技术实现方案|直播课

扫二维码预约提前锁定视频号直播

2022年11月底,美国一家名为OpenAI 的人工智能研究公司推出 ChatGPT 人工智能聊天机器人,可以实现与用户之间的智能聊天对话,两个月后,这款机器人在全球社交平台火爆出圈,媒体报道说它“上知天文下知地理,与人交流对答如流,既能写也能编还能改错。”人们惊叹于AI的创造力,也逐渐打开了有关AI 人工智能的想象空间。因此 ChatGPT 爆火后,紧随其后的便是各种场景对大语言模型的跃跃欲试,搜索、协同办公、社交等场景都摩拳擦掌。

文字对话的格式使得 ChatGPT 能够回答问题、承认错误、质疑不正确的问题并拒绝不适当的请求,其与 IM 即时通讯相结合,在陌生人社交场景的产品玩法、运营支撑等方面的表现尤其值得期待。

环信作为国内即时通讯云领域的开创者,率先将IM(即时通讯)和ChatGPT结合在社交领域中,可以为用户提供更加顺畅、高效和个性化的聊天体验,同时也有望在社交应用程序中实现更多的智能化功能,创新更多社交新玩法,从而帮助 APP 提高活跃度、用户满意度和忠诚度。
4月20日(周四),环信线上公开课将以《基于ChatGpt 的AI 社交App 技术实现方案》为题,讲解 ChatGPT 技术、在社交APP应用场景、开发的难点及挑战,以及环信在AI 对话社交 App 技术方案等内容。

时间地点

活动时间:4 月 20 日(星期四)19:00-20:00
活动地点:线上直播/环信视频号

演讲大纲

ChatGPT 技术简介
ChatGPT 国内外典型 app
AI 对话社交 App 开发的难点和挑战
AI 对话社交 App 技术方案
- 应用架构
- 实现方案
- 技术特点、要求
环信解决方案典型社交案例解析
AI 在社交领域的思考与展望

活动报名

扫二维码预约提前锁定视频号直播




看我如何用定值 Cookie 实现反爬

本文分享自华为云社区《我是怎么用一个特殊Cookie,限制住别人的爬虫的》,作者: 梦想橡皮擦 。 Cookie 生成 由于本案例需要用到一个特定的 Cookie ,所以我们需要提前将其生成,你可以直接设置一个固定的字符串,也可以使用 Python 加密模块来...
继续阅读 »

本文分享自华为云社区《我是怎么用一个特殊Cookie,限制住别人的爬虫的》,作者: 梦想橡皮擦 。


Cookie 生成


由于本案例需要用到一个特定的 Cookie ,所以我们需要提前将其生成,你可以直接设置一个固定的字符串,也可以使用 Python 加密模块来加密一段文本,例如本案例加密 梦想橡皮擦


下面是一个示例代码,展示了如何使用 Python 的 hashlib 模块进行加密:


import hashlib

# 要加密的文本
text = "梦想橡皮擦"

# 使用 sha256 算法进行加密
encrypted_text = hashlib.sha256(text.encode()).hexdigest()

print(encrypted_text)

在这个例子中,我们使用了 hashlib 模块中的 sha256 算法对文本进行加密。这个算法生成了一个长度为 64 位的十六进制哈希值,用于表示加密后的文本。



注意,这个算法只能用于加密文本,而不能用于解密。因此,一旦文本被加密,就无法恢复成原来的文本,即不可逆加密/单项加密。



Python Flask 框架生成 Cookie


在 Python 的 Flask 框架中,可以使用 make_response 函数和 set_cookie 方法来生成一个 Cookie。


例如,下面的代码片段展示了如何在 Flask 中设置一个名为 story 的 Cookie,并将它的值设为前文建立的加密串。


from flask import Flask, make_response

app = Flask(__name__)

@app.route('/')
def index():
resp = make_response('Setting a cookie')
encrypted_text = hashlib.sha256(text.encode()).hexdigest()
resp.set_cookie('story', encrypted_text)

return resp

在这个例子中,我们使用 make_response() 函数创建了一个响应对象,然后使用 set_cookie() 方法来设置 cookie。最后,我们将响应对象返回给客户端。


注意,上面的代码仅创建了一个简单的 Cookie,它只有名称和值两个部分。你还可以使用其他可选参数来设置 Cookie 的其他属性,例如过期时间、域名等。


接下来为大家在补充一下 make_response() 相关知识。


Flask make_response 加载模板


在 Flask 中,你可以使用 make_response() 函数和模板系统来生成带有模板的响应。


下面是一个示例代码,展示了如何使用 make_response() 函数加载模板:


from flask import Flask, make_response, render_template

app = Flask(__name__)

@app.route('/')
def index():
# 加载模板并渲染
rendered_template = render_template('index.html', title='梦想橡皮擦')
# 使用 make_response 函数创建响应
resp = make_response(rendered_template)
return resp

在这个例子中,我们首先使用 Flask 的 render_template() 函数加载并渲染了名为 index.html 的模板。然后我们使用 make_response() 函数创建了一个响应对象,并将渲染后的模板作为响应的内容。最后,我们返回了这个响应对象给客户端。



注意,你需要在 Flask 应用的模板目录中存在名为 index.html 的模板文件,才能正常使用上述代码。



然后我们将该视图函数补充完整,代码在 app/routes.py 文件中。


@app.route('/')
@app.route('/index')
def index():
item = {
"msg": "后台传递信息"
}
# 访问首页生成一个 Cookie 值,该值用于访问特定页面
rendered_template = render_template('index.html', title='梦想橡皮擦')
resp = make_response(rendered_template)
text = "梦想橡皮擦"

# 使用 sha256 算法进行加密
encrypted_text = hashlib.sha256(text.encode()).hexdigest()
resp.set_cookie('story', encrypted_text)
return resp

此时当我们访问爬虫训练场首页的时候,就会在 Cookie 中写入一个加密之后的字符串。

通过开发者工具,可以查看到响应头。



最后一步,就是在 Python Flask 框架中判断刚刚的 Cookie 值,如果存在则响应数据,否则返回 403。


Flask 判断指定 cookie 是否存在


在 Python 的 Flask 框架中,你可以使用 request.cookies 属性来判断指定的 Cookie 是否存在。


例如,下面的代码片段展示了如何判断一个名为 story 的 Cookie 是否存在:


from flask import Flask, request

app = Flask(__name__)

@app.route('/')
def index():
if 'story' in request.cookies:
# 如果存在 'story' cookie,则执行相应操作
# ...
else:
# 如果不存在 'story' cookie,则执行相应操作
# ...

将代码补充完整,文件是 app/antispider/index.py


@antispider.route('/cookie_demo')
def cookie_demo():
if 'story' in request.cookies:
# 如果存在 'story' cookie,则执行相应操作
# ...
return render_template("antispider/cookie_demo.html")
else:
return "没有权限", 403

补充知识点


在 Python 的 Flask 框架中,除了使用 set_cookie() 方法设置 cookie 以外,还有其他几种操作 cookie 的方法。


下面是一些常用的操作 cookie 的方法:




  • 设置 cookie 的值:你可以使用 set_cookie() 方法来设置 cookie 的值。例如:


    from flask import Flask, make_response


    app = Flask(name)


    @app.route('/')
    def index():
    resp = make_response('Setting a cookie')
    resp.set_cookie('user', 'xiangpica')
    return resp




  • 获取 cookie 的值:你可以使用 request.cookies字典来获取 cookie 的值。例如:


    from flask import Flask, request


    app = Flask(name)


    @app.route('/')
    def index():
    user = request.cookies.get('user')
    return user




  • 删除 cookie:你可以使用 set_cookie() 方法并将 cookie 的过期时间设为过去的时间来删除 cookie。例如:


    from flask import Flask, make_response


    app = Flask(name)


    @app.route('/')
    def index():
    resp = make_response('Deleting a cookie')
    resp.set_cookie('user', '', expires=0)
    return resp




点击关注,第一时间了解华为云新鲜技术~


作者:华为云开发者联盟
来源:juejin.cn/post/7217665415710244921
收起阅读 »

nginx带宽限制 limit_rate limit_rate_after

web
知识梳理 在高负载的网络环境下,为了保持服务的稳定性,限速 (download rate) 是一种必要的控制访问量的手段。Nginx 是一款高性能的 Web 服务器和反向代理服务器,可以使用 limit_rate_after 和 limit_rate 两个主要...
继续阅读 »

知识梳理


在高负载的网络环境下,为了保持服务的稳定性,限速 (download rate) 是一种必要的控制访问量的手段。Nginx 是一款高性能的 Web 服务器和反向代理服务器,可以使用 limit_rate_after 和 limit_rate 两个主要指令来完成流量控制和限速。


limit_rate_after 指令


指令 limit_rate_after 会在客户端成功建立连接之后,指定的大小后开始限制发送速度。这个指令的含义就是在连接建立后的 limit_rate_after 大小之后,数据发送速率将被限制。


以下是limit_rate_after 的语法和示例:


Syntax:	limit_rate_after size;
Default:
limit_rate_after 0;
Context: http, server, location, if in location

limit_rate_after 50m;

这个指令可以帮助您限制连接的初始流量,以便于服务器的带宽资源分配更为合理。


limit_rate 指令


limit_rate 指令是用来控制发送至客户端的数据传输速度的,它可以限制整个连接的流量,也可以限制单个客户端访问速度。


以下是 limit_rate 的语法和示例:


syntax:		limit_rate rate;
default: —
context: http, server, location

limit_rate 1k;

这个配置的作用是:在与客户端建立连接之后的 10 秒内,限制每秒发送的数据量不超过 50kB;之后如果连接仍然打开,则限制与该客户端的速率为 50kB/s。


需要提醒的一点是,尽管 limit_rate 可以一定程度上保护服务器资源,但是并不足以完全阻止恶意饱和攻击。因此,在考虑流量控制和限速的同时,还应该结合其他安全和防护机制来更好地保护服务器。


实验


配置传输速度为 1k



  • nginx配置


location / {
limit_rate 1k;
root html;
}



配置下载50m后开始限制传输速度



  • nginx配置


location / {
limit_rate_after 50m;
limit_rate 1k;
root html;
}


可以看到开始下载速度很快


在这里插入图片描述
在下载50m后,速度限制在1k以内
在这里插入图片描述


我遇到的坑



因为我的portal.tar文件没有读的权限,导致浏览器下载报403,使用 chmod 755 portal.tar 修改portal.tar文件的权限,如下图:


在这里插入图片描述


总结


Nginx 的限速功能对于控制访问量、防止恶意攻击具有很高的研究价值和实际意义。limit_rate 和 limit_rate_after 是 Nginx 常见的两个限速指令,它们可以配置在 http、server、location 等区块中,实现不同级别的流量限制和控制。一般情况下为了取得更好的限速效果,我们会同时使用两个指令,通过多事件流的限速进行灵活的控制。


希望这篇文章能够对 Nginx 限速功能有更深入的理解,帮助开发者在实际的生产环境中使用它来进行更好的流量控制和管理。


参考


nginx.org/en/docs/htt…
nginx.org/en/docs/htt…


作者:黄娟
来源:juejin.cn/post/7219889814115811388
收起阅读 »

一个前端大佬的十年回顾 | 漫画前端的前世今生

作者:京东科技 胡骏 引言 岁月如梭,十载流年 前端技术,蓬勃向前 HTML,CSS,JavaScript 演绎出璀璨夺目的技术画卷 回到十年前,前端技术就像一名戴着厚重眼镜的书呆子,总是小心翼翼,被各种各样的浏览器兼容性问题欺负(就像在小学被欺负一样)。 ...
继续阅读 »

作者:京东科技 胡骏


引言


岁月如梭,十载流年


前端技术,蓬勃向前


HTML,CSS,JavaScript


演绎出璀璨夺目的技术画卷




回到十年前,前端技术就像一名戴着厚重眼镜的书呆子,总是小心翼翼,被各种各样的浏览器兼容性问题欺负(就像在小学被欺负一样)。



但随着时间的推移,这个书呆子开始锻炼,变得越来越强壮,终于能够对抗那些讨厌的兼容性问题



进入中学时期,前端技术遇到了那个改变它一生的朋友——jQuery。在jQuery的帮助下,前端技术变得更加自信,能够在各种浏览器之间轻松穿梭(就像找到了武林秘籍,功力大增)。



随后,前端技术开始追求更高的境界。它遇到了三位美丽的姑娘:Angular、React和Vue。这三位姑娘带给了前端技术无尽的魅力,让它迅速崛起,成为了技术江湖中的一股新兴力量。



如今,前端技术已经变得越来越强大,像一个熟练掌握各种武功的高手。它的发展速度之快,令人瞠目结舌,仿佛在短短十年内成为了武林盟主。它带领着一群忠诚的拜金党(程序员),在技术江湖中闯荡,创造了一个又一个的传奇。



而现在,前端技术正在为未来的挑战做准备,它还能带给我们多少惊喜,以及如何抵抗那些不断涌现的挑战者?让我们一起拭目以待,看这场武林大戏如何演绎。


一、历程


前端技术开发在过去的十年里经历了从HTML、CSS到JavaScript的演变。在这个历程中,前端工程师的角色也发生了变化,他们不再只是单纯的代码开发者,还需要与设计师、产品经理、运营人员等其他团队成员协作,共同完成网站的开发。



• _2010年以前,_前端工程师主要负责网站的静态页面开发,如网页设计、图片处理等。在这个阶段,前端工程师的技能主要包括HTML、CSS和JavaScript等基本技术。


• _2010年,_JavaScript成为了前端开发的主要语言。随着JavaScript的普及和发展,越来越多的前端工程师开始关注JavaScript的应用和开发。


• _2011年,_jQuery成为了前端开发的主流库,并且HTML5和CSS3开始受到重视。这也是前端开发变得更加动态和交互性的开始。


• _2012年,_响应式设计和移动设备优先的设计理念开始流行,前端开发在移动端上崭露头角。


• _2013年,_Angular引入了模块化和数据绑定的概念,Bootstrap实现了响应式框架,前端开发变得更加简单和高效。


• _2014年,_React发布,革新出组件化的思想,前端开发变得更加灵活和可维护。


• _2015年,_ES6发布,带来了诸如箭头函数、模板字符串和解构赋值等语言的改进,使JavaScript变得更加易用和现代化。同年,Vue的发布迅速获得了广泛应用。


• _2016年,_前端工具链的发展得到了加速,例如Webpack和Babel等工具的普及使得前端工程化得到了广泛推广。


• _2017年,_JavaScript库和框架更加多样,Angular、React和Vue等都在不断地演进和优化。PWA技术的普及使得网页更接近原生应用的用户体验。


• _2018年,_JavaScript框架的选择更加复杂,同时CSS预处理器(例如Sass和Less)和CSS-in-JS的技术也逐渐成熟。


• _2019年,_前端技术继续保持快速发展的趋势,更加注重用户体验和开发效率。例如,React Hooks和Vue 3等技术的推出使得前端代码更简洁并可维护。


• _2020年,_因新冠疫情影响,居家办公及远程工作成为新趋势。虚拟会议和在线教育等普及推动了前端技术的发展,也更加重视了访问性和用户体验。


• _2021年,_新技术和工具不断推陈出新。Web Assembly使得前端代码获得更高的效率,而预渲染和静态站点生成等技术让前端应用可以获得更快的加载速度。


• _2022年,_VR(虚拟现实)和AR(增强现实)技术的不断发展,前端开发者需要开发出更加适合VR/AR场景的应用程序。


• _2023年至今,_AI(人工智能)技术的突破性进展,前端技术将在AI 技术的加持下得到更广泛的应用,从而带来更智能和更高效的前端开发体验。


二、HTML5和CSS3的普及


HTML5和CSS3,这两个神秘代码世界的统治者,它们的名字听起来像是一对科学家的昵称,但它们的影响力却是无与伦比的:让我们的网页从普通变得绚丽多彩。



作为一名网页开发者,我们经常需要面对一些令人头疼的问题:浏览器兼容性、页面加载速度缓慢等。但是,当HTML5和CSS3出现在我们的视野中时,一切都变得不一样了。


HTML5是一种用于网页开发的语言,它具有更强的多媒体功能,比如说可以轻松地嵌入音频和视频。它还具有更强的语义,使我们可以更容易地描述页面内容。


而CSS3则是一种用于美化网页的语言,它提供了更多的样式选项,比如说可以实现圆角、阴影等效果。它还支持响应式设计,可以让我们的网页在不同的设备上都能得到最佳的展示效果。


用HTML5和CSS3开发的网页不仅美观,而且更快。我们不再需要使用大量的JavaScript代码来实现一些简单的功能,因为HTML5和CSS3已经帮我们完成了这些工作。


不仅如此,HTML5和CSS3还使得网页开发变得更有趣。我们可以创造出各种各样的动画效果,比如说滚动、旋转等,而不需要依赖任何第三方工具。这不仅让我们的网页更具吸引力,也使我们的用户更容易理解和使用。



HTML5就像一个网页的“建造师”,它负责把网页的框架建造出来,而CSS3则是一个“装饰师”,它负责把网页的外观和感觉打造出来。这对搭档携手合作,把一栋美丽的大厦(网站)拔地而起。


三、JavaScript框架的崛起


JavaScript框架,从这个词语中我们就能感受到它的强大和威力,如同统治世界的巨龙,横行天下,让所有的开发者都震撼不已。



在过去的十年里,我们见证了许多JavaScript框架的诞生和发展。最早的Angular和Backbone逐渐被React和Vue等框架所取代。这些框架不仅简化了开发者的工作流程,还引入了组件化的开发思想,提升了Web应用的可维护性和可扩展性。


另外,JavaScript框架也推动了Web前端技术的进步,引入了许多新的概念和理念,如组件化、数据驱动等等,使得Web前端开发变得更加简单而清晰。


3.1 React:让你的用户界面如此简单


React,这是一个神奇的JavaScript框架,它可以让你的用户界面变得如此简单,以至于你会想:“这就是魔法吗?”


React的核心思想是组件化,它把用户界面拆分成许多小的组件,每个组件都可以独立运行,并且可以方便地复用。这样,你就可以更加简单高效地开发出高质量的用户界面。



React的另一个优秀特性是Virtual DOM,它可以帮助你更快速地渲染用户界面,并且不会影响用户体验。这就像是一个超级快速的缓存,让你的用户界面飞快地呈现在用户面前。


React还提供了一些非常实用的功能,比如说React Router,它可以帮助你管理路由,让用户界面更加流畅;而React Redux可以帮助你管理应用状态,让你的代码更加整洁。


此外,React是一个非常活跃的开源项目,它的开发者们一直在不断改进和完善,值得每一个前端开发者去学习和使用。


3.2 Vue:充满了年轻的活力和智慧


Vue是另一个JavaScript框架,可以让你快速构建网页,就像是一个魔术师,把一堆杂乱无章的东西变成了一个美丽的魔术。


Vue的核心思想是数据驱动+组件化。这听起来很高大上,但其实就像是你在做一道数学题,先把问题分解成若干小问题,再一步步解决。


Vue有一个很酷的特性:双向绑定。这听起来很神秘,但实际上就像是你和你的好朋友之间的对话,你说了什么,他就知道了。



学习和使用Vue的过程中,你会发现开发变得更加简单和有趣,就像是在做一道神奇的拼图,一步步把图片拼出来,比如说它有很多组件,就像是一个工具箱,你可以随时随地使用。组件的好处在于,它可以把复杂的功能分解成若干个简单的部分,这样就可以很容易地管理和维护你的代码。


同时,Vue有很多很多的插件,可以让你的开发体验更加顺畅。插件的好处在于,它可以帮助你实现一些复杂的功能,这样就不必自己写一坨代码。


Vue还有一个很棒的社区,可以帮助你解决一些棘手的问题,也方便了你与其他开发者交流经验,编码世界有了朋友,永远不会孤单。


3.3 谨慎:利剑具有两面性


JavaScript框架是一个非常重要的工具,就像一把利剑帮助开发者切开困难,让开发者更加简便高效地开发前端应用,也推动了前端技术的进步,并抵达成功的彼岸。



但是,请记住,刀刃朝向你,也有可能伤到自己,因此请开发者在使用JavaScript框架时要谨慎小心。


四、Node.js和前后端分离


首先,让我们回顾一下过去,那时候前后端是紧密结合在一起的,像一对结婚多年的夫妇。它们有着许多共同的爱好,但是有时它们也会产生冲突,就像夫妇间的争吵一样,前后端争吵也是不可避免。



但是,随着技术的发展,我们发现了一个新的解决方案:前后端分离。就像夫妇分居一样,前后端也可以分开,以避免冲突,Node.js就是这个分离的功臣。


Node.js可以帮助前端和后端分开,各自独立工作。前端可以专注于用户界面的开发,后端可以专注于数据的处理,就像夫妇分别在各自的工作岗位上工作一样,前后端也可以分别在各自的领域里工作。


Node.js的出现让JavaScript可以在服务器端运行,为前后端分离的架构模式提供了可能。前后端分离使开发者可以更加专注于前端应用的开发,提高开发效率。同时,Node.js的诞生也带来了诸如npm、yarn等包管理器的出现,开发者可以轻松地引入和管理第三方库。


4.1 npm:被忽视的少年


首先,让我们了解一下npm的历史。它曾经是一个年轻的少年,总是被忽视。但是随着它长大,它变得越来越强大,并且成为了Node.js开发的重要组成部分。


以前,如果我们想要安装一个库,需要手动下载,并且手动安装它。这是一件非常繁琐的事情,而且很容易出错。但是,随着npm的出现,一切都变得更简单了。只需要运行一条命令(如:npm install),就可以轻松地安装任何库。


npm还提供了一个巨大的软件仓库,其中包含了数以千计的库和工具。它就像一个图书馆,你可以随心所欲地查阅和使用。



但是,npm不仅仅是一个简单的安装工具。它还像一个管家,辅助我们管理依赖关系,并帮助我们发布代码和维护代码库。它还有许多其他功能,例如构建工具,测试工具等。因此,如果你想充分利用npm,请不要仅仅停留在它的基础功能上。


4.2 yarn:少年的替身


首先,让我们了解一下yarn的由来。它的诞生是为了解决npm的一些问题,就像是一个少年的替身,它试图取代npm并成为新的领导者。


yarn可以帮助我们快速安装依赖包,并管理依赖关系,像一个组织者可以帮助我们维护代码库,以此节省时间并提高开发效率。


yarn还提供了一个更好的版本控制系统,可以帮助我们管理依赖项的版本。如果你在多个项目中使用相同的依赖项,可以确保所有项目使用相同的版本,从而避免了版本冲突,譬如一个和平协调员。



除了管理依赖关系和解决依赖冲突外,yarn还可以帮助我们更快地进行安装,因为它可以在本地缓存安装过的依赖项。这意味着,如果你在多个项目中使用相同的依赖项,它们将不会再次下载,从而减少了安装时间。


此外,yarn支持并行安装,这意味着它可以同时安装多个依赖项,从而加快安装速度。这是一个非常有用的功能,特别是当你需要安装大量依赖项时。


yarn也有一个很棒的社区,可以帮助你解决任何问题。如果你在使用yarn时遇到问题,可以在社区中寻求帮助。这是一个非常有价值的资源,可以帮助你更快地解决问题。


五、构建工具和自动化


构建工具和自动化是现代软件开发的重要组成部分,就像给你的代码加上糖衣一样,帮助我们提高开发效率,并且可以让我们更专注于代码本身。


Grunt、Gulp、Webpack等工具的出现,使得开发者可以方便地实现代码压缩、合并、优化以及模块化等功能。而随着CI/CD的普及,自动化测试和部署变得更加便捷,大大提高了软件开发的质量和开发速度。


5.1 Grunt:猪叫的声音?


Grunt,这不是一个军人,也不是一个猪叫的声音。实际上,它是个非常酷的JavaScript任务运行器,可以帮助你自动化各种任务,如代码构建,单元测试和文件合并。它的目的是让你的工作变得更轻松、更有效率,而且不需要你不停地敲代码。



想象一下,每次你修改了一个文件,你就需要手动编译、压缩、合并、测试等等。这听起来很枯燥,对吧?但是,如果有一个工具能帮你自动完成这些任务,那该有多好!这就是Grunt的作用。


Grunt的核心思想是使用插件(plugins)来完成各种任务。有数以百计的插件可以帮助你实现从编译Sass到压缩JavaScript的各种任务。插件是通过npm安装的。Grunt有许多内置任务,例如:文件压缩,CSS预处理,JavaScript检查等。此外,还有大量第三方插件,也可以助你完成更多任务。


Grunt的配置文件是Gruntfile.js,用于定义任务和任务的配置。Grunt使用JavaScript代码配置任务,因此对JavaScript基础知识的了解是使用Grunt的必备条件。


Grunt的任务可以在命令行中通过运行以下命令执行:grunt task-name。如果你想要实时监控文件的变化,并在文件变化时自动执行任务,你可以使用grunt watch命令。


如果你是一个JavaScript开发者,那么Grunt是一个不可或缺的工具。它可以让你的工作变得更快捷、更高效,让你有更多的时间去做其他有趣的事情,比如喝咖啡、写文章或者是找对象。


5.2 Gulp:古老的咒语?


让我们来说说Gulp的名字。它的名字听起来有点像一个古老的魔法咒语,你想:“Gulp!” 然后你的代码就会变得更快、更简洁、更酷。不过,实际上Gulp并不是魔法,而是非常实用的构建工具。



Gulp的工作原理很简单:它通过创建一系列的任务,来自动完成你的工作流程。比如说,你可以创建一个任务,来自动编译你的Sass文件,或者压缩你的JavaScript文件。这样,你就不需要手动执行这些步骤了,Gulp会帮你完成。


Gulp还有一个非常酷的功能:它可以实时监控你的文件,并在你修改了文件后立即执行相应的任务。这样,你就可以实时看到更改的内容,而不需要手动重新执行。


Gulp如何使用呢?首先,你需要安装Node.js和npm,因为Gulp是基于Node.js的。其次,安装Gulp的命令行工具,只需在终端中运行以下命令即可:npm install gulp-cli -g。接下来,你需要在项目目录中创建一个package.json文件,这是npm的配置文件,用于管理项目依赖。你可以通过运行以下命令来创建一个package.json文件:npm init。然后,你需要安装Gulp,只需在项目目录中运行以下命令即可:npm install gulp--save-dev。最后,创建一个gulpfile.js文件,这是Gulp的配置文件,用于编写你的任务。


现在,你已经准备好使用Gulp了。开始编写你的任务,并运行以下命令来执行吧:gulp task-name


5.3 Webpack:订制的包包?


Webpack可以帮你把代码压缩成小而美的包,就像私人订制的收纳柜,它可以装下你所有的包包,并且把它们整齐地放在一起,使你的“奢侈”更加有序!


但是,如果你犯了错误,它就像一个恶魔般出现在你面前,吼叫着告诉你:“Error: This is error!”所以,请小心使用Webpack。


不过,只要你已经掌握了Webpack的使用方法,那么它将成为你的最佳伙伴,因为它可以为你节省大量的时间,并且让你的代码变得更加整洁。



你可以告诉Webpack:“嘿,Webpack!帮我处理图片和字体!” 然后Webpack就会用它的魔力,将它们变成小小的Data URL或文件。你不会相信,Webpack的魔力是如此的强大,它甚至可以帮你处理模块依赖。


那么,如何使用Webpack呢?首先,你需要安装它(就像是奢侈品店要先开门才能买包)。安装很简单,只需要在终端中输入:npm install webpack;然后,创建一个配置文件(就像是奢侈品店的导览图,告诉你每样包包在哪里)。配置文件一般命名为webpack.config.js,内容如下:module.exports = {entry:'./src/index.js',output: {filename:'bundle.js',path: __dirname +'/dist'}};。接下来,只需要在终端中输入打包命令:npx webpack;最后,引用打包后的文件bundle.js就可以了(背起新包包,开启一场冒险之旅)。


六、PWA和Web性能优化


在这个快节奏的数字化时代,越来越多的用户转向使用移动设备和Web应用程序。


PWA成为了一个重要的技术趋势,它的全称是“Progressive Web App”,翻译成中文就是“渐进式Web应用程序”。简单来说,PWA是一个既可以在浏览器上运行的Web应用程序,同时也可以像原生应用一样在离线时使用。它的最大优点就是可靠性,因为PWA可以像原生应用一样缓存数据和资源,这意味着它可以在离线时运行,而不会像普通的Web应用程序一样无法使用。


此外,Web性能优化也成为了开发者关注的重点。我们需要知道一个简单的事实,那就是用户喜欢快速的网站。如果你的网站速度太慢,那就会让你的用户感觉像一头正在沙漠里跑步的骆驼一样疲惫不堪,感到痛苦和沮丧,这会让他们不得不离开,去寻找新的绿洲。



所以,为了确保你的网站速度足够快,你需要采取一些优化措施。以下是一些可以提高Web应用性能的技巧:




  1. **使用CDN(内容分发网络):**CDN是一组分布在世界各地的服务器,它们可以缓存你的网站内容,并将其分发到全球各地的用户。这可以大大加快你的网站加载速度,因为用户可以从离他们最近的服务器获取内容。




  2. **压缩文件大小:**压缩你的HTML、CSS和JavaScript文件可以减少它们的大小,从而加快它们的加载速度。你可以使用像Gzip这样的压缩算法来实现这一点。




  3. **使用缓存:**缓存是一种将网站数据存储的技术。例如浏览器缓存:在响应头中设置缓存策略来控制缓存时间;以及服务器端缓存:使用Memcached或Redis等缓存服务器,以减少响应时间。这样一来,当用户再次访问你的网站时,它们可以从缓存中加载数据,而不必重新下载,大大加快你的网站加载速度。




  4. **减少HTTP请求:**有一个叫做“夹心饼干法则”的说法。这个法则认为,在一次HTTP请求中,中间的响应部分应该像夹心饼干一样短,而请求和响应头和尾应该像饼干的两端一样长。这听起来很有趣,但其实它也是有道理的,因为请求和响应头和尾中包含的信息比较少,而响应中间部分则包含了网页的实际内容,因此应该尽可能地减少其大小。你可以通过将HTML和CSS以及JavaScript文件合并成一个文件,或者通过使用CSS Sprites将多个图像合并成一个文件来减少HTTP请求的数量。




  5. **使用响应式图片:**图片是网站加载速度最慢的资源之一。为了提高网站加载速度,你可以使用响应式图片,这些图片可以根据用户的设备屏幕大小来动态地调整大小。这样一来,用户只会下载他们所需的图像大小,而不是下载整个大图像。




  6. **使用懒加载技术:**懒加载是一种延迟加载技术,它可以延迟加载页面上的图像、视频和其他资源,直到它们真正需要时才出现。这可以减少页面的初始加载时间,因为只有当用户滚动到需要加载的部分时,它们才会被加载。





你知道吗,Google Chrome浏览器可以使用一个名为“Lighthouse”的工具来检查网站的PWA和性能方面的指标。但你可能不知道的是,这个工具还有一个有趣的功能,就是可以为你的网站生成一份“独家报告”,这样你就可以像读报纸一样轻松地查看网站的PWA和性能状况了。但是,要牢记的是,优化Web应用性能是一个不断发展的过程,需要持续监测和调整以确保最佳体验。


七、Web组件和跨平台框架


Web组件和跨平台框架是现代Web开发中的两个热门话题,它们就像是现代Web开发的两座巨大城堡,吸引着无数开发者前来探索和征服。


首先,我们来谈谈Web组件。Web组件是一种现代的Web开发技术,它允许开发者将Web应用程序分解成可重用的组件,这些组件可以在不同的Web应用程序中共享和重用。比如,你可以将一个搜索框组件用于多个Web页面,而不必每次都重新编写。


Web组件的好处不仅在于可重用性,还在于它们的灵活性。你可以根据需要自定义Web组件,为你的Web应用程序添加新的功能和样式。



但是,Web组件并不是“银弹”,它们在某些方面仍然有限制。比如,Web组件难以处理动态数据,因为它们是静态的。此外,Web组件也不是完美的跨平台解决方案,因为它们可能无法兼容不同的Web浏览器和设备。


这就引出了我们的下一个话题:跨平台框架。跨平台框架是一种可以在多个平台上运行的软件框架,包括Web、移动和桌面应用程序。它们允许开发者编写一次代码,然后在不同的平台上运行,无需进行任何额外的修改。


跨平台框架的好处显而易见:它们可以大大减少开发时间和开发成本。但是,跨平台框架并非完美无缺。它们可能会受到不同平台的限制,从而无法充分利用每个平台的功能和性能。此外,跨平台框架还可能会导致性能问题和代码质量问题。


现在,我们来看看如何将这两种技术结合起来。使用Web组件和跨平台框架可以让你搭建你的虚拟王国,充分利用Web组件的可重用性和灵活性,同时充分利用跨平台框架的跨平台能力和效率。



当然,这并不是说将Web组件和跨平台框架混合在一起就是万无一失的。你需要仔细考虑你的应用场景,确保使用这两种技术的方式是最优的。


比如,你可以使用Web组件来构建你的用户界面,然后使用跨平台框架来将Web应用程序转换为移动应用程序。这样,你就可以在多个平台上运行相同的代码,而且用户体验也会更加一致。


或者,你可以使用跨平台框架来编写你的应用程序逻辑,然后使用Web组件来定制你的用户界面。这样,你可以在不同的Web应用程序中重用你的用户界面,而且你的应用程序逻辑也可以在多个平台上运行。


再者,你也可以将这两种技术都使用在同一个应用程序中。这样,你可以充分利用Web组件的可重用性和灵活性,同时充分利用跨平台框架的跨平台能力和效率。只要你能合理地使用这些技术,就可以打造出更好的Web应用程序。


Web组件和跨平台框架都是非常有前途的技术,它们可以为现代Web开发带来很多好处,为我们带来更加灵活、高效和强大的Web开发工具和平台。无论是Web组件还是跨平台框架,它们都是我们构建虚拟王国的重要基石。


八、前端安全问题


在当今数字化时代,前端安全已成为互联网世界中的重要一环。不管是个人用户,还是企业机构,前端安全都需要被高度重视。尽管我们已经发展出了各种各样的安全技术和防御手段,但是前端安全问题仍然是一个不断增长的挑战。


8.1 XSS攻击:你的网站很容易被攻击


你听说过XSS攻击吗?这种攻击方式是通过篡改网页的HTML并在用户浏览器中注入恶意代码的一种攻击方式。这些恶意代码通常是JavaScript脚本,它们可以被用来窃取用户的敏感信息,如用户名、密码、银行账户信息等等。


如果你的网站存在XSS漏洞,那么恶意攻击者就可以在你的网站上注入一些不良代码,这些代码可能会窃取用户的登录凭证或者其他敏感信息。所以,尽管你的网站已经被SSL加密保护,你的用户仍然面临着被XSS攻击的风险。



如何防御XSS攻击呢?其实非常简单,你只需要在所有的输入框中过滤掉所有的HTML标签和JavaScript脚本即可。但是,如果你认为这么做会影响用户体验,那么你可以考虑使用HTML的特殊字符转义功能来替换这些标签和脚本。


8.2 CSRF攻击:请勿相信恶意链接


现在让我们来谈谈CSRF攻击。这种攻击方式是通过篡改用户的HTTP请求来伪造用户的身份,从而进行一些非法的操作。这种攻击方式通常是通过欺骗用户点击一个恶意链接来实现的。一旦用户点击了这个链接,攻击者就可以获得用户的凭证,然后模拟用户的请求,从而执行一些非法的操作。


假设,你的网站有一个删除账户的功能,攻击者就可以利用CSRF攻击来让用户误删除自己的账户。这听起来非常可怕,但是不要担心,我们可以通过一些简单的方法来防御这种攻击。


首先,我们可以在所有的表单提交中添加一个随机的Token值。这个Token值可以通过后台生成,然后在前端将其嵌入到表单中。当用户提交表单时,我们可以检查这个Token值是否匹配,如果不匹配,则拒绝这个请求。这样就可以简单的避免CSRF攻击了。


8.3 CSP策略:请勿允许不信任的资源


CSP策略是一种非常有用的前端安全措施。CSP策略可以帮助我们限制网页中可加载的资源,从而减少被攻击的风险。例如,我们可以限制只允许加载来自指定域名的JavaScript文件,这样就可以避免恶意代码的注入。


但是,如果你不小心将不信任的资源允许加载到你的网页中,那么你的网站就可能面临被攻击的风险。假设你的网站允许用户上传文件,并在网页中显示这些文件,如果你没有限制文件的类型和内容,那么攻击者就可能上传恶意文件,并在用户浏览器中注入恶意代码。



所以,如果你想保证你的网站的安全性,那么你应该始终谨慎地过滤用户上传的文件,只允许加载来自可信任来源的资源。


我们可以认识到,前端安全是一项非常重要的技术挑战。如果你是一位前端开发人员,那么应该始终将前端安全作为开发过程中的一个重要考虑因素。只有这样,我们才能够为用户提供安全可靠的Web服务。


九、前端工程师的多元化技能


作为一名前端工程师,一定是个充满多元化技能的大神。不仅仅要会写代码,还要会与设计师沟通,管理版本控制,解决兼容性,甚至还要有点艺术细胞。





  1. **代码技能:**前端工程师最基本的技能,也是最重要的技能。不仅需要掌握 HTML、CSS、JavaScript,还需要掌握一些前端框架和库,比如 React、Vue、Angular 等。当然,这些都不是问题,对于一名优秀的前端工程师来说,这只是小菜一碟。




  2. **与设计师沟通:**设计师们总是有各种奇怪的想法,然后她们会告诉你:“我要实现这个效果,你帮我写一下”。但是,很快会发现这个效果并不现实,于是你需要与设计师进行沟通,告诉她们这个效果无法实现。当然,你不能用技术术语来向她们解释,否则她们会摆出一副“我听不懂”的表情。所以,你需要用她们喜欢听的语言,比如“我理解你的设计需求,并深刻认识到其对于网站效果的重要性。不过,由于技术和浏览器的限制,我们需要寻找其他的可行方案来实现类似的效果,以保证网站的性能和可访问性,我会尽最大的努力提供最佳的解决方案。”




  3. **管理版本控制:**代码管理是一个很重要的问题,特别是当你和其他人合作的时候。你需要使用Git进行版本控制,这样才能确保代码的稳定性和可靠性。当然,你也需要了解一些Git的命令,比如 commit、push、pull 等等。不过,如果你不小心把代码弄挂了,那也不用担心,只要跟团队的其他成员说“我不小心把代码弄挂了”,他们就会告诉你怎么做了。




  4. **解决兼容性:**不同的浏览器之间有很多不兼容,而前端工程师需要解决这些问题。比如,IE浏览器总是出现各种奇怪的bug,但是你不能告诉用户:“你用的是IE,这不是我的问题”。相反,你需要找到问题的根源,然后解决它。这是一个非常重要的技能,因为它涉及到用户体验和网站的稳定性。




  5. **有点艺术细胞:**前端工程师不仅仅是一个代码的机器,还需要有一点艺术细胞。毕竟,好的界面设计是网站的关键之一。所以需要了解一些基本的设计原则,比如颜色搭配、排版等等。当然并不需要成为一个设计师,但是需要知道如何运用这些原则来改进网站的设计。




  6. **学习新技能:**前端工程师是一个不断学习的过程。每天都有新的技术和框架出现,并且要不断学习并掌握这些技能。但是,并不需要成为一个全栈工程师,只要掌握所需要的技能,然后专注于自己的领域即可。当然,这也意味着要学会如何筛选有用的信息,因为不可能学习完所有的技术和框架。




  7. **解决问题:**前端工程师是一个解决问题的岗位。当网站出现问题时,需要迅速找到问题的根源,并解决它。但是,也不一定要独自解决所有的问题,可以向同事寻求帮助,或者参加一些开发者社区来寻找解决方案。最终要记住的是,解决问题是前端工程师最重要的技能之一。




  8. **与团队合作:**前端工程师需要和设计师、后端工程师、测试人员等等进行合作,以确保网站的成功。在与团队合作中,要学会如何与不同的人合作,并且尽力避免出现冲突。





前端工程师需要掌握很多不同的技能,但这并不意味着要成为一个万能的人。相反,只需要专注于自己的领域在不断地技术学习过程中成长。


十、AI与前端技术结合


回顾过去,畅想未来,立足当下,来讲个故事吧。


在一个遥远的星球上,有一个叫做前端技术的孤独王国。这个王国的居民们都是非常优秀的程序员,他们用HTML、CSS和JavaScript这三种神奇的武器来构建网站,为用户带来无尽的愉悦。然而,这个王国有一个问题,那就是他们一直无法征服一个名为AI的神秘国度。


终于有一天,一个勇敢的前端战士——HTML骑士,决定向AI国度发起挑战。他带着两个小伙伴:CSS猎人和JavaScript法师,踏上了一段充满挑战的探险之旅。



他们沿着神秘的网络海洋航行,一路上遇到了各种令人捧腹大笑的趣事。先是在一个叫做布局的洲际,他们被一群叫做“浮动”的怪兽困扰,CSS猎人拔出了他的弹性盒子弓箭,一箭穿心,解决了怪兽。接下来,他们来到了一个充满奇特生物的动画之地,JavaScript法师用他的神奇魔法,让这些生物如同表演马戏团一般,给他们带来了一场场精彩绝伦的表演。


然后,他们终于来到了AI国度的边境。在那里,他们遇到了一个脾气古怪的巨人,他叫做机器学习。这个巨人用一种叫做数学的强大力量来支配着这片土地。HTML骑士认为,要征服这个国度,就必须挑战巨人,并将他的力量与前端技术融合。



于是,在他们与巨人大战三百回合后,JavaScript法师从中意外领悟了神奇魔法,召唤出一个叫做TensorFlow.js的强大法宝。这个法宝让前端技术也能够掌握机器学习的力量。HTML骑士和CSS猎人纷纷表示赞叹,他们觉得自己终于找到了一种将AI与前端技术结合的方法。


在这之后,他们三人一起用TensorFlow.js建立了一个名为“智能前端”的新城堡。这座城堡里,前端技术与AI融合得天衣无缝,为用户带来前所未有的体验。


城堡的大门上,HTML骑士精心设计了一个智能问答系统。这个系统可以回答用户关于前端技术的各种问题,让新手程序员们感叹不已。而这一切,都得益于TensorFlow.js和机器学习的神奇力量。


城堡的内部,CSS猎人则利用AI技术打造了一套全新的自适应布局。这套布局能够根据用户的喜好和设备自动调整,让每个访问者都能享受到最佳的浏览体验。他还研发了一种名为“智能配色”的神奇法术,能够根据用户的喜好自动调整网页的颜色搭配,让网站变得更加美观大方。



而在城堡的核心区域,JavaScript法师则运用AI技术开发了一系列令人惊叹的交互功能。比如,他创造了一种可以根据用户行为自动优化的推荐算法,将用户感兴趣的内容精准推送。此外,他还设计了一款智能聊天机器人,可以与用户进行即时互动,解答他们的疑问。


在“智能前端”城堡的建设过程中,他们三人不仅发挥出了各自的特长,还不断地学习AI技术,将其与前端技术相互融合。这让他们的作品充满了趣味与智慧,吸引了无数程序员和用户前来参观。


在这段充满挑战的探险之旅中,HTML骑士、CSS猎人和JavaScript法师用他们的智慧和勇气,成功地将AI技术引入前端领域。他们的故事传遍了整个网络世界,成为了程序员们争相传颂的佳话。



如今,前端技术和AI的结合已经成为了一种趋势,越来越多的开发者开始探索这个领域的无限可能。而在这个探索过程中,他们总是能从HTML骑士、CSS猎人和JavaScript法师的故事中汲取勇气与智慧,勇往直前,为未来的网络世界描绘出一个更加美好、充满创意和智慧的蓝图。


有人说,前端技术与AI的结合就像一场狂欢。程序员们欢笑着跳动,发挥着自己的想象力,创造出一个又一个令人叹为观止的作品。在这场狂欢中,每个人都是舞者,每个人都是艺术家,每个人都在为这个美丽的网络世界贡献着自己的力量。


如同那个遥远的星球上,那个欢呼雀跃的前端王国,如今我们所生活的这个网络世界也充满了欢声笑语。而在这片欢乐的土地上,那些勇敢的前端战士们正携手AI,共同书写着属于他们的传奇!



随着技术的不断发展,我们相信前端技术与AI的结合将会走得更远,创造出更多不可思议的奇迹。也许有一天,我们的网络世界将变得如同童话般美好,充满智慧的光辉。而在那个时候,我们将不禁想起那个勇敢的HTML骑士、CSS猎人和JavaScript法师,怀念他们当年那段充满挑战的探险之旅,为他们的勇气与智慧而感慨不已。


在探险的道路上,我们将一路欢笑并肩前行,勇敢地追求那个梦寐以求的未来。也许在某个不经意的瞬间,我们会发现,那个童话般的前端王国,其实就在我们心中,等待着我们去探索、去发现、去唤醒它,让它绽放出最耀眼的光芒。


后记


前端技术的演进从未停歇,仍然充满了机遇与挑战……


让我们一起期待下一个十年,见证前端技术的更多精彩!


作者:京东云开发者
来源:juejin.cn/post/7220164534315548731
收起阅读 »

藏在微信里的温度,无障碍开发框架分享

web
👉 腾小云导读 现我国现有4471w视障/听障人士,60岁及以上人群达2.6亿规模。微信作为国民级应用,实现无障碍迫在眉睫。为了帮助他们更好地使用微信 App,Android微信完成了适老化及无障碍改造。本文主要介绍Android 微信开发团队根据适老化及无障...
继续阅读 »

图片


图片


👉 腾小云导读


现我国现有4471w视障/听障人士,60岁及以上人群达2.6亿规模。微信作为国民级应用,实现无障碍迫在眉睫。为了帮助他们更好地使用微信 App,Android微信完成了适老化及无障碍改造。本文主要介绍Android 微信开发团队根据适老化及无障碍需求,完成的一个协助业务侧进行无障碍功能开发的框架。希望能给广大开发爱好者带来帮助和启发!




👉 看目录,点收藏


1 无障碍需求框架背景


1.1 无障碍需求


1.2 框架简介


2 无障碍开发基础知识


2.1 读屏软件识别View原理


2.2 读屏软件后的事件分发原理


3 框架实现的整体流程和执行原理


3.1 整体流程


3.2 执行原理


4 核心说明:全局热区补足机制


4.1 背景说明


4.2 具体实现


4.3 额外说明


5 走查工具


6 总结


01、无障碍需求框架


目前,业界已经有共识性的无障碍开发守则。例如 Web Content Accessibility Guidelines (WCAG) 2.0,它是由互联网的主要国际标准组织万维网联盟 (W3C) 的Web可访问性倡议 (WAI) 发布的一系列 Web 可访问性指南的一部分。


此外,WAI-ARIA(可访问的富Internet应用程序套件)是由万维网联盟(W3C)发布的一项关于 A11 Y技术应用规范。该规范定义了一种使残障人士更易于访问 Web 内容和 Web 应用程序的方法,增加 HTML、JavaScript 和相关技术开发的网站动态内容以及用户界面组件的可访问性。


目前,Android没有官方统一、方便的框架,官方提供的原生api并不是特别好用,所以微信团队对其进行参考,开发了一个无障碍框架,基于原生的api进行了再封装,将繁琐的无障碍适配逻辑封装在底层,以声明式接口的形式,让上层业务能以更简便更解耦的代码,完成无障碍的适配。接下来我们进行分享:


1.1无障碍需求


本框架主要具备以下特性:



  • 可感知性 :包括大字体适配,颜色对比度等 。

  • 可操作性 :主要是过小热区的放大,提高老年人/残疾人的交互体验 。

  • 可理解性 :微信应提供读屏文案等信息,帮助盲人在开启 Talkback 等读屏软件的情况下,正常使用微信。


下面给出一些较为典型的需求:



  • 需求1:过小热区的放大


需求是要求微信内的所有可交互控件,可点击范围不得低于 44dp * 44dp。


大小不合规的控件,如果一个个进行排查、布局修改。工程量庞大。



  • 需求2:响应区域会随无障碍开关发生变化


图片


该 Item 由一个 SwitchButton + TextView 组成。


开启 Talkback 时,整个 Item 识别为一个焦点,选中双击是触发点击 switch的逻辑。在无障碍模式下,选中双击是直接触发相应控件的 Click 事件。但是在不开 Talkback 的情况下点击 Item 又无需响应,只响应 SwitchButton 。也就是点击区域会随 Talkback 开关发生变化。


实现可能是:在 ItemClick 中进行 if 判断。但这样写侵入性高,难维护。



  • 需求3:读屏文案由其他的控件的值组合


图片


选中头像,读屏文案:腾讯行政的头像,有 2 条未读消息。需要读出列表中其他关联内容,这种只能把适配代码侵入到 Adapter中。


1.2 框架简介


框架将不同的无障碍需求的实现进行封装,抽象成不同的规则。


业务侧可以将一个页面/业务的无障碍需求,在一个配置类里使用规则表达出来,再由框架进行处理。实现相应的效果。


class ChatAccessibility(activity: AppCompatActivity) :  
BaseAccessibilityConfig(activity) {
  override fun initConfig() {
        // 设置 contentDesc
   view(rootId,viewId).desc(R.string.send_smiley)
        // ...
  }
}

框架基类 BaseAccessibilityConfig 提供了一系列用于表达规则的 api,包括但不限于如下功能:




  • 通过配置统一设置 contentDescription




  • 支持把多个 View 组合成一体进行读屏




  • 通过配置禁用某个View被 Talkback 聚焦的能力




  • 支持按指定顺序进行读屏,支持局部控制 Talkback 聚焦顺序




  • 支持设定在 Activity 启动后的第一个读屏控件




  • 支持对某个父 View 的 disableChildren 功能




  • 在某个 View 满足条件时,对其进行读屏,但不聚焦




  • 在某个 View 满足条件时,读出提前设定的 string,但不聚焦




  • 全局热区宽高补齐至 44dp,并提供自定义热区放大/禁用热区放大的功能 ...




02、无障碍开发基础知识


在深入了解框架的设计前,先来介绍一些无障碍功能开发的基础知识。


2.1 基础知识1:读屏软件识别 View 原理


图片


读屏软件无法直接识别到View,只能识别到View提供的虚拟节点「Node」,View 和虚拟节点一般是一一对应的。当页面内容发生变化,比如 View 被设值,或者发生滚动等情况,View 会向无障碍系统发送一个事件,通知系统。


然后系统就回头向 View 索取节点,组成页面更新后新的节点树,而 「节点树 和 ViewTree 是一一对应的」。此时读屏软件拿到的就是新的内容了。


2.2 基础知识2:读屏软件后的事件分发流程


分为上下两部分:读屏软件拦截处理行为、读屏软件接受事件。


图片


流程如下:




  • 读屏软件拦截用户 Touch 事件,根据事件的坐标去定位到目标节点。




  • 将 Touch 事件解释为节点行为,这里以触摸选中为例,那么就是聚焦行为。




  • 读屏软件通过该节点向无障碍系统发送,无障碍系统又转发给View(聚焦产生的绿框就是在View的内部处理里去绘制的)。




  • 生成新的虚拟节点并提供给读屏软件后,读屏软件组合信息,通过 TTS 语音引擎的 api 读出。




读屏软件展示给用户的所有信息,全部来自虚拟节点。可以在节点生成的过程中,修改节点的信息,所以这里是一个绝佳的**「信息自定义」**的地方。


采用将所有的 View 都 「Wrap 一层 AccessibilityDelegate」 的方式,「在 onInitializeAccessibilityNodeInfo 方法中修改节点信息」。


03、框架实现整体流程与执行原理


3.1 整体流程


图片




  1. 业务侧实现规则配置类,编写的规则会进入配置池。




  2. 框架在View生成节点给系统的时候进行拦截 「(onInitializeAccessibilityNodeInfo)」




  3. 在配置池中寻找匹配的规则。




  4. 根据匹配的规则对节点进行修改。




  5. 最后生成的节点就会由系统交由给读屏软件进行读屏。




3.2 执行原理


图片


核心原理:采用基于责任链的流水线来处理。整体流程主要分为两部分:




  • View 预处理责任链(图示左边):执行预出来操作,如异步生成缓存、View标记等;




  • 节点处理责任链(图示右边):节点处理的同时会同步查找规则进行设置。




接下来主要简单介绍下框架的一个核心功能的实现:「全局热区补足机制」 (位于框架流程中的预处理责任链中的一环)。


04、核心说明:全局热区补足机制


4.1 背景说明



  • 需求说明


过小热区放大,即微信内的所有可交互控件可点击范围不得低于 44dp * 44dp,像一些大小不合规的控件,如果一个个进行排查、布局修改,工程量太庞大。还有热区其他一些需求 etc。



  • 问题难点


一般会选择直接修改 padding,有些甚至需要改动相应布局,但这样的改动工作量太大且容易影响原来视图布局。



  • 解决方案


需要一个全局的热区补足机制,将过小热区补足至规范。


4.2 具体实现


「创建 View 的统一入口」 去设置 TouchDelegate 代理,由父 View 作为TouchDelegate 的承载 View 去代理 Touch 事件,这里有几个问题需要解决:




  • 如何找到合适的承载View




  • 热区及时更新




  • 性能优化




  • 读屏模式下的热区扩大




下面我们分别展开讲。




  • 重点问题1:如何找到合适的承载 View




从目标 View 向上冒泡,找到一个合适的父 View。那么需要 「冒泡终止条件」。 首先条件一肯定是 「足够大」。当前 View 够大了就没必要再往上冒了。


图片


但是这样会存在问题:子 View 的 Click优先级高于父View的TouchDelegate。事件派发机制:


从父 View 往子 View 派发,从子 View 向上处理。View 的事件处理顺序是先 OnTouchListener,然后是 TouchDelegate,再是Click、LongClick。


所以会导致下图的情况:


图片


目前进行了折中处理,相比上图,显然是下图的放大后的体验更佳:


图片


同时加入了条件二:「该承载 View 是 Clickable、LongClickable」。最终方案流程确定如下:


图片




  • 重点问题2:热区及时更新




背景: 承载 View 的 TouchDelegate 需要的参数包含一个 Rect,也就是对扩大的热区进行响应。


问题: 这个矩阵是提前传入,且和 小 View 没有直接的关系。如果小 View 的布局发生变动,会导致扩大后热区没有及时跟上变化。导致热区错位。


解决方案: 在 小 View 的 onLayoutChange 中重新进行一遍 ·View 扩大方案· 的处理。同时为了防止 onLayoutChange  执行过于频繁,将 onLayoutChange 包装成 View 的一个事件。如果短时间内多次 onLayoutChange  ,则只在最后一次 onLayoutChange 的时候进行  「View扩大方案」处理。



  • 重点问题3:性能优化


背景 :最初的 View 扩大方案执行时机是在创建 View 的统一入口,也就是在 LayoutInflate 的 onCreateView 中同步执行,每个 View 都得执行。


问题:由于 View 数量较为庞大,所以存在较大的性能隐患。


解决方案:采用了异步方案并同时对 View 处理任务进行收拢。将执行时机提前到 LayoutInflate.inflate 并异步处理,在异步任务中去遍历该 inflate 的根 View的所有子 View。尽量不去阻塞主线程的运行。




  • 重点问题4:读屏模式下的热区扩大




通过上面的实现,点击热区确实是扩大了。但是在读屏模式下选中的时候,选中的框并没有扩大。那么首先需要知道,选中时的框是以什么作为 Bound。


绿框的绘制核心逻辑位于 ViewRootImpl 中的一个 drawAccessibilityFocusedDrawableIfNeeded(),该方法的调用时机是用户触摸选中某个View后,传递到 ViewRootImpl 时进行调用,也就是读屏选中的绿框是由系统绘制的,而不是由读屏软件绘制的。从源码中能够得知的是,绿框的Bound 根据是否有虚拟节点,分为两种情况:


private void drawAccessibilityFocusedDrawableIfNeeded(Canvas canvas) {  
    final Rect bounds = mAttachInfo.mTmpInvalRect;
    if (getAccessibilityFocusedRect(bounds)) {
        final Drawable drawable = getAccessibilityFocusedDrawable();
        if (drawable != null) {
            drawable.setBounds(bounds);
            drawable.draw(canvas);
        }
    } else if (mAttachInfo.mAccessibilityFocusDrawable != null) {
        mAttachInfo.mAccessibilityFocusDrawable.setBounds(0000);
    }
}

private boolean getAccessibilityFocusedRect(Rect bounds) {
    ...
    final AccessibilityNodeProvider provider = host.getAccessibilityNodeProvider();
    if (provider == null) {
        host.getBoundsOnScreen(bounds, true);
    } else if (mAccessibilityFocusedVirtualView != null) {
        mAccessibilityFocusedVirtualView.getBoundsInScreen(bounds);
    } else {
        return false;
    }
  ...
    return !bounds.isEmpty();
}

经过跟踪源码发现,这是因为 「绿框的绘制」 是根据 View.getBoundInScreen 获取的矩阵来做到的。而 TouchDelegate 的设置无法改变 View.getBoundInScreen 获取到的矩阵。在使用虚拟节点的情况下,才会使用虚拟节点的Bound进行绘制。


对于这个问题,我们的解决思路是:




  • 对每个 View 设置自定义的 AccessibilityDelegate, 并实现其中的 getAccessibilityNodeProvider 方法。




  • 如果判断 View 需要扩大,在 getAccessibilityNodeProvider 中返回自定义的 Provider。




  • 在自定义的 Provider 中,计算 View 的扩大后的矩阵在屏幕上的位置。




  • 将矩阵设置给虚拟节点,并返回给系统。




4.3 额外说明



  • 如何匹配规则与View?


框架将配置池按 Activity 划分,极大减少冲突概率,同时减少配置池大小,加快查找规则的速度,提供 layoutId + viewId,rootId + viewId 两种形式的 View 定位机制。由两个 Id 确定一个 View,减少冲突。



  • 查找规则时间长可能导致的主线程卡顿?


由于查找规则的时机是在生成节点,是由系统触发且无法异步。在查找规则的过程中,使用预处理的时候提前生成的缓存进行查找,尽可能减少耗时。


05、走查工具


5.1 背景


当完成无障碍需求的开发后,需进行验证。在验证过程中发现开启验证效率低下,需开启读屏软件后,逐个元素验证。


5.1.1 解决方案与原理


基于无障碍服务(AccessibilityService)开发、集成了在不开启 Talkback 的情况下能展现读屏区域一个无障碍功能走查工具,无需开启 Talkback 逐个手动触摸,就能高效检查无障碍适配情况。


图片


实现原理如下:




  • 自定义实现一个 AccessibilityService 用于获取到当前活跃窗口的根节点。




  • 每隔 0.5s 进行一次节点的获取:从当前活跃窗口的根节点遍历所有的节点,逐个进行判断是否会被聚焦。




  • 对通过允许聚焦的节点进行信息收集,在一次遍历完成后通知到 DrawService。




  • 提前在window中添加一个 View 用于绘制信息,由 DrawService 进行绘制。




5.2 具体实现


关键实现:如何判断一个节点能否被聚焦,即需理解 Talkback 是如何聚焦,流程如下:


1、如果是支持 WebView 中 Html 无障碍,特殊判断。


2、如果不可见,则不聚焦。


3、判断是否是画中画,像下图的红框这种就是画中画,如果是画中画,这个就是焦点。


图片


4、该节点是否和 window 边界重合等大。对于这种和 window 等大的节点,Talkback 选择不做聚焦。


5、检查该节点是否 clickable/longClickable/focusable 或者是列表的“会说话的” 顶层视图(满足->6 不满足->7)列表(ListView/RecycleView)的顶层视图例子如下:


图片


但是聚焦的前提是“会说话的”。“会说话的”包括以下几个条件:




  • HasText:包括 contentDescription、text、hintText(包括 Button 的 Text)。




  • hasStateDescription:包括 CheckBox 的已选未选状态、进度条的进度状态等。




  • hasNonActionableSpeakingChildren:含有无法聚焦、点击但是 HasText 的子 View(如上图通讯录中的 “新的朋友” TextView,就是无法聚焦、点击但是 HasText 的子 View)。




6、基本上满足了步骤5就可以视为可聚焦了,但是有一些View仅仅是 Focusable,但是却 ”什么话都没得说“ ,对于这种 View 应该是要排除的。故按如下步骤做判断:只要是没有子节点的 focusable/clickable/longclickable 的 View,全部聚焦 、“会说话的” 全部聚焦 6.3 剩下的就不聚焦了(“不会说话”、“有子节点”)。


7、能到这一步,说明步骤 5 不满足,即该节点是普通的不可聚焦的 View。但是防止错过一些没有点击事件的 TextView 之类的需要聚焦,需要再最后做一步判断(这一步也是啥为了保证所有的信息都可以不遗漏);如果没有可聚焦父节点,但仍然 hasText 或 hasStateDescription,聚集该节点。


8、一路闯关到这的 View,就终于逃离 TalkBack 的聚焦了。


06、总结


为了帮助老年人、视障/听障人群等更好地使用微信 App,Android微信完成了适老化及无障碍改造如上。本文主要介绍 Android 微信开发团队根据适老化及无障碍需求,完成的一个协助业务侧进行无障碍功能开发的框架。我们在介绍了无障碍开发所涉及的2大重点基础知识(读屏识别View原理和读屏软件后的事件分发原理)之后,为各位展开回顾了我们框架具体细节和方法。


以上是本次分享全部内容,欢迎大家在评论区分享交流。如果觉得内容有用,欢迎转发~


-End-


原创作者|许怀鑫


技术责编|许怀鑫


图片


现我国现有4471w视障/听障人士,60岁及以上人群达到2.6亿规模。信息无障碍(Web Accessibility)的概念在近几年受到关注。 信息无障碍是指通过信息化手段弥补身体机能、所处环境等存在的差异,使任何人(无论是健全人还是残疾人、无论是年轻人还是老年人)都能平等、方便、安全地获取、交互、使用信息。微信、QQ、腾讯新闻和腾讯地图等应用加适老化元素,配备为老人而设的“关怀模式”;搜狗输入法推出为视障群体量身打造的“保益盲人输入法”......


当说到无障碍,大家第一反应是弱势群体。实际上,无障碍是适用于全民的。每个人都可能有遇障时刻。当你手提重物或受伤时,你可能会选择乘坐无障碍电梯;当你处在嘈杂的环境下看视频时,你可能需要通过字幕获取信息……每个人都是无障碍环境的受益者,视障、听障人群、含残疾人、老年人是信息无障碍的重点受益群体。


事件分享:你还见到过哪些让你眼前一亮的信息无障碍案例?


脑洞时刻:程序员还可以为信息无障碍做些什么?


欢迎在公众号评论区聊一聊你的看法。在4月10日前将你的评论记录截图,发送给腾讯云开发者公众号后台,可领取腾讯云「开发者春季限定红包封面」一个,数量有限先到先得😄。我们还将选取点赞量最高的1位朋友,送出腾讯QQ公仔1个。4月10日中午12点开奖。快邀请你的开发者朋友们一起来参与吧!


回复「微信」,领取更多微信的技术case和论文资源


图片


阅读原文


作者:腾讯云开发者
来源:juejin.cn/post/7218015602769133625
收起阅读 »

接地气的前端代码规范

web
背景: 技术栈为 vue全家桶 更细节、更符合公司现状的一些约定、规范 优先级 A:必要的 这些规则会帮你规避错误,减少可能会产生的缺陷或者性能隐患。 JavaScript 在使用变量前,必须进行判空,必要时还需进行类型判断;若是对象,建议使用可选链 我们...
继续阅读 »

背景:



  • 技术栈为 vue全家桶

  • 更细节、更符合公司现状的一些约定、规范


优先级 A:必要的


这些规则会帮你规避错误,减少可能会产生的缺陷或者性能隐患。


JavaScript


在使用变量前,必须进行判空,必要时还需进行类型判断;若是对象,建议使用可选链


我们经常会遇到这样的情况:在定义变量时未赋默认值;根据接口返回值进行赋值,因数据等问题导致字段有缺失。若我们在使用这些变量时,未进行必要的判断,理所当然地去使用变量的属性、方法等,轻则导致console上出现一些error信息,再则出现功能无法正确运行,重则直接出现整个系统白屏!
注:在<template>中使用的变量,出现undefined而未进行判空,会导致系统白屏。目前vue@2.6.x还未支持<template>中使用可选链,后续会考虑是否升级到2.7.x。


// 反例
let a, b, c;

a = res.data.data.a;

b = JSON.parse(a);

c = b.includes("1");

// 正例
let a, b, c;

a = res?.data?.data?.a;

if (!!a) {
b = JSON.parse(a);
}

if (Array.isArray(b)) {
c = b.includes("1");
}

必须对接口报错进行处理,至少需进行错误提示


目前系统中对接口错误状态码、错误提示的处理良莠不齐,导致部分接口一旦出错,页面无任何反应,对用户很不友好。



  • 针对接口出现一些错误状态码(如status: 500),后续会在组件库的interceptor中对所有axios进行统一处理,给出错误提示,并往外抛。各个业务层可以对组件库抛出的信息进行进一步的处理,如关闭loading,回退处理等等。

  • 针对接口status: 200``success: false,需要在各个调用接口的地方给出提示语。优先以后端返回为准,否则提示语默认为:系统异常,请联系管理员。

  • 针对接口返回blob文件或其他可能会出现异常的情况,建议使用try...catch来捕获异常。


// 反例
function fetchUser (userId) {
return fetch(`/xxx/xxx/${userId}`);
}

// Promise的实现
function updateUserInfo (userId) {
fetchUser(userId).then(res => {
if (res.data.success) {
doSuccessAction();
}
})
}

// async/await的实现
async function updateUserInfo (userId) {
const res = await fetchUser(userId);
if (res.data.success) {
doSuccessAction();
}
}

// 正例
function fetchUser (userId) {
return fetch(`/xxx/xxx/${userId}`);
}

// Promise的实现
function updateUserInfo (userId) {
fetchUser(userId).then(res => {
if (res.data.success) {
doSuccessAction();
} else {
const errorInfo = res.data.error || "系统异常,请联系管理员";
this.$Message.error(errorInfo);
}
}).catch (error => {
this.$Message.error(error);
});
}

// async/await的实现
async function updateUserInfo (userId) {
try {
const res = await fetchUser(userId);
if (res.data.success) {
doSuccessAction();
} else {
const errorInfo = res.data.error || "系统异常,请联系管理员";
this.$Message.error(errorInfo);
}
} catch (error) {
this.$Message.error(error);
}
}

禁止频繁调用同一个接口,包括循环、监听、或未做节流防抖的按钮等情况下调用接口


频繁调用接口,会产生很多问题,列举如下:



  • 接口耗时长,页面白屏,用户体验不好

  • 对后端服务器造成一定压力

  • 同一个接口,在短时间内同时发出,因为网络延迟等因素,会造成接口返回不一定按照接口发起的顺序,导致最终结果与预期不符


目前代码中会有这些常见情况导致频繁调用,以下给出对应的解决方法:



  • 循环中调用:进行接口聚合,比如原先是每一次给后端一个key,后端返回对应的枚举值,可以改为将这些key组合成数组,一次性请求,获取所有对应的枚举值。

  • 监听中调用:这种情况最大的问题是对watch或者computed的触发场景或次数未知。这个没有统一的解法,需要具体情况具体分析。

  • 按钮中调用:点击按钮后调用接口,是一个特别常见的场景,一般情况下我们不会主动去在接口点击后频繁调用同一个接口,但是要防止用户频繁点击按钮。我们需要在按钮点击后,进入loading状态,或者加上节流或防抖,以避免上述用户操作导致的问题。

  • 表单中调用:在input、select、cascader组件的on-change 事件中调用接口,可以改为在输入框失焦,下拉面板收起时触发,即on-blur、on-open-change、visible-change。


Prop 定义应该尽量详尽,至少指定类型


细致的 prop 定义有两个好处:



  • 它们写明了组件的 API,所以很容易看懂组件的用法;

  • 在开发环境下,如果向一个组件提供格式不正确的 prop,Vue 将会告警,以帮助你捕获潜在的错误来源。


// 这样做只有开发原型系统时可以接受
props: ['status']

props: {
status: String
}

// 更好的做法!
props: {
status: {
type: String,
required: true,
validator: function (value) {
return [
'syncing',
'synced',
'version-conflict',
'error'
].indexOf(value) !== -1
}
}
}

拒绝硬编码值;拒绝魔法数字和字符串;


硬编码值和魔法数字和字符串在编程中往往代表着不好的编码习惯,缺点也很明显:



  1. 值的意义难以了解。

  2. 值需要变动时,需要频繁变更,而且可能要改不只一个地方。


// 反例
for (let i = 0; i < 10; i++) {
//...
}

// 正例
const numApples = 10;
for (let i = 0; i < numApples; i++) {
//...
}


// 反例
<template>
<section class="demo-page">
<span v-if="status === '0'">待付款</span>
<span v-if="status === '1'">待发货</span>
<span v-if="status === '2'">待收货</span>
<span v-if="status === '3'">待评价</span>
</section>
</template>
<script>
export default {
data() {
return {
status: "0",
};
},
...
}
</script>

// 正例
<template>
<section class="demo-page">
<span>{{ statusMap[status] }}</span>
</section>
</template>
<script>
import { getStatusMapApi } from "@/api/index";
export default {
data() {
return {
status: "0",
statusMap:{}
};
},
mounted() {
getStatusMapApi().then(res => {
/* {
"0": 待付款,
"1": 待发货,
"2": 待收货,
"3": 待评价,
} */
this.statusMap = ...
})
}
}
</script>

禁止使用refs.children[i]获取子组件,建议用ref属性;不建议使用ref直接调用子组件的api,以保持组件的独立性


refs是一个对象,持有注册过 ref attribute 的所有 DOM 元素和组件实例。children是当前实例的直接子组件,它并不保证顺序,也不是响应式的。因此使用refs.children[i]获取子组件,是一种不稳定的操作。
ref属性可以访问子组件实例或子元素,但这仅仅是一个直接操作子组件的应急方案;为了保持组件的独立性、稳定性,建议不要直接使用子组件的方法、变量等。


禁止在watch和computed中用$route


由于我们目前都是keep-alive模式,若是在watch和computed中用$route,那么在包括tab页签打开、切换等操作在内的每一次路由变化,都会触发watch和computed,不论是否跟本页面本组件有关系。这样子带来了巨大的性能损耗和一些奇奇怪怪的缺陷产生。


禁止增删改JavaScript 对象或Vue的原型,造成原型污染


原型上的属性可以通过遍历访问到的,原型污染会引起性能消耗或意外BUG。
实际上,大多数在写业务代码的场景下,修改原型的方式都可以采用别的方式替代。


注释要保证详细、完整


推荐使用vscode的koroFileHeader插件,进行快捷注释操作。
文件注释去掉,可以留一个description;
代码有更新,注释记得也要更新;


/**
* @description 这个方法是干嘛用的
* @param {*}
* @return {*}
*/


/**
* @description 这个接口是干嘛用的
* @param {*}
* @see yapi地址
*/


// 这个变量是干嘛用的

工程目录、文件(夹)命名、组件内部命名等需遵循以下内部规范


因篇幅过长,单独整理


CSS


必须为组件样式设置作用域,建议采用scoped属性或者class策略。


设置样式的作用域可以有效确保你的样式只会运用在你想要作用的组件上,而不会造成”污染“。



  • scoped 属性:控制CSS 只作用于当前组件中的元素,需要给<style> 标签加上 scoped属性。

  • class策略:不止要使用 scoped属性,使用唯一的 class 名可以帮你确保那些三方库的 CSS或者其他组件的CSS 不会运用在你自己的 HTML 上。


// 反例
<template>
<span class="title">xxxxxxxxxx</span>
</template>

<style>
.title {
color: red;
}
</style>

// 正例
<template>
<div class="xxx-mgr-page">
<span class="title">xxxxxxxxxx</span>
</div>
</template>

<!-- 使用 `scoped` attribute -->
<style scoped>
.xxx-mgr-page{
.title {
color: red;
}
}
</style>

禁止使用全局选择器、类型选择器等作用范围太大的选择器添加css规则,推荐使用类选择器进行精细化控制。


简单说一下这两种被禁止的选择器:



  • 全局选择器,是由一个星号(*)代指的,它选中了文档中的所有内容。

  • 类型选择器,也叫做”标签名选择器“或者是”元素选择器“,因为它在文档中选择了一个 HTML 标签/元素的缘故。


使用他们添加css规则,会造成以下影响:



  • 作用范围太大,会造成一些不想作用的地方却误伤到了

  • 从性能角度考虑,标签选择器的性能比类选择器要慢


禁止通过css选择器的权重和优先规则来覆盖样式


在项目中,可能一个简单的按钮,它的样式会取决于很多地方很多层:组件库为它定义了最底层、最基本的外观 -> 业务项目中的公共样式为它定义了本项目中的统一样式 -> 页面样式为它定义了布局 -> 具体到这个按钮的样式定义了它的独特样式。正是由于这么多层这么复杂的样式组成,导致在需要更新样式的时候,会出现一些很”偷懒“的做法——通过直接覆盖样式,而不是去找到原先写样式的地方去修改。


// 反例
<template>
<button class="ivu-btn btn-close" style="color: white;">X</button>
</template>

<style>
.btn-close {
color: red;
}
</style>

// 正例
<template>
<button class="ivu-btn btn-close">X</button>
</template>

<style>
.btn-close {
color: white;
}
</style>

使用不常用的js api 和 css attribute,注意确认下浏览器兼容性


本条推荐理由很简单。我们推荐使用了chrome浏览器版本号为80+,那兼容性就需要考虑。常用属性已经验证过了没问题,不常用的就需要自行验证。建议可以通过mdn web docs(developer.mozilla.org/zh-CN/docs/…)来查询。
image.png


超长溢出统一用title,而不是tooltip,以提高性能


推荐理由如下:



  • 由于我们全平台中产品设计倾向于单行文本显示,包括标题文本、下拉表单项、表格单元格等等,一个页面中有可能就有上千个。

  • tooltip是iview组件,样式美观可调整,但包含了多个DOM节点;title是HTML属性,样式无法变更。两者性能差异大。


因为涉及范围之广、两者性能差异之大,所以我们建议用title来处理超长溢出。




优先级 B:推荐的


这些规则能够在绝大多数工程中改善可读性和开发体验。即使你违反了,代码还是能照常运行,但例外应该尽可能少且有合理的理由。


禁止单个vue文件超过1000行,尽量500行;禁止复制黏贴超20行的代码


在平常项目开发中,大家都深深体会到了:一个文件太长,维护起来头很大,开发模式下编译时间也很长;大段相似的代码,很不优雅,若产生问题也很容易只改一处,造成缺陷。
之所以限制1000行、500行、20行,凭以往经验决定;只要有充分理由,可以灵活应变。不断地去抽象,去提炼,去封装,也是很考验开发者的功力,很有助于我们的成长。
注:后续会考虑通过eslint+git-hooks阻止超过1000行的文件被提交。


建议不要在html中有超过两个条件的逻辑判断;在js中不要超过两个并列的if,可以考虑优雅的if-else


html中,不要有超过两句话(尽量一个操作符)的逻辑,否则就用computed
js中,一段逻辑不要超过两个if(如果你是第三个应该评估优化一下),优雅的维护if-else。嵌套的if尽可能减少或者注释清楚判断逻辑


代码优化之后,确定不需要的代码建议直接删除,不确定的代码进行注释并写明注释原因;注释或删除一段代码,要把相关的代码一并处理干净


现有情况是存在很多大段注释的代码,太过冗余杂乱,影响代码阅读,因此建议不需要的代码直接删除。
但又存在部分情况是产品提出的要求暂时隐藏某个功能,后续可能会重新启用,因此只需进行注释即可。建议这种情况下,写明注释原因,供他人后续阅读代码或者优化代码提供指引。
注释或删除一段代码时,现在会存在部分情况下,只删除直接相关代码,其他相关代码放任不管。举个例子,比如产品要求隐藏”保存并启用“功能,最差最直接的做法是隐藏这个按钮就完成,但是发现要获取这个按钮权限,需要watch中调用接口,因此导致这个功能被隐藏了,但是接口调用仍在频繁调用。


布局嵌套尽量不要层级太深;不加没有必要的DOM节点;




优先级 C:小tips


这个分类下的是一些项目开发的小技能、小知识点或业务相关的点。


路由组件一定要有name ,以确保keep-alive生效


<keep-alive>includeexclude prop 允许组件有条件地缓存。匹配首先检查组件自身的 name 选项,如果 name 选项不可用,则匹配它的局部注册名称 (父组件 components 选项的键值)。匿名组件不能被匹配。


弹窗或其他未激活的tab别在mounted阶段加载,以减小白屏时间


操作闭环、逻辑闭环考虑


举个栗子:



  • 比如弹窗要考虑确定、取消、关闭、重新打开等一系列闭环操作的正确性;

  • 比如详情的新建、查看、编辑;

  • 比如表格分页,考虑页码跳转,分页器大小变化,过滤条件变化时初始化页码等;

  • 比如v-if v-else-if v-else;


使用every和some方法时,记得排除空数组;every返回始终为true,some始终false


let a = [];
a.every(i => { ... }) // true
a.some(i => { ... }) // false

let a = [];
if (a.length > 0) {
a.every(i => { ... }) // true
a.some(i => { ... }) // false
} else {
...
}

对象浅拷贝时,分清对象展开运算符和Object.assign的区别


let aa = { a : 1, b : 2, c : 3};
let bb = Object.assign(aa, {d : 4});

// 修改aa
delete aa.a;
// 结果bb也发生了变化
console.log(bb); // {b: 2, c: 3, d: 4}

let aa = { a : 1, b : 2, c : 3};
// 解法1
let bb = Object.assign({}, aa, {d : 4});
// 解法2
let cc = {...aa, d : 4};
// 修改aa
delete aa.a;
console.log(bb); // {a: 1, b: 2, c: 3, d: 4}
console.log(cc); // {a: 1, b: 2, c: 3, d: 4}

在mounted或created阶段获取路由信息


连续调用接口的方法中如果有路由这种会变化的传参时,不能使用this.$route获取路由,避免执行方法时用户通过点击页签切换路由导致后续接口报错


样式尽可能考虑不同分辨率的自适应,如1366、1920


作者:是秋天啊
来源:juejin.cn/post/7216526817371504697
收起阅读 »

面试官:你为什么你觉得你值 30 k💰

☀️ 前言大家好我是小卢,这段时间很多群友都去面试了,而遇到最多的一个不会回答的问题就是:你为什么你觉得你值xxk,我今天就简单说一下我的看法。首先,面试官问的这个问题与自我介绍不同,因为他更看重面试者作为前端开发人员的技能、经验和价值。虽然自我介绍可能会涉及...
继续阅读 »

☀️ 前言

  • 大家好我是小卢,这段时间很多群友都去面试了,而遇到最多的一个不会回答的问题就是:你为什么你觉得你值xxk,我今天就简单说一下我的看法。

  • 首先,面试官问的这个问题与自我介绍不同,因为他更看重面试者作为前端开发人员的技能、经验和价值。虽然自我介绍可能会涉及这些内容,但这个问题的回答通常会更全面地概述一位面试者的背景、兴趣和职业目标

  • 其次面试官问这个问题,并不是只看你自己的能力,而是想看看你到底可不可以通过简短的几分钟说服他,让他更清楚知道你几斤几两,如果这时候你连自己都无法说服,那么基本上面试就无望了。

  • 想要回答这个问题,你可以从这三个方面去回答,我会给出一些我总结的模版和简单说明,如果大家有需要可以收藏起来喔💖

    • 工作经验

    • 学习能力

    • 职业规划

✍🏻 工作经验

我会这样说:

  • 我相信我是贵公司该前端开发岗位的有力候选人。我在 ReactVueNode.js 等现代 Web 技术方面拥有丰富的经验。

  • 我在工作中熟练使用前端技术来创建可扩展高效响应迅速的复杂用户界面。

  • 在曾经的工作中,我负责了xx个大大小小的项目,其中x个项目作为主要开发人员,x个项目为主要开发人员,从小型网站到复杂的 Web 应用程序的各种项目上都有丰富的工作经验。

为什么❓

  • 在介绍自己的技术栈时,通过强调在流行的前端框架方面的经验,可以证明自己拥有处理复杂项目交付高质量结果的能力。

  • 通过突出我在各种项目中的工作经验,可以向面试官证明自己拥有广泛的技能并且可以适应不同的项目类型和环境。

  • 因为这会体现自己就像变色龙一样,有能力适应周围的环境并无缝融合。无论是小型项目还是复杂的企业项目,都具有调整方法和交付满足客户需求的结果所需的灵活性和多功能性。


📖 学习能力

我会这样说:

  • 我相信我的快速学习适应能力是我可以胜任这个岗位的关键优势。

  • 作为一名前端开发人员,我明白技术领域在不断发展,学习与时俱进的框架和工具对于交付高质量的工作至关重要。

  • 在我以前的工作中,由于业务的特殊性,我经常会遇到一些奇奇怪怪的挑战和问题,这需要我接触很多我不会甚至没听过的新技术

  • 围绕工作学习这是我的习惯,每当遇到新技术或新鲜事物时,我都会花时间深入研究,以便充分理解其底层原理以及如何在工作实践中应用。这使我能够从多个角度处理问题拓展我自己的知识广度和深度多学科发展并针对每个特定项目提出更好的解决方案。

为什么❓

  • 这可以通过强调自己的学习能力,向面试官证明自己有能力接受新的挑战适应不断变化的环境

  • 通过你能围绕工作学的特点,表明我在职业发展中有上进心积极主动的心态。

  • 这对面试官来说是一个究极无敌有吸引力的品质,因为这可以表明面试者不满足于固步自封,而是一直在寻找新的方法来提高和扩展自己的技能。


🔝 职业规划

我会这样说:

  • 当然啦,我至今在前端岗位也工作了x年了,我对前端开发的职业生涯还是有着清晰的愿景的。

  • 在未来几年,我希望继续加强我的技术技能专业知识,这不管对我以后想做管理层或者架构都很有帮助,我的目标是通过自己的影响力和努力成为前端开发社区公认的专家。

  • 我也渴望在我的组织中担任更大的领导角色,有可能管理一个开发团队并为公司的战略方向或者团队发展做出贡献。

为什么❓

  • 这可以表明自己的职业目标和抱负,向面试官证明在自己的职业生涯中有明确的目标和方向,并且有动力为自己和所工作的组织取得成功。

  • 很多面试官都喜欢有自己主见的人,这种人有自己的思考一般都有很大的潜力快速成长并且容易接受新的挑战

  • 在面试前可以尽可能多了解一下公司JD,展示个人目标与公司的目标之间的有强烈一致性,这可以表明愿意致力于为公司的成长和发展做出贡献。

👋 写在最后

  • 一个小小的问题其实也有大大的学问,但只要你足够自信,相信所有问题都不是问题。

作者:快跑啊小卢_
来源:juejin.cn/post/7217086166418784313

收起阅读 »

辞职卖烤肠

前景 为35后做准备 找到一个合适的位置 周末出门多转转,找个人多的地方、并且容易饿的地方 营销策略 制定一个LOGO,视频中up的摊子叫公路商店 准备个小黑板 带个好看的围裙 头套啥的也备上 装备 必要 载具(小电车、自行车) 制作台 (可以展开...
继续阅读 »

前景


为35后做准备


找到一个合适的位置


周末出门多转转,找个人多的地方、并且容易饿的地方


营销策略



  • 制定一个LOGO,视频中up的摊子叫公路商店


image.png



  • 准备个小黑板

  • 带个好看的围裙

  • 头套啥的也备上


装备


必要



  • 载具(小电车、自行车)

  • 制作台 (可以展开的箱子、桌子)

  • 厨具(卡式炉、各式烤盘、酱料瓶子)

  • 收钱码(零钱)


可选



  • 投币机小玩具


卖什么


烤肠



  • 淀粉肠

  • 沙拉酱

  • 番茄酱

  • 黑胡椒酱

  • 孜然

  • 辣椒面

  • 油壶

  • 小刷子

  • 食品袋子
    image.png


润奶宝


甜筒、奶油、棉花糖、小熊饼干的组合


image.png


章鱼小丸子



  • 原材料 (面粉、鸡蛋、奶油)

  • 配菜(洋葱、芝士)

  • 定价 (12元/4个、15元/6个)


周末机车露营节集市,章鱼小丸子依旧是游客的宠爱,现场歌舞助兴_哔哩哔哩_bilibili


image.png


手工冰淇淋



  • 保温桶

  • 学习制作

  • 脆皮桶

  • 定价(3元/个)

  • 要用动物奶油


接完女儿放学,再一起骑着二八大杠去摆摊赚钱,多少人儿时的回忆_哔哩哔哩_bilibili


image.png


刨冰



  • 刨冰机


30℃的天气,我用樱桃小丸子刨冰机贩卖快乐_哔哩哔哩_bilibili


image.png


作者:codeniu
来源:juejin.cn/post/7220351836331311159
收起阅读 »

前端正确处理“文字溢出”的思路

web
前言: 最近在项目中需要做到类似于 Mac 下这种,当屏幕宽度足以容下当前文件名称的时候,文件名称全部展示,不做省略。 然而当用户缩放浏览器显示的尺寸时,我们需要做到省略中间的文字,选择保留后缀这种方案。如下图所示: 我个人也是感觉这个方案是最好的,因为大...
继续阅读 »

前言: 最近在项目中需要做到类似于 Mac 下这种,当屏幕宽度足以容下当前文件名称的时候,文件名称全部展示,不做省略。

image.png

然而当用户缩放浏览器显示的尺寸时,我们需要做到省略中间的文字,选择保留后缀这种方案。如下图所示:

1.gif


我个人也是感觉这个方案是最好的,因为大部分情况下,用户更关心的是这个文件的类型,而后缀名的保留往往是最佳的选择。我个人也查阅了很多相关文章,并且借鉴了一些已有轮子的代码思路,实现了一个符合我们项目中需求的一个组件。




一.组件效果预览




  1. 单行文字溢出时自动省略,并且不保留后缀。

    image.png




  2. 单行文字溢出时自动省略,并且保留后缀。

    image.png




  3. 多行文字溢出时,然后再开始省略。这个情况是我们项目中比较特殊的场景。简单来说就是假设我现在想让文字显示两行,如果两行的时候没有溢出,那么正常显示。如果两行情况下还是溢出了,那么我再去处理溢出的文字。
    假设这是没有做任何操作的的效果:

    image.png

    使用我们的组件以后的效果:

    image.png

    (tips:不一定必须是两行,三行,四行都是可以的。我们接下来实现的组件会让你高度自定义去处理文字溢出的场景。)




  4. 如果你想自己先尝试一下效果,那么你可以快速使用 npm 安装一下。




    • npm i auto-ellipsis-text




    • pnpm i auto-ellipsis-text




    • yarn add auto-ellipsis-text




    原仓库地址: 🫱AutoEllipsisTxt自动省略文字




  5. 使用起来也非常简单,你只需要包裹住你的文字即可
    image.png
    image.png




  6. 话回正题,接下来我会一步一步讲解我实现这个组件的思路,我写的这个组件不一定是最优的,你需要做到知其然并知其所以然,然后完善我写的组件的不足之处,你可以实现自己的自动省略文本方案,才是本文的目的。




二. 单行溢出的处理




  1. 我们先只考虑单行的情况。通常我们在自己的应用中展示很多文件信息的时候,往往选择的布局方式就是高度是一定的,说白了就是高度其实我们是定死的,宽度我们不确定,因为用户有可能会在某些情况下拖动浏览器,造成宽度发生变化,但是总会给宽度一个最小值和一个最大值来保障排版的统一性。

    image.png




  2. 样式方面,在这里我使用的是 UnoCSS ,将样式內联在了标签里,如果你还不了解这种写法,你可以点击下方的文章学习。不过即使你之前从未了解过 UnoCSS ,也不会影响你下面的阅读,因为样式不是本文的重点,并不影响整体阅读。

    🫱手把手教你如何创建一个代码仓库




  3. 让我们先创造一个简单的溢出场景,代码很简单,容器是一个 width 最大值为 200pxheight 为固定 30pxdiv

    image.png

    现在页面上的效果如下图:

    image.png




  4. 可以很清晰的看出,由于我们文字在容器内放不下,但是我们又没对溢出这一特殊场景做出处理,所以就造成了当前页面的效果。先别急,我们一步一步来。




  5. 最开始我去查阅 MDN 的时候,查阅到了一个 “确认过眼神,你就是我要找到人” 的属性。

    image.png




  6. 什么?text-overflow,我们要找到不就是文字溢出时候的处理吗?我兴奋的赶快添加到了我的组件上。

    image.png

    效果如下:

    image.png

    然后看着毫无变化的页面,开始怀疑我自己是不是单词拼错了,然后一个字母字母的比对,排除了单词打错字的情况,但页面还是没有变化。🤔




  7. 于是我又返回 MDN 去查看自己是否遗漏了哪些东西,发现了这样一段文字。

    image.png

    这里直接说结论,其实 text-overflow 这个属性不会为了让文字省略而去创造省略的场景。它其实是在你处理过溢出场景之后,帮你做对于文字溢出的的二次特殊处理。当你对于页面溢出做没有任何操作时,这个属性其实是无效的。 (注意:它仅仅只处理文字溢出的场景。)




  8. 既然你说了,让我们添加额外的属性:overflow-hiddenwhite-space,那么我们就自己添加。我们先只添加一个 overflow-hidden 来看看会发生什么。

    image.png

    我们发现,下面多出去的文字倒是被省略了,但是我们的省略号呢??我就不卖官子了,其实造成这个的原因的答案就是下面这句话:

    image.png




  9. 我们仔细看上面我们溢出的场景。

    image.png

    我们下面两行文字其实是溢出在了盒子下方,正好对应了上面 text-overflow 的介绍,“无法在盒子下面溢出” 这句话。




  10. 在这里我们就需要制造一个让文字强制不换行的场景。那么就需要用到我们另外一个十分重要的属性,white-space

    image.png

    我们本节只需要关系 nowrap 这一个值即可。剩下的值如果读者有兴趣可以自行了解,我们不过多解释。




  11. 首先你要知道,其实我们 web 页面的换行,并不是毫无意义的自己就换行了,而是都有一个隐藏的换行符,你可以把这个隐藏的换行符浅浅的理解为 white-space(空格)

    image.png




  12. 理解了上面那段话,那我们的属性 white-space:nowrap 的中文含义就十分明显了。white-space对应空格no-wrap 代表不换行。连起来的意思就是,遇到空格不换行。而我们的换行其实有一个隐藏的 white-space ,那么我们添加这个属性以后,就会造成一个不会换行的场景。




  13. 让我们先把 text-ellipsisoverflow-hidden 属性删除,只添加 white-space:nowrap 看看页面效果会是怎么样。

    image.png

    效果如下:

    image.png

    可以看到,我们省略了那个隐藏的换行符,所以文字不会自动换行了,那么整段文字都显示到了一行上。此时我们再加上我们的两个属性,overflow-hiddentext-ellipsis,神奇的一幕就发生了。

    image.png

    我们仅仅只使用了几个 CSS 属性就完成了单行情况下不保留后缀的文字溢出处理。




三. 前期准备




  1. 首先你需要准备一个 autoEllipsis.vue 文件,首先写出下面的代码,来和我一起完成这个组件。


    <template>
    <div id="autoEllipsisWrapper" ref="container" v-bind="$attrs">
    <span ref="text">
    <slot />
    </span>
    </div>
    </template>




  2. 请注意这个 id 叫做 containerdiv 元素将在接下来的内容中起到至关重要的作用。




  3. 接下来使用 ref 分别去拿到这两个 dom 元素。

    image.png




  4. 最后我们需要设计一个函数,在组件挂载以后,让它去正确处理我们文字溢出的场景。

    image.png




  5. 接下来的需求就是,这个 autoEllipsis 函数如何去实现。别着急写代码,我知道你现在有可能还是一头雾水无从下手,让我先带你理清思路然后再开始写代码。




四. 理清思路




  1. 首先我们因为要做到通用性所以, container 的宽度是不能确定的,它的宽度需要根据它外层的父元素来决定,也就是上文中我们提到的有一个最大值最小值宽度的元素。

    image.png

    换句话说,我们这个 container 要去动态的拿到外层父元素的宽度。




  2. 我们先不讲代码如何实现,我们假设现在我们已经拿到了,就叫做 fatherWidth。然后我们再通过刚刚的 ref 获取到的 text dom 元素去拿到外面传进来的文字内容。通过拿到这个 span 元素的 offsetWidth ,就可以拿到文字的长度。通过判断文字的 offsetWidth 是否大于 fatherWidth 。然后我们通过两个宽度相减,可以得出我们到底溢出的文字宽度为多少。

    image.png




  3. 拿到溢出的宽度以后,那么我们就可以用溢出宽度来除以文字大小,(overWidth/fontSize) ,就可以算出我们到底溢出了多少文字。




  4. 假设现在我们现在溢出宽度为 200px。我们的文字大小为 20px,那么 200/20 就算出我们现在溢出了 10 个字。




  5. 我们并且一开始就拿到了总的文字内容,假如我们之前的文字总数为 30 个。那么在这个情况下我们屏幕上只展示了 20 个文字,因为有 10 个字溢出被我们忽略了。




  6. 到这里之后,我们要做的事情就非常简单了,我们只需要从原来 30 个字的中间开始做切割。一边去掉 5 个,那么此时容器恰好可以容下 20 个字。中间我们再手动加上 “...” 省略号不就完美达成了吗?




  7. 上面想表达的意思用大白话来讲,其实也就是去掉中间的10个文字,然后随便再找一个字替换成字符串三个点 ...




五. 完成 autoEllipsis 函数




  1. 第一步就是为了拿到我们放入的文字宽度。注释已经写的很清楚了,就不过多赘述。

    image.png




  2. 然后我们再去拿外面父元素的宽度。此时会出现第一个分支, container 的宽度小于父元素的宽度,很容易可以猜到现在我们的文字内容是完全可以容纳的,不需要做特殊处理。

    image.png




  3. 第二个分支,当我们的 container 宽度大于了父亲元素的宽度,那么我们可以通过传递 props 来区分是否需要保留后缀,如果不需要保留后缀,我们直接给 container设置我们第二个标题讲解的知识就OK了。

    image.png




六. 保留后缀的实现




  1. 如果看到这里,你还没有正确的保留后缀思路,我建议你重新去观看一下标题四,这里我们大致的思路就是为了拿到父元素可以容纳多少文字。

    image.png




  2. 这里我们的思路其实就是计算出得出我们需要删除多少个文字

    image.png




  3. 很简单的思路,就是字符串使用 slice 切割我们上面计算得出的,两边需要删除多少文字。

    image.png




  4. 最后的关键一步,我们需要把 containerwhite-space 属性设置为 normal,因为我们已经正确的处理了文字数量,现在的 container 已经不会溢出了。

    image.png




七. 源码


下面是本组件的核心代码 autoEllipsis 函数的源码


function autoEllipsis(container: HTMLElement, textNode: HTMLSpanElement) {
const str = premitiveText; //1.拿到的所有文字信息
textNode.textContent = str; //2.将所有文字放入到我们的 span 标签中
container.style.whiteSpace = "nowrap"; //3.先将文字全部放入到《一行》中,为了计算整体宽度
container.style.width = "fit-content"; //4. 给 container 设置 fit-content 属性,就可以拿到正确的内容宽度
const containerWidth = container.clientWidth; //5. 拿到了 container 的宽度

const parent = container.parentElement; // 拿到外部父元素的宽度
const parentWidth = parent!.clientWidth || parent!.offsetWidth;
if (containerWidth <= parentWidth) {
//如果container 的宽度《小于》父元素的宽度,不做任何处理
textNode.textContent = str;
return;
} else if (cssEntirely.value) {
container.style.width = parentWidth + "px";
container.style.whiteSpace = "nowrap";
container.style.textOverflow = "ellipsis";
container.style.overflow = "hidden";
return;
} else {
const textWidth = textNode.offsetWidth; //1. 拿到文字节点的宽度
const strNumer = str.length; //2. 拿到文字的数量
const avgStrWidth = textWidth / strNumer; //3. 拿到平均每个文字多少宽度
const canFitStrNumber = Math.floor(
(parentWidth * props.startEllipsisLine) / avgStrWidth //4. 根据父元素的宽度来计算出可以容纳多少文字
);

const shouldDelNumber = strNumer - canFitStrNumber + 1.5; //1. 算出需要删除几个文字(1.5是为了省略号的宽度
const delEachSide = shouldDelNumber / 2; //2. 因为要保留中间,所以我们不能只从开头删除,也需要从两头删除
const endLeft = Math.floor(strNumer / 2 - delEachSide); //3. 因为下面要用到 slice 所以需要计算出 index
const startRight = Math.ceil(strNumer / 2 + delEachSide); //4. 和上面同理

switch (props.suffix) {
case true: {
textNode.textContent =
str.slice(0, endLeft) + "..." + str.slice(startRight);
break;
}
case false: {
textNode.textContent = str.slice(0, -shouldDelNumber) + "...";

break;
}
}
container.style.wordBreak = "break-all";
container.style.whiteSpace = "normal";
}
}


八. 优化点


这个组件目前在 ... 省略号的文字占用上,并不能准确的根据文字大小调整所需的字数。也就是下面的 1.5 这个数字无法精确的算出,但是目前我们项目的文字大小是确定的,所以我也就没有再优化了,还希望各位可以提交 Pr 来一起完善这个组件。

image.png


原仓库地址: 🫱AutoEllipsisTxt自动省略文字


作者:韩振方
来源:juejin.cn/post/7218411904699924540
收起阅读 »

关于使用EaseTitleBar编译无报错,打开app崩溃与解决方法。相关日志及描述如下,(使用sdk版本为3.9.5)。

问题描述:使用EaseTitleBar编译无报错,打开app崩溃解决方法: 随便设置一下titleBarLeftImage的属性就可以了,没有使用这个属性的加上这个属性。日志:04/10 12:07:08: Launching 'app' on Nexus 5...
继续阅读 »

问题描述:使用EaseTitleBar编译无报错,打开app崩溃

解决方法: 随便设置一下titleBarLeftImage的属性就可以了,没有使用这个属性的加上这个属性。


日志:

04/10 12:07:08: Launching 'app' on Nexus 5X API 29.

App restart successful without requiring a re-install.
$ adb shell am start -n "com.example.my/com.example.my.MainActivity" -a android.intent.action.MAIN -c android.intent.category.LAUNCHER
Connected to process 13230 on device 'Nexus_5X_API_29 [emulator-5554]'.
Capturing and displaying logcat messages from application. This behavior can be disabled in the "Logcat output" section of the "Debugger" settings page.
I/com.example.my: Late-enabling -Xcheck:jni
E/com.example.my: Unknown bits set in runtime_flags: 0x8000
W/com.example.my: Unexpected CPU variant for X86 using defaults: x86
W/RenderThread: type=1400 audit(0.0:184): avc: denied { write } for name="property_service" dev="tmpfs" ino=7412 scontext=u:r:untrusted_app:s0:c137,c256,c512,c768 tcontext=u:object_r:property_socket:s0 tclass=sock_file permissive=0
D/libEGL: Emulator has host GPU support, qemu.gles is set to 1.
W/libc: Unable to set property "qemu.gles" to "1": connection failed; errno=13 (Permission denied)
D/libEGL: loaded /vendor/lib/egl/libEGL_emulation.so
D/libEGL: loaded /vendor/lib/egl/libGLESv1_CM_emulation.so
D/libEGL: loaded /vendor/lib/egl/libGLESv2_emulation.so
W/com.example.my: Accessing hidden method Landroid/view/View;->computeFitSystemWindows(Landroid/graphics/Rect;Landroid/graphics/Rect;)Z (greylist, reflection, allowed)
W/com.example.my: Accessing hidden method Landroid/view/ViewGroup;->makeOptionalFitsSystemWindows()V (greylist, reflection, allowed)
D/AndroidRuntime: Shutting down VM
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.my, PID: 13230
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.my/com.example.my.MainActivity}: android.view.InflateException: Binary XML file line #16 in com.example.my:layout/activity_main: Binary XML file line #16 in com.example.my:layout/activity_main: Error inflating class com.hyphenate.easeui.widget.EaseTitleBar
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3270)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3409)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:83)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2016)
at android.os.Handler.dispatchMessage(Handler.java:107)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7356)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
Caused by: android.view.InflateException: Binary XML file line #16 in com.example.my:layout/activity_main: Binary XML file line #16 in com.example.my:layout/activity_main: Error inflating class com.hyphenate.easeui.widget.EaseTitleBar
Caused by: android.view.InflateException: Binary XML file line #16 in com.example.my:layout/activity_main: Error inflating class com.hyphenate.easeui.widget.EaseTitleBar
Caused by: java.lang.reflect.InvocationTargetException
at java.lang.reflect.Constructor.newInstance0(Native Method)
at java.lang.reflect.Constructor.newInstance(Constructor.java:343)
at android.view.LayoutInflater.createView(LayoutInflater.java:854)
at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:1006)
at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:961)
at android.view.LayoutInflater.rInflate(LayoutInflater.java:1123)
at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:1084)
at android.view.LayoutInflater.inflate(LayoutInflater.java:682)
at android.view.LayoutInflater.inflate(LayoutInflater.java:534)
at android.view.LayoutInflater.inflate(LayoutInflater.java:481)
at androidx.appcompat.app.AppCompatDelegateImpl.setContentView(AppCompatDelegateImpl.java:706)
at androidx.appcompat.app.AppCompatActivity.setContentView(AppCompatActivity.java:195)
at com.example.my.MainActivity.onCreate(MainActivity.java:16)
at android.app.Activity.performCreate(Activity.java:7802)
at android.app.Activity.performCreate(Activity.java:7791)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1299)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3245)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3409)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:83)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2016)
at android.os.Handler.dispatchMessage(Handler.java:107)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7356)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
E/AndroidRuntime: at androidx.appcompat.app.AppCompatDelegateImpl.setSupportActionBar(AppCompatDelegateImpl.java:581)
at androidx.appcompat.app.AppCompatActivity.setSupportActionBar(AppCompatActivity.java:183)
at com.hyphenate.easeui.widget.EaseTitleBar.initToolbar(EaseTitleBar.java:180)
at com.hyphenate.easeui.widget.EaseTitleBar.init(EaseTitleBar.java:92)
at com.hyphenate.easeui.widget.EaseTitleBar.<init>(EaseTitleBar.java:62)
at com.hyphenate.easeui.widget.EaseTitleBar.<init>(EaseTitleBar.java:56)
... 28 more

收起阅读 »

做个清醒的程序员之努力工作为哪般

如果要问自己这样一个问题:“我们工作的意义到底是什么?”会得到怎样的答案? 是为了安身立命?是为了满足别人的期待?是为了得到社会的认同?抑或是索性认为工作是无意义的? 如果我说:工作的意义在于自我实现,你会同意吗?你会觉得这样的观点很片面吗?你会觉得这很理想化...
继续阅读 »

如果要问自己这样一个问题:“我们工作的意义到底是什么?”会得到怎样的答案?


是为了安身立命?是为了满足别人的期待?是为了得到社会的认同?抑或是索性认为工作是无意义的?


如果我说:工作的意义在于自我实现,你会同意吗?你会觉得这样的观点很片面吗?你会觉得这很理想化吗?


其实,依我的工作经验来看,上面列举出的常见答案其实都有道理。人在不同的阶段,不同状态,工作的意义便会发生变化。


坦率地讲,我从开始工作之后,就下定决心不再啃老。简单地说,就是不再向父母伸手要钱。于是,如何保障自己的温饱就是我工作的最首要和最基本目的。也就是说,我刚开始的时候,工作就是为了挣钱。


刚起步的时候我一个月有多少工资呢?很少,2500块。即便是毕业后转正,拿到手也只有三四千块。


但是,这些钱已经可以很好地满足我的温饱需要。即使我出去租房,不在家吃饭,这些钱其实也是够的,只不过很可能剩不下分文。很感谢我的父母,在我事业刚刚开始的时候,照顾了我的起居生活。


一开始,我眼中工作的意义就是为了能养活我自己,就是为了那三两碎银。所以,为了养活自己而选择工作,挣钱,然后达到目的,我至今也不觉得有什么丢人的。


后来呢?因为我一直在家吃饭,午饭的话公司也有食堂,所以基本没什么开销。唯一生活上的花销就是衣服和鞋,可偏偏我穿衣穿鞋算是比较省的,一件衣服基本上少说穿个四五年,鞋的话就更久了,我现在还经常穿七八年前买回来的经典款。我认为穿衣方面,买经典款总是不会错,而且很难因为流行趋势而过时。


话说回来,随着我的小金库慢慢积累变多,我就不愁“安身立命”的目标了。因为我是家族后代中唯一的男性,所以心中总会有一种使命感,虽然没有人给我这方面的压力。我感受到的最大的责任感其实是想让家人生活得更美好的目标,虽然我父母在这方面依然没有表现出很大的期待。


于是凭着这个我自认为的“责任感”,一直努力工作了很多年。其实我的想法很简单,就是想让爱自己和自己爱的人过得好一点。我觉得凭本事挣更多的钱,然后达到这个目标,更是无可厚非的事情,也没什么错。


后来呢?我其实很早就有写博客的习惯,随着读者对我的文章产生认同,更重要的是有出版社编辑的认同,我就产生了要获得社会认同感的目标。虽说是“社会认同感”,其实它所包括的内容很广泛。比如读者的、家人的、老同学的、(前)同事的等等。这种“社会认同感”还会顺便带来他人的尊重甚至是敬重。至少我这次的工作和上次的工作,在面试的时候基本上技术方面是被认可的,免去了“八股文”的考验。不过老实讲,如果考验我面试“八股文”,大概率我还真得吃败仗。


再到现在,金钱对我的诱惑依然存在,但已经大幅降低了。更多的是考虑如何实现自己的价值,真正地释放自己的潜力,对这个社会,对这个世界发挥光和热。也就是我在一开始说的“工作的意义在于自我实现”。


好了,这就是我的故事。一开始为了满足温饱,我去工作,去挣钱;后来,为了得到别人的认可,获得社会认同感而努力工作,顺便把钱给挣了,引一句读者的评论:“挣钱是重要的事情中最不重要的”;再到现在,自我实现对我是最重要的。


所以,在不同阶段,不同状态,对工作意义产生不同的观点,我觉得都是正常的,也都是正确的。


但是,你知道吗?在我刚刚工作的时候,我就立下目标,要在这个世界上留下点什么,留下自己活过的印记,不想虚度此生。但当时无论如何也想不到,自己会成为作者,通过文字和图片把枯燥的编程知识教授给需要的人。


不知道你有没有听说过“马斯洛需求层次”,莫名其妙地,从一开始我就攀登这个需求金字塔,直到现在,已过去十余年。


有读者说我是“长期主义者”,以现在的认知,我愿意做一个“长期主义者”。但当初的我,哪懂什么“长期主义”。我更偏向于用“轴”、“固执”或是“不见棺材不落泪,不撞南墙不死心”这类的修饰词来形容自己。所幸的是,在我“固执”的一路上,受到了很多人的帮助与支持,还有上天的眷顾。“运气”、“机遇”占了很大的比重。


回到最初的问题:“我们工作的意义到底是什么”?如果我们对一个疲于奔命的人说:“你要实现自我”。很遗憾,“实现自我”在这个人眼中,也许就只是保障温饱,他甚至会转过头来和别人说我们是神经病。因为阶段不同,状态不同,追求的东西自然也会不同。也许这很无情,很冷血。但它很真实,也很正常。


但即便我们处在最开始的温饱阶段,着眼于生计,但目光一定要看到那个“未来”,坚定不移地坚持走“长期主义”的道路。


另一方面,在学校里,往往努力就会有结果,这是具有极大的确定性的。踏入社会之后,这种确定性消失了,我们往往努力了,却并没有得到想要的结果。很多人在鼓吹“选择大于努力”,选择固然重要。选错了,越努力,走得越偏。但这种话听多了,就要当心它是否成为了不努力的借口。努力是为了在机会到来的时候,我们有能力抓住他。这就好像不慎跌落坑里的人,有人扔绳子救他,他得努力抓住绳子,才有被救的可能。如果一味求救,却不选一种力所能及的方法去做,无论有多少次生还的机会,也会错过。


所以,任何时候也不要轻视努力与坚持的重要性。这看上去很笨很傻,但它却是每一个平凡人实现“自我价值”的可行之路。就像歌中唱的那样:“老天爱笨小孩”。在这个充满不确定性的时代,更应如此。不确定性就像基因突变,可能会变糟,也可能会变好。当有好事到来时,便是好运来临之际。和“选择大于努力”相比,我更倾向于相信“机会总是留给有准备的人”。这句话出自法国著名的微生物学家、化学家路易斯·巴斯德,告诫人们:机遇往往不易察觉,可遇不可求,容易稍纵即逝。作为普通人,若要抓住机遇,就要把功夫用在平时,甚至是有点傻的、偏执的努力。


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

30岁女程序媛,能有什么好出路?

前言 最近技术社群上随处可见“前端已死”的观点,主要是经济环境和市场供需关系决定的;也可能是人工智能chatGPT一个demo,一张页面的草图,通过chatGPT可以生成一个完整的网站,就连按钮功能也用JS写好的;这些在一定程度上引发了作为前端从业者的焦虑。这...
继续阅读 »

前言


最近技术社群上随处可见“前端已死”的观点,主要是经济环境和市场供需关系决定的;也可能是人工智能chatGPT一个demo,一张页面的草图,通过chatGPT可以生成一个完整的网站,就连按钮功能也用JS写好的;这些在一定程度上引发了作为前端从业者的焦虑。这不禁的引发我们的思考,以及我们要做出一些应对的策略。作为程序员的我们,还有好出路吗?一名30岁的程序媛,能有什么好出路。


谁的青春不迷茫


我是出生于农村的一个普通女孩,经过河南省激烈的高考竞争,我考进河南一个普通的大学,大学所选的专业是计算机网络技术,大学期间,我对未来我要从事什么职业很迷茫,每天浑浑噩噩,得过且过。后来我听从家人的建议出去找实习,不在意薪资的高低,只为寻找适合自己的职业发展方向。在经过了服装销售、网页设计、经理助理等多种尝试后,我最终找到了适合我的职业发展方向,那就是前端开发,同时我参加了统招专升本考上了省内还不错的二本,全日制就读两年。


当时的前端开发被称做网页制作工程师,于是我从2014年暑假开始在慕课网上自学前端开发,刚一接触这一领域,我就深深被吸引住,如饥似渴的吸收着这方面的知识,平时没课我基本上都在宿舍抱着我的华硕电脑和一本红宝书自学前端开发。当时大学毕业时,我仅在慕课网上学习时长积累了 1200+小时


在大三期末的时候,由于我的努力,我获得了360第一届前端星校招实习名额,我跟学校请了两周的假,到360北京总部参加 【360前端星】 ,经过两周的培训后,很遗憾,我没有拿到offer,当时360培训的导师给的意见,可以把计算机基础打扎实,再考个研提升提升学历。


第一届360前端星.jpg



里面有你熟悉的面孔吗?



回去我自己分析我的情况,有三方面劣势。



  • 学历自卑:当时参加前端星的同学学历都很优秀,清一色985、211高校,相比较下我就很感到很自卑,我就读于双非二本,而且还是专升本,在学历上我没有优势。

  • 专业技能:我的前端技能主要靠在慕课网自学,而其它同学基本都跟着自己的导师做过一些项目,有实际的开发经验,而我从技术深度到技术广度都不太行。

  • 综合素质:在培训期间是那种不敢站出来说话,害怕说错话,当然也没有才艺,更没有积极展现自己。


360.jpeg


回到学校后,我深思熟虑后,决定考研来提升自己的学历。我当时的想法是只要拼命学,考上985、211就有希望。我踌躇满志,做好了拼命努力的心理准备。暑假期间,我复习的很认真,到了最后甚至有一种不在乎结果,单纯追求梦想的悲剧英雄主义。不过遗憾的是,我备考存在很多问题:学习方法上、还有备考院校、自主命题等信息差、资源差。初始分数还没出来,我隐约知道自己没有希望了,果不其然,来年分数出来,我落榜了。经过一段时间的失望和彷徨,我鼓足勇气跟家里人说我想去北京找实习,于是从家里拿了1万的生活费,开始了自己的北漂之旅。


重拾梦想


工作很多年后,每每想起考研失利,总是有些遗憾,我知道失去和失败是人生常态,可是我好想考研,好想成为研究生,最后已经有些魔怔,我就是要考,我这辈子注定是个研究生。放弃是一件容易的事,气一松,力一卸,就能完全放下。但是总有一份心底的坚持,让你不由自主的重燃斗志。一件事可以暂时不做,但不要彻底放弃,而从内心否定自己。于是我再次开始了我的在职考研之路。


择校


这次我没有选择计算机专业,计算机硕士考试,科目多并且难度大,跟身边的朋友了解后,发现MEM项目管理相对比较适合我,仔细对比了MBA和MEM的院校、分数、招生情况,我选择了北xMBA,毕竟按照往年的情况,报考北xMBA的只要过国家线,就能录取。这次应该是我离硕士最近的一次了吧。


平衡


在完成日常工作的同时,我还需要抽出足够的时间复习备考,这个任务很艰巨。不得不提一点的是,我在小组,新来的小组长拉帮结派、挤兑人,我们业务线,承接国内大小节日活动的前端开发。很过分的是,新组长把去年后半年都安排给我做(包括,但是不局限于,五一活动、六一活动、七夕活动、中秋活动、双11活动、2023春节活动),几个人的小组,我一个人承接了后半年的所有节假日活动,不仅仅要开发,每个节假日晚上凌晨12点我都要值班。由于我的业务量很饱和,再加上我要备考,我就没太多的时间和精力去搞KPI,我也不想多事,就忍了下来。


WechatIMG483.jpeg


阳了


2022年12年7号月份疫情放开,我做好一切措施,防止自己阳了影响备考,但是十几号的时候我还是中了,我开始发烧,我全身酸痛地躺在床上,难受的完全不能学习了,幸好我对象照顾了几天,我退烧能下床了我对象又病倒了,我只能先照顾他,抽空做押题和最后的查漏补缺。由于近两周都多没能好好学习,变的极其焦虑和急躁。


考研需要上报阴阳情况的时候,我还是阳性,后来就给分到阳性考场。阳性考场,咳嗽声此起彼伏。


WechatIMG506.jpeg


考完最后一场出来,见到那天的晚霞,真美。
晚霞.jpeg


今年2月份公布了考研初试分数,按照往年的情况我是能过初试的,我开始积极备考复试。梳理我的过往经历、了解所在公司的营收模式、剖析公司所在行业的发展前景以及竞对的优缺点。以及最近的热点新闻和二十大政治相关内容。面试那天我穿着我的小西服战袍走进了考场,在这之前,我刻意模拟了两次复试面试,我怀着淡淡的期待和自信考完了。


上周公布了拟录取名单,我被录了(圆了对象的北*梦)。我把消息发到家人群里,全家人都很开心,我还领了我爸的大红包,心里美滋滋~(终于上岸了)。


THE END


WechatIMG87.jpeg


也许备考期间我减少了与家人的联络,忽视了KPI,但是我自认为我兢兢业业,超低bug率、无线上事故。我想说我尽力了,我不够完美,在时间和精力上不能很好的平衡。也许根本没有平衡,只能取舍。愿我在自己存在的地方,成为一束光,照亮世界的一角。我是一个30岁的程序媛,我不知道哪里有更好的出路。我之前很卷,一直在卷工作;去年我换了一个赛道卷,卷学历;如今我上岸了,再卷回工作。也许你会说前端已死,但是逃避,就肯定输家。唯有面对,才是出路的第一步。


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

为什么ChatGPT不是中国搞出来的?

无论是2022年11月30日上线的ChatGPT,还是最近 OpenAI 正式发布的迄今为止规模最大、功能最强悍的多模态预训练模型 GPT-4,都着实让普罗大众感受到了AI人工智能的强大威力,宣告了强人工智能时代的到来。在这场人工智能引发的破圈热闹之下,我们不...
继续阅读 »

无论是2022年11月30日上线的ChatGPT,还是最近 OpenAI 正式发布的迄今为止规模最大、功能最强悍的多模态预训练模型 GPT-4,都着实让普罗大众感受到了AI人工智能的强大威力,宣告了强人工智能时代的到来。在这场人工智能引发的破圈热闹之下,我们不免要进一步深入思考一个问题,为什么这么多年过去了,引领产业技术革命的依然是美国而不是中国?


2021年,中国以全球AI顶级会发表的论文数27.6%的比例高居榜首,而欧盟和英国以19.0%紧随其后,美国以16.9%位居第3。无论AI人工智能论文发表量还是专利申请量,中国在全世界范围内都名列前茅,那么按道理来说中国在AI人工智能领域的研究既广泛又深入,像ChatGPT这样具有里程碑意义的人工智能应用应该在中国诞生才对。虽然说自然语言理解和生成是人工智能研究领域的一座高峰,想要攀登和征服就需要持续不断地高质量资源投入,而阿里达摩院、华为以及清华大学等研究机构也在做类似的事情。不过很遗憾,最终还是美国的OpenAI率先发布了强大的ChatGPT。今天我们试图客观的分析下是什么原因造成了我们没有成为新一代技术革命的发起人,以后我们应该怎么做才能成为未来技术的引领者而不是跟随者。



原因分析


也许是因为过去中国二十年互联网的蓬勃发展,国内一些互联网巨头公司的市值可以在国际上和美国的互联网公司掰手腕了,让我们产生了不少的错觉,误以为在技术上我们也可以和美国一较高下了,但是事实真的如此吗?


相信大家在新闻中经常可以看到,这几年为了遏制中国发展,美国政府不断向中国高科技行业挥舞制裁大棒。比如向中国华为公司实施技术封锁,以国家安全为由,禁止美国公司向华为出售技术和设备。一时间中国的高端芯片行业哀鸿遍野,遭遇技术卡脖子危机。我们在感叹美国政府无耻的同时,也为自身没有预先布局底层技术研究以及体系构建而感到扼腕叹息。这次OpenAI发布的ChatGPT以及GPT4又给我们上了一课,越来越多的人认识到中国和美国的技术差距何止高端芯片,前沿的人工智能技术、强大的算力网络等等诸多高新技术方面都和美国有不小的差距。美国政府打压中国高科技行业发展这一巴掌,终于把一些还对美国抱有幻想的技术人彻底打醒,无论高端芯片还是前沿人工智能等核心技术买不来也求不来,只能靠自己一步一个脚印地走出来。当然这也坚定了我们和美国有关的技术类别必须全部实现国产化替代的决心。


过度关注商业模式,忽视底层技术研究


在过去的20年当中,中国互联网行业处在高速发展阶段,据CNNIC(中国互联网络信息中心)报告,截至2022年12月,我国网民规模达10.67亿。随着互联网在线人数的持续攀升,各种各样形态的商业模式在这片热土上不断得以验证以及发展壮大。移动支付、电商物流、外卖打车、短视频等商业形态深刻改变了老百姓的日常生活方式,为大家带来了极大的生活便利以及娱乐消遣,同时也提升了整个社会的效率。但与此同时,各大互联网公司好像都沉浸在如何发展新商业模式以及维护已有商业格局的怪圈中不可自拔,几乎所有的精力都放在业务内卷上。因此投入底层技术研究的时间和资源相对就会被挤压甚至是完全没有,最终导致被别人技术卡脖子的概率大大提升,同时能够产生颠覆性创新技术的可能性却大大降低。


对比中美两国技术发展路线,我们可以发现一个有意思的现象。在中国,技术的发展大都是围绕业务进行,也就是说当一个业务模式被推出来之后,如果想要支撑这样的业务规模就需要发展什么样的技术,那就研究和发展什么技术,通过商业模式的创新、业务的发展推动技术向前发展演进。



但是美国好像正好相反,常常是一项技术的发展进步反向催生出新的商业形态。就拿推出ChatGPT的OpenAI公司来说,它原本就是一个非营利性的技术组织,集结了AI人工智能界的各路大佬,初衷就是让人工智能向着友好的方向发展,核心宗旨在于“实现安全的通用人工智能(AGI)”,使其有益于人类。GPT模型经过不断的迭代升级最终搞出来了ChatGPT,有望成为未来人工智能应用技术的基础底座,因此未来可能将会有海量的上层应用都是基于ChatGPT来进行构建,可以毫不夸张地说,ChatGPT或者说通用大模型也许就是人工智能领域的操作系统,将来会诞生多少新的的商业模式以及业务形态不可估量,这种技术革命是自下而上的颠覆性创新将会对未来的产业形态产生深远的影响。



而我们太关注上层应用商业模式的创新了,从而忽略了底层技术的研究。我们仿佛在一次又一次的电商大促、一次又一次的短视频娱乐中逐渐迷失了方向,表面上的热闹掩盖不了底层核心技术的匮乏。其实想想就背后发凉,当下无论是互联网技术、人工智能技术等前沿技术好像都是建立在别人的技术底座之上的。那些曾经我们引以为豪的移动支付、电商物流等等不过是这些底层技术的上层应用而已。如果被人家釜底抽薪,那真的就玩不转了。所以底层技术真的非常重要,可以说没有底层技术的支撑,所有上层的应用都是空中楼阁。


缺乏长期主义


也许是国内竞争压力太大了,无论是个人还是公司,都恨不得今天投进去资金,明天就能看到结果和收益。如果在短期看不到收益的项目,一般公司实际上很难有持续的资源投入,更别提看不到明确回报周期的基础技术研究了。浮躁的氛围,层出不穷的技术噱头,表面上的热热闹闹,只会让大家都热衷于赶时髦追风口,却没有多少人能真正静下心来进行长期的技术研究。又有多少公司可以有马老师那样的魄力和实力每年10个亿连投10年,最终投出来个阿里云。


说到这里,不知道大家还记不记得区块链、VR、元宇宙这些科技名词,一开始出现的时候受到了极大的关注和资本的热烈追捧。但是当热头过去之后,现在又有多少公司还在不断地进行产品迭代,不断地进行技术深度优化研究呢?不能说完全没有但绝对是寥寥无几,与其说是他们追捧高新技术,不如说他们追捧的是如何快速赚钱的工具。至于到底是VR、元宇宙还是ChatGPT其实都无所谓,对他们来说其实就是个技术名称而已。


但是实际上无论是ChatGPT还是GPT4这样的人工智能应用,都不是横空出世的,它们都是在GPT、GPT2以及GPT3基础上逐步迭代发展而来的,这其中耗费了多少顶尖人工智能专家长期坚持的心血以及大量资金的持续投入。OpenAI在获得微软投资之前的三四年里烧掉将近1亿美元,才搞出来一个以GPT第一代模型为基础的雏形,当时还远远达不到ChatGPT今天的效果。基础技术研究不是一朝一夕就能完成的事情,它需要不断的试错,需要对未来技术发展趋势的独到眼光,更加需要长期主义的坚持。不过让我们感到欣慰的是,阿里巴巴、华为在技术研究上的投入已经每年超过1000亿,相信在不远的将来一定会有新的技术突破出现。


缺乏技术体系支撑


就好比高端芯片制造,其中涉及芯片设计、芯片制造以及封装测试都是一整套复杂的工艺缺一不可。同样通用大模型也是一个复杂的系统工程,而通用大模型的训练离不开海量的高质量训练数据、优秀的深度学习算法以及强大的算力资源支撑。ChatGPT的成功得益于高质量的标注数据,OpenAI为了获得高质量的训练数据雇佣了超过40家承包商进行数据标注,这在国内几乎是不敢想象的事情。另外以ChatGPT在2023年1月的独立访客平均数1300万计算,其对应芯片需求为3万多片英伟达A100 GPU,如果没有庞大的算力支撑,很难训练出高质量的通用大模型。


也就是说,通用大模型的训练需要一个完整的人工智能技术体系来进行支撑,否则即便有先进的算法,但如何获得高质量的训练数据,到哪里去找庞大的训练算力资源,这些都是摆在通用大模型训练面前必须要解决的问题,否则没有高质量的数据以及算力支撑也是巧妇难为无米之炊。


如何破局


通用大模型是未来人工智能的重要发展方向,我们肯定要加大资源投入力度才能有机会在未来人工智能前沿技术的竞争中占有一席之地。那我们应该怎么做才能破局呢?我想可以从以下几个方面入手。


数据方面


虽然当前中文在线联网数据也算是海量,但是数据质量普遍不高,同时数据孤岛问题比较严重。如果我们想基于中文数据来训练通用大模型,就必须对中文在线数据进行治理提高中文在线数据质量,解决数据孤岛问题,推动实现数据的共享和互联互通。


算力建设方面


结合国家东数西算工程,协调各个区域的算力中心,形成全国通用算力基础设施,加快构建国家级一体化新型算力网络体系,为通用大模型训练提供源源不断的强大算力支撑。只有这样,才能满足大模型训练所需要的算力要求,同时为强人工智能时代的到来做好算力资源储备。



意识方面


和别人的技术差距,我们当然要尽力弥补。不过我们需要注意的是我们在进步,别人也在进步,所以我们光追赶别人不行,还要有更加长远的眼光,要有对于未来准确的判断力,进行提前技术验证以及产业布局,为下一次技术革命做准备,真正成为未来技术发展的引领者而不是追随者。


总结


科技的发展进步不是一朝一夕就能实现的,它可能需要一代又一代技术人员孜孜不倦地追求。虽然我们在很多领域都已经取得了长足的进步,但是同时我们也要清醒地认识到,在计算机领域、互联网技术领域以及人工智能技术领域我们还有不小的差距需要弥补。因此少一点网红、明星的曝光,多一点科技工作者勤恳务实地宣传,努力营造技术为先、科技为先的社会氛围才是未来科技行业蓬勃发展的社会环境基础。我相信如果我们可以少一点浮躁,多一点纯粹,也许下一次的科技革命就是在中国这片土地上诞生。最后和大家分享下马老师曾经说过的一段话,我觉得对我们当下思考未来技术发展非常有帮助。



我一直觉得填补空白这句话是有问题的,不是因为欧美的就是先进的,就是我们要去填补的。其实今天我们不应该要和哪个东西接轨,适应哪国的标准,填补哪个空白,今天我们要思考的是怎么和未来接轨,怎么适应未来的标准,怎么弥补未来的空白,我们要想明白未来是如何的,以及自己到底要做成一个什么样的体系,然后再去看看别人怎么做,如果永远重复别人的语言,讨论别人设定的主题,我们不但会迷失现在,而且会错失未来。


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

一个令人惊艳的ChatGPT项目,开源了!

最近在GitHub上发现了一个爆火的开源项目。 好家伙,凑近一看,居然还是由微软开源,并且和最近炙手可热的ChatGPT息息相关。 项目的名字叫做:Visual ChatGPT。 github.com/microsoft/v… 这个项目最早是3月上旬微软...
继续阅读 »

最近在GitHub上发现了一个爆火的开源项目。


好家伙,凑近一看,居然还是由微软开源,并且和最近炙手可热的ChatGPT息息相关


项目的名字叫做:Visual ChatGPT



github.com/microsoft/v…




这个项目最早是3月上旬微软开源的,项目宣布开源后仅用了短短一周,就斩获了2w+ star。



到现在为止,距离当初项目开源大约过去了3周多,仓库star数则来到了28k+,亦可谓是火箭式上涨(doge)。


众所周知,ChatGPT自2022年11月推出以来,持续走红。



ChatGPT具备强大的会话能力,可以理解文字、聊天、写小说、解答问题、编写代码... 但是目前还并不能直接处理或生成图像。


而Visual ChatGPT这个项目则可以把ChatGPT和一系列视觉基础模型(VFM,Visual Foundation Model)给联系起来,以便实现在ChatGPT聊天的过程中来发送和接收图像,也使得ChatGPT能够处理更为复杂的视觉任务。



讲白了,就是通过Visual ChatGPT,可以把一系列视觉基础模型给接入ChatGPT,使得ChatGPT能胜任更为复杂的视觉处理任务。



Visual ChatGPT的整体技术架构图如上所示,我们可以清楚地看到ChatGPT和视觉基础模型(VFM,Visual Foundation Model)分别位于其中的位置。


一方面,ChatGPT(或LLM)作为一个通用接口,继续发挥它本身的优势,提供对不同话题的智能理解。另一方面,基础视觉模型VFM则通过提供特定领域的深入知识来充当领域专家,它们通过交互管理模块(Prompt Manger)进行连接和适配。


这样聊可能比较抽象,我们可以拿官方给的一个例子来进行说明:



1、首先是用户:输入一张黄色的向日葵图片,并且要求ChatGPT根据该图像预测深度来生成一朵红花,然后再一步一步将其做成卡通画。


2、接着是交互管理模块(Prompt Manger)发挥作用,在它的协调和控制下,VFM模块开始发挥作用:



  • 首先需要运用深度估计模型来预测并生成图像的深度信息;

  • 然后需要运用深度图像模型来生成对应空间深度的红花图像;

  • 最后运用Stable Diffusion的风格迁移模型来完成图像风格的变换。


3、最后Visual ChatGPT系统再将最终结果返回给用户,完成本次对话。


说到这里,有兴趣的小伙伴可以可以看看微软给出的一篇有关Visual ChatGPT的论文。



里面关于这部分的流程解释得非常详细,而且还给出了多轮对话的案例、以及实验结果,有兴趣的小伙伴可以看看。


好了,今天的分享就到这里了,感谢大家的收看,我们下篇见。


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

阿里版ChatGPT:通义千问pk文心一言

随着 ChatGPT 热潮卷起来,百度发布了文心一言、Google 发布了 Bard,「阿里云」官方终于也宣布了,旗下的 AI 大模型“通义千问”正式开启测试! 申请地址:tongyi.aliyun.com/ 阿里云的 Ai 模型名为“通义千问”,由达摩院开...
继续阅读 »

随着 ChatGPT 热潮卷起来,百度发布了文心一言、Google 发布了 Bard,「阿里云」官方终于也宣布了,旗下的 AI 大模型“通义千问”正式开启测试!



申请地址:tongyi.aliyun.com/


阿里云的 Ai 模型名为“通义千问”,由达摩院开发,也是一款 NLP 大语言模型,主要能力也是专注在文本生成,可以做到跟 ChatGPT 类似的“你问我答”。


获得测试申请邀请码方法:



  • 注册一个阿里云帐号 (注册按钮在页面右上角)

  • 前往「通义千问」官网 tongyi.aliyun.com/

  • 填写「企业邮箱」进行报名预约,等待审核


官网宣称只邀请企业进行测试,但只要用户在报名时「使用企业邮箱」,并随便填写一个公司的名称信息即可。


 


通义千问pk文心一言


拿到测试邀请码以后,我第一时间试用了一下,并且和百度的文心一言做了对比。


为什么不直接对比ChatGPT,因为我的ChatGPT账号已经被封了哈哈。



 


 


提问:通义千问和文心一言谁更厉害?



 


 


 



 


 


 


 


提问:李明是李丽的哥哥,刘云是李丽的妈妈,李明是刘云的谁?



 


 


 



 


 


 


 


提问:通义千问没有AI绘画功能:


 



 


 



 


 


根据官网描述,通义千问是一个能专门响应人类指令的大模型,可以是一名效率助手,也可以是点子生成器。


可以非常方便地帮我们写作、写文案、翻译、解答问题等工作,支持上下文关联理解。其中官方表示比较出色的地方在于撰写文案,经测试,当说要它写一篇语文老师要求的“总分总”结构的文章它也能听得懂。


国产版ChagtGPT通义千问和文心一言,最后谁能胜出,你怎么看?


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

Android Key Value存储技术选型

一、 SP 问题: 卡顿anr 问题1: 写入大数据\当前资源较紧张情况进行写入, 切换页面(执行onstop), 会出现卡顿 1.1. SharedPreferencesImpl.apply, 异步操作 @Override public void appl...
继续阅读 »

一、 SP 问题: 卡顿anr


问题1: 写入大数据\当前资源较紧张情况进行写入, 切换页面(执行onstop), 会出现卡顿


1.1. SharedPreferencesImpl.apply, 异步操作

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

QueuedWork.addFinisher(awaitCommit);

Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};

SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
...
}


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

final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);
writtenToDiskLatch.countDown();//写完文件执行countDown
}
..
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};

1.2 ActivityThread.handleStopActivity(), onStop 生命周期

@Override
public void handleStopActivity(ActivityClientRecord r, int configChanges,
PendingTransactionActions pendingActions, boolean finalStateRequest, String reason) {
...

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

重点关注


if (!r.isPreHoneycomb()) { QueuedWork.waitToFinish(); }


public static void waitToFinish() {
try {
while (true) {
Runnable finisher;
synchronized (sLock) {
finisher = sFinishers.poll();
}
if (finisher == null) {
break;
}

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

while循环中finisher会阻塞当前线程,等待完成写入文件任务


问题2: sp本地文件巨大,初始化阶段(还未初始化完成), 去读sp数据, 会出现卡顿


2.1 初始化

SharedPreferencesImpl(File file, int mode) {
...
mLoaded = false;
...
startLoadFromDisk();
}
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}

2.2 从磁盘读取数据到内存

private void loadFromDisk() {
synchronized (mLock) {
if (mLoaded) {
return;
}
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
...

Map<String, Object> map = null;
...
try {
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
str = new BufferedInputStream(
new FileInputStream(mFile), 16 * 1024);
map = (Map<String, Object>) XmlUtils.readMapXml(str);
} catch (Exception e) {
Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
} finally {
IoUtils.closeQuietly(str);
}
}
...
synchronized (mLock) {
mLoaded = true;
...
}
finally{
//notify 线程
mLock.notifyAll();
}
}

2.3 获取数据

public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}

private void awaitLoadedLocked() {
...
while (!mLoaded) {
try {
//阻塞线程
mLock.wait();
} catch (InterruptedException unused) {
}
}
...
}

关键字: mLoaded、 mLock.wait() 、 mLock.notifyAll()


二、 mmkv 问题: 数据损坏


2.1 原理 mmap 内存映射文件


MMKV原理


2.2 问题


image.png




  • 应用程序异常退出或崩溃: 当应用程序在写入或更新数据过程中突然退出或崩溃时,可能导致MMKV数据损坏。例如,应用程序在写入数据时遇到内存不足或其他异常情况,可能会导致数据写入不完整。




  • 系统意外重启或关机: 如果设备在MMKV写入或更新数据过程中突然重启或关机,可能导致MMKV数据损坏。这种情况下,操作系统可能没有足够的时间将内存映射文件的内容写入磁盘。




三、DataStore


开发者指南


关键字: SingleProcessDataStore.updateData、downloadFlow:通过flow实现内存缓存


写文件

数据源->actor协程管理改为顺序执行->通过serializer写入文件中去 -> 同步内存缓存值


protoBuffer写文件,->存入内存缓存 保证了数据一致性


优点


  • 基于Flow,保证线程安全性

  • 可以监听到成功和失败

  • 自动完成 SharedPreferences 迁移到 DataStore,保证数据一致性,不会造成数据损坏


您可以使用 runBlocking() 从 DataStore 同步读取数据


http://www.jianshu.com/p/90b152565…


使用


  1. 创建preferenceDataStore


val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")


  1. 读取内容


val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
val exampleCounterFlow: Flow<Int> = context.dataStore.data
  .map { preferences ->
    // No type safety.
    preferences[EXAMPLE_COUNTER] ?: 0
}


  1. 写入内容



Preferences DataStore 提供了一个 edit() 函数,用于以事务方式更新 DataStore 中的数据。该函数的 transform 参数接受代码块,您可以在其中根据需要更新值。转换块中的所有代码均被视为单个事务。



suspend fun incrementCounter() {
  context.dataStore.edit { settings ->
    val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
    settings[EXAMPLE_COUNTER] = currentCounterValue + 1
  }
}


  1. 同步方式使用DataStore


val exampleData = runBlocking { context.dataStore.data.first() }

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

Android 使用AIDL传输超大型文件

最近在写车载Android的第5篇视频教程「AIDL的实践与封装」时,遇到一个有意思的问题,能不能通过AIDL传输超过 1M 以上的文件? 我们先不细究,为什么要用AIDL传递大文件,单纯从技术的角度考虑能不能实现。众所周知,AIDL是一种基于Binder实现...
继续阅读 »

最近在写车载Android的第5篇视频教程「AIDL的实践与封装」时,遇到一个有意思的问题,能不能通过AIDL传输超过 1M 以上的文件?


我们先不细究,为什么要用AIDL传递大文件,单纯从技术的角度考虑能不能实现。众所周知,AIDL是一种基于Binder实现的跨进程调用方案,Binder 对传输数据大小有限制,传输超过 1M 的文件就会报 android.os.TransactionTooLargeException 异常。


如果文件相对比较小,还可以将文件分片,大不了多调用几次AIDL接口,但是当遇到大型文件或超大型文件时,这种方法就显得耗时又费力。好在,Android 系统提供了现成的解决方案,其中一种解决办法是,使用AIDL传递文件描述符ParcelFileDescriptor,来实现超大型文件的跨进程传输。


ParcelFileDescriptor


ParcelFileDescriptor 是一个实现了 Parcelable 接口的类,它封装了一个文件描述符 (FileDescriptor),可以通过 Binder 将它传递给其他进程,从而实现跨进程访问文件或网络套接字。ParcelFileDescriptor 也可以用来创建管道 (pipe),用于进程间的数据流传输。


ParcelFileDescriptor 的具体用法有以下几种:




  • 通过 ParcelFileDescriptor.createPipe() 方法创建一对 ParcelFileDescriptor 对象,分别用于读写管道中的数据,实现进程间的数据流传输。




  • 通过 ParcelFileDescriptor.fromSocket() 方法将一个网络套接字 (Socket)转换为一个 ParcelFileDescriptor 对象,然后通过 Binder 将它传递给其他进程,实现跨进程访问网络套接字。




  • 通过 ParcelFileDescriptor.open() 方法打开一个文件,并返回一个 ParcelFileDescriptor 对象,然后通过 Binder 将它传递给其他进程,实现跨进程访问文件。




  • 通过 ParcelFileDescriptor.close() 方法关闭一个 ParcelFileDescriptor 对象,释放其占用的资源。




ParcelFileDescriptor.createPipe()和ParcelFileDescriptor.open() 都可以实现,跨进程文件传输,接下来我们会分别演示。


实践



  • 第一步,定义AIDL接口


interface IOptions {
void transactFileDescriptor(in ParcelFileDescriptor pfd);
}


  • 第二步,在「传输方」使用ParcelFileDescriptor.open实现文件发送


private void transferData() {
try {
// file.iso 是要传输的文件,位于app的缓存目录下,约3.5GB
ParcelFileDescriptor fileDescriptor = ParcelFileDescriptor.open(new File(getCacheDir(), "file.iso"), ParcelFileDescriptor.MODE_READ_ONLY);
// 调用AIDL接口,将文件描述符的读端 传递给 接收方
options.transactFileDescriptor(fileDescriptor);
fileDescriptor.close();

} catch (Exception e) {
e.printStackTrace();
}
}


  • 或,在「传输方」使用ParcelFileDescriptor.createPipe实现文件发送


ParcelFileDescriptor.createPipe 方法会返回一个数组,数组中的第一个元素是管道的读端,第二个元素是管道的写端。


使用时,我们先将「读端-文件描述符」使用AIDL发给「接收端」,然后将文件流写入「写端」的管道即可。


    private void transferData() {
try {
/******** 下面的方法也可以实现文件传输,「接收端」不需要任何修改,原理是一样的 ********/
// createReliablePipe 创建一个管道,返回一个 ParcelFileDescriptor 数组,
// 数组中的第一个元素是管道的读端,
// 第二个元素是管道的写端
ParcelFileDescriptor[] pfds = ParcelFileDescriptor.createReliablePipe();
ParcelFileDescriptor pfdRead = pfds[0];
// 调用AIDL接口,将管道的读端传递给 接收端
options.transactFileDescriptor(pfdRead);
ParcelFileDescriptor pfdWrite = pfds[1];
// 将文件写入到管道中
byte[] buffer = new byte[1024];
int len;
try (
// file.iso 是要传输的文件,位于app的缓存目录下
FileInputStream inputStream = new FileInputStream(new File(getCacheDir(), "file.iso"));
ParcelFileDescriptor.AutoCloseOutputStream autoCloseOutputStream = new ParcelFileDescriptor.AutoCloseOutputStream(pfdWrite);
) {
while ((len = inputStream.read(buffer)) != -1) {
autoCloseOutputStream.write(buffer, 0, len);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}


注意,管道写入的文件流 总量限制在64KB,所以「接收方」要及时将文件从管道中读出,否则「传输方」的写入操作会一直阻塞。




  • 第三步,在「接收方」读取文件流并保存到本地


private final IOptions.Stub options = new IOptions.Stub() {
@Override
public void transactFileDescriptor(ParcelFileDescriptor pfd) {
Log.i(TAG, "transactFileDescriptor: " + Thread.currentThread().getName());
Log.i(TAG, "transactFileDescriptor: calling pid:" + Binder.getCallingPid() + " calling uid:" + Binder.getCallingUid());
File file = new File(getCacheDir(), "file.iso");
try (
ParcelFileDescriptor.AutoCloseInputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(pfd);
) {
file.delete();
file.createNewFile();
FileOutputStream stream = new FileOutputStream(file);
byte[] buffer = new byte[1024];
int len;
// 将inputStream中的数据写入到file中
while ((len = inputStream.read(buffer)) != -1) {
stream.write(buffer, 0, len);
}
stream.close();
pfd.close();
} catch (IOException e) {
e.printStackTrace();
}
}
};


  • 运行程序


在程序运行之前,需要将一个大型文件放置到client app的缓存目录下,用于测试。目录地址:data/data/com.example.client/cache。



注意:如果使用模拟器测试,模拟器的硬盘要预留 3.5GB * 2 的闲置空间。



将程序运行起来,可以发现,3.5GB 的 file.iso 顺利传输到了Server端。



大文件是可以传输了,那么使用这种方式会很耗费内存吗?我们继续在文件传输时,查看一下内存占用的情况,如下所示:



  • 传输方-Client,内存使用情况




  • 接收方-Server,内存使用情况



从Android Studio Profiler给出的内存取样数据可以看出,无论是传输方还是接收方的内存占用都非常的克制、平缓。


总结


在编写本文之前,我在掘金上还看到了另一篇文章:一道面试题:使用AIDL实现跨进程传输一个2M大小的文件 - 掘金


该文章与本文类似,都是使用AIDL向接收端传输ParcelFileDescriptor,不过该文中使用共享内存MemoryFile构造出ParcelFileDescriptor,MemoryFile的创建需要使用反射,对于使用MemoryFile映射超大型文件是否会导致内存占用过大的问题,我个人没有尝试,欢迎有兴趣的朋友进行实践。


总得来说 ParcelFileDescriptor 和 MemoryFile 的区别有以下几点:



  • ParcelFileDescriptor 是一个封装了文件描述符的类,可以通过 Binder 传递给其他进程,实现跨进程访问文件或网络套接字。MemoryFile 是一个封装了匿名共享内存的类,可以通过反射获取其文件描述符,然后通过 Binder 传递给其他进程,实现跨进程访问共享内存。

  • ParcelFileDescriptor 可以用来打开任意的文件或网络套接字,而 MemoryFile 只能用来创建固定大小的共享内存。

  • ParcelFileDescriptor 可以通过 ParcelFileDescriptor.createPipe() 方法创建一对 ParcelFileDescriptor 对象,分别用于读写管道中的数据,实现进程间的数据流传输。MemoryFile 没有这样的方法,但可以通过 MemoryFile.getInputStream() 和 MemoryFile.getOutputStream() 方法获取输入输出流,实现进程内的数据流传输。


在其他领域的应用方面,ParcelFileDescriptor 和 MemoryFile也有着性能上的差异,主要取决于两个方面:



  • 数据的大小和类型。


如果数据是大型的文件或网络套接字,那么使用 ParcelFileDescriptor 可能更合适,因为它可以直接传递文件描述符,而不需要复制数据。如果数据是小型的内存块,那么使用 MemoryFile 可能更合适,因为它可以直接映射到物理内存,而不需要打开文件或网络套接字。



  • 数据的访问方式。


如果数据是需要频繁读写的,那么使用 MemoryFile 可能更合适,因为它可以提供输入输出流,实现进程内的数据流传输。如果数据是只需要一次性读取的,那么使用 ParcelFileDescriptor 可能更合适,因为它可以通过 ParcelFileDescriptor.createPipe() 方法创建一对 ParcelFileDescriptor 对象,分别用于读写管道中的数据,实现进程间的数据流传输。


本文示例demo的地址:github.com/linxu-link/…


好了,以上就是本文的所有内容了,感谢你的阅读,希望对你有所帮助。


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

Kotlin 协程中的并发问题:我明明用 mutex 上锁了,为什么没有用?

前言 最近在接手的某项目中,主管给我发来了一个遗留以久的 BUG,让我看看排查一下,把它修复了。 项目的问题大概是在某项业务中,需要向数据库插入数据,而且需要保证同种类型的数据只被插入一次,但是现在却出现了数据被重复插入的情况。 我点开代码一看,上一个跑路的老...
继续阅读 »

前言


最近在接手的某项目中,主管给我发来了一个遗留以久的 BUG,让我看看排查一下,把它修复了。


项目的问题大概是在某项业务中,需要向数据库插入数据,而且需要保证同种类型的数据只被插入一次,但是现在却出现了数据被重复插入的情况。


我点开代码一看,上一个跑路的老哥写的非常谨慎啊,判断重复的逻辑嵌套了一层又一层,先在本地数据库查询一次没有重复后又请求服务器查询一次,最后在插入前再查询本地数据库一次。总共写了三层判重逻辑。但是为什么还是重复了呢?


再细看,哦,原来是用了协程异步查询啊,怪不得。


可是,不对啊,你不是用 Mutex 上锁了吗?怎么还会重复?


Mutex 你在干什么?你锁了什么?你看看你都守护了什么啊。


此时的 Mutex 就像我一般,什么都守护不住。


但是,真的怪 Mutex 吗?这篇文章我们就来浅析一下使用 Mutex 实现协程的并发可能导致失效的问题,为我们老实本份的 Mutex 洗清冤屈。


前置知识:关于协程和并发


众所周知,对于多线程程序,可能会出现同步问题,例如,下面这个经典的例子:


fun main() {
var count = 0

runBlocking {
repeat(1000) {
launch(Dispatchers.IO) {
count++
}
}
}

println(count)
}

你们说,以上代码会输出什么?


我不知道,我也没法知道,没错,确实是这样的。


因为在上面的代码中,我们循环 1000 次,每次都启动一个新的协程,然后在协程中对 count 进行自增操作。


问题就在于,我们没法保证对 count 的操作是同步的,因为我们不知道这些协程何时会被执行,也无法保证这些协程在执行时 count 的值没有被其他协程修改过。


这就导致,count 值最终会是不确定的。


另一个众所周知,kotlin 中的协程其实可以简单理解成对线程的封装,所以实际上不同的协程可能运行在同一个线程也可能运行在不同的线程。


我们给上面的代码加一个打印所在线程:


fun main() {
var count = 0

runBlocking {
repeat(1000) {
launch(Dispatchers.IO) {
println("Running on ${Thread.currentThread().name}")
count++
}
}
}

println(count)
}

截取其中一部分输出:


Running on DefaultDispatcher-worker-1
Running on DefaultDispatcher-worker-4
Running on DefaultDispatcher-worker-3
Running on DefaultDispatcher-worker-2
Running on DefaultDispatcher-worker-5
Running on DefaultDispatcher-worker-5
Running on DefaultDispatcher-worker-2
Running on DefaultDispatcher-worker-6
Running on DefaultDispatcher-worker-2
Running on DefaultDispatcher-worker-2
Running on DefaultDispatcher-worker-7
Running on DefaultDispatcher-worker-7
Running on DefaultDispatcher-worker-7
Running on DefaultDispatcher-worker-7

……

可以看到,不同的协程可能运行在不同的线程上,也可能同一个线程会被用来运行不同的协程。由于这个特性,所以协程也会存在多线程的并发问题。


那么,什么是并发呢?


简单理解,就是在同一个时间段内执行多个任务,此时为了实现这个目的,不同的任务可能会被拆分开来穿插着执行。


与之对应的,还有一个并行的概念,简单说就是多个任务在同一个时间点一起执行:


1.png


总之,不管是并行还是并发,都会涉及到对资源的“争夺”问题,因为在同一时间可能会有多个线程需要对同一个资源进行操作。此时就会出现上面举例的情况,由于多个线程都在对 count 进行操作,所以导致最终 count 的值会小于 1000,这也很好理解,比如此时 count 是 1,被线程 1 读取到之后,线程 1 开始对它进行 +1 操作,但是在线程1还没写完的时候,来了个线程2,也读了一下 count 发现它是1,也对它进行 +1 操作。此时,不管线程1和2谁先写完,最终 count 也只会是 2,显然,按照我们的需求,应该是想让它是 3 才对。


那解决这个也简单啊,我们就不要让有这么多线程不就行了,只要只有一个线程不就行了?


确实,我们指定所有协程只在一个线程上执行:


fun main() {
// 创建一个单线程上下文,并作为启动调度器
val dispatcher = newSingleThreadContext("singleThread")

var count = 0

runBlocking {
repeat(1000) {
// 这里也可以直接不指定调度器,这样就会使用默认的线程执行这个协程,换言之,都是在同一个线程执行
launch(dispatcher) {
println("Running on ${Thread.currentThread().name}")
count++
}
}
}

println(count)
}

截取最后的输出结果如下:


……
Running on singleThread
Running on singleThread
Running on singleThread
Running on singleThread
Running on singleThread
Running on singleThread
Running on singleThread
Running on singleThread
Running on singleThread
Running on singleThread
Running on singleThread
1000

Process finished with exit code 0

可以看到,输出的 count 结果终于是正确了,那么,为什么还会有我这篇文章的问题呢?


哈哈,其实你被我绕进去了。


我们用协程(线程)的目的是什么?不就是为了能够执行耗时任务或者可以让多个任务同时执行,减少执行时间吗?即然你都用单线程了,那么有什么意义?


毕竟这里我们举例的代码只对 count 这一个变量进行操作,确实没有开多线程的必要,但是实际工作中肯定不止这么一个操作啊,难道我们要因为某个变量被其他线程占用了就不继续往下走了?就这么呆呆的阻塞住原地等待?显然不现实,醒醒吧,世界不是只有 count ,还有很多数据等待我们处理。所以我们用多线程的目的就是为了能够在某个变量(资源)不可用的时候可以去处理其他未被占用的资源,从而缩短总的执行时间。


但是,如果其他的代码执行到一定程度,绕不开必须要使用被占用的资源怎么办?


不管正在占用的线程是否解除占用直接硬去拿这个资源继续处理?显然不现实,因为这样就会造成我们前言中所述的情况发生。


所以如果我们遇到需要使用被占用的资源时,应当暂停当前线程,直至占用被解除。


在 java 中通常有三种方式解决这个问题:



  1. synchronized

  2. AtomicInteger

  3. ReentrantLock


但是在 kotlin 的协程中使用它们不太合适,因为协程是非阻塞式的,当我们需要协程“暂停”的时候(如 delay(1000)),协程通常是被挂起,挂起的协程并不会阻塞它所在的线程,此时这个线程就可以腾出身去执行其他的任务。


而在 java 中需要线程暂停时(如 Thread.sleep(1000)),通常就是直接阻塞这个线程,此时这个线程就会被限制,直到阻塞结束。


在 kotlin 中,提供了一个轻量级的同步锁:Mutex


什么是 Mutex


Mutex 是在 kotlin 协程中用于替代 java 线程中 synchronizedReentrantLock 的类,用于为不应该被多个协程同时执行的代码上锁,例如为前面例子中的 count 自增代码上锁,这样可以保证它在同一时间点只会被一个协程执行,从而避免了由于多线程导致的数据修改问题。


Mutex 有两个核心方法: lock()unlock() ,分别用于上锁和解锁:


fun main() {
var count = 0
val mutex = Mutex()

runBlocking {
repeat(1000) {
launch(Dispatchers.IO) {
println("Running on ${Thread.currentThread().name}")
mutex.lock()
count++
mutex.unlock()
}
}
}

println(count)
}

上述代码输出截取如下:


……
Running on DefaultDispatcher-worker-47
Running on DefaultDispatcher-worker-20
Running on DefaultDispatcher-worker-38
Running on DefaultDispatcher-worker-15
Running on DefaultDispatcher-worker-14
Running on DefaultDispatcher-worker-19
Running on DefaultDispatcher-worker-48
1000

Process finished with exit code 0

可以看到,虽然协程运行在不同的线程,但是依然能够正确的对 count 进行修改操作。


这是因为我们在修改 count 值时调用了 mutex.lock() 此时保证了之后的代码块仅允许被当前协程执行,直至调用 mutex.unlock() 解除了锁定,其他协程才能继续执行这个代码块。


Mutex 的 lockunlock 原理可以简单的理解成,当调用 lock 时,如果这个锁没有被其他协程持有则持有该锁,并执行后面的代码;如果这个锁已经被其他协程持有,则当前协程进入挂起状态,直至锁被释放,并拿到了锁。当被挂起时,它所在的线程并不会被阻塞,而是可以去执行其他的任务。详细的原理可以看看参考资料2。


在实际使用中,我们一般不会直接使用 lock()unlock() ,因为如果在上锁后执行的代码中出现异常的话,将会造成持有的锁永远不会被释放,此时就会造成死锁,其他的协程将永远等待不到这个锁被释放,从而永远被挂起:


fun main() {
var count = 0
val mutex = Mutex()

runBlocking {
repeat(1000) {
launch(Dispatchers.IO) {
try {
mutex.lock()
println("Running on ${Thread.currentThread().name}")
count++
count / 0
mutex.unlock()
} catch (tr: Throwable) {
println(tr)
}
}
}
}

println(count)
}

上述代码输出:


Running on DefaultDispatcher-worker-1
java.lang.ArithmeticException: / by zero

并且程序将会一直执行下去,无法终止。


其实要解决这个问题也很简单,我们只需要加上 finally ,使这段代码无论是否执行成功都要释放掉锁即可:


fun main() {
var count = 0
val mutex = Mutex()

runBlocking {
repeat(1000) {
launch(Dispatchers.IO) {
try {
mutex.lock()
println("Running on ${Thread.currentThread().name}")
count++
count / 0
mutex.unlock()
} catch (tr: Throwable) {
println(tr)
} finally {
mutex.unlock()
}
}
}
}

println(count)
}

上述代码输出结果截取如下:


……

Running on DefaultDispatcher-worker-45
java.lang.ArithmeticException: / by zero
Running on DefaultDispatcher-worker-63
java.lang.ArithmeticException: / by zero
Running on DefaultDispatcher-worker-63
java.lang.ArithmeticException: / by zero
Running on DefaultDispatcher-worker-63
java.lang.ArithmeticException: / by zero
1000

Process finished with exit code 0

可以看到,虽然每个协程都报错了,但是程序是能执行完毕的,不会被完全挂起不动。


其实这里我们可以直接使用 Mutex 的扩展函数 withLock


fun main() {
var count = 0
val mutex = Mutex()

runBlocking {
repeat(1000) {
launch(Dispatchers.IO) {
mutex.withLock {
try {
println("Running on ${Thread.currentThread().name}")
count++
count / 0
} catch (tr: Throwable) {
println(tr)
}
}
}
}
}

println(count)
}

上述代码输出内容截取如下:


……
Running on DefaultDispatcher-worker-31
java.lang.ArithmeticException: / by zero
Running on DefaultDispatcher-worker-31
java.lang.ArithmeticException: / by zero
Running on DefaultDispatcher-worker-51
java.lang.ArithmeticException: / by zero
Running on DefaultDispatcher-worker-51
java.lang.ArithmeticException: / by zero
Running on DefaultDispatcher-worker-51
java.lang.ArithmeticException: / by zero
1000

可以看到,使用 withLock 后就不需要我们自己处理上锁和解锁了,只需要把要保证只被同时执行一次的代码放进它的参数中的高阶函数里就行。


这里看一下 withLock 的源码:


public suspend inline fun <T> Mutex.withLock(owner: Any? = null, action: () -> T): T {
// ……

lock(owner)
try {
return action()
} finally {
unlock(owner)
}
}

其实也非常的简单,就是在执行我们传入的 action 函数前调用 lock() 执行完毕后在 finally 中调用 unlock()


说了这么多,可能读者想问了,你在这讲了半天,是不是偏题了啊?你的标题呢?怎么还不说?


别急别急,这不就来了吗?


为什么我都 mutex.withLock 了却没用呢?


回到我们的标题和前言中的场景,为什么项目中明明使用了 mutex.Unlock 将查重代码上锁了,还是会出现重复插入的情况?


我知道你很急,但是你别急,容我再给你看个例子:


fun main() {
var count = 0
val mutex = Mutex()

runBlocking {
mutex.withLock {
repeat(10000) {
launch(Dispatchers.IO) {
count++
}
}
}
}

println(count)
}

你猜这段代码能输出 10000 吗?再看一段代码:


fun main() {
var count = 0
val mutex = Mutex()

runBlocking {
mutex.withLock {
repeat(100) {
launch(Dispatchers.IO) {
repeat(100) {
launch(Dispatchers.IO) {
count++
}
}
}
}
}
}

println(count)
}

这段呢?你们猜能输出 10000 吗?


其实只要我们稍微想一想就知道,这个显然不可能输出 10000 啊。


虽然我们在最顶层加了 mutex.lockWith 。但是,我们却在其中新开了很多新的协程,这就意味着,事实上这个锁约等于没有加。


还记得我们上面看过的 mutex.lockWith 的源码吗?


此处相当于刚 lock 上,启动了一个新协程,直接 unlock 了,但是实际需要上锁的代码应该是新启动的协程里面的代码啊。


所以,我们在上锁时应该尽可能的缩小上锁的粒度,只给需要的代码上锁:


fun main() {
var count = 0
val mutex = Mutex()

runBlocking {
repeat(100) {
launch(Dispatchers.IO) {
repeat(100) {
launch(Dispatchers.IO) {
mutex.withLock {
count++
}
}
}
}
}
}

println(count)
}

这里,我们需要上锁的其实就是对 count 的操作,所以我们只需要把上锁代码加给 count++ 即可,运行代码,完美输出 10000 。


有了上面的铺垫,我们再来看看我接手项目的简化代码原型:


fun main() {
val mutex = Mutex()

runBlocking {
mutex.withLock {
// 模拟同时调用了很多次插入函数
insertData("1")
insertData("1")
insertData("1")
insertData("1")
insertData("1")
}
}
}

fun insertData(data: String) {
CoroutineScope(Dispatchers.IO).launch {
// 这里写一些无关数据的业务逻辑
// xxxxxxx

// 这里进行查重 查重结果 couldInsert
if (couldInsert) {
launch(Dispatchers.IO) {
// 这里将数据插入数据库
}
}
}
}

你们猜,此时数据库会被插入几个 1


答案显然是无法预知,一二三四五次都有可能。


我们来猜一猜,这哥们在写这段代码时的心路历程:



产品:这里的插入数据需要注意一个类型只让插入一个数据啊

开发:好嘞,这还不简单,我在插入前加个查重就行了

提测后

测试:开发兄弟,你这里有问题啊,这个数据可以被重复插入啊

开发:哦?我看看,哦,这里查询数据库用了协程异步执行,那不就是并发问题吗?我搜搜看 kotlin 的协程这么解决并发,哦,用 mutex 啊,那简单啊。

于是开发一顿操作,直接在调用查重和插入数据的最上级函数中加了个 mutex.withlock 将整个处理逻辑全部上锁。并且觉得这样就万无一失了,高枕无忧了,末了还不忘给 kotlin 点个赞,加锁居然这么方便,不像 java 还得自己写一堆处理代码。

那么,我是这么解决这个问题的呢?最好的解决方案,其实应该是能够将上锁粒度细化到具体的数据库操作的地方,但是还记得我上面说的吗,这个项目中嵌套了一层又一层的查询代码,想要在这其中插入上锁代码显然不容易,我可不想因为往里面插一个锁直接导致整座大山倒塌。


所以我的选择是给每个 launch 了新协程的地方又加了一堆锁……


这座山,因为我,变得更高了,哈哈哈哈哈。


所以,其实并不是 mutex 有问题,有问题的只是使用的人罢了。


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

我遇到的一个难题,早在1966年就已经有解决方案了...

1. 起因 这一切还得从前段时间接到的一个需求讲起。 业务方需要离线计算一批A附近5公里内的B,并统计聚合B的相关指标,进行展示。 嗯,听起来很合理。🤔 2. 问题 虽然在进行前期评估时,就已经预料到计算量会很大(当时的计算方案十分简陋)。 但在实际运行过程中...
继续阅读 »

1. 起因


这一切还得从前段时间接到的一个需求讲起。

业务方需要离线计算一批A附近5公里内的B,并统计聚合B的相关指标,进行展示。


嗯,听起来很合理。🤔


2. 问题


虽然在进行前期评估时,就已经预料到计算量会很大(当时的计算方案十分简陋)。

但在实际运行过程中,还是发现附近5km的逻辑,计算效率过于低下(按照城市编码将数据拆分为3个任务进行并行计算,但平均耗时还是会达到7-10个小时,无法接受)😦


3. 一些尝试


3.1 第一版:


最开始的计算逻辑很粗暴:把每个城市内的A和B进行full join,然后根据经纬度逐个计算距离,排除掉超出距离限制的集合。肉眼可见的低效。。。


c27f53441c4b15687e02c45821dbd306.gif


3.2 第二版:


由于全量计算十分耗时,并且大部分B的坐标也不会经常变更,因此开始考虑使用增量计算进行优化,减少重复计算。

但在实际任务运行过程中发现,大量耗时用在了历史数据和新增数据的合并写入,并没有有效的效率提升。


322fe630-a978-43d8-9284-78b0865067d3.jpg


3.3 第三版:


这个时候,已经没有什么优化的头绪了。只是一次偶然的搜索,让我发现了一个全新的实现逻辑。(没错,面向google编程)


ad5f2626-5584-4ca1-8809-567474172f11.jpg


一个周五的晚上,脑袋里思索着通过经纬度计算距离的逻辑,突然一个想法出现:既然经纬度可以进行距离计算,是否意味着经纬度的数字也是和距离有着一定的转换关系,比如经度小数点后1位,代表距离xx公里?


带着这个疑问,我发现了这两篇文章。


image.png


其中 高效的多维空间点索引算法 — Geohash 和 Google S2介绍的案例,与我的需求十分相似。(大神的文章值得好好阅读)


里面提到的geohash算法,则是1966年就有人提出的一种将经纬度坐标点进行一维化编码的解决思路。而后续的google的s2、uber的h3,均是在此设计理念的基础上优化而来的。


这种算法的本质就是对地球的每一块区域都进行编码(精度可调),也就是一个编码代表着一段经纬度范围内的区域。


那么接下来问题就简单了,找到合适的编码方案以及精度参数,测试验证即可。


具体的方案选择就不重复了。可以参考这个帖子:geohash、google s2、uber h3三种格网系统比较


我这边最终选择的是h3(h3-pyspark)。


4. 最终解决


第一步:将A的经纬度按照需要的精度进行编码,再获取该编码附近x公里的区域编码集合。
image.png


第二步:将B的经纬度按照同样的精度进行编码。


第三步:将两个数据集inner join,即可获得符合要求的集合。


是的,就是这么简单。(摊手)


5ae26b5d-d753-4900-9c61-213e400f87cd.png


5. 总结


通过这次的问题解决,学习到了这类场景的通用解决方案,受益匪浅。


6. 参考文章


高效的多维空间点索引算法 — Geohash 和 Google S2

彩云天气地理查询优化(2): 行政区划查询

geohash算法

geohash、google s2、uber h3三种格网系统比较

h3-pyspark

Uber H3使用


作者:一匹二维马
来源:juejin.cn/post/7213209438714527800
收起阅读 »

孤独的游戏少年

web
本人是95前端菜鸟一枚,目前在广州打工混口饭吃。刚好换了工作,感觉生活节奏变得慢了下来,打了这么多年工总觉得想纪录些什么,怕以后自己老了忘记自己还有这么一些风流往事。书接上回。 楔子 又是一个闲暇的周末,正读一年级的我坐在床上,把象棋的旗子全部倒了出来,根据...
继续阅读 »

本人是95前端菜鸟一枚,目前在广州打工混口饭吃。刚好换了工作,感觉生活节奏变得慢了下来,打了这么多年工总觉得想纪录些什么,怕以后自己老了忘记自己还有这么一些风流往事。书接上回。



楔子


又是一个闲暇的周末,正读一年级的我坐在床上,把象棋的旗子全部倒了出来,根据颜色分为红色和绿色阵营,旗子翻盖为建筑,正面为单位,被子和枕头作为地图障碍,双手不断的移动着双方阵营的象棋,脑海中演练着星级争霸的游戏画面,将两枚不同阵营的旗子进行碰撞后,通过我对两枚旗子主观的判断,一方阵营的旗子直接被销毁,进入回收,一方的单位对建筑物全部破坏取得游戏胜利。因为我的父亲酷爱玩这款游戏,年少的我也被其玩法、画面所深深吸引,不过最主要的还是父亲获胜或者玩累后,能幸运的奖励我玩上几把。星际争霸成了我第一个启蒙游戏,那时候怎么样都获胜不了,直到发现了show me the money。因为Blizzard这个英文单词一直在游戏的启动界面一闪一闪,那时候还以为这款游戏的名字叫Blizzard,最后才发现,其实Blizzard是魔鬼的意思。


纸笔乐趣


小学一二年级的时候家里管得严,电视也不给我看,电脑直接上锁,作为家里的独生子女,没有同龄人的陪伴,闲暇时间要不就看《格林童话》、《安徒生童话》、《伊索寓言》,要不就打开这副象棋在那里自娱自乐。



起源



在某一天的音乐课上,老师喉咙不舒服,在教室播放猫和老鼠给我们观看,正当我看的津津有味,前面的同学小张突然转过头来,问我“要不要跟我玩个游戏”。小孩子一般都比较贪玩,我直接拒绝了他,“我想看猫和老鼠”。小张满脸失落转了回去,因为我是个特别怕伤害别人情绪的人,所以不忍心又用铅笔的橡皮端戳了戳他,问他“什么游戏,好玩不”。他顿时也来了精神,滔滔不绝的介绍起了这款游戏。


“游戏的规则非常的简单~~在一张纸上中间画一条分割线,双方有各自的基地,每人基地都有5点血量,双方通过猜拳,获胜的一方可以在自己阵营画一个火柴人,火柴人可以攻击对面的火柴人和基地,基地被破坏则胜利。管你听没听懂,试一把就完了!”



我呆呆的看着他,“这么无聊,赢了又怎样?”。他沉默了一会,好像我说的的确有点道理,突然想到了一个好办法,“谁获得胜利扇对方一巴掌”。我顿时来了兴趣。随着游戏逐渐深入,我们的猜拳速度和行动力也越来越快,最后发现扇巴掌还是太狠,改为扇对方的手背,这节课以双方手背通红结束了。


游戏改良


这个《火柴人对战》小游戏在班里火了一会儿,但很快就又不火了,我玩着玩着也发现没啥意思,总是觉得缺少点什么,但毕竟我也只是个没有吃APTX4869的小学生,想不出什么好点子。时间一晃,受到九年义务教育的政策,我也成功成为了一名初中生。在一节音乐课上,老师想让我们放松放松,给我们班看猫和老鼠,隔壁同桌小王撕了一张笔记本的纸,问我有没有玩过《火柴人对战》游戏,只能说,熟悉的配方,熟悉的味道。


当天晚上回到家,闲来无事,我在想这个《火柴人游戏》是不是可以更有优化,这种形式的游戏是不是可以让玩家更有乐趣。有同学可能会问,你那个年代没东西玩的吗?既然你诚心诚意发问,那我就大发慈悲的告诉你。玩的东西的确很多,但是能光明正大摆在课桌上玩的基本没有,一般有一个比较新鲜的好玩的东西,都会有一群人围了过来,这时候老师会默默站在窗户旁的阴暗角落,见一个收一个。


坐在家里的椅子上,我整理了一下思绪,突然产生了灵感,将《魔兽争霸》《游戏王》这两个游戏产生化学反应,游戏拥有着资源,单位,建筑,单位还有攻击力,生命值,效果,攻击次数。每个玩家每回合通过摇骰子的方式获得随机能源点,能源能够解锁建筑,建筑关联着高级建筑和单位,通过单位进行攻击,直至对方玩家生命值为0,那么如何在白纸上面显示呢?我想到比较好的解决方案,单位的画像虽然名字叫骷髅,但是在纸上面用代号A表示,建筑骷髅之地用代号1表示。我花了几天时间,弄了两个阵营,不死族和冰结界。立刻就拿去跟同桌试玩了一下,虽然游戏很丰富,但是有一个严重的弊端就是玩起来还挺耗费时间的,而且要人工计算单位之间的扣血量,玩家的剩余生命,在纸片上去完成这些操作,拿个橡皮擦来擦去,突然觉得有点蠢,有点尴尬,突然明白,一张白纸的承受能力是有限的。之后,我再也没有把游戏拿出来玩过,但我没有将他遗忘,而是深深埋藏在我的心里。


筑梦


直到大学期间《炉石传说》横空出世,直到《游戏王》上架网易,直到我的项目组完成1.0后迎来空窗期一个月,我再也蚌埠住了,之前一直都对微信小游戏很有兴趣,每天闲着也是闲着,所以我有了做一个微信小游戏的想法。而且,就做一款在十几年前,就已经被我设计好的游戏。


但是我不从来不是一个好学的人,领悟能力也很低,之前一直在看cocos和白鹭引擎学习文档,也很难学习下去,当然也因为工作期间没有这么多精力去学习,所以我什么框架也不会,不会框架,那就用原生。我初步的想法是,抛弃所有花里胡哨的动效,把基础的东西做出来,再作延伸。第一次做游戏,我也十分迷茫,最好的做法肯定是打飞机————研究这个微信项目如何用js原生,做出一个小游戏。



虽然微信小游戏刚出来的时候看过代码,但是也只是一扫而过,而这次带着目标进行细细品味,果然感觉不一样。微信小游戏打飞机这个项目是js原生使用纯gL的模式编写的,主要就是在canvas这个画布上面作展示和用户行为。


  // 触摸事件处理逻辑
touchEventHandler(e) {
e.preventDefault()

const x = e.touches[0].clientX
const y = e.touches[0].clientY

const area = this.gameinfo.btnArea

if (x >= area.startX
&& x <= area.endX
&& y >= area.startY
&& y <= area.endY) this.restart()
}

点击事件我的理解就是用户点击到屏幕的坐标为(x, y),如果想要一个按钮上面做处理逻辑,那么点击的范围就要落在这个按钮的范围内。当我知道如何在canvas上面做点击行为时,我感觉我已经成功了一半,接下来就是编写基础js代码。


首先这个游戏确定的元素分别为,场景,用户,单位,建筑,资源(后面改用能源替代),我先将每个元素封装好一个类,一边慢慢的回忆着之前游戏是如何设计的,一边编程,身心完全沉浸进去,已经很久很久没有试过如此专注的去编写代码。用了大概三天的时间,我把基本类该有的逻辑写完了,大概长这个样子



上面为敌方的单位区域,下方为我方的单位区域,单位用ABCDEFG表示,右侧1/1/1 则是 攻击力/生命值/攻击次数,通过点击最下方的icon弹出创建建筑,然后创建单位,每次的用户操作,都是一个点击。


一开始我设想的游戏名为想象博弈,因为每个单位每个建筑都只有名称,单位长什么样子的就需要玩家自己去脑补了,我只给你一个英文字母,你可以想象成奥特曼,也可以想象成哥斯拉,只要不是妈妈生的就行。



湿了


游戏虽然基本逻辑都写好了,放到整个微信小游戏界别人一定会认为是依托答辩,但我还是觉得这是我的掌上明珠,虽然游戏没有自己的界面,但是它有自己的玩法。好像上天也认可我的努力,但是觉得这个游戏还能更上一层楼,在某个摸鱼的moment,我打开了微信准备和各位朋友畅谈人生理想,发现有位同学发了一幅图,上面有四个格子,是赛博朋克风格的一位篮球运动员。他说这个AI软件生成的图片很逼真,只要把你想要的图片告诉给这个AI软件,就能发你一幅你所描绘的图片。我打开了图片看了看,说实话,质感相当不错,在一系列追问下,我得知这个绘图AI软件名称叫做midjourney



midjourney



我迫不及待的登录上去,询问朋友如何使用后,我用我蹩脚的英格力士迫不及待的试了试,让midjourney帮我画一个能源的icon,不试不要紧,一试便湿了,眼睛留下了感动地泪水,就像一个阴暗的房间打开了一扇窗,一束光猛地照射了进来。


WechatIMG35.jpeg


对比我之前在iconfont下载的免费图标,midjourney提供这些图片简直就是我的救世主,我很快便将一开始的免费次数用完,然后氪了一个30美刀的会员,虽然有点肉痛,但是为了儿时的梦想,这点痛算什么


虽然我查找了一下攻略,别人说可以使用gpt和midjourney配合起来,我也试了一下,效果一般,可能姿势没有对,继续用我的有道翻译将重点词汇翻译后丢给midjourney。midjourney不仅可以四选一,还可以对图片不断优化,还有比例选择,各种参数,但是我也不需要用到那么多额外的功能,总之一个字,就是棒。


但当时的我突然意识到,这个AI如此厉害,那么会不会对现在行业某些打工人造成一定影响呢,结果最近已经出了篇报道,某公司因为AI绘图工具辞退了众多插画师,事实不一定真实,但是也不是空穴来风,结合众多外界名人齐心协力抵制gpt5.0的研发,在担心数据安全之余,是否也在担心着AI对人类未来生活的各种冲击。焦虑时时刻刻都有,但解决焦虑的办法,我有一个妙招,仍然还是奖励自己


门槛


当我把整个小游戏焕然一新后,便兴冲冲的跑去微信开放平台上传我的伟大的杰作。但微信突然泼了我一盆冷水,上传微信小游戏前的流程有点出乎意外,要写游戏背景、介绍角色、NPC、介绍模块等等,还要上传不同的图片。我的小游戏一共就三个界面,有六个大板块要填写,每个板块还要两张不同的图片,我当时人就麻了。我只能创建一个单位截一次图,确保每张图片不一样。做完这道工序,还要写一份自审自查报告。


就算做完了这些前戏,我感觉我的小游戏还是难登大雅之堂,突然,我又想到了这个东西其实是不是也能运行在web端呢,随后我便立刻付诸行动,创建一个带有canvas的html,之前微信小游戏是通过weapp-adapter这个文件把canvas暴露到全局,所以在web端使用canvas的时候,只需要使用document.getElementById('canvas')暴露到全局即可。然后通过http-server对应用进行启动,这个小游戏便以web端的形式运行到浏览器上了,终于也能理解之前为啥微信小游戏火起来的时候,很多企业都用h5游戏稍微改下代码进行搬运,原来两者之间是有异曲同工之妙之处的。


关于游戏


龙族.png


魔法学院.png
上面两张便是两个种族对应的生产链,龙族是我第一个创建的,因为我自幼对龙产生好感和兴趣,何况我是龙的传人/doge。魔法学院则是稍微致敬一下《游戏王》中黑魔导卡组吧。


其实开发难度最难的莫过于是AI,也就是人机,如何让人机在有限的资源做出合理的选择,是一大难题,也是我后续要慢慢优化的,一开始我是让人机按照创建一个建筑,然后创建一个单位这种形式去做运营展开,但后来我想到一个好的点子,我应该可以根据每个种族的特点,走一条该特点的独有运营,于是人机龙族便有了龙蛋破坏龙两种流派,强度提升了一个档次。


其实是否能上架到微信小游戏已经不重要了,重要的是这个过程带给我的乐趣,一步一步看着这个游戏被创建出来的成就感,就算这个行业受到什么冲击,我需要被迫转行,我也不曾后悔,毕竟是web前端让我跨越了十几年的时光,找到了儿时埋下的种子,浇水,给予阳光,让它在我的心中成长为一棵充实的参天大树


h5地址:hslastudio.com/game/


github地址: github.com/FEA-Dven/wa…


作者:很饿的男朋友
来源:juejin.cn/post/7218570025376350263
收起阅读 »

为什么说过早优化是万恶之源?

  Donald Knuth(高德纳)是一位计算机科学界的著名学者和计算机程序设计的先驱之一。他被誉为计算机科学的“圣经”《计算机程序设计艺术》的作者,提出了著名的“大O符号”来描述算法的时间复杂度和空间复杂度,开发了TeX系统用于排版科技文献,获得过图灵奖、...
继续阅读 »

  Donald Knuth(高德纳)是一位计算机科学界的著名学者和计算机程序设计的先驱之一。他被誉为计算机科学的“圣经”《计算机程序设计艺术》的作者,提出了著名的“大O符号”来描述算法的时间复杂度和空间复杂度,开发了TeX系统用于排版科技文献,获得过图灵奖、冯·诺伊曼奖、美国国家科学奖章等多项荣誉。今天要说的就是他所提出的一条软件设计重要原则 Premature optimization is the root of all evil 过早优化是万恶之源
在这里插入图片描述

为什么说“过早优化是万恶之源”? 我认为过早优化代码会让人陷入到错误的目标中去,从而忽视掉了最重要的目标。举个很简单的例子,你需要快速构建一个产品来抢占用户,你当下最重要的目标是让这个产品快速上线,而不是把这个产品打造的好用(在中国互联网下,这样的事数不胜数),如果你只关注到后者体验、性能问题而忽视了速度,在当下高度竞争的市场之下,你根本毫无机会。


  当然上面这个例子是从感性的层面说的,对很多程序猿来说也可能涉及不到产品层面的内容。我们从软件设计的层面,理性的来说,过早优化可能会导致以下的一些问题:




  1. 增加代码的复杂性:过度优化可能会导致代码的复杂性增加,从而降低代码的可读性和可维护性。如果代码过于复杂,可能会导致开发人员难以理解和维护代码,从而增加开发成本和时间。




  2. 耗费开发时间和资源:过度优化可能会导致开发人员花费大量时间和资源在代码的性能优化上,而忽略了其他重要的开发任务。这可能会导致项目进度延误和开发成本增加。




  3. 降低代码的可移植性:过度优化可能会导致代码的可移植性降低。如果代码过于依赖于特定的硬件或操作系统,可能会导致代码无法在其他环境中运行。




  4. 降低代码的可扩展性:过度优化可能会降低代码的可扩展性。如果代码过于依赖于特定的算法或数据结构,可能会导致代码无法适应未来的需求变化。




过早优化的典型案例


  在软件工程史上由于过度关注软件性能导致项目最终失败的案例比比皆是,比如我下面要说的一些项目,在软件工程史上都是非常知名的项目(当然可能有些新生代程序员已经不知道了)。




  1. IBM OS/360操作系统:在20世纪60年代,IBM公司开发了OS/360操作系统,这是当时最大的软件工程项目之一。在开发过程中,IBM公司过于关注代码的性能问题,导致代码的复杂性增加,开发时间延误,最终导致项目的失败。我知晓这个项目还是在我最近在阅读的一本软件工程经典书籍《人月神话》中,也推荐大家阅读下,这个项目虽然最终失败了,但也给整个软件工程领域留下了宝贵的经验。




  2. Netscape Navigator浏览器:在20世纪90年代,Netscape公司开发了Navigator浏览器,这是当时最流行的浏览器之一。在开发过程中,Netscape公司过于关注代码的性能问题,导致代码的复杂性增加,开发时间延误,最终导致浏览器市场份额严重下降。




  3. Windows Vista操作系统:在21世纪初,微软公司开发了Windows Vista操作系统,这是当时最大的软件工程项目之一。在开发过程中,微软公司过于关注代码的性能问题,导致代码的复杂性增加,开发时间延误,最终导致操作系统的用户体验不佳,市场反响不佳。话说这个操作系统我还用过呢,用户界面还是很漂亮的,很多UI设计也被沿用到了Window7中。




如何识别过早优化


  在软件开发过程中,如何判断是否过早优化呢?这里有一些概括性的判断标准,可以简单参考下:




  1. 是否存在性能问题:如果代码还没有性能问题,那么过早优化就是不必要的。因此,在进行优化之前,应该先测试代码的性能,确定是否存在性能问题。




  2. 是否优化了未来可能发生的问题:如果优化的是未来可能发生的问题,而不是当前存在的问题,那么就可能是过早优化。在进行优化之前,应该优先考虑当前存在的问题,而不是未来可能发生的问题。




  3. 是否牺牲了代码的可读性和可维护性:如果优化代码会导致代码的复杂性增加,降低代码的可读性和可维护性,那么就可能是过早优化。在进行优化之前,应该优先考虑代码的可读性、可维护性和可扩展性。




  4. 是否浪费了大量的开发时间和资源:如果优化代码会浪费大量的开发时间和资源,而不是提高代码的性能和效率,那么就可能是过早优化。在进行优化之前,应该评估优化的成本和收益,确定是否值得进行优化。




  判断是否过早优化需要根据具体情况进行评估。在进行优化之前,应该先测试代码的性能,确定是否存在性能问题。同时,也应该优先考虑代码的可读性、可维护性和可扩展性,避免过度优化。


总结


  作为一名在IT领域摸爬滚打多年的工程师,我深有体会地认识到过早优化是软件开发中的一大陷阱。在软件开发的初期,我们可能会过于关注代码的性能问题,而忽略了代码的可读性、可维护性和可扩展性。这种做法可能会导致代码的复杂性增加,降低代码的可读性和可维护性,甚至可能会浪费大量的开发时间和资源。


  在软件开发过程中,我们应该避免过早优化,而是优先考虑代码的可读性、可维护性和可扩展性。当需要进行性能优化时,应该在代码的基础上进行优化,通过分析性能瓶颈、优化算法和数据结构等方法来提高代码的性能和效率。同时,我们也应该意识到,性能优化并不是软件开发的唯一目标,我们还应该注重代码的可读性、可维护性和可扩展性,以便保证代码的质量和可靠性


作者:xindoo
来源:juejin.cn/post/7217305951552634935
收起阅读 »

实战经验:打造仿微信聊天键盘,解决常见问题

防苹果微信聊天页面,聊天中的布局不是,主要是键盘部分,键盘部分在做的过程中遇到了几个坑,记录一下,看看大家有没有越到过 分析ios微信聊天页面 UI组成看起来比较简单,但是包含的内容可真不少,首先语音、输入框、表情、更多四个简单元素,元素间存在互斥的一些状态...
继续阅读 »

防苹果微信聊天页面,聊天中的布局不是,主要是键盘部分,键盘部分在做的过程中遇到了几个坑,记录一下,看看大家有没有越到过


output_image.gif


分析ios微信聊天页面


UI组成看起来比较简单,但是包含的内容可真不少,首先语音、输入框、表情、更多四个简单元素,元素间存在互斥的一些状态操作,比如语音时,显示按住说话,键盘关闭,表情面板时面板关闭,面板关闭则联动表情和EditText图标的切换。


各状态分析



  1. 语音状态



语音状态时,语音与edit图标切换,EditText 与按住说话UI切换,此时如果键盘处于编辑状态,则收回键盘,此时键盘处于表情面板或者更多面板需要收回面板,若表情面板时,表情与edit图位置恢复表情icon。




  1. 键盘状态



点击语音与edit图标 位置时,icon 为语音标,键盘弹出,当前再表情面板时,点击表情与edit图标, 键盘弹出,icon 变换




  1. 表情状态



注意语音与edit图标 位置恢复即可




  1. 更多面板



注意语音与edit图标,表情与edit图标位置恢复



对于这四种状态直接使用LiveData, 然后与点击事件做出绑定,事件发生时处理对应状态即可


image.png


键盘UI组成


image.png


所以可以将结构设置为:


<LinearLayout
android:id="@+id/cl_voiceRoom_inputRoot"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:focusable="true"
android:focusableInTouchMode="true"
android:orientation="vertical"
tools:visibility="visible">


<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/imEditBgCL"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/im_chat_bottom_bg"
android:minHeight="60dp">


// 键盘顶部,表情输入框等

</androidx.constraintlayout.widget.ConstraintLayout>

// 指定面板占位
<androidx.fragment.app.FragmentContainerView
android:id="@+id/imMiddlewareVP"
android:layout_width="match_parent"
android:layout_height="300dp"
android:background="#F0EFEF"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/imEditBgCL"
tools:visibility="visible"
/>




</LinearLayout>

然后对应上述的状态进行UI和键盘的操作


键盘逻辑处理



  1. EditText 自动换行输入并将action设置为send 按钮


这一步很简单,但是有一个坑,按照正常逻辑,再xml中的EditText 设置以下属性,即可完成这个需求


android:imeOptions="actionSend"
android:inputType="textMultiLine"

按照属性的原义,这样将显示正常的发送按钮以及可自动多行输入,但是就是不显示发送,查资料发现imeOptions 需要使inputType 为text 时才显示,但是又实现不了我们的需求,最后处理方式


android:imeOptions="actionSend"
android:inputType="text"

//然后在代码中进行如下设置:
binding.imMiddlewareET.run {
imeOptions = EditorInfo.IME_ACTION_SEND
setHorizontallyScrolling(false)
maxLines = Int.MAX_VALUE
}



  1. 按照上面的状态互斥,我们需要动态监听软键盘的打开和关闭


系统没有提供对应的实现,所以我们才采取的办法是,监听软键盘的高度变化


View rootView = getWindow().getDecorView().getRootView();
rootView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
Rect rect = new Rect();
rootView.getWindowVisibleDisplayFrame(rect);
int heightDiff = rootView.getHeight() - rect.bottom;
boolean isSoftKeyboardOpened = heightDiff > 0;
// 处理软键盘打开或关闭的逻辑
}
});

通过判断高度来推算键盘的打开或者关闭


解决切换键盘问题


切换键盘时,比如表情和Edit 切换




  • 当面板是键盘时,点击图标区域



    • 取消Edit焦点

    • 关闭键盘

    • 打开emoji面板




  • 当面板是emoji时



    • 隐藏面板

    • 设置获取焦点

    • 打开键盘
      其他场景下切换没什么问题,但是当键盘和自定义面板切换时有可能出现这样的问题:




image.png


因为键盘的关闭和View的显示,或者View的隐藏和键盘的显示那个先执行完毕逻辑不能串行,导致会出现这种闪烁的画面


解决方案:


分析上述问题后会发现,导致的出现这种情况的原因就是逻辑不能串行,那我们保证二者的逻辑串行就不会出现这问题了,怎么保证呢?


首先要知道的是肯定不能让View先行,View先行一样会出现这个问题,所以要保证让键盘先行,我们看一下,键盘的打开和关闭:


// 显示键盘
private fun showSoftKeyBoard(view: View) {
val imm = mContext.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?
imm?.showSoftInput(view, InputMethodManager.SHOW_FORCED)
}

// 隐藏键盘
private fun hideSoftKeyBoard(view: View) {
val imm = mContext.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?
if (imm != null && imm.isActive) {
imm.hideSoftInputFromWindow(view.windowToken, 0)
}
}

这个代码对于键盘的显示隐藏是没有任何问题的,但是我们怎么判断它执行这个动作完毕了呢?


方法一:


上面我们有这样的操作,监听了键盘高度的监听,我们可以在执行切换操作时启动一个线程的死循环,然后再循环中判断高度,满足高度时执行上述逻辑。


方法二:


看下InputMethodManager 的源码,发现:


/**
* Synonym for {@link #hideSoftInputFromWindow(IBinder, int, **ResultReceiver**)}
* without a result: request to hide the soft input window from the
* context of the window that is currently accepting input.
*
* @param windowToken The token of the window that is making the request,
* as returned by {@link View#getWindowToken() View.getWindowToken()}.
* @param flags Provides additional operating flags. Currently may be
* 0 or have the {@link #HIDE_IMPLICIT_ONLY} bit set.
*/

public boolean hideSoftInputFromWindow(IBinder windowToken, int flags) {
return hideSoftInputFromWindow(windowToken, flags, null);
}

是不是很神奇,这个隐藏方法有一个ResultReceiver 的回调,卧槽,是不是看这个名字就感觉有戏,具体看一下:


public boolean hideSoftInputFromWindow(IBinder windowToken, int flags,
ResultReceiver resultReceiver)
{
return hideSoftInputFromWindow(windowToken, flags, resultReceiver,
SoftInputShowHideReason.HIDE_SOFT_INPUT);
}


ResultReceiver 是一个用于在异步操作完成时接收结果的类,它可以让你在不同的线程之间进行通信。在 hideSoftInputFromWindow() 方法中,ResultReceiver 作为一个可选参数,用于指定当软键盘隐藏完成时的回调。该回调会在后台线程上执行,因此不会阻塞主线程,从而提高应用程序的响应性能。


ResultReceiver 类有一个 onReceiveResult(int resultCode, Bundle resultData) 方法,当异步操作完成时,该方法会被调用。通过实现该方法,你可以自定义处理异步操作完成后的行为。例如,在软键盘隐藏完成后,你可能需要执行一些操作,例如更新 UI 或者执行其他任务。


在 hideSoftInputFromWindow()方法中,你可以通过传递一个 ResultReceiver 对象来指定异步操作完成后的回调。当软键盘隐藏完成时,系统会调用ResultReceiver对象的send()方法,并将结果代码和数据包装在 Bundle对象中传递给 ResultReceiver对象。然后,ResultReceiver 对象的 onReceiveResult() 方法会在后台线程上执行,以便你可以在该方法中处理结果。



然后看了showSoftInput 也同样有这个参数


public boolean showSoftInput(View view, int flags, ResultReceiver resultReceiver) {
return showSoftInput(view, flags, resultReceiver, SoftInputShowHideReason.SHOW_SOFT_INPUT);
}

那我们可以这样解决:


隐藏为例:
当我执行切换时,首先调用hideSoftInputFromWindow, 并创建ResultReceiver监听,当返回结果后,执行View的操作,保证他们的串行,以此解决切换键盘闪烁问题。


private fun hideSoftKeyBoard(view: View, callback: () -> Unit) {
val imm = mActivity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?
if (imm != null && imm.isActive) {
val resultReceiver = object : ResultReceiver(Handler(Looper.getMainLooper())) {
override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
super.onReceiveResult(resultCode, resultData)
// 在这里处理软键盘隐藏完成后的逻辑
callback.invoke()
//...
}
}
imm.hideSoftInputFromWindow(view.windowToken, 0, resultReceiver)
}
}

Emoji 显示


在 Android 中,Emoji 表情可以通过以下方式在字符串中表示:



  1. Unicode 编码:Emoji 表情的 Unicode 编码可以直接嵌入到字符串中,例如 "\u2764\ufe0f" 表示一个红色的心形 Emoji。其中,\u 是 Unicode 转义字符,后面跟着 4 个十六进制数表示该字符的 Unicode 编码。

  2. Unicode 代码点:Unicode 代码点是 Unicode 编码的十进制表示,可以使用 &# 后跟代码点数字和分号 ; 来表示 Emoji,例如 &#128512; 表示一个笑脸 Emoji。在 XML 中,可以使用 &#x 后跟代码点的十六进制表示来表示 Emoji,例如 &#x1f600; 表示一个笑脸 Emoji。

  3. Emoji 表情符号:在 Android 4.4 及以上版本中,可以直接使用 Emoji 表情符号来表示 Emoji,例如 😊 表示一个微笑的 Emoji。在 Android 4.3 及以下版本中,需要使用第一种或第二种方式来表示 Emoji。


我在此demo中使用第一种实现的,具体使用步骤:



  1. UI布局

  2. 数据



flow {

val pattern = Regex("^(\S+)\s+;\s+fully-qualified\s+#\s+((?:\S+\s+)+)(.+)$")
val filterNotNull = readAssetsFile("emoji.txt", IMApplication.context)
.trim()
.lines()
.map { line ->
val matchResult = pattern.find(line)
if (matchResult != null) {
val (emoji, codePointHex, comment) = matchResult.destructured
val codePoint = emoji.drop(2).toInt(16)
EmojiEntry(emoji, codePoint, "E${emoji.take(2)}", comment,codePointHex)
} else {
null
}
}.filterNotNull()
emit(filterNotNull)
}

使用



  • 使用google 提供的emoji库


implementation 'androidx.emoji:emoji:1.1.0'


  • 在Application中初始化


val fontRequest = FontRequest(
"com.google.android.gms.fonts",
"com.google.android.gms",
"Montserrat Subrayada",
R.array.com_google_android_gms_fonts_certs
)
val config = FontRequestEmojiCompatConfig(this, fontRequest)
EmojiCompat.init(config)

对于FontRequest 是使用的Goolge 提供的可下载字体配置进行初始化的,当然可以不用,但是系统的字体对于表情不是高亮的,看起来是灰色的(也可以给TextView 设置字体解决)


通过 Android Studio 和 Google Play 服务使用可下载字体



  1. 在 Layout Editor 中,选择一个 TextView,然后在 Properties 下,选择 fontFamily > More Fonts。


image.png



  1. 在 Source 下拉列表中,选择 Google Fonts。

  2. 在 Fonts 框中,选择一种字体。

  3. 选择 Create downloadable font,然后点击 OK


image.png


然后会在项目的res 下生成文字


<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:app="http://schemas.android.com/apk/res-auto"
app:fontProviderAuthority="com.google.android.gms.fonts"
app:fontProviderPackage="com.google.android.gms"
app:fontProviderQuery="Montserrat Subrayada"
app:fontProviderCerts="@array/com_google_android_gms_fonts_certs">
</font-family>

Emoji 面板中的删除操作


再IOS微信中,点击Emoji面板后输入框是没有焦点的,然后点击删除时Emoji会有一个问题,因为它的大小是2个byte,所以常规删除是不行的,


expressionDeleteFL.setOnClickListener {
val inputConnection =
editText.onCreateInputConnection(
EditorInfo()
)
// 找到要删除的字符的边界
val text = editText.text.toString()
val index = editText.selectionStart
var deleteLength = 1
if (index > 0 && index <= text.length) {
val codePoint = text.codePointBefore(index)
deleteLength = if (Character.isSupplementaryCodePoint(codePoint)) 2 else 1
}
inputConnection.deleteSurroundingText(deleteLength, 0)
}


  1. 首先,通过 editText.onCreateInputConnection(EditorInfo()) 方法获取输入连接器(InputConnection),它可以用于向 EditText 发送文本和控制命令。在这里,我们使用它来删除文本。

  2. 接着,获取 EditText 中当前的文本,并找到要删除的字符的边界。通过 editText.selectionStart方法获取当前文本的光标位置,然后使用 text.codePointBefore(index)方法获取光标位置前面一个字符的 Unicode 编码点。如果该字符是一个 Unicode 表情符号,它可能由多个 Unicode 编码点组成,因此需要使用 Character.isSupplementaryCodePoint(codePoint) 方法来判断该字符是否需要删除多个编码点。

  3. 最后,使用 inputConnection.deleteSurroundingText(deleteLength, 0)方法删除要删除的字符。其中,deleteLength 是要删除的字符数,0 表示没有要插入的新文本。


主要的技术点在于“text.codePointBefore(index)方法获取光标位置前面一个字符的 Unicode 编码点,然后向前探测,找到字符边界” 以此完成删除操作


打开面板时 RV布局的处理


这个就比较简单了



  1. 首先,通过 root.viewTreeObserver.addOnGlobalLayoutListener 方法添加一个全局布局监听器,该监听器可以监听整个布局树的变化,包括软键盘的弹出和隐藏。

  2. 在监听器的回调函数中,通过 root.getWindowVisibleDisplayFrame(r) 方法获取当前窗口的可见区域(不包括软键盘),并通过 root.rootView.height 方法获取整个布局树的高度,从而计算出软键盘的高度 keypadHeight。

  3. 接着,通过计算屏幕高度的 15% 来判断软键盘是否弹出。如果软键盘高度超过了屏幕高度的 15%,则认为软键盘已经弹出。

  4. 如果软键盘已经弹出,则通过 imMiddlewareRV.scrollToPosition(mAdapter.getItemCount() - 1) 方法将 RecyclerView滚动到最后一条消息的位置,以确保用户始终能看到最新的消息


root.viewTreeObserver.addOnGlobalLayoutListener {
val r = Rect()
root.getWindowVisibleDisplayFrame(r)
val screenHeight = root.rootView.height
val keypadHeight = screenHeight - r.bottom
//键盘是否弹出
val diff = screenHeight * 0.15
if (keypadHeight > diff) { // 15% of the screen height
imMiddlewareRV.scrollToPosition(mAdapter.getItemCount() - 1);
}
}

总结


仿照微信聊天键盘的方法,实现了一个包含表情等功能的键盘区域,并解决了一些常见的问题。通过实践和调查,解决了切换键盘的问题,并实现了Emoji的Unicode显示和自定义删除时向前探索字符边界完成表情删除等操作。在过程中,以为很简单的一个东西花了大量的时间调查原因,发现键盘这一块水很深,当我看到ResultReceiver时,看到了AIDL通信,所以再Android这个体系中,Binder的机制需要了然于胸的,刚好我最近在学习Binder得各种知识,不久后会发布对应的博客,关注我,哈哈。


此系列属于我的一个 《Android IM即时通信多进程中间件设计与实现》 系列的一部分,可以看看这个系列


项目地址


作者:麦客奥德彪
来源:juejin.cn/post/7215416975605628987
收起阅读 »

写给 Android 开发者的系统基础知识科普

与我以往的风格不同,本文为科普类文章,因此不会涉及到太过高深难懂的知识。但这些内容可能 Android 应用层开发者甚至部分 framework 层开发者都不了解,因此仍旧高能预警。 另外广东这两天好冷啊,大家注意保暖~ 虚拟机与运行时 对象的概念 假设 ge...
继续阅读 »

与我以往的风格不同,本文为科普类文章,因此不会涉及到太过高深难懂的知识。但这些内容可能 Android 应用层开发者甚至部分 framework 层开发者都不了解,因此仍旧高能预警。


另外广东这两天好冷啊,大家注意保暖~


虚拟机与运行时


对象的概念


假设 getObjectAddress(Object) 是一个获取对象内存地址的方法。


第一题:


考虑如下代码:


public static void main(String[] args) {
Object o = new Object();
long address1 = getObjectAddress(o);
// .......
long address2 = getObjectAddress(o);
}

main 方法中,创建了一个 Object 对象,随后两次调用 getObjectAddress 获取该对象的地址。两次获取到的对象地址是否有可能不同?换句话说,对象的地址是否有可能变更?










答:有可能。JVM 中存在 GC 即“垃圾回收”机制,会回收不再使用的对象以腾出内存空间。GC 可能会移动对象。


第二题:


考虑如下代码:


private static long allocate() {
Object o = new Object();
return getObjectAddress(o);
}

public static void main(String[] args) {
long address1 = allocate();
// ......
long address2 = allocate();
}

allocate() 创建了一个 Object 对象,然后获取它的对象地址。
main 方法中调用两次 allocate(),这两个对象的内存地址是否有可能相同?










答:有可能。在 allocate() 方法中创建的对象在该方法返回后便失去所有引用成为“不再需要的对象”,如果两次方法调用之间,第一次方法调用中产生的临时对象被上文中提到的 GC 机制回收,对应的内存空间就变得“空闲”,可以被其他对象占用。


第三题:


哎呀,既然上面说同一个对象的内存地址可能不相同,两个不同对象也有可能有相同的内存地址,而java 里的 == 又是判断对象的内存地址,那么


Object o = new Object();
if (o != o)

还有


Object o1 = new Object();
Object o2 = new Object();
if (o1 == o2)

这里的两个 if 不是都有可能成立?










答:不可能。== 操作符比较的确实是对象地址没错,但是这里其实还隐含了两个条件:



  1. 这个操作符比较的是 “那一刻” 两个对象的地址。

  2. 比较的两个对象都位于同一个进程内。


上述提到的两种情况都不满足“同一时间”这一条件,因此这两条 if 永远不会成立。


类与方法


第四题:


假设 Framework 是 Android Framework 里的一个类,App 是某个 Android App 的一个类:


public class Framework {
public static int api() {
return 0;
}
}

public class App {
public static void main(String[] args) {
Framework.api();
}
}

编译 App,然后将 Frameworkapi 方法的返回值类型从 int 改为 long,编译 Framework 但不重新编译 App,App 是否可以正常调用 Framework 的 api 方法?










答:不能。Java 类内存储的被调用方法的信息里包含返回值类型,如果返回值类型不对在运行时就找不到对应方法。将方法改为成员变量然后修改该变量的类型也同理。


第五题:


考虑如下代码:


class Parent {
public void call() {
privateMethod();
}
private void privateMethod() {
System.out.println("Parent method called");
}
}

class Child extends Parent {
private void privateMethod() {
System.out.println("Child method called");
}
}

new Child().call();

Child 里的 privateMethod 是否重写了 Parent 里的?call 中调用的 privateMethod() 会调用到 Parent 里的还是 Child 里的?










答:不构成方法重写,还是会调用到 Parent 里的 privateMethod。private 方法是 direct 方法,direct 方法无法被重写。


操作系统基础


多进程与虚拟内存


假设有进程 A 和进程 B。


第六题:


进程 A 里的对象 a 和进程 B 里的对象 b 拥有相同的内存地址,它们是同一个对象吗?










答:当然不是,上面才说过“对象相等”这个概念在同一个进程里才有意义,不认真听课思考是会被打屁屁的~


第七题:


进程 A 内有一个对象 a 并将这个对象的内存地址传递给了 B,B 是否可以直接访问(读取、写入等操作)这个对象?










答:不能,大概率会触发段错误,小概率会修改到自己内存空间里某个冤种对象的数据,无论如何都不会影响到进程 A。作为在用户空间运行的进程,它们拿到的所谓内存地址全部都是虚拟地址,进程访问这些地址的时候会先经过一个转换过程转化为物理地址再操作。如果转换出错(人家根本不认识你给的这个地址,或者对应内存的权限不让你执行对应操作),就会触发段错误。


第八题:


还是我们可爱的进程 A 和 B,但是这次 B 是 A 的子进程,即 A 调用 fork 产生了 B 这个新的进程:


void a() {
int* p = malloc(sizeof(int));
*p = 1;
if (fork() > 0) {
// 进程 A 也即父进程
// 巴拉巴拉巴拉一堆操作
} else {
// 进程 B 也即子进程
*p = 2;
}
}

(fork 是 Posix 内创建进程的 API,调用完成后如果仍然在父进程则返回子进程的 pid 永远大于 0,在子进程则返回 0)


(还是理解不了就把 A 想象为 Zygote 进程,B 想象为任意 App 进程)


这一段代码分配了一段内存,调用 fork 产生了一个子进程,然后在子进程里将预先分配好的那段内存里的值更改为 2。
问:进程 B 做出的更改是否对进程 A 可见?










答:不可见,进程 A 看见的那一段内存的值依然是 1。Linux 内核有一个叫做“写时复制”(Copy On Write)的技术,在进程 B 尝试写入这一段内存的时候会偷偷把真实的内存给复制一份,最后写入的是这份拷贝里的值,而进程 A 看见的还是原来的值。


跨进程大数据传递


已知进程 A 和进程 B,进程 A 暴露出一个 AIDL 接口,现在进程 B 要从 A 获取 10M 的数据(远远超出 binder 数据大小限制),且禁止传递文件路径,只允许调用这个 AIDL 接口一次,请问如何实现?










答:可以传递文件描述符(File Descriptor)。别以为这个玩意只能表示文件!举个例子,作为应用层开发者我们可以使用共享内存的方法,这样编写 AIDL 实现类把数据传递出去:


@Override public SharedMemory getData() throws RemoteException {
int size = 10 * 1024 * 1024;
try {
SharedMemory sharedMemory = SharedMemory.create("shared memory", size);
ByteBuffer buffer = sharedMemory.mapReadWrite();
for (int i = 0;i < 10;i++) {
// 模拟产生一堆数据
buffer.put(i * 1024 * 1024, (byte) 114);
buffer.put(i * 1024 * 1024 + 1, (byte) 51);
buffer.put(i * 1024 * 1024 + 2, (byte) 4);
buffer.put(i * 1024 * 1024 + 3, (byte) 191);
buffer.put(i * 1024 * 1024 + 4, (byte) 98);
buffer.put(i * 1024 * 1024 + 5, (byte) 108);
buffer.put(i * 1024 * 1024 + 6, (byte) 93);
}
SharedMemory.unmap(buffer);
sharedMemory.setProtect(OsConstants.PROT_READ);
return sharedMemory;
} catch (ErrnoException e) {
throw new RemoteException("remote create shared memory failed: " + e.getMessage());
}
}

然后在进程 B 里这样拿:


IRemoteService service = IRemoteService.Stub.asInterface(binder);
try {
SharedMemory sharedMemory = service.getData();
ByteBuffer buffer = sharedMemory.mapReadOnly();

// 模拟处理数据
int[] temp = new int[10];
for (int i = 0;i < 10;i++) {
for (int j = 0;j < 10;j++) {
temp[j] = buffer.get(i * 1024 * 1024 + j);
}
Log.e(TAG, "Large buffer[" + i + "]=" + Arrays.toString(temp));
}
SharedMemory.unmap(buffer);
sharedMemory.close();
} catch (Exception e) {
throw new RuntimeException(e);
}

这里使用的 SharedMemory 从 Android 8.1 开始可用,在 8.1 之前的系统里也有一个叫做 MemoryFile 的 API 可以用。
打开 SharedMemory 里的源码,你会发现其实它内部就是创建了一块 ashmem (匿名共享内存),然后将对应的文件描述符传递给 binder。内核会负责将一个可用的文件描述符传递给目标进程。
你可以将它理解为可以跨进程传递的 File Stream(只要能通过权限检查),合理利用这个

作者:canyie
来源:juejin.cn/post/7215509220750098488
小玩意有奇效哦 :)

收起阅读 »

调教ChatGPT提取公众号封面

牙叔教程 简单易懂 我抱着调教的心, 被ChatGPT调教了, 😭, 看看我是怎么被ChatGPT调教的 提取公众号prompt prompt就是提示语, ChatGPT会根据提示语做事情,  提取封面的提示语是: 用Node.js提取微信公众号封面的url...
继续阅读 »

牙叔教程 简单易懂


我抱着调教的心, 被ChatGPT调教了, 😭,


看看我是怎么被ChatGPT调教的




提取公众号prompt


prompt就是提示语, ChatGPT会根据提示语做事情, 


提取封面的提示语是: 用Node.js提取微信公众号封面的url


效果


每次登录ChatGPT太麻烦了, 还怕浏览器随时获取IP, 被openai审查, 所以自己写了个网页调用api,


想要网页的老铁, 公众号回复 api网页


用api网页可以降低封号概率, 我这个号可一次都没被封过



截图看不清楚, 我贴上文字;


只有第一行是我的, 其他的都是ChatGPT回复的内容


用Node.js提取微信公众号封面的url
要提取微信公众号封面的URL,可以使用Node.js和request模块来发送HTTP请求并解析返回的HTML页面。以下是一个示例代码:

```javascript
const request = require('request');
const cheerio = require('cheerio');

const url = 'https://mp.weixin.qq.com/s/xxxxxxxxxxxxxx'; // 公众号文章链接

request(url, function (error, response, body) {
if (!error && response.statusCode == 200) {
const $ = cheerio.load(body);
const coverUrl = $('meta[property="og:image"]').attr('content');
console.log(coverUrl);
}
});
```

在这个示例中,我们首先使用request模块发送HTTP请求,并在回调函数中处理响应。然后,我们使用cheerio模块解析返回的HTML页面,并使用jQuery样式的语法来获取页面中的元素。在这个例子中,我们使用`$('meta[property="og:image"]')`来获取页面中的`<meta>`标签,然后使用`.attr('content')`来获取`content`属性的值,即公众号封面的URL。最后,我们将URL打印到控制台中。

需要注意的是,这个示例只能提取公众号文章页面中的封面URL,而不能直接提取公众号的封面URL。如果要提取公众号的封面URL,可以参考以下代码:

```javascript
const request = require('request');
const cheerio = require('cheerio');

const url = 'https://mp.weixin.qq.com/profile?src=3&timestamp=1631955330&ver=2&signature=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; // 公众号主页链接

request(url, function (error, response, body) {
if (!error && response.statusCode == 200) {
const $ = cheerio.load(body);
const coverUrl = $('.profile_header .avatar img').attr('src');
console.log(coverUrl);
}
});
```

在这个示例中,我们首先发送HTTP请求,然后使用cheerio模块解析返回的HTML页面。我们使用`.profile_header .avatar img`来获取公众号主页中的头像图片元素,然后使用`.attr('src')`来获取`src`属性的值,即公众号的封面URL。最后,我们将URL打印到控制台中。

先试试第一段代码


const request = require('request');
const cheerio = require('cheerio');

const url = 'https://mp.weixin.qq.com/s/tcorJxJBWoF0yOUHOypuQw'

request(url, function (error, response, body) {
if (!error && response.statusCode == 200) {
const $ = cheerio.load(body);
const coverUrl = $('meta[property="og:image"]').attr('content');
console.log(coverUrl);
}
});

运行以后获取到的封面图片链接


$ node 1
https://mmbiz.qpic.cn/mmbiz_jpg/n3RS3ib3lbz9krBGCZ6jkE6xROetdnyvlPEl6wmoh27gIwaKTe7EuaV7lKgsQ69uXXnVpXnXzNSKc0oG8CdSf7g/0?wx_fmt=jpeg

node 1 的意思


代码是nodejs执行的, 1是文件名1.js


链接正确吗


正确到姥姥家了, 我滴个妈呀, 我就只说了一句话, ChatGPT把事情给我办的, 妥妥当当的


提取到的封面图



还想调教ChatGPT


调教个锤子哦, 一发就中, 饭碗不保了, 兄弟们


用不了GPT怎么办?


可以用 谷歌的Bard


也可以用 微软的NewBing


微信公众号 牙叔教程


作者:牙叔教程
来源:juejin.cn/post/7219599208608284730
收起阅读 »

俞敏洪:我曾走在崩溃的边缘

大家在人生的经历中遇到过很崩溃的事情吗? 我遇到过,遇到这类事情的时候,我会读读名人传记,看看他们有没有遇到我和我类似的事情;他们是怎么处理这些事情的;或者说他们的心路历程是怎么样的。他们的应对方式可能会对我有所启发。 长时间下来,这个习惯让我对名人的苦难经历...
继续阅读 »

大家在人生的经历中遇到过很崩溃的事情吗?


我遇到过,遇到这类事情的时候,我会读读名人传记,看看他们有没有遇到我和我类似的事情;他们是怎么处理这些事情的;或者说他们的心路历程是怎么样的。他们的应对方式可能会对我有所启发。


长时间下来,这个习惯让我对名人的苦难经历或者处理棘手问题的经历有强烈的好奇心。最近,读了俞敏洪的自传《我曾走在崩溃的边缘》,感觉挺有意思。


俞敏洪是新东方的老板,在“双减”政策之后,新东方转型做了直播,也就是大家熟知的东方甄选,可能很多人还买过他们直播间的大米。当然,我没有买过,因为理智促使我很少为情怀买单。


离开北大


俞敏洪曾经是北大的老师,他的梦想是出国留学。但老师的工资低,很难赚够出国的学费。作为南方人的他,天生的商人基因让他找到了赚钱的路子——开英语培训班。这条路子获得的收入比工资高十几倍,利润十分丰厚。


于是,他打着北大的招牌私下招生,这意味着和北大“官方”的托福培训班形成了竞争关系。学校当然不会允许北大老师和北大抢生意,况且学校禁止老师私下办培训班。俞老师无法避免地和校领导发生了冲突,并因此被处分。


图片


处分的通告在学校的高音喇叭上足足播了一个礼拜,这件事情闹得人尽皆知,对俞敏洪名声的伤害极大。后来,学校分房自然没有俞老师的份了。在中国的社会体系下,名声对一个人来说极其重要。这种“德治”社会虽然在人口大国里对秩序起着巨大的作用,但也给一些人带来了巨大伤害。一遭名声败坏,要背一辈子,这对当事人是多大的打击。


那时俞敏洪已经结婚,本可以在大学教书过安稳的生活,但这一纸处分,让他决定从北大离职。最后,他骑着三轮车拉着家当离开了北大,开启了新东方的事业。


图片


死磕办学证


办培训班需要办学证,类似于现在的牌照。如果没有就无法公开招生,这意味着无法扩大规模。俞敏洪没办法,找了当时一个叫东方大学的机构联合办培训班,条件是支付总收入的25%给东方大学。


东方大学不参与招生、培训等所有事情,却要分掉一大笔钱。随着培训班的规模越来越大,俞敏洪意识到这不是长久之计,他决定就算再难,死磕也要把办学证拿到手。


要拿到办学证要符合两个条件:一是必须有大学副教授以上职称,二是要经原单位同意。


俞敏洪在北大只是讲师,没有副教授职称,而且北京大学处分了他,不可能同意他办学。两个条件都不符合,教育局直接拒绝,并叫他不要来了。


不得不说,俞老师的脸皮是够厚的,每隔一两星期就去教育局和办事的人聊天,久了大家就混熟了。


大概耗了半年,教育局放低了办学的要求,只要他能够在人才交流中心拿到允许办学证明就放行。可是人才交流中心的工作人员根本不给他开证明。直到遇见他一个在这里工作的学生,在她的帮助下才拿到证明。


办学证到手后,俞敏洪离开东方大学,开始独立办培训班。原来的“东方大学外语培训部”这块招牌积累了相当的名气,新东方成立后,大量学生还去那边报名。为了顺利切换品牌,新的培训机构起名叫新东方,而且从东方大学买断了“东方大学外语培训部”三年的使用权,每年支付20万。


这一系列的操作,可见俞敏洪有相当不错的商业头脑。


被赶下董事长的位置


中国是一个人情社会,比如亲情、友情、同学情。在这种社会成长起来的人,自然会想到找自己熟悉的人一起做事业。俞敏洪也不例外。新东方的培训班办得风生水起,俞敏洪开始寻找人才。


除了拉亲人朋友入伙,他还出国把大学同学王强、徐小平拉回来一起跟他干事业。这三人被称为“东方三驾马车”,也就是电影《中国合伙人》的原型。


image.png


亲人、同学、朋友之间,天然有信任感,在事业的初创阶段一起工作沟通效率非常高,而且为了共同的目标,凝聚力非常强。


当公司到了一定的规模,这种人情关系构建起来的团队,会使公司的人事关系变得非常复杂。


一是,团队没有组织架构,决策效率低下;二是,老板没有话语权,下面的人不知道该听谁的,却谁都不敢得罪。


后来,在新东方改革期间,创始团队出现各种矛盾,俞敏洪无法短期内处理好这些矛盾,被管理层认为是不合格的董事长。于是,俞敏洪从位置上退了下来。


退位期间,其他几个领导轮流做主,也无法处理好团队的矛盾。俞敏洪开始大量阅读公司管理、股权管理的书籍,积累比其他领导更丰富的管理知识。两三年后,他重新回到董事长的位置上。


他能回到位置上,管理知识是一方面,我斗胆猜测,运气的成分占比很大。毕竟被自己的公司赶走的大有人在。


结尾


除了上面3个故事,俞敏洪还有很多非常精彩的故事,比如“被抢劫险些丧命”、“知识产权侵权风波”、“新东方上市”、“遭遇浑水公司做空”等等。


语言是思想的外衣。他来自农村,《我曾走在崩溃的边缘》这本书语言坦诚,像他本人一样。他的人生非常精彩,展现了他强大的韧性。


他的成功,有时代的机遇,也有个人的努力。我们可能无法准确把握时代的机遇,但可以学习他的努力和韧性,在崩溃之时屹立不倒。


作者:华仔很忙
来源:juejin.cn/post/7218487123212091450
收起阅读 »