注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

ChatGPT保姆级注册教学

1.前言 大家好,最近一款ChatGPT的AI产品横空出世,他的强大的功能引起了广大网友的热议,并迅速席卷全球。 根据UBS发布的研究报告显示, ChatGPT在1月份的月活跃用户数已达1亿,成为史上用户数增长最快的消费者应用,相比之下, TikTok花了九...
继续阅读 »

1.前言


大家好,最近一款ChatGPT的AI产品横空出世,他的强大的功能引起了广大网友的热议,并迅速席卷全球。
image.png


根据UBS发布的研究报告显示, ChatGPT在1月份的月活跃用户数已达1亿,成为史上用户数增长最快的消费者应用,相比之下, TikTok花了九个月的时间月活跃用户数才破亿,1月份平均每天有超过1300万名独立访问者使用ChatGPT ,是去年12月份的两倍多。


image.png


那么这么一款现象级的产品,我可不允许大家还没用过,今天就详细出一版ChatGPT注册教程,也是后面我们接入ChatGPT必须要做的第一步。


2.注册准备


ChatGPT把国内给禁掉了,包括港子和湾子,所以如果注册的话就要代理到其他国家。不然浏览器会提示该地区政策禁止使用。注册需要使用邮箱和手机号,手机号也有区域限制,因此手机号和邮箱也需要国外的邮箱手机才能注册成功。我也给大家梳理了以下个人感觉比较好用的虚拟站点,提供使用:



3.注册流程


3.1.账号创建


打开神秘力量上网工具,要走全局代理,不然也会被定位到国内不能使用。之后打开ChatGPT官网chat.openai.com/auth/login


image.png


进入创建账号页面,这里需要输入邮箱,我们先进入temp-mail.org/zh/ 获取免费的临时邮箱


image.png


输入邮箱,点击继续


image.png


输入密码,点击继续


image.png


3.2.邮箱验证


接下来就开始提示邮箱验证了,系统会发送一封邮件到邮箱,我们需要打开刚才的邮箱点击验证邮件。


image.png


打开temp-mail.org/zh/,发现下面多了一封openai发送的邮件,点开邮件


image.png


点击校验邮箱地址


image.png


3.3.身份输入


姓名输入,这个可以随便写,之后点击继续


image.png


3.4.手机号验证


接下来开始进行手机号验证


image.png


进入,sms-man.com/cn地址,先充值1美元的额度,接下来选择对应的国家,然后选择服务,找到OpenAi/ChatGPT,点击购买短信。


image.png


之后下方会多出一个号码,这个就是用来接收短信的号码


image.png


将短信复制到手机号填写的地方,删掉区号,点击发送短信


image.png


之后在虚拟手机号平台点击接收短信,这时候就会收到短信的验证码如下:


image.png


在ChatGPT验证码页面输入信息,验证通过后跳转对话页面。


3.5.Hello ChatGPT


验证码输入完成之后进入如下对话页面,来吧,作为传统第一步当然是来一句,Hello ChatGPT啦,到此,注册完毕,接下来就可以使用ChatGPT啦,你学废了吗?


image.png



作者:王老狮
来源:juejin.cn/post/7206732474113458231
收起阅读 »

ChatGPT 来我司面试前端工程师,考考他闭包

近日,有个叫 ChatGPT 的同学来我司面试前端,考考他面试八股文。先来问问闭包。 第一问,说说 JS 闭包 追问,详细讲讲闭包 篇幅太长,转成文字,以下皆为 ChatGPT 回答 闭包是一个非常重要的 JavaScript 概念,也是相对难以理解的概...
继续阅读 »

近日,有个叫 ChatGPT 的同学来我司面试前端,考考他面试八股文。先来问问闭包。


第一问,说说 JS 闭包


image.png


追问,详细讲讲闭包



篇幅太长,转成文字,以下皆为 ChatGPT 回答



闭包是一个非常重要的 JavaScript 概念,也是相对难以理解的概念之一。为了更好地理解闭包,我们需要介绍一些相关的概念和知识。



  1. 作用域


作用域(scope)是指在程序中能够访问变量、函数的区域。JavaScript 中有全局作用域和函数作用域两种。


全局作用域是指在整个程序中都可以访问的变量,它们在程序开始时就被创建,在程序结束时才被销毁。


函数作用域是指在一个函数内部声明的变量,只能在函数内部访问,而在函数外面是不能访问的。当函数执行完毕后,函数内部的变量会被销毁。



  1. 词法作用域


词法作用域(lexical scope)是指在编写程序时,变量和函数的作用域是通过它们在代码中声明的位置来确定的。


这意味着,函数可以访问在其外部定义的变量和函数,但在其内部定义的变量和函数不能被外部访问。这是因为 JavaScript 引擎识别变量和函数的作用域是根据它们在代码中的位置来决定的,而不是根据它们被调用的位置。



  1. 闭包的定义和作用


闭包(closure)是指函数能够访问其词法作用域之外的变量,即使在函数被调用后仍然可以访问。


换句话说,当一个函数在其外部定义的变量的作用域外被调用时,它可以访问这些变量,并且可以在调用完成之后继续访问这些变量。这就是闭包的作用。


举个例子,假设我们有一个函数 outer,它内部有一个变量 x,还返回了一个函数 inner,这个函数也访问了 x。那么,当我们调用 outer 函数并得到 inner 函数后,inner 函数就形成了一个闭包,它可以继续访问 outer 函数内部的变量 x


function outer() {
let x = 1;
function inner() {
console.log(x);
}
return inner;
}

let closure = outer();
closure(); // 1


在上面的例子中,我们通过调用 outer 函数得到了一个函数 closure,它可以访问 outer 函数内部的变量 x。在调用 closure 函数时,它会输出变量 x 的值。这就是闭包的作用。



  1. 闭包的用途和注意事项


闭包可以用来创建一些类似于私有变量和方法的功能,以及实现一些高阶函数,如柯里化等。


举例来说,我们可以使用闭包来实现一个计数器:


function counter() {
let count = 0;
return function() {
count++;
console.log(count);
};
}

let myCounter = counter();
myCounter(); // 1
myCounter(); // 2
myCounter(); // 3


在上面的例子中,counter 函数返回了一个函数,这个函数可以访问 counter 函数内部的变量 count,并且会在每次调用时将 count 加一,并输出加一后的结果。


需要注意的是,过多的闭包使用可能会导致性能问题和内存泄漏的风险,所以应该谨慎使用。特别是在循环语句中,不能滥用闭包。


for (var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i); // 输出的都是 10
}, 1000);
}

在上面的例子中,我们在循环中使用了闭包,希望在每次循环时输出当前的计数器。但实际上,这个程序的输出结果是十个 10,因为在循环中创建的闭包都是共享同一个变量 i,而这个变量在循环结束时已经变成了 10。这就是闭包的一个陷阱:不能在循环中共享变量,否则会导致意外的结果。


那么如何解决在循环中使用闭包的陷阱


要解决在循环中使用闭包的陷阱,我们需要创建一个独立的变量。在 ES6 中,我们可以使用 let 关键字来声明一个块级作用域的变量,每次循环都可以创建一个新的变量,从而避免在闭包中共享同一个变量。


例如,在下面的例子中,我们可以通过在循环中使用 let 关键字来解决闭包的陷阱:


for (let i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i); // 输出 0 到 9
}, 1000);
}

在上面的例子中,我们使用 let 关键字来声明变量 i,这样每次循环都会创建一个新的变量,并且每个闭包都可以访问自己私有的变量。因此,输出的结果会是 0 到 9。


如果使用 ES5 或更低版本的 JavaScript,我们可以使用一个立即执行函数表达式(IIFE)来创建一个块级作用域。例如:


for (var i = 0; i < 10; i++) {
(function(j) {
setTimeout(function() {
console.log(j); // 输出 0 到 9
}, 1000);
})(i);
}

在上面的例子中,我们创建了一个即时函数 function(j) { ... }(i),这个函数会立即执行并创建一个新的作用域,把变量 i 的值传递给参数 j,从而避免在闭包中共享同一个变量。


let 的块级作用域讲讲


ES6 中引入了 let 关键字,它的一个重要特性就是可以创建块级作用域。这意味着,变量在块级作用域中声明,只能在该作用域内部访问,离开作用域后就会被销毁。


块级作用域是指用花括号 {} 包围的代码块,例如函数、条件语句、循环语句等。


举个例子,在下面的代码中,我们使用 let 关键字来声明一个块级作用域的变量 x,它只能在块级作用域中访问。


function myFunction() {
if (true) {
let x = 10;
console.log(x); // 输出 10
}
console.log(x); // 报错:ReferenceError: x is not defined
}

在上面的代码中,变量 x 只能在 if 语句的代码块中访问,在代码块外部访问时会报错。


需要注意的是,块级作用域只适用于 letconst 关键字,而不适用于 var 关键字。


例如,在下面的代码中,我们使用 var 关键字来声明变量 x,它虽然在函数中声明,但是可以在函数外部访问。


function myFunction() {
if (true) {
var x = 10;
console.log(x); // 输出 10
}
console.log(x); // 输出 10
}

在上面的代码中,变量 x 虽然在 if 语句的代码块中声明,但是可以在函数外部访问。这是因为 var 关键字具有提升(hoisting)的特性,即变量会被提升到函数的顶部声明,所以可以在整个函数中访问。但是,它的值只有在实际声明之后才会被赋值。


最后


好了,今天先到这。回去等通知吧。




如果你觉得内容对你有帮助,欢迎「转发点赞收藏


你的鼓励是我最大的支持,猛戳“关注”,后续文章更精彩。


作者:李瑞丰_liruifengv
来源:juejin.cn/post/7206217352185184293
收起阅读 »

ChatGPT:吃喝嫖赌,哪个最伤身体?

ChatGPT:吃喝嫖赌,哪个最伤身体?吃喝嫖赌 哪个最伤身体?吃喝嫖赌这四个行为都可能对身体造成不良影响,但其影响程度可能因人而异,也取决于具体的行为情况和频率。然而,从普遍的角度来看,其中嫖和赌相对而言更容易带来身体上和心理上的负面影响。嫖娼:嫖娼可能会导...
继续阅读 »

ChatGPT:吃喝嫖赌,哪个最伤身体?



吃喝嫖赌 哪个最伤身体?

吃喝嫖赌这四个行为都可能对身体造成不良影响,但其影响程度可能因人而异,也取决于具体的行为情况和频率。然而,从普遍的角度来看,其中嫖和赌相对而言更容易带来身体上和心理上的负面影响。

  1. 嫖娼:嫖娼可能会导致性传播疾病(STDs)的传播和其他感染,如艾滋病等。嫖娼者也可能面临性侵害、暴力、精神问题等方面的风险。

  2. 赌博:赌博可能会导致赌徒出现赌瘾、债务累积、家庭破裂等问题,还可能导致抑郁症、焦虑症等心理问题。

  3. 喝酒:适量饮酒可能对身体并没有太大的伤害,但长期和过量饮酒可能导致多种健康问题,如肝损伤、胰腺炎、脑损伤等。

  4. 吃药:吸毒会对身体造成严重伤害,不仅可能导致健康问题,如肺病、心血管疾病等,还可能对神经系统和认知功能造成损害。

总的来说,以上这四种行为都是有害的,应该尽量避免。

继续

除了上述四种行为之外,还有其他一些行为也可能对身体造成不良影响,例如:

  1. 吸烟:吸烟是导致癌症、心血管疾病和呼吸系统疾病的主要原因之一,同时也会损害皮肤、牙齿和骨骼等多个方面的健康。

  2. 不健康的饮食:过量食用高热量、高脂肪和高糖的食物可能导致肥胖、糖尿病、心血管疾病等健康问题。

  3. 缺乏运动:缺乏运动会增加肥胖、心血管疾病、骨质疏松等疾病的风险,并可能影响心理健康。

  4. 缺乏睡眠:长期睡眠不足可能导致身体疲劳、免疫力下降、抑郁症等健康问题。

综上所述,以上这些行为都可能对身体造成伤害,人们应该注意健康的生活方式,避免不健康的行为,保护自己的身体健康。


来源:m.toutiao.com/is/SUJaT7K/

收起阅读 »

手把手教你使用 Python 调用 ChatGPT-3.5-API

大家好,我是老表 前天 OpenAI 开放了两个新模型的api接口,专门为聊天而生的 gpt-3.5-turbo 和 gpt-3.5-turbo-0301。 ChatGPT is powered by gpt-3.5-turbo, OpenAI’s mos...
继续阅读 »

大家好,我是老表


前天 OpenAI 开放了两个新模型的api接口,专门为聊天而生的 gpt-3.5-turbo 和 gpt-3.5-turbo-0301。




ChatGPT is powered by gpt-3.5-turbo, OpenAI’s most advanced language model.



从上面这句话,我们可以知道现在 chat.openai.com 官网就是由 gpt-3.5-turbo 模型提供的服务,现在官方公开了这一模型的调用接口,这使得我们这些普通开发者也能直接在自己的应用/服务中使用这个狂揽亿万用户的模型。


接下来将和大家介绍如何利用 Python 快速玩转 gpt-3.5-turbo。


本文所有代码已开源,持续更新中:XksA-me/ChatGPT-3.5-AP


先跑起来,再理解


首先你需要有一个 openai 账号,如何注册我就不多说了,网上教程很多,而且很详细,如果有问题可以加我微信:pythonbrief,添加通过后请直接描述你的问题+问题截图


访问下面页面,登录 openai 账号后,创建一个 api keys。


# api keys 创建页面
https://platform.openai.com/account/api-keys

接下来很简单了,安装 openai 官方的 Python SDK,这里需要注意的是得安装最新版本 openai,官方推荐的是 0.27.0 版本。


pip install openai==0.27.0

直接上请求代码:


import openai
import json

# 目前需要设置代理才可以访问 api
os.environ["HTTP_PROXY"] = "自己的代理地址"
os.environ["HTTPS_PROXY"] = "自己的代理地址"


def get_api_key():
# 可以自己根据自己实际情况实现
# 以我为例子,我是存在一个 openai_key 文件里,json 格式
'''
{"api": "你的 api keys"}
'''

openai_key_file = '../envs/openai_key'
with open(openai_key_file, 'r', encoding='utf-8') as f:
openai_key = json.loads(f.read())
return openai_key['api']

openai.api_key = get_api_key()

q = "用python实现:提示手动输入3个不同的3位数区间,输入结束后计算这3个区间的交集,并输出结果区间"
rsp = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": "一个有10年Python开发经验的资深算法工程师"},
{"role": "user", "content": q}
]
)

代码解析:




  • get_api_key() 函数是我自己写的一个从文件读取 api keys 的方法,我是存在一个 openai_key 文件里,json 格式,你可以改成你自己的获取方法,甚至可以直接写到代码里(虽然不推荐,但确实最简单)。




  • q 是请求的问题




  • rsp 是发送请求后返回结果




  • openai.ChatCompletion.create 中参数





    • model 是使用的模型名称,是一个字符串,用最新模型直接设置成gpt-3.5-turbo 即可





    • messages 是请求的文本内容,是一个列表,列表里每个元素类型是字典,具体含义如下表:




  • 程序运行返回内容,从响应回复内容我们可以看到,回复内容是一个 json 字符串,

    我们可以通过以下方法直接获取相关信息:








    • 返回消息内容




rsp.get("choices")[0]["message"]["content"]




    • 角色




rsp.get("choices")[0]["message"]["role"]




    • 问题+回答总长度




rsp.get("usage")["total_tokens"]

其他信息也可以通过类似方法获取。



  • 测试 ChatGPT 回答代码运行情况,可以看出代码逻辑和运行都没啥问题,注释也到位。



实现多轮对话


如何实现多轮对话?


gpt-3.5-turbo 模型调用方法 openai.ChatCompletion.create 里传入的 message 是一个列表,列表里每个元素是字典,包含了角色和内容,我们只需将每轮对话都存储起来,然后每次提问都带上之前的问题和回答即可。




  • 效果图





    • 可以看到,我首先问了“1+1=几”,然后问“为什么是这样”,ChatGPT 会根据前面的提问将新问题识别为“为什么1+1=2”。





    • 后面继续问水仙花数有哪些,再问“如何写个python程序来识别这些数”,ChatGPT 同样会根据前面的提问将新问题识别为“如何写个python程序来识别这些水仙花数”,并给出对应解答。




  • 实现代码




import openai
import json
import os

os.environ["HTTP_PROXY"] = "http://127.0.0.1:7890"
os.environ["HTTPS_PROXY"] = "http://127.0.0.1:7890"

# 获取 api
def get_api_key():
# 可以自己根据自己实际情况实现
# 以我为例子,我是存在一个 openai_key 文件里,json 格式
'''
{"api": "你的 api keys"}
'
''
openai_key_file = '../envs/openai_key'
with open(openai_key_file, 'r', encoding='utf-8') as f:
openai_key = json.loads(f.read())
return openai_key['api']
openai.api_key = get_api_key()



class ChatGPT:
def __init__(self, user):
self.user = user
self.messages = [{"role": "system", "content": "一个有10年Python开发经验的资深算法工程师"}]
self.filename="./user_messages.json"

def ask_gpt(self):
# q = "用python实现:提示手动输入3个不同的3位数区间,输入结束后计算这3个区间的交集,并输出结果区间"
rsp = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=self.messages
)
return rsp.get("choices")[0]["message"]["content"]


def writeTojson(self):
try:
# 判断文件是否存在
if not os.path.exists(self.filename):
with open(self.filename, "w") as f:
# 创建文件
pass
# 读取
with open(self.filename, 'r', encoding='utf-8') as f:
content = f.read()
msgs = json.loads(content) if len(content) > 0 else {}
# 追加
msgs.update({self.user : self.messages})
# 写入
with open(self.filename, 'w', encoding='utf-8') as f:
json.dump(msgs, f)
except Exception as e:
print(f"错误代码:{e}")


def main():
user = input("请输入用户名称: ")
chat = ChatGPT(user)

# 循环
while 1:
# 限制对话次数
if len(chat.messages) >= 11:
print("******************************")
print("*********强制重置对话**********")
print("******************************")
# 写入之前信息
chat.writeTojson()
user = input("请输入用户名称: ")
chat = ChatGPT(user)

# 提问
q = input(f"【{chat.user}】")

# 逻辑判断
if q == "0":
print("*********退出程序**********")
# 写入之前信息
chat.writeTojson()
break
elif q == "1":
print("**************************")
print("*********重置对话**********")
print("**************************")
# 写入之前信息
chat.writeTojson()
user = input("请输入用户名称: ")
chat = ChatGPT(user)
continue

# 提问-回答-记录
chat.messages.append({"role": "user", "content": q})
answer = chat.ask_gpt()
print(f"【ChatGPT】{answer}")
chat.messages.append({"role": "assistant", "content": answer})


if __name__ == '__main__':
main()

代码解析:




  • ChatGPT 类,包含三个函数:





    • __init__初始化函数,初始化了三个个实例变量,user、messages、filename(当前用户、消息列表、存储记录的文件路径)。





    • ask_gpt函数,将当前用户所有历史消息+最新提问发送给 gpt-3.5-turbo ,并返回响应结果。





    • writeTojson函数,结束/重置用户时记录当前用户之前的访问数据。




  • main函数,程序入口函数,用户输入用户名后进入与 ChatGPT 的循环对话中,输入 0 退出程序,输入 1 重置用户,退出和重置都会将当前用户之前访问数据记录搭配 json 文件中。




  • 由于 gpt-3.5-turbo 单次请求最大 token 数为:4096,所以代码里限制了下对话次数。




更多拓展



  • 你可以写个函数,从 json 文件读取历史用户访问记录,然后每次访问可以选用户。

  • 你可以写个 web 服务,使用 session 或者数据库支持多用户同时登录,同时访问。

  • 你可以基于之前分享的钉钉机器人项目,将 gpt-3.5-turbo 接入钉钉机器人。


你还可以上 Github 搜索更多 ChatGPT 相关项目,或者其他有意思的项目学习练手,欢迎学习交流。


我创建了个 ChatGPT 应用交流群,如果你感兴趣可以扫下方二维码添加我微信申请加入。


项目已开源,持续更新中:XksA-me/ChatGPT-3.5-AP


作者:老表
来源:juejin.cn/post/7206511128277024823
收起阅读 »

这一年,我终于成为了一名真正的前端

前言 时间很快,2022年一整年好像都很忙,也终于从学生过渡到了一名社会人 2022上半年,大四,独自一人去杭州实习,孤独,但在杭州实习的那几个月,是我成长最快的几个月。 2022下半年,毕业,拒绝掉转正offer,拒绝掉对于应届生来说可观的薪资。回成都找了一...
继续阅读 »

前言


时间很快,2022年一整年好像都很忙,也终于从学生过渡到了一名社会人


2022上半年,大四,独自一人去杭州实习,孤独,但在杭州实习的那几个月,是我成长最快的几个月。


2022下半年,毕业,拒绝掉转正offer,拒绝掉对于应届生来说可观的薪资。回成都找了一份离家近,但是薪资少一半的工作,理想中就是钱少事少,下班后能开启新生活的工作,但事实真的如此吗?好多同学、长辈问过我后不后悔,当然后悔了,肠子都悔青了,属于是亲身体会了什么叫卷,什么叫钱少事情多压力大,再不涨薪已经准备提桶跑路了。我说:不后悔,毕竟成长了。


打工人的眼泪


见过凌晨五点的成都吗,我见过,那时候我刚下班。但是第二个月看着我四位数的工资的时候,我还是没忍住骂了一句:wtf?我为什么不去送外卖?


image.png

我真的不想把一个原本完美的毕业人设硬生生的过成了一个需要从低薪摸爬滚打上去的人设,但事实是,我好像在第二条路上越走越远。看看现在的自己,确实是有点奋斗逼(贬义)的模样(伤感)。


不可忽略的成长


不可否认的是,这一年,我通过自己的努力从一个前端小白变成了一个可以独当一面的初级前端工程师。


因为大学的专业是数字媒体技术,大一大二学完计算机科班相关知识后,大三主攻的方向有三个:J2EE,游戏开发,动画。


在没经历过秋招的摧残之前,我还沉迷于游戏开发,3d建模,剪视频,做动画。秋招的时候信心满满的投简历,但悲惨的结果就是秋招0 offer(真不怪专业,怪我菜)。看到隔壁软件工程的同学秋招收获满满,想到自己做游戏开发也接触过脚本语言,于是一个从来没接触过web开发的焦虑的大四应届生果断入坑了web前端。


回顾我的2022,大概是这样的:




  • 1月 在系统学习了两个月前端基础知识(b站,红宝书,付费课程)的基础上,狂刷面试题。




  • 2月 继续狂刷面试题,自学vue3框架,做了一个很简单的后台管理系统(vue3+axios+element plus)。凭借这个项目找到了我的第一份正式的实习




  • 3月-5月 独自前往杭州实习,开始接触企业级项目,学会了git操作,css预编译sass,响应式布局,学会去封装组件,学会用node.js(egg.js框架)。与此同时,利用我的所有空闲时间去做毕业设计(原生微信小程序+mysql+nodejs+vue3后台管理系统)那段时间每天只能睡4-5个小时,睁开眼睛就要开始敲代码,但不得不说,这是我成长最最最快的三个月。




  • 6月 回到成都远程办公,完成我的毕业论文,顺利答辩,顺利毕业,转正,离职,毕业旅行。




  • 7月 毕业旅行完回成都找工作,因为7月只能参加社招,社招跟校招相比薪资待遇没那么好,但竞争不大。凭借之前的实习经验和看得过去的毕设一周拿到了三份offer(所以2023届的学弟学妹真的不要焦虑!),7月中旬入职,成为一名正式的前端开发工程师。




  • 8月 第一次接触大屏开发,因为项目压力很大,自己也有很多不熟悉的东西,经常加班到凌晨。(抗压能力++




  • 9月 第一次独立搭建项目,这段时间成长也很快。从0搭建一个项目与在一个大项目里面写功能写组件的差别很大!要考虑的东西非常多,从目录结构到变量命名都需要注意。这段时间我开始学习工程化,架构搭建的知识。了解vite的一些配置,开始注意一些代码规范(eslint,prettier),提交规范(commitlint)。也开始尝试使用TypeScript (独立开发能力++




  • 10月 可视化开发,开始熟悉echarts相关知识,转正。(新技能++




  • 11月 普通中后台项目的开发,之前的项目维护。与此同时,公司开发流程逐渐规范,对代码质量也有了一些要求,开始更注重代码的规范。公司有了技术分享会,前端后端一起学习分享,学到了一些工具的使用,大多都是在学校学不到的东西,收获很大,工作效率有了一定程度的提升。(代码整洁度++ 新技能++




  • 12月 继续之前项目的维护,新的项目开始使用公司更规范更统一的脚手架搭建。这个时候了解了一些自动化ci部署,github action的使用。同时开始总结知识,写了几篇质量不高的博客。并开始尝试研究Git flow




以上就是我这一年,从一个前端小白进化成一名初初初初级前端的过程,过程中很感谢大佬们正确的引导,虽然一直都是项目驱动成长,没有构建出完整的知识体系,但也算是入行了吧。可以给想要成为前端er的学弟学妹们一点点参考


2022年值得记录的瞬间


来杭州的第一天,感叹阿里人周末也要加班。


image.png

第一次做自我介绍


image.png

通宵肝毕设的日子


image.png

成为一名合格的铲屎官,小猫名字叫缓存,现在已经八斤啦


image.png

回成都后加班成为了日常便饭


image.png

2022给我带来了很多新奇的体验,浅浅分享一小部分最最值得纪念的瞬间。


关于2023的期望


立一点关于2023的flag:



  • 需要更加熟练已有的技能

  • 根据前端roadmap点亮更多技能。

  • 学习框架原理、库、网络、性能、浏览器、协议、工程化、node相关知识,完善知识体系,不需要全部深入,但要尽可能了解,提升知识广度。

  • 懂业务,深入业务,能解决业务痛点,更好的实现自己的价值

  • 学会做饭

  • 找个对象


最后最后:



希望自己:严于律己,减少焦虑


希望公司:减少加班,工资翻番



作者:一只囡狗
来源:juejin.cn/post/7187310000333652005
收起阅读 »

写给想转行学IT的朋友们的话

最近有很多朋友咨询我关于想转行学IT的问题,我想结合自身的经历和思考和各位聊几句,本文带有比较强的主观色彩,因此文中观点仅供参考,如有不当之处,敬请海涵。 笔者19年本科毕业于一所中流211,毕业之后一直在做Java开发,我并没有进大厂,也没有年薪百万,就是芸...
继续阅读 »

最近有很多朋友咨询我关于想转行学IT的问题,我想结合自身的经历和思考和各位聊几句,本文带有比较强的主观色彩,因此文中观点仅供参考,如有不当之处,敬请海涵。


笔者19年本科毕业于一所中流211,毕业之后一直在做Java开发,我并没有进大厂,也没有年薪百万,就是芸芸众生中普通的那一个。现在回顾当时入行的经历,基本是摸着石头过河,因此在有了一些经验之后,就想给和我当时一样处境的朋友一点建议,希望能对你有所启发。


很多问题其实都要结合个人的实际情况来看,每个人的知识、能力、经历都各不相同,所以无论做出任何决定,都需要结合自身的情况。


是否有必要参加培训机构?


这个问题的答案是因人而异的,最主要的点在于你是否有足够的恒心和自制力。


现在互联网上的学习资料已经非常多了,足够支撑你从零学到能够入行的过程。如果你的自制力比较好,那么你就可以尝试自学,不过自学的过程是孤独的,也是难熬的;如果你的自制力一般,无法在不是“学校”那么环境下进行学习,那就可以考虑培训机构。


需要特别强调的是,不能以为进了培训机构就以为一定能够就业了,说到底,学习这件事情,还是要靠你自己,别人是没有办法把知识灌进你的脑袋里的。培训机构也有一些无法忽视的问题,当你毕业之后,大概率机构会给你伪造一份简历,本来学习的时间就并不是很长,你很快可能就会发现,培训机构里面学的比较浅,还不足以应付面试官的八股文,这时候你可能就会想先入行再说,然后进了一家外包公司,然后开始混日子,这也是笔者见过的最多的案例。


参加培训机构最大的问题是简历,培训机构给大家做的项目都是极其雷同的,也可能会在培训机构之后,让你自己编项目经验,有经验的面试官很容易就能判断出,你是真的参与了项目开发,还是只是包装的。另外你可能还会碰到,很多公司需要上一家公司的离职证明和银行流水,甚至需要你个税APP报税界面的录屏等等问题,这些在求职的时候会碰到的现实问题,培训机构并不会告诉你。


总而言之,转行是一件对你的人生来说,是一件极其重要的事情,要反复思虑,不要脑子一热,听身边的朋友说,那个谁谁谁培训了几个月就月入过万,然后就稀里糊涂花了几万参加了培训机构,结果培训结束真正找工作的时候又发现困难重重,现实往往比想象的残酷。


高中毕业可以从事IT行业吗?


真相是,现在如果要从事IT行业,大专及以上学历是基本要求,不排除个人大专以下学历也找到了很好的IT工作,但是在当下这么“卷”的环境下,就是幸存者偏差了。大专的学历我也非常建议你先提升学历,有一个本科学历。


互联网爆发的红利期已经过去了,在没有新的风口出现的情况下,互联网的业务和从业人员都已经趋于饱和。换句话说,公司也不再会盲目进行扩张,进而产生大量的劳动力缺口,这也是目前为什么互联网公司会越来越在乎学历的原因之一。可以预见,进入这个行业的门槛会越来越高。


前端、后端还是测试?


其实从某种意义来说,选择岗位就是选择某种编程语言,选择编程语言也是在选择岗位,这个问题,你也可以结合下文编程语言之间的对比,找到自己合适的岗位。大体而言,测试、前端、后端,这三者的难度是依次递增的(对大部分人而言)。


如果你没有科班背景,但是又想从事IT行业,那么你可以考虑从事测试、运维工作甚至产品、运营的工作,其实学会写代码并不是唯一的选择。


如果你有一定的基础,但是逻辑思维又不是那么强,那么我建议你可以考虑前端,女生大部分学习编程都会选择前端,前端由于不需要考虑架构、性能(大部分场景),因此难度会小一些,对初学者更加友好。不过,前端经过这些年的发展,知识体系俨然已经非常庞大,后期也需要学习很多的东西。


如果你有一定的基础,且逻辑思维还可以,那么我建议你可以考虑后端,如果你不知道自己的思维能力怎么样,可以学一些Java或者JavaScript语言基础,看看自己是不是能够比较轻松的学会。


Java、Python、C++还是JavaScript?


编程语言只是工具,并没有优劣之分,我们需要在不同的场景选择合适的编程语言。下面我将对常见的几种编程语言做浅要的分析,希望能帮助你找到最适合你的编程语言。


随着时代发展,有了越来越多新的优秀的编程语言,比如Rust、kotlin、Dart等等,它们都在努力的甩掉前面编程语言的历史包袱,但是截止目前,相关的岗位的数量还不是很多,与我下面提到的编程语言相比,它们更适合作为你的第二门编程语言。


Java


如果你已经决定了要从事软件开发行业,但是还不清楚,要选择哪一门编程语言,那我推荐你可以学习Java,Java这门语言本身虽然并不优秀,截止目前,Java已经走过了20个年头,有着非常丰富的生态,web端,它有JSP、Servlet,移动端,它有Android,服务器端有SpringBoot,桌面端它有JavaFX,也有非常优秀的网络通信框架,比如Netty,甚至它也做出过操作系统(塞班系统),可以说,Java虽然很“烂”,但是它真的几乎无所不能。


不可否认Java的成功,但是Java也存在一些隐患,比如在Oracle收购了Sun公司之后,在Oracle不断地花式作妖下,Java的未来似乎也有些扑朔迷离。除此之外,Java在引以为傲的领域也逐渐有了一些竞争对手,比如服务端有go语言、nodejs,Android的开发官方首选语言已经变成kotlin,JSP的市场基本上已经被Vue、React等SPA框架所替代,但是Java却并没有开拓出自己新的应用场景,虽然笔者认为,基于VM的语言并不是消失,但是Java是能否一直守住自己的王座,还是要打一个问号的。


Java目前最多的就业方向就是服务端开发,如果你学习了Java,那么大概率会做服务端开发,短期内,服务端开发Java还是很难被其他语言替代。强类型的语言加上对并发编程的支持,让Java非常适合构建大型的服务端应用,这也是Java最深耕的方向,学会Java服务端的开发,也比较方便向大数据或数据分析岗位进行转型。


最后我想说,Java的岗位在我提到的编程语言里面也是最多的,这也是我推荐你学习它的一个重要原因,很多编程语言,虽然看起来很热门但是,但是当你真正学完去找工作的时候,却又发现岗位少的可怜,没错,说的就是你,Python。


Python


Python这门编程语言,“胶水语言”的特性让它看起来也几乎无所不能,虽然这种能力很有可能来自其他的语言,Python只是作为客户端,调用其他语言的类库。


不够“底层”其实并不是什么缺点,学习Python最大的问题是,是对学历有要求,Python应用最广泛的领域,大部分都对学历有要求,虽然你可能看过很多投放Python的广告,自动化办公、爬虫等等,但是这些特性在企业中的岗位是比较少的,而Python擅长的算法、人工智能、深度学习等领域都是需要研究生学历的。


我们总结一下,如果你有研究生学历,那么学习Python是一个不错的选择,不过我也见过有的研究生朋友学完Python之后发现岗位很少,又被迫转Java的案例。


C++


C++相比于其他的编程语言,其入门的门槛的会更高,花费在学习语言本身的时间的也会很多。C++在经历过这么多年的迭代之后,语言的特性非常多,会让初学者有一种眼花缭乱的特性,实现同一个功能,可能会有很多种写法。不可否认,C++是一门优秀的编程语言,但是高昂的学习成本也让人望而却步,如果你不是科班出身,只是想学一门技术进入互联网行业,那C++并不是一个很好的选择。


C++常见的就业方向有以下几种:服务器端开发、游戏、QT、嵌入式、人工智能。可以看出,C++最适合的还是那些对性能有要求的场景。原来很大一部分C++程序员都是做QT开发,但是C++在桌面端的市场已经出现了越来越多的竞争者,比如Hybrid 技术、React Native、Weex、Flutter,这些技术让原来web端的程序员或者移动端的程序员也能写出跨平台的应用,并且这些技术保留了他们原本的开发习惯,这对QT的打击,无疑是毁灭性的。


那么C++是不是已经过时,或者即将被淘汰呢?其实不然,C++只是让出了一些自己不是那么擅长的领域,但是在底层应用的开发,C++丰富的生态和优越的性能还是首选。如果你有志在这些领域发展,那么C++将是你很好的选择。


随着越来越多的人对核心自研技术的重视,这种比较偏底层的岗位,会越来越多,最近几年,国产操作系统的发展就是一个很好的例子,在政策的扶持下,各家公司几乎都在号称自研操作系统,可以预料,在不远的将来,这些核心的系统的研发,都需要大量C/C++语言的人才。


JavaScript


作为一门十天就被开发出来的编程语言,JavaScript身上的历史包袱也有很多,但这并不妨碍它的伟大,JavaScript在我们的日常生活中,几乎无处不在。


JavaScript也是一门易学难精的语言,虽然上手比较容易,但是后期也需要很多的时间去理解和巩固。JavaScript是前端程序员最重要的技术,没有之一,学好了JavaScript,就能在前端这个领域里无所不能。JavaScript现在也开始逐渐的在其他领域崭露头脚,它与TypeScript相辅相成,可以预见,JavaScript是一门历久弥新,并且前景良好的编程语言。


笔者对JavaScript最深的感受就是,与JavaScript相关的技术栈的更新迭代都非常的快,在我刚刚毕业的时候,Vue.js还是非常新鲜的东西,截止今天,Vue.js已经成为前端找工作的必备技能。JavaScript的框架总是层出不穷,让人有一种学不动了的感觉。造成这种“混乱”的背后原因有很多,这也从侧面反映了大家对JavaScript的热爱和探索。


路漫漫其修远兮,吾将上下而求索


我非常建议你在决定入行之前,提前找一些学习资料,自己尝试一下是否能够学的清楚,并且能从中获取乐趣。


兴趣和恒心是决定你能不能在这个行业长足发展的决定性因素。面对海量的,对你而言是闻所未闻的知识,如果没有兴趣,你每天都会深受折磨;这个行业特质决定了我们需要不断地学习,没有恒心,总有一天会掉队,跟不上技术更新迭代的脚步。


总而言之,道阻且长,希望本文能对你

作者:清风孤月残酌
来源:juejin.cn/post/7205549412352868412
有所启发,与君共勉。

收起阅读 »

Android:我是如何优化APP体积的

前言 在日常开发中,随着APP功能迭代发现打出的安装包体积越来越大,这里说的大是猛增的那种大,而并非一点一点增大。从最开始的几兆到后面的几十兆,虽然市面上的很多APP甚至达到上百兆,但毕竟别人功能强大,用到的一些底层库就特别占面积,流量也多所以也可理解。但自...
继续阅读 »

前言



在日常开发中,随着APP功能迭代发现打出的安装包体积越来越大,这里说的大是猛增的那种大,而并非一点一点增大。从最开始的几兆到后面的几十兆,虽然市面上的很多APP甚至达到上百兆,但毕竟别人功能强大,用到的一些底层库就特别占面积,流量也多所以也可理解。但自研的一些APP可经不住这些考验,所以能压缩就压缩,能优化就尽量优化,以得到用户最好的体验,下面就来说说我在项目中是如何优化APP体积的。



1. 本地资源优化


这里主要是压缩一些图片和视频。项目中本地资源用到最多的应该就是图片,几乎每个页面都离不开图标,甚至一些页面采用大图片的形式。你可知道,正常不经压缩的图片大的可以上大几十兆,小则也是一兆起步。这里做了个实验,同一个文件分别采用svg、png、使用tiny压缩后的png、webp四种类型图片进行展示(顺序是从左到右,从上到下):


image.png


可以看到,加载出来的效果几乎没有什么区别,但体积却有很大的差别(其中webp是采取的默认75%转换):


image.png


所以,别再使用png格式图片,太浪费资源了,就算经过压缩还是不及svg和webp,这里的webp其实还可以加大转换力度,但个人还是比较喜欢svg。


至于音视频文件也是可以通过其他工具进行压缩再放入本地,如非必要,尽量还是使用网络资源。


2. lib优化


一些三方库会使用底层so文件,一般在配置的时候我们尽量选择一种cpu类型,这里选择armeabi-v7a,其实几乎都兼容


ndk {
//设置支持的SO库架构 armeabi、armeabi-v7a、arm64-v8a、x86、x86_64、mips、mips64
abiFilters 'armeabi-v7a'
}

可以看看APK体积分析,每种cpu占用体积都比较大,少配置一种就能省下不少空间。
image.png


3. 代码混淆、无用资源的删除


在bulid.gradle中配置minifyEnabled true开启代码混淆,还需要配置混淆规则,否则无法找到目标类。shrinkResources true则是打包时不会将无用资源打入包内,这里有个小坑。之前使用腾讯地图时,某些第三方的静态资源会因为这个操作不被打入包内,导致无法找到资源,所以根据具体情况使用。


 release {
buildConfigField "boolean", "LOG_DEBUG", "false"
minifyEnabled true
// shrinkResources true 慎用,可能会导致第三方资源文件找不到
zipAlignEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}

4. 代码复用,剔除无用代码


项目中由于多人协同开发会出现各写各的情况,需要抽出一些公共库之类的工具,方便代码复用。一些注释掉的代码该删除就删除。其实这一部分优化的体积相当少,但也得做,也是对代码质量的一种提升。


总结


其实只要做到了以上四步,APP体积优化已经得到了很大程度的提升了,其他再怎么优化效果也不是很明显了,最主要的就是本地资源和第三方so包体积占用较多。图片的使用我们尽量做到:小图标用svg,全屏类的大图可以考虑webp,最好不要使用png。ndk配置最好只配置一款cpu,几乎都可兼容,万不得已再加一个。


以上便是全部内容,希望对大家有所帮助。



作者:似曾相识2022
来源:juejin.cn/post/7206292770277261368
收起阅读 »

面试题:Android 中 Intent 采用了什么设计模式?

答案是采用了原型模式。 原型模式的好处在于方便地拷贝某个实例的属性进行使用、又不会对原实例造成影响,其逻辑在于对 Cloneable 接口的实现。 话不多说看下 Intent 的关键源码:  // frameworks/base/core/java/andro...
继续阅读 »

答案是采用了原型模式


原型模式的好处在于方便地拷贝某个实例的属性进行使用、又不会对原实例造成影响,其逻辑在于对 Cloneable 接口的实现。


话不多说看下 Intent 的关键源码:


 // frameworks/base/core/java/android/content/Intent.java
 public class Intent implements Parcelable, Cloneable {
    ...
     private static final int COPY_MODE_ALL = 0;
     private static final int COPY_MODE_FILTER = 1;
     private static final int COPY_MODE_HISTORY = 2;
 ​
     @Override
     public Object clone() {
         return new Intent(this);
    }
 ​
     public Intent(Intent o) {
         this(o, COPY_MODE_ALL);
    }
 ​
     private Intent(Intent o, @CopyMode int copyMode) {
         this.mAction = o.mAction;
         this.mData = o.mData;
         this.mType = o.mType;
         this.mIdentifier = o.mIdentifier;
         this.mPackage = o.mPackage;
         this.mComponent = o.mComponent;
         this.mOriginalIntent = o.mOriginalIntent;
        ...
 ​
         if (copyMode != COPY_MODE_FILTER) {
            ...
             if (copyMode != COPY_MODE_HISTORY) {
                ...
            }
        }
    }
    ...
 }

可以看到 Intent 实现的 clone() 逻辑是直接调用了 new 并传入了自身实例,而非调用 super.clone() 进行拷贝。


默认的拷贝策略是 COPY_MODE_ALL,顾名思义,将完整拷贝源实例的所有属性进行构造。其他的拷贝策略是 COPY_MODE_FILTER 指的是只拷贝跟 Intent-filter 相关的属性,即用来判断启动目标组件的 actiondatatypecomponentcategory 等必备信息。无视启动 flagbundle 等数据。


 // frameworks/base/core/java/android/content/Intent.java
 public class Intent implements Parcelable, Cloneable {
    ...
     public @NonNull Intent cloneFilter() {
         return new Intent(this, COPY_MODE_FILTER);
    }
 ​
     private Intent(Intent o, @CopyMode int copyMode) {
         this.mAction = o.mAction;
        ...
 ​
         if (copyMode != COPY_MODE_FILTER) {
             this.mFlags = o.mFlags;
             this.mContentUserHint = o.mContentUserHint;
             this.mLaunchToken = o.mLaunchToken;
            ...
        }
    }
 }

还有中拷贝策略是 COPY_MODE_HISTORY,不需要 bundle 等历史数据,保留 action 等基本信息和启动 flag 等数据。


 // frameworks/base/core/java/android/content/Intent.java
 public class Intent implements Parcelable, Cloneable {
    ...
     public Intent maybeStripForHistory() {
         if (!canStripForHistory()) {
             return this;
        }
         return new Intent(this, COPY_MODE_HISTORY);
    }
 ​
     private Intent(Intent o, @CopyMode int copyMode) {
         this.mAction = o.mAction;
        ...
 ​
         if (copyMode != COPY_MODE_FILTER) {
            ...
             if (copyMode != COPY_MODE_HISTORY) {
                 if (o.mExtras != null) {
                     this.mExtras = new Bundle(o.mExtras);
                }
                 if (o.mClipData != null) {
                     this.mClipData = new ClipData(o.mClipData);
                }
            } else {
                 if (o.mExtras != null && !o.mExtras.isDefinitelyEmpty()) {
                     this.mExtras = Bundle.STRIPPED;
                }
            }
        }
    }
 }

总结起来:


Copy Modeaction 等数据flags 等数据bundle 等历史
COPY_MODE_ALLYESYESYES
COPY_MODE_FILTERYESNONO
COPY_MODE_HISTORYYESYESNO

除了 Intent,Android 源码中还有很多地方采用了原型模式。




  • Bundle 也实现了 clone(),提供了 new Bundle(this) 的处理:


     public final class Bundle extends BaseBundle implements Cloneable, Parcelable {
        ...
         @Override
         public Object clone() {
             return new Bundle(this);
        }
     }



  • 组件信息类 ComponentName 也在 clone() 中提供了类似的实现:


     public final class ComponentName implements Parcelable, Cloneable, Comparable<ComponentName> {
        ...
         public ComponentName clone() {
             return new ComponentName(mPackage, mClass);
        }
     }



  • 工具类 IntArray 亦是如此:


     public class IntArray implements Cloneable {
        ...
         @Override
         public IntArray clone() {
             return new IntArray(mValues.clone(), mSize);
        }
     }



原型模式也不一定非得实现 Cloneable,提供了类似的实现即可。比如:




  • Bitmap 没有实现该接口但提供了 copy(),内部将传递原始 Bitmap 在 native 中的对象指针并伴随目标配置进行新实例的创建:


     public final class ComponentName implements Parcelable, Cloneable, Comparable<ComponentName> {
        ...
         public Bitmap copy(Config config, boolean isMutable) {
            ...
             noteHardwareBitmapSlowCall();
             Bitmap b = nativeCopy(mNativePtr, config.nativeInt, isMutable);
             if (b != null) {
                 b.setPremultiplied(mRequestPremultiplied);
                 b.mDensity = mDensity;
            }
             return b;
        }
     }


  • <
    作者:TechMerger
    来源:juejin.cn/post/7204013918958649405
    li>
收起阅读 »

Android斩首行动——接口预请求

前言 开发同学应该都很熟悉我们页面的渲染过程一般是从Activity#onCreate开始,再发起网络请求,等请求回调回来后,再基于网络数据渲染页面。可以用下面这幅图来粗略描述这个过程: 可以看到,目标页面渲染完成前必须得等待网络请求,导致渲染速度并没有那么...
继续阅读 »

前言


开发同学应该都很熟悉我们页面的渲染过程一般是从Activity#onCreate开始,再发起网络请求,等请求回调回来后,再基于网络数据渲染页面。可以用下面这幅图来粗略描述这个过程:


image.png


可以看到,目标页面渲染完成前必须得等待网络请求,导致渲染速度并没有那么快。尤其是当网络并不好的时候感受会更加明显。并且,当目标页面是H5页面或者是Flutter页面的时候,因为涉及到H5容器与Flutter容器的创建,白屏时间会更长。


那么有没有可能提前发起请求,来缩短网络请求这一部分的等待时间呢?这就是我们今天要讲的部分,接口预请求。


目标


我们要达到的目标很简单,就是提前异步发起目标页面的网络请求,从而加快目标页面的渲染速度。改善后的过程可以用下图表示:


image.png


并且,我们的预请求能力需要尽量少地侵入业务,与业务解耦,并保证能力的通用性,适用于工程内的任意页面(Android页面、H5页面、Flutter页面)。


方案


整体链路


首先给大家看一下整体链路,具体的细节可以先不用去抠,下面会一一讲到。


image.png


预请求时机


预请求时机一般有三种选择:



  1. 由业务层自行选择时机进行异步预请求

  2. 点击控件时进行异步预请求

  3. 路由最终跳转前进行异步预请求


第1种选择,由业务层自行选择时机进行预请求,需要涉及到业务层的改造,以及对时机合理性的把握。一方面是存在改造成本,另一方面是无法保证业务侧调用时机的合理性。


第2种选择,点击控件时进行预请求。若点击时进行预请求,点击事件监听并不是业务域统一的,无法形成有效封装。并且,若后续路由拦截器修改了参数,或是终止了跳转,这次预请求就失去了意义。


因此这里我们选择第3种,基于统一路由框架,在路由最终跳转前进行预请求。既保证了良好的封装性,也实现了对业务的零侵入,同时也做到了懒请求,即用户必然要发起该请求时才会去预请求。这里需要注意的是必须是在最终跳转前进行预请求,可以理解为是路由的最后一个前置异步拦截器。


预请求规则配置


我们通过本地的json文件(当然,有需要也可以上云通过配置后台下发),对预请求的规则进行配置,并将这份配置在App启动阶段异步读入到内存。后续在路由过程中,只有命中了预请求规则,才能发起预请求。配置demo如下:


{
"routeConfig":{
"scheme://domain/path?param1=true&itemId=123":["prefetchKey"],
"
route2":["prefetchKey2"],
"
route3":["prefetchKey3","prefetchKey4"]
},
"
prefetcher":{
"
prefetchKey":{
"
prefetchType":"network",
"
prefetchInfo":{
"
api":"network.api.name",
"
apiVersion":"1.0",
"
method":"post",
"
needLogin":"false",
"
showLoginUI":"false",
"
params": {
"
itemId":"$route.itemId",
"
firstTime":"true"
},
"
headers": {

},
"
prefetchImgInResponse": [
{
"
imgUrl":"$data.imgData.img",
"
imgWidth":"$data.imgData.imgWidth",
"
imgHeight":150
}
]
}
},
"
prefetchKey2":{
"
prefetchType":"network",
"
prefetchInfo":{
"
api":"network.api.name2",
"
apiVersion":"1.0",
"
method":"post",
"
needLogin":"false",
"
showLoginUI":"false",
"
params": {
"
itemId":"$route.productId",
"
firstTime":"false"
},
"
headers": {

}
},
"
prefetchKey3":{
"
prefetchType":"image",
"
prefetchInfo":{
"
imgUrl":"$route.imgUrl",
"
imgWidth":"$route.imgWidth",
"
imgHeight": 150
}
},
"
prefetchKey4":{
"
prefetchInfo":{}
}
}
}

规则解读


参数名描述备注
routeConfig路由配置配置路由到预请求的映射
prefetcher预请求配置记录所有的预请求
prefetchKey预请求的key
prefetchType预请求类型分为network类型与image类型,两种类型所需要的参数不同
prefetchInfo预请求所需要的信息其中value若为route.param格式,那么该值从路由中获取;若为route.param格式,那么该值从路由中获取;若为data.param格式,则从响应数据中获取。
paramsnetwork请求所需要的请求params
headersnetwork请求所需要的请求headers
prefetchImgFromResponse预请求的响应返回后,需要预加载的图片用于需要预加载图片时,无法确定图片url,图片url只能从预请求响应中获取的场景。

举例说明


网络预请求


例如跳转目标页面,它的路由是scheme://domain/path?param1=true&itemId=123


首先我们在跳转路由时,若跳转的路由是这个目标页面,我们就会尝试去发起预请求。根据上面的demo配置文件,它将匹配到prefetchKey这个预请求。


那么我们详细看prefetchKey这个预请求,预请求类型prefetchTypenetwork,是一个网络预请求,prefetchInfo中具备了请求的基本参数(如apiName、apiVersion、method、请求params与请求headers,不同工程不一样,大家可以根据自己的工程项目进行修改)。具体看params中,有一个参数为itemId:$route.itemId。以$route.开头的意思,就是这个value值要从路由中获取,即itemId=123,那么这个值就是123。


图片预请求


在做网络预请求的过程中,我忽然想到图片做预请求也是可以大大提升用户体验的,尤其是当大图片首次下载到内存中渲染需要的时间会比较长。图片预请求分为url已知url未知两种场景,下面各举两个例子。


图片url已知

什么是图片url已知呢?比如我们在首页跳转首页的二级页面时,如果二级页面需要预加载的图片跟首页的某张图是一样的(尺寸可能不同),那么首页跳转路由时我们是能够提前知道这个图片的url的,所以我们看到prefetchKey3中配置了prefetchTypeimage的预请求。image的信息来自于路由参数,需要在跳转时将图片url和宽高作为路由参数之一。


比如scheme://domain/path?imgUrl=${encodeUrl}&imgWidth=200,那么根据配置项,我们将提前将encodeUrl这个图片以宽200,高150的尺寸,加载到内存中去。当目标页面用到这个图片时,将能很快渲染出来。


图片url未知

相反,当跳转目标页面时,目标页面所要加载的图片url没法取到,就对应了图片url未知的场景。


例如闪屏页跳转首页时,如果需要预加载首页顶部的图片,此时闪屏页是无法获取到图片的url的,因为这个图片url是首页接口返回的。这种情况下,我们只能依赖首页的预请求进行。


在demo配置文件中,我们可以看到prefetchImgFromResponse字段。这个字段代表着,当这个预请求响应回来之后,我需要去预请求某张图片。其中,imgUrl$data.param格式,以$data.开头,代表着这份数据是来自于响应数据的。响应数据就是一串json串,可以凭此,索引到预请求响应中图片url的位置,就能实现图片的提前加载了。


至于图片怎么提前加载到内存中,以及真实图片的加载怎么匹配到内存中的图片,这一部分是通过glide已有的preload机制实现的,感兴趣的同学可以去看一下源码了解一下,这里就不展开了。后面讲的预请求的方案细节,都只限于网络请求。


预请求匹配


预请求匹配指的是实际的业务请求怎样与已经执行的预请求匹配上,从而节省请求的空中时间,直接返回预请求的结果。


首先网络预请求执行前先在内存中生成一份PrefetchRecord,代表着已经执行的预请求,其中的字段跟配置文件中差不多,主要就是记录预请求相关的信息:


class PrefetchRecord {
// 请求信息
String api;
String apiVersion;
String method;
String needLogin;
String showLoginUI;
JSONObject params;
JSONObject headers;

// 预请求状态
int status;
// 预请求结果
ResponseModel response;
// 生成的请求id
String requestId;

boolean isMatch(RealRequest realRequest) {
requestId.equals(realRequest.requestId)
}
}

每一个PrefetchRecord生成时,都会生成一个requestId,用于跟实际业务请求进行匹配。requestId的生成规则可以自行制定,比如将所有请求信息包一起做一下md5处理之类。


在实际业务请求发起之前,也会根据同样的规则生成requestId。若内存中存在相同requestId对应的PrefetchRecord,那么就相当于匹配成功了。匹配成功后,再根据预请求的状态进行进一步的处理。


预请求状态


预请求状态分为START、FINISH、ABORT,对应“正在发起预请求”、“已经获得预请求结果”、“预请求被抛弃”。ABORT状态下一节再讲。


为什么要记录这个状态呢?因为我们无法保证,预请求的响应一定在实际请求之前。用图来表示:


image.png


因为预请求是一个并发行为。当预请求的空中时间特别长,长到目标页面已经发出实际请求了,预请求的响应还没回来,即预请求状态为START,而非FINISH。那么此时该怎么办?我们就需要让实际请求在一旁等着(记录到内存中,RealRequestRecord),等预请求接收到响应了,再根据requestId去进行匹配,匹配到RealRequestRecord了,就触发RealRequestRecord中的回调,返回数据。


另外,在匹配过程中需要注意一点,因为每次路由跳转,如果发起预请求了,总会生成一个Record在内存中等待匹配。因此在匹配结束后,不管是匹配成功还是匹配失败,都要及时释放将Record从内存中释放掉。


超时重试机制


基于实际请求等待预请求响应的场景,我们再延伸一下。若预请求请求超时,迟迟拿不到响应,该怎么办?用图表示:


image.png


假设目前的网络请求,端上默认的超时时间是30s。那么在超时场景下,实际的业务请求在30s内若拿不到预请求的结果,就需要重新发起业务请求,抛弃预请求,并将预请求的状态置为ABORT,这样即使后面预请求响应回来了也不做任何处理。


image.png


忽然想到一个很贴切的场景来比喻这个预请求方案。


我们把跳转页面理解为去柜台取餐。


预请求代表着我们人还没到柜台,就先远程下单让柜员去准备食物。


如果柜员准备得比较快,那么我们到柜台后就能直接把食物拿走了,就能快点吃上了(代表着页面渲染速度变快)。


如果柜员准备得比较慢,那么我们到柜台后还是得等一会儿才能取餐,但总体上吃上食物的速度还是要比到柜台后再点餐来得快。


但如果这个柜员消极怠工准备得太慢了,我们到柜台等了很久都没拿到食物,那么我们就只能换个柜员重新点了(超时后发起实际的业务请求),同时还不忘投诉一把(预请求空中时间太慢了)。


总结


通过这篇文章,我们知道了什么是接口预请求,怎么实现接口预请求。我们通过配置文件+统一路由处理+预请求发起、匹配、回调,实现了与业务解耦的,可适用于任意页面的轻量级预请求方案,从而提升页面的渲染速度。


作者:孝之请回答
来源:juejin.cn/post/7203615594390732855
收起阅读 »

写一个可以当镜子照的 Button

web
最近写了一个好玩的 Button,它除了是一个 Button 外,还可以当镜子照。 那这个好玩的 Button 是怎么实现的呢? 很容易想到是用到了摄像头。 没错,这里要使用浏览器的获取媒体设备的 api 来拿到摄像头的视频流,设置到 video 上,然后对...
继续阅读 »

最近写了一个好玩的 Button,它除了是一个 Button 外,还可以当镜子照。



那这个好玩的 Button 是怎么实现的呢?


很容易想到是用到了摄像头。


没错,这里要使用浏览器的获取媒体设备的 api 来拿到摄像头的视频流,设置到 video 上,然后对 video 做下镜像反转,加点模糊就好了。


button 的部分倒是很容易,主要是阴影稍微麻烦点。


把 video 作为 button 的子元素,加个 overflow:hidden 就完成了上面的效果。


思路很容易,那我们就来实现下吧。


获取摄像头用的是 navigator.mediaDevices.getUserMedia 的 api。


在 MDN 中可以看到 mediaDevices 的介绍:



可以用来获取摄像头、麦克风、屏幕等。


它有这些 api:



getDisplayMedia 可以用来录制屏幕,截图。


getUserMedia 可以获取摄像头、麦克风的输入。



我们这里用到的是 getUserMedia 的 api。


它要指定音频和视频的参数,开启、关闭、分辨率、前后摄像头啥的:



这里我们把 video 开启,把 audio 关闭。


也就是这样:


navigator.mediaDevices.getUserMedia({
video: true,
audio: false,
})
.then((stream) => {
//...
}).catch(e => {
console.log(e)
})

然后把获取到的 stream 用一个 video 来展示:


navigator.mediaDevices.getUserMedia({
video: true,
audio: false,
})
.then((stream) => {
const video = document.getElementById('video');
video.srcObject = stream;
video.onloadedmetadata = () => {
video.play();
};
})
.catch((e) => console.log(e));

就是这样的:



通过 css 的 filter 来加点感觉:


比如加点 blur:


video {
filter: blur(10px);
}


加点饱和度:


video {
filter: saturate(5)
}



或者加点亮度:


video: {
filter: brightness(3);
}


filter 可以组合,调整调整达到这样的效果就可以了:


video {
filter: blur(2px) saturate(0.6) brightness(1.1);
}


然后调整下大小:


video {
width: 300px;
height: 100px;
filter: blur(2px) saturate(0.6) brightness(1.1);
}


你会发现视频的画面没有达到设置的宽高。


这时候通过 object-fit 的样式来设置:


video {
width: 300px;
height: 100px;
object-fit: cover;
filter: blur(2px) saturate(0.6) brightness(1.1);
}

cover 是充满容器,也就是这样:



但画面显示的位置不大对,看不到脸。我想显示往下一点的画面怎么办呢?


可以通过 object-position 来设置:


video {
width: 300px;
height: 100px;
object-fit: cover;
filter: blur(2px) saturate(0.6) brightness(1.1);
object-position: 0 -100px;
}

y 向下移动 100 px ,也就是这样的:



现在画面显示的位置就对了。


其实现在还有一个特别隐蔽的问题,不知道大家发现没,就是方向是错的。照镜子的时候应该左右翻转才对。


所以加一个 scaleX(-1),这样就可以绕 x 周反转了。


video {
width: 300px;
height: 100px;
object-fit: cover;
filter: blur(2px) saturate(0.6) brightness(1.1);
object-position: 0 -100px;
transform: scaleX(-1);
}


这样就是镜面反射的感觉了。


然后再就是 button 部分,这个我们倒是经常写:


function Button({ children }) {
const [buttonPressed, setButtonPressed] = useState(false);

return (
<div
className={`button-wrap ${buttonPressed ? "pressed" : null}`}
>

<div
className={`button ${buttonPressed ? "pressed" : null}`}
onPointerDown={() =>
setButtonPressed(true)}
onPointerUp={() => setButtonPressed(false)}
>
<video/>
</div>
<div className="text">{children}</div>
</div>

);
}

这里我用 jsx 写的,点击的时候修改 pressed 状态,设置不同的 class。


样式部分是这样的:


:root {
--transition: 0.1s;
--border-radius: 56px;
}

.button-wrap {
width: 300px;
height: 100px;
position: relative;
transition: transform var(--transition), box-shadow var(--transition);
}

.button-wrap.pressed {
transform: translateZ(0) scale(0.95);
}

.button {
width: 100%;
height: 100%;
border: 1px solid #fff;
overflow: hidden;
border-radius: var(--border-radius);
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.25), 0px 8px 16px rgba(0, 0, 0, 0.15),
0px 16px 32px rgba(0, 0, 0, 0.125);
transform: translateZ(0);
cursor: pointer;
}

.button.pressed {
box-shadow: 0px -1px 1px rgba(0, 0, 0, 0.5), 0px 1px 1px rgba(0, 0, 0, 0.5);
}

.text {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
color: rgba(0, 0, 0, 0.7);
font-size: 48px;
font-weight: 500;
text-shadow:0px -1px 0px rgba(255, 255, 255, 0.5),0px 1px 0px rgba(255, 255, 255, 0.5);
}

这种 button 大家写的很多了,也就不用过多解释。


要注意的是 text 和 video 都是绝对定位来做的居中。


再就是阴影的设置。


阴影的 4 个值是 x、y、扩散半径、颜色。


我设置了个多重阴影:




然后再改成不同透明度的黑就可以了:



再就是按下时的阴影,设置了上下位置的 1px 黑色阴影:


.button.pressed {
box-shadow: 0px -1px 1px rgba(0, 0, 0, 0.5), 0px 1px 1px rgba(0, 0, 0, 0.5);
}

同时,按下时还有个 scale 的设置:



再就是文字的阴影,也是上下都设置了 1px 阴影,达到环绕的效果:


text-shadow:0px -1px 0px rgba(255, 255, 255, 0.5),0px 1px 0px rgba(255, 255, 255, 0.5);


最后,把这个 video 嵌进去就行了。


完整代码如下:


import React, { useState, useEffect, useRef } from "react";
import "./button.css";

function Button({ children }) {
const reflectionRef = useRef(null);
const [buttonPressed, setButtonPressed] = useState(false);

useEffect(() => {
if (!reflectionRef.current) return;
navigator.mediaDevices.getUserMedia({
video: true,
audio: false,
})
.then((stream) => {
const video = reflectionRef.current;
video.srcObject = stream;
video.onloadedmetadata = () => {
video.play();
};
})
.catch((e) => console.log(e));
}, [reflectionRef]);

return (
<div
className={`button-wrap ${buttonPressed ? "pressed" : null}`}
>

<div
className={`button ${buttonPressed ? "pressed" : null}`}
onPointerDown={() =>
setButtonPressed(true)}
onPointerUp={() => setButtonPressed(false)}
>
<video
className="button-reflection"
ref={reflectionRef}
/>

</div>
<div className="text">{children}</div>
</div>

);
}

export default Button;

body {
padding: 200px;
}
:root {
--transition: 0.1s;
--border-radius: 56px;
}

.button-wrap {
width: 300px;
height: 100px;
position: relative;
transition: transform var(--transition), box-shadow var(--transition);
}

.button-wrap.pressed {
transform: translateZ(0) scale(0.95);
}

.button {
width: 100%;
height: 100%;
border: 1px solid #fff;
overflow: hidden;
border-radius: var(--border-radius);
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.25), 0px 8px 16px rgba(0, 0, 0, 0.15),
0px 16px 32px rgba(0, 0, 0, 0.125);
transform: translateZ(0);
cursor: pointer;
}

.button.pressed {
box-shadow: 0px -1px 1px rgba(0, 0, 0, 0.5), 0px 1px 1px rgba(0, 0, 0, 0.5);
}

.text {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
color: rgba(0, 0, 0, 0.7);
font-size: 48px;
font-weight: 500;
text-shadow:0px -1px 0px rgba(255, 255, 255, 0.5),0px 1px 0px rgba(255, 255, 255, 0.5);
}

.text::selection {
background-color: transparent;
}

.button .button-reflection {
width: 100%;
height: 100%;
transform: scaleX(-1);
object-fit: cover;
opacity: 0.7;
filter: blur(2px) saturate(0.6) brightness(1.1);
object-position: 0 -100px;
}

总结


浏览器提供了 media devices 的 api,可以获取摄像头、屏幕、麦克风等的输入。


除了常规的用途外,还可以用来做一些好玩的事情,比如今天这个的可以照镜子的 button。


它看起来就像我上厕所时看到的这个东西一样😂:



作者:zxg_神说要有光
来源:juejin.cn/post/7206249542752567333
收起阅读 »

测试妹子提了个bug,为什么你多了个options请求?

web
测试妹子给我提了个bug,说为什么一次操作,network里面两个请求。 我脸色一变”不可能,我写的代码明明是一次操作,怎么可能两个请求“。走过去一看,原来是多了个options请求。 ”这个你不用管,这个是浏览器默认发送的一个预检请求“。可是妹子很执着”这可...
继续阅读 »

测试妹子给我提了个bug,说为什么一次操作,network里面两个请求。


我脸色一变”不可能,我写的代码明明是一次操作,怎么可能两个请求“。走过去一看,原来是多了个options请求。


”这个你不用管,这个是浏览器默认发送的一个预检请求“。可是妹子很执着”这可肯定不行啊,明明是一次请求,干嘛要两次呢?“。


”哟呵,挺固执啊,那我就给你讲个明白,到时候你可别说听不懂“。


HTTP的请求分为两种简单请求非简单请求


简单请求


简单请求要满足两个条件:



  1. 请求方法为:HEADGETPOST

  2. header中只能包含以下请求头字段:

    • Accept

    • Accept-Language

    • Content-Language

    • Content-Type: 所指的媒体类型值仅仅限于下列三者之一

      • text/plain

      • multipart/form-data

      • application/x-www-form-urlencoded






浏览器的不同处理方式


对于简单请求来说,如果请求跨域,那么浏览器会放行让请求发出。浏览器会发出cors请求,并携带origin。此时不管服务端返回的是什么,浏览器都会把返回拦截,并检查返回的responseheader中有没有Access-Control-Allow-Origin是否为true,说明资源是共享的,可以拿到。如果没有这个头信息,说明服务端没有开启资源共享,浏览器会认为这次请求失败终止这次请求,并且报错。


非简单请求


只要不满足简单请求的条件,都认为是非简单请求。


发出非简单cors请求,浏览器会做一个http的查询请求(预检请求)也就是optionsoptions请求会按照简单请求来处理。那么为什么会做一次options请求呢?


检查服务器是否支持跨域请求,并且确认实际请求的安全性。预检请求的目的是为了保护客户端的安全,防止不受信任的网站利用用户的浏览器向其他网站发送恶意请求。
预检请求头中除了携带了origin字段还包含了两个特殊字段:



  • Access-Control-Request-Method: 告知服务器实际请求使用的HTTP方法

  • Access-Control-Request-Headers:告知服务器实际请求所携带的自定义首部字段。
    比如:


OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.other
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type

以上报文中就可以看到,使用了OPTIONS请求,浏览器根据上面的使用的请求参数来决定是否需要发送,这样服务器就可以回应是否可以接受用实际的请求参数来发送请求。Access-Control-Request-Method告知服务器,实际请求将使用 POST 方法。Access-Control-Request-Headers告知服务器,实际请求将携带两个自定义请求标头字段:X-PINGOTHER 与 Content-Type。服务器据此决定,该实际请求是否被允许。


什么时候会触发预检请求呢?



  1. 发送跨域请求时,请求头中包含了一些非简单请求的头信息,例如自定义头(custom header)等;

  2. 发送跨域请求时,使用了 PUT、DELETE、CONNECT、OPTIONS、TRACE、PATCH等请求方法。


我得意的说“讲完了,老妹你听懂了吗?”


妹子说“似懂非懂”


那行吧,带你看下实际场景。(借鉴文章CORS 简单请求+预检请求(彻底理解跨域)的两张图)


image.png


image.png


妹子说“这样就明了很多”,满是崇拜的关闭了Bug。


兄弟们,妹子都懂了,你懂了吗?😄


参考:


CORS 简单请求+预检请求(彻底理解跨域)


OPTIONS | MDN


跨源资源共享(CORS)| MDN


说明一下哈,以上事件是真实事件,只不过当时讲的时候没有那么的详细,😂


作者:翰玥
来源:juejin.cn/post/7206264862657445947
收起阅读 »

前端要对用户的电池负责!

web
前言 我有一个坏习惯:就是下班之后不关电脑,但是电脑一般来说第二天电量不会有什么损失。但是后来突然有一天它开不起来了,上午要罢工?那也不好吧,也不能在光天化日之下划水啊;聪明的我还是找到了原因:电池耗尽了;于是赶紧来查一查原因,到底是什么程序把电池耗尽了呢? ...
继续阅读 »

前言


我有一个坏习惯:就是下班之后不关电脑,但是电脑一般来说第二天电量不会有什么损失。但是后来突然有一天它开不起来了,上午要罢工?那也不好吧,也不能在光天化日之下划水啊;聪明的我还是找到了原因:电池耗尽了;于是赶紧来查一查原因,到底是什么程序把电池耗尽了呢?


这里我直接揭晓答案:是一个Web应用,看来这个锅必须要前端来背了;我们来梳理一下js中有哪些耗电的操作。


js中有哪些耗电的操作


js中有哪些耗电的操作?我们不如问问浏览器有哪些耗电的操作,浏览器在渲染一个页面的时候经历了GPU进程、渲染进程、网络进程,由此可见持续的GPU绘制、持续的网络请求和页面刷新都会导致持续耗电,那么对应到js中有哪些操作呢?


Ajax、Fetch等网络请求


单只是一个AjaxFetch请求不会消耗多少电量,但是如果有一个定时器一直轮询地向服务器发送请求,那么CPU一直持续运转,并且网络进程持续工作,它甚至有可能会阻止电脑进行休眠,就这样它会一直轮询到电脑电池不足而关机;
所以我们应该尽量少地去做轮询查询;


持续的动画


持续的动画会不断地触发GPU重新渲染,如果没有进行优化的话,甚至会导致主线程不断地进行重排重绘等操作,这样会加速电池的消耗,但是js动画与css动画又不相同,这里我们埋下伏笔,等到后文再讲;


定时器


持续的定时器也可能会唤醒CPU,从而导致电池消耗加速;


上面都是一些加速电池消耗的操作,其实大部分场景都是由于定时器导致的电池消耗,那么下面来看看怎么优化定时器的场景;


针对定时器的优化


先试一试定时器,当我们关闭屏幕时,看定时器回调是否还会执行呢?


let num = 0;
let timer = null;
function poll() {
clearTimeout(timer);
timer = setTimeout(()=>{
console.log("测试后台时是否打印",num++);
poll();
},1000*10)
}

结果如下图,即使暂时关闭屏幕,它仍然会不断地执行:


未命名.png


如果把定时器换成requestAnimationFrame呢?


let num = 0;
let lastCallTime = 0;
function poll() {
requestAnimationFrame(() =>{
const now = Date.now();
if(now - lastCallTime > 1000*10){
console.log("测试raf后台时是否打印",num++);
lastCallTime = now;
}
poll();
});
}

屏幕关闭之前打印到了1,屏幕唤醒之后才开始打印2,真神奇!


未命名.png


当屏幕关闭时回调执行停止了,而且当唤醒屏幕时又继续执行。屏幕关闭时我们不断地去轮询请求,刷新页面,执行动画,有什么意义呢?因此才出现了requestAnimationFrame这个API,浏览器对它进行了优化,用它来代替定时器能减少很多不必要的能耗;


requestAnimationFrame的好处还不止这一点:它还能节省CPU,只要当页面处于未激活状态,它就会停止执行,包括屏幕关闭、标签页切换;对于防抖节流函数,由于频繁触发的回调即使执行次数再多,它的结果在一帧的时间内也只会更新一次,因此不如在一帧的时间内只触发一次;它还能优化DOM更新,将多次的重排放在一次完成,提高DOM更新的性能;


但是如果浏览器不支持,我们必须用定时器,那该怎么办呢?


这个时候可以监听页面是否被隐藏,如果隐藏了那么清除定时器,如果重新展示出来再创建定时器:


let num = 0;
let timer = null;
function poll() {
clearTimeout(timer);
timer = setTimeout(()=>{
console.log("测试后台时是否打印",num++);
poll();
},1000*10)
}

document.addEventListener('visibilitychange',()=>{
if(document.visibilityState==="visible"){
console.log("visible");
poll();
} else {
clearTimeout(timer);
}
})

针对动画优化


首先动画有js动画、css动画,它们有什么区别呢?


打开《我的世界》这款游戏官网,可以看到有一个keyframes定义的动画:


未命名.png


切换到其他标签页再切回来,这个扫描二维码的线会突然跳转;


因此可以得出一个结论:css动画在屏幕隐藏时仍然会执行,但是这一点我们不好控制。


js动画又分为三种种:canvas动画、SVG动画、使用js直接操作css的动画,我们今天不讨论SVG动画,先来看一看canvas动画(MuMu官网):


未命名.png


canvas动画在页面切换之后再切回来能够完美衔接,看起来动画在页面隐藏时也并没有执行;


那么js直接操作css的动画呢?动画还是按照原来的方式一直执行,例如大话西游这个官网的”获奖公示“;针对这种情况我们可以将动画放在requestAnimationFrame中执行,这样就能在用户离开屏幕时停止动画执行


上面我们对大部分情况已经进行优化,那么其他情况我们没办法考虑周到,所以可以考虑判断当前用户电池电量来兼容;


Battery Status API兜底


浏览器给我们提供了获取电池电量的API,我们可以用上去,先看看怎么用这个API:


调用navigator.getBattery方法,该方法返回一个promise,在这个promise中返回了一个电池对象,我们可以监听电池剩余量、电池是否在充电;


navigator.getBattery().then((battery) => {
function updateAllBatteryInfo() {
updateChargeInfo();
updateLevelInfo();
updateChargingInfo();
updateDischargingInfo();
}
updateAllBatteryInfo();

battery.addEventListener("chargingchange", () => {
updateChargeInfo();
});
function updateChargeInfo() {
console.log(`Battery charging? ${battery.charging ? "Yes" : "No"}`);
}

battery.addEventListener("levelchange", () => {
updateLevelInfo();
});
function updateLevelInfo() {
console.log(`Battery level: ${battery.level * 100}%`);
}

battery.addEventListener("chargingtimechange", () => {
updateChargingInfo();
});
function updateChargingInfo() {
console.log(`Battery charging time: ${battery.chargingTime} seconds`);
}

battery.addEventListener("dischargingtimechange", () => {
updateDischargingInfo();
});
function updateDischargingInfo() {
console.log(`Battery discharging time: ${battery.dischargingTime} seconds`);
}
});


当电池处于充电状态,那么我们就什么也不做;当电池不在充电状态并且电池电量已经到达一个危险的值得时候我们需要暂时取消我们的轮询,等到电池开始充电我们再恢复操作


后记


电池如果经常放电到0,这会影响电池的使用寿命,我就是亲身经历者;由于Web应用的轮询,又没有充电,于是一晚上就耗完了所有的电,等到再次使用时电池使用时间下降了好多,真是一次痛心的体验;于是后来我每次下班前将Chrome关闭,并关闭所有聊天软件,但是这样做很不方便,而且很有可能遗忘;


如果每一个前端都能关注到自己的Web程序对于用户电池的影响,然后尝试从定时器和动画这两个方面去优化自己的代码,那么我想应该就不会发生这种事情了。注意:桌面端程序也有可能是披着羊皮的狼,就是披着原生程序的Web应用;


参考:
MDN


作者:蚂小蚁
来源:juejin.cn/post/7206331674296746043
收起阅读 »

37岁程序员被裁,120天没找到工作,无奈去小公司,结果懵了...

从短期来看,程序员的确算是个不错的工作,薪水也比一般岗位高很多,但是从长远来看,程序员的中年危机会比其他岗位来的更早,很多程序员只有到了35岁左右,才能真正认清楚互联网行业,尤其是被裁之后,找工作无门的这种无奈。最近在职场论坛上看到这样一个帖子:37岁程序员被...
继续阅读 »


从短期来看,程序员的确算是个不错的工作,薪水也比一般岗位高很多,但是从长远来看,程序员的中年危机会比其他岗位来的更早,很多程序员只有到了35岁左右,才能真正认清楚互联网行业,尤其是被裁之后,找工作无门的这种无奈。

最近在职场论坛上看到这样一个帖子:37岁程序员被裁,120天没找到工作,面试华为阿里均被拒,无奈降薪去小公司后懵了!

这是怎么回事呢?


据这位程序员说自己写代码15年了,目前在二线互联网公司当个小主管,可是在最近被公司优化裁掉了。

由于自己已经37岁,找了4个月工作都没有着落,无奈降薪去小公司,可是小公司的面试官却还说我技术太落后!现在上有老下有少,自从被裁后每天都睡不着,很难熬!

那么,网友是怎么说的?


有网友说你可能没认清自己吧,什么叫做小公司还挑挑拣拣你呢?自己能力不够,就抱怨找不到工作,为什么当初不把技术学精呢!有网友对此进行反驳,你也会有35岁那天,程序员的今天就是你的明天,何必要嘲讽呢?


还有网友现在华为,阿里等大公司,只要超过35岁了,连面试机会都不会给你,简历那一关就被卡住了。去年我还说华为加班太多不想去,可现在想去都没机会了。


还有网友说那就没办法了,只能选择降薪,毕竟一家人总要生活,工资低点就低点,总比没工作要强。其实这也是很多程序员收入虽然高,却不敢在大城市买房的真正原因。要是等到30多岁被裁了,到时候每天还房贷的压力太大,根本不敢想象没有工作的情况。


你怎么看呢?欢迎转发分享吐槽!

来源:互联网架构师

收起阅读 »

写给想转行学IT的朋友们的话

最近有很多朋友咨询我关于想转行学IT的问题,我想结合自身的经历和思考和各位聊几句,本文带有比较强的主观色彩,因此文中观点仅供参考,如有不当之处,敬请海涵。 笔者19年本科毕业于一所中流211,毕业之后一直在做Java开发,我并没有进大厂,也没有年薪百万,就是芸...
继续阅读 »

最近有很多朋友咨询我关于想转行学IT的问题,我想结合自身的经历和思考和各位聊几句,本文带有比较强的主观色彩,因此文中观点仅供参考,如有不当之处,敬请海涵。


笔者19年本科毕业于一所中流211,毕业之后一直在做Java开发,我并没有进大厂,也没有年薪百万,就是芸芸众生中普通的那一个。现在回顾当时入行的经历,基本是摸着石头过河,因此在有了一些经验之后,就想给和我当时一样处境的朋友一点建议,希望能对你有所启发。


很多问题其实都要结合个人的实际情况来看,每个人的知识、能力、经历都各不相同,所以无论做出任何决定,都需要结合自身的情况。


是否有必要参加培训机构?


这个问题的答案是因人而异的,核心点在于你是否有足够的恒心和自制力。


现在互联网上的学习资料已经非常多了,足够支撑你从零学到能够入行的过程。如果你的自制力比较好,那么你就可以尝试自学,不过自学的过程是孤独的,也是难熬的;如果你的自制力一般,无法在不是“学校”那么环境下进行学习,那就可以考虑培训机构。


需要特别强调的是,不能以为进了培训机构就以为一定能够就业了,说到底,学习这件事情,还是要靠你自己,别人是没有办法把知识灌进你的脑袋里的。培训机构也有一些无法忽视的问题,当你毕业之后,大概率机构会给你伪造一份简历,本来学习的时间就并不是很长,你很快可能就会发现,培训机构里面学的比较浅,还不足以应付面试官的八股文,这时候你可能就会想先入行再说,然后进了一家外包公司,然后开始混日子,这也是笔者见过的最多的案例。


参加培训最大的还是简历,培训机构给大家做的项目都是极其雷同的,也可能会在培训结构之后,让你自己编项目经验,有经验的面试官很容易就能判断出,你是真的参与了项目开发,还是只是包装的。另外你可能还会碰到,很多公司需要上一家公司的离职证明和银行流水,甚至需要你个税APP报税界面的录屏等等问题,这些培训机构并不会告诉你。


总而言之,转行是一件对你的人生来说,是一件极其重要的事情,要反复思虑,不要脑子一热,听身边的朋友说,那个谁谁谁培训了几个月就月入过万,然后就稀里糊涂花了几万参加了培训机构,结果培训结束真正找工作的时候又发现困难重重,现实往往比想象的残酷。


高中毕业可以从事IT行业吗?


真相是,如果要从事IT行业,大专及以上学历是基本要求,不排除个人大专以下学历也找到了很好的IT工作,但是在当下这么“卷”的环境下,就是幸存者偏差了。


大专的学历我也非常建议你先提升学历,有一个本科学历。


前端、后端还是测试?


其实从某种意义来说,选择岗位就是选择某种编程语言,选择编程语言也是在选择岗位,这个问题,你也可以结合下文编程语言之间的对比,找到自己合适的岗位。大体而言,测试、前端、后端,这三者的难度是依次递增的(对大部分人而言)。


如果你没有科班背景,但是又想从事IT行业,那么你可以考虑从事测试、运维工作甚至产品、运营的工作,其实开发写代码并不是唯一的选择。


如果你有一定的基础,但是逻辑思维又不是那么强,那么我建议你可以考虑前端,女生大部分学习编程都会选择前端,前端由于不需要考虑架构、性能(大部分场景),因此难度会小一些,对初学者更加友好。不过,前端经过这些年的发展,知识体系俨然已经非常庞大,后期也需要学习很多的东西。


如果你有一定的基础,且逻辑思维还可以,那么我建议你可以考虑后端,如果你不知道自己的思维能力怎么样,可以学一些Java或者JavaScript语言基础,看看自己是不是能够比较轻松的学会。


Java、Python、C++还是JavaScript?


JavaScript


作为一门十天就被开发出来的编程语言,JavaScript身上的历史包袱也有很多,但这并不妨碍它的伟大,JavaScript在我们的日常生活中,几乎无处不在。


JavaScript也是一门易学难精的语言,虽然上手比较容易,但是后期也需要很多的时间去理解和巩固。JavaScript是前端程序员最重要的技术,学好了JavaScript,就能在前端这个领域里无所不能。JavaScript现在也开始逐渐的在其他领域崭露头脚,它与TypeScript相辅相成,可以预见,JavaScript是一门历久弥新,并且前景良好的编程语言。


Java


如果你已经决定了要从事软件开发行业,但是还不清楚,要选择哪一门编程语言,那我推荐你可以学习Java,Java这门语言本身虽然并不优秀,截止目前,Java已经走过了20个年头,有着非常丰富的生态,web端,它有JSP、Servlet,移动端,它有Android,服务器端有SpringBoot,桌面端它有JavaFX,也有非常优秀的网络通信框架,比如Netty,甚至它也做出过操作系统(塞班系统),可以说,Java虽然很烂,但是它真的几乎无所不能。


不可否认Java的成功,但是Java也存在一些隐患,比如在Oracle收购了Sun公司之后,在Oracle不断地花式作妖下,Java的未来似乎也有些扑朔迷离。除此之外,Java在引以为傲的领域也逐渐有了一些竞争对手,比如服务端有go语言、nodejs,Android的开发官方首选语言已经变成kotlin,JSP的市场基本上已经被Vue、React等SPA框架所替代,但是Java却并没有开拓出自己新的应用场景,虽然笔者认为,基于VM的语言并不是消失,但是Java是能否一直守住自己的王座,还是要打一个问号的。


Java目前最多的就业方向就是服务端开发,如果你学习了Java,那么大概率会做服务端开发,短期内,服务端开发Java还是很难被其他语言替代。强类型的语言加上对并发编程的支持,让Java非常适合构建大型的服务端应用,这也是Java最深耕的方向,学会Java服务端的开发,也比较方便向大数据或数据分析岗位进行转型。


最后我想说,Java的岗位在我提到的编程语言里面也是最多的,这也是在选择编程语言很重要的一个需要考虑的点,很多编程语言,看起来很热门但是,真正学完去找工作的时候,发现岗位少的可怜,没错,说的就是你,Python。


Python


Python这门编程语言,“胶水语言”的特性让它看起来也几乎无所不能,虽然这种能力很有可能来自其他的语言,Python只是作为客户端,调用其他语言的类库。


不够“底层”其实并不是什么缺点,学习Python最大的问题是,是对学历有要求,Python应用最广泛的领域,大部分都对学历有要求,虽然你可能看过很多投放Python的广告,自动化办公、爬虫等等,但是这些特性在企业中的岗位是比较少的,而Python擅长的算法、人工智能、深度学习等领域都是需要研究生学历的。


我们总结一下,如果你有研究生学历,那么学习Python是一个不错的选择,不过我也见过有的研究生朋友学完Python之后发现岗位很少,又被迫转Java的案例。


C++


C++相比于其他的编程语言,其入门的门槛的会更高,花费在学习语言本身的时间的也会很多。C++在经历过这么多年的迭代之后,语言的特性非常多,会让初学者有一种眼花缭乱的特性,实现同一个功能,可能会有很多种写法。不可否认,C++是一门优秀的编程语言,但是高昂的学习成本也让人望而却步,如果你不是科班出身,只是想学一门技术进入互联网行业,那C++并不是一个很好的选择。


C++常见的就业方向有以下几种。



  • 服务器端开发

  • 游戏

  • QT

  • 嵌入式

  • 人工智能


总结来说,C++最适合的还是那些对性能有要求的场景。原来很大一部分C++程序员都是做QT开发,但是C++在桌面端的市场已经出现了越来越多的竞争者,比如Hybrid 技术、React Native、Weex、Flutter,这些技术让原来web端的程序员或者移动端的程序员也能写出跨平台的应用,并且这些技术保留了他们原本的开发习惯,这对QT的打击,无疑是毁灭性的。


那么C++是不是已经过时,或者即将被淘汰呢?其实不然,C++只是让出了一些自己不是那么擅长的领域,但是在底层应用的开发,C++丰富的生态和优越的性能还是首选。如果你有志在这些领域发展,那么C++将是你很好的选择。


随着越来越多的人对核心自研技术的重视,这种比较偏底层的岗位,会越来越多,最近几年,国产操作系统的发展就是一个很好的例子,在政策的扶持下,各家公司几乎都在号称自研操作系统,可以预料,在不远的将来,这些核心的系统的研发,都需要大量C/C++语言的人才。


路漫漫其修远兮,吾将上下而求索


我非常建议你在决定入行之前,提前找一些学习资料,自己尝试一下是否能够学的清楚,并且能从中获取乐趣。


兴趣和恒心是决定你能不能在这个行业长足发展的决定性因素。面对海量的,对你而言是闻所未闻的知识,如果没有兴趣,你每天都会深受折磨;这个行业特质决定了我们需要不断地学习,没有恒心,总有一天会掉队,跟不上技术更新迭代的脚步。


总而言之,道阻且长,希望本文能对你有所启发,与君共勉。


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

浅谈敏捷开发与互联网江湖的爱恨情仇!

敏捷开发 通常互联网产品都采用一套称之为 敏捷开发 的研发流程 @需求阶段 定方向:企划部门和产品部门一道确定产品方向; 出需求:产品经理提供: 需求文档(对产品体系和功能的详细描述,常用的格式为Word配合PPT) + 产品原型(一个能在PC/手机浏览器...
继续阅读 »

敏捷开发


通常互联网产品都采用一套称之为 敏捷开发 的研发流程


@需求阶段


image.png



  • 定方向:企划部门和产品部门一道确定产品方向;

  • 出需求:产品经理提供: 需求文档(对产品体系和功能的详细描述,常用的格式为Word配合PPT) + 产品原型(一个能在PC/手机浏览器上进行页面跳转和简单交互的demo);


image.png


@研发阶段



  • 研发:前后端开发和美术部门根据产品需求开始工作;

  • 设计师/美术:根据产品原型产出一整套页面设计图;


image.png



  • 服务端/后端:根据产品功能说明着手数据库设计和接口设计,产出接口文档,供前端调用以获取业务数据;

  • 客户端/前端:根据设计效果图开发PC/手机H5/小程序/App等用户端的界面 + 数据渲染 + 用户交互;

  • 接口联调:开发期间前后端开发者需要进行接口联调,以确保前端能通过后端提供的接口正确地获取到数据;


@测试阶段



  • 提交测试:前后端共同开发完毕后,打包成品供测试人员进行测试;

  • 产品测试:测试人员代入用户的角色,带着挑剔的眼光和看我不整死你的愉快心情对产品进行使用测试,并将宏伟的BUG列表这口黝黑锃亮的大锅戴在项目经理头上;


image.png



  • 修复BUG:项目经理在开发团队(前后端)内对锅进行二次分配,精确到具体责任人;

  • 反复完善:开发人员修复BUG,再次打包提交测试,测试再挑刺-开发再分锅修改-再提交测试,重复以上循环若干遍以后,测试表示产品凑合可以上线了,签字画押;

  • 产品上线:运维同学将产品部署到生产环境与用户见面;


程序员在产品上线时的样子↓
image.png


@维护阶段



  • 升级维护:产品上线以后还需要进行多个版本的迭代升级,包括新一轮的需求的开发,以及收集和修复用户反馈的产品BUG等,于是就继续产生2.0、3.0...


互联网江湖的爱恨情仇



  • 通常当开发进度完成到99%的时候,产品经理脑门一拍,哎呀这个地方可能得小改一下,很简单的你这样弄一下就行:XXOO@#$%^&*blablabla...,一想到打人要进橘子你默默地收起了磨过无数回的菜刀,开始彻底推翻重做...

  • 针对上述情况美术人员表示我早就习惯了;

  • 当然产品和研发的博弈为我们的茶余饭后提供了不少欢乐的素材


image.png


其实谁还不是一样...


image.png




“这不需要测试,肯定是好的,不必担心!”


image.png

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

离职后在家躺了一年,顺便写了本书

大家好,我卡颂。 从字节离职已经一年多了。 最开始离职的契机是电子社的编辑向我约书稿。以我的能力是没法同时兼顾字节的高强度工作以及在规定的时间内成书。 考虑到进厂上班的机会以后还有(但现在这行情,没准儿也没厂子要我也说不定),但写书的经历还没体验过,于是就辞职...
继续阅读 »

大家好,我卡颂。


从字节离职已经一年多了。


最开始离职的契机是电子社的编辑向我约书稿。以我的能力是没法同时兼顾字节的高强度工作以及在规定的时间内成书


考虑到进厂上班的机会以后还有(但现在这行情,没准儿也没厂子要我也说不定),但写书的经历还没体验过,于是就辞职了。


接下来聊聊程序员写书是种什么体验。


欢迎加入人类高质量前端框架研究群,带飞


为什么写书?


在大概5年前,写书还是一种行之有效的提升个人影响力的方式。


很多人在找工作前、创业前,都会写本书作为个人能力的背书


甚至还出现专门帮人写书的公司,这些公司的目标客户是那些由于某些契机突然出名的草根(比如一些现象级语言类综艺的选手)。


但最近几年,相比写书短视频塑造个人人设是更短平快的提升个人影响力途径。所以,写书的人就逐渐少了。


而且,写书真的不赚钱。卖一本书的版税收入可能就几块钱。相比于赚钱,一些急于通过写书提升个人影响力的人,甚至得反向给出版社钱,比如花钱买书号(1w多)。


所以,对于为什么写书,主要是想体验下这段经历,以及可能带来的些许个人影响力提升。


什么样的程序员能写书?


可能你会觉得写书是件很厉害的事情,那你觉得写一本掘金小册厉不厉害呢?


如果你觉得写一本掘金小册也挺厉害,那写某一技术领域的系列文章厉不厉害呢?


好像还行对吧,那深入学习某个技术领域厉不厉害呢?


是不是努努力就能达到?


实际上这是条很平滑的路径,就像打游戏升级一样,在很多关键节点都会有任务npc主动找你(比如小册运营,出版社编辑)。


所以,与其说写书很厉害,不如说写书的人很执着,把这条支线任务打通关了(虽然任务奖励并不丰厚)。


我写了本什么书?


书名叫React设计原理,基于React18,从理念、架构、实现三个层面解构React



虽然在此之前写过一本开源电子书react技术揭秘,但既然要出版,就好好对待吧。


于是,在规划这本书时,我主要从两方面下手:




  • 内容尽可能硬核




  • 怎么讲读者才听得懂?




先说第一点,为了理通react运行的方方面面,除了常规的阅读源码跟踪核心成员的各种进展外,为了搞懂React18的运行细节,我从0实现了一个React18,所有实现步骤可以在big-react项目中看到。


内容硬核很重要,但写出来能让人看懂同样很重要。为了达到这个目标,我主要参考了两个优秀的作品:



前者在行文过程中一直在提倡知识屏蔽(在教学过程中只关注当前学习的知识,屏蔽超纲知识对读者的干扰的原则),后者做到了理论与实践(丰富的在线示例)结合。


所以,我这本书存在两条脉络:




  • 抽象层级逐渐降低




  • 实现越来越复杂的模块




对于前者,本书的抽象层级会逐渐从理念到架构,最后到实现,每一层都屏蔽前一层的影响。


这也是为什么ReactDOM.createRoot这个初始化API会放到第六章再讲解 —— 在这个具体API的背后,是他的理念与架构。


对于后者,本书会从0实现与react相关的6个模块,最后我们会一起在React源码内实现一个新的原生Hook


一点感悟


这一年时间有很多朋友问我为什么不去找个班上。答案很简单 —— 因为我不喜欢上班。


所以,为了做喜欢的事,就得提前准备。就拿写书这件事来说,就像做游戏任务,他的每一步都是有迹可循的。


如果你也对现状不满,那就行动起来吧。为了想要的生活而探索,这个过程本身就充满了乐趣。


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

考研失败,加入国企当程序员,真香!

下面是正文。 最近考研出成绩了,大家考得怎么样?分享一个自己考研失败后,入职国企的故事。 1、考研失败 我是工作了3年后才参加考研的。 老家成都,本科毕业于帝都某所以能歌善舞著称的985学校,哲学专业。 大学选专业时家里人不懂,自己全凭爱好,第一志愿就是哲学...
继续阅读 »

下面是正文。




最近考研出成绩了,大家考得怎么样?分享一个自己考研失败后,入职国企的故事。


1、考研失败


我是工作了3年后才参加考研的。


老家成都,本科毕业于帝都某所以能歌善舞著称的985学校,哲学专业。


大学选专业时家里人不懂,自己全凭爱好,第一志愿就是哲学。上学时有多快乐,毕业时就多难找工作。


侥幸自己选修过大数据课程,并且听说程序员工资还不错,通过校招加入了隔壁省的一家制造业当程序员。


公司管吃管住,工资1w出头,干了2年,感觉到了危机。自己羞于靠关系上位,但技术上,计算机知识太薄弱也发展有限,于是就想趁着还年轻,考个计算机硕士,提升一下自己。


在工作的第3年决定考研,边工作边考研,压力真是不小。让本来就是学渣的我,从研究生考场走出来就知道了最终的结果:没戏了。


成绩出来后,果然不出所料。


2、加入国企


知道初试成绩以后我难过了几天,认真思考了一下自己的未来发展:旁边的帝都我也回不去了,又不甘心留在这个小城市,于是决定回老家成都发展。


毕业后一直在私企,总是听说国企好、央企不错,所以我这次投简历也想试一下。正好看到一篇讲程序员国企的文章:值得程序员加入的173家国企汇总,网友:这下彻底躺平了,于是就按照文中的思路,找到了成都的一些所谓不错的国企投递试试。


成功加入后发现:真香!




  • 福利真 ** 好!和其它公司谈薪酬,别人都是能压多低压多低,来这家国企,竟然还说我工作年限够长,在我期望的基础上加了3k。六险二金更不用说,平时的各种生活保障也是非常到位。不夸张的说,从私企转到国企的我,有一种刘姥姥进大观园的感觉。




  • 真卷!**是谁告诉我国企适合养老的?**这比我以前在私企工作强度大多了好嘛?而且我第一次听说部门平均加班时长影响个人绩效这种规则。




  • 技术上不激进。可能对于程序员来说,不停的学习新技术是一种常态,但是在这家国企,基本都是传统老技术,我打开代码还看到了我们领导1997年写的头文件。当然,你也可以认为这是一种不好的事。这里的技术对我来说,够了。




  • 同事关系很融洽。我人生中第一次去按摩店找技师,是女同事带我去的,谁能信!不过必须说一句,成都的按摩店是真正规啊!技师可真有劲。




去年回来工作快1年了,我现在对于考研已经释怀了。听说我们部门今年招人开始硕士起步了,**有时候我还挺庆幸自己去年没考研成功的,**不然即使上了研究生,我这实力,也不一定能进入现在的单位了。


3、写在最后


现在回过头来看,有一个不同的体会:考研是一件好事,但如果本身不是沉迷于科研事业,而是更想赚钱的话,有好的工作机会也别错过。


另外,多关注有用的信息很重要,有时候别人的一番话,可能就需要自己经历几年的曲折才能总结出来。


大家有任何程序员求职问题,欢迎在评论区和我交流~


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

FlutterComponent最佳实践之动画的显和隐

Flutter中包含大量的动画组件和自定义动画方式,所以,在合适的场景下选择合适的动画实现方式就成了决定代码质量好坏的一个重要因素。 动画选择决策树 Flutter中的动画从广义上来讲可以分为两类,一类是基于绘制的动画(Drawing-based animat...
继续阅读 »

Flutter中包含大量的动画组件和自定义动画方式,所以,在合适的场景下选择合适的动画实现方式就成了决定代码质量好坏的一个重要因素。


动画选择决策树


Flutter中的动画从广义上来讲可以分为两类,一类是基于绘制的动画(Drawing-based animations),另一类是基于代码的动画(Code-based animations)。


下面这个决策树,是Flutter动画选择的总纲,这里梳理了不同的动画的作用场景和功能,我们来看下它具体的实现。
image.png
首先,我们需要区分是使用CustomPainter,或者是使用Lottie、Flare这种第三方库,这一类的动画很容易区分——如果你第一感觉,这个动画我做不了,那它大概率就是了。


接下来,就是区分是使用「显示动画」还是「隐式动画」。


简单的说,它们的区别如下:



  • 隐式动画:不用循环播放、不用随时中断、不用多个动画协同,它实现的是一种状态到另一种状态的改变

  • 显示动画:需要自己控制动画过程


最后,就是看现有组件是否满足需求,如果不行,那么就需要自定义相应的动画。


这就是整个动画决策树的执行过程。它们的开发难度,如下所示。
image.png
下面我们就具体来分析下不同的动画实现。本文首先介绍显示动画和隐式动画。


Implicit Animations——隐式动画


在Flutter中,很多常用组件都有其自带的隐式动画版本,例如下图所示的这些组件。
image.png
这些组件在Flutter中被称之为隐式动画Widget,下面以AnimatedContainer为例,来看下Implicit Animations的使用。



隐式动画有一个特点,那就是它们都是以「Animated」开头。



基本使用


AnimatedContainer的使用非常简单,甚至和普通的Container没有太大的区别,代码如下所示。


AnimatedContainer(
margin: EdgeInsets.only(top: 20),
width: size,
height: size,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(radius),
),
curve: Curves.easeIn,
duration: Duration(milliseconds: 300),
),

当通过setState函数改变AnimatedContainer中的属性时,AnimatedContainer会经过一段动画效果,然后再完成相应的改变。在隐式动画中,你依然可以定义Curve和Duration等参数,但是你无法控制动画,即动画的执行和结束,是由属性改变来驱动的。


使用场景


Implicit Animations可以非常方便的使Widget具有动画效果而不需要写很多额外的动画代码,结合FutureBuilder或者StreamBuilder,甚至不用写setState,下面这个例子就演示了如何将Implicit Animations和FutureBuilder结合起来使用,代码如下所示。


FutureBuilder(
future: future,
builder: (context, snapshot) {
var width = .0;
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
case ConnectionState.active:
width = .0;
break;
case ConnectionState.done:
width = 100.0;
break;
}
return AnimatedContainer(
width: width,
duration: Duration(seconds: 1),
curve: Curves.easeIn,
child: Image.asset('images/logo.png'),
);
},
),

通过FutureBuilder的各种状态回调,就可以设置不同的Widget,并在FutureBuilder完成并显示正常的Widget时,产生一个动画效果,而不是非常生硬的出现。


TweenAnimationBuilder


TweenAnimationBuilder是自定义隐式动画的方式,借助它,你可以给一个指定的Widget作用一个动画效果,一个简单的示例代码如下所示。


TweenAnimationBuilder(
tween: Tween<double>(begin: 0, end: 48),
onEnd: (){}
duration: Duration(seconds: 1),
builder: (BuildContext context, double size, Widget child) {
return IconButton(
iconSize: size,
color: Colors.blue,
icon: child,
);
},
child: Icon(Icons.aspect_ratio),
)

借助TweenAnimationBuilder,就可以将一个指定的Tween作用于builder中的Widget,builder中的第二个参数,就是Tween所指定的参数的类型,通过TweenAnimationBuilder,就可以在Widget参数变化的时候产生动画效果。



TweenAnimationBuilder的builder中如果有不变的Child Widget,可以放在TweenAnimationBuilder的child属性中,因为builder在产生动画时会重建,所有不变的Widget,都可以放在TweenAnimationBuilder的child中,再通过builder的第三个参数来传递这个Widget,以避免重建。



通常我们在开发中,会借助Transform来完成动画效果,在builder中,根据Tween返回的数值,使用不同的Transform来修改动画状态。



TweenAnimationBuilder中的begin,只在第一次使用,后面更新时,只看end的值,例如10-30,修改end为50,实际变化是30-50。如果不传begin,那么默认和end相等。



Explicit Animations——显示动画


与隐式动画不同,显示动画给了开发者对动画过程的完全掌控,开发者可以根据自己的需要来控制动画,Flutter中内置了很多显示动画,如下所示。
image.png



显示动画也有一个很明显的特点,那就是它们都以「Transition」结尾。



基本使用


以RotationTransition为例,下面来演示下如何使用Flutter中的显示动画。


显示动画是通过AnimationController来进行驱动的,所以,使用显示动画的第一步,就是需要创建AnimationController。有了AnimationController之后,就可以通过控制AnimationController的状态来控制动画的驱动过程,整个代码如下所示。


AnimationController controller;

@override
void initState() {
super.initState();
controller = AnimationController(vsync: this, duration: Duration(seconds: 2))..repeat();
}

@override
void dispose() {
controller.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
onTap: () {
if (controller.isAnimating) {
controller.stop();
} else {
controller.repeat();
}
},
child: RotationTransition(
turns: controller,
child: FlutterLogo(
size: 100,
),
),
),
);
}

与隐式动画相比,显式动画通过AnimationController来获取动画的行进状态和参数,从而让调用者能够控制动画的行进过程。



显式动画可以实现隐式动画的所有功能,但是比隐式动画多了管理动画生命周期的工作



当Flutter内置显示动画不能满足开发者的需求时,Flutter提供了AnimatedBuilder和AnimatedWidget来让开发者对显示动画进行自定义。


AnimatedWidget


前面提到的都是Flutter中使用动画的最基本方式,但实际上,Flutter提供了很多关于动画的封装组件,可以让开发者更加方便的使用动画,这就是AnimatedWidget。AnimatedWidget也有很多实现类,如图所示。
image.png
AnimatedWidget是实现自定义显示动画的另一种方式,它可以将一些动画的逻辑以Widget的形式封装起来,从而让build函数中的代码逻辑更加清晰,下面是AnimatedWidget的示例代码。


@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
AnimWidget(animation: controller),
Center(child: FlutterLogo(size: 100)),
],
);
}

class AnimWidget extends AnimatedWidget {
const AnimWidget({
Key? key,
required Animation<double> animation,
}) : super(key: key, listenable: animation);

@override
Widget build(BuildContext context) {
Animation<double> animation = listenable as Animation<double>;
return Container(
decoration: BoxDecoration(
gradient: RadialGradient(
colors: const [Colors.red, Colors.transparent],
stops: [0, animation.value],
),
),
);
}
}

那么这种方式和之前直接使用AnimationController和Tween有什么区别呢?细心的读者可能已经发现了,AnimatedWidget不需要自己去监听动画的回调,也不需要通过setState来刷新动画,这些操作,AnimatedWidget已经封装好了,这就是AnimatedWidget的作用。


AnimatedBuilder


AnimatedBuilder是一个特殊的AnimatedWidget,它可以直接指定一个动画作用于Widget上,而不需要重新创建一个自定义的AnimatedWidget,它可以帮助开发者处理动画的监听,当一个Widget Tree中有一些需要动画的Widget,也有一些不需要动画的Widget时,用AnimatedBuilder可以很方便的避免非动画Widget的重绘,所以说,AnimatedBuilder可以更加方便的给一个Widget增加动画效果。


AnimatedBuilder与其它的显示动画一样,也是通过AnimationController驱动的,借助AnimatedBuilder,开发者可以根据需要,自己创建Animation并控制它,下面的代码演示了如何通过控制RadialGradient的stop属性来控制RadialGradient的显示大小,从而形成动画效果,代码如下所示。


@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: controller,
builder: (context, widget) {
return Stack(
children: <Widget>[
Container(
decoration: BoxDecoration(
gradient: RadialGradient(
colors: [Colors.red, Colors.transparent],
stops: [0, controller.value],
),
),
),
Center(child: FlutterLogo(size: 100))
],
);
},
);
}

上面的代码演示了如何使用AnimatedBuilder,实际上非常简单,与使用内置的显示动画的过程基本一致。


在使用AnimatedBuilder的过程中,需要尽可能多的将需要动画的部分和不需要动画的部分区分开来,这样可以避免多余的重绘,从而提高动画性能,例如上面的代码,可以将FlutterLogo和Stack放置在最外层,这样只需要让RadialGradient产生动画就可以了,代码如下所示。


@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
AnimatedBuilder(
animation: controller,
builder: (context, widget) {
return Container(
decoration: BoxDecoration(
gradient: RadialGradient(
colors: [Colors.red, Colors.transparent],
stops: [0, controller.value],
),
),
);
},
),
Center(child: FlutterLogo(size: 100))
],
);
}

AnimatedBuilder接收了一个animation,在child中,可以直接使用这个animation的值,其它都和普通的AnimatedWidget类似。


实际上,AnimatedBuilder就是AnimatedWidget的子类,所以在本质上,这两种实现自定义显示动画的方式想相同的,开发者可以根据自己的喜好来选择相应的方式来创建自己的显示动画。



AnimateWidget负责组件的抽离,可以看出组件中杂糅了动画逻辑。而AnimatedBuilder恰好相反,它不在意组件是什么,只是将动画抽离达到复用简单。



Flutter中的显示动画和隐式动画,几乎可以解决大部分我们平时在开发中遇到的动画场景,借助动画选择决策树,我们可以对动画的选择了如指掌,剩下的工作,就是对动画进行拆解,分而治之。


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

Android:实现一个自定义View扫描框

扫码功能都用过吧,打开扫码功能后都会有类似封面图的效果。其实就是一个自定义View的遮罩,话不多说,今天这篇我们就来讲解如何实现一个扫面框动效。 首先,我们先分析下动效的组成,有利于待会拆分实现: 四周类似角标的白线 角标框住的浅白色背景 一条由上而下由快到...
继续阅读 »

扫码功能都用过吧,打开扫码功能后都会有类似封面图的效果。其实就是一个自定义View的遮罩,话不多说,今天这篇我们就来讲解如何实现一个扫面框动效。


首先,我们先分析下动效的组成,有利于待会拆分实现:



  1. 四周类似角标的白线

  2. 角标框住的浅白色背景

  3. 一条由上而下由快到慢移动的扫描线


一经分析,其实非常简单,整体效果就是由这几个简单的元素组成。接下来我们就创建一个ScanView继承自View来实现这个动效。(由于代码古老,这里使用Java)


public final class ScanView extends View {

private Paint paint, scanLinePaint,reactPaint;//三种画笔
private Rect frame;//整个区域

public ScanView(Context context) {
this(context, null);
}

public ScanView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public ScanView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initPaint()
}

private void initPaint() {
/*遮罩画笔*/
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(color);
paint.setAlpha(CURRENT_POINT_OPACITY);
paint.setStyle(Paint.Style.FILL);

/*边框线画笔*/
reactPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
reactPaint.setColor(reactColor);
reactPaint.setStyle(Paint.Style.FILL);

/*扫描线画笔*/
scanLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
scanLinePaint.setStyle(Paint.Style.FILL);
scanLinePaint.setDither(true);
scanLinePaint.setColor(scanLineColor);
}
}

三种画笔初始化完成接下来就是使用画笔在画布上绘制效果图了,重写onDraw()方法


public void onDraw(Canvas canvas) {
//绘制取景边框
drawFrameBounds(canvas, frame);
//绘制遮罩
drawMaskView(canvas, frame);
//绘制扫描线
drawScanLight(canvas, frame);
}

再来分析,边框的四个角其实拆开来看,就是两条线组成,或者说是两个填充的矩形框垂直相交组成,那么四个角就可以按照这个思路完成,遮罩其实就是一个矩形框。


//绘制四个角,注意是外线而不是内线
private void drawFrameBounds(Canvas canvas, Rect frame) {
// 左上角
canvas.drawRect(frame.left - corWidth, frame.top, frame.left, frame.top + corLength, reactPaint);
canvas.drawRect(frame.left - corWidth, frame.top - corWidth, frame.left + corLength, frame.top, reactPaint);
// 右上角
canvas.drawRect(frame.right, frame.top, frame.right + corWidth,frame.top + corLength, reactPaint);
canvas.drawRect(frame.right - corLength, frame.top - corWidth, frame.right + corWidth, frame.top, reactPaint);
// 左下角
canvas.drawRect(frame.left - corWidth, frame.bottom - corLength,frame.left, frame.bottom, reactPaint);
canvas.drawRect(frame.left - corWidth, frame.bottom, frame.left + corLength, frame.bottom + corWidth, reactPaint);
// 右下角
canvas.drawRect(frame.right, frame.bottom - corLength, frame.right + corWidth, frame.bottom, reactPaint);
canvas.drawRect(frame.right - corLength, frame.bottom, frame.right + corWidth, frame.bottom + corWidth, reactPaint);
}

//绘制遮罩
private void drawMaskView(Canvas canvas, Rect frame) {
canvas.drawRect(frame.left, frame.top, frame.right, frame.bottom, paint);
}

到此,我们还剩最后一个扫描线的动画效果,这条线其实就是一张图片,首先需要将图片以Bitmap形式绘制在扫描区域内,然后通过ValueAnimator来控制图片Y坐标点,这样就能达到图片上下移动的效果,至于由快到慢的效果是添加了插值器,这里使用内置的DecelerateInterpolator,同学们可以根据自己想要的效果自己搭配。


scan_light.png


if (valueAnimator == null) {
valueAnimator = ValueAnimator.ofInt(frame.top, frame.bottom-10);//图片Y坐标取值范围
valueAnimator.setDuration(3000);//单次动画时间3秒
valueAnimator.setInterpolator(new DecelerateInterpolator());//由快到慢插值器
valueAnimator.setRepeatMode(ValueAnimator.RESTART);//重复动画
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);//无限次数
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
scanLineTop = (int) animation.getAnimatedValue();//当前时刻获取的Y值
invalidate();//刷新视图
}
});
valueAnimator.start();
}

到此就可以实现封面的效果,甚至可以添加别的酷炫效果,只要你敢想敢做。


总结


其实一些动效看似很复杂,但通过认真分析,我们可以将其拆分成多个简单的小块,将每个小块实现后再逐个拼装,最后达到完整的效果。本节主要是通过自定义View实现,用到绘制矩形框(drawRect),属性动画(ValueAnimator),两者使用也是非常简单。另外需要注意动画的使用和释放,避免导致不必要的内存泄漏。


好了,以上便是本篇所有内容,希望对大家有所帮助!


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

Kotlin Collection KTX:让你的集合操作如丝般顺滑

ktx
当今移动应用开发,常常需要使用各种集合类型来存储和操作数据。Kotlin 提供了 Collection KTX 扩展库,为我们操作集合提供了非常方便的 API。在本篇文章中,我们将介绍 Collection KTX 中包含的所有扩展函数,让你的集合操作变得更加...
继续阅读 »

当今移动应用开发,常常需要使用各种集合类型来存储和操作数据。Kotlin 提供了 Collection KTX 扩展库,为我们操作集合提供了非常方便的 API。在本篇文章中,我们将介绍 Collection KTX 中包含的所有扩展函数,让你的集合操作变得更加高效、简单、易读。


除了 Collection KTX,Kotlin 还提供了许多其他扩展库,例如 Android KTX、Coroutines、Serialization KTX 等,它们都可以大大简化我们的开发流程。在接下来的文章中,我们还将为您介绍这些扩展库的详细信息,让你的 Kotlin 开发之路更加畅通无阻


使用


dependencies {
    implementation "androidx.collection:collection-ktx:1.2.0"
}

用法合集


Collection 扩展函数



  • filterNot():过滤掉指定元素后的新 Collection。

  • filterNotNull():过滤掉 null 元素后的新 Collection。


List 扩展函数



  • sorted():按自然顺序排序后的新 List。

  • sortedBy():按指定方式排序后的新 List。

  • sortedDescending():按自然顺序降序排序后的新 List。

  • sortedByDescending():按指定方式降序排序后的新 List。

  • distinct():去重后的新 List。

  • distinctBy():按指定方式去重后的新 List。

  • minus():删除指定元素后的新 List。

  • plus():添加指定元素后的新 List。

  • drop():去掉前几个元素后的新 List。

  • dropWhile():去掉符合指定条件的元素后的新 List。

  • take():前几个元素组成的新 List。

  • takeWhile():符合指定条件的元素组成的新 List。

  • partition():按指定条件分隔后的 Pair。

  • groupBy():按指定方式分组后的 Map。

  • associate():按指定方式关联后的新 Map。

  • associateBy():按指定方式关联键后的新 Map。

  • associateWith():按指定方式关联值后的新 Map。

  • zip():按指定方式组合后的新 List。


MutableList 扩展函数



  • removeLast():移除最后一个元素,并返回该元素。

  • removeFirst():移除第一个元素,并返回该元素。

  • move():将指定元素移动到新位置。


Set 扩展函数



  • minus():删除指定元素后的新 Set。

  • plus():添加指定元素后的新 Set。

  • partition():按指定条件分隔后的 Pair。

  • groupBy():按指定方式分组后的 Map。

  • associate():按指定方式关联后的新 Map。

  • associateBy():按指定方式关联键后的新 Map。

  • associateWith():按指定方式关联值后的新 Map。


MutableSet 扩展函数



  • remove():移除指定元素,并返回是否移除成功。

  • retainAll():仅保留符合指定条件的元素。

  • addAll():添加指定元素后的新 MutableSet。


Map 扩展函数



  • minus():删除指定键对应的元素后的新 Map。

  • plus():添加指定键值对后的新 Map。

  • partition():按指定条件分隔后的 Pair。

  • filterKeys():按指定条件过滤键后的新 Map。

  • filterValues():按指定条件过滤值后的新 Map。

  • mapKeys():按指定方式映射键后的新 Map。

  • mapValues(): 按指定方式映射值后的新map


MutableMap 扩展函数



  • remove():移除指定键对应的元素,并返回该元素。

  • putAll():添加指定键值对后的新 MutableMap。

  • putIfAbsent():仅在指定键不存在时添加指定键值对。

  • compute():更新指定键对应的元素,并返回更新后的值。

  • computeIfAbsent():仅在指定键不存在时更新该键对应的元素。

  • computeIfPresent():仅在指定键存在时更新该键对应的元素。


Iterable 扩展函数



  • reduceOrNull():对所有元素进行指定操作,如果为 null 则返回 null。

  • reduceIndexedOrNull():对所有元素进行指定操作,同时考虑元素的索引,如果为 null 则返回 null。

  • foldOrNull():对所有元素进行指定操作并给定初始值,如果为 null 则返回 null。

  • foldIndexedOrNull():对所有元素进行指定操作并给定初始值,同时考虑元素的索引,如果为 null 则返回 null。


ListIterator 扩展函数



  • previousOrNull():返回上一个元素,如果不存在则返回 null。


Sequence 扩展函数



  • reduceOrNull():对所有元素进行指定操作,如果为 null 则返回 null。

  • reduceIndexedOrNull():对所有元素进行指定操作,同时考虑元素的索引,如果为 null 则返回 null。

  • foldOrNull():对所有元素进行指定操作并给定初始值,如果为 null 则返回 null。

  • foldIndexedOrNull():对所有元素进行指定操作并给定初始值,同时考虑元素的索引,如果为 null 则返回 null。

  • distinct():去重后的新 Sequence。

  • distinctBy():按指定方式去重后的新 Sequence。

  • filterNotNull():过滤掉 null 元素后的新 Sequence。

  • filterNot():过滤掉指定元素后的新 Sequence。

  • partition():按指定条件分隔后的 Pair。

  • sorted():按自然顺序排序后的新 Sequence。

  • sortedBy():按指定方式排序后的新 Sequence。

  • sortedDescending():按自然顺序降序排序后的新 Sequence。

  • sortedByDescending():按指定方式降序排序后的新 Sequence。

  • take():前几个元素组成的新 Sequence。

  • takeWhile():符合指定条件的元素组成的新 Sequence。

  • zip():按指定方式组合后的新 Sequence


了解工具的尿性


工具的职责就是提高工作效率



  1. 使用 Collection KTX 可以大大简化集合操作的代码,使代码更加简洁易读,同时可以提高代码的可维护性

  2. 在使用集合时,应该尽可能使用 Kotlin 标准库中的函数和 Collection KTX 中的扩展函数,而不是手写循环或通过 Java API 进行操作,这可以减少代码量和提高代码可读性。

  3. 了解不同的集合类型及其特性,选择合适的集合类型可以使代码更加高效。例如,如果需要频繁添加或删除元素,则应该使用可变集合类型。

  4. 避免频繁进行集合类型的转换,因为这会导致性能降低。如果需要对集合进行不同的操作,可以考虑使用不同的集合类型来解决。

  5. 尽量避免对空集合进行操作,因为这可能会导致空指针异常。在使用 Collection KTX 时,可以使用非空断言或者空安全操作符来处理可能为空的集合。


当然使用时也要注意,kotlin 的扩展函数让代码的可读性要求增高了少,所以用的使用为了能保证团队的统一,因该注意:



  1. 对于代码中的扩展函数,应该在函数名称中体现其作用,以便其他开发者更容易理解代码。例如,“find”函数可以改名为“findFirstOrNull”或“findLastOrNull”。

  2. 在使用 Collection KTX 时,应该注意性能问题。某些操作可能会导致性能下降,例如对大型集合进行循环和操作,因此应该考虑使用 Sequence 和 Flow 来提高性能。

  3. 避免重复操作。使用 Collection KTX 可以使代码更加简洁和易读,但是不应该过度使用,如果某个操作已经通过一个函数实现了,就不要再手动写同样的操作。

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

Flutter 知识集锦 | 基于 Flow 实现滑动显隐层

1. 前言最近要实现一个小需求,涵盖了很多知识点,比如手势、动画、布局等。挺有意思的,写出来和大家分享一下。如下所示,分为上下两层;当左右滑时,上层会随偏移量而平移,从而让上层产生滑动手势显隐的效果:标题这里上层通过不透明度 0.2 的蓝色示意,实际使用时可以...
继续阅读 »
1. 前言

最近要实现一个小需求,涵盖了很多知识点,比如手势、动画、布局等。挺有意思的,写出来和大家分享一下。如下所示,分为上下两层;当左右滑时,上层会随偏移量而平移,从而让上层产生滑动手势显隐的效果:

标题
88.gif89.gif

这里上层通过不透明度 0.2 的蓝色示意,实际使用时可以改为透明色。很多直播间的浮层就是这种交互逻辑,通过右滑来隐藏浮层。

直播右滑中
35f86de59086435083bf387a5efcc24.jpgc6c435e20ded4325899c6134f7be1d9.jpg

2. 实现思路

思路其实非常简单,监听横向滑动的手势事件,根据偏移量让上层组件进行偏移。当放手时,根据偏移量是否达到宽度的一半,使用动画进行移出或者关闭。

image.png

偏移的实现方式有很多,但需要自由地进行布局和矩阵变换、透明度,并且需要支持动画的变化,Flow 组件是一个非常不错的选择。 Flow 组件可以通过代理类对子组件进行自定义布局,灵活性极强;如果是 CustomPaint 是 绘制之王 可以绘制万物,那么 Flow 就是 布局之王,可以摆放万物。三年前写过一篇介绍 Flow 使用的文章: 《【Flutter高级玩法- Flow 】我的位置我做主》 。 本文就不对 Flow 的基础使用进行介绍了。


另外,在滑动过程中需要注意限制偏移量,使偏移量在 0~size.width 之内;当放手时,通过动画控制器来驱动动画,使用补间让偏移量运动到 0 (打开) 或 size.width(关闭) 。当关闭时,在右下角展示一个按钮用于点击展开:

image.png


3. 布局的代码实现

Flow 组件布局最重要的是实现 FlowDelegate,在其中的 paintChildren 方法中实现布局的逻辑。和 CustomPainter 类似,FlowDelegate 的实现类也可以通过 super 构造为 repaint 入参设置可监听对象。可监听对象的变化会触发 paintChildren 重新绘制:

SwipeFlowDelegate 实现类再构造时传入可监听对象 offsetX,在绘制索引为 1 的孩子时,通过 Matrix4 进行偏移。这样只要在手势水平滑动中,更新 offsetX 值即可。另外,可以根据 offsetX.value 是否达到 size.width 知道是否是关闭状态,如果已经关闭,绘制按钮。

class SwipeFlowDelegate extends FlowDelegate {
final ValueListenable<double> offsetX;

SwipeFlowDelegate(this.offsetX) : super(repaint: offsetX);

@override
void paintChildren(FlowPaintingContext context) {
Size size = context.size;
context.paintChild(0);
Matrix4 offsetM4 = Matrix4.translationValues(offsetX.value, 0, 0);
context.paintChild(1, transform: offsetM4);

// 偏移量对于父级尺寸
if (offsetX.value == size.width) {
Matrix4 m1 = Matrix4.translationValues(size.width / 2 - 30, size.height / 2 - 30, 0);
context.paintChild(2, transform: m1);
Matrix4 m2 = Matrix4.translationValues(size.width / 2 - 30, -(size.height / 2 - 50), 0);
context.paintChild(3, transform: m2);
}
}

@override
bool shouldRepaint(covariant SwipeFlowDelegate oldDelegate) {
return oldDelegate.offsetX.value != offsetX.value;
}
}

从这里可以看出,FlowDelegate 的最大优势是可以自定义孩子的绘制与否,还可以在绘制时通过 Matrix4 对孩子进行矩阵变换,还有可选参数可以控制透明度。接下来使用 Flow 组件时,提供 SwipeFlowDelegate ,并在 children 列表中依次放入子组件。其中前两个组件由外界传入,分别是底组件和上层组件,这样组件的布局就完成了,接下来监听事件,更新 factor 即可:

final ValueNotifier<double> factor = ValueNotifier(0);

Flow(
delegate: SwipeFlowDelegate(factor),
children: [
widget.content,
widget.overflow,
GestureDetector(
onTap: open,
child: const Icon(Icons.menu_open_outlined, color: Colors.white)),
GestureDetector(
onTap: () {
Navigator.of(context).pop();
},
child: const Icon(Icons.close, color: Colors.white))
],
)

4. 手势的监听

这里手势的处理是非常简单的,通过 GestureDetector 监听水平拖拽事件。在 onHorizontalDragUpdate 中根据拖拽的偏移量更新 factor 的值,其中通过 .clamp(0, widget.width) 可以限制偏移量的取值区间。

@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onHorizontalDragUpdate: _onHorizontalDragUpdate,
onHorizontalDragEnd: _onHorizontalDragEnd,
child: SizedBox(
height: MediaQuery.of(context).size.height,
width: widget.width,
child: Flow( delegate:// 同上,略...
);
}

void _onHorizontalDragUpdate(DragUpdateDetails details) {
double cur = factor.value + details.delta.dx;
factor.value = cur.clamp(0, widget.width);
}

void _onHorizontalDragEnd(DragEndDetails details) {
if (factor.value > widget.width / 2) {
close();
} else {
open();
}
}

最后在 _onHorizontalDragEnd 回调中,根据当前偏移量是否大于一般宽度,决定关闭还是打开。期间过程使用动画进行偏移量的过渡变化。


5. 动画的使用

动画的使用,主要是通过 AnimationController 动画控制器来驱动数值的变化;在放手时 Tween 创建补间动画器,监听动画器数值的变化更新偏移量。这样偏移量就可以在指定时间内,在两个值之间渐变,从而产生动画效果。比如抬手时,open 方法是让偏移量从当前位置变化到 0 :

class _ScrollHideWrapperState extends State<ScrollHideWrapper> with SingleTickerProviderStateMixin {
late AnimationController _ctrl;

final ValueNotifier<double> factor = ValueNotifier(0);

@override
void initState() {
super.initState();
_ctrl = AnimationController(
duration: const Duration(milliseconds: 200),
vsync: this,
);
}

@override
Widget build(BuildContext context) {
// 略同...
}

// 动画关闭
Future<void> close() async {
Animation<double> anim = Tween<double>(begin: factor.value, end: widget.width).animate(_ctrl);
anim.addListener(() => factor.value = anim.value);
await _ctrl.forward(from: 0);
}

// 动画打开
Future<void> open() async {
Animation<double> anim = Tween<double>(begin: factor.value, end: 0).animate(_ctrl);
anim.addListener(() => factor.value = anim.value);
await _ctrl.forward(from: 0);
}
}

如果想让动画的变化非匀速,可以使用 Curve 来控制动画曲线。这样,基于 Flow 实现的自定义布局,就可以根据手势和动画,完成特定的交互功能。从这里可以看出 Flow 自定义布局的灵活性非常强,很多疑难杂症,都可以使用它来完成。

比如企业微信中:侧滑展示左栏,而且上层不会全部消失,通过 Flow 来自定义布局就很容易实现。大家可以基于本文,自己实现一下作为练习。那本文就到这里,谢谢观看 ~

标题关闭
90.gif91.gif


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

从零打造现代化绘图框架 Plait

web
我司大概从今年(2022年)年初决定思维导图,经过半年多的研究与实践,我们基于自研的绘图框架初步完成了一个脑图组件并成功集成到我们 PingCode Wiki 中,这篇文章主要探讨下这个绘图框架(Plait)的一些设计点和思维导图落地的一些关键技术。 概论 对...
继续阅读 »

我司大概从今年(2022年)年初决定思维导图,经过半年多的研究与实践,我们基于自研的绘图框架初步完成了一个脑图组件并成功集成到我们 PingCode Wiki 中,这篇文章主要探讨下这个绘图框架(Plait)的一些设计点和思维导图落地的一些关键技术。


概论


对于思维导图、流程图前期我们做了很多调研工作,流程图方向我们研究了 excalidraw 和 react-flow,它们都是基于 react 框架实现的库,在社区中有很高的知名度,思维导图方向我们研究了 mind-elixir、 mindmap-layouts (自动布局算法),在开源领域中思维导图发展不是很好,没有成熟、知名的作品。


mind-exlixir 介绍:



mind-elixir 功能示意图


优点:



  1. 麻雀虽小但五脏俱全

  2. 纯 JS 库、轻量


缺点:



  1. 不依赖前端框架、开发方式和主流的方式不同

  2. 架构设计没有太多可取之处,节点布局方式不易扩展


虽然我们前期的目标是研发 「思维导图」 ,但是最终我们的产品目标应该是做一个 「一体化的交互式绘图画板」 ,包含思维导图、流程图、自由画笔等。


最终调研发现目前开源社区恰恰缺少这样一个一体化的绘图框架,用于实现一体化的交互式绘图编辑器,集思维导图、流程图、自由画笔于一体,所以我们结合做富文本编辑器的经验,重新架构了一套绘图框架(Plait)、拥有插件机制,并在它的基础上实现思维导图插件、落地到 PingCode Wiki 产品中,所以今天分享的主角就是 Plait 框架。


下面正式进入今天的主题,分为四部分:



  1. 绘图框架设计

  2. 思维导图整体方案

  3. 思维导图自动布局算法

  4. 框架历程/未来


一、绘图框架设计


这部分首先会先简单介绍下绘图方案的选型(Canvas vs SVG)考量,然后介绍下 Plait 绘图框架中核心部分的设计:插件机制、数据管理,最后介绍下框架优势。


绘图方案:Canvas vs SVG


Canvas 还是 SVG 其实社区中也没有一个明确的答案,我们参考了一些知名产品的方案选型,SVG 和 Canvas 都有并且实现的效果都不差,比如语雀的白板使用的是 SVG,ProcessOn 使用的是 Canvas,Excalidraw 使用的是 Canvas,drawio 使用的是 SVG 等等。因为我们没有 Canvas 的使用经验,加上我们的思维导图节点希望支持富文本内容,所以暂时选定对 DOM 更友好的 SVG,觉得先按照这个方案试试水。


经过这么长时间的验证,发现基于 SVG 的方案并没有什么明显的不足,性能问题我们也经过验证,支持 1000+ 的思维导图节点渲染完全没有问题、操作依然很流畅。



对于 SVG 绘制我们没有直接使用 SVG 的底层API ,而是使用了一个第三方的绘图库 roughjs。



下面我们看看 Plait 框架「插件机制」的部分,这部分的灵感来源于富文本编辑器框架 Slate。


插件机制


Web 前端的画图领域有很多可以深度研发的业务方向,如何基于同一个框架实现不同业务方向功能开发,就需要用到插件机制了。


插件机制是 Plait 框架一个重要特性,框架底层并不提供具体的业务功能实现,业务功能都需要基于插件机制实现,如下图所示:



插件机制通俗讲就是框架层构建的一座基础桥梁,为实现具体的业务功能提供必要的支持,Plait 插件机制有三个核心要素:



  1. 抽象数据范式(插件数据)

  2. 可复写行为(识别交互)

  3. 可复写渲染(控制渲染)


具体到流程图、思维导图这类绘图场景,它的核心是基于用户交互行为(鼠标、键盘操作)实现符合交互预期的元素绘制、渲染,如果做成可扩展的那就插件开发者可以自定义交互行为、自定义节点元素渲染,基于自定义交互生成插件数据,基于插件数据控制插件元素渲染,构成插件闭环,如下图所示(插件机制闭环示意图):




这部分的核心就是设计可重写方法,目前 Plait 中主要有两类:

第一类用于实现自定义交互:mousedown、mouseup、mousemove、keydow、keyup。

第二类用于实现自定义渲染:drawElement、redrawElement、destroyElement 然后就是框架层与插件衔接部分的设计了,这一部分在 plait/core 中目前被设计的是比较松散的,drawElement 可以返回一个 SVGGElement 类型的 DOM 元素也可以返回一个框架组件,既可以直接衔接框架也可以基于 DOM 的方式对接。

目前 Plait 框架整个是基于 Angular 框架实现的,后续可能会考虑脱离框架的设计模式,这不是本文的重点。



举个例子: 画圆插件三步走



步骤一:定义数据结构


export interface CircleElement {
type: 'cirle';
radius: number;
dot: [x: number, y: number];
}

步骤二:处理画圆交互


board.mousedown = (event: MouseEvent) => {
if (board.cursor === 'circle') {
start = toPoint(event.x, event.y, board.host);
return;
}
mousedown(event);
};
board.mousemove = (event: MouseEvent) => {
if (start) {
end = toPoint(event.x, event.y, board.host);
if (board.cursor === 'circle') {
// fake draw circle
}
return;
}
mousemove(event);
};
board.globalMouseup = (event: MouseEvent) => {
globalMouseup(event);
if (start) {
end = toPoint(event.x, event.y, board.host);
const radius = Math.hypot(end[0] - start[0], end[1] - start[1]);
const circleElement = { type: 'circle', dot: start, radius };
Transforms.insertNode(board, circleElement, [0]);
}
};

步骤三:实现画圆方法


board.drawElement = (context) => {
if (context.elementInstance.element.type === 'circle') {
const roughSVG = HOST_TO_ROUGH_SVG.get(board.host) as RoughSVG;
const circle = context.elementInstance.element as unknown as CircleElement;
roughSVG.circle(circle.dot[0], circle.dot[1], circle.radius);
}
return drawElement(context);
}

这是一个最简单的插件示例,通过框架提供的桥梁实现一个画圆插件:拖放画一个圆 -> 生成对应圆数据 -> 根据数据渲圆。


插件机制大概就是这些内容,下面看看数据管理部分。


数据管理


数据管理是 Plait 框架中非常重要的部分,它是框架的灵魂,前面的插件机制是外在表现,主要包含以下特性:



  1. 提供基础数据模型

  2. 提供基于原子的数据变化方法(Transfroms)

  3. 基于不可变数据模型(基于 Immer)

  4. 提供 Change 机制,与框架配合完成数据驱动渲染

  5. 与插件机制融合,数据修改的过程可以被拦截处理


这些都是非常优秀的特性,既可以完成数据的约束,又可以灵活实现很多复杂高级的需求,感觉这块的设计和实现其实可以算是一种特定场景的状态管理。


框架状态流转图:



上面说到的插件机制的闭环要依赖数据模型状态作为的标准,最终的插件闭环如下图所示:



这里可以列举两个具体的业务场景,都是我们开发中经常落入的陷阱,体现数据管理在的约束作用及灵活性(这部分可能不好理解,谨慎阅读,其实也是框架作用的具体说明):


场景 1: 自动选中根节点


下面这张图是一个需求示意:新建脑图时自动选中根节点并弹出工具栏



新建思维导图自动弹出工具栏示需求意图


这是一个合理的需求,但它不是常规的交互路径(常规路径是用户点击节点,触发节点选中,进而触发工具栏弹出),我们的新同学在最开始的时候就选择了一种不标准的数据修改方式(手动修改数据)去完成这个需求。


常规点选:




框架数据会存储一个选区状态(点击位置或者框选位置),点击操作会触发选区数据变化,选区变化会触发 Change 事件,基于这个机制处理节点选中和工具栏弹出。



自动选中(手动修改数据):



不推荐的操作路径示意图



这里的思路是首先模拟位置(根据自动选择的节点计算),手动修改数据,然后自己手动调用工具栏的弹出、强制刷新界面让节点选中,这就是典型的没有按照框架约束实现需求的例子,前面说到的数据流没有正确运转,需要做很多特殊处理。 不通过 Transfrom 的方式手动修改数据是不被框架允许的,不会触发框架 Change 行为,理论上应该直接抛出异常(很可惜当时没有做到这步)。



自动选中(标准路径):



标准路径就是基于模拟位置通过 Transfrom 的方式修改数据(相当于模拟点击了要自动选中的节点),后面的流程就可以依赖框架机制去控制执行,无须再做很多手动的处理。


场景 2: 思维导图节点删除后节点的选中状态自动切换到临近节点


这是一个很基础的需求,目前我们的实现是拦截节点删除行为(按 Delete/Backspace 键)处理,这样做有两个弊端:



  1. 假如将来要做右键菜单删除需要把这部分的处理代码再写一遍,即使封装成工具方法,也要额外增加一个调用入口。

  2. 一个不太好解决的问题,新建一个节点后按 Ctrl + Z 撤回,无法把选中状态转移到临近节点上,虽然这里撤回执行的也是节点删除操作。


推荐路径:


拦截节点删除操作,前面提到框架统一了数据修改的方法,所以插件开发者可以对数据修改过程进行拦截,这个拦截过程可以在数据修改前,也可以在在数据修改后(Change),在这个地方做出就不会有任何漏掉的场景。


框架作用



  1. 插件机制实现分层

  2. 数据管理实现约束

  3. 配合框架规范数据流


框架最大的意义就是分层解构,降低业务的复杂度,每个领域或者每个模块只处理自己的事情。



比如前端框架,组件化开发,就是能够把一定的逻辑归拢到一个逻辑域中(组件),而不是所有东西杂糅在一起,这是架构演进的趋势。



架构图:



二、思维导图整体方案


这里简单介绍下思维导图插件的整体技术方案,但是不会介绍特别细,因为它是基于 Plait 框架实现,大的方案肯定是与 Plait 框架一致。



思维导图整体技术方案导图


1、整体方案


我们整体是 SVG + Richtext 的方案。


绘图使用 SVG ,目前我们脑图节点、节点连线、展开收起图标等等都是基于 SVG 绘制的。


节点内容使用 foreignObject 包括嵌入到 SVG 中,这种方案使节点内容支持富文本


2、功能方案


脑图核心交互处理及渲染都是可重写方法完成,与 Plait 集成。


脑图插件仅仅负责脑图部分的渲染,至于整个画布的渲染以及画布的移动放大缩小等等是框架底层功能。


因为 Plait 是支持扩展任意元素的,支持渲染多个元素,它只由数据决定,所以它支持同时渲染多个脑图。


底层脑图插件并不包含工具栏实现,它只处理核心交互、渲染、布局。


3、组件化


脑图组件渲染、节点渲染的整体机制就是我们前端经常提到的:组件化、数据驱动,虽然组件内部节点渲染还是创建 DOM、销毁 DOM,但是大的功能还是通过组件来进行划分的。


基于脑图业务里面有两个非常重要的组件:MindmapComponent 、MindmapNodeComponent,MindmapComponent 处理脑图整体的逻辑,比如执行节点布局算法,MindmapNodeComponent 处理某一个节点的逻辑,比如节点绘制、连线绘制、节点主题绘制等。


之所以把这个部分提出来说一下,是因为我觉得这块的思想其实是主流前端框架思想的延续,包括和 Plait 框架整体的机制是统一的。


4、绘图编辑器


这里可以理解为业务层的封装,业务层级决定集成那些扩展插件,以及进一步扩展插件上层功(比如脑图节点工具栏实现),Mindmap 插件层不依赖于我们的组件库和业务组件,所以工具栏这类需要组件库组件的场景统一放到业务层实现,这样 Mindmap 插件层可以减少依赖、保持聚焦。


思维导图具体落地到 PingCode Wiki 业务中,其实有一个更虽复杂、但清晰的分层结构:



三、自动布局算法


节点自动布局是思维导图的一个核心技术,它是思维导图美观以及内容表现力的决定性因素,它关注节点如何分布,这部分说复杂不复杂,说简单也不简单,包含以下几个部分:



  1. 布局分类

  2. 节点抽象

  3. 算法过程

  4. 方向变换

  5. 布局嵌套


布局分类


介绍说明下常规思维导图的布局分类:



示意图


标准布局:



逻辑布局:



缩进布局:



时间线:



鱼骨图:



美学标准


前面说过思维导图对可视化树的展现有很高的要求,需要它是美观(这个就很直观,每个人的审美可能不一样,但是它也应当有一些基础标准)的,所以需要基础的美学标准:



  1. 节点不重叠

  2. 子节点按照指定的顺序排列

  3. 父节点在子节点中心(逻辑布局)

  4. 主轴方向上不同层级节点不重叠


节点抽象


为了简化可视化树的绘制,[Reingold-Tilford] 提出可以把节点之间的间距和绘制连线抽象出来。通过在节点的宽度和高度上添加间隙来添加节点之间的间距。如下图中的实线框显示了原始宽度和高度,虚线框显示了添加间隙后的宽度和高度。



[Reingold-Tilford] 可视化树节点抽象示意图


我们的思维导图自动布局遵循这个抽象:



  1. 节点布局本身不关注节点连线,只关注节点的宽高和间距

  2. 节点从间距中抽象出来(节点宽高和节点间隙作抽象为一个虚拟节点 LayoutNode)

  3. 布局算法基于 LayoutNode 进行布局


节点在布局时它的宽和高已经融合了实际宽高和上下左右的间隙了,这样可以降低自动布局的复杂度,上图其实是我们布局后的结果,节点从间距中抽象出来之后,节点的垂直顶部位置是其父节点的底部坐标,而父节点的底部坐标又是其顶部坐标加上其高度,真实节点与虚拟节点的逻辑关系如下图所示:



LayoutNode 示意图


算法执行过程


算法流程图:



自动布局算法执行流程图


用一个包含三个节点的例子介绍它自动布局过程,理想的结果应当如下图所示:



步骤一、前置操作: 构造 LayoutNode


这个就是前面提到的节点抽象,基于节点宽高和节点之间的间隙构建布局使用的抽象节点,此时三个处于初始状态,x、y 坐标均为零且相互重叠,如下图所示:



初始状态示意图



左边是真实状态,右侧虚线框部分没有特别的意义,只是一个不重叠的示意



步骤二、布局准备: 垂直分离



布局准备:垂直分离示意图


基于节点的父子级关系进行分层,保证垂直方向是父子级节点不重叠(节点 0与节点 1、2不重叠)。


步骤三:分离兄弟节点



分离兄弟节点过程示意图


就是分离「节点 1」和「节点 2」,保证他们水平不重叠。


步骤四:定位父节点



父级节点定位示意图


基于「节点 1」和「节点 2」重新地位父节点「节点 0」的水平位置,保证父节点水平方向上居中与「节点 1」 和「节点 2」。


布局结果:



以上就是一个的完整布局过程(逻辑下布局),逻辑并不复杂,即使多一些层级和节点也只需要递归执行「步骤三」和「步骤四」。


可以看出「逻辑下布局」只用了「算法流程图」中的前四步就完成了,最后一步「方向变换」就是在「逻辑下布局」的基础上通过数学变换的方式实现「逻辑上」「逻辑右」等布局,下面对方向变换进行专门的解释。


方向变换


1、逻辑下 -> 逻辑上



可以看出这是垂直方向上的变换关系,它们应该是基于一个水平线对称,具体的变换关系如下图所示:



逻辑上变换图


可以看最右侧最下方的节点的「y 点」应该就对应的最右侧最上方的节点「y点」,它们的位置关系应该就是:y= y - (y-yBase) * 2 - node.height。



注意上下变换应该只涉及位移,不涉及上下翻转,也就是节点内部的方向不变,y 对应 y`这两个对应的都是节点的下边上的点位。



2、逻辑下 -> 逻辑右



逻辑右示意图


从上图可以看出,这个逻辑变换也不复杂:就是一个垂直到水平的变换过程,反应到布局算法层中 x、y 方向以及节点宽高的变换,比如:



  1. 垂直分层:需要将垂直分层变换为水平分层

  2. 增加 buildTee 过程:基于分层的节点需要将节点宽度变换高度、x 坐标变为 y 坐标



处理水平布局:增加 buildTree 过程示意图


最后在「方向变换」中将宽高和 x、y 再变换回来:



得到布局结果:



3、逻辑右 -> 逻辑左


逻辑右到逻辑左的位置对应关系应该和最上面说逻辑下到逻辑上的类似,这里不再赘述。


方向变换大概就这三种,下面介绍下下布局嵌套的思路。


布局嵌套


先看一个布局嵌套的示意:



上图第二个子节点使用了另外一种布局(缩进布局),这就属于布局嵌套,布局嵌套仍然需要保证前面说到的「美学标准」比如节点不重叠、父节点居中对齐等。


简单思考: 布局嵌套中的那个有独立布局的子树,它对于整体布局的影响在于它的延伸方向的不受控制,但是如果把有独立布局的子树看做一个整体,提前计算出子树的布局,然后把子树作为整体代入布局算法就可以屏蔽子树延伸方式对整体布局的影响。


整体的处理思路如下图所示:



布局嵌套处理思路示意图


这里可以有一个抽象:把有独立布局的子节点抽象成一个黑盒子(我把它叫做 BlackNode),那么子树布局的影响就会被带入到主布局中,而子树的布局可以保持独立性。



关键点:需要先计算出有独立布局的子树的布局,然后才可以计算父节点布局



四、框架历程/未来


从技术调研到架构设想再到架构落地到产品中,历时大概1年左右的时间,核心工作集中在 2022 年的 1-9 月份,大概的时间线如下:



Plait 框架未来的一些设想



结束语


本文主要介绍从零开始做画图应用、自研画图框架、落地思维导图场景的一些技术方案,作为一个 Web 前端开发者有机会做这样的东西个人感觉很幸运,对于 Plait 框架未来还有很多事情要做,希望它可以发展成为一个成熟的开源社区作品,也期待对画图框架有兴趣的同学可以加入到 Plait 的开源建设中。


作者:pubuzhixing
来源:juejin.cn/post/7205604505647988793
收起阅读 »

简述html2canvas遇见的坑点及解决方案

web
前言 大家好,最近公司在做公众号的海报图生成功能,功能不难,但是其中也遇到了一些坑还有一些细节的问题,这里给大家复盘一下,相互借鉴及学习 制作海报选用工具 这里我看了几款生成图片的工具: html2canvas dom-to-image 这里我选用的是ht...
继续阅读 »

前言


大家好,最近公司在做公众号的海报图生成功能,功能不难,但是其中也遇到了一些坑还有一些细节的问题,这里给大家复盘一下,相互借鉴及学习


制作海报选用工具


这里我看了几款生成图片的工具:



这里我选用的是html2canvas,因为大部分人使用这个比较多,而且我也只听过这个🤣,另一个大家可以去自行摸索,毕竟我看github上也有9k的star


image.png


开始使用插件生成


引入插件


// npm 下载插件
npm install html2canvas
// 项目引入插件
import html2canvas from 'html2canvas';

html2canvas的option配置


属性名默认值描述
allowTaintfalse是否允许跨域图像。会污染画布,导致无法使用canvas.toDataURL 方法
backgroundColor#ffffff画布背景色(如果未在DOM中指定),设置null为透明
canvasnull现有canvas元素用作绘图的基础
foreignObjectRenderingfalse如果浏览器支持,是否使用ForeignObject渲染
imageTimeout15000加载图像的超时时间(以毫秒为单位),设置0为禁用超时
ignoreElements(element) => false谓词功能,可从渲染中删除匹配的元素
loggingtrue启用日志以进行调试
onclonenull克隆文档以进行渲染时调用的回调函数可用于修改将要渲染的内容,而不会影响原始源文档
proxynull代理将用于加载跨域图像的网址。如果保留为空,则不会加载跨域图像
removeContainertrue是否清除html2canvas临时创建的克隆DOM元素
scalewindow.devicePixelRatio用于渲染的比例。默认为浏览器设备像素比率
useCORSfalse是否尝试使用CORS从服务器加载图像
widthElement widthcanvas的宽度
heightElement heightcanvas的高度
xElement x-offset裁剪画布X坐标
yElement y-offset裁剪画布X坐标
scrollXElement scrollX渲染元素时要使用的x滚动位置(例如,如果Element使用position: fixed)
scrollXElement scrollY呈现元素时要使用的y-scroll位置(例如,如果Element使用position: fixed)
windowWidthWindow.innerWidth渲染时使用的窗口宽度Element,这可能会影响媒体查询之类的内容
windowHeightWindow.innerHeight渲染时要使用的窗口高度Element,这可能会影响媒体查询之类的内容

调用html2canvas时传入两个参数,第一个参数是dom节点,第二个参数是options配置项(配置项可根据上方表格进行对应配置)


html2canvas(document.body).then(function(canvas) {
document.body.appendChild(canvas);
});

调用方法生成海报


1.获取节点:let img = document.querySelector("#myImg");


2.配置需要参数:


let options = {
useCORS: true,// 开启跨域
backgroundColor: "#caddff",// 背景色
ignoreElements: (ele) => {},// dom节点
scale: 4,// 渲染出来的比例
};

3.调用方法


html2canvas(img, options).then((canvas) => {
let url = canvas.toDataURL("image/png"); // canvas转png(base64)
this.url = url;
this.isShow = false;
});

到这里html2canvas的相关使用及配置就介绍完了,接下来就是遇见的问题


使用时遇见的坑点及解决方案


图片跨域问题


第一次用我就遇见了这个问题,第一个就是百度的方法,配置useCORS: true,// 开启跨域,然后图片标签上加crossorigin="anonymous",但是结果没用,图片依旧跨域,这时候咱们前端就要硬气一点,直接让后端处理,让后端把图片地址改成base64的形式传给你,或者服务器配置跨域


生成海报时图片模糊问题


生成海报如果模糊,建议把配置项的scale配置高一点,生成的canvas图片把盒子固定大小,显示的图片就更清晰


dom之间有一道横杠


本人是在公众号上做生成海报功能,dom元素顶部是两张图片,图片顶部有一道白线,而且两张图片之间还有一道杠(不好形容),后面发现是因为生成这个海报我在公众号上用的是image标签,改成img标签就没用影响了,具体原因应该是uniapp内部处理image标签时的一些样式问题吧,这是我的猜测


注意:app上不支持html2canvas生成海报(我也是调试的时候发现的)


全部代码


这里代码仅供大家参考


<template>
<view class="poster-content">
<view class="poster-img" id="myImg" v-if="isShow">
<img class="flow" src="../../static/QC-code.png" />
<view class="card-item">
<view class="title-card">爽卡优势</view>
<view class="tip-content">
<view class="left">
<p>零月租,随充随用,不用不扣费</p>
<p>全程4G、不限APP、不限速</p>
<p>支持多场景使用</p>
<p>官方正品、品质保证</p>
</view>
<view class="right">
<view class="right-item">
<view class="qr-code">
<img
id="codeImg"
:src="imgUrl"
style="width: 100%; height: 100%"
class="flow"
crossorigin="anonymous"
/>

</view>
<view style="color: #0032d0">扫码免费领取</view>
</view>
</view>
</view>
</view>
</view>
<view v-else class="canvas-img">
<img style="width: 100vw" :src="url" alt="" />
</view>
<view v-if="isShow" style="padding-bottom: 10px">
<view @click="getImage" class="createPoster">点击生成海报</view>
</view>
<view style="padding-bottom: 50px">
<view
@click="close"
class="createPoster"
style="background-color: #fff; color: #4f80e6"
>
关闭</view
>
</view>
</view>
</template>


<script>
import html2canvas from "html2canvas";
export default {
props: {
imgUrl: {
type: String,
default: "",
},
hasQrCode: {
type: Boolean,
default: false,
},
},
data() {
return {
url: "",
isShow: true,
};
},
onShow() {},
methods: {
close() {
this.$emit("closePop");
},
getImage() {
// this.saveImg()
this.saveImg();
},
saveImg() {
let img = document.querySelector("#myImg");
let options = {
useCORS: true,
backgroundColor: "#caddff",
ignoreElements: (ele) => {},
scale: 4,
};
html2canvas(img, options).then((canvas) => {
let url = canvas.toDataURL("image/png"); // canvas转png(base64)
this.url = url;
this.isShow = false;
});
},
},
};
</script>


<style lang="scss" scoped>
.poster-content {
width: 100vw;
background-color: #caddff;
height: calc(100vh - 50px);
overflow: scroll;
margin-top: -50px;
}
.poster-img {
width: 80vw;
margin: 0 auto;
background-color: #caddff;
// display: flex;
// flex-direction: column;
padding: 40upx 20upx;
text-align: center;
.title {
width: 203px;
height: 96px;
}
.flow {
width: 100%;
height: 300px;
}
.card-item {
background-color: #fff;
font-size: 14px;
margin-top: -12upx;
padding: 20upx 0 40upx;
text-align-last: left;
border-radius: 20upx;
.title-card {
color: #0032d0;
font-size: 36upx;
font-weight: 700;
padding-left: 10upx;
}
.tip-content {
display: flex;
justify-content: space-between;
font-size: 26upx;
.left {
flex: 1;
margin-top: 10upx;
& > p {
line-height: 1.5em;
margin-top: 20upx;
padding-left: 40upx;
position: relative;
&::after {
content: "";
position: absolute;
top: calc(50% - 10upx);
left: 8upx;
width: 20upx;
height: 20upx;
border-radius: 20upx;
background-color: #0256ff;
}
}
}
.right {
width: 90px;
font-size: 20upx;
display: flex;
align-items: center;
padding-right: 16upx;
.right-item {
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
// height: 143px;
.qr-code {
width: 160upx;
height: 160upx;
background-color: #fff;
border-radius: 10upx;
display: flex;
justify-content: center;
align-items: center;
}
}
}
}
}
.canvas-img {
width: 100vw;
height: calc(100vh - 50px);
overflow: scroll;
}
}
.createPoster {
line-height: 2.8em;
width: 90%;
background-color: #5479f7;
margin: auto;
border-radius: 40upx;
color: #fff;
text-align: center;
}
</style>


结尾


这些就是本人在做海报功能所遇见的一些问题及解决方案,希望掘友们相互学习共同进步,如果有什么描述错误的地方希望给我指正,欢迎大家跟我一起交流


作者:一骑绝尘蛙
来源:juejin.cn/post/7168667322956120101
收起阅读 »

不惑之年谈中年危机

web
今年正式进入不惑之年。按 2011 年统计数据,国内 40 岁以上的程序员约是 1.5%,乐观来看,我进入了前 5% 的群体。 美国 2016 年此比例已经有 12.6%,大家还是应该乐观点。 大家都知道的国内第一代程序员,求伯君/雷军已经退休或做 CEO 了...
继续阅读 »

今年正式进入不惑之年。按 2011 年统计数据,国内 40 岁以上的程序员约是 1.5%,乐观来看,我进入了前 5% 的群体。


美国 2016 年此比例已经有 12.6%,大家还是应该乐观点。


大家都知道的国内第一代程序员,求伯君/雷军已经退休或做 CEO 了,而目前还在活跃的骨灰级程序员有陈皓(左耳朵耗子)。估计之后应该也会有越来越多的老程序员,或者说是目前活跃的程序员变成老程序员,出现在大家的视野。


骨灰级程序员:陈皓(左耳朵耗子)


程序员这个职业发展起来也就 20 年,还是一个很年轻的职业,年龄焦虑这个事情着实没有必要。


换位思考一下,如果我是公司的技术招聘者,15年内工作经验的都是大有前途的,只要你技术过得去,价格合理,我都愿意招你。


相反,15年以上的程序员就没那么受欢迎了。


因为初入职场就是互联网的蓬勃发展期,人才短缺是一直存在的,知识积累不够不要紧,个人品行不妥也不要紧,只要你敢于承担一些压力,那你的职业道路都会比较顺畅。如果再幸运一些,就能进入大厂。


升职加薪,迎娶白富美,走上人生巅峰


但是,这也造成了一些不良的现象,在这行业,投机份子其实挺多的。什么热门就跟风炒一炒,能不能做成不要紧,最主要是自己的 KPI 好看。另外,互联网企业的优待,让他们多少有些娇气。从大厂出来,薪资待遇要翻倍,要股票要期权,要好资源要好项目。


所以,一般的公司未必能容纳这些人。他们也未必愿意去这些公司。于是,35岁危机就来了。


35岁危机


年轻人其实是很难感受到中年危机的,中年危机与年轻失业的区别就像新冠与感冒的区别。你以为你经历了中年危机,实际上只是年轻失业。


今年经济不景气,裁员潮估计让一部分人离开了这个行业。年纪大的想要回炉再造,是很难的。但是,如果你还年轻,平时多积累,我相信能很轻松地找到一份工作的。


如何面对危机?


年轻人都会说苟住,换个正能量的说法是活在当下。


读好书、做好事,是能切切实实忘掉焦虑的。


今年读的《反脆弱》、《心流》和《毫无意义的工作》,将这三本书放在一起,还是很有意思的,能看到不同的观点:


jpg.jpg




  • 《反脆弱》让我对工作也有了新认识。看似很稳定的工作,会有可能让你过度依赖,如果遭遇失业就手足无措了。而类似的士司机,饿一餐饱一顿的,反而平时就很有充足的经验应对收入不稳定的情况。


    今年的形势让人更趋于进入大公司、国企、公务员单位,然后这些稳定的工作就真的这么值得大家去追随吗?越是追求稳定,最后是否会适得其反?




  • 《毫无意义的工作》今年敲醒了不少人,他提醒我们,日常琐碎的工作中消磨了我们的生命。然而这本书更多是情绪的宣泄,并没有什么好的解决方法。




  • 而《心流》则希望我们投入去做事情,只有投入了才会获得心流,才会有幸福感。同时,它让我意识到,无法逃避的事实是,工作占了我们生命的 1/4 时间。如果无法从中获得乐趣,那我们的人生注定是悲剧的。




这几本书都让我重新思考我与工作的关系,即使今年外部恶劣的情况,我们也应该重拾自身的信心,重新找回我们的热情、专注。


在面对人员缩减,项目被砍的情况下,我们也许可以把目光放在现有的项目上。


就前端而言,SSR 做不了,Docker 做不了,那就看看 nginx 缓存优化能不能做;低代码做不了,那就看看页面模板能不能做;开源做不了,就看看公共组件能不能做;什么都做不了,那首屏优化,静态资源优化,图片压缩也是能做的,而且还能做得很深。


R-C (1).jpg


只要你想,总有做不完的事情。并且这些事情,其实是我们要还的技术债务。


而此时也是做好技术积累的时机。


面向对象、设计模式、函数式编程、类型编程、异步编程这些基础都可以恶补一下;网络安全、网络通信、内存、CPU等等向外延伸的各类计算机知识也是我们必须掌握的。


工作上认真对待自己的每一行代码,生活中认真对待自己的每一分钱。


深挖知识,深入研究,懂得越多,焦虑自然就越少。


有足够的知识与经验,你的中年危机也许永远不会来


最后,对于年龄的焦虑,再推荐大家看看方励老师在“一席”的演讲《即使是像我这把年龄的人,好奇心也从来没变过,因为我们还活在人间的》


作者:陈佬昔没带相机
来源:juejin.cn/post/7187069862965936188
收起阅读 »

考研失败,加入国企当程序员,真香!

1、考研失败老家成都,本科毕业于帝都某所以能歌善舞著称的985学校,哲学专业。侥幸自己选修过大数据课程,并且听说程序员工资还不错,通过校招加入了隔壁省的一家制造业当程序员。在工作的第3年决定考研,边工作边考研,压力真是不小。让本来就是学渣的我,从研究生考场走出...
继续阅读 »

最近考研出成绩了,大家考得怎么样?分享一个自己考研失败后,入职国企的故事。

1、考研失败

我是工作了3年后才参加考研的。

老家成都,本科毕业于帝都某所以能歌善舞著称的985学校,哲学专业。

大学选专业时家里人不懂,自己全凭爱好,第一志愿就是哲学。上学时有多快乐,毕业时就多难找工作。

侥幸自己选修过大数据课程,并且听说程序员工资还不错,通过校招加入了隔壁省的一家制造业当程序员。

公司管吃管住,工资1w出头,干了2年,感觉到了危机。自己羞于靠关系上位,但技术上,计算机知识太薄弱也发展有限,于是就想趁着还年轻,考个计算机硕士,提升一下自己。

在工作的第3年决定考研,边工作边考研,压力真是不小。让本来就是学渣的我,从研究生考场走出来就知道了最终的结果:没戏了。

成绩出来后,果然不出所料。

2、加入国企

知道初试成绩以后我难过了几天,认真思考了一下自己的未来发展:旁边的帝都我也回不去了,又不甘心留在这个小城市,于是决定回老家成都发展。

毕业后一直在私企,总是听说国企好、央企不错,所以我这次投简历也想试一下。正好看到一篇讲程序员国企的文章:值得程序员加入的173家国企汇总,网友:这下彻底躺平了,于是就按照文中的思路,找到了成都的一些所谓不错的国企投递试试。

成功加入后发现:真香!

  • 福利真 ** 好!和其它公司谈薪酬,别人都是能压多低压多低,来这家国企,竟然还说我工作年限够长,在我期望的基础上加了3k。六险二金更不用说,平时的各种生活保障也是非常到位。不夸张的说,从私企转到国企的我,有一种刘姥姥进大观园的感觉。

  • 真卷!是谁告诉我国企适合养老的?这比我以前在私企工作强度大多了好嘛?而且我第一次听说部门平均加班时长影响个人绩效这种规则。

  • 技术上不激进。可能对于程序员来说,不停的学习新技术是一种常态,但是在这家国企,基本都是传统老技术,我打开代码还看到了我们领导1997年写的头文件。当然,你也可以认为这是一种不好的事。这里的技术对我来说,够了。

  • 同事关系很融洽。我人生中第一次去按摩店找技师,是女同事带我去的,谁能信!不过必须说一句,成都的按摩店是真正规啊!技师可真有劲。

去年回来工作快1年了,我现在对于考研已经释怀了。听说我们部门今年招人开始硕士起步了,有时候我还挺庆幸自己去年没考研成功的,不然即使上了研究生,我这实力,也不一定能进入现在的单位了。

3、写在最后

现在回过头来看,有一个不同的体会:考研是一件好事,但如果本身不是沉迷于科研事业,而是更想赚钱的话,有好的工作机会也别错过。

另外,多关注有用的信息很重要,有时候别人的一番话,可能就需要自己经历几年的曲折才能总结出来。

大家有任何程序员求职问题,欢迎在评论区和我交流~

作者:程序员晚枫
来源:juejin.cn/post/7205541247590973499

收起阅读 »

迄今为止我写过最复杂的算法

web
《亲戚计算器》大概是我迄今为止写过最复杂的算法了,它可能看起来它好像逻辑简单,仅1个方法调用而已,却耗费了我大量的时间!从一开始灵光乍现,想到实现它的初步思路,到如今开源已7年多了。这期间,我一直在不断更新才让它日趋完善,它的工作不仅是对数据的整理,还有我对程...
继续阅读 »

《亲戚计算器》大概是我迄今为止写过最复杂的算法了,它可能看起来它好像逻辑简单,仅1个方法调用而已,却耗费了我大量的时间!从一开始灵光乍现,想到实现它的初步思路,到如今开源已7年多了。这期间,我一直在不断更新才让它日趋完善,它的工作不仅是对数据的整理,还有我对程序逻辑的梳理和设计思路的推敲。


如果你也对传统文化稍微有点兴趣,不妨耐心的看下去……也许你会发现:原理我们日常习以为常的一个称呼,需要考虑那么多细节。


称谓系统的庞大


中国的亲戚称呼系统复杂在于,它对每种亲戚关系都有特定的称呼,同时对于同种关系不同地方、对于不同性别的人都可能有不同的称呼。




  1. 对外国人而言,父母的兄弟姐妹不外乎:uncle、aunt;而对于我们来说,父母的兄弟姐妹有:伯父、叔叔、姑姑、舅舅、姨妈;




  2. 不同地方对同个亲戚的称呼都是不一样的,以爸爸为例,别称包含有:爸爸、父亲、老爸、阿爸、老窦、爹地、老汉、老爷子等等;




  3. 不同关系链可能具有相同的称呼;比如“舅公”一词,可以是父母亲的舅舅,也可以是老公的舅舅,而这两种关系辈分却不同。究其原因我猜测是,传统上由姻亲产生的亲戚关系,为表达谦卑会自降一辈,随子女称呼配偶的长辈。




  4. 一个称呼中可能是多种关系的合称。比如:“父母”、“子女”、“公婆”,他们不是指代一个人物关系,而是几个关系的合称。




在设计这套算法的时候,我希望它能尽量包含各种称呼、各种关系链,因为我之所以做这个项目就是像让它真正集合多种需求,否则如果它不够全面那终究是个代码演示而已。


关系网络的表达


亲戚的关系网络是以血缘和婚姻为纽带联系在一起的,每个节点都是一个人,每个人都有诸如:父、母、兄、弟、姐、妹、子、女、夫、妻这样的基础关系。关系网络中的节点数量随着层级的加深而指数增长!如果是5层关系,大概就有9x9x9x9x9 = 59049种关系了(当然,这其中有小部分是重复的)。如果想要把几万个关系,数十万个称呼全部尽收其中显然是不可能的,没人有那个精力去维护。


xixik_627466c7fa1e646e.jpg


如何将亲戚关系网络中每个节点之间的关系用数据结构表现出来是一个难点。它需要保证数据量尽量全、占用体积小、易检索、可扩展等特点,这样才能保证算法检索关系时的完整性和高效性。


网络的寻址问题


既然是计算,那一定不是简单通过父、母、子、女等这些基础关系找对应称呼了。否则这就是简单的字典查询而已,谈不上算法。如果问的是:“舅妈的儿子的奶奶的外孙”又该如何呢?首先,需要在网络中找到单一称呼,如“舅妈”,而下一步找她的“儿子”,而非你自己的“儿子”。这就要求有类似于指针的功能,关系链每往前走一步,指针就指引着关系的节点,最终需找到答案。


而就像前面说到的一样,某些称谓可能对应多条关系,同时有些关系并不是唯一的。比方说你爸爸的儿子就是你吗?有没有可能是弟弟或者哥哥?而这些是不是同时取决于你的性别呢?
因为如果你是女的,那么你爸爸的儿子必然不是你呀!


这就对算法提出了一个要求,它必须准确的包含多种可能性。



年龄和性别的推测


随着关系链的复杂,最终得到的答案也有多种。那有没有一种可能,在对关系链的描述中是否存在一些词,可以通过逻辑判断知道对方的性别或年纪大小,进而排除一些不可呢?


例如“爱人的婆婆的儿子”,单从“爱人”二字我们并不能推测自己的性别,而后的“婆婆”确是只有女性才有的亲戚,“爱人的婆婆”就足以推断自己是男的,那么“爱人的婆婆的儿子”必然包含自己。相反,“爱人的婆婆的女儿”一定不是自己,只能是自己的姊妹。




再比如:自己哥哥的表哥也是你的表哥,你弟弟的表哥还是你表哥吗?因为你无法判断你弟弟和他的表哥谁大,自然无法判断对方是你的表哥还是表弟。既然都有可能存在,就需要保留可能性进一步计算。这就涉及到了在关系链的计算中不仅仅需要考虑隐藏的性别线索,还有年龄线索。




身份角度的切换


单从亲戚和自己的关系链条中开始算亲戚的称呼,仅仅是单向的推算,只需要一个个关系往下算就好。如果想知道对方称呼为我什么,这就需要站在对方的角度,重新逆向的调理出我和他之间的关系了。比如我的“外孙”应该叫我什么?



另一方面,如果把我置身于第三者,想知道我的两个亲戚他们之间如何称呼,就必须要同时站在两个亲戚的角度,看待他们彼此之间的关系了。比如:我的“舅妈”该叫我的“外婆”什么呢?



年龄排序的问题


前面说到的都是对不同关系链中的可能性推敲,那如果相同的关系如何判断年龄呢?如果你有3个舅舅呢?虽然不管哪个舅舅,他们对于你的关系都一样,他们的老婆你都得叫声“舅妈”。但他们毕竟有年龄区别,自然就有长幼的排序了。有了排序,就又引发了对他们之间关系的思考。


还是举例说明下:“舅舅”和“舅妈”是什么关系?相信大部分第一反应就是夫妻关系呗!其实不尽然,毕竟有些人不会只有一个舅舅吧?那“大舅妈”和“二舅”就不是夫妻关系了,他们是叔嫂关系呀。“二舅”得管“大舅妈”叫“嫂子”,“大舅妈”得管“二舅”叫“小叔子”。




再进一步说,“二舅的儿子”得叫“大舅妈”为“伯母”,“大舅的儿子”得叫“二舅”为“二叔”。这些由父辈的排序问题影响自己称谓的不同,而是我这套算法需要考虑的内容。




怎么样?是不是没有想象中的那么简单?
如果你想了解更多实现和思路的细节,可以关注本项目开源代码哦:github.com/mumuy/relat…


你也可以在此了解算法的基础原理:算法实现原理介绍


作者:passer-by6061
来源:juejin.cn/post/7203734711779196986
收起阅读 »

序列化和反序列化

序列化隐秘的吭,你踩过了没? 序列化和反序列化 Java序列化的目的主要有2个: 网络传输 对象持久化 当2个相对独立的进程,需要进行跨进程服务调用时,就需要把被传输的Java对象编码为字节数组或者ByteBuffer对象。 接收方只需要把这些字节数...
继续阅读 »

序列化隐秘的吭,你踩过了没?


序列化和反序列化



Java序列化的目的主要有2个:




  • 网络传输

  • 对象持久化


image-20230301144505527


当2个相对独立的进程,需要进行跨进程服务调用时,就需要把被传输的Java对象编码为字节数组或者ByteBuffer对象


接收方只需要把这些字节数组或者Bytebuf对象重新解码成内存对象即可实现通信、调用的作用。


image-20230301145117301


那么在我们使用序列化的时候有哪些需要注意的,避免的坑呢?


成员变量不能以is开头



阿里的《Java开发手册》明文规定了:成员变量禁止使用类似 isXxxx 的命名方式,也不要有isXxx命名的方法



image-20230301150018030


image-20230301145401694


大概的意思就是:不要加is前缀,因为部分框架序列化的时候,会以为对应的字段名是isXxxx后面的Xxxx



  • 比如:isSucceed序列化成Succeed,前端读取isSucceed的时候就会发现没有这个字段,然后出错了。


u=4214115302,3196714167&fm=253&fmt=auto&app=120&f=JPEG


这里面的序列化框架其实就是fastjson,我们可以直接去看他的源码


fastjson源码分析:computeGetters



去找get前缀的方法,然后进行字符串切割找到get后面的



image-20230301161434898



去找is前缀的方法,然后进行字符串切割



image-20230301161413220



  • 这里还进行了驼峰命名的判断:ixXxx,第三个字符是否是大写等判断


所以isSucceed字段会被fastjson框架认为Succeed字段。


image.png


默认值



成员变量的默认值同样会带来坑



同样是阿里的《Java开发手册》里面也是规定了:POJO类、RPC方法必须使用包装类型


image.png


关于包装类型和基本类型的区别,如果还有不清楚的,赶紧去看,这是最基础的面试知识点..


POJO类必须使用包装类型



尽量让错误暴露在编译期,不要拖到运行期



基本类型具有初始值,比如:



  • Int:0

  • float:0.0f

  • boolean:false


一个统计点赞的接口里面的返回值包含一个表示点赞数变化的字段,当发生错误的时候,这个字段没有进行赋初始值,就会出现以下情况:



  • 基本类型:读默认值,0,表达的意思就是没有点赞数变化,程序上并不知道是服务器那边出了错。

  • 包装类型:读到了个null,程序上是知道服务器那边出错了,可以进行对应的显示,比如显示 - ,表示读取不到等操作。


u=3180711090,4079282331&fm=253&fmt=auto&app=138&f=JPEG


总的来说就是:如果字段设置为基础类型并且基础类型的默认值具有业务意义,那么就会出错,并且无法感知错误


RPC方法的返回值和参数必须使用包装类型



RPC调用常常具有超时导致调用失败的情况



如果用包装类型,那么在接收方,就能感知到,这次RPC调用是成功,还是失败。


包装数据类型的null值具有表示额外的信息功能



彦祖来都来了,点个赞👍再走吧,这对我来说真的非常重要



作者:Ashleejy
来源:juejin.cn/post/7205478140914843709
收起阅读 »

扯什么 try-catch 性能问题?

“yes,你看着这鬼代码,竟然在 for 循环里面搞了个 try-catch,不知道try-catch有性能损耗吗?”老陈煞有其事地指着屏幕里的代码: for (int i = 0; i < 5000; i++) { try { ...
继续阅读 »

“yes,你看着这鬼代码,竟然在 for 循环里面搞了个 try-catch,不知道try-catch有性能损耗吗?”老陈煞有其事地指着屏幕里的代码:


 for (int i = 0; i < 5000; i++) {
try {
dosth
} catch (Exception e) {
e.printStackTrace();
}
}

我探过头去看了眼代码,“那老陈你觉得该怎么改?”


“当然是把 try-catch 提到外面啊!”老陈脑子都不转一下,脱口而出。


“你是不是傻?且不说性能,这代码的目的明显是让循环内部单次调用出错不影响循环的运行,你其到外面业务逻辑不就变了吗!”


老陈挠了挠他的地中海,“好像也是啊!”



“回过头来,catch 整个 for 循环和在循环内部 catch,在不出错的情况下,其实性能差不多。” 我喝一口咖啡不经意地提到,准备在老陈前面秀一下。


“啥意思?”老陈有点懵地看着我,“try-catch是有性能损耗的,我可是看过网上资料的!”


果然,老陈上钩了,我二话不说直接打开 idea,一顿操作敲了以下代码:


public class TryCatchTest {

@Benchmark
public void tryfor(Blackhole blackhole) {
try {
for (int i = 0; i < 5000; i++) {
blackhole.consume(i);
}
} catch (Exception e) {
e.printStackTrace();
}
}

@Benchmark
public void fortry(Blackhole blackhole) {
for (int i = 0; i < 5000; i++) {
try {
blackhole.consume(i);
} catch (Exception e) {
e.printStackTrace();
}
}
}

}

“BB 不如 show code,看到没,老陈,我把 try-catch 从 for 循环里面提出来跟在for循环里面做个对比跑一下,你猜猜两个差多少?”


“切,肯定 tryfor 性能好,想都不用想,不是的话我倒立洗头!”老陈信誓旦旦道。


我懒得跟他BB,直接开始了 benchmark,跑的结果如下:



可以看到,两者的性能(数字越大越好)其实差不多:



  • fortry: 86,261(100359-14098) ~ 114,457(100359+14098)

  • tryfor: 95,961(103216-7255) ~ 110,471(103216+7255)


我再调小(一般业务场景 for 循环次数都不会很多)下 for 循环的次数为 1000 ,结果也是差不多:



老陈一看傻了:“说好的性能影响呢?怎么没了?”


我直接一个javap,让老陈看看,其实两个实现在字节码层面没啥区别:



tryfor 的字节码



异常表记录的是 0 - 20 行,如果这些行里面的代码出现问题,直接跳到 23 行处理




fortry 的字节码



差别也就是异常表的范围小点,包的是 9-14 行,其它跟 tryfor 都差不多。



所以从字节码层面来看,没抛错两者的执行效率其实没啥差别。


“那为什么网上流传着try-catch会有性能问题的说法啊?”老陈觉得非常奇怪。


这个说法确实有,在《Effective Java》这本书里就提到了 try-catch 性能问题:



并且还有下面一段话:



正所谓听话不能听一半,以前读书时候最怕的就是一知半解,因为完全理解选择题能选对,完全不懂蒙可能蒙对,一知半解必定选到错误的选项!


《Effective Java》书中说的其实是不要用 try-catch 来代替正常的代码,书中的举例了正常的 for 循环肯定这样实现:



但有个卧龙偏偏不这样实现,要通过 try-catch 拐着弯来实现循环:



这操作我只能说有点逆天,这两个实现的对比就有性能损耗了


我们直接再跑下有try-catch 的代码和没 try-catch的 for 循环区别,代码如下:



结果如下:



+-差不多,直接看前面的分数对比,没有 ry-catch 的性能确实好些,这也和书中说的 try-catch 会影响 JVM 一些特定的优化说法吻合,但是具体没有说影响哪些优化,我猜测可能是指令重排之类的。


好了,我再总结下有关 try-catch 性能问题说法:



  1. try-catch 相比较没 try-catch,确实有一定的性能影响,但是旨在不推荐我们用 try-catch 来代替正常能不用 try-catch 的实现,而不是不让用 try-catch

  2. for循环内用 try-catch 和用 try-catch 包裹整个 for 循环性能差不多,但是其实两者本质上是业务处理方式的不同,跟性能扯不上关系,关键看你的业务流程处理。

  3. 虽然知道try-catch会有性能影响,但是业务上不需要避讳其使用,业务实现优先(只要不是书中举例的那种逆天代码就行),非特殊情况下性能都是其次,有意识地避免大范围的try-catch,只 catch 需要的部分即可(没把握全 catch 也行,代码安全执行第一)。


“好了,老陈你懂了没?”


“行啊yes,BB是一套一套的,走请你喝燕麦拿铁!” 老陈一把拉起我,我直接一个挣脱,“少来,我刚喝过咖啡,你那个倒立洗头,赶紧的!”我立马意识到老陈想岔开话题。


“洗洗洗,我们先喝个咖啡,晚上回去给你洗!”


晚上22点,老陈发来一张图片:



你别说,这头发至少比三毛多。


我是yes,我们下篇见~


作者:yes的练级攻略
来源:juejin.cn/post/7204121228016091197
收起阅读 »

实战:一天开发一款内置游戏直播的国产版Discord应用【附源码】(下)

上篇:https://www.imgeek.net/article/825362923声网RTC接入, 直播与语音实现接入在views/Channel/components文件夹下新增一个组件StreamHandler, 该组件为后续我们处理游戏房间的组件, ...
继续阅读 »

上篇:https://www.imgeek.net/article/825362923

声网RTC接入, 直播与语音实现

接入

views/Channel/components文件夹下新增一个组件StreamHandler, 该组件为后续我们处理游戏房间的组件, 先初步编写声网接入逻辑

// views/Channel/components/StreamHandler/index.js

const options = {
appId:
process.env.REACT_APP_AGORA_APPID || "default id",
channel: process.env.REACT_APP_AGORA_CHANNEL || "test",
token:
process.env.REACT_APP_AGORA_TOKEN ||
"default token",
uid: process.env.REACT_APP_AGORA_UID || "default uid",
};

const StreamHandler = (props) => {
// 组件参数: 用户信息, 当前频道所有消息, 当前频道id, 是否开启本地语音
const { userInfo, messageInfo, channelId, enableLocalVoice = false } = props;

const [rtcClient, setRtcClient] = useState(null);
// 声网client连接完成
const [connectStatus, setConnectStatus] = useState(false);

// RTC相关逻辑
useEffect(() => {
AgoraRTC.setLogLevel(3);
const client = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" });
// TODO: use right channel
client
.join(options.appId, options.channel, options.token, userInfo?.username)
.then(() => {
setConnectStatus(true);
console.log("[Stream] join channel success");
})
.catch((e) => {
console.log(e);
});

setRtcClient(client);
return () => {
// 销毁时, 自动退出RTC频道
client.leave();
setRtcClient(null);
};
}, []);

return (
<>
{!connectStatus && <Spin tip="Loading" size="large" />}
</>
);
}


// 我们需要全局状态中的userinfo, 映射一下到当前组件的props中
const mapStateToProps = ({ app }) => {
return {
userInfo: app.userInfo,
};
};
export default memo(connect(mapStateToProps)(StreamHandler));

然后回到Channel中, 在之前的renderStreamChannel函数中添加上StreamHandler组件

// view/Channel/index.js
const [enableVoice, setEnableVoice] = useState(false);
const toggleVoice = () => {
setEnableVoice((enable) => {
return !enable;
});
}

// 保留了输入窗口, 可以在它的菜单栏中添加游戏频道独有的一些逻辑,
// 这里我加入了开关本地语音的逻辑, 拓展Input的细节可以参照完整版代码
const renderStreamChannel = () => {
return (
<>
<div className={s.messageRowWrap}>
<StreamHandler messageInfo={messageInfo} channelId={channelId} enableLocalVoice={enableVoice} />
</div>
<div className={s.iptWrap}>
<Input chatType={CHAT_TYPE.groupChat} fromId={channelId} extraMenuItems={renderStreamMenu()} />
</div>
</>
);
}

const renderStreamMenu = () => {
return [
{
key: "voice",
label: (
<div
className="circleDropItem"
onClick={toggleVoice}
>
<Icon
name="person_wave_slash"
size="24px"
iconClass="circleDropMenuIcon"
/>
<span className="circleDropMenuOp">
{enableVoice ? "关闭语音" : "开启语音"}
</span>
</div>
),
}
];
}

此时我们创建一个video-开题的游戏频道, 应该可以看到命令行中输出了RTC连接成功信息. [Stream] join channel success

音视频推流

接下来我们继续做实质的RTC推流逻辑, 及用户上下播的入口. 但在那之前, 先简单过一下声网RTC中的一些概念.

参考以下步骤实现音视频通话的逻辑:

  1. 调用 createClient 方法创建 AgoraRTCClient 对象。
  2. 调用 join 方法加入一个 RTC 频道,你需要在该方法中传入 App ID 、用户 ID、Token、频道名称。
  3. 先调用 createMicrophoneAudioTrack 通过麦克风采集的音频创建本地音频轨道对象,调用 createCameraVideoTrack 通过摄像头采集的视频创建本地视频轨道对象;然后调用 publish 方法,将这些本地音视频轨道对象当作参数即可将音视频发布到频道中。
  4. 当一个远端用户加入频道并发布音视频轨道时:
  5. 监听 client.on(“user-published”) 事件。当 SDK 触发该事件时,在这个事件回调函数的参数中你可以获取远端用户 AgoraRTCRemoteUser 对象 。
  6. 调用 subscribe 方法订阅远端用户 AgoraRTCRemoteUser 对象,获取远端用户的远端音频轨道 RemoteAudioTrack 和远端视频轨道 RemoteVideoTrack 对象。

在这里插入图片描述

(以上内容来自声网官方文档)

在上面的接入中, 我们已经完成了创建对象并加入频道两步.
在RTC中, 可以传输音频和视频信号, 由于单个RTC客户端要传输不同种类的数据, 每个单独的音视频源被分成不同的track(由于它们都是实时不断产生的, 我们称作流), 随后通过publish方法, 将我们本地的信号源交付给RTC客户端传输.
随后通过user-published事件的回调来在其他用户发布信号源时进行处理, 首先需要subscribe该用户来获取后续数据, 随后根据不同类型的信号流做处理.
离开时需要关闭本地当前的信号源, 并退出RTC客户端.
最后通过user-unpublished事件监听其他用户退出, 移除它们对应的信号流.

逻辑理清楚后代码就很容易看懂了

// views/Channel/components/StreamHandler/index.js
const StreamHandler = (props) => {
...
// 本地视频元素
const localVideoEle = useRef(null);
// 远程视频元素
const canvasEle = useRef(null);
const [rtcClient, setRtcClient] = useState(null);
const [connectStatus, setConnectStatus] = useState(false);
// 当前直播的用户
const [remoteUser, setRemoteUser] = useState(null);
// 远程音视频track
const [remoteVoices, setRemoteVoices] = useState([]);
const [remoteVideo, setRemoteVideo] = useState(null);

// RTC相关逻辑
useEffect(() => {
...
// client.join 后

// 监听新用户加入
client.on("user-published", async (user, mediaType) => {
// auto subscribe when users coming
await client.subscribe(user, mediaType);
console.log("[Stream] subscribe success on user ", user);
if (mediaType === "video") {
// 获取直播流
if (remoteUser && remoteUser.uid !== user.uid) {
// 只能有一个用户推视频流
console.error(
"already in a call, can not subscribe another user ",
user
);
return;
}
// 播放并记录下视频流
const remoteVideoTrack = user.videoTrack;
remoteVideoTrack.play(localVideoEle.current);
setRemoteVideo(remoteVideoTrack);
// can only have one remote video user
setRemoteUser(user);
}
if (mediaType === "audio") {
// 获取音频流
const remoteAudioTrack = user.audioTrack;
// 去重
if (remoteVoices.findIndex((item) => item.uid === user.uid) == -1) {
remoteAudioTrack.play();
// 添加到数组中
setRemoteVoices([
...remoteVoices,
{ audio: remoteAudioTrack, uid: user.uid },
]);
}
}
});

client.on("user-unpublished", (user) => {
// 用户离开, 去除流信息
console.log("[Stream] user-unpublished", user);
removeUserStream(user);
});
setRtcClient(client);
return () => {
client.leave();
setRtcClient(null);
};
}, []);

const removeUserStream = (user) => {
if (remoteUser && remoteUser.uid === user.uid) {
setRemoteUser(null);
setRemoteVideo(null);
}
setRemoteVoices(remoteVoices.filter((voice) => voice.uid !== user.uid));
};
}

接着我们根据之前提到的自定义消息判断当前在播状态, 以最后一条自定义消息为准.

// views/Channel/components/StreamHandler/index.js
const StreamHandler = (props) => {
const { userInfo, messageInfo, channelId, enableLocalVoice = false } = props;

// 第一条 stream 消息, 用于判断直播状态
const firstStreamMessage = useMemo(() => {
return messageInfo?.list?.find(
(item) => item.type === "custom" && item?.ext?.type === "stream"
);
}, [messageInfo]);

// 是否有直播
const hasRemoteStream =
firstStreamMessage?.ext?.status === CMD_START_STREAM &&
firstStreamMessage?.ext?.user !== userInfo?.username;
// 本地直播状态
const [localStreaming, setLocalStreaming] = useState(
firstStreamMessage?.ext?.status === CMD_START_STREAM &&
firstStreamMessage?.ext?.user === userInfo?.username
);

// 本地直播流状态
const toggleLocalGameStream = () => {
if (hasRemoteStream) {
return;
}
setLocalStreaming(!localStreaming);
};
// 根据直播状态选择渲染
return (
<>
{!connectStatus && <Spin tip="Loading" size="large" />}
{hasRemoteStream ? (
<RemoteStreamHandler
remoteUser={firstStreamMessage?.ext?.user}
localVideoRef={localVideoEle}
channelId={channelId}
userInfo={userInfo}
rtcClient={rtcClient}
/>
) : (
<LocalStreamHandler
localStreaming={localStreaming}
canvasRef={canvasEle}
toggleLocalGameStream={toggleLocalGameStream}
rtcClient={rtcClient}
userInfo={userInfo}
channelId={channelId}
/>
)}
</>
);
}

我们根据hasRemoteStream分成两种逻辑RemoteStreamHandlerLocalStreamHandler(可以先用div+文字的空实现占位), 首先我们来看本地游戏的逻辑

// view/Channel/components/StreamHandler/local_stream.js
const LocalStreamHandler = (props) => {

const {
toggleLocalGameStream,
canvasRef,
localStreaming,
rtcClient,
userInfo,
channelId,
} = props;

const [localVideoStream, setLocalVideoStream] = useState(false);
const localPlayerContainerRef = useRef(null);

// 开启本地视频流
useEffect(() => {
if (!localPlayerContainerRef.current) return;
const f = async () => {
// 暂时使用视频代替游戏流
let lgs = await AgoraRTC.createCameraVideoTrack();
lgs.play(localPlayerContainerRef.current);
setLocalGameStream(lgs);
}
f();
}, [localPlayerContainerRef])

const renderLocalStream = () => {
return (
<div style={{ height: "100%" }} ref={localPlayerContainerRef}>
</div>
)
}

// 控制上下播
const renderFloatButtons = () => {
return (
<FloatButton.Group
icon={<DesktopOutlined />}
trigger="click"
style={{ left: "380px" }}
>
<FloatButton
onClick={toggleLocalGameStream}
icon={
localStreaming ? <VideoCameraFilled /> : <VideoCameraOutlined />
}
tooltip={<div>{localStreaming ? "停止直播" : "开始直播"}</div>}
/>
</FloatButton.Group>
);
};

// 渲染: 悬浮窗和本地流
return (
<>
<div style={{ height: "100%" }}>
{renderFloatButtons()}
{renderLocalStream()}
</div>
</>
);
}

现在我们进入直播房间已经可以看到本地摄像头的内容了, 但我们还没有将视频流投放到RTC中, 且上播逻辑也没有处理

// view/Channel/components/StreamHandler/local_stream.js
useEffect(() => {
// 发布直播推流
if (!localStreaming || !rtcClient || !localVideoStream) {
return;
}
console.log("height", canvasRef.current.height);
console.log("publishing local stream", localVideoStream);
// 将流publish到rtc中
rtcClient.publish(localVideoStream).then(() => {
// 频道中发布一条消息, 表示开始直播
sendStreamMessage(
{
user: userInfo?.username,
status: CMD_START_STREAM,
},
channelId
).then(() => {
message.success({
content: "start streaming",
});
});
});
return () => {
// 用户退出的清理工作,
// unpublish流(远程), 停止播放流(本地), 发送直播关闭消息(频道)
if (localVideoStream) {
rtcClient.unpublish(localVideoStream);
localVideoStream.stop();
sendStreamMessage(
{
user: userInfo?.username,
status: CMD_END_STREAM,
},
channelId
);
message.info({
content: "stop streaming",
});
}
};
}, [rtcClient, localStreaming, canvasRef, userInfo, channelId, localVideoStream]);

为了测试直播效果, 我们需要登录第二个账号(使用浏览器的匿名/开其他的浏览器, 此时cookie没有共享, 可以多账号登录), 进入相同频道, 开启直播, 此时第一个账号应该会自动刷新状态(如果没有则手动切换一下频道), 进入到RemoteStreamHandler, 说明我们直播的逻辑已经完成.

本地语音的逻辑也是类似的, 这里就不再重复.

接下来是远程流的渲染逻辑, 它的逻辑相对简单, 观看者可以选择开始/停止观看直播流

// view/Channel/components/StreamHandler/remote_stream.js
const RemoteStreamHandler = (props) => {

const {
remoteUser,
localVideoRef,
toggleRemoteVideo,
channelId,
userInfo,
rtcClient,
} = props;

// 这里加一个强制t人的开关, 由于debug
const enableForceStop = true;
const forceStopStream = () => {
sendStreamMessage(
{
user: userInfo?.username,
status: CMD_END_STREAM,
},
channelId
);
};
const renderRemoteStream = () => {
return (
<div style={{ height: "100%" }}>
<div
id="remote-player"
style={{
width: "100%",
height: "90%",
border: "1px solid #fff",
}}
ref={localVideoRef}
/>
<div
style={{
display: "flex",
justifyContent: "center",
marginTop: "10px",
}}
>
<span style={{ color: "#0ECD0A" }}>{remoteUser}</span>
&nbsp; is playing{" "}
</div>
</div>
);
};
const renderFloatButtons = () => {
return (
<FloatButton.Group
icon={<DesktopOutlined />}
trigger="click"
style={{ left: "380px" }}
>
<FloatButton
onClick={toggleRemoteVideo}
icon={<VideoCameraAddOutlined />}
tooltip={<div>观看/停止观看直播</div>}
/>
{enableForceStop && (
<FloatButton
onClick={forceStopStream}
icon={<VideoCameraAddOutlined />}
tooltip={<div>强制停止直播</div>}
/>
)}
</FloatButton.Group>
);
};
return (
<>
<div style={{ height: "100%" }}>
{renderFloatButtons()}
{renderRemoteStream()}
</div>
</>
);
}

开关远程流的代码在StreamHander中, 作为参数传给RemoteStream

// views/Channel/components/StreamHandler/index.js
const toggleRemoteVideo = () => {
if (!hasRemoteStream) {
return;
}
console.log("[Stream] set remote video to ", !enableRemoteVideo);
// 当前是关闭状态,需要打开
// 开关远程音频的逻辑也与此类型.
if (enableRemoteVideo) {
remoteVideo?.stop();
} else {
remoteVideo?.play(localVideoEle.current);
}
setEnableRemoteVideo(!enableRemoteVideo);
};

ok, 现在我们已经实现了基于声网RTC, 在环信超级社区集成视频直播的功能.

直播替换为游戏流

接下来我们来将直播流升级一下, 替换成模拟器包, 为了方便测试, 我们直接使用打包好的版本(https://github.com/a71698422/web-0.1.1), pkg包解压后直接放置到项目根目录,

RustNESEmulator 是一个基于Rust语言的NES模拟器, 我们在web平台可以使用它编译好的wasm版本

并将mario.nes文件放到src/assets目录下, 这是初代马里奥游戏的ROM文件(你也可以使用你喜欢的nes游戏, 如果遇到问题, 欢迎到RustNESEmulator中提issue)

加入前端的模拟器适配代码

// views/Channel/components/StreamHandler
// from tetanes.

import * as wasm from "@/pkg";
class State {
constructor() {
this.sample_rate = 44100;
this.buffer_size = 1024;
this.nes = null;
this.animation_id = null;
this.empty_buffers = [];
this.audio_ctx = null;
this.gain_node = null;
this.next_start_time = 0;
this.last_tick = 0;
this.mute = false;
this.setup_audio();
console.log("[NES]: create state");
}

load_rom(rom) {
this.nes = wasm.WebNes.new(rom, "canvas", this.sample_rate);
this.run();
}

toggleMute() {
this.mute = !this.mute;
}

setup_audio() {
const AudioContext = window.AudioContext || window.webkitAudioContext;
if (!AudioContext) {
console.error("Browser does not support audio");
return;
}
this.audio_ctx = new AudioContext();
this.gain_node = this.audio_ctx.createGain();
this.gain_node.gain.setValueAtTime(1, 0);
}

run() {
const now = performance.now();
this.animation_id = requestAnimationFrame(this.run.bind(this));
if (now - this.last_tick > 16) {
this.nes.do_frame();
this.queue_audio();
this.last_tick = now;
}
}

get_audio_buffer() {
if (!this.audio_ctx) {
throw new Error("AudioContext not created");
}

if (this.empty_buffers.length) {
return this.empty_buffers.pop();
} else {
return this.audio_ctx.createBuffer(1, this.buffer_size, this.sample_rate);
}
}

queue_audio() {
if (!this.audio_ctx || !this.gain_node) {
throw new Error("Audio not set up correctly");
}

this.gain_node.gain.setValueAtTime(1, this.audio_ctx.currentTime);

const audioBuffer = this.get_audio_buffer();
this.nes.audio_callback(this.buffer_size, audioBuffer.getChannelData(0));
if (this.mute) {
return;
}
const source = this.audio_ctx.createBufferSource();
source.buffer = audioBuffer;
source.connect(this.gain_node).connect(this.audio_ctx.destination);
source.onended = () => {
this.empty_buffers.push(audioBuffer);
};
const latency = 0.032;
const audio_ctxTime = this.audio_ctx.currentTime + latency;
const start = Math.max(this.next_start_time, audio_ctxTime);
source.start(start);
this.next_start_time = start + this.buffer_size / this.sample_rate;
}
// ...
}

export default State;

改造local_stream

// view/Channel/components/StreamHandler/local_stream.js

import mario_url from "@/assets/mario.nes";
import * as wasm_emulator from "@/pkg";
import State from "./state";

const LocalStreamHandler = (props) => {
// 模拟器 state
const stateRef = useRef(new State());

// 注意要将原来的代码注释掉
/*
const [localVideoStream, setLocalVideoStream] = useState(false);
const localPlayerContainerRef = useRef(null);

// 开启本地视频流
useEffect(() => {
if (!localPlayerContainerRef.current) return;
const f = async () => {
// 暂时使用视频代替游戏流
let lgs = await AgoraRTC.createCameraVideoTrack();
lgs.play(localPlayerContainerRef.current);
setLocalGameStream(lgs);
}
f();
}, [localPlayerContainerRef])

// 推流的函数也暂时注释
useEffet...
*/


useEffect(() => {
// 本地游戏
if (!canvasRef) {
return;
}
// 开启键盘监听等全局事件
wasm_emulator.wasm_main();
fetch(mario_url, {
headers: { "Content-Type": "application/octet-stream" },
})
.then((response) => response.arrayBuffer())
.then((data) => {
let mario = new Uint8Array(data);
// 加载 rom数据
stateRef.current.load_rom(mario);
});
}, [canvasRef]);

// 更新本地流渲染
const renderLocalStream = () => {
return (
<div style={{ height: "100%" }}>
<canvas
id="canvas"
style={{ width: 600, height: 500 }}
width="600"
height="500"
ref={canvasRef}
/>
</div>
);
};
}

这一步完成后, 我们就可以在本地试玩马里奥游戏了, 键盘绑定为

A      = J
B = K
Select = RShift
Start = Return
Up = W
Down = S
Left = A
Right = D

将推本地视频流改为游戏流

  useEffect(() => {
// 发布直播推流
if (!localStreaming || !rtcClient) {
return;
}
// 只修改了流获取部分
// canvas的captureStream接口支持获取视频流
// 我们用这个视频流构造一个声网的自定义视频流
let stream = canvasRef.current.captureStream(30);
let localVideoStream = AgoraRTC.createCustomVideoTrack({
mediaStreamTrack: stream.getVideoTracks()[0],
});
console.log("height", canvasRef.current.height);
console.log("publishing local stream", localVideoStream);
rtcClient.publish(localVideoStream).then(() => {
sendStreamMessage(
{
user: userInfo?.username,
status: CMD_START_STREAM,
},
channelId
).then(() => {
message.success({
content: "start streaming",
});
});
});
return () => {
if (localVideoStream) {
rtcClient.unpublish(localVideoStream);
localVideoStream.stop();
sendStreamMessage(
{
user: userInfo?.username,
status: CMD_END_STREAM,
},
channelId
);
message.info({
content: "stop streaming",
});
}
};
}, [rtcClient, localStreaming, canvasRef, userInfo, channelId]);

最后总结一下房间的流程图
在这里插入图片描述

至此该项目的完整流程就算结束啦,如果有哪些步骤细节不太明确, 可以参照完整版项目
环信超级社区项目
注册环信
模拟器直播项目github源码获取

收起阅读 »

实战:一天开发一款内置游戏直播的国产版Discord应用【附源码】(上)

游戏直播是Discord产品的核心功能之一,本教程教大家如何1天内开发一款内置游戏直播的国产版Discord应用,用户不仅可以通过IM聊天,也可以进行语聊,看游戏直播,甚至自己进行游戏直播,无任何实时音视频底层技术的Web开发者同样适用,效果如下: 开整!St...
继续阅读 »

游戏直播是Discord产品的核心功能之一,本教程教大家如何1天内开发一款内置游戏直播的国产版Discord应用,用户不仅可以通过IM聊天,也可以进行语聊,看游戏直播,甚至自己进行游戏直播,无任何实时音视频底层技术的Web开发者同样适用,效果如下:



开整!

Step1 初始化

本项目基于环信超级社区的实例项目, 所以我们先从Circle-Demo-Web这个仓库开启做初始化

  1. 克隆项目 git clone https://github.com/easemob/Circle-Demo-Web.git
  2. 安装依赖 npm install
  3. 设置appKey src/utils/WebIM.js 中设置appKey,AppKey为环信后台项目对应的key,注册环信,https://console.easemob.com/user/register,登录console后台获取Appkey
  4. 运行项目 npm run start

运行后, 登录完毕效果如下,
在这里插入图片描述

与discord设计逻辑相似, 左边功能区有

  • 个人信息页
  • 好友会话页
  • 当前加入的频道
  • 创建新频道
  • 加入服务器

超级社区的逻辑为

社区(Server)、频道(Channel) 和子区(Thread) 三层结构

社区为一个独立的结构, 不同社区直接相互隔离, 社区包含不同的频道, 代表了不同的话题, 用户在频道中聊天, 而针对一条单独信息产生的回复为子区.

我们本次的项目主要集中在频道部分, 需要加入一个服务器后, 创建一个测试社区, 保证你具有管理员权限.

Step2 协议设置

在这里插入图片描述

我们的目标是尽量利用现有api扩展功能, 有几个问题需要解决

  1. 如何区分普通频道和游戏频道?
  2. 如何区分当前频道是否有玩家直播, 如果有直播如何获取玩家信息, 第二玩家的状态?
  3. 多人聊天的状态?

如何区分普通频道和游戏频道

这里直接简单采用频道前缀做特殊区分, 创建频道前缀带video-的识别为游戏频道, 同时将渲染内容做替换

// views/Channel/index.js


const isVideoChannel = useMemo(() => {
return currentChannelInfo?.name?.startsWith("");
}, [currentChannelInfo]);

const renderTextChannel = () => {
// 原来的渲染逻辑
return (
<>
<MessageList
messageInfo
={messageInfo}
channelId
={channelId}
handleOperation
={handleOperation}
className
={s.messageWrap}
/>
<div className={s.iptWrap}>
<Input chatType={CHAT_TYPE.groupChat} fromId={channelId} />
</div>
</>
);
}

const renderStreamChannel = () => {
// 先填充一个占位符
return (
<>This is a Stream Channel<>
);
}

return (
...
<div className={s.contentWrap}>
{isVideoChannel ? renderStreamChannel() : renderTextChannel()}
</div>
...
);

如果需要区分图标, 可以搜索channelNameWrap, 分别在channelItemChannel/components/Header中添加一个css类, 通过这个类设置图标图片

如何区分当前频道是否有玩家直播, 如果有直播如何获取玩家信息, 第二玩家的状态?

我们可以复用在频道中发送消息的机制, 直播开始, 结束都可以当做一条特殊的消息发送, 只不过这条消息不承载用户的信息, 而是表达用户上下播的行为

当然这个机制存在一定实时性的问题, 不过大致是可行的.

首先我们来看一条普通的消息是如何发送的

  // components/input

//发消息
const sendMessage = useCallback(() => {
if (!text) return;
getTarget().then((target) => {
let msg = createMsg({
chatType,
type: "txt",
to: target,
msg: convertToMessage(ref.current.innerHTML),
isChatThread: props.isThread
});
setText("");
deliverMsg(msg).then(() => {
if (msg.isChatThread) {
setThreadMessage({
message: { ...msg, from: WebIM.conn.user },
fromId: target
});
} else {
insertChatMessage({
chatType,
fromId: target,
messageInfo: {
list: [{ ...msg, from: WebIM.conn.user }]
}
});
scrollBottom();
}
});
});
}, [text, props, getTarget, chatType, setThreadMessage, insertChatMessage]);

去除掉与输入框逻辑耦合的部分, 可以分为两步, createMsg创建消息, deliverMsg发送消息, 这两个功能都是环信SDK功能的封装, 经过查阅文档, 它支持发送自定义消息.
在utils中新建一个stream.js文件来封装直播的逻辑

// utils/stream.js
const sendStreamMessage = (content, channelId) => {
let msg = createMsg({
chatType: CHAT_TYPE.groupChat,
type: "custom",
to: channelId,
ext: {
type: "stream",
...content,
},
});
return deliverMsg(msg)
.then(() => {
console.log("发送成功");
})
.catch(console.error);
};

它接收content表示我们的额外信息, 用户名和上下播状态, channelId区分不同的channel, 对它的调用可以如下

// 定义在 utils/stream.js 中
const CMD_START_STREAM = "start";
const CMD_END_STREAM = "end";

// 上播
sendStreamMessage(
{
user: userInfo?.username,
status: CMD_START_STREAM,
},
channelId
);
// 下播
sendStreamMessage(
{
user: userInfo?.username,
status: CMD_END_STREAM,
},
channelId
);

第二玩家的状态可以类比第一个玩家用额外的自定义消息实现, 这里不做重复.

关于自定义消息, 原本它的作用是邀请用户加入频道, 你可以在components/CustomMsg中找到, 我们要额外识别一下直播消息(可以渲染在消息列表里, 也可以直接屏蔽掉).

// components/CustomMsg/index.js
const isStream = message?.ext?.type === "stream";


// 屏蔽
const renderStream = () => {
return (<>)
}
if (isStream) {
return renderStream();
} else {
...
}

多人聊天的状态?

我们引入声网RTC sdk, 每个进入直播房间的用户都对应维护一个声网客户端,
通过on事件感知远端视频/音频流.

根据文档 进行如下操作,

  1. 注册声网开发者, 并在后台创建一个测试项目
  2. 项目根目录创建.env文件, 存放api token等信息
# channel, uid 暂时设置为固定
REACT_APP_AGORA_APPID = your app id
REACT_APP_AGORA_CHANNEL = test
REACT_APP_AGORA_TOKEN = your token
REACT_APP_AGORA_UID = 123xxx
  1. 添加声网sdk依赖 npm install agora-rtc-sdk-ng

我们在下一章中编写接入逻辑

声网RTC接入, 直播与语音实现

收起阅读 »

迎接35岁,我在美团的22年总结及23年规划

22年10月,喜提二胎,同时儿子还不到两岁,工作中已经苦练写作基本功了,很难有心力在工作外写东西。 但作为一个技术从业者,定期写技术博客和总结规划,是确保高效工作的好习惯。工作已经十年了,越来越认可“选择比努力更重要”。 多数人都不喜欢思考,经常用战术上的勤奋...
继续阅读 »

22年10月,喜提二胎,同时儿子还不到两岁,工作中已经苦练写作基本功了,很难有心力在工作外写东西。


但作为一个技术从业者,定期写技术博客和总结规划,是确保高效工作的好习惯。工作已经十年了,越来越认可“选择比努力更重要”。


多数人都不喜欢思考,经常用战术上的勤奋掩饰自己战略上的懒惰,我也是如此,人生才如此被动。


本文不仅会分享自己在2022年所作出的重大人生选择及2023年的规划,还会在字里行间反思自己从高考后一些重大选择的得失。希望自己能以此洗心革面、痛改前非和开启新生,也希望能给有缘人在人生选择上有一些参考。


自我简介



本节主要是流水账的自我回顾,枯燥而乏味,建议读者有选择地看,或者跳过看后面的总结及规划哈~



从小爱学习


我出生于广西一个瑶族自治县的农村家庭,虽然家庭很不富裕,但童年回忆起来还是有很多快乐。但在青少年时期,父母吵闹较多,邻里关系不和,让看似阳光开朗的我,其实在内心已经种下了忧郁的种子,即使成年后,远离家乡,也时常会做相关的噩梦。‘


从小我就喜欢学习,记得小学一二年级,我经常6点多就到学校,有时周末也去学校,还被人笑话,但成绩只有数学还可以。直到四年级,我才开始获得奖状,印象中四至六年级的班主任,总是让我感觉自己很优秀,于是我的成绩真的变好了。


求学之路


初中,通过二婶大姐的帮助,把我安排到了县城最好中学的农村班,开始了住校的生活。初二开始,经常能考全班第一,全校前30。虽然中考不是很好,但还是考上了市里最好的高中。


从高二开始,成绩不时能考全班第一。高考虽然自我感觉发挥不好,但总分还是能排到全校应届第10,全广西一千六百多名。


由于家庭不富裕,高考后我优先考虑读军校,这样大学几年不会让家里负担过重。但因为各种原因,提前批我居然录取上。


于是我就来到了当时随手填的大学及专业,大一时发现可以选拔在校国防生,毕业后和军校生一样直接派到部队任职。于是大二时,我通过选拔加入了国防生队伍。


大学四年的成绩不是很好,因此没有资格保研。但我心中还是很想读研的,但因为国防生的身份,清华不允许报考,丧失了动力后自然考不上,这也成了后来我主动离开部队的原因之一。


军旅生涯


毕业后,我被分配到了北京军区某部的作战部队,在基层连队体验了一个多月后,就被派到南京某军校进行学习培训。军校培训期满后,我回到了北京的部队,开始了指挥军官的工作。


同多数国防生一样,我对基层部队的生活很不适应,被冻晕过几次,加上单位不同意我报考国防大学的研究生,我在13年底提出了复员转业申请。整体还算顺利,提出申请两个多月后,审批就通过了,然后我就离开了部队。


虽然从军入伍有减轻家庭负担的考虑,但离开部队后我还是会经常梦回连队,内心依然很向往部队的工作,也许还有壮志未酬,更有愧疚之情。即使不在部队了,我相信在国防建设上,还是有很多我可以参与的。


离开部队后,我全身心地投入了考研,因为计算机专业考公务员的岗位比较多,所以我选择了跨专业考清华的计算机专业。但没想到我专业课差得太多,只过了国家线,虽有一些院校联系我调剂,但我还是放弃了。


工作履历


然后,我就准备找C++相关的工作,通过大二时就认识的国防生朋友,内推到了一个做3G&4G 通信卡的外企gemalto,正式开启了程序员的生活。


由于做嵌入式操作系统的开发比较乏味,所以我们主动参与了一个小组的 Android 开发工作,学得差不多后,我们都先后离开了外企,他去了360做安卓开发,我去了搜狐做RN开发。


从搜狐开始,我从RN入门大前端,逐步掌握了react web开发、node 中间层或后端开发、vue web 开发、flutter 或 uni-app 跨端开发、PHP、Java、Go和Ruby后端开发。


从2017下半年开始,我的工作主要是管理团队和推进重点项目,团队最多时接近30人,职能上不仅管理过前端、安卓和iOS,也负责过设计团队。


疫情开始的半年多时间,尝试过在线教育创业,后来因为进展不符合预期和需要结婚生娃,在20年下半年入职了美团的前端基建团队。


回顾2022年


2022年初,因为老婆怀上了二胎,我终于做出了两个人生非常重要的选择:一是离开北京;二是从管理者向技术专家转变。


离京的选择


现在想想,有时选对城市,比选对行业或公司都重要。从2007年来到北京上大学,到2022年挥别北京,转眼间,我已经在北京度过了15个春秋。


离开北京时,我居然没有一丝伤感,仔细一想,15年来自己都没有真正考虑过要留在北京。虽然2007年时,就把户口迁到了北京。因为入伍地是北京,离开部队后,只要找到国企接收,也能把户口留在北京。但当时报考的是清华计算机深圳分院,索性就把户口迁回老家了。


21年12月底,父母都在北京,于是全家在北京过了年,但父亲还是不适应北京的气候,春节后就回老家了。而我亲弟在上海,老家只有我爸和爷爷,出了意外不能及时回到他们身边处理,加上老婆怀上了二胎,未来两个孩子的户口及上学问题,使得我不得不考虑离京的问题。


因为我所在的团队,在上海也有很多研发,有时我也需要去上海出差,加上我弟、堂妹、表姐和表弟等都在上海,所以当时优先考虑去上海,这样也不需要换部门或公司。


但由于年初上海的疫情比较严重且排外严重,同时上海离老家还是太远,看到公司在深圳也有岗位,聊了两个部门都有意向后,我选择了深圳,代价是需要换个部门,绩效和调薪都会受影响。选择深圳还有另外一个考虑,去上海的话,至少要缴满一年社保才能有户口,而深圳只需要缴满一个月即可。


三月底发起活水申请并逐步交接工作,四月中旬我就来到了深圳,五一后就把全家接到深圳安定了下来,6月初就办理好了全家的户口迁移。


当时在知乎上,反复看了深圳、上海、广州和北京的城市对比。对我来说,还是深圳最适合我。相比北京,深圳空气质量好很多,我老婆来了深圳后皮肤变好了很多,还有深圳的政务办事效率很高,日常生活很方便。


回想在北京时,早上我得六点前开车上班,否则不知道被五环堵到什么时候,下班时我得八点半前走,否则不知道什么时候可以离开望京。来到深圳后,我就住在公司附近,出门到工位不到10分钟就可以了,有了更多工作时间和关注生活。


回顾城市的选择,我真的很后悔没有早点来到深圳,若不是因为老婆执意要离开北京,因为22年初申请到了工作居住证,很有可能我下半年就在北京买房继续麻木地北漂了。


工作的选择


2022年,全世界所有的互联网公司都不好过,裁员消息层出不穷。作为一个即将35岁的大龄码农,我也是危机感满满,做好了随时被裁的心理建设。


对我来说,留在原部门原团队是最保险的,也是最好的:之前的工作以管理沟通为主,比较得心应手,领导们也比较认可,绩效和调薪也都有保障,同时负责的基本是中后台技术项目或前端基建项目,如组件库、物料管理、提效工具等,既有技术深度又有较好的工作节奏。


但因为原部门不能在深圳放团队,所以我只能通过活水到在深圳有岗位的团队,结合自己长远的职业规划,我选择了人工智能方向的前端团队。虽然团队只有十人上下,但可以做的事情却很有技术深度,同时团队的学历也比较好,60%都是硕士,还有一个是北大的硕士。


来到新团队后,我先加入近期很火的人工智能创作项目,使用 vite2 + vue3 从0开发了一个以图片处理为主要功能的web应用,经常加班赶项目进度,业余时间自学图形渲染。


然而,来到深圳不到半个月,就有一个前端伙伴要离职了,需要开发维护他从零开发的渲染引擎,工作难度比较大加上来深圳后因为燥热一直休息不好,我萌生了先辞职休息一段时间,再重新找工作的想法。


期间虽然没有好好准备面试,抱着了解招聘行情的心态,也和腾讯、字节、虾皮及一些传统行业的公司聊了聊,能拿到offer的基本是管理岗,传统行业一般是大前端总监,管理五十人以上团队,直接向CTO或CEO汇报。


虽然没有调薪,但领导们多次挽留,并给我争取了一些股票,特别是我的直接leader,相当nice,可以让我选择自己想做的事情,并允许我休个长假调整身心,所以我选择留了下来。


做出这个选择的考虑:一是管理岗可遇不可求,毕业后80%的工作都是管理,技术沉淀不够;二是web端的图形渲染和AI推理技术门槛高且非常有趣,并且近几年的人才缺口大,以后即使做管理,招不到人时,自己也得能搞定。


休假回来后:在业务支持上,我调整到了模型训练和管理相关的中后台项目,便于更好地掌握AI应用开发相关的知识技能;在研发提效上,我基于在前端基建团队的建设成果,修订了我们团队的前端工程规范和推进了项目的工程改造;在技术产出上,我主导了web推理引擎的立项,从零实现了 WebAssembly 计算方案,推进了在人脸验证和智能创作等项目的落地。


在新的团队,因为少了很多管理相关的会议,让我有了更多的机会和时间,结合项目需要,系统学习图形渲染和AI应用开发相关的知识技能。


截屏2023-01-27 18.53.30.png


过去的一年,因为两个孩子比较小需要更多精力放在家庭,同时也因为变换城市和岗位,还有新冠的影响,工作产出应该只达到了我预期的70%。但很幸运,让我遇见了一个很好的团队,有了领导的信任和优秀的伙伴们,相信新的一年,我一定会收获满满。


2023年规划


2023年,我首先要养成三一习惯:每周跑一十公里强体魄、每周看一好书启智慧、每周做一公益得快乐。


其次,在生活上,我要好好研究做饭和带娃。虽然我很想为公司的外卖业务贡献力量,但是对孩子们来说,能选择的很少,而且安全和营养都不好保证。生娃养娃容易,但教好孩子很不容易,童年的创伤,我相信一定可以在养育孩子的过程中治愈。同时,带娃也能帮助我理解一些人工智能相关的理论,现在感觉模型训练和教小孩真的很相似~


最后,在工作上:上半年,我要带领小伙伴们进一步完善Web推理引擎,同时提供一系列面向web应用研发同学的AI入门教程;下半年,我将从提升模型部署易用性出发,规划并建设一个全端的AI推理系统。


截屏2023-01-27 18.55.28.png


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

【架构师李肯】成为了公司股东,而我却失眠了!

提笔的现在是2023年1月13日农历腊月廿二,凌晨四点半,我刚失眠醒来不久,头脑里的思绪万千,无法入睡,索性把它写下来,以便有个回忆。 惊喜与荣誉 说起这次失眠,源于昨天发生的一件事,下午上着班,突然收到微信群邀请,点进去一看,都是公司BOSS、高管、中层l...
继续阅读 »

提笔的现在是2023年1月13日农历腊月廿二,凌晨四点半,我刚失眠醒来不久,头脑里的思绪万千,无法入睡,索性把它写下来,以便有个回忆。


图片


惊喜与荣誉


说起这次失眠,源于昨天发生的一件事,下午上着班,突然收到微信群邀请,点进去一看,都是公司BOSS、高管、中层leader、以及各研发部门的绝对核心,你还别说,真有一种【误入藕花深处】的感觉。我一想,可能有事要发生,且很有可能事儿还不小。


果然,随后群主简单说了下,今晚BOSS要请大家喝酒撸串,请大家务必抽空。也没说啥主题,就发了时间和地点,大家按时到达即可。


直到饭局上,才从BOSS口中得到了确切的消息,也的确算得上是一个好消息:今晚是公司新旧股东的感谢会加欢迎会。其中,我和几个小伙伴就是新加入的那一批股东。


我一听这好消息,有些喜出望外,但又感觉合乎常理,因为我知道公司每年都有配股的传统,也算是公开的小秘密,只要你表现足够优秀,就一定能被高层看见,这也许就是【小庙】的好处吧。


席上,我端起来了闲置了大半年的酒杯,是因为身体原因,也是本身并不嗜酒,所以每次拿开车作为借口搪塞一下也就过去了。但这次有些不一样,毕竟BOSS端着酒在你面前,你说能不能不喝?


有人的地方,就有江湖。烤串局上,一个个推杯换盏,很是热闹,大家笑说旧年的总结,展望新年的目标。


之余,让我印象最深的是公司分管销售的副总裁的敬酒。对,没错,就是公司业绩扛把子的那位。他跟我说,恭喜你加入公司的股东会,恭喜你在2年内就得到了公司的认可,但你知道我花了多久才被这个行业认可吗?答案是【20年】!


哗哦,不真不令人竖起大拇指,绝对对得起他的那些远赴盛名,值得我辈学习。


图片


满载的收获


聊到这,乘着这次机会,再次聊聊2022年的收获吧。


2022年在工作上始终不遗余力,有领导的绝对核心信任,工作方面得心应手,架构设计,疑难攻关,前沿预研,传帮分享,一个没落下,也终于在自己的努力下,再次拿下了年度绩效S考核的优异成绩,不出什么幺蛾子的话,【优秀XX人】的奖杯应该正在制作中!


工作上,至少在我负责的研发领域,可以说收获到了极致,其他平行能力领域还需进一步拓展。


同时,2022年是我致力于打造【架构师李肯】这个技术IP的元年,这一年我在工作之余,沉心创作了100+的技术原创博文,谈不上有多高产,比起那种年入上W篇的量产大户,我这基本拿不上台面。但必须要承认的是,我自己的好些博文是真的用心在这的,有时候为了写清楚一个问题,前前后后耗时都接近一周。很荣幸,这些博文都得到了行业大佬们的认可,比如在RT-Thread技术论坛上,这些博文都被追加了【优秀】,这是一种肯定与荣耀,也是基于这些优异的社区贡献,RT-Thread在2022年度的开发者大会上授予了我【2022年度RT-Thread社区杰出布道者】。这又是更高一级的认可,反正这个牛批可以拿出去吹好一会了!又比如在电子发烧友论坛上,我也收获了很多,技术专栏是我一个亮点,也是因为内容够硬,很荣幸收到了电子发烧友的年度表彰,成为了【年度优秀专栏创作者】。


技术创作是一方面,参加课外大赛是另一方面。期间参加的国民技术应用设计大赛和瑞萨电子应用设计大赛都收获了第一名的好成绩,还有一笔不错的奖金,真是美滋滋。


2022年一共受邀参加了4场在线演讲,写了4份PPT,谈不上多亮点,但只要我用心,有那么一页PPT能够帮助到有心人,这就够了!


还有一个最重要的,今年收获了很多技术道路上志同道合的朋友,我们一起打怪升级,感谢你们的支持,你们就是【架构师李肯】这个技术IP持续发展的最大动力。


图片


追忆与迷茫


这部分话题,本来想再写写的,看着天快亮了,留个记号吧 // TODO


图片


新年新希望


2022年注定是不平凡的一年,有着太多的不堪回首和刻骨铭心,而对于我来说,却也是我收获的一年。最后,新的一年里,祝大家在技术的路上兔飞猛进,在职场的发展上兔步青云;愿大家都能收获心中所想,实现新的远大目标!


图片


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

iOS 3年开发迷茫随心聊

iOS 3年开发迷茫随心聊 从毕业开始做iOS,到现在已经是第4个年头了。第一家公司,做了一年,项目没上线就倒闭了,导致找第二家公司的时候也没有一个项目能拿的出手。第二家公司误入一家游戏公司,每天工作就是将H5小游戏做成一个App,想办法上线,一年过去了,技术...
继续阅读 »

iOS 3年开发迷茫随心聊


从毕业开始做iOS,到现在已经是第4个年头了。第一家公司,做了一年,项目没上线就倒闭了,导致找第二家公司的时候也没有一个项目能拿的出手。第二家公司误入一家游戏公司,每天工作就是将H5小游戏做成一个App,想办法上线,一年过去了,技术其实也没什么长进,但是通过这个过程了解一些苹果上架的知识。也有了几个上线项目经验了。由于想找个正经互联网公司做App,也离职找工作。


找工作头一个月,发现面试面的问题都是与底层相关,一问三不知,在家埋头学了2个月底层相关知识(可以理解背题)。原理是懂一点了,但是没有在实际项目中运用,工资也上不太去。在面了20多家公司后,终于找到现在第三份工作。


由于有第二份工作的经历,在第三家公司上班的时候一直在学习,有意识的去面试的原理去解决一些开发中的问题,例如使用runtime解决一些问题,却发现runtime如果没有很强的理解,还是不要用在项目里,因为可能出现未知的风险。例如使用交换方法,全局做了修改,但是后期项目需求更改,保持全局修改的前提下,对其他情况要做不同处理。也没有太多需求会使用到原理的内容,性能也不需要优化。


小公司对技术不太感冒,能完成需求就行,虽然自己力所能及的去做一些规范,但觉得做的还是不够,也不清楚其他公司到底是如何做的。小公司个人感觉对员工做事的责任心更加看重。需求就是写页面,页面还原的好,做的快一点,bug少一点就行了。不是理想的一个团队有什么方案,规范,让开发更有效率。最大感触是还好没有成为一个油腻的开发~。


在现在的公司,做了几个项目,也没有大的bug。学会了Swift进行开发。也许也算是一种收获吧。但是公司不加薪,今年的目标是想学点东西换一份工作。


学了1个月RxSwift,感觉也快学不下去了,公司是不可能用了,网上也有人说这个架构太重了。语言是个问题,自己英语水平有限,学习速度太慢了。如果有看到的这篇文章的小伙伴,也可以给我点意见。


最近想学一点提高开发效率的技能。和面试相关的内容。如果有大神经历过我这个时期,还麻烦给点建议。建议退iOS坑的就不必留言了。个人虽然菜,但是如果还没有做到小公司天花板的话,目前不考虑退坑。


第一次发文章也不知道说啥,后面会更新一些学习笔记啥的。感谢包容。


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

Android:面向单Activity开发

记得前一两年很多人都跟风面向单Activity开发,顾名思义,就是整个项目只有一个Activity。一个Activity里面装着N多个Fragment,再给Fragment加上转场动画,效果和多Activity跳转无异。其实想想还比较酷,以前还需要关注多个Ac...
继续阅读 »

记得前一两年很多人都跟风面向单Activity开发,顾名思义,就是整个项目只有一个Activity。一个Activity里面装着N多个Fragment,再给Fragment加上转场动画,效果和多Activity跳转无异。其实想想还比较酷,以前还需要关注多个Acitivity之间的生命周期,现在只需关注一个,但还是需要对Fragment的生命周期进行关注。



其实早在六七年前GitHub上就有单Activity的开源库Fragmentation,后来谷歌也出了一个库Navigation。本来以为官方出品必为经典,当时跟着官方文档一步一步踩坑,最后还是放弃了该方案。理由大概如下:



  1. 需要创建XML文件,配置导航关系和跳转参数等

  2. 页面回退是重新创建,需要配合livedata使用

  3. 貌似还会存在卡顿,一些栈内跳转处理等问题


而Github上Fragmentation库已经停止维护,所幸的是再lssuse中发现了一个基于它继续维护的SFragmentation,于是正是开启了面向单Activity的开发。


提供了可滑动返回的版本


dependencies {
//请使用最新版本
implementation 'com.github.weikaiyun.SFragmentation:fragmentation:latest'
//滑动返回,可选
implementation 'com.github.weikaiyun.SFragmentation:fragmentation_swipeback:latest'
}

由于是Fragment之间的跳转,我们需要将原有的Activity跳转动画在框架初始化时设置到该框架中


Fragmentation.builder() 
//设置 栈视图 模式为 (默认)悬浮球模式 SHAKE: 摇一摇唤出 NONE:隐藏, 仅在Debug环境生效
.stackViewMode(Fragmentation.BUBBLE)
.debug(BuildConfig.DEBUG)
.animation(
R.anim.public_translate_right_to_center, //进入动画
R.anim.public_translate_center_to_left, //隐藏动画
R.anim.public_translate_left_to_center, //重新出现时的动画
R.anim.public_translate_center_to_right //退出动画
)
.install()

因为只有一个Activity,所以需要在这个Activity中装载根Fragment


loadRootFragment(int containerId, SupportFragment toFragment)

但现在的APP几乎都是一个页面多个Tab组成的怎么办呢?


loadMultipleRootFragment(int containerId, int showPosition, SupportFragment... toFragments);

有了多个Fragment的显示,我们需要切换Tab实际也很简单


showHideFragment(ISupportFragment showFragment);

是不是使用起来很简单,首页我们解决了,关于跳转和返回、参数的接受和传递呢?


//启动目标fragment
start(SupportFragment fragment)
//带返回的启动方式
startForResult(SupportFragment fragment,int requestCode)
//接收返回参数
override fun onFragmentResult(requestCode: Int, resultCode: Int, data: Bundle?) {
super.onFragmentResult(requestCode, resultCode, data)
}
//返回到上个页面,和activity的back()类似
pop()

对于单Activity而言,我们其实也可以注册一个全局的Fragment监听,这样就能掌控当前的Fragmnet


supportFragmentManager.registerFragmentLifecycleCallbacks(
object : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentAttached(fm: FragmentManager, f: Fragment, context: Context) {
super.onFragmentAttached(fm, f, context)
}
override fun onFragmentCreated(
fm: FragmentManager,
f: Fragment,
savedInstanceState: Bundle?
) {
super.onFragmentCreated(fm, f, savedInstanceState)
}
override fun onFragmentStarted(fm: FragmentManager, f: Fragment) {
super.onFragmentStarted(fm, f)
}
override fun onFragmentResumed(fm: FragmentManager, f: Fragment) {
super.onFragmentResumed(fm, f)
}
override fun onFragmentDestroyed(fm: FragmentManager, f: Fragment) {
super.onFragmentDestroyed(fm, f)
}
},
true
)

接下来我们看看Pad应用。对于手机应用来说,一般不会存在局部页面跳转的情况,但是Pad上是常规操作。


image.png


如图,点击左边列表的单个item,右边需要显示详情,这时候再点左边的其他item,此时的左边页面是保持不动的,但右边的详情页需要跳转对应的页面。使用过Pad的应该经常见到这种页面,比如Pad的系统设置等页面。这时只使用Activty应该是不能实现的,必须配合Fragment,左右分为两个Fragment。


但问题又出现了,这时候点击back怎么区分局部返回和整个页面返回呢?


//整个页面回退,主要是用于当前装载了Fragment的页面回退
_mActivity.pop()
//局部回退,被装载的Fragment之间回退
pop()

如下图,这样的页面我们又应该怎么装载呢?
image.png


可以分析,页面最外面是一个Activty,要实现单Activity其内部必装载了一个根Fragment。接着这个根Fragment中使用ViewPage和tablayout完成主页框架。当前tab页要满足右边详情页的单独跳转,还得将右边页面作为主页面,以此装载子Fragment才能实现。


image.png


总结


单Activity开发在手机和平板上使用都一样,但在平板上注意的地方更多,尤其是平板一个页面可能是多个页面组成,其局部还能单独跳转的功能,其中涉及到参数回传和栈的回退问题。使用下来,我还是觉得某些页面对硬件要求很高的使用单Activity会出现体验不好的情况,有可能是优化不到位。手机应用我还是使用多Activity方式,平板应用则使用该框架实现单Activity方式。


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

2023也该知道了:kotlin协程取消和异常传播机制

什么是结构化并发? 说好的异常传播为啥失效了? 怎么还有async不抛异常的问题? 1 结构化并发(Structured Concurrency) 1.1 java的"离散性并发" kotlin 的Coroutine是【结构化并发】,与结构化并发对应的方式是...
继续阅读 »
  • 什么是结构化并发?

  • 说好的异常传播为啥失效了?

  • 怎么还有async不抛异常的问题?


1 结构化并发(Structured Concurrency)


1.1 java的"离散性并发"


kotlin 的Coroutine是【结构化并发】,与结构化并发对应的方式是【fire and forget 】姑且称之为【离散性并发】吧,可能不太准确。一个例子解释下离散性并发,java里我们开启一个线程之后,是不具备跟踪管理这个线程的能力的。如下


    public void javaThreadFun() {
       Thread thread = new Thread(new Runnable() {
           @Override
           public void run() {
               //do some work
          }
      });
       thread.setName("child-thread");
       thread.start();
  }

这个例子中,调用javaThreadFun()方法所在的线程,创建并启动child-thread线程之后两个线程没有明确的父子关系,avaThreadFun()方法所在的线程不能天然的感知在自己线程里启动的"子线程",子线程发生异常之后也不会影响到自己。如果父线程要取消中止在自己线程里启动的那些线程也没有现成的方式去供使用。总之,层级关系管理上很离散。


1.2 kotlin 协程的的结构化并发


image.png


kotlin的协程天然的具备父协程管理取消子协程、子协程的异常失败影响父协程或者父协程感知子协程错误和失败的能力。如下示例


      GlobalScope.launch {
           val parentJob = launch {
               val childJob = launch {
                   delay(1_000)//子任务做一些事情
                   throw NullPointerException() //会导致父协程任务和兄弟协程任务都会被取消
              }
               delay(5_000)
          }
      }


  • childJob失败抛出异常,会影响到父job,进而父job会取消掉其所有的子job。

  • 另外,父job也会等待所有的子任务结束后自己才会结束。


与传统的相比



  • 有跟踪:在协程左右域里启动协程会作为该协程的子协程,该协程会跟踪这些协程的状态。而不是像线程那些开启之后就忘记没有跟踪。父协程的结束也是要在所有子协程都完成之后自己才会完成,颇有家长负责制的感觉。

  • 可取消:取消父协程也会,把其子协程一并取消掉。如上图,取消掉parent-job会导致从属于他的所有子协程取消。

  • 能传播:这特性体现在,子协程发生异常,会通知其父协程,父协程会取消掉自己所有的子协程然后再向上传递直到根协程,或者supervisorJob.(这个下文我们会展开分析)


2 取消机制


2.1 父协程的取消会取消子协程


这一章节我们展开聊下Kotlin协程的取消机制,上一节我们提到,父协程/作用域的取消也会取消其子协程我们看个例子。


 GlobalScope.launch {
       val mParentJOb: Job = this.launch {
           val child1Job: Job = this.launch {
               this.launch {
                   delay(300)
              }.invokeOnCompletion { throwable ->
                   println("child1Job 执行完毕,收到了${throwable}")
              }
               val child2Job = this.launch {
                   delay(500)
              }.invokeOnCompletion { throwable ->
                   println("child2Job 执行完毕,收到了${throwable}")
              }
          }
           delay(100)
      }
       mParentJOb.invokeOnCompletion { throwable ->
           println("mParentJOb 执行完毕,收到了${throwable}")
      }
       println("发起取消 mParentJOb")
       mParentJOb.cancel()
  }.join()

运行结果:


发起取消 mParentJOb
child1Job 执行完毕,收到了kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@100b06de
child2Job 执行完毕,收到了kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@100b06de
mParentJOb 执行完毕,收到了kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelled}@100b06de

2.2 兄弟协程取消不影响


private suspend fun brotherCoroutine() {
   coroutineScope {
       launch {
           delay(500)
           println("is running")
      }
       launch {
           delay(100)
           cancel()
      }.invokeOnCompletion {
           println("job2 is canceled")
      }
  }
}

这似乎没有什么可解释的,某个协程的取消并不会影响到其兄弟协程。


2.3 携程的取消是协作式的


协程的取消是协作式的体现在,对取消的通知需要主动的需要主动嗅探感知做出处理。举个例子


private suspend fun coroutineCanceling() {
   coroutineScope {
       val job = launch {
           var i = 0
           while (true) {//1
               println(" is running ${i++}")
          }
      }
       job.invokeOnCompletion {
           println("job is completion ${it}")
      }
       delay(50)
       job.cancel()
  }
}

会发现上面这个段代码并不能被取消,原因就是协程并没有感知到自己已经被取消了。这一点跟java thead interrupt机制类似,需要我们感知取消。感知取消的方式有



  • 可以使用CoroutineScope.isActive()的方法check是否已经被取消做出反应,代码一处可改成while (isActive)

  • 所有的suspend方法内部也会感知cancel。比如delay()方法就是一个suspend方法。


2.4 做好善后取消


协程取消后我们可能会做一些诸如回收资源的动作,但在一个已经处于取消状态的协程里再调用suspend方法就抛出CancellationException异常。此时我们要使用 withContext(NonCancellable) 做取消后的工作


private suspend fun handleCanceling() {
   coroutineScope {
       val job = launch {
           try {
               delay(100)//do Something
          } finally {
               withContext(NonCancellable) {
                   delay(100)
              }
          }
      }
       job.invokeOnCompletion {
           println("job is completion ${it}")
      }
       delay(50)
       job.cancel()
  }
}

另外,还有特别注意的一点是,被取消的协程会向外抛出异常如果使用try-catch捕获但不抛出异常CancellationException,会影响到异常的传播,也就破坏了协程的异常传播机制,具体下一节异常传播机制展开。


2.5 kotlin协程的父子结构


看下面这段代码,思考一个问题,2处字符串会被打印出出来吗,为什么?


private suspend fun parentChildStructTest() {
   coroutineScope {
      val job1 =  launch {
        val job2 =  launch(Job()) {//1
               delay(500)
               println("job2 is finish")//2
          }
           delay(100)
           this.cancel()
      }
  }
}

不会打印,不知道你有没有答对。


不是说好的,取消父协程的时候会取消掉其子协程吗?而且子协程里还调用了delay()方式,也会响应取消。问题的关键点在于,job1和job2的父子结构被破坏了。示例代码里1处传入了一个Job对象,此时job2的父层级已经变成了传入的job对象。我们稍加改造下,这里只是为了理解,不建议这么用,会发现job2可以被取消了。


private suspend fun parentChildStructTest() {
   coroutineScope {
       val job1 = launch {
           val j = Job()
           val job2 = launch(j) {
               delay(500)
               println("job2 is finish")
          }.invokeOnCompletion {
               println("job2 OnCompletion $it")
          }
           delay(100)
           j.cancel() //1
      }
  }
}

新协程的context的组成有两个公式


parentContext = scopeContext + AddionalContext(launch方法传入的context)

childContext = parentConxtext + job(新建)

1_zuX5Ozc2TwofXlmDajxpzg.webp(图来自[Roman Elizarov])



  • 新协程的context是【parent context】和【新建job】的相加操作而来。

  • 【parent context】是由父层级的context和传入的参数context相加操作而来。

  • 子协程的job会和父层级中context的job建立一个父子关系。


当我们使用coroutineScope.launch(Job()){}传入了一个job实例的时候,其实子协程的job和传入的job实例建立了父子结构,破坏了原本的父子结构。


3 异常传播机制


3.1 异常的传播


private suspend fun destroyCoroutineScope() {
   coroutineScope {
       launch {
           launch {
               delay(500)
               throw NullPointerException()
          }.invokeOnCompletion {
               println("job-1 invokeOnCompletion $it")
          }

           launch {
               delay(800)
          }.invokeOnCompletion {
               println("job-2 invokeOnCompletion $it")
          }
      }.invokeOnCompletion {
           println("job-parent completion $it")
      }
  }
}


  • 子job异常后,传播到父协程,父协程会取消到自己所有的子协程,然后再往上传播

  • 如果是一个取消异常(CancellationException)并不会被取消协程,父协程的处理器会忽略他。也就是在子协程上抛出异常之后,父协程接收到不会做处理。


3.2 监督作用域异常传播(Supervision)


基本表现:使用supervisorScope启动的子协程发生异常时,不影响父协程和兄弟协程。


private suspend fun supervisorJobTest() {
   supervisorScope {
       launch {
           delay(100)
           throw NullPointerException()
      }
       launch {
           delay(800)
           println("job 2 is running")
      }
  }
}

如上代码,supervisor范围内第一个job抛出异常后,并不会影响第二个job;把错误异常控制在范围内。



  • SupervisorCoroutine的子协程发生了异常之后不会影响父协程自身,也不会向上传播。

  • 如果 CoroutineContext没有设置CoroutineExceptionHandle,最终异常会传播到ExceptionHandler


但其他的结构化并发特性仍然存在



  • 当父协程取消,他的协程也被取消。

  • 子协程取消不影响父协程。

  • 父协程抛出异常,子协程也会被取消。

  • 父协程要等所有子协程完成后才结束。


简单的讲,监督协程具备单向传播的特性,即子协程的异常和取消不影响父协程,父协程的异常和取消会影响子协程


两种方式:



  • 构建CoroutineScope时传入SupervisorJob()

  • 使用supervisorScope{}产生


注意


监督协程中的每一个子作业应该通过异常处理机制处理自身的异常。如果不处理异常会被吞掉。


3.3 CoroutineExceptionHandler


用于捕获协程执行过程中未捕获的异常,被用来定义一个全局的异常处理器。



  • 不能恢复异常,只是打印、记录、重启应用。

  • 只能在【根作用域】或者【supervisorScope的直接子协程】启动协程是传入才生效。


举个例子


suspend fun coroutineExceptionHandlerTest() {
   supervisorScope {
       val handler = CoroutineExceptionHandler { _, _ -> println("handleException in coroutineExceptionHandler") }
       launch(handler) {
           delay(100)
           throw NullPointerException()
      }
  }
}  

3.4 浅看源码


主从作用域和协作作用域的表现区别上文已经讲到了,通常我们构建一个协程作用域两种方式


val scope = CoroutineScope(Job())
val supervisorJob = CoroutineScope(SupervisorJob())


  • CourotineScope()方法(没错这是个方法),通过传入Job()SupervisorJob生成的对象最终获得主从作用域和协同作用域。


  supervisorScope { scope -> xx }
 coroutineScope { scope ->xx }


  • 通过supervisorScope()或者coroutineScope()构建作用域。



private class SupervisorCoroutine<in T>(
   context: CoroutineContext,
   uCont: Continuation<T>
) : ScopeCoroutine<T>(context, uCont) {
   override fun childCancelled(cause: Throwable): Boolean = false
}

private class SupervisorJobImpl(parent: Job?) : JobImpl(parent) {
   override fun childCancelled(cause: Throwable): Boolean = false
}

两种作用域在代码上的区别是 fun childCancelled(cause: Throwable) 方法的实现不同,监督作用域直接返回fasle表示不处理子协程的错误异常。让其自己处理


//JobSupport
   private fun cancelParent(cause: Throwable): Boolean {
...
       return parent.childCancelled(cause) || isCancellation //1
  }

   private fun finalizeFinishingState(state: Finishing, proposedUpdate: Any?): Any? {
...
      val handled = cancelParent(finalException) || handleJobException(finalException)//2
      if (handled) (finalState as CompletedExceptionally).makeHandled()
    ...
  }

源代码中的核心逻辑,



  • 1处的parent.childCanceled的值的最终来源其实就是我们实现的childCancelled方法的返回值

  • 2处当我们是一个监督作用域起cancelParent的返回值为false,这种情况下代码就会执行后半句handleJobException(),这半句的内部其实最终是执行了我们设置的CoroutineExceptionHandler。

  • 2处cancelParent除了在我们监督作用域的时候返回fasle,在根协程下会返回fasle,这也就是为什么CoroutineExceptionHandler设置在根协程下生效的原因。


代码很多细节不展开有兴趣的自行研究。


4 异常传播需注意问题


4.1 supervisorScope的孙子协程


private suspend fun childChildSupervisorJob() {
   supervisorScope { // SupervisorCouroutine
       launch {  // ScopeCoroutine
          val job1 =  launch {
               delay(100)
               throw NullPointerException()
          }
          val job2 = launch {
               delay(800)
               println("job 2 is running")
          }.invokeOnCompletion {
               println("job2 is completion $it")
          }
      }
  }
}


  • 看上面这个例子job1抛出空指针异常后,job2会不会受影响。

  • 是正常的coroutineScope而非supervisorScope,因此supervisorScope的“孙子协程”不遵循互不影响原则


4.2 注意不要破坏父子结构


private suspend fun textSupervisorJob() {
   supervisorScope {
       launch(SupervisorJob()) {//1
           launch {
               delay(100)
               throw NullPointerException()
          }
           launch {
               delay(800)
               println("job 2 is running")
          }.invokeOnCompletion {
               println("job2 is completion $it")
          }
      }
  }
}


  • job1抛出异常也会影响job2,原因1处虽然传入了SupervisorJob,但是这个实例其实是作为父context的job传入的,真是job1和job2的parentContext还是job类型,而不是SupervisorJob。具体原理可以看2.5小节


5 关于async的误会


通常构建一个协程除了使用CoroutineScope.launch{}还会使用CoroutineScope.async{}。


经常看到这种说法,async方式启动的协程返回一个Deferred对象,当调用deffered的await()方法的时候才会抛出异常


private suspend fun asyncSample() {
   val h = CoroutineScope(CoroutineExceptionHandler { _, _ -> println("发生了异常") })
   val d = h.launch {
       async {
           delay(100)
           throw NullPointerException()
      }
       launch { //job2
           delay(500)
           println("job 2 is finish")
      }
  }.join()
}

这个例子没有调用await(),实际发现也会立马抛出异常,导致jo2都没执行完。跟我们认为的不一样。


实际情况是这样的:当async被用作构建根协程(由协程作用域直接管理的协程)或者监督作用域直接管理协程时,异常不会主动抛出,而是在调用.await()时抛出。其他情况不等待await就会抛出异常。


6 总结


本文梳理了Kotlin的协程的取消和异常传播处理机制。机制的设置总的来说是服务于结构化并发的。本文应该能让我们了解掌握以下问题才算合格



  • kotlin协程结构化并发的特性

  • 协程的context是怎么来的?怎么构成的?父协程的context和协程的parentContext是同一个概念吗?

  • kotlin的协程是怎么传播的?主从作用域监督作用域的区别?怎么实现

  • async方式启动的协程要await()的时候才抛出异常?

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

Java 中为什么要设计 throws 关键词,是故意的还是不小心

我们平时在写代码的时候经常会遇到这样的一种情况 提示说没有处理xxx异常 然后解决办法可以在外面加上try-catch,就像这样 所以我之前经常这样处理 //重新抛出 RuntimeException public class ThrowsDemo { ...
继续阅读 »

我们平时在写代码的时候经常会遇到这样的一种情况


throws.png


提示说没有处理xxx异常


然后解决办法可以在外面加上try-catch,就像这样


trycatch.png


所以我之前经常这样处理


//重新抛出 RuntimeException
public class ThrowsDemo {

public void demo4throws() {
try {
new ThrowsSample().sample4throws();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

//打印日志
@Slf4j
public class ThrowsDemo {

public void demo4throws() {
try {
new ThrowsSample().sample4throws();
} catch (IOException e) {
log.error("sample4throws", e);
}
}
}

//继续往外抛,但是需要每个方法都添加 throws
public class ThrowsDemo {

public void demo4throws() throws IOException {
new ThrowsSample().sample4throws();
}
}

但是我一直不明白


这个方法为什么不直接帮我做


反而要让我很多余的加上一步


我处理和它处理有什么区别吗?


而且变的好不美观


本来缩进就多,现在加个try-catch更是火上浇油


public class ThrowsDemo {

public void demo4throws() {
try {
if (xxx) {
try {
if (yyy) {

} else {

}
} catch (Throwable e) {
}
} else {

}
} catch (IOException e) {

}
}
}

上面的代码,就算里面没有业务,看起来也已经比较乱了,分不清哪个括号和哪个括号是一对


还有就是对Lambda很不友好


lambda.png


没有办法直接用::来优化代码,所以就变成了下面这样


lambdatry.png


本来看起来很简单很舒服的Lambda,现在又变得又臭又长


为什么会强制 try-catch


为什么我们平时写的方法不需要强制try-catch,而很多jdk中的方法却要呢


那是因为那些方法在方法的定义上添加了throws关键字,并且后面跟的异常不是RuntimeException


一旦你显式的添加了这个关键字在方法上,同时后面跟的异常不是RuntimeException,那么使用这个方法的时候就必须要显示的处理


比如使用try-catch或者是给调用这个方法的方法也添加throws以及对应的异常


throws 是用来干什么的


那么为什么要给方法添加throws关键字呢?


给方法添加throws关键字是为了表明这个方法可能会抛出哪些异常


就像一个风险告知


这样你在看到这个方法的定义的时候就一目了然了:这个方法可能会出现什么异常


为什么 RuntimeException 不强制 try-catch


那为什么RuntimeException不强制try-catch呢?


因为很多的RuntimeException都是因为程序的BUG而产生的


比如我们调用Integer.parseInt("A")会抛出NumberFormatException


当我们的代码中出现了这个异常,那么我们就需要修复这个异常


当我们修复了这个异常之后,就不会再抛出这个异常了,所以try-catch就没有必要了


当然像下面这种代码除外


public boolean isInteger(String s) {
try {
Integer.parseInt(s);
return true;
} catch (NumberFormatException e) {
return false;
}
}

这是我们利用这个异常来达成我们的需求,是有意为之的


而另外一些异常是属于没办法用代码解决的异常,比如IOException


我们在进行网络请求的时候就有可能抛出这类异常


因为网络可能会出现不稳定的情况,而我们对这个情况是无法干预的


所以我们需要提前考虑各种突发情况


强制try-catch相当于间接的保证了程序的健壮性


毕竟我们平时写代码,如果IDE没有提示异常处理,我们完全不会认为这个方法会抛出异常


我的代码怎么可能有问题.gif


我的代码怎么可能有问题!


不可能绝对不可能.gif


看来Java之父完全预判到了程序员的脑回路


throws 和 throw 的区别


java中还有一个关键词throw,和throws只有一个s的差别


throw是用来主动抛出一个异常


public class ThrowsDemo {

public void demo4throws() throws RuntimeException {
throw new RuntimeException();
}
}

两者完全是不同的功能,大家不要弄错了


什么场景用 throws


我们可以发现我们平时写代码的时候其实很少使用throws


因为当我们在开发业务的时候,所有的分支都已经确定了


比如网络请求出现异常的时候,我们常用的方式可能是打印日志,或是进行重试,把异常往外抛等等


所以我们没有那么有必要去使用throws这个关键字来说明异常信息


但是当我们没有办法确定异常要怎么处理的时候呢?


比如我在GitHub上维护了一个功能库,本身没有什么业务属性,主要就是对于一些复杂的功能做了相应的封装,提供给自己或别人使用(如果有兴趣可以看看我的库,顺便给Star,嘿嘿


对我来说,当我的方法中出现异常时,我是不清楚调用这个方法的人是想要怎么处理的


可能有的想要重试,有的想要打印日志,那么我干脆就往外抛,让调用方法的人自己去考虑,自己去处理


所以简单来说,如果方法主要是给别人用的最好用throws把异常往外抛,反之就是可加可不加


结束


很多时候你的不理解只是因为你还不够了解


作者:不够优雅
来源:juejin.cn/post/7204594495996100664
收起阅读 »

ProtoBuf 动态拆分Gradle Module

预期 当前安卓的所有proto都生成在一个module中,但是其实业务同学需要的并不是一个大杂烩, 只需要其中他们所关心的proto生成的类则足以。所以我们希望能将这样一个大杂烩的仓库打散,拆解成多个module。 buf.yaml Protobuf是Pr...
继续阅读 »

预期


当前安卓的所有proto都生成在一个module中,但是其实业务同学需要的并不是一个大杂烩, 只需要其中他们所关心的proto生成的类则足以。所以我们希望能将这样一个大杂烩的仓库打散,拆解成多个module


结构图.png


buf.yaml



Protobuf是Protocol Buffers的简称,它是Google公司开发的一种数据描述语言,用于描述一种轻便高效的结构化数据存储格式,并于2008年对外开源。Protobuf可以用于结构化数据串行化,或者说序列化。它的设计非常适用于在网络通讯中的数据载体,很适合做数据存储或 RPC 数据交换格式,它序列化出来的数据量少再加上以 K-V 的方式来存储数据,对消息的版本兼容性非常强,可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。开发者可以通过Protobuf附带的工具生成代码并实现将结构化数据序列化的功能。



在我司proto相关的都是由后端大佬们来维护的,然后这个协议仓库会被android/ios/后端/前端 依赖之后生成对应的代码,然后直接使用。


而proto文件中允许导入对于其他proto文件的依赖,所以这就导致了想要把几个proto转化成一个java-library工程,还需要考虑依赖问题。所以由
我们的后端来定义了一个buf.yaml的数据格式。


version: v1
name: buf.xxx.co/xxx/xxxxxx
deps:
- buf.xxxxx.co/google/protobuf
build:
excludes:
- setting
breaking:
use:
- FILE
lint:
use:
- DEFAULT

name代表了这个工程的名字,deps则表示了他依赖的proto的工程名。基于这份yaml内容,我们就可以大概确定一个proto工程编译需要的基础条件。然后我们只需要一个工具或者插件来帮助我们生成对应的工程就够了。


模板工程


现在我们基本已经有了一个单一的proto工程的输入模型了,其中包含工程名依赖的工程还有对应文件夹下的proto文件。然后我们就可以基于这部分输入的模型,生成出第一个模板工程。


plugins {
id 'java-library'
id 'org.jetbrains.kotlin.jvm'
id 'com.google.protobuf'
}


java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

sourceSets {
def dirs = new ArrayList<String>()
dirs.add("src/main/proto")
main.proto.srcDirs = dirs
}

protobuf {
protoc {
if (System.getProperty("os.arch").compareTo("aarch64") == 0) {
artifact = "com.google.protobuf:protoc:$version_protobuf_protoc:osx-x86_64"
} else {
artifact = "com.google.protobuf:protoc:$version_protobuf_protoc"
}
}
plugins {
grpc {
if (System.getProperty("os.arch").compareTo("aarch64") == 0) {
artifact = 'io.grpc:protoc-gen-grpc-java:1.36.1:osx-x86_64'
} else {
artifact = 'io.grpc:protoc-gen-grpc-java:1.36.1'
}
}
}
generateProtoTasks {
all().each { task ->
task.generateDescriptorSet = true
task.builtins {
// In most cases you don't need the full Java output
// if you use the lite output.
java {

}

}
task.plugins {
grpc { option 'lite' }
}
}
}
}
afterEvaluate {
project.tasks.findByName("compileJava").dependsOn(tasks.findByName("generateProto"))
project.tasks.findByName("compileKotlin").dependsOn(tasks.findByName("generateProto"))
}
dependencies {
implementation "org.glassfish:javax.annotation:10.0-b28"
def grpcJava = '1.36.1'
compileOnly "io.grpc:grpc-protobuf-lite:${grpcJava}"
compileOnly "io.grpc:grpc-stub:${grpcJava}"
compileOnly "io.grpc:grpc-core:${grpcJava}"
File file = new File(projectDir, "depend.txt")
if (!file.exists()) {
return
}
def lines = file.readLines()
if (lines.isEmpty()) {
return
}
lines.forEach {
logger.lifecycle("project:" + name + " implementation: " + it)
implementation(it)
}
}

如果需要将proto编译成java代码,就需要依赖于com.google.protobuf插件,依赖于上面的build.gradle基本就可以将一个proto输入编译成一个jar工程。


另外我们需要把所有的proto文件拷贝到这个壳工程的src/main/proto文件夹下,最后我们会将buf.yaml中的name: buf.xxx.co/xxx/xxxxxx/xxx/xxxxxx转化成工程名,去除掉一些无法识别的字符。


我们生成的模板工程如下:


image.png


其中proto.version会记录proto内的gitsha值还有文件的lastModified时间,如果输入发生变更则会重新进行一次文件拷贝操作,避免重复覆盖的风险。


input.txt则包含了所有proto文件路径,方便我们进行开发调试。


deps 转化


由于proto之间存在依赖,没有依赖则会导致无法将proto转化成java。所以这里我讲buf.yaml中读取出的deps转化成了一个depend.txt.


com.xxxx.api:google-protobuf:7.7.7

depend.txt内会逐行写入当前模块的依赖,我们会对name进行一次转化,变成一个可读的gradle工程名。其中7.7.7的版本只是一个缺省而已,并没有实际的价值。


多线程操作


这里我们出现了一点点的性能问题, 如果可以gradle插件中尽量多使用点多线程,尤其是这种需要io的操作中。


这里我通过ForkJoinPool,这个是ExecutorService的实现类。其中submit方法中会返回一个ForkJoinTask,我们可以将获取gitsha值和lastModified放在这个中。之后把所有的ForkJoinTask放到一个数组中。


fun await() {
forkJoins.forEach {
it.join()
}
}

然后最后暴露一个await方法,来做到所有的获取方法完成之后再继续向下执行。


另外则就是壳module的生成,我们也放在了子线程内执行。我们这次使用了线程池的invokeAll方法。


protoFileWalk.hashMap.forEach { (_, pbBufYaml) ->
callables.add(Callable<Void> {
val root = FileUtils.getRootProjectDir(settings.gradle)
try {
val file = pbBufYaml.copyLib(File(root, "bapi"))
projects[pbBufYaml.projectName()] = file.absolutePath ?: ""
} catch (e: Exception) {
e.printStackTrace()
e.message.log()
}
null
})
}
executor.invokeAll(callables)

这里有个面试经常出现的考点,多线程操作Hashmap,之后我在测试环节随机出现了生成工程和include不匹配的问题。所以最后我更换了ConcurrentHashMap就没有出现这个问题了。


加载壳Module


这部分就和源码编译插件基本是一样的写法。


projects.forEach { (s, file) ->
settings.include(":${s}")
settings.project(":${s}").projectDir = File(file)
}

把工程插入settings 即可。


结尾


最终结果大概就是原先一个Module,现在被拆分成100+的Module,而且基于buf.yaml 文件动态生成,基本符合第一期需求。


这部分方案这样也就大概完成了一半,剩下的一半我们需要逐一把生层业务的依赖进行一次变更,这样就可以做到依赖最小化,然后也可以去除掉一部分无用的代码块。


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

拥有思想,你就是高级、资深、专家、架构师

当然要想成为高级工程师或者架构师,光看书是不行的,书本上来的东西都是工具型编程的体现,何为工具型编程呢? 就是说可以依据书本、网络等渠道就能完成的编程就是工具型编程,那怎么解决呢? 为什么要提升编程思想 这个问题我想大家都有答案,编程思想就是一个程序员的灵魂...
继续阅读 »

当然要想成为高级工程师或者架构师,光看书是不行的,书本上来的东西都是工具型编程的体现,何为工具型编程呢?


就是说可以依据书本、网络等渠道就能完成的编程就是工具型编程,那怎么解决呢?


为什么要提升编程思想



这个问题我想大家都有答案,编程思想就是一个程序员的灵魂,没有灵魂的程序员,只配ctrl + C/V.



专业一点来讲,提升编程思想的重要性在于它能够帮助开发者更好地解决问题、提高效率、减少错误,并提高代码的可读性、可维护性和可扩展性,而这些点位就是成为一个高级Android工程师或者架构师必不可少的技能,也是每一个程序员应该具备的技能。在国外,很多面试更看重的是学习能力和编程思想,其实也是,一个10年的经验丰富的程序员学习一门新的语言或者技术如同探囊取物,对于一个公司、一个团队、一个业务成本来讲,有这样一个人是最经济的。更具体来讲:



  1. 解决问题能力:良好的编程思想能够帮助开发者更好地理解问题,设计出高效、可靠、可扩展的解决方案。

  2. 代码质量提升:优秀的编程思想可以帮助开发者写出易于阅读、易于维护的代码,并使其更加健壮、可靠、可测试。

  3. 工作效率提高:合理的编程思想可以使开发者更加高效地编写代码,并降低代码调试和修复的时间。

  4. 技术实力提升:良好的编程思想可以使开发者更加深入地理解编程语言和计算机科学原理,并在实践中掌握更多的技能和技巧。

  5. 职业发展:具备良好编程思想的开发者在技术水平和职业发展方面具有更好的竞争力和前景。


良好的编程思想可以帮助开发者更好地解决问题、提高效率、提高代码质量和可维护性,并在职业发展中具有更好的前景和竞争力,这也就成了了中、高、架构等分级程序员的区别之分。


如何提升自己的编程思想



  1. 练习算法和数据结构:熟悉算法和数据结构可以帮助你更好地理解和解决问题,优化你的代码并提高你的代码质量。

  2. 阅读源代码:阅读其他优秀项目的源代码可以帮助你学习其他开发人员的编程思想,理解他们是如何解决问题的,进而提高自己的编程思维。

  3. 学习设计模式:设计模式是一种常用的编程思想,它可以帮助你更好地组织你的代码,提高代码的可维护性和可扩展性。

  4. 参与开源项目:参与开源项目可以帮助你学习其他开发人员的编程思想,理解他们是如何解决问题的,同时也可以帮助你获取更多的开发经验和知识。

  5. 持续学习:跟上Android开发的最新技术和趋势可以让你更好地了解开发环境和市场需求,并提升自己的编程思想。

  6. 经常review自己的代码:经常review自己的代码可以帮助你发现自己代码中的问题并及时改进,同时也可以帮助你学习其他开发人员的编程思想。


接下来,我们对这些步骤的项目进行分析和说明,


练习算法和数据结构



熟悉算法和数据结构可以帮助你更好地理解和解决问题,优化你的代码并提高你的代码质量, Android 这种移动平台,性能要求是非常高的,但是他的机型众多,系统不一,所以我们应该从编程角度就减少不必要的麻烦。怎么练习呢?




  1. 选择适合自己的算法练习平台:例如LeetCode、HackerRank、Codeforces等。这些平台都提供了大量的算法题目,可以帮助你提高算法水平。

  2. 学习基础算法:如排序、查找、树、图等算法,这些算法是其他算法的基础,掌握好基础算法对于提高算法能力非常有帮助。

  3. 练习算法的具体类型:例如贪心算法、动态规划、分治算法等,这些算法在实际开发中非常常见,掌握好这些算法可以让你更好地解决实际问题。

  4. 尝试实现算法:通过手写实现一些经典算法,你可以更好地理解算法的思想和实现方式,并加深对算法的理解。

  5. 参与算法竞赛:参与算法竞赛可以帮助你提高算法能力,同时也可以认识到其他优秀的算法工程师。


很多人在开发过程中公司是不会要求有算法参与的,特别是在Android端,也很少有人在开发中精心设计一款算法用于Android业务,Android的数据量级都在可控范围内,但是优秀的程序员不是应公司要求而编程的,我们因该在面对数据的时候,自然而然的想到算法,想到时间复杂度、空间复杂度的问题算法-时间复杂度 这是之前我在公司分享过的一篇文章,大家可以参考一下,基本涵盖了时间复杂度及在不同场景中的计算方式以及在特殊场景下的计算概念。


举几个例子,怎么将算法运用到平时的开发中呢?



  1. 优化算法复杂度:在实际开发中,我们常常需要处理大量数据,如果算法复杂度高,就容易导致程序运行缓慢。因此,优化算法复杂度是非常重要的。比如,在ListView或RecyclerView中使用二分查找算法可以快速查找到指定位置的数据。

  2. 应用动态规划算法:动态规划算法可以用于解决一些经典问题,例如最长公共子序列、背包问题等。在实际开发中,我们也可以应用动态规划算法解决一些问题,例如路径规划、字符串匹配等。

  3. 应用贪心算法:贪心算法是一种可以获得近似最优解的算法,可以用于一些优化问题。在Android开发中,例如布局优化、图片压缩等方面,也可以应用贪心算法来达到优化的效果。

  4. 应用其他常用算法:除了上述算法外,其他常用算法也可以应用于Android开发中,例如图像处理算法、机器学习算法等。对于一些比较复杂的问题,我们也可以引入其他算法来解决。


反正就是学之前要理解对应算法的大致用途,在类似场景中,直接尝试搬套,先在伪代码中演算其结果,结果正趋向时果断使用。


阅读源码



阅读其他优秀项目的源代码可以帮助你学习其他开发人员的编程思想,理解他们是如何解决问题的,进而提高自己的编程思维



前边不是说了,工具型编程可以不用看吗,是的,但是阅读源码不是查看工具,而是提升你的编程思想,借鉴思想是人类进化的最主要体现之一,多个思想的碰撞也能造就更成功的事件。那怎么阅读源码呢?这个每个人都有自己的方法,阅读后要善于变通,运用到自己的项目中,我是这么做的。



  1. 选择合适的开源项目:选择一个合适的开源项目非常重要。你可以选择一些知名度比较高的项目,例如Retrofit、OkHttp、Glide等,这些项目通常质量比较高,也有一定的文档和教程。

  2. 确定目标和问题:在阅读源码前,你需要明确自己的目标和问题。例如,你想了解某个库的实现原理,或者你想解决一个具体的问题。

  3. 仔细阅读源码:在阅读源码时,需要仔细阅读每一个类、方法、变量的注释,了解每一个细节。同时也需要了解项目的整体结构和运行流程。

  4. 了解技术背景和思路:在阅读源码时,你需要了解作者的技术背景和思路,了解为什么选择了某种实现方式,这样可以更好地理解代码。

  5. 实践运用:通过阅读源码,你可以学到许多好的编程思想和技巧,你需要将这些思想和技巧运用到自己的开发中,并且尝试创新,将这些思想和技巧进一步发扬光大。


阅读源码需要持之以恒,需要不断地实践和思考,才能真正学习到他人的编程思想,并将其运用到自己的开发中。


学习设计模式



设计模式本就是编程思想的总结,是先辈们的经验绘制的利刃,它可以帮助你更好地组织你的代码,提高代码的可维护性和可扩展性。




  1. 学习设计模式的基本概念:学习设计模式前,需要了解面向对象编程的基本概念,例如继承、多态、接口等。同时也需要掌握一些基本的设计原则,例如单一职责原则、开闭原则等。

  2. 学习设计模式的分类和应用场景:学习设计模式时,需要了解每个设计模式的分类和应用场景,例如创建型模式、结构型模式、行为型模式等。你需要了解每个模式的特点,以及何时应该选择使用它们。

  3. 练习设计模式的实现:练习实现设计模式是学习设计模式的关键。你可以使用一些例子,例如写一个简单的计算器、写一个文件读写程序等,通过练习来加深对设计模式的理解。

  4. 将设计模式应用到实际项目中:将设计模式应用到实际项目中是学习设计模式的最终目标。你需要从项目需求出发,结合实际场景选择合适的设计模式。举例来说,下面是一些在Android开发中常用的设计模式:

    • 单例模式:用于创建全局唯一的实例对象,例如Application类和数据库操作类等。

    • 适配器模式:用于将一个类的接口转换成客户端期望的另一个接口,例如ListView的Adapter。

    • 工厂模式:用于创建对象,例如Glide中的RequestManager和RequestBuilder等。

    • 观察者模式:用于实现事件机制,例如Android中的广播机制、LiveData等。




学习设计模式需要不断练习和思考,通过不断地练习和实践,才能真正将设计模式灵活运用到自己的项目中。


参与开源或者尝试商业SDK开发



参与开源,很多同学是没有时间的,并且国内缺少很多开发团队项目,都是以公司或者团队模式开源的,个人想在直接参与比较困难,所以有条件的同学可以参与商业SDK的开发,
商业SDK比较特殊的点在于受众不同,但是他所涉及的编程思想较为复杂,会涉及到很多设计模式和架构模式。



比如,Android商业SDK开发涉及到很多方面,下面列举一些常见的考虑点以及经常使用的架构和设计模式:



  1. 安全性:SDK需要考虑用户隐私保护和数据安全,确保不会泄露敏感信息。

  2. 稳定性:SDK需要保证在不同的环境下运行稳定,不会因为异常情况而崩溃。

  3. 可扩展性:SDK需要考虑未来的扩展和升级,能够方便地添加新的功能和支持更多的设备和系统版本。

  4. 性能:SDK需要保证在各种设备和网络条件下,响应速度和性能都有足够的表现。

  5. 兼容性:SDK需要考虑在不同版本的Android系统和各种厂商的设备上,都能够正常运行。


经常用到的架构和设计模式包括:



  1. MVVM架构:MVVM是Model-View-ViewModel的简称,通过将视图、模型和视图模型分离,可以实现更好的代码组织和更容易的测试。

  2. 单例模式:单例模式是一种创建全局唯一对象的模式,在SDK中常用于创建全局的配置、管理器等。

  3. 工厂模式:工厂模式是一种创建对象的模式,SDK中常用于创建和管理复杂的对象。

  4. 观察者模式:观察者模式是一种事件机制,SDK中常用于通知应用程序有新的数据或事件到达。

  5. 适配器模式:适配器模式用于将一个接口转换成另一个接口,SDK中常用于将SDK提供的接口适配成应用程序需要的接口。

  6. 策略模式:策略模式是一种动态地改变对象的行为的模式,SDK中常用于在运行时选择不同的算法实现。


Android商业SDK开发需要综合考虑多个方面,选择适合的架构和设计模式能够提高代码质量、开发效率和维护性。


了解市场、了解业务,不要埋头敲代码



掌握最新的市场需求,比如网络框架的发展历程,从开始的HttpURLConnection的自己封装使用,到okhttp,再到retrofit, 再后来的结构协程、Flow等等,其实核心没有变化就是网络请求,但是,从高内聚到,逐层解耦,变的是其编程的思想。



CodeReview



可以参考该文章,此文章描述了CodeReview 的流程和方法,值得借鉴,CodeReview 是一个天然的提升自己业务需求的过程,
zhuanlan.zhihu.com/p/604492247



经常写开发文档



设计和编写开发文档是一个很重要的工作,它不仅能够提升自己的编程思想,也能够帮助团队提高协作效率和减少沟通成本.



如果要求你在开发一个需求前对着墙或者对着人讲一遍开发思路,你可能讲不出来,也不好意思,且没有留存,开发文档可以满足你,当你写开发文档时,你记录了你的对整个需求的开发,以及你编程的功底,日益累积后,你的思想自然会水涨船高,因为你写开发文档的过程就是在锻炼自己,比如我在前公司开发国际化适配时写的文档(当然只是我的粗鄙想法国际化ICU4J 适配及SDK设计,我会先分析问题,为什么?然后设计,并且会思考可能遇到的问题,也一并解决了。时间长了,设计模式、思想也会得到提升。


当然,也要分场景去设计,按需求去设计,可以采纳以下建议:
设计和编写开发文档是一个很重要的工作,它不仅能够提升自己的编程思想,也能够帮助团队提高协作效率和减少沟通成本。下面是一些关于如何设计一份好的开发文档的建议:



  1. 明确文档的目标和受众:在编写文档之前,需要明确文档的目标和受众,确定文档需要包含的内容和写作风格

  2. 使用清晰的语言和示例:使用简洁、清晰的语言描述问题,使用示例代码和截图帮助读者理解问题。

  3. 分层次组织文档:文档应该按照逻辑和功能分层次组织,每一层都有明确的目标和内容。

  4. 使用图表和图形化工具:图表和图形化工具能够有效地展示复杂的概念和数据,帮助读者更好地理解文档内容。

  5. 定期更新和维护文档:开发文档需要定期更新和维护,以反映最新的代码和功能。


通过设计一份好的开发文档,可以提升自己的编程思想,使得代码更加清晰和易于维护,同时也能够提高团队的协作效率和代码质量。


向上有组织的反馈



经常向领导有组织的汇报开发进度、问题、结果,不仅可以提升编程思想,还能够提高自己的工作效率和沟通能力



首先,向领导汇报开发进度、问题和结果,可以让自己更加清晰地了解项目的进展情况和任务的优先级,帮助自己更好地掌控项目进度和管理时间。


其次,通过向领导汇报问题,可以促使自己更加深入地了解问题的本质和解决方案,同时也能够得到领导的反馈和指导,帮助自己更快地解决问题。


最后,向领导汇报开发结果,可以帮助自己更好地总结经验和教训,促进自己的成长和提高编程思想。同时,也能够让领导更清晰地了解自己的工作成果,提高领导对自己的认可和评价。


向领导有组织地汇报开发进度、问题和结果,不仅能够提升编程思想,还能够提高工作效率和沟通能力,促进自己的成长和发展。


总结



  1. 编程思想的提升



  • 学习数据结构和算法,尤其是常见的算法类型和实际应用

  • 阅读优秀开源代码,理解代码架构和设计思想,学习开发最佳实践

  • 学习设计模式,尤其是常见的设计模式和应用场景



  1. 实际项目开发中的应用



  • 通过代码重构,优化代码质量和可维护性

  • 运用算法解决实际问题,例如性能优化、数据处理、机器学习等

  • 运用设计模式解决实际问题,例如代码复用、扩展性、灵活性等



  1. 沟通与协作能力的提高



  • 与团队成员保持良好的沟通,及时反馈问题和进展情况

  • 向领导有组织地汇报开发进度、问题和结果,以提高工作效率和沟通能力

  • 参加技术社区活动,交流分享经验和知识,提高团队的技术实力和协作能力


以上是这些方面的核心点,当然每个方面都有很多细节需要关注和完善,需要持续学习和实践。


附件



以下是我之前为项目解决老项目的图片框架问题而设计的文档,因名称原因只能图片展示



首先,交代了背景,存在的问题


image.png


针对问题,提出设计思想


image.png


开始设计,从物理结构到架构


image.png


image.png


作者:狼窝山下的青年
来源:juejin.cn/post/7200944831114264637
收起阅读 »

Flutter webview_flutter滑动监听

前言 当需要使用webview时,常用的插件有webview_flutter、flutter_webview_plugin、flutter_inappwebview等。 项目中已经使用的是官方的webview_flutter 。 问题: 总的来说webview...
继续阅读 »

前言


当需要使用webview时,常用的插件有webview_flutterflutter_webview_pluginflutter_inappwebview等。


项目中已经使用的是官方的webview_flutter


问题:


总的来说webview_flutter可以满足大部分的webview相关需求,直到有一天产品小可爱说网页在顶部时,顶部标题栏背景为白色,之后网页滑动到一定距离的时候需要变成透明。我一听,so easy啊,WebViewController里有相关的方法监听:


image.png


我只需要加个Listener手势监听,在move里判断getScrollY()的距离就OK了。


Listener(
child: WebView(initialUrl: "http://www.baidu.com"),
onPointerMove: (PointerMoveEvent event) {
webViewController?.getScrollY().then((value) {
if (value > 200) {
//标题栏背景变白色
} else {
////标题栏背景变透明
}
});
},
);

然而....


image.png


手势滑动是有惯性的,当快速滑动的时候,手指离开屏幕,会因为惯性继续滑动一段距离。而由于我们监听的是手势操作,当手指离开屏幕,手势监听也就从onPointerMove走到了onPointerUp,也就没办法再监听webViewController?.getScrollY(),惯性滚动的距离就没有监听到,那此时所展现的效果自然就不理想了:


快速下滑屏幕的时候,假设手指离开屏幕的时候监听到getScrollY()=500,但由于惯性网页继续滚动,直到滚回网页顶部。但由于手指离开了屏幕没有继续监听getScrollY(),getScrollY()的最后取值就是500,可此时网页已经惯性滚回到顶部了,此时标题栏依然是白色,而不是变回透明。


image.png


解决


遇到问题,咱就解决问题。
反复翻看了webview_flutter的代码,确实没有这个监听,然后联想到即便是在做Android原生开发的时候,webview也是没有提供公开的方法或设置让我们对网页的滚动进行监听的,如果要监听那就需要在原生webview的onScrollChanged增加监听。


修改FlutterWebViewClient,增加方法


void getOffsetY(int offsetY) {
Map<String, Object> args = new HashMap<>();
args.put("offsetY", offsetY);
methodChannel.invokeMethod("getOffsetY", args);
}

image.png


修改FlutterWebView


@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
@SuppressWarnings("unchecked")
FlutterWebView(
final Context context,
BinaryMessenger messenger,
int id,
Map<String, Object> params,
View containerView) {

///增加监听
if (webView instanceof CustomWebView){
((CustomWebView)webView).setOnScrollChangedCallback(new CustomWebView.OnScrollChangedCallback() {
@Override
public void onScroll(int dx, int dy) {
flutterWebViewClient.getOffsetY(dy);
Log.d("onScroll",dy+"");
}
});
}

image.png


platform_interface.dart里修改WebViewPlatformCallbacksHandler


增加供外部调用的getOffsetY()


void getOffsetY(int offsetY);

image.png


webview_method_channel.dart修改MethodChannelWebViewPlatform


_onMethodCall里新增getOffsetY类型


case 'getOffsetY':
_platformCallbacksHandler.getOffsetY(call.arguments['offsetY']);
return null;

webview_flutter.dart修改WebView


新增getOffsetY


image.png


_PlatformCallbacksHandler里新增


@override
void getOffsetY(int offsetY){
if (_widget.getOffsetY != null) {
_widget.getOffsetY!(offsetY);
}
}

image.png


至此,就成功增加了webview的滑动监听


image.png


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

我发现了 Android 指纹认证 Api 内存泄漏

我发现了 Android 指纹认证 Api 内存泄漏 目前很多市面上的手机基本都有指纹登陆功能。Google 也提供了调用相关功能 API,安全类的App 也基本都在使用。接下来就一起捋一捋今天的主角 BiometricPrompt 先说问题,使用Biome...
继续阅读 »

我发现了 Android 指纹认证 Api 内存泄漏


目前很多市面上的手机基本都有指纹登陆功能。Google 也提供了调用相关功能 API,安全类的App 也基本都在使用。接下来就一起捋一捋今天的主角 BiometricPrompt


先说问题,使用BiometricPrompt 会造成内存泄漏,目前该问题试了 Android 11 到 13 都发生,而且没有什么好的办法。目前想到的最好的方法是漏的少一点。当然谁有好的办法欢迎留言。


问题再现


先看动画


在这里插入图片描述


动画中操作如下



  1. MainAcitivity 跳转到 SecondActivity

  2. SecondActivity 调用 BiometricPrompt 三次

  3. 从SecondActivity 返回到 MainAcitivity


以下是使用 BiometricPrompt 的代码


public fun showBiometricPromptDialog() {
val keyguardManager = getSystemService(
Context.KEYGUARD_SERVICE
) as KeyguardManager;

if (keyguardManager.isKeyguardSecure) {
var biometricPromptBuild = BiometricPrompt.Builder(this).apply {// this is SecondActivity
setTitle("verify")
setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL or BiometricManager.Authenticators.BIOMETRIC_WEAK)
}
val biometricPromp = biometricPromptBuild.build()
biometricPromp.authenticate(CancellationSignal(), mExecutor, object :
BiometricPrompt.AuthenticationCallback() {

})
}
else {
Log.d("TAG", "showLockScreen: isKeyguardSecure is false");
}
}

以上逻辑 biometricPromp 是局部变量,应该没有问题才对。


内存泄漏如下


在这里插入图片描述
可以看到每启动一次生物认证,创建的 BiometricPrompt 都不会被回收。


规避方案:


修改方案也简单


方案一:



  1. biometricPromp 改为全局变量。

  2. this 改为 applicationContext


方案一存在的问题,SecondActivity 可能频繁创建,所以 biometricPromp 还会存在多个实例。


方案二(目前想到的最优方案):



  1. biometricPromp 改为单例

  2. this 改为 applicationContext


修改后,App memory 中只存在一个 biometricPromp ,且没有 Activity 被泄漏。


想到这里,应该会觉得奇怪,biometricPromp 为什么不会被回收?提供的 API 都看过了,没有发现什么方法可以解决这个问题。直觉告诉我这个可能是系统问题,下来分析下BiometricPrompt 吧。


BiometricPrompt 源码分析


在这里插入图片描述


App 相关信息通过 BiometricPrompt 传递到 System 进程,System 进程再通知 SystemUI 显示认证界面。


App 信息传递到 System 进程,应该会使用 Binder。这个查找 BiometricPrompt 使用哪些 Binder。


private final IBiometricServiceReceiver mBiometricServiceReceiver =
new IBiometricServiceReceiver.Stub() {

......
}

源码中发现 IBiometricServiceReceiver 比较可疑,IBiometricServiceReceiver 是匿名内部类,内部是持有 BiometricPrompt 对象的引用。


接下来看下 System Server 进程信息(注:系统是 UserDebug 的手机,才可以查看,买的手机版本是不支持的)


在这里插入图片描述



😂 App 使用优化后(方案二)App 只存在一个 IBiometricServiceReceiver ,而 system 进程中存在三个 IBiometricServiceReceiver 的 binder proxy。 每次启动 BiometricPrompt 都会创建一个。这个就不解释为什么会出现三个binder proxy,感兴趣可以看下面推荐的文章。GC root 是 AuthSession。

再看下 AuthSession 的实例数


在这里插入图片描述


果然 AuthSession 也存在三个。


在这里插入图片描述


这里有个知识点,binder 也是有生命周期的,三个 Proxy 这篇文章也是解释了的。有兴趣的可以了看下。


Binder | 对象的生命周期


一开始,我以为 AuthSession 没有被置空,看下代码,发现 AOSP 的代码,还是比较严谨的,有置空的操作。


细心的同学发现,上图中 AuthSession 没有被任何对象引用,AuthSession 就是 GC Root,哈哈哈。


问题解密


一个实例什么情况可以作为GC Root,有兴趣的同学,可以自行百度,这里就不卖关子了,直接说问题吧。


Binder.linkToDeath()


public void linkToDeath(@NonNull DeathRecipient recipient, int flags) {
}

需要传递 IBinder.DeathRecipient ,这个 DeathRecipient 会被作为 GC root。当调用 unlinkToDeath(@NonNull DeathRecipient recipient, int flags),GC root 才被收回。


AuthSession 初始化的时候,会调用 IBiometricServiceReceiver .linkToDeath。


public final class AuthSession implements IBinder.DeathRecipient {
AuthSession(@NonNull Context context,
......
@NonNull IBiometricServiceReceiver clientReceiver,
......
) {
Slog.d(TAG, "Creating AuthSession with: " + preAuthInfo);
......
try {
mClientReceiver.asBinder().linkToDeath(this, 0 /* flags */);//this 变成 GC root
} catch (RemoteException e) {
Slog.w(TAG, "Unable to link to death");
}

setSensorsToStateUnknown();
}
}

Jni 中 通过 env->NewGlobalRef(object),告诉虚拟机 AuthSession 是 GC Root。


core/jni/android_util_Binder.cpp

static void android_os_BinderProxy_linkToDeath(JNIEnv* env, jobject obj,
jobject recipient, jint flags)
// throws RemoteException
{
if (recipient == NULL) {
jniThrowNullPointerException(env, NULL);
return;
}

BinderProxyNativeData *nd = getBPNativeData(env, obj);
IBinder* target = nd->mObject.get();

LOGDEATH("linkToDeath: binder=%p recipient=%p\n", target, recipient);

if (!target->localBinder()) {
DeathRecipientList* list = nd->mOrgue.get();
sp<JavaDeathRecipient> jdr = new JavaDeathRecipient(env, recipient, list);//java 中 DeathRecipient 会被封装为 JavaDeathRecipient
status_t err = target->linkToDeath(jdr, NULL, flags);
if (err != NO_ERROR) {
// Failure adding the death recipient, so clear its reference
// now.
jdr->clearReference();
signalExceptionForError(env, obj, err, true /*canThrowRemoteException*/);
}
}
}

JavaDeathRecipient(JNIEnv* env, jobject object, const sp<DeathRecipientList>& list)
: mVM(jnienv_to_javavm(env)), mObject(env->NewGlobalRef(object)),// object -> DeathRecipient 变为 GC root
mObjectWeak(NULL), mList(list)
{
// These objects manage their own lifetimes so are responsible for final bookkeeping.
// The list holds a strong reference to this object.
LOGDEATH("Adding JDR %p to DRL %p", this, list.get());
list->add(this);

gNumDeathRefsCreated.fetch_add(1, std::memory_order_relaxed);
gcIfManyNewRefs(env);
}

unlinkToDeath 最终会在 Jni 中 通过 env->DeleteGlobalRef(mObject),告诉虚拟机 AuthSession 不是GC root。


virtual ~JavaDeathRecipient()
{
//ALOGI("Removing death ref: recipient=%p\n", mObject);
gNumDeathRefsDeleted.fetch_add(1, std::memory_order_relaxed);
JNIEnv* env = javavm_to_jnienv(mVM);
if (mObject != NULL) {
env->DeleteGlobalRef(mObject);// object -> DeathRecipient GC root 被撤销
} else {
env->DeleteWeakGlobalRef(mObjectWeak);
}
}

解决方式


AuthSession 置空的时候调用 IBiometricServiceReceiver 的 unlinkToDeath 方法。


总结


以上梳理的其实就是 Binder 的造成的内存泄漏。


问题严重性来看,也不算什么大问题,因为调用 BiometricPrompt 的进程被杀,system 进程相关实例也就回收释放了。一般 app 也不太可能出现,常驻进程,而且还频繁调用手机认证的。


这里主要介绍了一种容易被忽略的内存泄漏,Binder.linktoDeath()。
Google issuetracker


参考资料


Binder | 对象的生命周期


作者:Jingle_zhang
来源:juejin.cn/post/7202066794299129914
收起阅读 »

简单回顾位运算

前言 位运算其实是一个比较常用的操作,有的人可能说,并没有啊,我好像基本就没怎么用过位运算,但如果你经常看源码,你就会发现源码里面有很多位运算的操作,而且这些操作我个人觉得确实是有很意思,所以位运算很重要,平时用不多,也需要经常去回顾复习。 因为临时写的,有些...
继续阅读 »

前言


位运算其实是一个比较常用的操作,有的人可能说,并没有啊,我好像基本就没怎么用过位运算,但如果你经常看源码,你就会发现源码里面有很多位运算的操作,而且这些操作我个人觉得确实是有很意思,所以位运算很重要,平时用不多,也需要经常去回顾复习。


因为临时写的,有些源码的操作我不太记得是出自哪里了,如果以后碰到了,会在评论区进行补充。比如会有一些取反计算再取反回来的操作,比如左移和右移,我现在不太记得是出自哪里了,反正比较经典的。我的个人可能就记得用位运算来表示状态,因为我会经常用这个。


位运算基础


简单来回顾一下基础的计算,位运算会分为一元运算和二元运算。


一元有左移<<,右移>>,无符号右移动>>>和取反~


左移是是什么?比如 0010 左移动一位就变成 0100 (注意这里是二进制的表示),右移动就是 0100 变成 0010。 当然没这么简单啦,二进制也有表示正负值的标志位,右移之后,左边会补标志位的数。而无符号右移动是左边补0。也就是说正数的右移和无符号右移动的结果相同,而负数就不同了。这样说应该好理解吧?那思考一下为什么没有无符号左移


还是不好懂,没关系,我们讲慢些,假如8转成2进制是00001000

而-8就是先取反码,反码就是取反,所以8的反码是11110111,然后再用反码取补码,补码就是+1,所以这里得到补码1111000。即-8转成二进制是11111000

先看看我们对11111000取反码,得00000111,再取补码得00001000,看到从-8到8也是同样的操作。


然后我们看右移一位和无符号右移一位的效果。

-8的二进制11111000,右移一位是11111100,这是多少啊?我们可以用上面的计算看它的正数是多少,反码00000011,补码00000100,这是4吧,所以11111100是-4。

同理看无符号右移,得01111100,反码10000011,补码10000100,这是多少?2的7次方+4,所以是不同的。


取反就好理解了,取反就是逻辑非,比如0010取反就是1101


二元有与&&、或||、异或^


这些都好理解吧,与运算 1010 && 1001 = 1000 , 或运算 1010 || 1001 = 1010 , 异或 1010 ^ 1001 = 0011 ,这没什么好讲的,很基础。


位运算很简单?


一看,哎哟,真的简单,就这?1分钟学会了。学会?那会用吗?


没关系,我们来看一题算法:


一个整型数组里除了两个数字只出现一次,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。例如:


输入:


[1,4,1,6]

返回值:


[4,6]

明确说了,这题是用位运算,但是怎么做?


开发中位运算的使用


这个我因为临时写的,android源码里确实是有些比较经典的位运算使用场景,我现在不完全记得,只能先列举几个,其它的如果以后想起来,会在评论区中做补充。


用来表示状态

位运算可以用来表示状态,我之前也写过一篇文章 juejin.cn/post/715547… 差不多也是这个意思。


比如window的flags,看看它的定义


public static final int FLAG_LAYOUT_IN_SCREEN   = 0x00000100;
public static final int FLAG_LAYOUT_NO_LIMITS = 0x00000200;
public static final int FLAG_FULLSCREEN = 0x00000400;
public static final int FLAG_FORCE_NOT_FULLSCREEN = 0x00000800;
......

那他这样做有什么好处,他能一个变量表示多个维度的状态。比如FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_NO_LIMITS|FLAG_FULLSCREEN|FLAG_FORCE_NOT_FULLSCREEN就是 1111 (二进制表示)


如果你要判断这个window是不是同时设置了这4个flag,要怎么判断,就直接if(flags == 15)啊,多简单


但是如果你用多个变量存flag要怎么判断, if(isScreen && isNoLimits && usFullscreen && isForceNot),这样写就很难看,很不方便,我window的flag多着呢,难道你要排火车?


数组扩容

来看看ArrayList的扩容源码


private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}

这里的int newCapacity = oldCapacity + (oldCapacity >> 1);就是阔人操作,这个右移是什么?看不懂也没关系,自己套一个数字进去算就知道了,是除2吧,那ArrayList的扩容就是旧容量的一半。


看着简单是吧?那有没有想过一个问题,他写这个代码,为什么不写oldCapacity/2,而写oldCapacity >> 1


那既然右移一位 >> 1 是除2,那左移一位 << 1 是什么操作呢?是什么计算呢?


总结


我这里确实是很久没有用位运算,所以需要复习一下。这个东西对开发来说很重要,比如你是开发应用层的,你觉得这个是底层用到的,你用不到,并不是这样。就拿那个表示状态的来说,自从我看到源码用这一招之后,只要有合适的场景,我也会这样用。


不管是做数学运算,还是逻辑运算,位运算都能适用,它是很简单就能学会,但是学会用,那就是另外一回事,当然不是说看完我这篇文章就开始瞎用,能在合适的场合去使用,那效果十分的好,用不上也没关系,至少要有个意识,这样不管在看源码还是其它时候,都是能帮到你的。


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

Flutter动态化调研实践

一,前言 1,什么是动态化? 目前移动端应用的版本更新, 最常见的方式是定期发版,无论是安卓还是iOS,都需要提交新的安装包到应用市场进行审核。审核通过后,用户在应用市场进行App的下载更新。 而动态化, 就是不依赖更新程序安装包, 就能动态实时更新页面的技术...
继续阅读 »

一,前言


1,什么是动态化?


目前移动端应用的版本更新, 最常见的方式是定期发版,无论是安卓还是iOS,都需要提交新的安装包到应用市场进行审核。审核通过后,用户在应用市场进行App的下载更新。


而动态化, 就是不依赖更新程序安装包, 就能动态实时更新页面的技术。


2,动态化的必要性


为什么需要动态化技术呢? 因为上述定期发版更新应用的方式存在一些问题,比如:



  1. 审核周期长, 且可能审核不通过。 周期长导致发版本不够灵活, 紧急的业务需求不能及时上线。

  2. 线上出现急需修复的bug时,需要较长修复周期,影响用户体验。

  3. 安装包过大, 动辄几十兆几百兆的应用升级可能会让用户比较抗拒。

  4. 即使上线了,也无法达到全部用户升级, 服务端存在兼容多版本App的问题。


面对这些问题,如果能实现app增量、无感知更新,实现功能同步。无论是对公司还是用户都是非常重要的需求,能实现app动态化更新就显得非常重要,能很好的解决以上问题:



  1. 随时实现功能升级,不存在应用市场长时间审核和拒绝上线问题,达到业务需求快速上线的目的。

  2. 线上bug可以实时修复,提高用户体验。

  3. 可以减小发版功能包体积,只需要替换新增功能即可。

  4. 功能保持一致,类似网页一样,发版后用户同步更新,不存在旧版本兼容问题。


接下来,我们就来分析一下,目前业内主要的Flutter动态化更新方式。


二,动态化方案调研


在Flutter实践层面,简单来说分为三个流派:




  • 方案一:JavaScript是最好的语言(🤣碰瓷PHP)
    主要思路:利用Flutter做渲染,开发使用js,逻辑层通过v8/jscore解释运行。代表框架是腾讯的MXFlutter。这个框架是开源的,大写的👍。




  • 方案三:布局,逻辑,一把梭


    主要思路:与方案一最主要的区别是,逻辑层也是使用dart,增加了一层语法解析和运行时。有一个代表,美团的MTFlutter,然而没有开源动向,无从考察更多。




  • 方案二:DSL + JS


    主要思路:基于模板实现动态化,主要布局层采用Dart转DSL的方式,逻辑层使用JS。代表框架是58同城开源的Fair




MXFlutter


项目简介



MXFlutter 是一套基于 JavaScript 的 Flutter 框架,可以用极其类似 Dart 的开发方式,通过编写 JavaScript 代码,来开发 Flutter 应用,或者使用 mxjsbuilder 编译器,把现有Flutter 工程编译为JS,运行在 mxflutter 之上。



核心思想



核心思路是把 Flutter 的渲染逻辑中的三棵树中的第一棵,放到 JavaScript 中生成。用 JavaScript 完整实现了 Flutter 控件层封装,可以使用 JavaScript,用极其类似 Dart 的开发方式,开发Flutter应用,利用JavaScript版的轻量级Flutter Runtime,生成UI描述,传递给Dart层的UI引擎,UI引擎把UI描述生产真正的 Flutter 控件。



MxFlutter 目前已经停止维护,具体请看MXFlutter
MxFlutter通过JavaScript编写Dart,加载线上js文件,通过引擎在运行时转化并显示,从而达到动态化效果。 官方在0.7.0版本开始接入TypeScript,引入npm生态,优化了js开发的成本,向前端生态进一步靠拢。
很遗憾,在对比各大厂的方案时,发现MxFlutter的性价比极低,学习成本也高,而且又抛弃Dart生态。开发及维护成本都很高。


MTFlutter


项目简介



美团的MTFlutter团队flap项目采用的静态生产DSL方案,通过对Dart语言注解,保证平台一致性。实现了动态下发与解释的逻辑页面一体化的 Flutter 动态化方案。Flap 的出现让 Flutter 动态化和包大小这两个短板得到了一定程度的弥补,促进了 Flutter 生态的发展。



核心思想



通过静态生产 DSL+Runtime 解释运行的思路,实现了动态下发与解释的逻辑页面一体化的 Flutter 动态化方案,建设了一套 Flap 生态体系,涵盖了开发、发布、测试、运维各阶段。



布局和逻辑层都使用Dart, 增加了一层语法解析和运行时。然而没有开源动向,无从考察更多。


Fair


项目简介



Fair是为Flutter设计的动态化框架,通过Fair Compiler工具对原生Dart源文件的自动转化,使项目获得动态更新Widget Tree和State的能力。


创建Fair的目标是支持不发版(Android、iOS、Web)的情况下,通过业务bundle和JS下发实现更新,方式类似于React Native。与Flutter Fair集成后,您可以快速发布新的页面,而无需等待应用的下一个发布日期。Fair提供了标准的Widget,它可以被用作一个新的动态页面或作为现有Flutter页面的一部分,诸如运营位的排版/样式修改,整页面替换,局部替换等都可以使用。



核心思想



Fair是58自研的的动态化框架,通过Fair Compiler工具对原生Dart源文件的自动转化,使项目获得动态更新Widget Tree和State的能力。



pic_d3WbXUd1d1V9d1WcXU37U7U75aXdd17b


三,方案对比


经过上述三个方案的调研,我们来大概对比一下上述框架


方案开源方核心思想优点缺点
MXFlutter腾讯用js编写Dart,动态拉取js脚本目前相对最完整的Flutter使用JS开发方案采用js方式编写Dart,维护困难
MTFlutter美团布局,逻辑都使用Dart,增加语法解析和运行时支持布局动态化和逻辑动态化未开源
Fair58通过bundle和js实现热更新支持布局动态化和逻辑动态化开源社区活跃, 开发工具丰富部分语法不支持

可以看到, MXFlutter需要使用js写Dart, 官方已经停止更新,而这种方式我们不能接受, MTFlutter目前未开源,无从继续研究。 接下来着重看一下 Fair


四,Fair接入过程


1,添加依赖


推荐使用pub形式引入


# add Fair dependency
dependencies:
fair: 2.7.0

# add compiler dependency
dev_dependencies:
build_runner: ^2.0.0
fair_compiler: ^1.2.0

# switch "fair_version" according to the local Flutter SDK version
dependency_overrides:
fair_version: 3.0.0

Flutter版本切换


通过切换 flutter_version 版本进行版本兼容。例如,将本机切换为 flutter 2.0.6 后,Fair 需要同步切换


# switch to another stable flutter version
dependency_overrides:
fair_version: 2.0.6

2,使用 Fair


在App中接入Fair步骤如下:


将 FairApp 添加为需要动态化部分的顶级节点

常见做法是作为 App 的根节点,如果不是全局采用也可以作为子页面的根节点


void main() {
WidgetsFlutterBinding.ensureInitialized();

FairApp.runApplication(
_getApp(),
plugins: {
},
);
}

dynamic _getApp() => FairApp(
modules: {
},
delegate: {
},
child: MaterialApp(
home: FairWidget(
name: 'DynamicWidget',
path: 'assets/bundle/lib_src_page_dynamic_widget.fair.json',
data: {"fairProps": json.encode({})}),
),
);

添加动态组件

每一个动态组件由一个FairWidget表示。


FairWidget(
name: 'DynamicWidget',
path: 'assets/bundle/lib_src_page_dynamic_widget.fair.json',
data: {"fairProps": json.encode({})}),

根据不同场景诉求,FairWidget可以混合和使用



  1. 可以作为不同组件混合使用

  2. 一般作为一个全屏页面

  3. 支持嵌套使用,即可以局部嵌套在普通Widget下,也可以嵌套在另一个FairWidget下


五,Fair接入体验


1,fork,下载工程


将官方Github工程fork到自己仓库后, 下载工程。使用官方提供的 test_case/best_ui_templates工程体验fair的体验。


2, 执行 pub get


在 best_ui_templates工程中,执行 pub get命令获取依赖。


3,开发业务


接下来正式开始开发流程。 把一个页面改写为 用Fair 编写:



  1. 创建需要动态化的 componnet, 并添加 @FairPatch() 注解。添加上注解后,在Fair生成产物时,会把此Component build生成 FairWidget加载的产物。


image-20221116173528472


2, 执行 Fair工具链插件的命令生成产物, 如图:


<u>image-20221116173837910</u>


3, 最终生成的产物,拷贝到 assets/bundle目录下(配置config.json后,会自动拷贝)


<u><u>image-20221116182132104</u></u>


4, 看效果, 下图为使用 Fair 改造后的页面:



Screenshot_2022_1116_172859
Screenshot_2022_1116_192940

六,Fair优势


1,社区活跃度高


官方对Fair维护力度大,版本更新较快,问题修复及时,活跃的开发者社区氛围。


使得开发者在开发Fair过程中遇到的问题, 能够及时反馈给官方, 并能得到快速的帮助和解决。


2, 一份代码,灵活使用


Fair的区别于MTFlutter和MXFlutter这2种动态化方案,Fair能让同一份代码在Flutter原生和动态之间随意切换。在开发跟版本需求时,使用原生代码发布,以此持续保持Flutter的性能优势;而在热更新场景可以通过下发动态文件来达到动态更新Widget的目的。使用方式更加灵活。


3,配套开发工具丰富


Faircli配套工具链

官方为了让开发者快速上手,降低接入门槛, 解决在接入过程中的痛点。 Fair团队开发了Faircli配套工具链,主要包含三个部分:



  • 工程创建:快速搭建Fair载体工程及动态化工程。

  • 模板代码:提供页面及组件模板。

  • 本地热更新:线下开发使用,实现开发阶段快速预览Fair动态化功能。


在安装了工具链提供的dart命令行工具及AS插件后, 通过创建模板, 构建产物, 本地启服务,体验热更新功能,开发者可以轻松接入并体验Fair。


Fair语法检测插件

官方为了让开发者在Fair开发过程中,出现不正确或者不支持的语法问题。 开发了配套插件去提示用户使用Fair语法糖。


查看以下示例:


1,build方法下if的代码检测,及提示引导信息


44b58320-e608-420f-854f-799b5bf03cf5image


2,点击more action 或者 AS代码提示快捷键


41094a86-2aea-43e6-b7f0-69aef1c653c0image


3,根据提示点击替换


image.png


通过插件,在编写fair过程中,可以快速识别并解决不支持的语法问题。 提高开发Fair效率。


Fair Web代码编辑器

Fair其中一个方向是在线动态化平台,即在网页中编辑dart代码,在线预览Flutter效果和Fair动态化效果,并且发布Fair动态化产物。


通过在Fair Web代码编辑器,开发者可以在没有复杂的IDE配置的情况下,在网页端开发Fair并预览。 这无疑是降低了接入成本, 为开发者可以快速体验Fair提供了非常便捷的方式。


七,总结


通过近期对各大互联网公司在Flutter动态化方向上的探究方案。 发现这些方案都还没有达到成熟阶段,想在实际业务上落地, 还得看各团队后期的维护力度和开发投入程度。


MXFlutter使用js编写Dart的方式, 抛弃了原本Flutter的开发模式, 导致开发成本大,以及后续维护成本也大,官方已停止维护。


MTFlutter采用布局,逻辑都是使用Dart, 通过静态生产 DSL+Runtime 解释运行的思路,解决布局和逻辑的动态化,然而并没有开源计划,无从深入研究。


Fair通过Fair Compiler工具对原生Dart源文件的自动转化,使项目获得动态更新Widget Tree和State的能力。目前官方维护力度较大, 社区活跃,并且有比较全面的Fair生态工具。 期待 Fair 团队可以解决在开发Fair过程中一些体验问题,如语法支持不全等, 让Fair成为真正能够让开发者可以快速接入,能够达到和正常开发Flutter接近的体验。 为广大Flutter开发人员解决动态化的痛点。


支持我们


欢迎大家使用 Fair,也欢迎大家为我们点亮star

Github地址:github.com/wuba/fair

Fair官网:fair.58.com/


欢迎贡献


通过Issue提交问题,贡献代码请提交Pull Request,管理员将对代码进行审核。


作者:王猛猛
来源:juejin.cn/post/7174978087879671865
收起阅读 »

全网最优雅安卓控件可见性检测

引子 view.setOnClickListener { // 当控件被点击时触发的逻辑 } 正是因为 View 对控件点击采用了策略模式,才使得监听任何控件的点击事件变得易如反掌。 我有一个愿望。。。 如果 View 能有一个可见性监听该多好啊! view...
继续阅读 »

引子


view.setOnClickListener { // 当控件被点击时触发的逻辑 }

正是因为 View 对控件点击采用了策略模式,才使得监听任何控件的点击事件变得易如反掌。


我有一个愿望。。。


如果 View 能有一个可见性监听该多好啊!


view.setOnVisibilityChangeListener { isVisible: Boolean ->   }

系统并未提供这个方法。。。


但业务上有可见性监听的需要,比如曝光埋点。当某控件可见时,上报XXX。


数据分析同学经常抱怨曝光数据不准确,有的场景曝光多报了,有的场景曝光少报了。。。


开发同学看到曝光埋点也很头痛,不同场景的曝光检测有不同的方法,缺乏统一的可见性检测入口,存在一定重复开发。


本文就试图为单个控件以及列表项的可见性提供统一的检测入口。


控件的可见性受到诸多因素的影响,下面是影响控件可见性的十大因素:



  1. 手机电源开关

  2. Home 键

  3. 动态替换的 Fragment 遮挡了原有控件

  4. ScrollView, NestedScrollView 的滚动

  5. ViewPager, ViewPager2 的滚动

  6. RecyclerView 的滚动

  7. 被 Dialog 遮挡

  8. Activity 切换

  9. 同一 Activity 中 Fragment 的切换

  10. 手动调用 View.setVisibility(View.GONE)

  11. 被输入法遮盖


能否把这所有的情况都通过一个回调方法表达?目标是通过一个 View 的扩展方法完成上述所有情况的检测,并将可见性回调给上层,形如:


fun View.onVisibilityChange(block: (view: View, isVisible: Boolean) -> Unit) {}

若能实现就极大化简了上层可见性检测的复杂度,只需要如下代码就能实现任意控件的曝光上报埋点:


view.onVisibilityChange { view, isVisible ->
if(isVisible) { // 曝光埋点 }
else {}
}

控件全局可见性检测


可见性检测分为两步:



  1. 捕获时机:调用检测算法检测控件可见性的时机。

  2. 检测算法:描述如何检测控件是否对用户可见。


拿“手动调用 View.setVisibility(View.GONE)”举例,得先捕获 View Visibility 发生变化的时机,并在此刻检测控件的可见性。


下面是View.setVisibility()的源码:


// android.view.View.java
public void setVisibility(@Visibility int visibility) {
setFlags(visibility, VISIBILITY_MASK);
}

系统并未在该方法中提供类似于回调的接口,即一个 View 的实例无法通过回调的方式捕获到 visibility 变化的时机。


难道通过自定义 View,然后重写 setVisibility() 方法?


这个做法接入成本太高且不具备通用性。


除了“手动调用 View.setVisibility(View.GONE)”,剩下的影响可见性的因素大多都可找到对应回调。难道得在fun View.onVisibilityChange()中对每个因素逐个添加回调吗?


这样实现太过复杂了,而且也不具备通用性,假设有例外情况,fun View.onVisibilityChange()的实现就得修改。


上面列出的十种影响控件可见性的因素都是现象,不同的现象背后可能对应相同的本质。


经过深挖,上述现象的本质可被收敛为下面四个:



  1. 控件全局重绘

  2. 控件全局滚动

  3. 控件全局焦点变化

  4. 容器控件新增子控件


下面就针对这四个本质编程。


捕获全局重绘时机


系统提供了ViewTreeObserver


public final class ViewTreeObserver {
public void addOnGlobalLayoutListener(OnGlobalLayoutListener listener) {
checkIsAlive();
if (mOnGlobalLayoutListeners == null) {
mOnGlobalLayoutListeners = new CopyOnWriteArray();
}
mOnGlobalLayoutListeners.add(listener);
}
}

ViewTreeObserver 是一个全局的 View 树变更观察者,它提供了一系列全局的监听器,全局重绘即是其中OnGlobalLayoutListener


public interface OnGlobalLayoutListener {
public void onGlobalLayout();
}

当 View 树发生变化需要重绘的时候,就会触发该回调。


调用 View.setVisibility(View.GONE) 之所以能将控件隐藏,正是因为整个 View 树触发了一次重绘。(任何一次微小的重绘都是从 View 树的树根自顶向下的遍历并触发每一个控件的重绘,不需要重绘的控件会跳过,关于 Adroid 绘制机制的分析可以点击Android自定义控件 | View绘制原理(画多大?)


在可见性检测扩展方法中捕获第一个时机:


fun View.onVisibilityChange(block: (view: View, isVisible: Boolean) -> Unit) {
viewTreeObserver.addOnGlobalLayoutListener {}
}

其中viewTreeObserver是 View 的方法:


// android.view.View.java
public ViewTreeObserver getViewTreeObserver() {
if (mAttachInfo != null) {
return mAttachInfo.mTreeObserver;
}
if (mFloatingTreeObserver == null) {
mFloatingTreeObserver = new ViewTreeObserver(mContext);
}
return mFloatingTreeObserver;
}

getViewTreeObserver() 用于返回当前 View 所在 View 树的观察者。


全局重绘其实覆盖了上述的两个场景:



  1. 同一 Activity 中 Fragment 的切换

  2. 手动调用 View.setVisibility(View.GONE)

  3. 被输入法覆盖


这两个场景都会发生 View 树的重绘。


捕获全局滚动时机



  1. ScrollView, NestedScrollView 的滚动

  2. ViewPager, ViewPager2 的滚动

  3. RecyclerView 的滚动


上述三个时机的共同特点是“发生了滚动”。


每个可滚动的容器控件都提供了各自滚动的监听


// android.view.ScrollView.java
public interface OnScrollChangeListener {
void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY);
}

// androidx.viewpager2.widget.ViewPager2.java
public abstract static class OnPageChangeCallback {
public void onPageScrolled(int position, float positionOffset, @Px int positionOffsetPixels) {}
public void onPageSelected(int position) {}
public void onPageScrollStateChanged(@ScrollState int state) {}
}

// androidx.recyclerview.widget.RecyclerView.java
public abstract static class OnScrollListener {
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {}
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {}
}

难道要针对不同的滚动控件设置不同的滚动监听器?


这样可见性检测就和控件耦合了,不具有通用性,也愧对View.onVisibilityChange()这个名字。


还好又在ViewTreeObserver中找到了全局的滚动监听:


public final class ViewTreeObserver {
public void addOnScrollChangedListener(OnScrollChangedListener listener) {
checkIsAlive();

if (mOnScrollChangedListeners == null) {
mOnScrollChangedListeners = new CopyOnWriteArray();
}

mOnScrollChangedListeners.add(listener);
}
}

public interface OnScrollChangedListener {
public void onScrollChanged();
}

在可见性检测扩展方法中捕获第二个时机:


fun View.onVisibilityChange(block: (view: View, isVisible: Boolean) -> Unit) {
viewTreeObserver.addOnGlobalLayoutListener {}
viewTreeObserver.addOnScrollChangedListener {}
}

捕获全局焦点变化时机


下面这些 case 都是焦点发生了变化:



  1. 手机电源开关

  2. Home 键

  3. 被 Dialog 遮挡

  4. Activity 切换


同样借助于 ViewTreeObserver 可以捕获到焦点变化的时机。


到目前为止,全局可见性扩展方法中已经监听了三种时机,分别是全局重绘、全局滚动、全局焦点变化:


fun View.onVisibilityChange(block: (view: View, isVisible: Boolean) -> Unit) {
viewTreeObserver.addOnGlobalLayoutListener {}
viewTreeObserver.addOnScrollChangedListener {}
viewTreeObserver.addOnWindowFocusChangeListener {}
}

捕获新增子控件时机


最后一个 case 是最复杂的:动态替换的 Fragment 遮挡了原有控件。


该场景如下图所示:
1668323952343.gif


界面中有一个底边栏,其中包含各种 tab 标签,点击其中的标签会以 Fragment 的形式从底部弹出。此时,底边栏各 tab 从可见变为不可见,当点击返回时,又从不可见变为可见。


一开始的思路是“从被遮挡的 View 本身出发”,看看某个 View 被遮挡后,其本身的属性是否会发生变化?


View 内部以is开头的方法如下所示:


微信截图_20221113152433.png


我把其中名字看上去可能和被遮挡有关联的方法值全都打印出来了,然后触发 gif 中的场景,观察这些值在触发前后是否会发生变化。


几十个属性,一一比对,在看花眼之前,log 告诉我,被遮挡之后,这些都没有发生任何变化。。。。


绝望。。。但还不死心,继续寻找其他方法:


微信截图_20221113152922.png


我又找了 View 内部所有has开头的方法,也把其中看上去和被遮挡有关的方法全打印出来了。。。你猜结果怎么着?依然是徒劳。。。。


我开始质疑出发点是否正确。。。此时一声雷鸣劈醒了我。


视图只可能了解其自身以及其下层视图的情况,它无法得知它的平级甚至是父亲的绘制情况。而 gif 中的场景,即是在底边栏的同级有一个 Fragment 的容器。而且当视图被其他层级的控件遮挡时,整个绘制体系也不必通知那个被遮挡的视图,否则多低效啊(我yy的,若有大佬知道内情,欢迎留言指点一二。)


经过这层思考之后,我跳出了被遮挡的那个视图,转而去 Fragment 的容器哪里寻求解决方案。


Fragment 要被添加到 Activity 必须提供一个容器控件,容器控件提供了一个回调用于监听子控件被添加:


// android.view.ViewGroup.java
public void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) {
mOnHierarchyChangeListener = listener;
}

public interface OnHierarchyChangeListener {
void onChildViewAdded(View parent, View child);
void onChildViewRemoved(View parent, View child);
}

为了监听 Fragment 被添加的这个瞬间,得为可见性检测扩展方法添加一个参数:


fun View.onVisibilityChange(
viewGroup:
ViewGroup? = null, // 容器
block: (
view: View, isVisible: Boolean) -> Unit
)
{ }

其中 viewGroup 表示 Fragment 的容器控件。


既然 Fragment 的添加也是往 View 树中插入子控件,那 View 树必定会重绘,可以在全局重绘回调中进行分类讨论,下面是伪代码:


fun View.onVisibilityChange(
viewGroup:
ViewGroup? = null,
block: (
view: View, isVisible: Boolean) -> Unit
)
{
var viewAdded = false
// View 树重绘时机
viewTreeObserver.addOnGlobalLayoutListener {
if(viewAdded){
// 检测新插入控件是否遮挡当前控件
}
else {
// 检测当前控件是否出现在屏幕中
}
}
// 监听子控件插入
viewGroup?.setOnHierarchyChangeListener(object : OnHierarchyChangeListener {
override fun onChildViewAdded(parent: View?, child: View?) {
viewAdded = true
}
}
override fun onChildViewRemoved(parent: View?, child: View?) {
viewAdded = false
}
})
}

子控件的插入回调总是先于 View 树重绘回调。所以先在插入时置标志位viewAdded = true,以便在重绘回调中做分类讨论。(因为检测子控件遮挡和是否出现在屏幕中是两种不同的检测方案)


可见性检测算法


检测控件的可见性的算法是:“判断控件的矩形区域是否和屏幕有交集”


为此新增扩展属性:


val View.isInScreen: Boolean
get() = ViewCompat.isAttachedToWindow(this) && visibility == View.VISIBLE && getLocalVisibleRect(Rect())

val 类名.属性名: 属性类型这样的语法用于为类的实例添加一个扩展属性,它并不是真地给类新增了一个成员变量,而是在类的外部新增属性值的获取方法。


当前新增的属性是 val 类型的,即常量,所以只需要为其定义个 get() 方法来表达如何获取它的值。


View 是否在屏幕中由三个表达式共同决定。



  1. 先通过 ViewCompat.isAttachedToWindow(this) 判断控件是否依附于窗口。

  2. 再通过 visibility == View.VISIBLE 判断视图是否可见。

  3. 最后调用getLocalVisibleRect()判断它的矩形相对于屏幕是否可见:


// android.view.View.java
public final boolean getLocalVisibleRect(Rect r) {
final Point offset = mAttachInfo != null ? mAttachInfo.mPoint : new Point();
if (getGlobalVisibleRect(r, offset)) {
r.offset(-offset.x, -offset.y);
return true;
}
return false;
}

该方法会先获取控件相对于屏幕的矩形区域并存放在传入的 Rect 参数中,然后再将其偏移到控件坐标系。如果矩形区域为空,则返回 false 表示不在屏幕中,否则为 true。


刚才捕获的那一系列时机,有可能会被多次触发。为了只将可见性发生变化的事件回调给上层,得做一次过滤:


val KEY_VISIBILITY = "KEY_VISIBILITY".hashCode()

val checkVisibility = {
// 获取上一次可见性
val lastVisibility = getTag(KEY_VISIBILITY) as? Boolean
// 获取当前可见性
val isInScreen = this.isInScreen() && visibility == View.VISIBLE
// 无上一次可见性,表示第一次检测
if (lastVisibility == null) {
if (isInScreen) {
// 回调可见性回调给上层
block(this, true)
// 更新可见性
setTag(KEY_VISIBILITY, true)
}
}
// 当前可见性和上次不同
else if (lastVisibility != isInScreen) {
// 回调可见性给上层
block(this, isInScreen)
// 更新可见性
setTag(KEY_VISIBILITY, isInScreen)
}
}

过滤重复事件的方案是记录上一次可见性(记录在 View 的 tag 中),如果这一次可见性检测结果和上一次相同则不回调给上层。


将可见性检测定义为一个 lambda,这样就可以在捕获不同时机时复用。


以下是完整的可见性检测代码:


fun View.onVisibilityChange(
viewGroups:
List<ViewGroup> = emptyList()
, // 会被插入 Fragment 的容器集合
needScrollListener: Boolean = true,
block: (view: View, isVisible: Boolean) -> Unit
) {
val KEY_VISIBILITY = "KEY_VISIBILITY".hashCode()
val KEY_HAS_LISTENER = "KEY_HAS_LISTENER".hashCode()
// 若当前控件已监听可见性,则返回
if (getTag(KEY_HAS_LISTENER) == true) return

// 检测可见性
val checkVisibility = {
// 获取上一次可见性
val lastVisibility = getTag(KEY_VISIBILITY) as? Boolean
// 判断控件是否出现在屏幕中
val isInScreen = this.isInScreen
// 首次可见性变更
if (lastVisibility == null) {
if (isInScreen) {
block(this, true)
setTag(KEY_VISIBILITY, true)
}
}
// 非首次可见性变更
else if (lastVisibility != isInScreen) {
block(this, isInScreen)
setTag(KEY_VISIBILITY, isInScreen)
}
}

// 全局重绘监听器
class LayoutListener : ViewTreeObserver.OnGlobalLayoutListener {
// 标记位用于区别是否是遮挡case
var addedView: View? = null
override fun onGlobalLayout() {
// 遮挡 case
if (addedView != null) {
// 插入视图矩形区域
val addedRect = Rect().also { addedView?.getGlobalVisibleRect(it) }
// 当前视图矩形区域
val rect = Rect().also { this@onVisibilityChange.getGlobalVisibleRect(it) }
// 如果插入视图矩形区域包含当前视图矩形区域,则视为当前控件不可见
if (addedRect.contains(rect)) {
block(this@onVisibilityChange, false)
setTag(KEY_VISIBILITY, false)
} else {
block(this@onVisibilityChange, true)
setTag(KEY_VISIBILITY, true)
}
}
// 非遮挡 case
else {
checkVisibility()
}
}
}

val layoutListener = LayoutListener()
// 编辑容器监听其插入视图时机
viewGroups.forEachIndexed { index, viewGroup ->
viewGroup.setOnHierarchyChangeListener(object : ViewGroup.OnHierarchyChangeListener {
override fun onChildViewAdded(parent: View?, child: View?) {
// 当控件插入,则置标记位
layoutListener.addedView = child
}

override fun onChildViewRemoved(parent: View?, child: View?) {
// 当控件移除,则置标记位
layoutListener.addedView = null
}
})
}
viewTreeObserver.addOnGlobalLayoutListener(layoutListener)
// 全局滚动监听器
var scrollListener:ViewTreeObserver.OnScrollChangedListener? = null
if (needScrollListener) {
scrollListener = ViewTreeObserver.OnScrollChangedListener { checkVisibility() }
viewTreeObserver.addOnScrollChangedListener(scrollListener)
}
// 全局焦点变化监听器
val focusChangeListener = ViewTreeObserver.OnWindowFocusChangeListener { hasFocus ->
val lastVisibility = getTag(KEY_VISIBILITY) as? Boolean
val isInScreen = this.isInScreen
if (hasFocus) {
if (lastVisibility != isInScreen) {
block(this, isInScreen)
setTag(KEY_VISIBILITY, isInScreen)
}
} else {
if (lastVisibility == true) {
block(this, false)
setTag(KEY_VISIBILITY, false)
}
}
}
viewTreeObserver.addOnWindowFocusChangeListener(focusChangeListener)
// 为避免内存泄漏,当视图被移出的同时反注册监听器
addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View?) {
}

override fun onViewDetachedFromWindow(v: View?) {
v ?: return
// 有时候 View detach 后,还会执行全局重绘,为此退后反注册
post {
try {
v.viewTreeObserver.removeOnGlobalLayoutListener(layoutListener)
} catch (_: java.lang.Exception) {
v.viewTreeObserver.removeGlobalOnLayoutListener(layoutListener)
}
v.viewTreeObserver.removeOnWindowFocusChangeListener(focusChangeListener)
if(scrollListener !=null) v.viewTreeObserver.removeOnScrollChangedListener(scrollListener)
viewGroups.forEach { it.setOnHierarchyChangeListener(null) }
}
removeOnAttachStateChangeListener(this)
}
})
// 标记已设置监听器
setTag(KEY_HAS_LISTENER, true)
}

该控件可见性检测方法,最大的用处在于检测 Fragment 的可见性。详细讲解可以点击 页面曝光难点分析及应对方案


Talk is cheap,show me the code


上述源码可以在这里找到。


推荐阅读


业务代码参数透传满天飞?(一)


业务代码参数透传满天飞?(二)


全网最优雅安卓控件可见性检测


全网最优雅安卓列表项可见性检测


页面曝光难点分析及应对方案


你的代码太啰嗦了 | 这么多对象名?


你的代码太啰嗦了 | 这么多方法调用?


作者:唐子玄
来源:juejin.cn/post/7165427955902971918
收起阅读 »

一个艰难就业的23年应届生的2022年

自我介绍 我的家乡是浙江-宁波-余姚,是一名就读于一所位于宁波-慈溪(学校:笑死,这就我一所大学,你直接报我名字得了)的双非独立学院的软件工程专业的23年应届生,7到10月有在南京实习,现在是孤身一人在杭州实习的社恐前端实习生,前端练习时长一年半,擅长唱、跳、...
继续阅读 »

自我介绍


我的家乡是浙江-宁波-余姚,是一名就读于一所位于宁波-慈溪(学校:笑死,这就我一所大学,你直接报我名字得了)的双非独立学院的软件工程专业的23年应届生,7到10月有在南京实习,现在是孤身一人在杭州实习的社恐前端实习生,前端练习时长一年半,擅长唱、跳、rap... 还只擅长Vue的渣渣前端程序猿,有兴趣可以关注我的公众号程序猿青空,23年开始我会时不时分享各种优秀文章、学习资源、学习课程,探索初期,还请多多关照。这篇文章会是我公众号的第一篇文章,主要对我这一年来的经历做一个简单的流水账总结,涉及到恋爱、租房、学习、工作等各方面内容,希望这份经验对你也能有所帮助。


学习


大二下半年的时候分流,自主报名到了我们学校的产业学院——企业和学校联合创办的培养应用型人才的学院。我文科相当薄弱,埋头考研会相当痛苦,也很清楚自己做不来官僚主义那一套,公职也不是适合我的职业(没错我对公职有偏见),很坚定就业这条路。因为还没有毕业,我的身份归根结底就是一个双非下流本科的一名大学生,为了避免自己毕业即失业,看当时产业学院的宣传也不错就去了。


事实上因为产业学院刚创办不久,而且并不是所有人来到这里都是为了就业的,也有可能是为了学分、助学金等其他方面的原因,课程设计、师资力量、同学质量等各方面都良莠不齐、鱼龙混杂。每门课程的期末大作业基本都是一个小项目,大三一年里两个期末都有为了大作业通宵的几天,再加上1500💰凑活过的生活费,死贵的电费和食堂伙食费,在这里学习和生活有时候还蛮辛苦的。好在我很清楚自己应该做什么,天赋不够,努力来凑,本来起跑线就低,更应该比别人卷一点。当然我也不是那种能够没日没夜卷的人(👀),关注了鱼皮,加入了他的知识星球,在星球天天学习健身(没错我还健身💪)打卡的flag没两个礼拜就立不住了,知识付费的事咱也没少干,就是说能一直坚持下来的着实不多,咱也明白咱就是个普通人,逆袭这种事确实还是很难做到的,我这人还是比较佛系的。


大三这一年我用一年的时间从零学前端,自认为还算是没有辜负自己,这一年时间的学习也还算有成果,虽然没法和卷王们争第一,也能跟在他们后面做个万年老二(😭呜呜呜)。下半年开始实习后更别说了,新的技术栈的学习基本就停滞了。实习前我还天真的以为能有更多的时间学习,正相反,比在学校学的更少,因为下班到家七八点,生活琐事会比在学校里多得多,而且我下班后还要花一个多钟头健身,再加上忙碌一天后更无心学习,只想躺平。


下半年做过的最卷的事也就参与了字节青训营,课题选择了前端监控平台,可惜的就是没能在青训营期间完成(😭呜呜呜,队友都摆烂了),当然也就没有结营证书。但我也不甘心就这样算罢,这个项目我就自己拉出来,作为我的毕业设计去完成它。解决实习期间学习效率低的最好办法就是在公司学习一些对公司业务有关或者优化公司项目的知识,名正言顺地摸鱼。我是Vue入门的,这一年里也一直守着Vue,来年第一季度目标就是学习React和Nest,开发一个自己的数据聚合的网站,能变现就最好了(😎欸嘿)。


生活&实习


大三下,也就是今年上半年,为了冲刺暑期实习,也就没去做兼职了,感叹本就艰难的生活的同时,殊不知这是为数不多还能自己自由掌控的日子了(😥我哭死)。其实我开始准备实习还是挺晚了,再加上期末没有太多时间,准备并不是太充分,没有太多自信心,投了几家大厂,不是没回应,就是笔试挂,就有点望而却步。


在我一个大佬同学的介绍下,面试了一家南京的小厂,过程很顺利,实习薪资给的也很可观,当时就没考虑那么多,就选择接受offer了(后来在杭州实习认识了几个小伙伴,才学了没几个月,暑假就面试进了独角兽企业,我那个时候确实应该再多投一投的)。刚开始的想法是第一次出门实习,有份经验就可以,在什么城市没关系,然而事实是工作上确实没什么关系,生活上关系可大了。7月13日第一次一个人拎上行李,义无反顾地去了南京,以为自己终于能够大展拳脚,再不济也能够在公司有所贡献,然而现实总是没那么理想。


上路


因为一个人前往外地工作,第一件事情便是租房,为了省点钱就托南京实习公司的一个同事看房子,因为他的房租到期也要找房子就顺便可以租在一起,有个照应。然而实际上因为是第一次出远门工作和生活,一切和自己的理想差距显然大了许多:因为不是自己实地看的房,而且也是第一次租房,虽然房租只有850💰,但是也可能因为是夏季大家都开空调,差不多50多💰一个礼拜的电费和其他乱七八糟的费用,一个月光租房子就差不多得1200💰,并不算贵,但是性价比极低;我的房间没地方晒衣服,只能晒在那个同事的房间的阳台,作为一个社恐患者,每次去都要做很多心理斗争(他会不会睡了,他会不会在忙....🙃);桌上只能堪堪放下我的显示器和笔记本,鼠标活动范围极小;床应该是睡过好几个租客了,明显的不舒服;吃的方面因为有点水土不服不能随便乱吃,同时也是为了省钱所以选择自己做饭,因此还得购置很多厨具调味品等等,一次性的开销💰不小;回学校的频率比我想象的高,因此来回车费也成为一大负担;当时租房合同是同事代签的,他签了一年,我那时候也不懂也没问,再加上当时换工作离开的比较急,没时间找转租,违约金直接血亏1700💰。


日常挤地铁


生活的种种问题都还能接受或者解决,然而工作方面,因为进入公司的时间段比较特殊再加上疫情影响,在南京实习的三个月里,我始终没有能够在技术上得到足够的提升,再加上与公司和领导的气场不合,使得我在公司整天如坐针毡,甚至有点无所事事(总之就是过的很不开心),虽然有不低的实习薪资,但是我始终没法在那里躺平。因此在中秋决定参与秋招,开始寻找第二份实习工作。


然而今年找工作并不简单,因为频繁发作的疫情,再加上互联网行业这些年的发展,行业的形势非常的严峻,各大公司都削减了HC(head count,人头数,就是最终录用的人数,肯定有小伙伴不懂这个词,我一开始就不懂🤏),作为一个民本23年应届生,在今年的秋招着实很难找到一份理想的工作。那段时间的想法就是尽快找到下一份工作(急急急急急急,我是急急国王),找到一份离家近、工资高、平台大至少满足两个的工作。从9月10日中秋就开始投出第一份简历,到10月19日确定来到杭州的一家四五百人的SaaS公司,这期间投出过几百份简历,得到的回应却寥寥无几,这是一段非常难忘的经历。


这一个月里每一天都在为找工作烦恼,一开始专注于线上面试,却始终的得不到理想工作的认可,持续的碰壁使得开始怀疑自己这些年的学习,自己的选择是不是错了,是不是自己能力确实没法满足他们的要求(被ktv了),后来也决定不放过线下面试的机会,顶着疫情在南京、杭州、家、学校几地频繁奔波,在杭州线下面试的那一天还是顶着自己身体上的各种不适(持续拉肚子,全身酸痛,萎靡不振),仍然要拿出饱满的精神去面对面试,好在当时就获得了面试官也是现在的leader的认可,简直就是久旱逢甘霖,虽然并不是直接发的offer,但是也是十分有信心。杭州比起南京的工作,实习薪资低了很多,但是因为线下面试,对于当时感受到的公司的氛围十分的心动,也就放弃了其他小公司更高薪资的offer,决定了自己的第二份实习工作。


又上路啦


换工作又是换城市,所以又需要租房搬家,购置各种必需品,又是一大笔开销,在还没进公司前始终在担忧自己先择了薪资更低的工作,到时候会不会付出了这么多,结果又远不如预期让自己更痛苦。不过在经过了一个月左右实习后,我在杭州的公司工作的感受让我相信自己的选择没有错。


10月23日我再一次拖着一大堆行李开始了迁徙,本来打算先简单看房子,先回家住几天再自驾,拖着行李回来看房子签合同,所以我把被子等一些大件的行李都寄回家了,但是这次进入杭州后就黄🐎了(之前几地来回跑黄都没黄一下),只能多看几套房子然后就签下来,好在当天就看到一个自己满意的,10几平,押一付一,一个月算上水电差不多也就1300💰,不至于睡大街,但是我没有被子,当时杭州刚开始降温,温度也就个位数,但是买被子太亏了,之后用不上,就买了床毛毯,多盖几件衣服,凑活过了两天(真的凑活,冷的雅痞)。


杭州租的房


11月1日正式入职,正式开启了在杭州的工作生活,有条不紊的入职手续,时长1周的实习生培训,认识了许多和我一起实习的小伙伴,刚进来还赶上公司的双十一活动,让我对未来的工作生活充满希望。


双十一零食自助


第一月开始接触了一些简单的业务,重新开始了健身,第二个月就参与开发了一个简单的项目,还封装了公共组件、开发了简单的提高开发效率的脚手架工具,我终于能够继续有条不紊运转了。


在南京实习的期间除了参加了字节青训营和准备面试而巩固基础外,专业上可以说是没有丝毫提升,不过生活经验确实收获满满,坚定了自己的目标,职业生涯规划更加清晰,为了达到目标去学会自律。这几个月的开销给自己和父母都增添了不小得负担,好在现在稳定下来勉强能够在杭州自给自足,生活重新步入正轨,比起在南京,杭州的生活更加得心应手。但是并不是说南京不好,南京是一个非常优雅的城市,这里有他躺在超市里超乖的猫猫,超治愈


超乖的猫猫


离开南京前我也花时间去好好游玩了两天(去了一些免费的博物馆,景点)。


忘记叫啥地了


比起杭州,我认为南京更适合生活,我只是去到了一个不适合我的公司和因为经验不足吃了不少亏才离开了这个城市。我很珍惜在杭州的这份工作,也非常享受现在忙碌充实的生活,我也希望自己的能力能够不断得到认可,继续探索自己的人生价值。


感情


呜呜呜,鼠鼠该死啊,鼠鼠长了个恋爱脑,但是好在现在穷的雅痞,我还社恐,可以心无旁骛地工作学习(搞💰)。出来实习没几个礼拜就跟在一起一年的女孩子分手了,其实在上半年因为我们对未来规划的分歧就吵过架,她想留在慈溪,而我更向往大城市(当然不止这一点原因啦),那个时候我就很清楚这段感情肯定没法坚持很久,下半年又异地,在各自的城市实习,天天吵架,自然而然就吵分了,累觉不爱。我深知自己不是啥好男人(男人没一个好东西),还没有资本,毕业前绝对要水泥封心(做杭州第一深情)。


其实我家离学校很近,但是从念大学开始还是很少回家了,在学校里没有什么感觉,直到独自出门在外工作才知道在家真好,爸爸妈妈真好(我是妈宝男,呜呜呜😭),看这篇文章的小伙伴不要再随便跟爸爸妈妈撒气了哦。家里的老人只剩下奶奶独自在乡下了,以后一定要多打电话。


展望


在未来的一年中,希望自己能够吸收已经犯过的错误的经验,保质保量地完成未来的各项工作,作为一名程序员最重要的最重要的就是自我驱动,持续学习,通过不断学习才能够在未来的工作中创造更多的价值,以下是我23年的一些计划


学习



  • 这个月先抓紧时间把自己的毕设解决,写复盘的分享博客,之后顺利毕业

  • 上半年学习React,Nest,开发一个数据聚合分享平台,同样做分享

  • 运营自己的博客和各平台账号,不说多少粉丝,能坚持不凉就行,争取每周一个博客

  • 每季至少阅读一本书,学习一个技术栈

  • 坚持自己的每日计划和每月复盘总结(包含年中和年终总结)


工作



  • 因为现在常态化了,不知道今年的就业形势会是什么样的,着实不想再像去年那样被支配了,所以还是希望得到自己满意的薪资的前提下在这里转正,但愿不要出什么幺蛾子吧

  • 继续卷进部门更深层业务,目标负责6个项目

  • 学习更多优化开发效率和质量的技术栈,明年就简单定个两个的目标吧,要求不高


生活



  • 我真的超级想买机车的,但是杭州主城区禁摩,所以先23年下半年花时间考个D照,看情况决定买个机车还是电驴

  • 3月份房租到期了,看房肯定又要放进日程了,看看到时候有没有合租的小伙伴吧,如果有人有兴趣到时候可以分享一下杭州租房经验

  • 健身肯定是要继续的,有一说一我肉体确实没啥天赋(也可能是吃得不够多),健身更多的是一种生活态度吧

  • 我是一个很不喜欢打电话的人,尤其是和长辈,感觉没话聊,但是老人家接到自己孩子的电话,知道孩子过得不错,真的会很开心。明年定个小目标,一个月给奶奶打一通电话。


2022年好像所有人都过的很艰难,或许所有人都想离开浪浪山,但是也不要忘记看看浪浪山的风景,让我们一起加油吧。最后再打个广告,关注公众号程序猿青空,免费领取191本计算机领域黑皮书电子书,更有集赞活动免费挑选精品课程(各个领域的都有),不定期分享各种优秀文章、学习资源、学习课程,能在未来(因为现在还没啥东西)享受更多福利。


作者:CyanSky
来源:juejin.cn/post/7189562801159929915
收起阅读 »

iOS 3年开发迷茫随心聊

iOS 3年开发迷茫随心聊 从毕业开始做iOS,到现在已经是第4个年头了。第一家公司,做了一年,项目没上线就倒闭了,导致找第二家公司的时候也没有一个项目能拿的出手。第二家公司误入一家游戏公司,每天工作就是将H5小游戏做成一个App,想办法上线,一年过去了,技术...
继续阅读 »

iOS 3年开发迷茫随心聊


从毕业开始做iOS,到现在已经是第4个年头了。第一家公司,做了一年,项目没上线就倒闭了,导致找第二家公司的时候也没有一个项目能拿的出手。第二家公司误入一家游戏公司,每天工作就是将H5小游戏做成一个App,想办法上线,一年过去了,技术其实也没什么长进,但是通过这个过程了解一些苹果上架的知识。也有了几个上线项目经验了。由于想找个正经互联网公司做App,也离职找工作。


找工作头一个月,发现面试面的问题都是与底层相关,一问三不知,在家埋头学了2个月底层相关知识(可以理解背题)。原理是懂一点了,但是没有在实际项目中运用,工资也上不太去。在面了20多家公司后,终于找到现在第三份工作。


由于有第二份工作的经历,在第三家公司上班的时候一直在学习,有意识的去面试的原理去解决一些开发中的问题,例如使用runtime解决一些问题,却发现runtime如果没有很强的理解,还是不要用在项目里,因为可能出现未知的风险。例如使用交换方法,全局做了修改,但是后期项目需求更改,保持全局修改的前提下,对其他情况要做不同处理。也没有太多需求会使用到原理的内容,性能也不需要优化。


小公司对技术不太感冒,能完成需求就行,虽然自己力所能及的去做一些规范,但觉得做的还是不够,也不清楚其他公司到底是如何做的。小公司个人感觉对员工做事的责任心更加看重。需求就是写页面,页面还原的好,做的快一点,bug少一点就行了。不是理想的一个团队有什么方案,规范,让开发更有效率。最大感触是还好没有成为一个油腻的开发~。


在现在的公司,做了几个项目,也没有大的bug。学会了Swift进行开发。也许也算是一种收获吧。但是公司不加薪,今年的目标是想学点东西换一份工作。


学了1个月RxSwift,感觉也快学不下去了,公司是不可能用了,网上也有人说这个架构太重了。语言是个问题,自己英语水平有限,学习速度太慢了。如果有看到的这篇文章的小伙伴,也可以给我点意见。


最近想学一点提高开发效率的技能。和面试相关的内容。如果有大神经历过我这个时期,还麻烦给点建议。建议退iOS坑的就不必留言了。个人虽然菜,但是如果还没有做到小公司天花板的话,目前不考虑退坑。


第一次发文章也不知道说啥,后面会更新一些学习

作者:MissSunRise
来源:juejin.cn/post/7071892765763698719
笔记啥的。感谢包容。

收起阅读 »

乱打日志的男孩运气怎么样我不知道,加班肯定很多!

.markdown-body{word-break:break-word;line-height:1.75;font-weight:400;font-size:16px;overflow-x:hidden;color:#252933}.markdown-bod...
继续阅读 »

前言


线上出现问题,你的第一反应是什么?


如果是我的话,第一时间想的应该是查日志:



  1. if…else 到底进入了哪个分支?

  2. 关键参数是不是有缺失?

  3. 入参是不是有问题,没做好校验放进去了?


良好的日志能帮我们快速定位到问题所在,坑你的东西往往最为无形,良好的日志就是要让这些玩意无所遁形!


日志级别


Java应用中,日志一般分为以下5个级别:



  • ERROR 错误信息

  • WARN 警告信息

  • INFO 一般信息

  • DEBUG 调试信息

  • TRACE 跟踪信息


1)ERROR


ERROR 级别的日志一般在 catch 块里面出现,用于记录影响当前线程正常运行的错误,出现 Exception 的地方就可以考虑打印 ERROR 日志,但不包括业务异常。


需要注意的是,如果你抛出了异常,就不要记录 ERROR 日志了,应该在最终的地方处理,下面这样做就是不对的:


try {
   int i = 1 / 0;
} catch (Exception e) {
   log.error("出错了,什么错我不知道,啊哈哈哈!", e);
   throw new CloudBaseException();
}

2)WARN


不应该出现,但是不会影响当前线程执行的情况可以考虑打印 WARN 级别的日志,这种情况有很多,比如:



  • 各种池(线程池、连接池、缓存池)的使用超过阈值,达到告警线

  • 记录业务异常

  • 出现了错误,但是设计了容错机制,因此程序能正常运行,但需要记录一下


3)INFO


使用最多的日志级别,使用范围很广,用来记录系统的运行信息,比如:



  • 重要模块中的逻辑步骤呈现

  • 客户端请求参数记录

  • 调用第三方时的参数和返回结构


4)DEBUG


Debug 日志用来记录自己想知道的所有信息,常常是某个功能模块运行的详细信息,已经中间的数据变化,以及性能信息。


Debug 信息在生产环境一般是关闭状态的,需要使用开关管理(比如 SpringBoot Admin 可以做到),一直开启会产生大量的 Debug,而 Debug 日志在程序正常运行时大部分时间都没什么用。


if (log.isDebugEnabled()) {
   log.debug("开始执行,开始时间:[{}],参数:[{}]", startTime, params);
   log.debug("通过计算,得到参数1:[{}],参数2:[{}]", param1, param2);
   log.debug("最后处理结果:[{}]", result);
}

5)TRACE


特别详细的系统运行完成信息,业务代码中一般不使用,除非有特殊的意义,不然一般用 DEBUG 代替,事实上,我编码到现在,也没有用过这个级别的日志。


使用正确的格式


如果你是这样打印日志的:


log.info("根据条件id:{}" + id + "查询用户信息");

不要这样做,会产生大量的字符串对象,占用空间的同时也会影响性能。


正确的做法是使用参数化信息的方式:


log.info("根据条件id:[{}],查询用户信息", id);

这样做除了能避免大量创建字符串之外,还能明确的把参数隔离出去,当你需要把参数复制出来的时候,只需要双击鼠标即可,而不是用鼠标慢慢对准再划拉一下。


这样打出来的日志,可读性强,对排查问题的帮助也很大!


小技巧


1)多线程


遇到多个线程一起执行的日志怎么打?


有些系统,涉及到并发执行,定时调度等等,就会出现多次执行的日志混在一起,出问题不好排查,我们可以把线程名打印进去,或者加一个标识用来表明这条日志属于哪一次执行:


if (log.isDebugEnabled()) {
   log.debug("执行ID=[{}],处理了ID=[{}]的消息,处理结果:[{}]", execId, id, result);
}

2)使用 SpringBoot Admin 灵活开关日志级别


image-20220727155526217


写在最后


一开始写代码的时候,没有规范日志的意识,不管哪里,都打个 INFO,打印出来的东西也没有思考过,有没有意义,其实让自己踩了不少坑,加了不少班,回过头,我想对学习时期的我说一句:”能让你加班的东西,都藏在各种细节里!写代码之前,先好好学习如何打日志!“


作者:你算哪块小蛋糕
来源:juejin.cn/post/7124958610123128839
收起阅读 »

你可能忽略的10种JavaScript快乐写法

.markdown-body{word-break:break-word;line-height:1.75;font-weight:400;font-size:16px;overflow-x:hidden;color:#252933}.markdown-bod...
继续阅读 »

你可能忽略的10种JavaScript快乐写法


前言



  • 代码的简洁、美感、可读性等等也许不影响程序的执行,但是却对人(开发者)的影响非常之大,甚至可以说是影响开发者幸福感的重要因素之一;

  • 了解一些有美感的代码,不仅可以在一定程度上提高程序员们的开发效率,有些还能提高代码的性能,可谓是一举多得;


笔者至今难以忘记最开始踏入程序员领域时接触的一段List内嵌for的Python代码:


array = [[16, 3, 7], [2, 24, 9], [4, 1, 12]]
row_min = [min(row) for row in array ]
print(row_min)

这可能就是动态语言非常优秀的一点,而JavaScript同样作为动态语言,其中包含的优秀代码片段也非常之多,比如我们通过JavaScript也可以非常轻松地实现上述的功能:


const array = [[16, 3, 7], [2, 24, 9], [4, 1, 12]]
const row_min = array.map(item => Math.min(...item))
console.log(row_min)

能写出优秀的代码一直是笔者所追求的,以下为笔者在开发阅读过程积累的一些代码片段以及收集了互联网上一些优秀代码片段,希望对你有所帮助


概述


这里,考虑到有些技巧是大家见过的或者说是已经烂熟于心的,但总归有可能有些技巧没有留意过,为了让大家更加清楚的找到自己想要查阅的内容以查漏补缺,所以这里笔者贴心地为大家提供了一张本文内容的索引表,供大家翻阅以快速定位,如下:


应用场景标题描述补充1补充2
数组去重通过内置数据解构特性进行去重[] => set => []通过遍历并判断是否存在进行去重[many items].forEach(item => (item <不存在于> uniqueArr) && uniqueArr.push(item))
数组的最后一个元素获取数组中位置最后的一个元素使用at(-1)
数组对象的相关转换对象到数组:Object.entries()数组到对象:Obecjt.fromEntries()
短路操作通过短路操作避免后续表达式的执行a或b:a真b不执行a且b:a假b不执行
基于默认值的对象赋值通过对象解构合并进行带有默认值的对象赋值操作{...defaultData, ...data}
多重条件判断优化单个值与多个值进行对比判断时,使用includes进行优化[404,400,403].includes
交换两个值通过对象解构操作进行简洁的双值交换[a, b] = [b, a]
位运算通过位运算提高性能和简洁程度
replace()的回调通过传入回调进行更加细粒度的操作
sort()的回调通过传入回调进行更加细粒度的操作根据字母顺序排序根据真假值进行排序

数组去重


这不仅是我们平常编写代码时经常会遇到的一个功能实现之一,也是许多面试官在考查JavaScript基础时喜欢考查的题目,比较常见的基本有如下两类方法:


1)通过内置数据结构自身特性进行去重


主要就是利用JavaScript内置的一些数据结构带有不包含重复值的特性,然后通过两次数据结构转换的消耗[] => set => []从而达到去重的效果,如下演示:


const arr = ['justin1go', 'justin2go', 'justin2go', 'justin3go', 'justin3go', 'justin3go'];
const uniqueArr = Array.from(new Set(arr));
// const uniqueArr = [...new Set(arr)];

2)通过遍历并判断是否存在进行去重


白话描述就是:通过遍历每一项元素加入新数组,新数组存在相同的元素则放弃加入,伪代码:[many items].forEach(item => (item <不存在于> uniqueArr) && uniqueArr.push(item))


至于上述的<不存在于>操作,可以是各种各样的方法,比如再开一个for循环判断新数组是否有相等的,或者说利用一些数组方法判断,如indexOfincludesfilterreduce等等


const arr = ['justin1go', 'justin2go', 'justin2go', 'justin3go', 'justin3go', 'justin3go'];
const uniqueArr = [];
arr.forEach(item => {
// 或者!uniqueArr.includes(item)
if(uniqueArr.indexOf(item) === -1){
uniqueArr.push(item)
}
})

结合filter(),判断正在遍历的项的index,是否是原始数组的第一个索引:


const arr = ['justin1go', 'justin2go', 'justin2go', 'justin3go', 'justin3go', 'justin3go'];
const uniqueArr = arr.filter((item, index) => {
return arr.indexOf(item, 0) === index;
})

结合reduce(),prev初始设为[],然后依次判断cur是否存在于prev数组,如果存在则加入,不存在则不动:


const arr = ['justin1go', 'justin2go', 'justin2go', 'justin3go', 'justin3go', 'justin3go'];
const uniqueArr = arr.reduce((prev,cur) => prev.includes(cur) ? prev : [...prev,cur],[]);

数组的最后一个元素


对于获取数组的最后一个元素,可能平常见得多的就是arr[arr.length - 1],我们其实可以使用at()方法进行获取


const arr = ['justin1go', 'justin2go', 'justin3go'];
console.log(arr.at(-1)) // 倒数第一个值
console.log(arr.at(-2)) // 倒数第二个值
console.log(arr.at(0)) // 正数第一个
console.log(arr.at(1)) // 正数第二个


注:node14应该是不支持的,目前笔者并不建议使用该方法,但获取数组最后一个元素是很常用的,就应该像上述语法一样简单...



数组对象的相互转换



    const entryified = [
["key1", "justin1go"],
["key2", "justin2go"],
["key3", "justin3go"]
];

const originalObject = Object.fromEntries(entryified);
console.log(originalObject);

短路操作


被合理运用的短路操作不仅非常的优雅,还能减少不必要的计算操作


1)基本介绍


主要就是||或操作、&&且操作当第一个条件(左边那个)已经能完全决定整个表达式的值的时候,编译器就会跳过该表达式后续的计算



  • 或操作a || b:该操作只要有一个条件为真值时,整个表达式就为真;即a为真时,b不执行;

  • 且操作a && b:该操作只要有一个条件为假值时,整个表达式就为假;即a为假时,b不执行;


2)实例


网络传输一直是前端的性能瓶颈,所以我们在做一些判断的时候,可以通过短路操作减少请求次数:


const nextStep = isSkip || await getSecendCondition();
if(nextStep) {
openModal();
}

还有一个经典的代码片段:


function fn(callback) {
// some logic
callback && callback()
}

基于默认值的对象赋值



  • 很多时候,我们在封装一些函数或者类时,会有一些配置参数。

  • 但这些配置参数通常来说会给出一个默认值,而这些配置参数用户是可以自定义的

  • 除此之外,还有许许多多的场景会用到的这个功能:基于默认值的对象赋值。


function fn(setupData) {
const defaultSetup = {
email: "justin3go@qq.com",
userId: "justin3go",
skill: "code",
work: "student"
}
return { ...defaultSetup, ...setupData }
}

const testSetData = { skill: "sing" }
console.log(fn(testSetData))

如上{ ...defaultSetup, ...setupData }就是后续的值会覆盖前面key值相同的值。


多重条件判断优化


if(condtion === "justin1go" || condition === "justin2go" || condition === "justin3go"){
// some logic
}

如上,当我们对同一个值需要对比不同值的时候,我们完全可以使用如下的编码方式简化写法并降低耦合性:


const someConditions = ["justin1go", "justin2go", "justin3go"];
if(someConditions.includes(condition)) {
// some logic
}

交换两个值


一般来说,我们可以增加一个临时变量来达到交换值的操作,在Python中是可以直接交换值的:


a = 1
b = 2
a, b = b, a

而在JS中,也可以通过解构操作交换值;


let a = 1;
let b = 2;
[a, b] = [b, a]

简单理解一下:



  • 这里相当于使用了一个数组对象同时存储了a和b,该数组对象作为了临时变量

  • 之后再将该数组对象通过解构操作赋值给a和b变量即可


同时,还有种比较常见的操作就是交换数组中两个位置的值:


const arr = ["justin1go", "justin2go", "justin3go"];
[arr[0], arr[2]] = [arr[2], arr[0]]

位运算


关于位运算网上的讨论参差不齐,有人说位运算性能好,简洁;也有人说位运算太过晦涩难懂,不够易读,这里笔者不发表意见,仅仅想说的是尽量在使用位运算代码的时候写好注释!


下面为一些常见的位运算操作,参考链接


1 ) 使用&运算符判断一个数的奇偶


// 偶数 & 1 = 0
// 奇数 & 1 = 1
console.log(2 & 1) // 0
console.log(3 & 1) // 1

2 ) 使用~, >>, <<, >>>, |来取整


console.log(~~ 6.83)    // 6
console.log(6.83 >> 0) // 6
console.log(6.83 << 0) // 6
console.log(6.83 | 0) // 6
// >>>不可对负数取整
console.log(6.83 >>> 0) // 6

3 ) 使用^来完成值交换


var a = 5
var b = 8
a ^= b
b ^= a
a ^= b
console.log(a) // 8
console.log(b) // 5

4 ) 使用&, >>, |来完成rgb值和16进制颜色值之间的转换


/**
* 16进制颜色值转RGB
*
@param {String} hex 16进制颜色字符串
*
@return {String} RGB颜色字符串
*/

function hexToRGB(hex) {
var hexx = hex.replace('#', '0x')
var r = hexx >> 16
var g = hexx >> 8 & 0xff
var b = hexx & 0xff
return `rgb(${r}, ${g}, ${b})`
}

/**
* RGB颜色转16进制颜色
*
@param {String} rgb RGB进制颜色字符串
*
@return {String} 16进制颜色字符串
*/

function RGBToHex(rgb) {
var rgbArr = rgb.split(/[^\d]+/)
var color = rgbArr[1]<<16 | rgbArr[2]<<8 | rgbArr[3]
return '#'+ color.toString(16)
}
// -------------------------------------------------
hexToRGB('#ffffff') // 'rgb(255,255,255)'
RGBToHex('rgb(255,255,255)') // '#ffffff'

replace()的回调函数


之前写过一篇文章介绍了它,这里就不重复介绍了,F=>传送


sort()的回调函数


sort()通过回调函数返回的正负情况来定义排序规则,由此,对于一些不同类型的数组,我们可以自定义一些排序规则以达到我们的目的:



  • 数字升序:arr.sort((a,b)=>a-b)

  • 按字母顺序对字符串数组进行排序:arr.sort((a, b) => a.localeCompare(b))

  • 根据真假值进行排序:


const users = [
{ "name": "john", "subscribed": false },
{ "name": "jane", "subscribed": true },
{ "name": "jean", "subscribed": false },
{ "name": "george", "subscribed": true },
{ "name": "jelly", "subscribed": true },
{ "name": "john", "subscribed": false }
];

const subscribedUsersFirst = users.sort((a, b) => Number(b.subscribed) - Number(a.subscribed))

最后



  • 个人能力有限,并且代码片段这类东西每个人的看法很难保持一致,不同开发者有不同的代码风格,这里仅仅整理了一些笔者自认为还不错的代码片段;

  • 可能互联网上还存在着许许多多的优秀代码片段,笔者也不可能全部知道;

  • 所以,如果你有一些该文章中没有包含的优秀代码片段,就不要藏着掖着了,分享出来吧~


同时,如本文有所错误,望不吝赐教,友善指出🤝


Happy Coding!🎉🎉🎉


QQ图片20230223164124.gif


QQ图片20230223164124.gif


QQ图片20230223164124.gif


参考


作者:Justin3go
来源:juejin.cn/post/7203243879255277623
收起阅读 »

一次线上OOM问题分析

OOM
现象 线上某个服务有接口非常慢,通过监控链路查看发现,中间的 GAP 时间非常大,实际接口并没有消耗很多时间,并且在那段时间里有很多这样的请求。 原因分析 先从监控链路分析了一波,发现请求是已经打到服务上了,处理之前不知道为什么等了 3s,猜测是不是机器当时...
继续阅读 »

现象


线上某个服务有接口非常慢,通过监控链路查看发现,中间的 GAP 时间非常大,实际接口并没有消耗很多时间,并且在那段时间里有很多这样的请求。



原因分析


先从监控链路分析了一波,发现请求是已经打到服务上了,处理之前不知道为什么等了 3s,猜测是不是机器当时负载太大了,通过 QPS 监控查看发现,在接口慢的时候 CPU 突然增高,同时也频繁的 GC ,并且时间很长,但是请求量并不大,并且这台机器很快就因为 Heap满了而被下掉了。



去看了下日志,果然有 OOM 的报错,但是从报错信息上并没办法找到 Root Cause。


system error: org.springframework.web.util.NestedServletException: Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: Java heap space   at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1055)   at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)   at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)   at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)   at javax.servlet.http.HttpServlet.service(HttpServlet.java:681) 

另外开发同学提供了线索,在发生问题的时候在跑一个大批量的一次性 JOB,怀疑是不是这个 JOB 导致的,马上把 JOB 代码拉下来分析了下,JOB 做了分批处理,代码也没有发现什么问题。


虽然我们系统加了下面的 JVM 参数,但是由于容器部署的原因,这些文件在 pod 被 kill 掉之后没办法保留下来。


-XX:+HeapDumpOnOutOfMemoryError -XX:ErrorFile=/logs/oom_dump/xxx.log -XX:HeapDumpPath=/logs/oom_dump/xxx.hprof

这个现象是最近出现的,猜测是最近提交的代码导致的,于是去分析了最近提交的所有代码,很不幸的都没有发现问题。。。


在分析代码的过程中,该服务又无规律的出现了两次 OOM,只好联系运维同学优先给这个服务加了 EFS (Amazon 文件系统)等待下次出现能抓住这个问题。


刚挂载完 EFS,很幸运的就碰到了系统出现 OOM 的问题。


dump 出来的文件足有 4.8G,话不多说祭出 jvisualvm 进行分析,分析工具都被这个dump文件给搞挂了也报了个java.lang.OutOfMemoryError: Java heap space,加载成功之后就给出了导致OOM的线程。



找到了具体报错的代码行号,翻一下业务代码,竟然是一个查询数据库的count操作,这能有啥问题?


仔细看了下里面有个foreach遍历userId的操作,难道这个userId的数组非常大?



找到class按照大小排序,占用最多的是一个 byte 数组,有 1.07G,char 数组也有1.03G,byte 数组都是数字,直接查看 char 数组吧,点进去查看具体内容,果然是那条count语句,一条 SQL 1.03G 难以想象。。。




这个userId的数据完全是外部传过来的,并没有做什么操作,从监控上看,这个入参有 64M,马上联系对应系统排查为啥会传这么多用户过来查询,经过一番排查确认他们有个bug,会把所有用户都发过来查询。。。到此问题排查清楚。


解决方案


对方系统控制传入userId的数量,我们自己的系统也对userId做一个限制,问题排查过程比较困难,修改方案总是那么的简单。


别急,还有一个


看到这个问题,就想起之前我们还有一个同样类似的问题导致的故障。


也是突然收到很多告警,还有机器 down 机的告警,打开 CAT 监控看的时候,发现内存已经被打满了。



操作和上面的是一样的,拿到 dump 文件之后进行分析,不过这是一个漫长的过程,因为 down了好几台机器,最大的文件有12GB。


通过 MAT 分析 dump 文件发现有几个巨大的 String(熟悉的味道,熟悉的配方)。



接下来就是早具体的代码位置了,去查看了下日志,这台机器已经触发自我保护机制了,把代码的具体位置带了出来。


经过分析代码发现,代码中的逻辑是查询 TIDB(是有同步延迟的),发现在极端情况下会出现将用户表全部数据加载到内存中的现象。



于是找 DBA 拉取了对应时间段 TIDB 的慢查询,果然命中了。



总结


面对 OOM 问题如果代码不是有明显的问题,下面几个JVM参数相当有用,尤其是在容器化之后。


-XX:+HeapDumpOnOutOfMemoryError -XX:ErrorFile=/logs/oom_dump/xxx.log -XX:HeapDumpPath=/logs/oom_dump/xxx.hprof

另外提一个参数也很有用,正常来说如果程序出现 OOM 之后,就是有代码存在内存泄漏的风险,这个时候即使能对外提供服务,其实也是有风险的,可能造成更多的请求有问题,所以该参数非常有必要,可以让 K8S 快速的再拉起来一个实例。


-XX:+ExitOnOutOfMemoryError

另外,针对这两个非常类似的问题,对于 SQL 语句,如果监测到没有where条件的全表查询应该默认增加一个合适的limit作为限制,防止这种问题拖垮整个系统。


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

程序媛员的博客之旅

写博客困境 自从成为了一名程序媛,就一直有很多前辈,苦口婆心的告诉我:一定要写博客,好处多多!而我,作为一枚勤奋好学(haochilanzuo)的程序媛,其实心里一直埋藏着一颗写博客的小小种子。 无奈的是,每次冲动的热情都只能持续到更新两三篇技术文章,然后就没...
继续阅读 »

写博客困境


自从成为了一名程序媛,就一直有很多前辈,苦口婆心的告诉我:一定要写博客,好处多多!而我,作为一枚勤奋好学(haochilanzuo)的程序媛,其实心里一直埋藏着一颗写博客的小小种子。


无奈的是,每次冲动的热情都只能持续到更新两三篇技术文章,然后就没办法继续更新下去了。所以工作了这么多年,自己都成为老学姐了,还是没有拿得出手的个人博客,实在是惭愧。


经过深刻的自我反省之后,我觉得阻碍我更新博客的原因,主要有以下几个方面:


1. 工作太忙。


大家都知道,相比其他工种,作为程序员的工作强度还是蛮大的。每天都有做不完的需求,开不完的会议。经常要到晚上快下班了才有时间写代码,于是加班就成了家常便饭。下了班回家也感觉很累,不想再打开电脑,只能刷刷手机、看看综艺,做一些不费脑子的娱乐活动。


2. 文字功底太差。


作为一名理科生,上学的时候语文成绩就很差,作文都靠模板以及背的素材拼凑起来。高中毕业之后,几乎没有完整写过什么。而写博客,需要高强度大量输出内容,还要有组织有架构,逻辑条理清晰,这个对我来说简直太难了。所以经常写两篇之后,发现自己写的东西惨不忍睹,于是就暂停了更新博客的计划。


3. 没什么内容可写。


虽然每天都在写代码,但是很多时候做的都是重复性工作,并没有太多有技术含量、技术深度的内容,可以支撑我写出高大上的博客。


我个人更新不下去博客的主要原因就是上面几点,相信有很多想要更新博客却坚持不下去的同学,也都有同样的感受。


如何突破困境


我想说一下,我为什么觉得自己这次能克服这几个问题,以及克服这几个问题的方法。如果大家也和我有类似的问题,可以往下读一读,看有没有什么可以借鉴的地方。主要还是给我自己未来的日更之旅打打鸡血。



一、工作太忙,没时间。



每个人的一天都是24小时,为什么有些人能做更多的事情,实现更高的成就呢?我觉得这和每个人的时间管理方式是息息相关的。掌握高效的时间管理策略,是每个高效能人士的必备技能。


我以前觉得是因为程序员的工作比其他行业更忙,所以没有时间。但是看周围,把博客或者副业运营很好的那群人,工作也不闲。所以说,这个理由只是一个对自己时间管理无能的借口而已。真正的强者,从来不会找没有时间的借口,而不去做一些尝试。


当下,为了能够实现工作、写作(其实是搞副业)和生活之间的平衡,我决定先从这几个角度来优化我的时间使用效率。


1. 为任务分配合理的优先级


事情是永远做不完的,如果想做的太多,那么时间永远都不够。我准备用重要紧急四象限法来管理任务。每拿到一个任务后,先决策这个任务是属于哪个象限的,然后再安排做的时间。
image.png


我们之所以感觉每天忙忙碌碌,却没有什么进步,主要是因为在“紧急-不重要”的事情上,浪费了太多的时间。仔细想想,上班时间有多少是浪费在了,对未来成长没有任何意义的所谓“紧急”的事情上了。而真正“重要”的事情,却被我们以没有时间做,而一直推迟。


前段时间看到的一句话,对我触动很大:
Today you do things others don't do.
Tomorrow you do things others can't do.


做“重要-不紧急”的事情,不会对你的人生产生立竿见影的效果。但是长期下去,效果一定是惊人的,而且能给你带去很多别人没有的机会。


“人们总是容易高估一天的影响,而低估长期的影响”。比如学英语、写作,可能努力了一个月都没有效果,很多人就开始放弃了,转而去寻找其他的方法。但有些人坚持了下来,于是这些人坚持了一年、两年甚至几年之后,最后到达了很高的高度,才发现原来每一天的坚持都没有浪费,最后都是有效果的。


2. 减少任务切换,提高做事情的效率


提高做事情的效率,最好的办法就是进入“心流”的状态。不管是写代码、写文字还是看书学习,在“心流”的状态下,效率比平时要提高好几倍。


“心流”的状态,就是一种忘我的境界,忘记了时间、忘记了周围所处的环境,甚至忘记了身体上的痛苦,专心沉浸在当下所做的事情上。我相信这种状态,大家多多少少都有体会,比如在废寝忘食打游戏的时候。这种状态下,人所爆发出来的潜能是巨大的。


要达到“心流”的状态,最简单易行的方法,就是减少任务的切换。就像CPU线程切换,需要缓存上一个任务的执行状态,加载下一个任务的运行环境,效率很低。人脑也是,在上下文切换的时候,需要耗费很多的时间和精力。


而工作中,经常会被工作软件的消息提醒所打断,很难进入”心流“状态。比如,正在尝试解决一个疑难的问题,但是突然来了一条工作上的消息,于是不得不中断当前的工作,去看这个消息。等处理完消息后,在回到工作,可能已经忘记之前做到哪里了,又需要花时间才能重新进入状态。


可以尝试”番茄钟"的方法。在每个番茄钟开始的时候,屏蔽消息,集中精神工作25分钟,然后再花5分钟处理这25分钟到达的消息。处理完后,进入下一个番茄钟。


3. 不要给自己定太高的目标


之前我写博客,总是一篇文章写很长,想要在一篇文章中讲完和标题有关的所有知识点。但是这样会让自己很累,每次写一篇文章都要花很长的时间和精力,到后面甚至排斥写文章这件事情。


所以这次,我决定不给自己设太高的目标,每篇技术文章,争取讲完一个知识点就可以,如果内容特别多,可以采用连载的形式。最后可以新建一篇索引的文章,将各个连载的文章串起来。


PS:时间管理是一个复杂的事情,我之前也看过一些相关的书籍,后续我也想通过更系统的文章分享出来。先在这里挖个坑,如果想看就先关注我吧,后续我会慢慢把坑都填上。



二、文字功底太差



另一个困扰我的因素,就是自己的文字功底太差了。几乎没怎么写过文章的我,不知道怎么表达自己。有时候心里有很多想说的话,但是一写起来就读不通,没办法完整表达自己的意思。


为了能顺利完成日更的目标,我决定尝试下面的方法。


1. 先写起来,自然而然就会有进步


第一个就是不管怎么样,不管写得有多烂,先写起来,以量变来引起质变。我现在的写作量,可能连那些大V一个月的量都不到,凭什么觉得自己的水平就能和人家一样。如果每天输出500字,一年就是18.25万字。坚持写,我相信写一年之后,水平肯定会有进步。


没有什么是刻意练习不能达成的,如果有,那肯定是练习不够多。


2. 多看多模仿


写文章也是有方法可以借鉴的。去看好的文章是什么样的,向优秀的文章和作者学习。


比如,我之前看一个技术博主,会在每篇文章的开头放一个脑图,描述整片文章的整体架构,我觉得这个方法就很好。首先自己可以根据这篇脑图往里填充资料,速度更快也更清晰,同时,读者也可以在看文章之前对文章的内容有一个整体的感知,很快就能定位到自己需要的内容上。之后我的文章也可以借鉴这个方法。



三、没什么内容可写



关于没什么内容可写,以前做业务开发的时候,确实有这个问题,但是现在做系统开发了,几乎每天都在学习新的知识,所以完全不愁没有内容可写了。


如果有同学想开始写博客,但是又觉得没有内容可写,可以从以下几个方面去尝试:


1. 提前想一些topic,主动积累


在开始写博客之前,提前收集一些topic。我现在就有一个文档,专门用来放我想写的文章topic,现在这个文档里面已经有几十个可以写的topic了。


提前脑暴一些topic,或者列一个知识图谱,到时候如果发现没什么内容可写,直接去list里面找一个topic就好了。


2. 主动去学习一些新的东西


对于一些业务开发的同学,可以在开发之余,主动push自己去学一些新的技术。比如看一些技术书籍和博客。


博客内容


之后我的博客,主要会围绕下面这些方向:


Android性能优化


作为一名Android开发,更新的内容主要还是在Android相关的技术点上。由于我近期工作的重点主要在性能优化方面,所以前期的文章主要会更新性能优化相关的文章,包括启动时间、存储空间、稳定性、ANR等优化方案,以及一些相关的技术原理。


Android面试集锦


等把Android性能优化相关的内容写完,会再写一些面试相关的内容。作为一个拿过各大互联网offer、一毕业就当上面试官的学姐,在面试方面还是有不少经验的。


算法题解


算法题可能也会写一些,写一些我觉得好的题目的题解(主要是算法题比较好水文章,实在不行了就来篇题解)


读书笔记


我平常也会读一些技术之外的书籍,会写一些读书笔记,到时候会更新一些这方面的内容。


新的技术方向


除了Android开发以外,未来想学习一些新的技术方向,到时候也会更新到这个博客里,比如Web3相关的内容。


杂七杂八的思考


一些思考想法,对当下事件的看法,对未来的思考,个人成长、时间管理、投资理财等等相关内容,都会记录在博客里。


总结


说了这么多,也不知道会不会有人看我写的文章,毕竟现在Android开发已经不流行了。而且ChatGPT兴起之后,普通的内容生产者,会受到非常大的冲击。可能以后查东西都不需要去搜博客文章了,直接问ChatGPT就好了。之后我的博客文章,说不定也会让ChatGPT帮我写一部分。


总之不管有没有人看,不管AI是否会把我的工作取代,我还是会把日更坚持下去。如果对我更新的内容感兴趣,欢迎点点关注呀~~


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

【2022年终总结】北漂两年,领证,换工作

很高兴见到你 👋 我是 Flywith24~ 这是我第三篇个人年终总结。由于最近在处理工作,搬家等事情,所以拖到现在才发布。 2022 年几乎没输出什么内容,以至于前些天收到这样的评论~ 在之前两篇年终总结中,除了常规的流水账,我都会写一些当年的思考: 2...
继续阅读 »

很高兴见到你 👋 我是 Flywith24~


这是我第三篇个人年终总结。由于最近在处理工作,搬家等事情,所以拖到现在才发布。


image.png


2022 年几乎没输出什么内容,以至于前些天收到这样的评论~


47546a0a1e9bb52b90b06c374bad155f.jpg


在之前两篇年终总结中,除了常规的流水账,我都会写一些当年的思考:


2020年 分享了学习 Android 的经历和感悟;


2021年 分享了如何高效学习/工作的思考;


这一次我想分享关于工作中快速融入团队,社交关系这两部分内容。


工作方面


性能优化


Q1 参加了一个冷启动性能优化的专项,学了很多相关的知识


image.png


晋升


5 月通过了晋升答辩,提升了画图与 PPT 能力 🤪


image.png


技术栈扩容


6 月中旬由于工作需要,学习了 RN 并开发了一个独立的模块。快手动态化基建做的不错,所以熟悉了 TypeScript 的语法就开始进行需求开发了。


8 月使用 ts 写了一个在 Github 个人主页显示最近文章列表的小工具,详情请参考 开源项目:Github 主页显示最近发布的掘金文章(支持动态更新)



这也是 2022年在掘金发的唯一一篇文章 😳



image.png


之后学习了心心念念的 Jetpack Compose,将个人项目用 Compose 进行了重写。


面试


11 月开始居家办公,这期间面了几家公司,最后入职了小红书。



关于入职时间拉扯了一番,最终于 2023.02.08 入职。(02.08 也是我 2020 年入职快手的日子)



image.png


生活方面


读书


image.png

今年读了很多技术方面的书,不过都没有读完,倒是读完几本杂书,其中我最喜欢的是这本:


image.png

这本书另一个译名是:狗屁工作(bullshit jobs),它的译者在播客里表示由于过审等因素用了现在的译名



知识星球人数破千


image.png

2022 年知识星球人数破千 🥳 该星球永久免费,详情可点此查看


全拼改双拼


一直以来,我都在追求更高效的工作方式,减少无用且重复的活动。


2022 年在朋友的建议下使用双拼打字,过程中伴随着阵痛,但结果令人满意。


关于什么是双拼,可以看下这个 up 主的视频


双拼这点东西,up居然讲了十分钟——十分钟了解双拼到底是啥


image.png

西部大善人


2018 年买了《荒野大镖客2》,但一直停留在第一章。2022 年打了 100 多个小时,体验了亚瑟的一生。


image.png


领证


2022.06.06 人生清单迎来一个里程碑的节点,领证啦~


image.png

旅行


2022 年点亮了三座城市,作为东北人好喜欢云南的天气


image.png
image.png
image.png
image.png


快速融入新团队


在北京我两次面临这个问题,希望我的经验对各位小伙伴有所帮助。


上手项目


快速上手公司项目,对于新人在团队内第一时间建立影响力是非常重要的。这里站在客户端开发的角度聊下我的经验。



  • 在 app 里体验所在团队负责内容(拿到 offer 后就可以做,这点很重要)

  • 认真阅读团队内提供的文档(如果有的话)

  • 将项目里自研库和没用过的第三方库通过写 demo 的方式学习其使用流程(了解原理更佳,后面会避免很多坑)

  • 从负责业务的入口开始分析,用 CodeLocator 这类工具抓一下主要界面的视图结构,形成文档,这对未来的新人很有帮助

  • 有些细节可以在做相关需求时再去了解,不要心急


了解同事


融入团队另一个重要的事是了解同事。作为新人会有相互介绍的环节,一次记住二十几个人是不容易的(对于有些脸盲的我则更加困难)。


所以我会制作一份座位表,罗列每个人工位,名字,Android 还是 iOS。实践了两次,对于我很有效。


之后我会利用需求管理平台查看每个人最近的需求以及代码,对同事主要负责的业务有一个初步了解。


不懂就问


在项目里如果遇到用了很大力气却没搞懂的问题可以果断向同事提问。


不过需要注意,不懂就问是建立在「正确提问」的基础上的


提问时可将自己的尝试和猜测说出来。


image.png



摘自 2021 年终总结



对于应届或实习生来说,如果遇到的 mentor 比较内向不爱说话,需要自己更加主动,认真思考并整理遇到的问题大胆提问。


社交关系



声明:本节只是分享我的思考与选择


我尊重每个人的选择与喜好。例如有的人喜欢工作与生活有明确边界的同事关系。选择没有好坏之分,自己喜欢即可。



选大学,我们选择的是专业,老师,同学。


同样,我们在工作中选择的是公司,团队,同事。


本质上,我们选择的是圈子,环境。


即使去一家上万人的公司,对自己影响最大的也只有身边的人。


因此我喜欢这样的社交关系:



  • 工作时我们是一起奋战的战友

  • 摸鱼时我们时一起讨论的群友

  • 用餐时我们是一起行动的饭友

  • 假期时我们是一起玩耍的好友

  • 分别时我们是一起相送的朋友


如果你想与我有这样的社交关系


芝麻掉进针眼儿里


image.png


小红书内容发布客户端组 Android 和 iOS 还有 hc


点击该链接查看内推详情


关于我


人总是喜欢做能够获得正反馈(成就感)的事情,如果感觉本文内容对你有帮助的话,麻烦点亮一下👍,这对我很重要哦~


我是 Flywith24人只有通过和别人的讨论,才能知道我们自己的经验是否是真实的,加我微信交流,让我们共同进步。



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