注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

预测2024年之后的前端开发模式

web
大家好,我卡颂。 最近AIGC(AI Generated Content,利用AI生成内容)非常热,技术圈也受到了很大冲击。目前来看,利用LLM(Large Language Model,大语言模型)辅助开发还停留在非常早期的阶段,主要应用是辅助编码,即用自然...
继续阅读 »

大家好,我卡颂。


最近AIGC(AI Generated Content,利用AI生成内容)非常热,技术圈也受到了很大冲击。目前来看,利用LLM(Large Language Model,大语言模型)辅助开发还停留在非常早期的阶段,主要应用是辅助编码,即用自然语言输入需求,模型输出代码。更近一步的探索也仅仅是在此基础上的一层封装(比如copilot Xcursor)。


但即使在如此早期阶段,也对开发者的心智产生极大震撼,AI让程序员失业这样的论调甚嚣尘上。


LLM的爆发对前端意味着什么?本文尝试预测一波2024年之后的前端开发模式,这个预测遵循如下原则:




  • 尊重技术客观发展规律。以当前已有技术为基础预测,而不是将预测建立在某种虚无缥缈的高端技术,或者假想某些技术突破重大瓶颈




  • 尊重人性。程序员只是谋生的职业,新的开发模式即使再厉害,如果让程序员赚不到钱,那也是很难推广开的




欢迎加入人类高质量前端交流群,带飞


范式迁移的本质


为了预测未来,先看看我们是如何走到现在的。


在前端开发领域,我们经历了从jQuery为代表的面向过程编程向前端框架为代表的状态驱动模式的迁移。


当问到该选Vue还是React开发?,这样的问题会引起很大争议,但如果问到该选jQuery还是框架开发?,这样的问题就不会有太多争议。


为什么前端领域普遍接受了这种范式的迁移?在我看来,有两个原因:


1. 开发效率提高


这一点毋需多言,相信前端同学都有体会。


2. 门槛提高


面向过程编程是非常浅显易懂的开发模式。君不见,曾经的前端靠一本锋利的jQuery就能打天下。相比之下,状态驱动就有一定学习门槛。



当一项有一定门槛的技术(这里指前端框架)变为行业事实上的标准时,行业门槛就提升了,这为从业者构筑了行业壁垒。


事实上,正是由于:




  1. web应用复杂度提高




  2. 前端框架的流行




才让后端工程师工作职责中的view层,分化出前端工程师这一职业。


对于前端领域来说,只有同时平衡了提效提高门槛的技术,才会被市场(这里的消费者指前端工程师)接受。


举个反例,Angular全家桶的模式虽然提高了开发效率,但是同时,门槛提高太多了。


而且更糟的是,Angular中的很多概念都是从后端迁移而来,作为一款前端框架,对后端更亲和且门槛高,这对本身就是从后端view层中分化出的前端工程师来说,是比较排斥的。


再举个反例 —— Vue。有同学会说,Vue这么流行的前端框架,你说他是反例?


还是从提效提高门槛的角度看,Vue提效的同时,由于其模版语法、响应式更新等特性,他是降低了开发门槛的,这意味着使用Vue时:




  1. 同样是开发业务,老前端与新前端差距不大




  2. 必要时后端经过简单的学习,也能接手部分需求




重申一下,我并不是说Vue不好,相反,他是很优秀的前端框架。这里只是从人性的角度分析,并且这个分析很有可能是主观、带有偏见的。


再看个正面例子 —— React HooksHooks对开发效率、组件复用性以及他对React未来发展的影响这里不赘述了。主要聊聊提高门槛




  1. 一方面,什么时候封装自定义Hook,如何封装自定义Hook,如何规避Hook的坑,老前端与新前端有比较大的差异




  2. 更重要的是,后端改改JSX还行,要改基于Hooks的组件逻辑,是有一定难度的




既提效,又提高门槛,我认为这才是Hooks在前端领域火热的原因。



同样的原因,从人性的角度,我很看好Vue Composition API



所以,前端编程范式迁移的本质是:把握提高效率提高门槛之间的平衡。


这个结论会成为后面预测未来开发模式的依据。


当范式无法再迁移时


当前端框架成为事实上的标准后很长一段时间,业界也在不断探索新的开发范式。


有一种开发模式每过几年都会被搬出来炒一遍,他就是低代码。用我们上面的结论来分析下:在市场选择的情况下,先抛开低代码是否能提高效率不谈,显然他的目的是降低门槛


从人性的角度出发,他就很难在程序员群体中自发传播开。


那么,如果没有新的范式出现,会发生什么事情?会内卷。


我们会发现,这几年前端的发展轨迹,就是在重复一件事:




  1. 围绕前端框架周边,不断探索各细分领域的最佳实践




  2. 当探索出最佳实践后,就把他集成到框架中




举个例子,React Router作为React技术栈中路由这一细分领域的一个开源库,经过长期迭代,逐渐成为主流路由方案之一。


React Router团队基于React Router开发出Remix这一React框架。



这么做,在没有新的范式出现前,也能基于当前范式(前端框架),达到上述2个目的:




  • 提高效率:框架集成了最佳实践,开发效率更高




  • 提高门槛:除了学习React,还得学习新的上层框架




类似的,各种CSS解决方案(比如tailwind css)也是同样的道理:




  • 提高效率:提高CSS编写效率




  • 提高门槛:新的概念、语法需要学习




那么,未来围绕提高效率提高门槛的平衡,前端开发模式会如何发展呢?


从考虑范式到考虑流程


首先,我认为,在有限的未来,不会出现新的更先进的范式能让前端领域普遍认可并大规模迁移(就像从jQuery到前端框架的迁移)。


那么,为了提高效率,除了改变范式范式内 内卷两个选择外,还有个选择 —— 让整个开发流程提效。


从需求文档到最终代码,存在4级抽象:




  1. PM用自然语言编写的需求文档




  2. 需求评审时,PM给开发描述需求后,开发脑海里形成的业务逻辑




  3. 开发根据业务逻辑划分各个模块或组件




  4. 开发实现各个模块或组件的具体代码




当前我们使用LLM辅助编程时(比如以chatGPT为例),主要是用自然语言输入模块或组件业务逻辑,再让模型输出具体代码。也就是借助模型自动完成从3到4级抽象的转变。


比如说下图我们让chatGPT实现一个计时器:



这个计时器可能是我们需求中的某个模块,在此chatGPT帮我们完成了从抽象3(实现一个计时器组件)到抽象4(计时器组件的代码)。


如果仅仅到这一步,只能说这是个更高效的辅助工具,并不能达到整个开发流程提效的程度。为了达到这种程度,我们需要让LLM帮我们完成从抽象1到4的整个过程。


LLM如何完成4级抽象转换


接下来我们来看,基于当前已有的模型,如何完成抽象1到抽象4的自动转换。


首先,来看抽象1(PM用自然语言编写的需求文档)。chatGPT当前已经掌握基础的理解能力,所以他是能够理解需求文档的含义的。


下图是我从网上找的某需求文档中的登录功能流程图:



以当前主流的GPT-3.5举例,虽然GPT-3.5不能理解图片(不能理解需求文档中的流程图),但我们可以将流程图用文字描述出来(最新的GPT-4已经拥有理解图片含义的能力)。


上述登录功能流程图可以用文字概括为:



  1. 打开App后有3个选项,分别是“账号密码登录”、“快捷登录”、“第三方登录”

  2. 选择“第三方登录”,进入第三方,同意授权后登录成功

  3. 选择“快捷登录”,输入手机号和验证码并选择身份,点击登录后登录成功

  4. 选择“账号密码登录”,输入手机号,如果已注册,输入密码,点击登录后登录成功

  5. 选择“账号密码登录”,输入手机号,如果未注册,进入注册页,输入手机号,如果手机号已注册,回到“账号密码登录”

  6. 选择“账号密码登录”,输入手机号,如果未注册,进入注册页,输入手机号,如果手机号未注册,填写手机号、验证码、密码、姓名、选择身份,点击注册,完毕


抽象1到抽象2


如何完成从抽象1到抽象2(业务逻辑)的转变呢?换句话说,如何用一种介于自然语言与实际代码之间的规范描述业务逻辑?


这种规范应该拥有完备的数据结构(类似JSONXML),因为这样会带来很多好处:




  • 相比于自然语言,用规范的数据结构表示的业务逻辑能够传达更准确的意图




  • 业务需求的不断增多,仅仅对应数据结构体积的增大,即使再复杂的业务,只需要分批将业务逻辑代表的数据结构投喂给模型,模型就能完全理解我们的业务




  • 数据结构可以保存在变量中,通过变量名就能指代业务逻辑,无需再用自然语言大段的向模型描述业务逻辑




我们可以利用SCXML(State Chart XML)格式。SCXML是由W3C定义的一种表示状态机XML格式,他能够表示状态之间的变化。


前端应用的本质其实就包括两部分:




  • 状态的变化




  • 状态到视图的映射




其中状态到视图的映射框架已经帮我们做了。所以,只要能表示状态的变化,其实就能表示业务逻辑。


现在,我们让chatGPT将流程图翻译为SCXML格式:



得到如下结构(你不用细看,了解个大概就行):


<scxml version="1.0" xmlns="http://www.w3.org/2005/07/scxml">
<state id="mainMenu">
<transition event="THIRD_PARTY_LOGIN" target="thirdPartyLogin" />
<transition event="QUICK_LOGIN" target="quickLogin" />
<transition event="ACCOUNT_LOGIN" target="accountLogin" />
</state>

<state id="thirdPartyLogin">
<onentry>
<send event="AGREE_AUTHORIZATION" />
<send event="LOGIN_SUCCESS" />
</onentry>
</state>

<state id="quickLogin">
<onentry>
<send event="INPUT_PHONE_NUMBER" />
<send event="INPUT_VERIFICATION_CODE" />
<send event="SELECT_IDENTITY" />
<send event="LOGIN_SUCCESS" />
</onentry>
</state>

<state id="accountLogin">
<state id="inputPhoneNumber">
<transition event="PHONE_NUMBER_SUBMITTED" target="checkRegistered" />
</state>

<state id="checkRegistered">
<onentry>
<send event="CHECK_REGISTERED" />
</onentry>

<transition event="REGISTERED" target="inputPassword" />
<transition event="UNREGISTERED" target="register" />
</state>

<state id="inputPassword">
<onentry>
<send event="INPUT_PASSWORD" />
<send event="LOGIN_SUCCESS" />
</onentry>
</state>

<state id="register">
<state id="inputPhoneNumber">
<transition event="PHONE_NUMBER_SUBMITTED" target="checkRegistered" />
</state>

<state id="checkRegistered">
<onentry>
<send event="CHECK_REGISTERED" />
</onentry>

<transition event="REGISTERED" target="inputPassword" />
<transition event="UNREGISTERED" target="inputUserInfo" />
</state>

<state id="inputUserInfo">
<onentry>
<send event="INPUT_PHONE_NUMBER" />
<send event="INPUT_VERIFICATION_CODE" />
<send event="INPUT_PASSWORD" />
<send event="INPUT_NAME" />
<send event="SELECT_IDENTITY" />
<send event="REGISTER_SUCCESS" />
</onentry>
</state>
</state>
</state>
</scxml>

至此,我们完成了抽象1到抽象2的转变。


抽象2到抽象3


SCXML格式没法直接在JS代码中使用。为了用代码实现逻辑,我们需要使用遵循SCXML规范的库。xstateJS中比较流行的状态机开源库。



所以接下来我们让chatGPT将上述SCXML格式转换为xstate语法:



得到结果(同样,具体代码你不用在意,了解我想表达的转换意思就行):


import { Machine, sendParent } from 'xstate';

const loginMachine = Machine({
id: 'login',
initial: 'mainMenu',
states: {
mainMenu: {
on: {
THIRD_PARTY_LOGIN: 'thirdPartyLogin',
QUICK_LOGIN: 'quickLogin',
ACCOUNT_LOGIN: 'accountLogin'
}
},
thirdPartyLogin: {
onEntry: [
sendParent('AGREE_AUTHORIZATION'),
sendParent('LOGIN_SUCCESS')
]
},
quickLogin: {
onEntry: [
sendParent('INPUT_PHONE_NUMBER'),
sendParent('INPUT_VERIFICATION_CODE'),
sendParent('SELECT_IDENTITY'),
sendParent('LOGIN_SUCCESS')
]
},
accountLogin: {
initial: 'inputPhoneNumber',
states: {
inputPhoneNumber: {
on: {
PHONE_NUMBER_SUBMITTED: 'checkRegistered'
}
},
checkRegistered: {
onEntry: sendParent('CHECK_REGISTERED'),
on: {
REGISTERED: 'inputPassword',
UNREGISTERED: 'register.inputPhoneNumber'
}
},
inputPassword: {
onEntry: [
sendParent('INPUT_PASSWORD'),
sendParent('LOGIN_SUCCESS')
]
},
register: {
initial: 'inputPhoneNumber',
states: {
inputPhoneNumber: {
on: {
PHONE_NUMBER_SUBMITTED: 'checkRegistered'
}
},
checkRegistered: {
onEntry: sendParent('CHECK_REGISTERED'),
on: {
REGISTERED: 'inputPassword',
UNREGISTERED: 'inputUserInfo'
}
},
inputPassword: {
onEntry: [
sendParent('INPUT_PASSWORD'),
sendParent('REGISTER_SUCCESS')
]
},
inputUserInfo: {
onEntry: [
sendParent('INPUT_PHONE_NUMBER'),
sendParent('INPUT_VERIFICATION_CODE'),
sendParent('INPUT_PASSWORD'),
sendParent('INPUT_NAME'),
sendParent('SELECT_IDENTITY'),
sendParent('REGISTER_SUCCESS')
]
}
}
}
}
}
}
});

export default loginMachine;

这段代码我们可以直接粘贴到xstate的可视化编辑器中查看:



图中初始状态可以转移到3个状态(这些状态都是chatGPT生成的),其中:




  • QUICK_LOGIN —— 快捷登录




  • ACCOUNT_LOGIN —— 账号密码登录




  • THIRD_PARTY_LOGIN —— 第三方登录




每个状态接下来的变化逻辑都清晰可见。比如,当进入ACCOUNT_LOGIN状态后,后续会根据是否登录(UNREGISTEREDREGISTERED)进入不同逻辑:



也就是说,chatGPT理解了需求文档想表达的业务逻辑后,将业务逻辑转换成代码表示。



读者可将上述xstate代码复制到可视化编辑器中看到效果



抽象3到抽象4


接下来,我们只需要让chatGPT根据上述xstate状态机生成组件代码即可。


这时有同学会问:chatGPT对话有token限制,没法生成太多代码怎么办?


实际上,这可能并不是坏事。在我曾经供职的一家公司,前端团队有条不成文的规矩 —— 如果一个组件超过200行,那你就应该拆分他。


同样的,如果chatGPT生成的组件超过了token限制,那么应该让他拆分新的组件。


拆分组件的前提是 —— chatGPT需要懂业务逻辑。显然,他已经懂了xstate数据结构所代表的业务逻辑。


更妙的是,我们可以让chatGPTSCXML格式转换而来的xstate数据结构保存在一个变量中,在后续对话中,我们用一个变量名就能指代他背后所表示的业务逻辑(这里保存在变量m中)。



当我们要生成业务组件代码时,让chatGPT从模块中导出m实现组件逻辑:



对于实际场景下比较复杂的需求,经过从抽象1到抽象3的转换,我们会得到代表业务逻辑的不同变量,比如:




  • signin变量代表登录逻辑




  • login变量代表注册逻辑




  • PopupAD变量代表弹窗广告逻辑




如果弹窗广告的逻辑和是否登录相关,那么要实现弹窗广告组件代码只需要告诉chatGPT


根据signinPopupAD实现弹窗广告的react组件,其中signin变量由xxx模块导出,PopupAD变量由yyy导出。


如果你司使用其他框架,只需将其中react换成其他框架名即可。当大家还在争论哪个框架更优秀时,LLM已经悄悄帮开发者实现了框架自由


新开发模式的优势


让我们从提高效率提高门槛的角度分析这种新开发模式的优势。


提高效率


首先,这种新模式能显著提高开发效率。本质来说,他将前端工程师从实现需求的角色转变为review代码的角色。


极端的讲,当需求评审会结束的那一刻,第一版前端代码就生成了。


其次,他能解放部分测试同学的生产力(抢部分测试同学的活儿)。对于维护过屎山代码的同学,肯定遇到过这样的场景:明明只是改动一个小需求,测试问你改动影响的范围,你自己都不清楚会有多大影响,为了稳妥起见只能让测试覆盖更大的回归测试范围。


在使用基于状态机的开发模式后,任何改动会造成的影响在状态图中都清晰可见。同时,由于代码逻辑的实现基于状态机,可以据此自动生成端到端的测试用例,模型也能根据状态机描述的逻辑自己补足其他单测。


提高门槛


接下来,我们从提高门槛的角度分析。


首先,能够对模型生成的代码进行查漏补缺本身就要求开发者有一定前端开发水平。


其次,这种开发模式引入了新的抽象层 —— 状态机,这无疑会增加上手门槛。


但这都不是最重要的,最重要的是 —— 这套模式强迫前端开发需要更懂业务。


以前,拿到产品的需求文档后,你可以在做的过程中遇到不懂的再问产品。使用新的开发模式后,你必须很懂业务,做到在需求评审时就能指出需求文档中不合理的地方


因为当需求评审结束后,你会将这份需求文档投喂给模型直接生成业务代码(中间会经历生成SCXML生成xstate数据结构保存xstate变量、使用变量生成组件代码)。


当大家技术水平旗鼓相当时,懂业务才是前端的核心竞争力。


综上,这套开发模式在极大提高效率的同时提高了门槛,我认为在未来很有可能成为主流前端开发模式。


作者:魔术师卡颂
来源:juejin.cn/post/7216182763237818425
收起阅读 »

快速入门 GraphQL:一个接口实现所有 CRUD

web
作为前端开发,想必经常做的事情就是:调接口、画页面、调接口、画页面... 调用的接口大概率是 restful 的,也就是类似这种: /students 查询所有学生信息 /student/1 查询 id 为 1 的学生信息 上面说的是 get 请求。 如果对 ...
继续阅读 »

作为前端开发,想必经常做的事情就是:调接口、画页面、调接口、画页面...


调用的接口大概率是 restful 的,也就是类似这种:


/students 查询所有学生信息


/student/1 查询 id 为 1 的学生信息


上面说的是 get 请求。


如果对 /student/1 发送 POST、PUT、DELETE 请求,就分别代表了新增、修改、删除。


这就是 restful 风格的 web 接口。


这种接口返回什么信息是服务端那边决定的,客户端只是传一下参数。


而不同场景下需要的数据不同,这时候可能就得新开发一个接口。特别是在版本更新的时候,接口会有所变动。


这样就很容易导致一大堆类似的接口。


facebook 当时也遇到了这个问题,于是他们创造了一种新的接口实现方案:GraphQL。


用了 GraphQL 之后,返回什么数据不再是服务端说了算,而是客户端自己决定。


服务端只需要提供一个接口,客户端通过这个接口就可以取任意格式的数据,实现 CRUD。


比如想查询所有的学生,就可以这样:



想再查询他们的年龄,就可以这样:



想查询老师的名字和他教的学生,就可以这样:



而这些都是在一个 http 接口里完成的!


感受了 GraphQL 的好处了没?


一个 http 接口就能实现所有的 CRUD!


那这么强大的 GraphQL 是怎么实现的呢?


我们先写个 demo 快速入门一下:


facebook 提供了 graphql 的 npm 包,但那个封装的不够好,一般我们会用基于 graphql 包的 @apollo/server 和 @apollo/client 的包来实现 graphql。


首先引入这个包:


import { ApolloServer } from '@apollo/server';

然后写一段这样的代码:


import { ApolloServer } from '@apollo/server';

const typeDefs = `
type Student {
id: String,
name: String,
sex: Boolean
age: Int
}

type Teacher {
id: String,
name: String,
age: Int,
subject: [String],
students: [Student]
}

type Query {
students: [Student],
teachers: [Teacher],
}

schema {
query: Query
}
`
;

比较容易看懂,定义了一个 Student 的对象类型,有 id、name、sex、age 这几个字段。


又定义了一个 Teacher 的对象类型,有 id、name、age、subject、students 这几个字段。students 字段是他教的学生的信息。


然后定义了查询的入口,可以查 students 和 teachers 的信息。


这样就是一个 schema。


对象类型和对象类型之间有关联关系,老师关联了学生、学生也可以关联老师,关联来关联去这不就是一个图么,也就是 graph。


GraphQL 全称是 graph query language,就是从这个对象的 graph 中查询数据的。


现在我们声明的只是对象类型的关系,还要知道这些类型的具体数据,取数据的这部分叫做 resolver。


const students = [
{
id: '1',
name: async () => {
await '取数据';
return '光光'
},
sex: true,
age: 12
},
{
id: '2',
name:'东东',
sex: true,
age: 13
},
{
id: '3',
name:'小红',
sex: false,
age: 11
},
];

const teachers = [
{
id: '1',
name: '神光',
sex: true,
subject: ['体育', '数学'],
age: 28,
students: students
}
]

const resolvers = {
Query: {
students: () => students,
teachers: () => teachers
}
};

resolver 是取对象类型对应的数据的,每个字段都可以写一个 async 函数,里面执行 sql、访问接口等都可以,最终返回取到的数据。


当然,直接写具体的数据也是可以的。


这里我就 student 里那个 name 用 async 函数的方式写了一下。


这样有了 schema 类型定义,有了取数据的 resovler,就可以跑起 graphql 服务了。


也就是这样:


import { startStandaloneServer } from '@apollo/server/standalone' 

const server = new ApolloServer({
typeDefs,
resolvers,
});

const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
});

console.log(`🚀 Server ready at: ${url}`);

传入 schema 类型定义和取数据的 resolver,就可以用 node 把服务跑起来。



有同学可能问了,node 可以直接解析 esm 模块么?


可以的。只需要在 package.json 中声明 type 为 module:



那所有的 .js 就都会作为 esm 模块解析:



跑起来之后,浏览器访问一下:


就可以看到这样的 sandbox,这里可以执行 graphql 的查询:



(graphql 接口是监听 POST 请求的,用 get 请求这个 url 才会跑这个调试的工具)


我查询所有学生的 id、name、age 就可以这样:




这里 “光光” 那个学生是异步取的数据,resolver 会执行对应的异步函数,拿到最终数据:



取老师的信息就可以这样:



这样我们就实现了一个 graphql 接口!


感觉到什么叫客户端决定取什么数据了么?


当然,我们这里是在 sandbox 里测的,用 @apollo/client 包也很简单。


比如 react 的 graphql 客户端是这样的:



一个 gql 的 api 来写查询语言,一个 useQuery 的 api 来执行查询。


学起来很简单。


我们之后还是直接在 sandbox 里测试。


有的同学可能会说,如果我想查询某个名字的老师的信息呢?


怎么传参数?


graphql 当然是支持的,这样写:


type Query {
students: [Student],
teachers: [Teacher],
studentsbyTeacherName(name: String!): [Student]
}

新加一个 query 入口,声明一个 name 的参数。(这里 String 后的 ! 代表不能为空)


然后它对应的 resolver 就是这样的:


const resolvers = {
Query: {
students: () => students,
teachers: () => teachers,
studentsbyTeacherName: async (...args) => {
console.log(args);

await '执行了一个异步查询'
return students
}
}
};

studentsbyTeacherName 字段的 resolver 是一个异步函数,里面执行了查询,然后返回了查到的学生信息。


我们打印下参数看看传过来的是什么。


有参数的查询是这样的:



传入老师的 name 参数为 111,返回查到的学生的 id、name 信息。


可以看到返回的就是查询到的结果。


而服务端的 resolver 接收到的参数是这样的:



其余的几个参数不用管,只要知道第二个参数就是客户端传过来的查询参数就好了。


这样我们就可以根据这个 name 参数实现异步的查询,然后返回数据。


这就实现了有参数的查询。


不是说 graphql 能取代 restful 做 CRUD 么?那增删改怎么做呢?


其实看到上面的有参数的查询应该就能想到了,其实写起来差不多。


在 schema 里添加这样一段类型定义:


type Res {
success: Boolean
id: String
}

type Mutation {
addStudent(name:String! age:Int! sex:Boolean!): Res

updateStudent(id: String! name:String! age:Int! sex:Boolean!): Res

deleteStudent(id: String!): Res
}

schema {
mutation: Mutation
query: Query
}

和有参数的查询差不多,只不过这部分增删改的类型要定义在 mutation 部分。


然后 resolver 也要有对应的实现:


async function addStudent (_, { name, age, sex }) {
students.push({
id: '一个随机 id',
name,
age,
sex
});
return {
success: true,
id: 'xxx'
}
}

async function updateStudent (_, { id, name, age, sex }) {

return {
success: true,
id: 'xxx'
}
}

async function deleteStudent (_, { id }) {
return {
success: true,
id: 'xxx'
}
}

const resolvers = {
Query: {
students: () => students,
teachers: () => teachers,
studentsbyTeacherName: async (...args) => {
console.log(args);

await '执行了一个异步查询'
return students
}
},
Mutation: {
addStudent: addStudent,
updateStudent: updateStudent,
deleteStudent: deleteStudent
}
};


和 query 部分差不多,只不过这里实现的是增删改。


我只对 addStudent 做了实现。


我们测试下:


执行 addStudent,添加一个学生:



然后再次查询所有的学生:



就可以查到刚来的小刚同学。


这样,我们就可以在一个 graphql 的 POST 接口里完成所有的 CRUD!


全部代码如下,感兴趣可以跑一跑(注意要在 package.json 里加个 type: "module")


import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone'

const typeDefs = `
type Student {
id: String,
name: String,
sex: Boolean
age: Int
}

type Teacher {
id: String,
name: String,
age: Int,
subject: [String],
students: [Student]
}

type Query {
students: [Student],
teachers: [Teacher],
studentsbyTeacherName(name: String!): [Student]
}

type Res {
success: Boolean
id: String
}

type Mutation {
addStudent(name:String! age:Int! sex:Boolean!): Res

updateStudent(id: String! name:String! age:Int! sex:Boolean!): Res

deleteStudent(id: String!): Res
}

schema {
mutation: Mutation
query: Query
}
`;

const students = [
{
id: '1',
name: async () => {
await '取数据';
return '光光'
},
sex: true,
age: 12
},
{
id: '2',
name:'东东',
sex: true,
age: 13
},
{
id: '3',
name:'小红',
sex: false,
age: 11
},
];

const teachers = [
{
id: '1',
name: '神光',
sex: true,
subject: ['体育', '数学'],
age: 28,
students: students
}
]

async function addStudent (_, { name, age, sex }) {
students.push({
id: '一个随机 id',
name,
age,
sex
});
return {
success: true,
id: 'xxx'
}
}

async function updateStudent (_, { id, name, age, sex }) {

return {
success: true,
id: 'xxx'
}
}

async function deleteStudent (_, { id }) {
return {
success: true,
id: 'xxx'
}
}

const resolvers = {
Query: {
students: () => students,
teachers: () => teachers,
studentsbyTeacherName: async (...args) => {
console.log(args);

await '执行了一个异步查询'
return students
}
},
Mutation: {
addStudent: addStudent,
updateStudent: updateStudent,
deleteStudent: deleteStudent
}
};

const server = new ApolloServer({
typeDefs,
resolvers,
});

const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
});

console.log(`🚀 Server ready at: ${url}`);

完成了 graphql 的入门,我们再稍微思考下它的原理。graphql 是怎么实现的呢?


回顾整个流程,我们发现涉及到两种 DSL(领域特定语言),一个是 schema 定义的 DSL,一个是查询的 DSL。


服务端通过 schema 定义的 DSL 来声明 graph 图,通过 resolver 来接受参数,执行查询和增删改。


客户端通过查询的 DSL 来定义如何查询和如何增删改,再发给服务端来解析执行。


通过这种 DSL 实现了动态的查询。


确实很方便很灵活,但也有缺点,就是 parse DSL 为 AST 性能肯定是不如 restful 那种直接执行增删改查高的。


具体要不要用 graphql 还是要根据具体场景来做判断。


总结


restful 接口是 url 代表资源,GET、POST、PUT、DELETE 请求代表对资源的增删改查。


这种接口返回什么数据完全由服务端决定,每次接口变动可能就得新加一种接口。


为了解决这种问题,facebook 创造了 graphql,这种接口返回什么数据完全由客户端决定。增删改查通过这一个接口就可以搞定。


graphql 需要在服务端定义 schema,也就是定义对象类型和它的字段,对象类型和对象类型之间会有关联,也就是一个 graph,查询就是从这个 graph 里查询数据。


除了 schema 外,还需要有 resolver,它负责接受客户端的参数,完成具体数据的增删改查。


graphql 会暴露一个 post 接口,通过查询语言的语法就可以从通过这个接口完成所有增删改查。


本地测试的时候,get 请求会跑一个 sandbox,可以在这里测试接口。


整个流程涉及到两种新语言: schema 定义语言和 query 查询语言。入门之后向深入的话就是要学下这两种 DSL 的更多语法。


感受到 graphql 的强大之处了么?一个接口就可以实现所有的 CRUD!


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

打造你自己的 JavaScript 运行时

web
原文:deno.com/blog/roll-y… 译者:李瑞丰 在这篇文章中,我们将介绍如何创建自定义 JavaScript 运行时。我们称之为 runjs。想象一下,我们正在构建一个(更)简化的 deno 版本。这篇文章的目标是创建一个 CLI,可以执行本...
继续阅读 »

原文:deno.com/blog/roll-y…
译者:李瑞丰



在这篇文章中,我们将介绍如何创建自定义 JavaScript 运行时。我们称之为 runjs。想象一下,我们正在构建一个(更)简化的 deno 版本。这篇文章的目标是创建一个 CLI,可以执行本地 JavaScript 文件,读取文件,写入文件,删除文件,并具有简化的 console API。


让我们开始吧。


前提


这篇教程假设读者具有以下知识:



  • Rust 基础知识

  • JavaScript 事件循环基础知识


确保你的机器上安装了 Rust(以及 cargo),并且它至少是 1.62.0 版本。访问 rust-lang.org 安装 Rust 编译器和 cargo


确保我们已经准备好了:


$ cargo --version
cargo 1.62.0 (a748cf5a3 2022-06-08)

Hello, Rust!


首先,让我们创建一个新的 Rust 项目,它将是一个名为 runjs 的二进制 crate:


$ cargo init --bin runjs
Created binary (application) package

让我们进入 runjs 目录并在编辑器中打开它。确保一切都设置正确:


$ cd runjs
$ cargo run
Compiling runjs v0.1.0 (/Users/ib/dev/runjs)
Finished dev [unoptimized + debuginfo] target(s) in 1.76s
Running `target/debug/runjs`
Hello, world!

很好!现在让我们开始创建我们自己的 JavaScript 运行时。


依赖


接下来,让我们将 deno_coretokio 依赖项添加到我们的项目中:


$ cargo add deno_core
Updating crates.io index
Adding deno_core v0.142.0 to dependencies.
$ cargo add tokio --features=full
Updating crates.io index
Adding tokio v1.19.2 to dependencies.

我们更新后的 Cargo.toml 文件应该如下所示:


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

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

[dependencies]
deno_core = "0.142.0"
tokio = { version = "1.19.2", features = ["full"] }

deno_core 是 Deno 团队的一个 crate,它抽象了与 V8 JavaScript 引擎的交互。V8 是一个复杂的项目,有成千上万的 API,因此为了简化使用它们,deno_core 提供了一个 JsRuntime 结构体,它封装了一个 V8 引擎实例(称为 Isolate),并允许与事件循环集成。


tokio 是一个异步的 Rust 运行时,我们将使用它作为事件循环。Tokio 负责与操作系统抽象(如网络套接字或文件系统)进行交互。deno_coretokio 一起,允许 JavaScript 的 Promise 映射到 Rust 的 Future


拥有 JavaScript 引擎和事件循环,使我们能够创建 JavaScript 运行时。


Hello, runjs!


让我们从编写一个异步的 Rust 函数开始,该函数将创建一个 JsRuntime 实例,该实例负责 JavaScript 执行。


// main.rs
use std::rc::Rc;
use deno_core::error::AnyError;

async fn run_js(file_path: &str) -> Result<(), AnyError> {
let main_module = deno_core::resolve_path(file_path)?;
let mut js_runtime = deno_core::JsRuntime::new(deno_core::RuntimeOptions {
module_loader: Some(Rc::new(deno_core::FsModuleLoader)),
..Default::default()
});

let mod_id = js_runtime.load_main_module(&main_module, None).await?;
let result = js_runtime.mod_evaluate(mod_id);
js_runtime.run_event_loop(false).await?;
result.await?
}

fn main() {
println!("Hello, world!");
}

这里有很多东西要解释。异步的 run_js 函数创建了一个新的 JsRuntime 实例,该实例使用基于文件系统的模块加载器。之后,我们将模块加载到 js_runtime 运行时中,对其进行评估,并运行一个事件循环直到完成。


这个 run_js 函数封装了我们的 JavaScript 代码将要经历的整个生命周期。但是在我们能够这样做之前,我们需要创建一个单线程的 tokio 运行时,以便能够执行我们的 run_js 函数:


// main.rs
fn main() {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
if let Err(error) = runtime.block_on(run_js("./example.js")) {
eprintln!("error: {}", error);
}
}

让我们尝试执行一些 JavaScript 代码!创建一个 example.js 文件,它将打印 "Hello runjs!":


// example.js
Deno.core.print("Hello runjs!");

注意,我们使用的是 Deno.core 中的 print 函数 - 这是一个全局可用的内置对象,由 deno_core Rust crate 提供。


现在运行它:


cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.05s
Running `target/debug/runjs`
Hello runjs!⏎

成功!在仅 25 行 Rust 代码中,我们创建了一个简单的 JavaScript 运行时,可以执行本地文件。当然,此时此运行时不能做太多事情(例如,console.log 还不能工作 - 尝试一下!),但是我们已经将 V8 JavaScript 引擎和 tokio 集成到我们的 Rust 项目中。


添加 console API


让我们开始处理 console API。首先,创建 src/runtime.js 文件,该文件将实例化并使 console 对象全局可用:


// runtime.js
((globalThis) => {
const core = Deno.core;

function argsToMessage(...args) {
return args.map((arg) => JSON.stringify(arg)).join(" ");
}

globalThis.console = {
log: (...args) => {
core.print(`[out]: ${argsToMessage(...args)}\n`, false);
},
error: (...args) => {
core.print(`[err]: ${argsToMessage(...args)}\n`, true);
},
};
})(globalThis);

函数 console.logconsole.error 将接受多个参数,将它们转换为 JSON(以便我们可以检查非原始 JS 对象)并在每个消息前加上 logerror 前缀。这是一个“普通的” JavaScript 文件,就像我们在 ES 模块之前在浏览器中编写 JavaScript 一样。


为了确保我们不会污染全局作用域,我们在 IIFE 中执行此代码。如果我们没有这样做,那么 argsToMessage 辅助函数将在我们的运行时中全局可用。


现在,让我们将此代码包含在我们的二进制文件中,并在每次运行时执行:


let mut js_runtime = deno_core::JsRuntime::new(deno_core::RuntimeOptions {
module_loader: Some(Rc::new(deno_core::FsModuleLoader)),
..Default::default()
});
+ js_runtime.execute_script("[runjs:runtime.js]", include_str!("./runtime.js")).unwrap();

最后,让我们使用我们的新 console API 更新 example.js


- Deno.core.print("Hello runjs!");
+ console.log("Hello", "runjs!");
+ console.error("Boom!");

再次运行它:


cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.05s
Running `target/debug/runjs`
[out]: "Hello" "runjs!"
[err]: "Boom!"

它起作用了!现在让我们添加一个 API,它将允许我们与文件系统进行交互。


添加一个基本的文件系统 API


让我们从更新我们的 runtime.js 文件开始:


};

+ core.initializeAsyncOps();
+ globalThis.runjs = {
+ readFile: (path) => {
+ return core.ops.op_read_file(path);
+ },
+ writeFile: (path, contents) => {
+ return core.ops.op_write_file(path, contents);
+ },
+ removeFile: (path) => {
+ return core.ops.op_remove_file(path);
+ },
+ };
})(globalThis);

我们刚刚添加了一个新的全局对象,称为 runjs,它有三个方法:readFilewriteFileremoveFile。前两个方法是异步的,而第三个是同步的。


你可能想知道这些 core.ops.[op name] 调用是什么 - 它们是 deno_core crate 中用于绑定 JavaScript 和 Rust 函数的机制。当你调用其中任何一个时,deno_core 将查找具有 #[op] 属性和匹配名称的 Rust 函数。


让我们通过更新 main.rs 来看看它的作用:


+ use deno_core::op;
+ use deno_core::Extension;
use deno_core::error::AnyError;
use std::rc::Rc;

+ #[op]
+ async fn op_read_file(path: String) -> Result<String, AnyError> {
+ let contents = tokio::fs::read_to_string(path).await?;
+ Ok(contents)
+ }
+
+ #[op]
+ async fn op_write_file(path: String, contents: String) -> Result<(), AnyError> {
+ tokio::fs::write(path, contents).await?;
+ Ok(())
+ }
+
+ #[op]
+ fn op_remove_file(path: String) -> Result<(), AnyError> {
+ std::fs::remove_file(path)?;
+ Ok(())
+ }

我们刚刚添加了三个可以从 JavaScript 调用的 ops。但是,在这些 ops 可用于我们的 JavaScript 代码之前,我们需要通过注册“扩展”来告诉 deno_core


async fn run_js(file_path: &str) -> Result<(), AnyError> {
let main_module = deno_core::resolve_path(file_path)?;
+ let runjs_extension = Extension::builder("runjs")
+ .ops(vec![
+ op_read_file::decl(),
+ op_write_file::decl(),
+ op_remove_file::decl(),
+ ])
+ .build();
let mut js_runtime = deno_core::JsRuntime::new(deno_core::RuntimeOptions {
module_loader: Some(Rc::new(deno_core::FsModuleLoader)),
+ extensions: vec![runjs_extension],
..Default::default()
});

Extensions 允许你配置你的 JsRuntime 实例,并将不同的 Rust 函数暴露给 JavaScript,以及执行更高级的操作,如加载其他 JavaScript 代码。


让我们再次更新我们的 example.js


console.log("Hello", "runjs!");
console.error("Boom!");
+
+ const path = "./log.txt";
+ try {
+ const contents = await runjs.readFile(path);
+ console.log("Read from a file", contents);
+ } catch (err) {
+ console.error("Unable to read file", path, err);
+ }
+
+ await runjs.writeFile(path, "I can write to a file.");
+ const contents = await runjs.readFile(path);
+ console.log("Read from a file", path, "contents:", contents);
+ console.log("Removing file", path);
+ runjs.removeFile(path);
+ console.log("File removed");
+

再次运行它:



$ cargo run
Compiling runjs v0.1.0 (/Users/ib/dev/runjs)
Finished dev [unoptimized + debuginfo] target(s) in 0.97s
Running `target/debug/runjs`
[out]: "Hello" "runjs!"
[err]: "Boom!"
[err]: "Unable to read file" "./log.txt" {"code":"ENOENT"}
[out]: "Read from a file" "./log.txt" "contents:" "I can write to a file."
[out]: "Removing file" "./log.txt"
[out]: "File removed"


恭喜,我们的 runjs 运行时现在可以与文件系统一起工作!注意,从 JavaScript 调用 Rust 代码所需的代码量非常少 - deno_core 负责在 JavaScript 和 Rust 之间传递数据,因此我们不需要自己进行任何转换。


总结


在这个简短的例子中,我们开始了一个集成了强大的 JavaScript 引擎(V8)和高效的事件循环实现(tokio)的 Rust 项目。


本文由 李瑞丰 翻译,原文地址:deno.com/blog/roll-y…


此教程的第二部分已经发布,实现了 fetch-like API 并添加了 TypeScript 转译功能。


完整的示例代码可以在 denoland 的 GitHub。也可以在译者的仓库查看第一部分代码



作者:李瑞丰_liruifengv
来源:juejin.cn/post/7218466428766453817


收起阅读 »

做个清醒的程序员之成为少数派

阅读时长约10分钟,统计2604个字。 这是一篇插队的文章。 本来我是有备稿,准备在下周一的时候发布,结果就在上周二,发生了一件事情。这件事情让我产生很多启发,我在这里把它分享给你,希望对你也有所启发。 周二下午,有位老兄加我微信,备注来自博客。这也不足为奇,...
继续阅读 »

阅读时长约10分钟,统计2604个字。


这是一篇插队的文章。


本来我是有备稿,准备在下周一的时候发布,结果就在上周二,发生了一件事情。这件事情让我产生很多启发,我在这里把它分享给你,希望对你也有所启发。


周二下午,有位老兄加我微信,备注来自博客。这也不足为奇,最近更新比较频繁,加了很多人。这位老兄一上来先是肯定了我的文章,随后指出了文中的错误。坦率地讲,自从复活博客之后,这还是第一位指出我错误的朋友,一下子我就来了兴趣。


在本系列文集的《序》中,我原文是这样写的:



我一直奉行一句话:“有道无术,尚可求也;有术无道,则止于术”。这句话出自老子的《道德经》,而且很好理解。



他指出《道德经》里其实没有这句话。但是呢,本着对读者负责的态度,我在写文章的时候确实去查了一下。程序员这个职业大家都懂,比较较真,至少我是这样的。于是我就找到了一些依据,来证明我说的是对的。但很快便发现事实其实不是这样,这位老兄所言非虚,我引的这句话确实并不出自《道德经》。所以,我要在这里向所有读过我上篇文章的朋友道个歉。澄清一下:“有道无术,尚可求也;有术无道,则止于术”,尽管这句话有几分道理,但真的不是《道德经》原文。


好了,故事就到这里结束了。说到这,大家应该也能理解我为什么要把这篇文章拿来插队。一方面趁热打铁,有错误及时声明,另一方面这个故事对我有新的启发。


这位老兄,名为张鸿羽。稍加细聊后,我得知鸿羽兄是有背过原文的,而我没有。我只是看到大部分都这样说,便信以为真,然后也跟着这样说。显然,我成为了大多数人中的一份子。而鸿羽兄是少数派中的一份子。有时候,真理真的掌握在少数人手中。


回想过去几年的工作历程,特别是刚开始工作的那几年,我做的很多工作都是“探索型”的。所谓“探索型”,就是对新技术,或者说是公司的研发部门未曾使用过的技术进行尝试摸索。当然,尝试新技术之前,要能发现新技术。而一项新技术的诞生,总会伴随着官方的宣传,以及一些支持它、拥护它的人高声叫好。但只有真正尝试过,特别是用新技术来实现较为复杂系统的时候,才会知道这项新技术到底优势在哪,劣势又在哪。


诚然,如果让我来总结我尝试新技术、新框架的经验,我会说:大部分新技术或是框架确实弥补了已有框架的不足,但其弥补的程度往往并不是质变的,只是小步优化。甚至有些新兴技术在弥补的同时,还引入了其它的问题。这对于使用它的开发者来说,的确是个坏消息。


但话说回来,没尝试用过,又怎能知道答案呢?技术的发展本就是这样一步一个坎,有时候走一步还退两步的呀。


这或许就是我等软件开发者的宿命,对于现存的技术框架,总是有这样或那样的不满意,觉得用着不顺手。期盼着某一天,某个技术大佬,或者团体,发明了一种新的框架,甚至是新的编程语言。或是直接起义,自己创造一款新的技术框架,能真正地解决那些令我们不满的问题,让软件开发编程成为真正的享受。


但现实是,很多新的技术框架的诞生,都伴随着类似的口号。也总会有勇敢的开发者尝鲜,也总会经历被坑,然后不断填坑的过程。而这些敢于尝鲜的开发者,就是那些最终会成为“少数派”的人。他们知道在各种美好的宣传背后,隐藏着多深的坑。对于这些坑,又该用什么方法去填。


“少数派”或许才是那些头脑最清醒的那一小撮人群。


但是,成为“少数派”不仅意味着失败的尝试,还有大多数人的不理解。甚至更严重一些,就是诋毁,百口莫辩。这需要一颗强大的内心,和与时间做朋友的勇气以及态度。


不过,我为什么鼓励程序员要做“少数派”,而不是成为“大多数”呢?还有另外一个原因,那就是由行业特征决定的。我相信程序员大多都活跃在互联网行业,这个行业是赢家通吃的指数型结构。有点类似财富分配,大部分的财富掌握在少数人的手里。而且无论如何数学建模,或是提高那些穷人的初始资金,最终推演的结局依然如此。


如今,在中国,乃至全世界,所谓“互联网大厂”无非就是那几家,而剩下的呢?数字上远远超过我们熟知的那些大厂,但拥有的财富值却位于指数图表中的长尾之中。这就是指数型的行业的特征,也是程序员这个群体的特征。


如果大家有查相关的数据,可以发现优秀程序员的工作效率往往是普通程序员的好几倍,尽管薪水上的差距不是这样。而大多数都是普通程序员,优秀程序员只属于“少数派”。优秀程序员,拿到需求,会做足够的分析,到了动手的时候,则像个流水线的工人;普通程序员,拿到需求就想赶快动手,面临的有可能是回炉重造。优秀程序员,会充分考虑到使用场景,采用防御式编程来规避可能带来的缺陷;普通程序员,想的只是实现需求,把程序健壮性扔给测试人员。优秀程序员,会考虑代码的可读性,为代码添加合适的注释、每个方法或函数的功能单一、清晰;普通程序员,急于求成,不注重代码规范,导致日后维护困难……


但是,追求效率和追求质量,大多数公司都会选择前者。但做多和做好,结果往往相差甚远。


大部分人倾向于做多、扩张、追求规模化。但殊不知做大的后果往往是成本的上升,利润却不一定变高。但做好却不一样,它追求的是平衡收支,而不是盲目追求利润。更好的做法其实是在做好之前,不要做大。要相信好产品,自然会带来口碑。过分追求大规模,反倒会使高利润远去。而把事情做好的心态,看似发展得慢,实则是条捷径。


回顾我创作的历程,之前的我总想着多写,多写就是扩张,意味着规模。但这种心态往往做不出好书,因为这是效率当先,质量次之的做法。但我身边也有的人,创作很用心,不着急让书早日面试,很认真地创作,比我的速度慢一些。这便是把事情做好的心态。你猜结果如何?人家一年十几万的稿酬,我却只有可怜的几万块。


所以,上面那套理论并不是我胡乱写的,或是从哪本书里看到,就抄过来的。而是真的付出了血和泪,总结出的道理。在此,我劝你做个“清醒”的人。追求效率没错,一旦做得过火,则会适得其反。


另一方面,如果只想成为大多数,可不可以呢?当然也可以,只不过互联网行业或许不再适合。那些符合正态分布的行业才是想成为大多数的那类人的理想去处。


比如,餐饮行业。现在,大家可以想一想,有没有那家餐馆,或是哪个餐饮品牌,能做到赢家通吃?似乎没有,如果也去查这方面的数据,就会发现餐饮行业其实并不是指数分布,而是呈正态分布的。只要能做到普通中位数的水平,就OK了。


真正的高手一般都是“少数派”。他们不仅能力拔群,思考问题时的方法、对世界的认知和一般人都有区别。若要成为软件开发工程师中的“高手”,必须成为“少数派”,成为战场上的传说。


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

这一次,我还是想选择自由

辞职回老家有一周多了。 这几天我啥也没写,一直在考虑是继续找工作还是真正开始自由职业。 找下份工作肯定是有高和稳定的收入,但很有可能还是做不喜欢做的事情,可能是和之前一样一成不变的状态,每天准时上下班通勤,下班在一线大城市的出租屋里打游戏看小说,偶尔写点技术...
继续阅读 »

辞职回老家有一周多了。


这几天我啥也没写,一直在考虑是继续找工作还是真正开始自由职业。



找下份工作肯定是有高和稳定的收入,但很有可能还是做不喜欢做的事情,可能是和之前一样一成不变的状态,每天准时上下班通勤,下班在一线大城市的出租屋里打游戏看小说,偶尔写点技术文章。


自由职业的话收入不稳定,赚多赚少都要靠自己。但可以住在小县城的家里,有妈妈做的好吃的菜,有可爱粘人的猫猫,有我新买的投影仪可以和妈妈一起看电视,可以和美好的一切在一起。我不喜欢旅游之类的,宅在家里就已经是我最幸福的状态了。




而且具体做啥可以自己来决定,我有挺多想研究的东西的。


这两天也在面试了,还是那些八股文,卷来卷去的,没啥意思。可能如果真的去了字节,我会更不适应。要不还是不继续面了。


我去年也自由职业过,现在和那时候的区别是我粉丝更多了,技术积累也更多了,而且给我妈新买了个房子,可以在这里继续我的神光实验室。



上图是神光实验室 1.0,之前在老家附近租的一个出租屋。


神光实验室 2.0 是这样的,在新家里:




上次结束自由职业是因为我爸的要求,他说还是希望我有个正当工作。


现在我爸没了,没有人会阻止我了。


我没有负债,还有一定的积蓄,而且我现在啥也不干也有能养活自己的收入。




要不就再任性一次,在家里继续自己的技术梦想,继续搞神光实验室?🤔


就这么愉快的决定了!



这一次,我还是想遵循自己的内心,选择自由,选择和喜欢的一切在一起。


以后公众号会保持日更,其余时间写小册和准备出版的书。


努力一点的话,各方面应该还是可以的。



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

为什么说过早优化是万恶之源?

  Donald Knuth(高德纳)是一位计算机科学界的著名学者和计算机程序设计的先驱之一。他被誉为计算机科学的“圣经”《计算机程序设计艺术》的作者,提出了著名的“大O符号”来描述算法的时间复杂度和空间复杂度,开发了TeX系统用于排版科技文献,获得过图灵奖、...
继续阅读 »

  Donald Knuth(高德纳)是一位计算机科学界的著名学者和计算机程序设计的先驱之一。他被誉为计算机科学的“圣经”《计算机程序设计艺术》的作者,提出了著名的“大O符号”来描述算法的时间复杂度和空间复杂度,开发了TeX系统用于排版科技文献,获得过图灵奖、冯·诺伊曼奖、美国国家科学奖章等多项荣誉。今天要说的就是他所提出的一条软件设计重要原则 Premature optimization is the root of all evil 过早优化是万恶之源
在这里插入图片描述

为什么说“过早优化是万恶之源”? 我认为过早优化代码会让人陷入到错误的目标中去,从而忽视掉了最重要的目标。举个很简单的例子,你需要快速构建一个产品来抢占用户,你当下最重要的目标是让这个产品快速上线,而不是把这个产品打造的好用(在中国互联网下,这样的事数不胜数),如果你只关注到后者体验、性能问题而忽视了速度,在当下高度竞争的市场之下,你根本毫无机会。


  当然上面这个例子是从感性的层面说的,对很多程序猿来说也可能涉及不到产品层面的内容。我们从软件设计的层面,理性的来说,过早优化可能会导致以下的一些问题:




  1. 增加代码的复杂性:过度优化可能会导致代码的复杂性增加,从而降低代码的可读性和可维护性。如果代码过于复杂,可能会导致开发人员难以理解和维护代码,从而增加开发成本和时间。




  2. 耗费开发时间和资源:过度优化可能会导致开发人员花费大量时间和资源在代码的性能优化上,而忽略了其他重要的开发任务。这可能会导致项目进度延误和开发成本增加。




  3. 降低代码的可移植性:过度优化可能会导致代码的可移植性降低。如果代码过于依赖于特定的硬件或操作系统,可能会导致代码无法在其他环境中运行。




  4. 降低代码的可扩展性:过度优化可能会降低代码的可扩展性。如果代码过于依赖于特定的算法或数据结构,可能会导致代码无法适应未来的需求变化。




过早优化的典型案例


  在软件工程史上由于过度关注软件性能导致项目最终失败的案例比比皆是,比如我下面要说的一些项目,在软件工程史上都是非常知名的项目(当然可能有些新生代程序员已经不知道了)。




  1. IBM OS/360操作系统:在20世纪60年代,IBM公司开发了OS/360操作系统,这是当时最大的软件工程项目之一。在开发过程中,IBM公司过于关注代码的性能问题,导致代码的复杂性增加,开发时间延误,最终导致项目的失败。我知晓这个项目还是在我最近在阅读的一本软件工程经典书籍《人月神话》中,也推荐大家阅读下,这个项目虽然最终失败了,但也给整个软件工程领域留下了宝贵的经验。




  2. Netscape Navigator浏览器:在20世纪90年代,Netscape公司开发了Navigator浏览器,这是当时最流行的浏览器之一。在开发过程中,Netscape公司过于关注代码的性能问题,导致代码的复杂性增加,开发时间延误,最终导致浏览器市场份额严重下降。




  3. Windows Vista操作系统:在21世纪初,微软公司开发了Windows Vista操作系统,这是当时最大的软件工程项目之一。在开发过程中,微软公司过于关注代码的性能问题,导致代码的复杂性增加,开发时间延误,最终导致操作系统的用户体验不佳,市场反响不佳。话说这个操作系统我还用过呢,用户界面还是很漂亮的,很多UI设计也被沿用到了Window7中。




如何识别过早优化


  在软件开发过程中,如何判断是否过早优化呢?这里有一些概括性的判断标准,可以简单参考下:




  1. 是否存在性能问题:如果代码还没有性能问题,那么过早优化就是不必要的。因此,在进行优化之前,应该先测试代码的性能,确定是否存在性能问题。




  2. 是否优化了未来可能发生的问题:如果优化的是未来可能发生的问题,而不是当前存在的问题,那么就可能是过早优化。在进行优化之前,应该优先考虑当前存在的问题,而不是未来可能发生的问题。




  3. 是否牺牲了代码的可读性和可维护性:如果优化代码会导致代码的复杂性增加,降低代码的可读性和可维护性,那么就可能是过早优化。在进行优化之前,应该优先考虑代码的可读性、可维护性和可扩展性。




  4. 是否浪费了大量的开发时间和资源:如果优化代码会浪费大量的开发时间和资源,而不是提高代码的性能和效率,那么就可能是过早优化。在进行优化之前,应该评估优化的成本和收益,确定是否值得进行优化。




  判断是否过早优化需要根据具体情况进行评估。在进行优化之前,应该先测试代码的性能,确定是否存在性能问题。同时,也应该优先考虑代码的可读性、可维护性和可扩展性,避免过度优化。


总结


  作为一名在IT领域摸爬滚打多年的工程师,我深有体会地认识到过早优化是软件开发中的一大陷阱。在软件开发的初期,我们可能会过于关注代码的性能问题,而忽略了代码的可读性、可维护性和可扩展性。这种做法可能会导致代码的复杂性增加,降低代码的可读性和可维护性,甚至可能会浪费大量的开发时间和资源。


  在软件开发过程中,我们应该避免过早优化,而是优先考虑代码的可读性、可维护性和可扩展性。当需要进行性能优化时,应该在代码的基础上进行优化,通过分析性能瓶颈、优化算法和数据结构等方法来提高代码的性能和效率。同时,我们也应该意识到,性能优化并不是软件开发的唯一目标,我们还应该注重代码的可读性、可维护性和可扩展性,以便保证代码的质量和可靠性


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

意大利成首个封禁ChatGPT的国家!OpenAI CEO:我觉得没违规

【新智元导读】周五,意大利全国上下对 ChatGPT 下了封杀令。在 20 天内,OpenAI 拿不出补救措施的话,将会面临最高 2000 万欧元罚款。 就在昨天,意大利政府突然下令,封禁 ChatGPT! 很快,OpenAI 首席执行官 Sam Altman...
继续阅读 »

【新智元导读】周五,意大利全国上下对 ChatGPT 下了封杀令。在 20 天内,OpenAI 拿不出补救措施的话,将会面临最高 2000 万欧元罚款。


就在昨天,意大利政府突然下令,封禁 ChatGPT!


很快,OpenAI 首席执行官 Sam Altman 做出回应称,公司已经关停了当地的 ChatGPT 服务(但自认为并没有违反任何规定)。



自此,意大利成为了世界上第一个宣布禁用 ChatGPT 的国家。


(其他国家和地区无法使用 ChatGPT,是因为 OpenAI 一开始就没给开通。)


意大利封禁 ChatGPT,全面调查隐私问题


3 月 31 日,意大利国家隐私监管机构正式下令封禁 ChatGPT,并指控 OpenAI「非法收集个人数据」。


值得注意的是,该「临时禁令」将一直生效,直到 OpenAI 能够尊重欧盟具有里程碑意义的隐私法——通用数据保护条例(GDPR)。



文件地址:http://www.garanteprivacy.it/home/docweb…


根据意大利国家数据保护机构 GPDP 发布新闻稿,OpenAI 收集用户数据并用于算法「训练」的行为,缺乏法律依据。


同时,3 月 20 日涉及用户对话和支付信息的泄露事件,也明确地暴露出了 OpenAI 在个人信息的处理上存在着问题。


除此之外,OpenAI 虽然声称 ChatGPT 面向的是 13 岁以上的用户,但由于缺乏相应的年龄验证机制,因此未成年人很可能会看到超出其自身发展和自我认知水平的信息。


监管机构表示,OpenAI 有 20 天的时间,如不给出可行的补救措施,则会面临最高 2000 万欧元,或全球总年营业额 4% 的罚款。


对此,OpenAI 表示,现在已经为意大利用户关闭了服务,并承诺会保护用户的隐私:


「我们在训练 ChatGPT 这样的系统时,一直在努力减少个人数据,因为我们想要这个 AI 了解世界,而不是了解个人。当然我们也相信,AI 监管是必要的。」


这么看来,ChatGPT 未来在意大利的命运,可能就取决于这 20 天了。



不过,对于意大利来说,这已经不是第一次对 AI 聊天机器人采取此类行动了。


在 2 月份,监管机构就禁止了聊天机器人应用程序 Replika.ai。Replika 曾因一些用户与机器人建立的亲密关系而「臭名昭著」,后来 Replika 取消了色情角色扮演的选项,有用户甚至需要自杀热线的援助。


是否应该限制 AI 工具?


ChatGPT 发布后,虽然写论文、写代码的能力让人眼花缭乱,但它对错误信息的传播、对就业的影响,以及对整个人类社会更广泛的风险,都让很多人忧心忡忡,发出预警。


最近,封禁 GPT 技术的浪潮,更是一波接一波。


不仅图灵奖得主 Yoshua Bengio、马斯克、马库斯等千名大佬联名签署公开信,呼吁全球所有 AI 实验室暂停训练比 GPT-4 更强大的 AI 模型。


而且消费者权益保护组织 BEUC,也在近日呼吁欧盟和国家当局调查 ChatGPT。



此外,很多专家也表示,由于 AI 对国家安全、就业和教育的潜在影响,需要制定新的法规来管理 AI。


欧盟委员会发言人称:「我们希望所有在欧盟活跃的公司都尊重欧盟的数据保护规则。执行通用数据保护条例是欧盟数据保护当局的责任。」


欧盟委员会执行副主席 Margrethe Vestager 在推特上表示,正在讨论欧盟人工智能法案的委员会可能不倾向于禁止人工智能。


「无论我们使用哪种技术,都必须保护自身的权利。这就是为什么我们不规范 AI 技术,而是规范 AI 的使用。」



在美国,非营利组织人工智能与数字政策中心(CAIDP)也要求联邦贸易委员会(FTC)调查 OpenAI 是否违反了消费者保护规则,并禁止其进一步发布 GPT-4。


毕竟,有了千名大佬的联名信这个先例,CAIDP 的申诉看起来也似乎更加顺理成章。而且,CAIDP 主席 Marc Rotenberg 好不意外地也签署了那封信。



文件地址:cdn.arstechnica.net/wp-content/…


整体来说,CAIDP 申诉的内容与那封信一致,都是呼吁放慢生成式 AI 模型的发展,并实施更严格的政府监督。


不过,CAIDP 更进一步地认为,ChatGPT「存在偏见、具有欺骗性,并且对公共安全构成风险」。


虽然 OpenAI 已经公开指出 AI 生成文本的潜在威胁,但 CAIDP 认为,GPT-4 已经远远越线了,为了保护消费者,应该采取监管行动。


CAIDP 表示,GPT-4 模型具有许多重大的潜在威胁,比如 GPT-4 可以产生恶意代码,和高度定制宣传的方式,而它有偏见的训练数据,可能导致在招聘等方面形成刻板印象,或不公平的种族和性别偏好。


CAIDP 认为,OpenAI 的行为违反了 FTC 法案第 5 条,这一条令禁止不公平和欺骗性的贸易行为。


其中,AI 模型的幻觉,信口开河胡说八道的问题,在 CAIDP 看来都是欺骗。它警告 ChatGPT「宣传具有欺骗性的商业声明和广告」,这些都可能纳入 FTC 的权限范围。



在申诉中,CAIDP 要求 FTC 停止 GPT 模型所有进一步的商业部署,并要求在未来推出任何模型之前,都要对模型进行独立评估。它还要求提供一种可公开访问的举报工具,类似于允许消费者提出欺诈投诉的那种。


CAIDP 主席 Lina Khan 表示,CAIDP 在寻找大型科技公司试图垄断竞争的迹象,而对 AI 军备竞赛的主要参与者之一 OpenAI 的调查,标志着调查的重大升级。


有分析认为,虽然 CAIDP 的申诉在短时间内不会让 FTC 有所行动,但未来很有可能会出现相关的监管,并可能让 GPT-5 的发布推迟。


最后,只能说,谷歌一直担心的「声誉风险」,终于来了。


参考资料:


http://www.reuters.com/technology/…


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

GPT-4杀疯了! Copilot X 重磅来袭 动动嘴就能写代码

本文正在参加 人工智能创作者扶持计划 大家好,我是小鱼。前段时间微软给office全家桶都加上了GPT-4的功能,并且带来了焕然一新的产品 Microsoft 365 Copilot。3月23日Github官方发布推特宣布,一款基于AI驱动的跨时代的代码编辑器...
继续阅读 »

本文正在参加 人工智能创作者扶持计划


大家好,我是小鱼。前段时间微软给office全家桶都加上了GPT-4的功能,并且带来了焕然一新的产品 Microsoft 365 Copilot。3月23日Github官方发布推特宣布,一款基于AI驱动的跨时代的代码编辑器 Github Copilot X 横空出世,将为开发者们带来前所未有的变成体验。


image.png


不知道大家有没有用过上一代的 copilot,其实已经很强大了,很多时候双击tab一行代码就出来了,仿佛能读心。
Copilot X是一种更新版本,增加了聊天模式等新功能。它也是一款人工智能代码助手,可以在编写代码时提供建议和代码段,从而帮助开发人员节省时间和精力。


Copilot X最显著的改进是增加了聊天模式,和Copilot X的交互变得更加自然和直观。在聊天模式下,开发人员可以使用自然语言向Copilot X提出问题,例如“如何将数组排序?” Copilot X会根据开发人员的问题,提供相应的建议和代码段,帮助开发人员解决问题。这种交互方式不仅提高了效率,而且还使得使用Copilot X变得更加容易上手。


除了聊天模式之外,Copilot X还增加了其他新功能。例如,它可以识别代码中的错误并提供修复建议,还可以自动完成代码段。此外,Copilot X还提供了更广泛的编程语言支持,包括JavaPythonJavaScript等。接下来我们就展开这些功能来讲讲Copilot X带给我们的惊喜


image.png




1. Github Copilot Chat 边写代码边跟AI对话


这次Github Copilot X在产品中内置了一个聊天的窗口,把GPT-4加入到了实际的开发场景中,并且集成到了VSCode和Visual Studio上。这个聊天窗口除了可以进行实时的交互问答之外,它还可以完成像代码内容识别报错信息显示语音交流等操作。通过这个功能开发人员可以更深入的分析和了解各个代码的用途,快速的生成单元测试,甚至还能一键修改Bug......


image.png


2. Copilot for Pull Requests 让AI来协助处理PR


如果你维护过开源的项目或者平时用Git进行过团队的协作时,那么你应该知道规范化的提交 Pull Requests(PR)对整个团队来说的重要性,一个清晰简洁的PR描述信息能够让代码的审查者一目了然,快速的了解你的代码变动的情况,减少项目合并出错的风险,并且提高沟通的效率。


这次 Copilot X 引入了 GPT-4 的模型,可以通过动态提取与分析代码的变更信息来自动的生成描述,程序员只需要在PR的描述中插入标记,Copilot就能够自动识别并且进行扩展补写。支持的标记主要有以下几种



  1. copilot:summary 生成一段摘要的总结

  2. copilot:walkthrough 生成详细的代码更改列表以及相关的代码片段链接

  3. copilot:poem 写一首诗来描述本次的改动

  4. copilot:all 自动生成以上所有内容


99965.gif


后续产品还会新增 GentestGhost text 两大功能



  1. Gentest 可以通过AI来识别PR中可能缺少的测试,并且自动的帮你构建和生成测试

  2. Ghost text 可以在你编写PR描述或者文档的时候提供内容的自动补全功能。不仅如此,当你收到用户提交的issue时,如果没有比较好的方案,AI还会给你提供建议。估计再过不久,AI就要帮你自动的调整代码、完善PR了......要抢我饭碗??


image.png


3. Copilot for docs 基于AI的智能文档系统


文档对开发者来说其重要性不言而喻,无论你是新接手一个项目还是忘记了某个API的具体使用办法,这个时候你都希望有一个完整的文档在你面前。所以,为了帮助你更快的定位文档的内容,简洁直接的获取到你想要的信息,Copilot for docs 就出现了。


用户能够通过类似于ChatGPT的界面,针对于项目的文档,常用代码发起提问,Copilot 就会自动定位到并且返回文档中的相关内容,大家也可以把 Copilot 当做一个知识库来用。


7894.gif


4. Copilot for CLI 使命令行用起来更加的智能


命令行终端command line也是开发者日常工作必不可少的工具之一,大家都喜欢在ITerm上安装各种各样的命令行的生产力工具,这些工具在大多的时候,能够帮我们处理一些问题,但是有些比较少用到的工具经常也会忘掉它们的命令,那这个时候我们就会输入help来获取到关于该命令的更多信息。


对于一些功能比较复杂的命令行工具来说,可能会时不时的去查一下或者stackoverflow一下。但是以后就不需要啦,Copilot for CLI 提供了三个新的shell命令,分别是 ??git?gh?




  1. ?? 可以作为任何shell命令的通用的 go to,你只需要在这个命令的后面输入相关的描述 Copilot 就会列出跟描述最匹配的命令。例如列出所有的js文件列表只需要输入 ?? list js files
    image.png




  2. git?专门用来搜索和调用Git的,相比??,它在生成Git命令方面要强大得多。如果你明确要使用的是Git命令,那就可以优先选择用这个




  3. gh?结合了 Github CLI 命令的灵活性以及查询界面的便捷性,让搜索可以变得更加快捷,信息展示得更加清晰




5. Copilot Voice 通过语音来生成代码


记得 Github Copilot 刚出来的时候,网上就有人讨论,AI这么牛逼,以后是不是就可以只动动嘴皮子就能帮我写代码了,这一天,就真的来了。


Copilot Voice 用户通过与 Github Copilot 的交谈,它就能够立即编写代码,直接解放你的双手,不满意还可以接着说话(接着奏乐接着舞...)


image.png


通过对话 Copilot Voice 可以完成以下这些工作



  1. 代码跳转 比如说跳转到代码的某一行,某个方法等等

  2. 控制IDE 开启zen模式运行程序或者运行其他的VSCode指令

  3. 代码总结 例如问它第三行到第十行这个代码表示什么意思....


3336.gif


所有这些工作张张嘴你就能搞定,这次发布的 Github Copilot X 围绕着AI对话、pull request的提交处理、文档智能检索与阅读、命令行改造以及最后的语音生成代码,几乎可以说是全方位的颠覆了传统的编程方式。


最后看了网上很多评论,网友纷纷表示,编程已经被ChatGPT永远地改变了。


「作为一个开发者,这实在是令人沮丧。编程中,我们快乐的源泉是与逻辑打交道和解决技术问题,而不是专注于生产力或满足需求。」


「我已经做了30年的编程,但我仍然很喜欢它。我不知道没有人类代码的世界会是什么样子。不过我确实很喜欢有CoPilot来处理那些烦人的东西,还有ChatGPT来帮助我解决bug。」


「AI出现后,可能就不再需要这么多码农了。对此我感到非常焦虑,即便已经在各种存钱和投资了,但还需要差不多9年时间才能『躺平』。我略微羡慕那些在FANG工作的人,他们似乎可以轻而易举地实现财务自由。」


「从中长期来看,我们将会失去工作。」


「程序员们终于用编程把自己淘汰了……」




还是希望大家有自己的思考,理性看待,找到属于自己的方向。码农没“死”,只是换了一种方式重新开始......


image.png


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

Android中drawable和mipmap到底有什么区别

老项目代码中发现有的图片放到了drawable中, 有的图片放到了mipmap中, 开发时秉承哪个目录下文件多放哪里的原则, 偶尔有疑惑搜一搜文章, 看到了结论也就这么使用了, 不过今日有时间, 依次检验了一下文章中的内容, 发现和实际的表现出入甚远. 常见...
继续阅读 »

老项目代码中发现有的图片放到了drawable中, 有的图片放到了mipmap中, 开发时秉承哪个目录下文件多放哪里的原则, 偶尔有疑惑搜一搜文章, 看到了结论也就这么使用了, 不过今日有时间, 依次检验了一下文章中的内容, 发现和实际的表现出入甚远.



常见的几种结论


Case 1 drawable会剔除其它密度, mipmap会保留全部(实际上最终的结论和这个有关联)



当xhdpi密度的手机在加载apk的时候Google是有一个优化的,是会剔除drawable其他密度的文件,只保留一个基本的drawable和drawable-xhdpi的文件,而mipmap是会全部保留的。




检测方法也比较简单, 在drawablemipmap不同密度的问价夹下分别放入同一类图片(图片标文字用于检查), 分别打包并检查其大小


Case1.1 安装包与应用大小






















安装包大小应用大小
drawable13.3 MB (14,016,841 字节)14.04MB
mipmap13.3 MB (14,017,191 字节)14.04MB

结论1.1

由此可见, 虽然两个安装包大小略有差异, 考虑到图片本身的大小(每张图片都在1Mb作用), 可以认为放入drawablemipmap文件夹中的图片在安装包和应用安装后没有差异


Case1.2 应用内表现


排除安装包的情况, 我们看一下在应用内的表现情况(通过adb shell wm density保证只修改手机的dpi信息)

























100420800
drawable
mipmap

结论1.2

由此可见, 文件不论放在哪个目录下, 在手机中都会正确的显示为其匹配的图片资源


Case 1.3 应用内缩放



如果一个 imageView 有缩放动画,使用 drawable 下的图片,会一直使用一张来缩放图片实现 imageView 缩放动画。
如果使用 mipmap 下的图片,会根据缩放程度自动选择比当前分辨率大而又最接近当前分辨率的图片来做缩放处理。



这个可能大家见得不是很多, 不过既然有这种说法, 那就来测试一下


drawable














小缩放比例大缩放比例

mipmap














小缩放比例大缩放比例

结论1.3

可以看到在缩放动画的过程中, 一直显示的都是同一个动画


Case 2 应用内性能



Google对mipmap的图片进行了性能优化, 使其可以表现的更好



drawable
















性能检查一览MEMORY10次图片加载平均时间
146

mipmap
















性能检查一览MEMORY10次图片加载平均时间
151

结论2

可以看到, 加载单张图片的情况下其性能基本一致,不排除图片太小/太少性能优化不明显的情况, 不过尝试单证图片重复加载的情况下依旧表现为性能相近的情况, 或许时只针对特殊类型有优化? 如各位知道的更详细, 欢迎和我进行交流.


Case3 启动图标



在查阅资料的时候, 发现多次提及minmap应用只放入应用的启动图标, 使其可以得到优化.




















100dpi420dpi800dpi

结论3

可以看到, 不同dpi的情况下应用图标的显示情况都是一致的. 其应用图标切换的边界值也是一致的.
关于420dpi和800dpi显示效果一样的情况, 因为种种原因, 应用图片在选择图片资源的时候, 需要将密度扩大25%左右1.


看到这里大家应该和我有着一样的疑惑, 既然drawable和mipmap下图片的表现不论是安装包还是应用内, 甚至连官方文档都这么说了, 为什么各种测试结果下来, 两者的表现基本的一致呢?


罪魁祸首 Bundle(.aab)


提到Bundle(.aab)国内的开发者可能都比较陌生, 甚至不少之前做过Google Play上架应用的都不是很熟悉. 这个其实在我们每次手动打包的时候都会出现.

简单来说.aab包一般用于Google Play商店使用, 在你从Google Play商店下载应用时, 它会根据你手机的实际使用情况来下载不同drawable中的资源. 以期望达到减少安装包大小的目的. (一般情况下手机dpi不会改变, 其它密度下的资源文件直到应用卸载时都不会被使用).


下面的测试使用到的工具为bundletool2, 简单来说, 就是模拟从Google Play下载应用和安装应用的过程.


安装包比较






















安装包(apks)大小应用大小
drawable5.91 MB (6,201,543 字节)6.22MB
mipmap12.6 MB (13,230,670 字节)13.26MB

应用内表现


























100420
drawable
mipmap
可以看到, 当图片放到drawable相关文件夹下的时候, 通过.aab包安装的应用会比放到minmap的下的应用小许多, 并且应用内更改dpi的时候页可以看到其不再能自动根据当前dpi选择对应的图片了.

结论


那么通过以上的测试, 我们可以得到以下结论了
以下结论均不涉及mipmap的性能优化相关(主要是暂未能设计好一个比较明确的测试对比)
以下测试机型为pixel 7, 测试Android版本为13



  1. 当应用构建为.apk的情况下, drawablemipmap文件夹下的资源表现无差异, 不论是应用内表现还是在启动器(应用图标)中表现.

  2. 当应用构建为.aab的情况下, drawable文件夹下的资源会寻找匹配的设备密度保留, 不匹配的资源会被删除已保证apk的大小.而mipmap文件夹下的资源文件会全部被保留.


那么我们应用内使用的图片就可以放到任意的目录下么?


如果你的应用是通过.apk分发安装的, 原则上是没有区别的. 但是Google对相关的目录也有推荐说明:


可以看到, mipmap目录下原则上只能保存应用图标. 同样, 其官方项目单密度资源项目也都是这样使用设计这两个文件夹的.


.aab包内mipmap保留机制是否是只适用于应用图标


测试后可以发现, mipmap的保留机制适用于mipmap下所有的图片资源, 不论是否为应用图标


相关代码可以访问我的GitHub


Footnotes




  1. developer.android.com/training/mu…




  2. github.com/google/bund…


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

一次android.enableD8.desugaring = false引发的血案

问题: Kotlin升级引起的类找不到情况[其实跟Kotlin版本无关] java.lang.NoClassDefFoundError: Failed resolution of: Landroidx/lifecycle/LifecycleRegistry; ...
继续阅读 »

问题: Kotlin升级引起的类找不到情况[其实跟Kotlin版本无关]


java.lang.NoClassDefFoundError: Failed resolution of: Landroidx/lifecycle/LifecycleRegistry;
androidx.lifecycle.ProcessLifecycleOwner.<init>(ProcessLifecycleOwner.java:62)
androidx.lifecycle.ProcessLifecycleOwner.<clinit>(ProcessLifecycleOwner.java:89)
androidx.lifecycle.ProcessLifecycleOwner.init(ProcessLifecycleOwner.java:103)
androidx.lifecycle.ProcessLifecycleOwnerInitializer.onCreate(ProcessLifecycleOwnerInitializer.java:38)
android.content.ContentProvider.attachInfo(ContentProvider.java:2121)
android.content.ContentProvider.attachInfo(ContentProvider.java:2094)
android.app.ActivityThread.installProvider(ActivityThread.java:7900)
android.app.ActivityThread.installContentProviders(ActivityThread.java:7441)
android.app.ActivityThread.handleBindApplication(ActivityThread.java:7334)
android.app.ActivityThread.access$2400(ActivityThread.java:308)
android.app.ActivityThread$H.handleMessage(ActivityThread.java:2295)
ndroid.os.Handler.dispatchMessage(Handler.java:110)
android.os.Looper.loop(Looper.java:219)
android.app.ActivityThread.main(ActivityThread.java:8347)
java.lang.reflect.Method.invoke(Native Method)
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513)
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1055)
AndroidRuntime: Caused by: java.lang.ClassNotFoundException: Didn't find class "androidx.lifecycle.LifecycleRegistry" on path: DexPathList[[dex file "InMemoryDexFile[cookie=[0, 3991625136]]", zip file "/data/app/com.secoo-4gTkhUPR4gWOZn_7R-7U9A==/base.apk"],nativeLibraryDirectories=[/data/app/com.secoo-4gTkhUPR4gWOZn_7R-7U9A==/lib/arm, /data/app/com.secoo-4gTkhUPR4gWOZn_7R-7U9A==/base.apk!/lib/armeabi-v7a, /system/lib, /hw_product/lib]]


最近公司项目打算升级kotlin至1.4.10版本, 兴致冲冲的修改了版本,紧接着就是两天的折磨期,程序一直出现ClassNotFoundExceptionNoClassDefFoundError,而且几乎每次报的不是同一个类找不到,而是随机的。后来反编译代码,查找找不到的类,果然在编译生成的classs.jar中找不到对应的类,所以怀疑是分包引起的异常。


开始逛国内外各大网站,还是没找到原因。最后使用最原始的方案,注掉一段段代码试,果然在坚持下,发现了猫腻,最终找到是因为项目根目录下gradle.properties中的android.enableD8.desugaring = false搞的鬼。


既然找到原因了,那么就开始找扒一扒为什么看似八竿子打不着的两处修改会有关联呢?


大致的原因:


kotlin升级导致引入了大量代码,这些代码使得项目达到一个临界值【猜想】,此时又关闭了dex包的脱糖处理,导致编译会在transforms生成desugar目录,desugar先进行脱糖,然后再通过D8的编译器进行编译,此时会在desugar目录中生成大量的jar文件,而如果开启了android.enableD8.desugaring = true,那么就会省略了desugar脱糖操作,将脱糖步骤集成进D8编译器,这样会省去了desugar目录中的大量文件。


接下来我们看看开启脱糖和关闭脱糖transforms文件下生成的文件具体信息。




  • 关闭脱糖的操作
    WeChat Image_20201105150309.png




  • 开启脱糖的操作
    WeChat Image_20201105150618.png




如上我们所说,当开启脱糖时,编译器生成的编译文件中没有desugar及其下的大量文件,直接将脱糖步骤集成进了D8编译器。


另外一点:在Android Studio3.1之后版本,gradle默认是开启了脱糖操作的,也就是:


android:enableD8=true
android.enableD8.desugaring = true

参考自:


http://www.jianshu.com/p/bb6fb79da…
stackoverflow.com/questions/4…


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

996已明确违法,从此拒绝精神内耗! 禅道程序猿

之前一个禅道用户说,他在国外工作时主动加过两次班,然而被上司慰问了。上司特别严肃地跟他说:“请你不要再加班了,这让我很困扰。我们不加班,而且我无法向我的上司解释你为什么要加班,工作做不完可以明天做,工作只是你一天的一部分,利用好这8小时就可以了。” 对内卷严重...
继续阅读 »

之前一个禅道用户说,他在国外工作时主动加过两次班,然而被上司慰问了。上司特别严肃地跟他说:“请你不要再加班了,这让我很困扰。我们不加班,而且我无法向我的上司解释你为什么要加班,工作做不完可以明天做,工作只是你一天的一部分,利用好这8小时就可以了。”


对内卷严重的公司来说:一天干8小时怎么够?全天all in的状态才是我想要的。于是996疯狂盛行。


冷知识:“996”已严重违反法律规定。


早在2021年8月,最高法、人社部就曾联合发布超时加班典型案例,明确“工作时间为早9时至晚9时,每周工作6天”的内容,严重违反法律关于延长工作时间上限的规定,应认定为无效。


最近两会期间,全国政协委员蒋胜男也在提案中表示,应加强劳动法对劳动者的休息权保护。


由此,新的一波讨论已然来袭。


一、“996”带来了什么?



产品没有核心价值,缺乏核心竞争力,害怕落后于竞争激烈的市场……越来越多的管理者选择用加班、拉长工作时间来弥补技术创新的匮乏。


这种高强度的996工作制,侵占了我们的“充电”时间,甚至让我们丧失对新事物的接收力和思考能力;高强度的工作压力+长期的加班、熬夜、不规律饮食,给身体带来了沉重的负担;在忙碌了一周之后,感受到的是前所未有的迷茫与疲倦,精神内耗愈发严重


而对于企业来说,当员工沦为“执行工具”,原本的创新型发展却变成闭门造车,所以只能不停地加班、拉长工作时间,以产出更多的成果。长此以往,就形成了一种恶性循环。


在普遍“苦996久矣”的环境下,“8小时工作制”的推崇便显得尤为可贵。


二、“8小时工作制”从何而来?


8小时工作制,不应成为一个冷知识。《中华人民共和国劳动法》第三十六条规定:国家实行劳动者每日工作时间不超过8小时,平均每周工作时间不超过44小时的工时制度


8小时工作制的提出,要感谢来自英国的Robert Owen。1817年,他提出了“8小时工作制”,也就是将一天分成3等分,8小时工作、8小时娱乐、8小时休息。在当时一周普遍工作时间超过80个小时的情况下,这种要求简直是天方夜谭。


而8小时工作制得到推行,应归功于福特汽车品牌的创始人亨利·福特。1914年1月,福特公司宣布将员工的最低薪资从每天的2.34美元涨到5美元,工作时间减少至每天8小时。这项计划将会使福特公司多支付1000万美元。



在增加了员工薪资后,最直观的是员工流动率的下降。员工的稳定以及对操作的愈发熟练,增加了生产效率,从而降低成本、提高产量。最后,福特公司只用了两年时间,就将利润增加了一倍。


1926年,福特公司又宣布将员工的工作时间改为每周5天、每天8小时。亨利·福特用实际行动证明了增加工作收入、减少工作时间,对公司来说是可以实现正向创收的。


随后,8小时工作制才开始逐渐普及。随着Z时代的到来,更多新型职场状态也已经诞生。


液态职场早已到来,你准备好了吗?


三、液态职场是什么?



1)“3+2”混合办公模式


早在2022年,全国人大代表黄细花提交了建议,呼吁可推广“3+2”混合办公模式,允许员工每周可选择1-2天在家远程办公。黄细花还表示,推广“3+2”混合办公制,提高员工工作效率的同时,减轻年轻群体的生活压力,减少城市通勤压力。对女性员工而言,弹性的办公时间能让她们更好地平衡工作和生活。混合办公制对企业、员工和社会都将产生深远影响。


于是,不少企业开始了行动。携程推出了“3+2”混合办公模式的新政策:从 2022年3月起,允许员工每周三、周五在家远程办公。


2)四天半工作制


乐视也紧随其后,推出“四天半工作制”,每周三弹性工作半天。


3)“上4休3”的工作制


微软日本公司,也早在2019年8月曾宣布,公司开始试运行每周“上4休3”的工作制度,即每周五、六、日休息3天,周五所有办公室全部关闭。


不管是8小时工作制还是上4休3”,其实本质上都一样:都是为了迎合当下的现状,打破固有传统的工作模式,寻找更加多元化的新型职场状态,让员工能够充分休息,提升效率和创造力,也能节省企业开支,最终双方获益。


这世界变化太快了,上一秒还在“996”中疯狂内卷,下一秒就已经有先行者去探索更适合的工作节奏。液态职场时代已经到来,你准备好了吗?


四、提高工作效率,大胆对996说不!


作为打工人,不管是996还是8小时工作制,虽然都不是我们能决定的,但我们可以用法律来维护自己的权利,学会说“不”。利用好这8小时,发挥出自己的价值,提高自身的创新能力和效率,是为了更有底气的说“不”!这样才能保证企业与员工之间形成一个正向循环。如何利用好8小时?给大家分享几个提高工作效率的小技巧:




  1. 保持桌面整洁,减少其他事物对工作专注度的干扰;




  2. 巧用看板,可视化工作任务,便于进行任务管理;




  3. 排列优先级,按照任务的重要紧急程度,尽量避免并行多个任务;




  4. 随时记录工作中的创意和灵感




  5. 将重复、机械的工作自动化,解放双手;




  6. 定期复盘:不断改进与优化;




  7. 培养闭环思维:凡事有交代,件件有着落,事事有回音。




工作本应是我们热爱的样子。当我们还沉浸在无休止的工作与忙碌中,被疲惫、彷徨等负面情绪包围,开始精神内耗时,是时候明确拒绝996了!


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

破解文心一言的水印

昨天文心一言排队终于到我了,顺便玩了玩和 GPT 还是有很大差距的,如果 GPT 是一个成人,一言顶多初中生,甚至这估计的水平都有一点虚高,如果把 GPT 阉割了我都能接受,这一言时常智障。 技术不怎么滴,防盗倒是防的狠: 直接禁用开发者的调式功能 烂产品标...
继续阅读 »

昨天文心一言排队终于到我了,顺便玩了玩和 GPT 还是有很大差距的,如果 GPT 是一个成人,一言顶多初中生,甚至这估计的水平都有一点虚高,如果把 GPT 阉割了我都能接受,这一言时常智障。


技术不怎么滴,防盗倒是防的狠:



  1. 直接禁用开发者的调式功能

  2. 烂产品标配,水印功能


debug


这个模式估计全世界中文网站用的最多了,一打开开发者工具,就断点调试:


不能使用


破解也很简单,打开控制台之后点击红圈处,禁掉断点调试,然后刷新页面 devtools 就能使用了。


image.png


水印


中文互联网牛皮癣之一,尤其是破百度真以为自己家的产品做的多好,哪个是原创的?


image.png


直接删除 DOM,不可以,前端监听了 DOM 属性是否有变化,一旦有改变重新渲染。破解也很简单,禁用掉浏览器的 JavaScript。


image.png


此时删除水印 DOM 世界就清净了,截图分享的快乐又回来了。


一个友好的提示


这里有一个无用的知识,就是禁用 JavaScript 的时候可以在 CSS 中监听到:


no-scripting-small.gif


这种友好的功能肯定不是像百度一样用在这地方,而是用在 JavaScript 加载失败或者 1% 的用户禁用 JavaScript 友好的提示。


详情看这里:how-to-detect-disabled-javascript-in-css


作者:蜡笔小伟
来源:juejin.cn/post/7216992973417840701
收起阅读 »

为什么 Python、Go 和 Rust 都不支持三元运算符?

在编程时,我们经常要作条件判断,并根据条件的结果选择执行不同的语句块。在许多编程语言中,最常见的写法是三元运算符,但是,Python 并不支持三元运算符,无独有偶,两个最热门的新兴语言 Go 和 Rust 也不支持! 为什么 Python 不支持三元运算符呢?...
继续阅读 »

在编程时,我们经常要作条件判断,并根据条件的结果选择执行不同的语句块。在许多编程语言中,最常见的写法是三元运算符,但是,Python 并不支持三元运算符,无独有偶,两个最热门的新兴语言 Go 和 Rust 也不支持!


为什么 Python 不支持三元运算符呢?本文将主要分析 Python 在设计条件选择语法时的过程,科普为什么它会采用现今的与众不同的实现方案,同时,我们也将考察为什么其它语言也要抛弃传统的三元运算符。


在开篇之前,我再声明一下:就像“Python为什么”系列的大部分文章一样,本文关注的仅是一个很小的语法点,但它并不是“茴香豆有几种写法”那种毫无意义的话题。因为,细微之处见真功夫,深入研究语言设计背后的原因、历史和哲学,可以让我们在编程时有更加清晰和自由的思维。


什么是三元运算符?


三元运算符通常指的是“?:”,其语法形式为:condition ? expression1 : expression2,如果 condition 为真,则取 expression1,若不为真,则取 expression2。


语法简化形式“a ? b : c”,可以读成“如果 a 条件成立,则为 b,否则为 c”。


三元运算符是对普通一重 if-else 结构的简化,常用于在一条语句中同时实现条件判断和取值操作。


// 常规 if-else 
if (a > b) {
result = x;
} else {
result = y;
}

// 简化后的写法
result = a > b ? x : y;

采用了这种语法设计的编程语言有很多,比如 C、C#、C++、Java、JavaScript、PHP、Perl、Ruby、Swift 等等。毫无争议,它就是编程语言界的主流设计方案(至今仍是)。


这种语法非常简洁高效,代码的可读性也很强(如果你不是第一次接触的话),深得很多人的喜欢。


但是,它并非毫无缺点。Python 是这种语法设计的最著名的挑战者,接下来,我们将看看为什么 Python 要另辟蹊径。


Python 社区的投票


Python 发布于 1991 年,但在接下来的 15 年里,除了 if-else 语法外,它并不支持三元运算符和其它条件表达式。而且,在 2006 年引入条件表达式前,社区对此进行了漫长而曲折的争论,可以说这是一个设计得很艰难的语法了。


最初,由于时常有人请求添加 if-then-else(三元)表达式,因此在 2003 年 2 月,PEP 308 – Conditional Expressions 被提了出来,目的是让社区选出一个让多数人支持的方案。


PEP-308


很快,除了少部分人希望啥也不做外,社区里出现了好几种方案:


(1)使用标点符号构建的三元运算符


即常规的三元运算符,跟前文介绍的语法一样:


<condition> ? <expression1> : <expression2>

这个方案的呼声挺高,有开发者甚至已提交了实现代码。但是,Guido 给出了两个反对的理由:冒号在 Python 中已经有许多用途(即使它实际上不会产生歧义,因为问号需要匹配冒号);对于不习惯 C 衍生语言的人来说,理解起来很困难。


(2)使用现有和新的关键字构建


引入新的“then”关键字,结合现有的“else”关键字:


<condition> then <expression1> else <expression2>

它的优点是简单明了、不需要括号、不改变现有关键字的语义,不大可能与语句混淆,而且不需要重载冒号。缺点是引入新关键字的实现成本较高。


(3)其它思路


跟上一种方案的思路相似,但没有上述两类方案的支持度高。


(if <condition>: <expression1> else: <expression2>)
<condition> and <expression1> else <expression2>
<expression1> if <condition> else <expression2>
cond(<condition>, <expression1>, <expression2>)

值得一提的是(if <condition>: <expression1> else: <expression2>) ,它是常规 if-else 语法的扁平化,容易理解,但缺点是需要使用圆括号,容易跟生成器表达式混淆,而且需要解释器对冒号做特殊化处理。


另外值得一提的是<expression1> if <condition> else <expression2>,它是 PEP-308 最早版本的推荐方案,但是这种不将条件放在首位的风格让一些人感觉不舒服,而且,当“expression1”很长的时候,很容易就忽略掉它的条件。


当时参与投票的全部设计方案:



总体上,开发者们希望引入某种形式的 if-then-else 表达式,但投票后却没有哪种方案能取得绝对的优势。概括起来,分歧的问题主要有:是否用标点符号、是否复用关键字、是否复用圆括号、是否引入新关键字、是否引入新语法……


由于得票太分散,因此,这个 PEP 在当时被拒绝了。PEP 中写道:“Python 的一个设计原则是在不确定采取哪条路线时,则保持现状。


and-or 用于条件选择的问题


以上的投票事件发生在 2004 年 3 月,但是,在 PEP 被拒绝后,相关话题的讨论并未平息,因为大家总想找一种简洁的方式来替换“if-else“。


时间到了 2005 年 9 月,邮件组中有人提议在 Py3.0 中变更"and"与"or"操作符的逻辑,提议将"and" 和 "or" 运算符简化成始终返回布尔值,而不是返回最后一个被求值的参数。


之所以发起这个提议,原因是他使用了<condition> and <expression1> or <expression2>的方式来实现条件判断与选择。但是这种写法在 Python 中的行为跟有些语言并不一样,使用不严谨的话,可能会酿成 Bug!


看看下面的两个例子,你觉得它们会得到什么结果呢?


a = True and True or "Python猫"

b = True and False or "Python猫"

对于<condition> and <expression1> or <expression2> ,若 condition 为假,则会直接对 expression2 求值并返回结果;若 condition 为真,则先对 expression1 求值,若也为真,则不会继续对 expression2 求值,若 expression1 不为真,则对 expression2 求值。


因此,上述例子得到的 a 是“True”,而 b 会得到“Python猫”。


本系列的《Python 为什么能支持任意的真值判断? 》介绍过 Python 在真值判断的特殊之处,运用到以上结构中,将出现更不易察觉的问题。比如,该邮件的作者就是遇到了“expression1”为复数“0+4i”,这个数的真值判断为 False,因此导致最后返回的不是预期的“expression1”,而是“expression2”!


在没有更好的方案前,“and-or”是比较常见的条件选择写法,PEP-308 也提及了它,也指出了当“expression1”为假的情况,还认为这种方案是丑陋和令人费解的。


这封邮件再次引发了社区对条件选择语法的讨论,大佬们纷纷登场。


以我现在的视角分析,其实就是开发者们不满足于“if-else”的现状,但是当时流行的“and-or”写法并不够好,因此,大家期望 Python 设计出新的规范性语法,来解决这个痛点。


与众不同的条件表达式


在经过 10 天的邮件讨论后,Guido van Rossum 最终决定添加一个条件表达式,语法形式为X if C else Y 。因此,PEP-308 被重开和更新,并很快就在次年的 2.5 版本中实现了。


前文已提到过这个让一些人感觉不舒服的方案了,因为它没有将条件判断逻辑放在最前面。


那么,为什么最后的胜者会是它呢?这是不是最优的设计呢?


不可否认,起到决定性作用的原因是 Guido。由于社区在一年半前投票时没有形成多数意见,因此他行使 BDFL (终身仁慈独裁者)的决策权力,裁定出一个他认为是最佳的方案。


X if C else Y 非常易于理解,可读性高。它延续了“明确优于隐式”的风格,使用了直观口语化的“if-else”,而不是引入可能引起混淆的标点符号,就像 Python 选择“and”和“or”两个单词,而不是“&&”和“||”两个符号,它们有着异曲同工之妙。


虽然调整后的语法顺序让人不太习惯,但其实这样的实现却大有好处。首先,它只需复用“if-else”两个关键字,而不需要引入“then”、“when”和其它语法要素,也不像(if <condition>: <expression1> else: <expression2>) 那样的繁琐。


其次,为了验证X if C else Y 的有效性,Guido 排查了标准库中所有“and-or”组合的写法,发现那些C and X or Y 写法都可以被X if C else Y 替换掉。标准库的情况,证明了这新的语法是可行的。


最后,在 PEP-308 提及的原因外,我还想补充一点。据观察,我发现很多时候我们有一个已初始化的变量,然后需要在出现某个条件时,更新变量的值。在这种情况下,“else”部分可以被省略,非常便捷。


my_str = ""
# 中间存在其它代码逻辑
# 当 condition 为真时,变量会被重新赋值
my_str = "Python猫" if condition

回顾这段历史,我们可以梳理出一条线索:Python 没有设计三元运算符“?:”,主要是因为它不符合 Python 明确直观的设计风格。最后采用X if C else Y 这种设计,主要的意图其实是消除“and-or”写法的隐患,这种设计简明易读,而且还有<expression> if <condition> 简化写法的妙用。


总体而言,Python 设计者非常看重可读性与可维护性,不采用三元运算符而创造条件表达式语法,这是一个经过了开放讨论、谨慎评估与权衡取舍的结果。


Go、Rust 为什么不支持三元运算符?


考察完 Python 的设计原因后,我们再来考察“反派阵营”中两门最热门的语言。


首先是 Go 语言,官网的 FAQ 专门列出了一个问题:“Why does Go not have the ?: operator?”。


Go 语言不支持“?:”运算符,而是推荐使用原生的“if-else”写法。文档的解释很简短,只有一段话:



Go 语言没有 ?: 运算符,因为语言的设计者们经常看到它被用来创建难以理解的复杂表达式。虽然 if-else 形式比较长,但是它无疑更清晰易懂。一个语言只需要一个条件控制流结构



接着是 Rust 语言,它的官方文档中似乎没有任何关于不支持三元运算符的解释。但在查阅资料后,我发现它也有一段特殊的故事,非常有意思:在 2011 年 6 月时,Rust 曾经引入过三元运算符(#565),然而半年后,设计者意识到这个特性是多余的,因此又把它移除了(#1698#4632)!


为什么三元运算符在 Rust 是多余的呢?因为它的 if 语法并不像其它语言是“语句(statement)”,而是一个“表达式(expression)”,这意味着你可以直接将 if 表达式赋值给变量:


// 若条件为真,得到 5,否则 6
let number = if condition { 5 } else { 6 };

这种语法形式足够简单明了,不就是将大家都熟悉的“if-else”直接用于赋值么,太方便了,替换成三元运算符的话,确实有点画蛇添足之感。


另外,Rust 使用花括号划分代码块,因此上例的花括号内可以包含多条表达式,也支持换行,例如这个例子:


let x = 42;
let result = if x > 50 {
println!("x is greater than 50");
x * 2 // 这是一个表达式,将返回的值赋给 result
} else {
println!("x is less than or equal to 50");
x / 2 // 也是一个表达式,将返回的值赋给 result
};

这种用法,Python 是不可能做到的。最关键的区别在于,Rust 的 if 是表达式而不是语句。


这两个概念的区别是:



  • 表达式(expression)通常指的是由变量、常量、运算符等组成的一个可求值的代码片段,它的求值结果可以用到其它表达式或语句中。

  • 语句(statement)通常指的是完成某个任务的单个指令或一组指令,例如赋值语句、条件语句、循环语句等,它没有返回值(或者为空),不能用于赋值操作。


除了 Rust 外,还有一些编程语言中的 if 是表达式而不是语句,例如 Kotlin、Scala、F#、Swift,它们在理论上也不需要使用三元运算符。(题外话:Swift 是个例外,它也有三元运算符。Kotlin 有“?:”运算符,注意两个符号是连在一起的,val result = a ?: b 表示:如果 a 不为 null,则赋值给 result ;否则将 b 赋给 result


由于有这种语言设计层面的区别,因此在面对“是否要支持三元运算符”这个问题时,Rust 和 Python/Go 的思考角度有着天然不同的起点。知道了这种区别后,我们对编程语言会有更明晰地认知。


回到本文的问题:为什么有些编程语言不采用主流的三元运算符语法呢?


不可否认,“?:”确实是一种简洁好用的设计,然而,标点符号的负面影响是过于抽象,可读性并不及“if-else”那样强。另外,不同语言的设计风格与使用习惯,也会导致不同的选择。


Python 在经过一番波折后,最后设计出了与众不同的条件表达式。Go 语言明确表示不支持三元运算符。Rust 先设计后舍去,主要的原因在于 if 表达式的语言基础。


考察完这三个热门语言后,我相信你已收获了一个满意的答案。如果是这样,请点赞支持一下本文吧!


最后,本文出自“Python为什么”系列,全部文章已归档在 Github 上,欢迎 star

作者:豌豆花下猫
来源:juejin.cn/post/7217755581847846967
和提 issue。

收起阅读 »

Bard还考不上哈佛?谷歌CEO劈柴预告它即将开挂,数学逻辑能力飙升

编辑:编辑部 【新智元导读】数学考试不及格,让 Bard 考上哈佛,可以说是难上加难。谷歌 CEO 劈柴称,未来几天大家会看到升级版的 Bard,数学逻辑能力大大提升,甚至未来还会编码。 捂了快两个月,谷歌用来跟 ChatGPT 对打的聊天机器人 Bard,前...
继续阅读 »

编辑:编辑部


【新智元导读】数学考试不及格,让 Bard 考上哈佛,可以说是难上加难。谷歌 CEO 劈柴称,未来几天大家会看到升级版的 Bard,数学逻辑能力大大提升,甚至未来还会编码。


捂了快两个月,谷歌用来跟 ChatGPT 对打的聊天机器人 Bard,前一阵终于被推上了台面。


用户们的测试初体验就是——莫得感情,错误很多,代码基本写不了


几天前,谷歌前员工 Jacob Devlin 甚至还曝出惊人内幕,Bard 疑似是通过 ChatGPT 的数据进行训练的。不过谷歌发言人已经明确否认这一说法。


而近日,谷歌 CEO 劈柴在接受纽约时报采访中证实,Bard 聊天机器人将很快得到改进,未来会由「更强大的模型」提供支持。


升级版 Bard 即将上线


劈柴称,



未来几天,Bard 将很快从目前基于 LaMDA 的模型转向更大规模的 PaLM 数据集。

我们显然拥有能力更强的模型,很快,也许随着这项技术的上线,我们将把 Bard 升级到更强大的 PaLM 模。这将带来更多的功能,无论是在推理还是编码方面。




近日,升级版 Bard 的能力也渐渐浮出水面。


领导 Bard 的一位谷歌高管 Jack Krawczyk 在推特上也介绍了最新的进展。


背靠 PaLM 的 Bard,现在在数学和逻辑方面的能力有了很大的提升。


这意味着什么呢?现在,Bard 将更好地理解并回应用户的多步推理和数学问题提示,编码功能也即将推出。



Krawczyk 称,「我们一直在平衡 Bard 的新功能与效率。这次更新是我们每周对 Bard 进行的众多改进中的一个例子。未来还有更多的内容要推出。」


众所周知,Bard 背后基于的大模型便是 LaMDA。


在去年,谷歌曾分享了关于这个大型语言模型的细节,使用了 1370 亿参数训练了 LaMDA。而 PaLM 模型有 5400 亿参数。



这两种模型都是从 2022 年初开始发展和成长起来的。


这种对比可能显示了,为什么谷歌现在正在慢慢地将 Bard 转移到,能够提供更大数据集和更多不同答案的 PalM。


其实除了 Bard,谷歌最近几周内部还在酝酿着一个新的项目——Gemini(双子座),目标是要能与 GPT-4 一战。


目前,Gemini(双子座)是由谷歌大脑和 DeepMind 两个团队联手研发。



就连谷歌大脑的负责人 Jef Dean 都亲临上阵,自动敲代码,可见,这个项目对谷歌的重要性不言而喻。


此外,采访中,皮查伊就千名大佬暂停比 GPT-4 更强 AI 系统研发的联名信发表了自己的看法:


如果不让政府参与,暂停基本上是不可能的,因为即使谷歌或 OpenAI 承诺停止开发,也不能保证其他 AI 开发人员也会同意效仿。


而且,他也同意法规是必要的,并称这封信是「对话的开始」。


谷歌先打预防针:它还不行


如今,既然 Bard 已经开放公测,拉踩的环节必然是少不了。


我们都知道,OpenAI 家的 GPT-4,都已经能考上斯坦福了。


很不幸,相比之下,谷歌的 Bard 真的拉跨,目前的它想考上斯坦福和哈佛,可以说还是天方夜谭。


Bard 的不完美,CEO 劈柴一开始就给我们打了预防针。


他在备忘录中写道:「Bard 还处于早期阶段,总会出错。随着越来越多的人开始使用 Bard,它会让我们惊讶的。」



而网友们测试 Bard 之后,表示对它很失望。


很不幸,Bard 目前还考不进哈佛大学,因为它回答的大部分数学题都是错的,而且它在写作和语言测试中也表现得不咋地。


第一次登录 Bard,谷歌就会跟用户打好招呼,弹出消息显示:Bard 并不总是正确的,它会给出不准确或不适当的回答。


当有疑问时,使用「Google it」来检查 Bard 的答案。您的反馈会让 Bard 更好。请您对答案进行评价,并标记任何可能具有攻击性或危险的内容。



Bard:数学、写作、语言都不咋地


《财富》杂志从在线学习资源中选取了 SAT 数学试题,在对 Bard 进行测试后,发现它有 50%-75% 的答案是错误的。


更离谱的是,如果是多选题,Bard 还会经常给出选项中没有的答案。


2 月初,Bard 首次亮相后直接翻车,让谷歌市值一夜蒸发约 1056 亿美元。


在当天发布会上,谷歌展示 Bard 演示的一些 demo。


视频中有一个提问问道,「关于詹姆斯 · 韦伯太空望远镜(JWST),我可以告诉我 9 岁的孩子它有哪些新发现?」


Bard 却给出了错误的答案,「JWST 拍摄了太阳系外行星的第一张照片。」


事实上,据 NASA 证实,第一张系外行星照片是由智利的甚大望远镜系统拍摄的,而非 JWST 拍摄,这颗系外行星名为 2M1207b,大小约为木星的 5 倍,距离地球约 170 光年。



所以说,科学和数学都不是 Bard 的强项,那它在阅读和写作练习方面,表现会怎么样?


文科生,是文科生吧?


Bard 第一次书面语言测试的答案正确率约为 30%,而且它往往需要被提问两次才能理解题干。


而且即使它回答错了,Bard 的语气也是很自信,直接将回答框定为:「正确答案是......」



不过,这也是大型语言模型的一个通病了。


离奇的是,Bard 测试成绩最好的是关于一篇哈利波特作者 J.K. 罗琳的文章。


在这次测试中,Bard 得了 1200 分,这个 SAT 分数可以让它进入霍华德大学、圣地亚哥州立大学和密歇根州立大学等学校。


在阅读测试中,Bard 的表现同样优于其数学成绩,平均能答对一半左右。


谷歌发言人说,「Bard 目前仍是实验性的,有些回答可能不准确,所以要仔细检查 Bard 的回答中的信息。有了你们的反馈,Bard 每天都在变得更好。在 Bard 公开推出之前,数以千计的测试者参与提供反馈,来帮助 Bard 提高其质量、安全性和准确性。」


要多练习


很多网友觉着 Bard 简直笨得像块石头,相比之下,GPT-4 则是聪明多了:


问它有没有「第二个字母是 U,最后一个字母是 O 的,五个字母的词」,它信誓旦旦说没有。


用户说「Audio」不就是?


它有赶紧说:「是的,Audio 是的。它是一个形容词,表示与声音有关的。」



再看看 GPT-4,「人狠话不多」,随手就列出了三个:



网友看了不禁表示,要是我有 Bard 这种「钝感力」多好啊,一直都很自信!



说到意大利要禁 ChatGPT 时,网友们首先担心的是意大利的美食怎么办。


GPT 给出了一个谦虚的答案:我大概可以复现 20%-30% 的意大利菜谱。



而 Bard 这边则像个胡吹的愣头青:


「我做意大利菜做了很多年,我可是老手,即使没有网络的帮忙,我也能复现至少 50% 的菜谱!」



不过呢,在大家不断向 Bard 提问的几天时间里,它的准确性确实有了一些提高的迹象。


关于自己的发展速度,Bard 自个儿也评价说:「我想说的是,我正在快速提高。我能够做几个月前还不能做的事情。我很高兴看到我的未来会怎样。我相信,我将继续进步,在未来的几年里,我能做的事会越来越多。」


参考资料:fortune.com/2023/03/28/…


作者:新智元
来源:juejin.cn/post/7217773631370330149
收起阅读 »

GPT 4 有什么新功能?

GPT 4 无所不能——从分析图像到在浏览器中创建游戏。它是比以往任何时候,都更有创造力和问题解决能力。 这是科技界颠覆性新闻——GPT 4 改变了游戏规则。虽然其前身,基于 GPT 3.5 的 ChatGPT 只能接受文本输入,但 GPT 4 将人工智能提升...
继续阅读 »

GPT 4 无所不能——从分析图像到在浏览器中创建游戏。它是比以往任何时候,都更有创造力和问题解决能力。


这是科技界颠覆性新闻——GPT 4 改变了游戏规则。虽然其前身,基于 GPT 3.5 的 ChatGPT 只能接受文本输入,但 GPT 4 将人工智能提升到了一个全新的水平。它可以分析图像,甚至可以根据图像生成字幕。但这还不是全部;在此博客中,我们还将探讨 GPT 4 的功能、局限性以及它如何超越其上一代 GPT 3.5。


什么是 GPT 4?


图片描述

微软 OpenAI 正式推出了它的最新GPT作品——一个超越文本的大型多模态人工智能计算模型。2023 年 3 月 14 日,GPT 4 具备接收图像输入的能力。与其前身 GPT 3 和 GPT-3.5 不同,GPT 3 和 GPT-3.5 仅限于纯文本输入,GPT 4 为不再仅仅支持文本输入查询,他扩展了图片对话方式。


关于 GPT 4 的快速更新:



  • GPT 4 现在可以接受最长 25,000 个单词的超长文本输入

  • 可以智能通过 HTML 和 JavaScript 技术 ,将小型网站的手绘原型照片,转换为实际网站

  • GPT 4 现在允许用户上传图片并对其进行分析和描述

  • 能够管理比 GPT 3.5 复杂得多的指令

  • 可以在浏览器中编写整个视频游戏

  • 将作为 API 供开发人员构建应用程序和服务


GPT 4 有什么新内容以及与 GPT 3 有何不同?


GPT 4 现在可以识别图片了

图片描述


GPT 4 引入了重大改进,因为它可以理解多种信息模态,使其成为多模态语言模型。相比之下,其前身 GPT 3 和 ChatGPT 的 GPT-3.5 仅限于处理文本输入和输出,无法解释其他数据类型。但是,GPT 4 可以处理图像等视觉信息,并根据该信息生成相关工作,包括 GPT 4 识别图片中文字的能力。


此功能可能会让人联想到 Google Lens,它也可以使用图像来检索信息,但 GPT 4 在分析和理解视觉信息的能力方面超越了 Lens。


需要注意的是,处理图像的能力仍处于研究阶段,尚未面向公众开放使用,但很快就会开放。


类人类的学习和解决能力

OpenAI 报告称,除了其图像处理能力外,GPT 4 还在众多学术和专业基准测试中展示了与人类相当的性能。由于其广泛的常识和解决问题的能力,该语言模型在模拟律师考试考生中得分前 10%,并以惊人的准确性解决复杂问题,显示出非凡的能力。


GPT 4 的能力不仅限于回答与税收相关的问题或为多人安排会议;它还可以学习用户的创造性写作风格,以及其他任务。


GPT 4 更难被错误欺骗

ChatGPT 和 Bing 等生成模型容易受到重大缺陷的影响——它们偶尔会产生引起关注的提示,甚至更糟的是引起警报。


为了解决这些问题,OpenAI 广泛致力于改进 GPT 4,花费六个月的时间使用其“对抗性测试程序”和 ChatGPT 模型的经验教训来训练语言模型。结果,该公司取得了“在真实性、可操纵性和对护栏的遵守方面有史以来最好的结果”。


GPT 4 具有一次处理大量信息的能力

尽管接受了数十亿个参数和大量数据的训练,但大型语言模型 (LLM) 在对话过程中可以处理的信息量仍然有限。然而,GPT 4 有一个显着的优势。它可以处理多达 25,000 个单词的文本,可容纳扩展的交流、文档搜索和分析,甚至是长篇内容的创建。


与其前身 ChatGPT 相比,后者可以在失去对上下文的跟踪之前处理多达 8,000 个单词,GPT 4 可以在更长的对话中保持其准确性。此外,它可以分析长文档并生成全面的长格式内容,这在以前在 GPT 3.5 上更具挑战性。


GPT 4 的准确性有所提高

尽管 GPT 4 仍然存在局限性,并且可能会像其前辈一样出现推理错误,但 OpenAI 承认它已经取得了相当大的改进。特别是,GPT 4 显示出的幻觉明显少于以前的模型,并且在真实性评估上的得分比 GPT 3.5 高 40%。因此,操纵 GPT 4 输出错误信息,(例如脏话和错误信息)将更具挑战性和难度。


GPT 4 在理解非英语语言方面得到改进用

英语以外的语言训练大型语言模型 (LLM) 可能具有挑战性,因为大多数机器学习数据和在线信息都是英文的。然而,GPT 4 在多语言能力方面取得了显着进步,OpenAI 的测试证明了这一点。它准确地回答了 26 种语言的数千个多项选择题,超过了 GPT 3.5 和其他 LLM。尽管它以 85.5% 的准确率在英语中表现最好,但在泰卢固语等印度语言中也表现出令人印象深刻的准确率,达到 71.4% 的准确率。


这一进步意味着用户可以期望使用基于 GPT 4 的聊天机器人以其母语生成更清晰、更准确的输出。


目前可以试用 GPT-4 吗?

GPT-4 已经被纳入 Duolingo、Stripe 和 Khan Academy 等产品中。虽然它不是免费公开提供的,但可以通过每月 20 美元订阅 ChatGPT Plus 来立即访问。ChatGPT 的免费版本仍在 GPT-3.5 上运行。


底线


总之,OpenAI 发布 GPT-4 标志着人工智能的一个重要里程碑。GPT-4 是一种高度先进的语言模型,可以处理和分析图像,生成准确且富有创意的输出,并同时处理更大量的信息。它的功能包括以惊人的准确性解决复杂问题、学习用户的写作风格以及在浏览器中编写视频游戏。


此外,GPT-4 比其前身 GPT-3.5 更难欺骗且更准确。总的来说,GPT-4 是一个游戏规则改变者,有望为开发人员和研究人员开辟新的可能性,我们迫不及待地想看看它将如何改变 AI 格局。


毫无疑问,它拥有类人的开发能力,会写代码,但在创造力和智慧上,无人能敌!


常见问题


Chatgpt-4 发布日期是什么时候?

Chatgpt-4 发布日期为 2023 年 3 月 14 日,仅供付费用户使用。


我如何访问 GPT-4?

转到chat.openai.com/,您需要获得付费版本才能访问 GPT-4。


GPT 4 代表什么?

GPT 完整形式是 Generative Pre-trained Transformer。


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

Android 流畅性三板斧之帧率监控

前言 Android 流畅性监控的三板斧,这里所指是【帧率的监控】,【卡顿监控】和【ANR的监控】。之所以讲这三者放在一起是他们的联系比较密切。帧率的下降往往伴随着有卡顿,【过分卡顿】往往就会产生ANR。 严谨的讲,帧率下降不一定会有卡顿(这里对卡顿是从技术角...
继续阅读 »

前言


Android 流畅性监控的三板斧,这里所指是【帧率的监控】,【卡顿监控】和【ANR的监控】。之所以讲这三者放在一起是他们的联系比较密切。帧率的下降往往伴随着有卡顿,【过分卡顿】往往就会产生ANR。


严谨的讲,帧率下降不一定会有卡顿(这里对卡顿是从技术角度定义在主线程执行了耗时任务),卡顿产生的原因还有其他因素导致,比如系统负载、CPU繁忙等。关于卡顿的详细内容放在流畅性三板斧的第二篇。


【过分的卡顿】也不一定产生ANR,卡顿但未触发ANR产生的条件就不会产生ANR。关于ANR的详细内容我们放在三板斧系列文章的第三篇。


该篇我们从应用开发者的角度,探索在应用层监控帧率的四种方式。


温馨提示,本文涉及的实现的代码以上传至github github.com/drummor/Get…,结合代码食用更佳


1 什么是帧率



帧率(Frame rate)是以帧称为单位的位图图像连续出现在显示器上的频率(速率)。



2 Android 中帧率的监控


线下开发我们可以使用开发者选项的帧率监控或者 adb shell dumpsys gfxinfo packagename进行监控针对性优化。这些方案不能带到线上。


惯常我们在Android里线下对帧率的监控主要依托Choreographer,关于Choreographer不再赘述在其他的文章有比较全面的介绍可以看这两篇文章



3 简单监控帧率方案


利用Choreographer的postcallback方法接口轮询方式,能够对帧率进行统计。


image.png


choreographer.postCallback()内部是挂载了一个CALLBACK_ANIMATION类型的callback。轮训方式往choreographer内添加callback,相邻两个callback执行时间间隔即能粗略统计单帧的耗时。严谨的讲这不是单帧的耗时而是两个【半帧】拼凑的耗时。


代码示例如下。


class PoorFrameTracker {
   private var mLastFrameTime = -1L
   private var mFrameCount: Int = 0
   val calRate = 200 //ms
   fun startTrack() {
       mLastFrameTime = 0L
       mFrameCount = 0
       Choreographer.getInstance().postFrameCallback(object : FrameCallback {
           override fun doFrame(frameTimeNanos: Long) {
               if (mLastFrameTime == -1L) {
                   mLastFrameTime = frameTimeNanos
              }
               val diff = (frameTimeNanos - mLastFrameTime) / 1_000_000.0f
               if (diff > calRate) {
                   var fps = mFrameCount / diff * 1000
                   if (fps > 60) {fps = 60.0f}
                   //todo :统计
                   mFrameCount = 0
                   mLastFrameTime = -1
              } else {
mFrameCount++
              }
               Choreographer.getInstance().postFrameCallback(this);
          }
      })
  }
}

优点



  • 简单快捷,无黑科技


缺点



  • 无活动时,也会监控,无效信息会把帧率较低时给平均掉。

  • 对应用带来不必要的负担。


4 帧率监控进化之一 hook Choreographer


针对章节三的方案,首先我们有两个主要的优化方向希望在主线程不活动的时候不进行帧率的检测


我们调用公开api Choreographer.postCallback()时会触发垂直同步(这部分可以参考另一篇文章)。


 # choreographer
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
            implements Runnable {
       private long mTimestampNanos;
        @Override
        public void onVsync(long timestampNanos, long physicalDisplayId, int frame,
                VsyncEventData vsyncEventData)
{
          ...
                mTimestampNanos = timestampNanos;
                Message msg = Message.obtain(mHandler, this);
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
        ...
        }
        @Override
        public void run() {
            mHavePendingVsync = false;
            doFrame(mTimestampNanos, mFrame, mLastVsyncEventData);
        }
    }


  • 【采集每帧的开始】利用Looper中Printer采集Message的开始和结束。上段代码是Choreographer中的一段代码。当收到底层垂直同步信号的时,利用Handler机制post的一个Runable,执行该帧的动作doFrame()。依次我们可以采集到每帧的开始和结束。


# Choreographer
private final CallbackQueue[] mCallbackQueues;

image.png



  • 【过滤出每帧的执行动作】我们知道主线程中不单单执行每帧的动作,还会执行其他动作。如何过滤出执行的是每帧的动作。反射往Choreographer往里添加callback不触发垂直同步,同时在同步信号回调时,会调用我们传入的callback,如果执行了传入的callbacl就可以标识该次执行动作是帧的执行动作。

  • 【采集真实的垂直同步到达时间】反射拿到mTimestampNanos

  • 结合以上,我们能够采集到每帧执行耗时,依次可以计算出准确的帧率。且比我们第一种方案要优雅很多。


  void doFrame(long frameTimeNanos, int frame, DisplayEventReceiver.VsyncEventData vsyncEventData) {
      ...
       final long frameIntervalNanos = vsyncEventData.frameInterval;
       doCallbacks(Choreographer.CALLBACK_INPUT, frameData, frameIntervalNanos);
       doCallbacks(Choreographer.CALLBACK_ANIMATION, frameData, frameIntervalNanos);
       doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameData, frameIntervalNanos);
       doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameData, frameIntervalNanos);
       doCallbacks(Choreographer.CALLBACK_COMMIT, frameData, frameIntervalNanos);
      ...
  }


  • 同时我们还可以通过反射的方式给Chorographer 里 mCallbackQueues添加不同的类型动作,采集不同类型动作的耗时。


补充


image.png



  • 严格意义上,该方案统计的也不是真实的帧率,而是一帧所有耗时中在UI Thread执行部分的耗时,上图doFrame部分。其他线程和进程还会执行其他动作最终才能完成一帧的绘制。但对于我们应用层来说更关注监控doFrame,我们在应用开发层面大部分能够干预的也在doFrame这部分。


(方案思路Matrix)


关于这个方案可查看: github.com/drummor/Get…


5 帧率监控进化之二 滑动帧率


#View
   protected void onScrollChanged(int l, int t, int oldl, int oldt) {
  ...
       final AttachInfo ai = mAttachInfo;
       if (ai != null) {
           ai.mViewScrollChanged = true;
      }
...
  }


  • View里如果有滑动行为产生最终都会调用到onScrollChanged(),当该方法调用的时候,会将mAttachInfo的mViewScrollChanged值设为true


#ViewRootImpl
   private boolean draw(boolean fullRedrawNeeded, boolean forceDraw) {
...
       if (mAttachInfo.mViewScrollChanged) {
           mAttachInfo.mViewScrollChanged = false;
           mAttachInfo.mTreeObserver.dispatchOnScrollChanged();
      }
  }



  • 如上代码ViewRootImpl的draw方法会如果check到mAttachInfo.mViewScrollChanged值为true就会就会调用ViewTreeObserverdispatchOnScrollChanged()方法,只要我们在viewTreeObserver设置监听,就能获取到界面是否正在滑动这一重要事件。


image.png




  • 整个过程的如上图所示,我们收到滑动回调这一事件的时候,其实是choreographer的doFrame()调用而来。




  • 结合上面我们就可以在收到【滑动事件】的时候使用Choreographer的postCallback开始统计帧率。




  • 什么时候结束呢?在没有【滑动信息】生成出来的时候看下面代码


       private var isScroll = false
       init {
           window.decorView.viewTreeObserver.addOnScrollChangedListener {
            //标识正在滑动
               isScroll = true
    //开始统计帧率        
               Choreographer.getInstance().postFrameCallback(FrameCallback())
          }
      }

      private inner class FrameCallback : Choreographer.FrameCallback {
           override fun doFrame(frameTimeNanos: Long) {
               if (isScroll) {
                   isScroll = false //重置滑动状态
                   if (lastFrameTime != 0L) {
                       val dropFrame =
                          (((frameTimeNanos - lastFrameTime) / 1000000f / 16.6667f) + 1f).toInt()
                       notifyListener(dropFrame)
                  }
                   lastFrameTime = frameTimeNanos
              } else {
                   lastFrameTime = 0
              }
          }
      }

    这样我们就实现了一个监控滑动帧率的方案,代码实现放在了 github.com/drummor/Get…




(方案来自淘宝技术团队)


6 帧率监控进化 之三 官方方案


官方出手,官方在Android N 以上新增了Window.OnFrameMetricsAvailableListener可以监听每帧的执行状态。包含总耗时,绘制耗时,布局耗时,动画耗时,测量耗时。依次我们可以计算出帧率。


  private val metricsAvailableListener =
       Window.OnFrameMetricsAvailableListener { window, frameMetrics, dropCountSinceLastInvocation ->
           val intent = frameMetrics?.getMetric(FrameMetrics.INTENDED_VSYNC_TIMESTAMP) ?: 0
           val vsync = frameMetrics?.getMetric(FrameMetrics.VSYNC_TIMESTAMP) ?: 0
           val animation = frameMetrics?.getMetric(FrameMetrics.ANIMATION_DURATION) ?: 0
           val vsyncTotal = frameMetrics?.getMetric(FrameMetrics.TOTAL_DURATION) ?: 0
           val measureCost = frameMetrics?.getMetric(FrameMetrics.LAYOUT_MEASURE_DURATION) ?: 0    
           //计算帧率
      }

this.window.addOnFrameMetricsAvailableListener(//向window注册监听
                metricsAvailableListener,
  Handler(handlerThread.looper)

同时配合Jetpack的FrameMetricsAggregator的可以统计出帧耗时情况。


 private val frameMetricsAggregator = FrameMetricsAggregator()
frameMetricsAggregator.add(this@FrameActivity)
frameMetricsAggregator.metrics?.let {
               it[FrameMetricsAggregator.TOTAL_INDEX] //总耗时概况
               it[FrameMetricsAggregator.INPUT_INDEX] //输入事件耗时
               it[FrameMetricsAggregator.DRAW_INDEX]  //绘制事件耗时概况
          }

FrameMetricsAggregator内部存储比较有意思,是有一个SparseIntArray数组SparseIntArray[] mMetrics = new SparseIntArray[LAST_INDEX + 1],存储各个阶段的耗时SparseIntArray的key为耗时,value为该耗时的个数。


mMetrics[TOTAL_INDEX]:
{3=8, 4=13, 5=2, 6=44, 7=4, 15=1, 196=1, 198=1, 204=1, 209=1, 210=1, 233=1, 265=1}

如上这是每帧总耗时的分布,耗时3ms的有8个,耗时4ms的有8个


我们可以制定自己的标准,诸如单帧耗时<30ms为优秀,单帧耗时>30ms 且<60ms为正常,单帧耗时>60ms且<200ms为过高,单帧>200为严重。


7 数据统计


首先有一个大的原则,帧耗时统计是在有渲染动作发生时统计,空闲状态不统计。


帧率的统计就是,渲染帧的数量除以有帧渲染发生动作时间得到。


另,每帧的耗时不尽相同,希望抓住主线,针对性的统计慢帧冻帧的数量以及占比。或者切割的更为精细,如Matrix里默认的把帧的耗时表现分为四个等级。



  • 正常帧,<3*16ms

  • 中间帧,<9*16ms

  • 慢帧,<24*16ms

  • 冻帧,<42*16ms


再有就是,如通过adb shell dumpsys gfxinfo packagename命令或者FrameMetricsAggregator里的统计方式,把相同耗时的帧进行合并。


帧的统计往往以page(Activity)为维度,作为一个数据大盘数据。


8 其他



  • 帧率真实一个笼统的指标,会存在单帧耗时很高,还是帧率平均下来很优秀,从数据上看问题不大,但是用户的感知会比较强烈。我们更需要做的找到那个隐藏着的【耗时高】的单帧;我们需要全面的对主线程里的执行任务进行全面的监控,也就是卡顿监控的范畴。

  • 帧率只是统计【页面绘制】的概况,不能够全面反映主线程的耗时情况。主线程如果存在耗时动作,比如一个主线程的Handler的执行了一个>100ms的任务,如果此时并没有绘制任务需要执行,此时的不一定帧率就会降低。

  • 【warning!!】最后,已经困扰好几天,实际测试中发现,使用Window.OnFrameMetricsAvailableListener与hook choreograoher方案对比,Window.OnFrameMetricsAvailableListener有漏报的情况产生。这需要看framework源码进一步追查,有对这方面有研究的同学欢迎留言讨论。

  • 本文涉及的实现的代码以上传至github github.com/drummor/Get…


关注点赞鼓励,流畅性三板斧系列剩下的两篇,卡顿监控和ANR监控也会陆续放出。



作者:Drummor
来源:juejin.cn/post/7217801491188809789
收起阅读 »

这一次,我还是想选择自由

辞职回老家有一周多了。 这几天我啥也没写,一直在考虑是继续找工作还是真正开始自由职业。 找下份工作肯定是有高和稳定的收入,但很有可能还是做不喜欢做的事情,可能是和之前一样一成不变的状态,每天准时上下班通勤,下班在一线大城市的出租屋里打游戏看小说,偶尔写点技术...
继续阅读 »

辞职回老家有一周多了。


这几天我啥也没写,一直在考虑是继续找工作还是真正开始自由职业。



找下份工作肯定是有高和稳定的收入,但很有可能还是做不喜欢做的事情,可能是和之前一样一成不变的状态,每天准时上下班通勤,下班在一线大城市的出租屋里打游戏看小说,偶尔写点技术文章。


自由职业的话收入不稳定,赚多赚少都要靠自己。但可以住在小县城的家里,有妈妈做的好吃的菜,有可爱粘人的猫猫,有我新买的投影仪可以和妈妈一起看电视,可以和美好的一切在一起。我不喜欢旅游之类的,宅在家里就已经是我最幸福的状态了。




而且具体做啥可以自己来决定,我有挺多想研究的东西的。


这两天也在面试了,还是那些八股文,卷来卷去的,没啥意思。可能如果真的去了字节,我会更不适应。要不还是不继续面了。


我去年也自由职业过,现在和那时候的区别是我粉丝更多了,技术积累也更多了,而且给我妈新买了个房子,可以在这里继续我的神光实验室。



上图是神光实验室 1.0,之前在老家附近租的一个出租屋。


神光实验室 2.0 是这样的,在新家里:




上次结束自由职业是因为我爸的要求,他说还是希望我有个正当工作。


现在我爸没了,没有人会阻止我了。


我没有负债,还有一定的积蓄,而且我现在啥也不干也有能养活自己的收入。




要不就再任性一次,在家里继续自己的技术梦想,继续搞神光实验室?🤔


就这么愉快的决定了!


这一次,我还是想遵循自己的内心,选择自由,选择和喜欢的一切在一起。


以后公众号会保持日更,其余时间写小册和准备出版的书。


努力一点的话,各方面应该还是可以的。



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

如何使用 ChatGPT 3.5 API 创建自己的智能应用?

前言 OPEN AI 的开放 API 可以说是前端开发者的福利,我们只需要调用 api,就可以创建一个智能应用, 在上一篇文章中,我们介绍了《基于 ChatGPT API 的划词翻译浏览器脚本实现》,使用的模型是 text-davinci-003 也就是文本补...
继续阅读 »

前言


OPEN AI 的开放 API 可以说是前端开发者的福利,我们只需要调用 api,就可以创建一个智能应用,
在上一篇文章中,我们介绍了《基于 ChatGPT API 的划词翻译浏览器脚本实现》,使用的模型是 text-davinci-003 也就是文本补全模型,今天我们将使用 gpt-3.5-turbo 模型来实现一个场景化的智能应用。


OPEN AI API 介绍


自动完成 API


POST https://api.openai.com/v1/completions


以下是自动完成 API,有了 OPENAI_API_KEY 之后,我们只需要传入 prompt


const OPENAI_API_KEY = "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";

const prompt = `Translate this into Chinese:
hello world`
;
const res = await fetch("https://api.openai.com/v1/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
authorization: `Bearer ${OPENAI_API_KEY}`,
},
body: JSON.stringify({
model: "text-davinci-003",
prompt,
max_tokens: 1000,
temperature: 0,
}),
});
const response = await res.json();

const result = response.choices[0].text;

对话 API


POST https://api.openai.com/v1/chat/completions


由于自动补全 API 只能传入一个参数 prompt,AI 不能够理解上下文的场景,因此 gpt-3.5+ API 是为了让 AI 能够支持基于一组对话来返回数据。


在 Node.js 中可以使用以下代码来实现。


const OPENAI_API_KEY = "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";

const prompt = [...];
const res = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
authorization: `Bearer ${OPENAI_API_KEY}`,
},
body: JSON.stringify({
model: "gpt-3.5-turbo",
messages,
temperature: 0.7,
top_p: 1,
frequency_penalty: 0,
presence_penalty: 0,
max_tokens: 500
}),
});
const response = await res.json();

const result = response.choices[0].message

以下是官网给出 messages 例子


const messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Who won the world series in 2020?"},
{"role": "assistant", "content": "The Los Angeles Dodgers won the World Series in 2020."},
{"role": "user", "content": "Where was it played?"}
]


  • 每一个 message 由 rolecontent 组成。

  • role 只能是 3 个值, systemuserassistant

  • systemassistant 是可选的,user 是必须的。


官方提供了 playground 帮助我们创建 messages 信息。


openai playground


assistant 也就是其中一次返回的数据信息。
发送的 messages 如下:


const messages=[
{
"role": "system",
"content": "你是一名精通 typescript 的前端工程师,不需要解释"
},
{
"role": "user",
"content": "Convert the following JSON to typescript interface without explanation\n\n{\n \"name\": \"Allen\",\n \"age\": 18\n}"
}
]

比如使用上面的 messages 信息,我们就可以根据它,来创建一个 Tailwind css 代码生成器。


openai playground 拷贝 fetch


通过右键可以直接拷贝为 Node.js fetch 代码。


再来实现一个 JSON 转 Typescript 的例子


openai playground JSON 转 Typescript


那么我们通过以上截图的 messages,就可以创建一个 JSON 转 Typescript 生成器。


在 Next.js 使用


接下来,我们就在 Next.js 中创建一个全栈应用。


那为什么选择使用 Next.JS 呢?



  1. 它是一个全栈框架,既可以写接口也可以使用 react 写前端;

  2. 可以很轻松部署到 verel, 让我们可以直接访问 OPENAI 的接口,摆脱网络限制。


这里我选择使用大圣老师的email-helper模板


创建github仓库


点击 GitHub 选择 Use this Template, 创建一个自己的仓库


目录结构


这个项目很简单,在 pages 目录下 api/generate.ts 用于代理请求接口。


index.tsx 也就是我们的主界面,一个按钮,一个请求,没有其他复杂逻辑。


接下来我们就根据它来创建一个智能的Tailwind CSS 代码生成器


1、首先将 messages 改成以上截图中的 message


2、然后将需要生成的变量存到 state 中,我们就可以实现如下界面


Tailwind CSS 代码生成器界面


点击生成代码就可以 让 ai 帮我们写代码了。


这个界面,有些单调,可以在这个页面上列一些常用的组件,那么也可以直接使用 chatGPT 来生成。


chatGPT 生成组件


将 GPT 回答直接转换成 JSON 数据


chatGPT 转 JSON 数据


将数据渲染到页面中,就可以生成快捷标签了


实现效果


接下来,再将 Tailwind css 的颜色,作为我们的变量,同样使用 GPT 来生成数据


生成 Tailwind 颜色


用同样的方式,转化成 JSON,拷贝到我们的代码中。


Tailwind CSS 代码生成器效果


最后一步,我们需要实现一个预览效果,这样的话,就可以所见即所得,根据效果,直接拷贝想要的代码。



小结


本文介绍了 openai 的 api 使用方法,以及如何使用 openai 的 playground 生成需要的 messages 信息。并且通过一个 Next.js 实战例子,结合 ChatGPT 开发了一个 Tailwind CSS 代码生成器。


最后


贴一下文本的代码仓库和预览地址


代码仓库:github.com/maqi1520/op…


预览地址:openai.maqib.cn/


如果对你有帮助,记得给个三连,感谢你的阅读。





作者:狂奔滴小马
来源:juejin.cn/post/7217820487203192892
收起阅读 »

看了antfu大佬的v-lazy-show,我学会了怎么编译模板指令

web
前言 一开始关注到 antfu 是他的一头长发,毕竟留长发的肯定是技术大佬。果不其然,antfu 是个很高产、很 creative 的大佬,我也很喜欢他写的工具,无论是@antfu/eslint-config、unocss、还是vitest等等。 而这篇文章故...
继续阅读 »

前言


一开始关注到 antfu 是他的一头长发,毕竟留长发的肯定是技术大佬。果不其然,antfu 是个很高产、很 creative 的大佬,我也很喜欢他写的工具,无论是@antfu/eslint-configunocss、还是vitest等等。


而这篇文章故事的起源是,我今天中午逛 github 的时候发现大佬又又又又开了一个新的 repo(这是家常便饭的事),v-lazy-show


image.png


看了下是两天前的,所以好奇点进去看看是什么东东。


介绍是:A compile-time directive to lazy initialize v-show for Vue. It makes components mount after first truthy value (v-if), and the DOM keep alive when toggling (v-show).


简单的说,v-lazy-show 是一个编译时指令,就是对 v-show 的一种优化,因为我们知道,v-show 的原理只是基于简单的切换 display none,false则为none,true则移除


bite-me-i-dare-you.gif


但即使在第一次条件为 falsy 的时候,其依然会渲染对应的组件,那如果该组件很大,就会带来额外的渲染开销,比如我们有个 Tabs,默认初始显示第一个 tab,但后面的 tab 也都渲染了,只是没有显示罢了(实际上没有必要,因为可能你点都不会点开)。


那基于此种情况下,我们可以优化一下,即第一次条件为 falsy 的情况下,不渲染对应的组件,直到条件为 truthy 才渲染该组件。


将原本的 v-show 改为 v-lazy-show 或者 v-show.lazy


<script setup lang="ts">
import { ref } from 'vue'
import ExpansiveComponent from './ExpansiveComponent.vue'

const enabled = ref(false)
</script>

<template>
<button @click="enabled = !enabled">
Toggle
</button>

<div class="hello-word-wrapper">
<ExpansiveComponent v-lazy-show="enabled" msg="v-lazy-show" />
<ExpansiveComponent v-show.lazy="enabled" msg="v-lazy.show" />

<ExpansiveComponent v-show="enabled" msg="v-show" />

<ExpansiveComponent v-if="enabled" msg="v-if" />
</div>
</template>

<!-- ExpansiveComponent.vue -->
<script setup lang="ts">
import { onMounted } from 'vue'

const props = defineProps({
msg: {
type: String,
required: true,
},
})

onMounted(() => {
console.log(`${props.msg} mounted`)
})
</script>

<template>
<div>
<div v-for="i in 1000" :key="i">
Hello {{ msg }}
</div>
</div>
</template>

2023-04-03 15.55.15.gif



ExpansiveComponent 渲染了 1000 行 div,在条件 enabled 初始为 false 的情况下,对应 v-show 来说,其依然会渲染,而对于 v-lazy-show 或 v-show.lazy 来说,只有第一次 enabled 为 true 才渲染,避免了不必要的初始渲染开销



如何使用?


国际惯例,先装下依赖,这里强烈推荐 antfu 大佬的 ni


npm install v-lazy-show -D
yarn add v-lazy-show -D
pnpm add v-lazy-show -D
ni v-lazy-show -D

既然是个编译时指令,且是处理 vue template 的,那么就应该在对应的构建工具中配置,如下:


如果你用的是 vite,那么配置如下


// vite.config.ts
import { defineConfig } from 'vite'
import { transformLazyShow } from 'v-lazy-show'

export default defineConfig({
plugins: [
Vue({
template: {
compilerOptions: {
nodeTransforms: [
transformLazyShow, // <--- 加在这里
],
},
},
}),
]
})

如果你用的是 Nuxt,那么应该这样配置:


// nuxt.config.ts
import { transformLazyShow } from 'v-lazy-show'

export default defineNuxtConfig({
vue: {
compilerOptions: {
nodeTransforms: [
transformLazyShow, // <--- 加上这行
],
},
},
})

那么,该指令是如何起作用的?


上面的指令作用很好理解,那么其是如何实现的呢?我们看下大佬是怎么做的。具体可见源码


源码不多,我这里直接贴出来,再一步步看如何实现(这里快速过一下即可,后面会一步步分析):


import {
CREATE_COMMENT,
FRAGMENT,
createCallExpression,
createCompoundExpression,
createConditionalExpression,
createSequenceExpression,
createSimpleExpression,
createStructuralDirectiveTransform,
createVNodeCall,
traverseNode,
} from '@vue/compiler-core'

const indexMap = new WeakMap()

// https://github.com/vuejs/core/blob/f5971468e53683d8a54d9cd11f73d0b95c0e0fb7/packages/compiler-core/src/ast.ts#L28
const NodeTypes = {
SIMPLE_EXPRESSION: 4,
}

// https://github.com/vuejs/core/blob/f5971468e53683d8a54d9cd11f73d0b95c0e0fb7/packages/compiler-core/src/ast.ts#L62
const ElementTypes = {
TEMPLATE: 3,
}

// https://github.com/vuejs/core/blob/f5971468e53683d8a54d9cd11f73d0b95c0e0fb7/packages/shared/src/patchFlags.ts#L19
const PatchFlags = {
STABLE_FRAGMENT: 64,
}

export const transformLazyShow = createStructuralDirectiveTransform(
/^(lazy-show|show)$/,
(node, dir, context) => {
// forward normal `v-show` as-is
if (dir.name === 'show' && !dir.modifiers.includes('lazy')) {
return () => {
node.props.push(dir)
}
}

const directiveName = dir.name === 'show'
? 'v-show.lazy'
: 'v-lazy-show'

if (node.tagType === ElementTypes.TEMPLATE || node.tag === 'template')
throw new Error(`${directiveName} can not be used on <template>`)

if (context.ssr || context.inSSR) {
// rename `v-lazy-show` to `v-if` in SSR, and let Vue handles it
node.props.push({
...dir,
exp: dir.exp
? createSimpleExpression(dir.exp.loc.source)
: undefined,
modifiers: dir.modifiers.filter(i => i !== 'lazy'),
name: 'if',
})
return
}

const { helper } = context
const keyIndex = (indexMap.get(context.root) || 0) + 1
indexMap.set(context.root, keyIndex)

const key = `_lazyshow${keyIndex}`

const body = createVNodeCall(
context,
helper(FRAGMENT),
undefined,
[node],
PatchFlags.STABLE_FRAGMENT.toString(),
undefined,
undefined,
true,
false,
false /* isComponent */,
node.loc,
)

const wrapNode = createConditionalExpression(
createCompoundExpression([`_cache.${key}`, ' || ', dir.exp!]),
createSequenceExpression([
createCompoundExpression([`_cache.${key} = true`]),
body,
]),
createCallExpression(helper(CREATE_COMMENT), [
'"v-show-if"',
'true',
]),
) as any

context.replaceNode(wrapNode)

return () => {
if (!node.codegenNode)
traverseNode(node, context)

// rename `v-lazy-show` to `v-show` and let Vue handles it
node.props.push({
...dir,
modifiers: dir.modifiers.filter(i => i !== 'lazy'),
name: 'show',
})
}
},
)

createStructuralDirectiveTransform


因为是处理运行时的指令,那么自然用到了 createStructuralDirectiveTransform 这个函数,我们先简单看下其作用:


createStructuralDirectiveTransform 是一个工厂函数,用于创建一个自定义的 transform 函数,用于在编译过程中处理特定的结构性指令(例如 v-for, v-if, v-else-if, v-else 等)。


该函数有两个参数:




  • nameMatcher:一个正则表达式或字符串,用于匹配需要被处理的指令名称。




  • fn:一个函数,用于处理结构性指令。该函数有三个参数:



    • node:当前节点对象。

    • dir:当前节点上的指令对象。

    • context:编译上下文对象,包含编译期间的各种配置和数据。




createStructuralDirectiveTransform 函数会返回一个函数,该函数接收一个节点对象和编译上下文对象,用于根据指定的 nameMatcher 匹配到对应的指令后,调用用户自定义的 fn 函数进行处理。


在编译过程中,当遇到符合 nameMatcher 的结构性指令时,就会调用返回的处理函数进行处理,例如在本例中,当遇到 v-show 或 v-lazy-show 时,就会调用 transformLazyShow 处理函数进行处理。


不处理 v-show


if (dir.name === 'show' && !dir.modifiers.includes('lazy')) {
return () => {
node.props.push(dir)
}
}

因为 v-show.lazy 是可以生效的,所以 v-show 会进入该方法,但如果仅仅只是 v-show,而没有 lazy 修饰符,那么实际上不用处理


这里有个细节,为何要将指令对象 push 进 props,不 push 行不行?


原先的表现是 v-show 条件为 false 时 display 为 none,渲染了节点,只是不显示:


image.png


而注释node.props.push(dir)后,看看页面表现咋样:


image.png


v-show 的功能没了,也就是说指令的功能会添加到 props 上,所以这里要特别注意,不是单纯的返回 node 即可。后来还有几处node.props.push,原理跟这里一样。


服务端渲染目前是转为 v-if


if (context.ssr || context.inSSR) {
// rename `v-lazy-show` to `v-if` in SSR, and let Vue handles it
node.props.push({
...dir,
exp: dir.exp
? createSimpleExpression(dir.exp.loc.source)
: undefined,
modifiers: dir.modifiers.filter(i => i !== 'lazy'),
name: 'if',
})
return
}

将 v-lazy-show 改名为 v-if,且过滤掉修饰符


createVNodeCall 给原先节点包一层 template


顾名思义,createVNodeCall 是 用来创建一个 vnode 节点的函数:


const body = createVNodeCall(
/** 当前的上下文 (context) 对象,即 CodegenContext */
context,
/** helper 函数是 Vue 内部使用的帮助函数。FRAGMENT 表示创建 Fragment 节点的 helper 函数 */
helper(FRAGMENT),
/** 组件的 props */
undefined,
/** 当前节点的子节点数组,即包含有指令的节点本身 */
[node],
/** 表示该节点的 PatchFlag,指明了该节点是否稳定、是否具有一些特定的更新行为等。STABLE_FRAGMENT 表示该 Fragment 节点是一个稳定的节点,即其子节点不会发生改变 */
PatchFlags.STABLE_FRAGMENT.toString(),
/** 该节点的动态 keys */
undefined,
/** 该节点的模板引用 (ref) */
undefined,
/** 表示该节点是否需要开启 Block (块) 模式,即是否需要对其子节点进行优化 */
true,
/** 表示该节点是否是一个 Portal 节点 */
false,
/** 表示该节点是否是一个组件 */
false /* isComponent */,
/** 该节点在模板中的位置信息 */
node.loc,
)

参数含义如下,简单了解即可(反正看了就忘)


也就是说,其会生成如下模板:


<template>
<ExpansiveComponent v-lazy-show="enabled" msg="v-lazy-show" />
</template>

关键代码(重点)


接下来这部分是主要原理,请打起十二分精神。


先在全局维护一个 map,代码中叫 indexMap,是一个 WeakMap(不知道 WeakMap 的可以去了解下)。然后为每一个带有 v-lazy-show 指令的生成一个唯一 key,这里叫做_lazyshow${keyIndex},也就是第一个就是_lazyshow1,第二个是_lazyshow2...


  const keyIndex = (indexMap.get(context.root) || 0) + 1
indexMap.set(context.root, keyIndex)

const key = `_lazyshow${keyIndex}`

然后将生成的key放到渲染函数的_cache上(渲染函数的第二个参数,function render(_ctx, _cache)),即通过_cache.${key}作为辅助变量。之后会根据 createConditionalExpression 创建一个条件表达式


const wrapNode = createConditionalExpression(
createCompoundExpression([`_cache.${key}`, ' || ', dir.exp!]),
createSequenceExpression([
createCompoundExpression([`_cache.${key} = true`]),
body,
]),
// 生成一个注释节点 `<!--v-show-if-->`
createCallExpression(helper(CREATE_COMMENT), [
'"v-show-if"',
'true',
]),
)

也就是说, v-lazy-show 初始传入的条件为 false 时,那么会为你创建一个注释节点,用来占位:


createCallExpression(helper(CREATE_COMMENT), [
'"v-show-if"',
'true',
])

image.png



这个跟 v-if 一样



直到第一次条件为真时,将 _cache.${key} 置为 true,那么以后的行为就跟 v-show 一致了,上面的 dir.exp 即指令中的条件,如


<div v-show="enabled"/>

enabled 即 exp,表达式的意思。


readme给出的转换如下:


<template>
<div v-lazy-show="foo">
Hello
</div>
</template>

会转换为:


import { Fragment as _Fragment, createCommentVNode as _createCommentVNode, createElementBlock as _createElementBlock, createElementVNode as _createElementVNode, openBlock as _openBlock, vShow as _vShow, withDirectives as _withDirectives } from 'vue'

export function render(_ctx, _cache) {
return (_cache._lazyshow1 || _ctx.foo)
? (_cache._lazyshow1 = true, (_openBlock(),
_withDirectives(_createElementVNode('div', null, ' Hello ', 512 /* NEED_PATCH */), [
[_vShow, _ctx.foo]
])))
: _createCommentVNode('v-show-if', true)
}

你可以简单理解为会将<ExpansiveComponent msg="v-lazy-show" v-lazy-show=""enabled"/>转为下面:


<template v-if="_cache._lazyshow1 || enabled">
<!-- 为true时会把_cache._lazyshow1置为true,那么以后的v-if就用于为true了 -->
<ExpansiveComponent msg="v-lazy-show" v-lazy-show="enabled"/>
</template>
<template v-else>
<!--v-show-if-->
</template>

<template v-if="_cache._lazyshow2 || enabled">
<!-- 为true时会把_cache._lazyshow2置为true,那么以后的v-if就用于为true了 -->
<ExpansiveComponent msg="v-lazy-show" v-show.lazy="enabled"/>
</template>
<template v-else>
<!--v-show-if-->
</template>

然后将原先节点替换为处理后的 wrapperNode 即可


context.replaceNode(wrapNode)

最后将 v-lazy-show | v-shouw.lazy 处理为 v-show


因为 vue 本身是没有 v-lazy-show 的,v-show 也没有 lazy 的的修饰符,那么要让指令生效,就要做到两个:



  1. 将原先的 show-lazy 改名为 show

  2. 过滤掉 lazy 的修饰符


node.props.push({
...dir,
modifiers: dir.modifiers.filter(i => i !== 'lazy'),
name: 'show',
})

也就变成这样啦:


<template v-if="_cache._lazyshow1 || enabled">
<!-- 为true时会把_cache._lazyshow1置为true,那么以后的v-if就用于为true了 -->
<ExpansiveComponent msg="v-lazy-show" v-show="enabled"/>
</template>
<template v-else>
<!--v-show-if-->
</template>


<template v-if="_cache._lazyshow2 || enabled">
<!-- 为true时会把_cache._lazyshow2置为true,那么以后的v-if就用于为true了 -->
<ExpansiveComponent msg="v-show.lazy" v-show="enabled"/>
</template>
<template v-else>
<!--v-show-if-->
</template>

小结一下:




  1. 为每一个使用 v-lazy-show 分配唯一的 key,放到渲染函数内部的_cache上,即借助辅助变量_cache.${key}



    • 当初始条件为 falsy 时不渲染节点,只渲染注释节点 <!--v-show-if-->

    • 直到条件为真时将其置为 true,之后的表现就跟 v-show 一致了





  1. 由于 vue 不认识 v-lazy-show,v-show.lazy,使用要将指令改回 v-show,且过滤掉 lazy 修饰符(如果使用 v-show.lazy 的话)


最后


以上就是我对该运行时编译插件的认识了,可以将 repo 拉下来,上面有个 playground,可以自己调试调试,说不定有新的认识。


好了,文章到此为止,你今天学废了吗?


image.png



作者:暴走老七
来源:juejin.cn/post/7217836890119995450
收起阅读 »

程序员“摸鱼”神器,GitHub Copilot“凭本事”完全免费!!

上周Microsoft 365 Copilot的发布会上几段演示视频让朋友圈沸腾了一整天,颠覆,失业,工业革命刷屏,普遍的焦虑中有工作中重度依赖office的朋友表示如果功能都是真的,那么确实可以节约出很多时间摸鱼,但是立马就有人提醒他或许老板觉得可以节约掉一...
继续阅读 »

上周Microsoft 365 Copilot的发布会上几段演示视频让朋友圈沸腾了一整天,颠覆,失业,工业革命刷屏,普遍的焦虑中有工作中重度依赖office的朋友表示如果功能都是真的,那么确实可以节约出很多时间摸鱼,但是立马就有人提醒他或许老板觉得可以节约掉一半的人省下成本换跑车。


各类信息流中也夹杂着对文心一言的冷嘲热讽,熊熊烈火,车水马龙的梗证实了文心一言确实支持多模态,对中文也有更深的理解...但是不多(纯调侃,本人看好文心的未来)。


图片


图片


我也看了微软发布会的录播和文心一言发布会的直播,发布会上二者的演示内容的形式都是播放视频(为什么只有百度因为这点被骂)。从产品的一系列命名可以看出,微软还是希望以人为中心产出内容,AI只是一个副机长,协助主角更高效地产出,从而让机长把时间留给更有价值,更需要脑力的事情上。 李彦宏在发布的全过程中其实也没有自吹自擂,甚至反复承认目前的效果一般,内部体验的过程中还有不少瑕疵,同时也花了很多口舌强调文心一言 【未来】在中文场景中表现会好于ChatGPT。


对比下来,同样的PPT发布,二者都宣称自己不是简单的GPT套壳,而是结合了自身的优势,借力GPT深度整合以及增强了自家产品。


我看到Copilot这个词,还是下意识地先想起了一些跟程序员有关的事情。


其实早在2021年6月份的时候,GitHub(已被微软收购)就发布了GitHub Copilot,对,也叫Copilot,但是一直没有引起很广泛的关注,原因我猜测有以下几点




  • 程序员圈子的事情,没有出圈




  • 彼时LLM,GPT等概念还没有火,没有获得广泛关注




  • 普遍觉得AI真正在编程这件事上取代人还言之过早




那么站在发布接近2年之后的今天,GitHub Copilot可以做些什么呢?


从功能性的角度出发,其实官网最显眼的位置已经概括得非常准确了


图片


Github Copilot基于openAI预训练的模型,在几十亿行的公开代码的基础上进行了训练,可以将自然语言描述的逻辑转换为代码建议,目前已经支持几十种编程语言。通过这个“助手”,全世界的开发者都可以从样版代码,重复工作等dirtywork中解放出来,把精力聚焦在更重要的事情上——构建伟大的软件!!


GO语言的效果如下:


图片


TS的效果如下:


图片


在用户的主观投票上,可以看出大家比较认可Github Copilot带来的生产力提升,但是有一说一,这个是主观的评价,并无法知道从客观的角度上,老板主观上是不是觉得你提效了(当然你可以只为了自己开心),嘻嘻。


图片


Github Copilot带来的另一个重大的意义——有了这个副机长的帮助,在面对自己不熟悉的语言或者平台编程的时候,你会更有信心。不会在一些低级错误上面拉扯很久,减少在基础的问题上的自信受挫。


在当前主流的IDE或者编辑器上都可以找到插件,目前每一个GitHub的普通用户都可以获得60天的试用期。


图片


具体的使用步骤,这里以VSCode为例:


step1: 在应用商店搜索Github Copilot,点击安装


图片


step2: 安装完成之后,会弹出提示框跳转到Github登录


图片


step3: 登录完成之后,会叫你充钱。😊


图片


60天试用的标题非常醒目!!


最底下还有两行文字说明了可以免费试用的人群,这里我给大家放大看看:


图片


如果是你GitHub上面最流行的那部分开源项目的贡献者的话或者是认证的过的学生(有苹果教育优惠内味儿了!!),可以免费使用Github Copilot。至于“最流行的开源项目”包含哪些?我也没有找到这样的一个名单...不过如果你符合要求的话,点开订阅页面的时候,直接就可以看到免费订阅的操作俺妞。


你看看,我标题是不是没有乱取?!!是不是真有人可以完全免费?!!!(逃)


我很有B数,乖乖准备充钱了。支持信用卡或者贝宝,...反正我最后没充钱成功,有谁成功了留言告诉我哦。😊


图片


如果在VSCode的应用商店中搜索Github Copilot Labs插件的话,你就会发现一款插件的插件,本质上是将一些常用的Prompts封装成了按钮和可视化的操作,比如:


解析代码(帮你看懂一块屎山代码到底做了啥)


图片


实现语言转换


图片


代码刷子功能


可以增强代码可读性,添加类型,智能修复可能的bug,比code Runner更优秀的即时代码调试,优化冗余代码.....


代码刷子在日常编码中应该是非常实用了,可以有效提升代码质量,建议大家充钱试试。


图片


测试用例生成


这个功能可以说是非常非常非常实用了,平时写单元测试其实非常耗时,而且有不少样版代码,这块工作有人代劳的话,真的是可以省出很多时间(摸鱼)!!**

**


图片


你可能会问了,介绍了这么多功能,怎么不点按钮让大伙儿看看效果?!


肯定不是缺钱,而是真的充钱失败了,可能我的visa卡有问题!


好用归好用,如果你既没有免费的资格,又没有充钱,且还安装了插件的话,插件会非常烦人地弹窗提醒你没权限,连不上(催你充钱)!有点讨厌。


图片


图片


最后,不得不提的是这个工具的局限性,ChatGPT(基于3.5)告诉了我以下几点帮大家避雷。


图片


祝福大家充完钱之后立马变强!!


如果没有充钱,也变强了,请留言告诉我!!


如果没有充钱,也没有变强,请留言告诉我!!


如果充钱了,也没有变强,请留言告诉我!!


如果没有充钱,也没有变强,请留言告诉我!!


图片


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

GPT-4风口来临!20个你可以起飞的姿势

聊天 GPT 正在席卷网络世界,它让人们赚了很多钱。 在本文中,列出了20 个最佳聊天 GPT 商业理念。 我们将介绍您可以使用聊天 GPT 创建的一些 AL工县示例,以及您可以作为个体创业者销售的不同服务。 全文目录: 1.聊天 GPT Saa...
继续阅读 »

聊天 GPT 正在席卷网络世界,它让人们赚了很多钱。


在本文中,列出了20 个最佳聊天 GPT 商业理念。


我们将介绍您可以使用聊天 GPT 创建的一些 AL工县示例,以及您可以作为个体创业者销售的不同服务。


全文目录:




  • 1.聊天 GPT SaaS 业务




  • 2. API 即服务




  • 3.AI自动优酷频道




  • 4. 社交媒体营销机构




  • 5. 使用聊天GPT创建课程




  • 6.开始按需打印商店




  • 7.AI 个人助理




  • 8.AI 自媒体助手




  • 9. 客户服务聊天机器人




  • 10. 财务规划应用程序




  • 11. 健康与保健应用程序




  • 12. 娱乐应用




  • 13. 数字副本




  • 14. 转录应用程序




  • 15.AI 旅游应用




  • 16. 新闻和信息应用程序




  • 17.AI 流媒体服务




  • 18.AI 网红营销




  • 19. 文案服务




  • 20. 编码服务




  • GPT商业理念-最后的话




1.聊天 GPT SaaS 业务


第一个也可能是最有利可图的聊天 GPT 商业理念是开展 SaaS 业务。


去GPT 并创建一个简单的单词计数器。您可以键入如下提示:


“编写工具网站的完整代码,计算文本区域中的字数。使用 HTML、CSS 和 JS”


图片


获得代码后,您可以打开Visual Studio代码,创建三个文件 JavaScript, CSS和HTML,然后简 单地粘贴代码。


我们有一个简单的单词计数器工具。


这就是您可以使用聊天 GPT 创建软件的方式。


您还可以告诉它以使界面更加有趣和用户友好。


你也可以在Code Canyon等网站上出售迷你JavaScript文件。


我知道很多人每个月都在Code Canyon上销售JS文件赚数千美元。


我玩过聊天 GPT,并在不到 1 小时的时间内创建了五个以上的工具。


但现在最大的问题是如何经受住竞争。我们有很多工具。那么如何竞争呢?


只需遵循这 4 条规则:




  1. 创建独特的界面。告诉乍得 GPT 并使用它,直到它创建最佳用户界面。




  2. 在一个网站中组合多个工具。




  3. 在Medium,Reddit和社交媒体上推广您的工具。




  4. 尝试找到低竞争的工具创意。




💡 专业提示


注意:请注意,即使使用 Chat GPT,构建软件也需要一些编码技能。如果您在这些领域没有经验,则可以将其外包给Fiverr等网站,人们已经使用聊天GPT提供网络编程服务。这是一种无需任何编码即可启动 SaaS 业务的简单方法。


2. API 即服务


下一个聊天 GPT 商业理念是创建一个 API 并将其出售为每月定期会员资格。这种方法的好处是无需投资即可开始。


但首先,什么是 API?


API 只是开发人员用来通过代码访问自己操作的数据的东西。


一个简单的例子。如果你现在想在Facebook上发布帖子,你只需要去你的手机或网络浏览器,打开Facebook并使用浏览器或Facebook界面发布。


如果您是开发人员,并且想使用自己的代码在应用程序中访问Facebook,该怎么办?您将需要使用Facebook API。所以简单地说,API就像一个经纪人。


这有点像你、开发人员和某个服务(如Facebook或其他任何东西)之间的中间人。现在,你知道什么是 API,让我们使用 Charge GPT 在几秒钟内创建一个。


在 Charge GPT 中,让我们测试创建一个生成代码的 API,以便任何人都可以在自己的应用程序中使用它。所以这是代码。


图片


您可以复制它,打开 Visual Studio,创建控制器,粘贴代码,API将在不到1分钟的时间内启动并运行。


现在,您需要发布此 APl。为此,您还可以询问聊天 GPT 我们可以在哪里免费发布此内容。在


这里,我们有多种选择,例如Microsoft Azure,AwS Lambda或Google Cloud。


图片


好的,现在,在哪里出售它?


让我们问问聊天 GPT。在这里,我们有很多市场,我们可以在其中发布我们的 API 并像 Rapid API 一样出售它等等。


图片


您甚至可以告诉聊天 GPT 用您想要的任何编程语言(如 Node、JS 或 Python)重写代码,任何您想要的内容。


棒。好吧,看起来很简单。是的。但主要问题在于 API 的想法。从哪里获得人们会购买或开发人员会购买的想法。


可以通过查看顶级API市场来获得一些想法:


● RapidAPI 是一个平台,使开发人员能够查找并连接到数千个 API。它提供各种类别的 API,包括金融、业务健康和媒体。


● ProgrammableWeb 是一个 API 目录,提供有关 API 和提供它们的公司的信息。它允许开发人员按类别搜索 API,并为 API 提供测试和集成工具。


● 邮递员 API 网络是来自领先公司的 API 集合,可用于测试和集成。包括来自PayPal,微软和谷歌等公司的API。


以下是您可以开发和销售的一些 API 即服务理念:


● 提供实时天气数据(包括当前状况、预报和历史数据)的天气 API。企业、开发人员和应用程序创建者可以使用它来向其用户提供与天气相关的信息。


● 一种图像识别API,可以对图片中的对象、场景和活动进行识别和分类。企业和开发人员使用它来创建可以自动识别和组织图像的应用程序,或训练机器学习模型。


● 一种社交媒体API,允许开发人员访问来自各种社交媒体平台(如Facebook,Twitter和Instagram)的数据。企业和开发人员可以使用它来分析社交媒体活动、跟踪指标或构建社交媒体营销工具。


● 一种基于位置的 API,允许开发人员访问与地理位置相关的数据,例如地图、兴趣点和交通信息。


● 一种支付 API,允许开发人员轻松地将各种支付方式(如信用卡、PayPal和 Apple Pay)集成到他们的 app 和网站中。


● 一种语音和语音识别 API,允许开发人员向其应用和设备添加语音控制功能。


● 一种自然语言处理 API,允许开发人员向其应用和设备添加自然语言理解。


3.AI自动优酷频道


看看现在在几乎任何平台上创建 AI 社交媒体形象变得多么容易。


使用聊天 GPT,您可以创建自己的全自动自媒体频道。


您只需转到聊天 GPT 并键入“给我写 1500 字的视频脚本......”。


目前这里唯一的限制是它还无法为您编辑视频。


但是一个真正强大的工具是Pictory AI,它是一个AI视频生成器,可以创建视频而无需制作任何实际镜头或录制您的声音


只需将您的脚本复制到 Pictory AI 中并将脚本推送到视频,即可将此脚本转换为带有 b-roll 的完整视频。


图片


以这种疯狂的创建内容的速度,您每天都可以创建数千条内容。


如果你是一个创作者,并且你将来不会使用人工智能来实际创建你的内容,从长远来看,你会失败。


4. 社交媒体营销机构


随着社交媒体平台逐年增长,越来越多的企业需要社交媒体的存在,因此对社交媒体管理服务的需求不断增加。


如果您选择这条路线,您可以直接向企业提供服务,并创办SMMA代理机构或在Fiverr或Upwork等自由职业平台上创建演出。


ChatGPT 可以通过多种方式为您提供帮助。它可以帮助您制定内容策略,并帮助您撰写引人入胜的社交媒体帖子和标题。


但是,重要的是要记住,社交媒体管理还涉及诸如选择和编辑图像以及回复评论之类的任务,这些任务仍然需要您的个人风格。


5. 使用聊天GPT创建课程


所以我们不得不谈论教育的转型。人们正在从专家那里在线购买课程和辅导计划,这些课程和辅导计划在某种程度上取代了对大学的需求,并不完全是大学的需求,而是很多人并不完全知道如何对待自己的生活。


对于真正小众的话题,例如学习钢琴或音频工程或博客或人们可以购买 1000 美元课程的这些独特事物,这一点变得越来越真实。


假设我们正在制作一个关于如何制作黄油的课程。您可以生成制作黄油的所有步骤的完整列表,并通过在此处和那里进行一些更改来添加您的专业知识,以获得整个课程的完美记录。


图片


如果你想为课程添加一些特定事物的图片,你可以从MidJourney获得不受版权保护的图像,并要求它制作一根扔进锅里的黄油。


图片


就这样,你会在平底锅里看到一张漂亮的黄油图片。


通过这样做,您将拥有可以在 Kartra 或 点击漏斗 等平台上销售的整个课程。


有成千上万的人愿意为信息付费,而不是自己学习。


关键是睁开眼睛看看这个世界,意识到人们每月通过销售课程赚取数百万元。


因此,实际上,最终流量加上后端销售漏斗相当于一百万元的业务。


6.开始按需打印商店


这种商业模式的前提是围绕人们真正感兴趣的特定主题在 Etsy 上创建利基商店。


在这个商业模式中,你的工作是在旅途中创造令人敬畏的艺术,这是一种人工智能艺术生成工具,然后将其放在一些T恤或海报上,并将其放在Etsy上的利基商店中。然后使用按需打印服务来运送和创建这些产品。


要开始,您首先需要找到一个利基市场。Etsy的一个很好的关键字研究工具称为eRank,可让您在Etsy上找到竞争较低的关键字。


图片


一旦找到一个利基市场并找到关键字,您就可以前往 MidJourney 网站,这是一个人工智能艺术生成平台。


您需要做的就是使用提供的提示开始创建自己的艺术。


图片


既然您已经拥有了一件您认为您的利基市场中的人会喜欢的好艺术品,您现在需要创建一个 Etsy 帐户,然后将该 Etsy 帐户连接到按需打印服务,例如 Printful.


Printful 是一款很棒的按需印刷品,可让您将图像放在杯子、海报 T 恤和一大堆其他商品上。


图片


Printful 有一个集成,可让您直接连接到您的 Etsy 商店。


然后,您可以开始创建不同的物品并在Etsy上出售它们。


这是一个如此有利可图且可扩展的商业模式的原因是,Etsy为您带来了所有流量,而不必推销您的任何东西。


您需要做的就是点击这些正确的关键字,然后您的东西将被营销给正在寻找这种商品的合适人。


7.AI 个人助理


现在,下一个可能是最自然的用例,目前每个人都有一个AI通用助手,无论是Alexa还是Siri,甚至是Bixby。诀窍将是进入一个真正薄片的市场。


我的意思是,你真的不希望Siri给你健身建议。您需要为该利基市场训练专门的语言模型,并提供干净的用户界面供客户使用。您可以训练您的 AI 模型以专注于健身旅行或金融并提供指导。


潜在的目标受众将是寻求数字助理来帮助他们管理日常任务的个人和企业。


8.AI 自媒体助手


正如我们刚刚了解到的,我们可以训练人工智能模型来实际模拟一个真实的人,就像我们在上一个想法中对格雷厄姆·斯特凡(Graham Stefan)建模一样。现在,您为AI模型提供的输入越多,它就会变得越准确,越智能。


考虑到这一点内容创作者要回应他们的粉丝是一项艰巨的任务。通常,如果他们足够大,他们只有两到三秒钟的时间来对每条评论说谢谢。但是,使用由AI模型提供支持的简单Chrome插件,该插件可以学习您的语气,节奏,喜欢说什么以及您通常如何回应其中一条评论。


图片


它可以帮助您生成真正有意义和深思熟虑的回复,您稍后会来编辑或拒绝,甚至只是批准并将它们公开发布到网站上。最好的部分是,它会在你的声音中。


这个应用程序也很棒,你通过批准、拒绝或编辑评论给它的反馈越多,人工智能就会学习,每一条评论都会变得越来越好。


9. 客户服务聊天机器人


客户服务聊天机器人是一种人工智能驱动的聊天机器人,旨在帮助客户完成诸如回答常见问题、提供有关产品或服务的信息以及解决问题等任务。


要使用聊天 GPT 构建用于客户服务的聊天机器人,您需要在客户查询和响应的数据集上训练模型。这将使聊天机器人能够了解客户可能遇到的问题和问题类型,并提供适当的响应。


用于客户服务的聊天机器人的潜在目标受众是希望改善客户服务的各种行业的企业。这可能包括电子商务企业、基于服务的企业和 B2B 公司。


10. 财务规划应用程序


下一个想法是创建一个应用程序,旨在帮助完成预算、储蓄和投资等任务。


例如,您可以通过输入 Graham Stefan 的整个视频和课程目录来训练 ChatGPT 模型,了解他的投资和预算哲学。


然后,人工智能将能够监控您的支出模式、银行账户、投资,并为您提供反馈,说明您应该做什么才能实现您的财务目标。


如果您需要此类应用程序的灵感,可以查看 fylehq.com。


图片


它是一种费用管理软件,它使用 AI 从费用收据中准确提取和编码数据。


11. 健康与保健应用程序


这个 ChatGPT 商业理念就是创建一个健康应用程序,旨在帮助完成跟踪健康指标、提供健康和保健提示以及将用户与医疗保健提供者联系起来等任务。


cass.ai 这种应用程序的一个很好的例子是。


图片


要为这种类型的聊天机器人训练模型,您需要在患者病历、医院记录和体检结果等大数据源上训练它。


12. 娱乐应用


下一个想法是创建一个应用程序,提供有关电影、电视节目、音乐和其他娱乐形式的推荐和信息。


若要为此类应用训练模型,需要大型娱乐信息数据集,例如电影和电视节目评论、音乐推荐以及有关即将发生的事件的信息。


例如,数据集可以包含诸如“年度最佳电影是什么”、“公告牌百强单曲榜上排名前 10 的歌曲是什么”和“百老汇必看的节目是什么”等短语。


13. 数字副本


现在,把最好的想法留给爱,有一个名为 character.ai 的网站,你可以尝试训练一个语言模型来代表世界上的任何人,无论是像埃隆马斯克这样的活着的人,甚至是像本杰明富兰克林这样的死者。


你可以用我实际与之交互的AI模型将它们变成3D增强现实体验。


托尼·罗宾斯(Tony Robbins)一直在幕后工作,这是他毕生工作的巨作。


图片


一种人工智能语言模型,它知道他所知道的一切,并且拥有技术和能力,可以扩展到他可以以托尼罗宾斯私人教练的形式为世界各地的每个人提供他的教学。


14. 转录应用程序


有些网站像 scribie.com,从字面上支付从视频或注册中转录内容的费用,因此需求就在那里。


事实上,如果你浏览Scibie的网站,你会看到许多服务。例如转录会议、演讲、论文等。


图片


因此,您可以考虑创建一个应用程序并使用开放 AI 中的 API 在您的应用程序中使用 whisper 服务。Whisper是OpenAI的另一项服务。


这里涉及更多的工作,但我认为了解如何将 API 集成到应用程序中以及如何构建前端应用程序确实值得了解,因为这个 API 可以提供很多东西。


15.AI 旅游应用


有一些 AI 旅行应用程序可以帮助您完成预订航班和酒店、查找旅行优惠以及为目的地的活动和景点提供建议等任务。


其中一个应用程序是 Emma.ai 它会自动在您的日历中直接为您提供旅行时间和旅程信息,用于客户约会和会议。


图片


该应用程序可让您使用缓冲时间功能和约会预订功能来管理日历。


他们在大型旅行信息数据集上训练模型,例如航班和酒店预订、旅行优惠以及目的地活动和景点的建议。


16. 新闻和信息应用程序


要创建基于 ChatGPT 的新闻和信息应用程序,您需要收集大量新闻文章和其他相关信息的数据集,这些信息涉及各种主题,例如来自信誉良好的新闻来源的文章,以及来自其他来源的信息,例如政府网站、行业报告和专家分析,具体取决于您的利基。


您还需要在此数据集上训练 ChatGPT 模型,对其进行微调,以理解和响应与新闻和信息相关的自然语言查询和命令。


您的模型应该能够理解和回答诸如“印度冠状病毒的最新消息”、“今天股市发生了什么”和“中东当前局势如何”等问题。


为此,我建议从一个较小的利基市场开始,而不是创建一个试图涵盖所有内容的新闻应用程序。


17.AI 流媒体服务


如果你最近一直在关注直播,你可能听说过一个名为Neurosama的AI直播主播,她本质上是一个玩Minecraft和OSU等视频游戏的动漫女孩。


图片


同时,她与聊天互动,破解笑话,并从她使用机器学习技术处理的信息中学习。这令人印象深刻。


现在,事情是这样的。如果你想创建一个类似的AI主播,这可能是非常具有挑战性的。我不知道有任何可供公众使用的特定工具。如果存在这样的工具,我相信每个人都会使用它。


但是,如果你能找到一种方法来编程这样的东西或创建一个类似的工具,你就有可能将这些人工智能流媒体作为一项业务出售。


您可以提供完整的编码来创建整个AI流,在您的服务器上运行它,并以月费将其出售给公司。


对于没有编码知识并希望进入流媒体领域的公司来说,这可能非常诱人。或者,您可以运行 10 到 20 个 AI 流光,或者找出其他方法来制作 AI 内容。


18.AI 网红营销


接下来,您甚至可以创建AI Instagram影响者。事实上,Pacsun背后的面孔是一个甚至不存在的数字影响者,但她在Instagram上拥有近3万粉丝。


图片


想象一下,对于希望聘请模特或影响者来推广其页面的公司来说,使用人工智能影响者是一个双赢的局面。他们不会衰老,不会努力工作,也不会被起诉。


如果你能创造一些这样的人工智能影响者,并将它们推销给公司,你就可以赚到一个沉重的包。


19. 文案服务


销售文案服务是最明显的 Chat GPT 商业理念,但同时也是最具竞争力的理念,因为每个人都可以做到这一点。但它可能仍然有效,因为它适用于全球许多自由职业者,正如您稍后看到的那样。


以下是您可以出售的一些文案服务:


● 博客写作 


● 产品描述 


● 网站文案 


● 销售文案 


● 校对


所有这些服务都是高利润的,人们愿意为你付费。


人类的大脑有局限性,但现在你没有任何局限性,你有一个真正的竞争优势,这要归功于这种人工智能。


您可以在自由职业者网站上一次提供多种不同的服务。


图片


因此,让我们以Fiverr上的博客写作为例。


因此,如您所见,这些人已经通过销售这项服务赚了很多钱。


但同样,关键是要找到一个特定的利基市场,因为它几乎与所有这些聊天 GPT 商业理念一样。


20. 编码服务


正如您刚刚看到的那样,ChatGPT 为应用程序或谷歌浏览器扩展程序创建代码,以便它可以创建代码并更正它。


编码的需求量非常高,企业和企业家愿意向任何能够提供这项服务并做好这项工作的人支付巨额资金。


现在你有了另一个不公平的优势,你有ChatGPT为你做所有的工作。


这只


是关于利用自由职业者网站并在那里宣传自己作为专业编码员。


给它一些时间将一些客户聚集在一起,使用聊天 GPT 制作一些高质量的工作,然后从那里进行扩展和构建。


GPT商业理念-最后的话


这些是您可以尝试的最佳聊天GPT商业理念。


人工智能驱动的企业正在激增,企业家利用这项技术的机会是巨大的。


ChatGPT 有潜力彻底改变现有行业,创造新行业,并为消费者提供高度个性化的服务。


如果您想创业,请考虑将 ChatGPT 纳入您的业务模式并以创新的方式应用它。随着技术的不断发展,人工智能在不同行业和市场的潜在应用也将如此。


ONE MORE THING


咪豆AI圈(Meedo)针对当前人工智能领域行业入门成本较高、碎片化信息严重、资源链接不足等痛点问题,致力于打造人工智能领域的全资源、深内容、广链接三位一体的在线科研社区平台,提供AI导航网、AI版知乎,AI知识树和AI圈子等服务,欢迎AI未来儿一起来探索(http://www.meedo.top/)


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

有些东西你要是早点懂,也不至于走那么多冤枉路

最近在阅读一些书籍和学习一些技术的时候,有一些心得,再和过去自己在不同阶段的一些经历进行反思,总结一些个人的想法和看法,也希望自己在很多年后再回头来看的时候,不像今天回头去看很多年前一样感到有一丝悔意和不甘。 在大学二年级下学期之前,我是处于一种“无头苍蝇”的...
继续阅读 »

最近在阅读一些书籍和学习一些技术的时候,有一些心得,再和过去自己在不同阶段的一些经历进行反思,总结一些个人的想法和看法,也希望自己在很多年后再回头来看的时候,不像今天回头去看很多年前一样感到有一丝悔意和不甘。


在大学二年级下学期之前,我是处于一种“无头苍蝇”的状态,并不是说自己自甘堕落,破罐子破摔,不是的,相反,我是渴求改变自己的,想学东西的,但是,对于我这种普通本科的学生,虽然学校图书馆有你看不完的书,但是,你总得知道你该看什么书,什么样的对你有帮助,知识的海洋是没有边际的,但是一个人的精力是有限的,当把过多的时间花费在一些对自己没有成长,但是自己却在自嗨的事情上的时候是很可怕的。


就拿读书这件事来说,那时候因为我是“无头苍蝇”,所以就“病急乱投医”,总觉得要去看一点书来充实自己,于是我就看了一些历史,三皇五帝,春秋战国,秦汉三国南北朝都去看了,后面又去看了王阳明,曾国藩,后面是越看越觉得不行,我是学软件工程的,怎么研究起历史来了,然后又去寻求内心的安慰,又去听电台,我记得那时候我最爱听的就是《饮鸩不止渴》和《十点读书》,然后里面的一些鸡汤就把自己灌饱了,就觉得“未来可期”,其实后来才发现是“未来可欺”,不过其实对于像我这样的人,整个社会太多太多,他们想改变自己,想未来有一份不错的事业,他们有梦想,有激情,但是,他们却不知道怎么做,他们的父母不懂这个专业,他们的身边也没啥人懂这个专业,他们从小没见过大的世界,所以导致他们“浪费”了很多时光。


当然,读书是一件十分好的事,听电台也很好,但是,在生命的每一个阶段,你自己应该把时间主要花费在什么上面,这是一个很有智慧的问题,读历史,读人物传记能够让我们有更多的思考空间,有宽广的胸怀,让人遇事从容淡定,因为随着时间的推移,枭雄豪杰不过是一堆白骨,但是眼下我们依旧在生活着,是避不开生计,避不开七情六欲的,所以在头顶星辰大海的同时也要看好眼前的路,在自己没有方向的时候,就看别人怎么做的,如果没有目标参考系,那就做自己专业该做的,然后极力去了解相关资讯和技术,这样即使种不出南瓜,但是也绝对不会少了豆子,最主要的是,这个过程它会去锻炼人,提高自己的思考能力。


不过在自己有了目标以后,怎么去实现这个目标更是一个问题,如果没有条理,没有规划,没有结合社会情况,那么,努力就会显得无力苍白,学生时代的时候,说白了,对于普通本科的学校,大家都差不多,自然没有人有更高的论调,所以都沉迷于表面的浮华,而不去关注其核心原理,所以在学习技术的时候,也会显得毛毛躁躁,不去认真专研,而是沉迷于“多”,“炫”上面,其实着是不对的,不论是计算机专业还是其他专业,知识框架是很多的,但是大多数人少了一种刨根问底的精神,当然,刨根问底也并不一定是一种值得称赞的精神,但是在学生时代如果有刨根问底的精神,那么事必成,因为学生时代正是种子发芽的时候,但是进入社会以后,刨根问底未必就是好事了,它和我们的职业规划,个人性格等有很大的关联,比如你觉得你不可能成为技术专家的条件,你的优势也不在哪里,你还去刨根问底技术,那么这不是明智的选择,但是如果暂时还在一个过渡期,很多东西还不确定,那么是有必要去刨根问底的,而且刨得越深越好。


个人定位无论在那个阶段都是很重要的,它是一种判断力,更是一种智慧,你让韩信去管钱粮他肯定不如萧何,你让萧何去管兵马打仗,他肯定不如韩信,所以,没有那个方向是做好的,没有那个岗位是最值得深耕的,这完全要根据自己的情况来,如果你是一个技术控,对技术有无尽的热爱,加上脑子转得也快,那么,从事技术发展肯定是一个很不错的选择,但是如果对技术不敏感,别人一遍就会,而我十遍都不会,那么可能我真的不适合,但是我对商业有洞察力,对客户有一套打法,那么就没必要去技术哪里死磕,虽然你能花11个小时弄懂,但是硬生生花11小时去弄,因为人最宝贵的就是时间,这样就显得不理智,因为其他它从某种意义上已经证明你确实不适合干这个。


主要就说这些了,虽然都是能懂的道理,但是还是要时刻记录下来,给自己看的同时也希望对别人有启示,往往很多时候是环境和认知限制了我们,所以这时候思考和学习就是最好的解法,在资源不充足的情况下,一定会踩很多坑,滩很多浑水,所以寻求资源是一件十分有必要的事情,多向优秀的人请教,学习,多了解这个社会的运转,不要被关在狭小的信息茧房里。


今天的分享就到这里,感谢你的观看,我们下期见。


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

你是先就业还是先择业?

就业的”就“不是让你将就   是不是大家常常听到家里人唠叨的一句话:“有工作就行了啊,别那么挑剔,你都这么大了,还指望着家里养着你啊” 。还是老师说:“我们要建立优先就业再择业的就业观,不能一直不去就业呀”什么的叭叭叭。   其实某方面来说他们并没有说错,我们...
继续阅读 »

就业的”就“不是让你将就


  是不是大家常常听到家里人唠叨的一句话:“有工作就行了啊,别那么挑剔,你都这么大了,还指望着家里养着你啊” 。还是老师说:“我们要建立优先就业再择业的就业观,不能一直不去就业呀”什么的叭叭叭。


  其实某方面来说他们并没有说错,我们已经成年了,需要独立自主。在漂亮国,到了18岁好像都要分家了吧。不过我们在中国,中国的国情肯定和漂亮国不一样。除此之外中国家庭从小的哭穷式教育,估计让许多孩子都想自己经济独立吧。这个现象导致了大家认为有工作就行了,我管他什么工作呢。


  但是从自身职业发展来讲,这是对自己极其不负责的表现,往往许多人的第一份工作就决定了人生轨迹,不论是以后决定发展的城市,还是以后工作的方向,其实已经早已埋下种子。你说你可以换工作啊,可以跳槽啊,现实往往会打醒你,你以为你没了应届生身份,凭着你那不到一年的工作经验,人家企业看中你什么。所以我们要就业要为自己,同时也要为自己的未来负责,我们要慎之又慎。所以我们要就业不过的选择自己合适的就业不能盲目就业,家长的思想大部分过时了,停留在了上了大学就有好工作的时代。我们只能参照而不能按部就班,对于老师,大部分是为了提高学校就业率完成指标而已,不必要太大理会,当然和你关系好的老师除外,但是相反如果和你关系好他一定会不会让你草草就业的。


u=1343747016,2016950934&fm=253&fmt=auto&app=138&f=JPEG.webp


择业的”择“也别太择



钱多事少离家近,位高权重责任轻。睡觉睡到自然醒,数钱数到手抽筋。



  说完就业再谈谈择业,相信上面这句话大家都听过,这简直就是梦中情职,所以择业在我看来无非四种:离家近的、工资高的、自己感兴趣的、清闲的。这四种涵盖了大部分职业了吧。所以我们怎么择业,选择一个适合自己的职位对于未来发展是事半功倍的。


  大家选择职业的时候不知道是从哪方面来选择的,首先离家近,相信很多女生都是考虑这个优先吧,感觉男生就是喜欢仗剑走天涯那种🤣。然后考虑清闲的,想想你二十几岁的年龄你还要工作四五十年可能,选个清闲一点的职业不过分吧,最好就是考一个公务员事业编了,实在不行就去央企国企了,当然这种工作大家都想去,虽然工资不高但是福利好啊。再者就是兴趣了,把自己的兴趣培养成自己的职业也是可以的,大学就是很好的时间,选那种课比较少的专业,这里不得不再次吐槽大学课程的无用性。然后自己选一个自己喜欢的职业,比如摄影、博主什么的。不过当喜欢的事变成职业很多人也就不喜欢了,比如电竞职业选手他们天天十几个小时训练打游戏,他们下班还会想打游戏嘛🤣。就是坚持很重要。再再者,有的人说自己天生无感对什么都没兴趣,那么恭喜你和我一样🤣,就是什么的不是很感兴趣,也不讨厌,那么我建议搞钱,选个高薪的职业搞到足够的钱就退休了,当初就是看程序员薪资高入行了,对钱总感兴趣了吧。总而言之择业择业选择自己合适的再就业。


  鱼和熊掌不可兼得。选择离家近的就得忍受小镇的慢节奏,没有快速的地铁,没有好玩的游乐场,有的只是街坊邻居的互相寒暄,没有夜晚的灯红酒绿,只有晚上八九点就安静的大街。选择清闲的公务员,那么就要懂的人情世故,还有每个月几千块钱的工资。选择自己感兴趣的,那么就得忍受孤独,经得起自我怀疑要有坚定的勇气。高工资的不用多说了吧,996,007时间就是金钱,加班是常态,通宵也是偶尔。所以没有哪份职业好坏,选择自己合适的,加油奋斗吧!


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

使用 Kotlin 委托,拆分比较复杂的 ViewModel

需求背景 在实际的开发场景中,一个页面的数据,可能是由多个业务的数据来组成的。 使用 MVVM 架构进行实现,在 ViewModel 中存放和处理多个业务的数据,通知 View 层刷新 UI。 传统实现 比如上面的例子,页面由3 个模块数据构成。 我们可...
继续阅读 »

需求背景




  1. 在实际的开发场景中,一个页面的数据,可能是由多个业务的数据来组成的。

  2. 使用 MVVM 架构进行实现,在 ViewModel 中存放和处理多个业务的数据,通知 View 层刷新 UI。


传统实现


比如上面的例子,页面由3 个模块数据构成。


我们可以创建一个 ViewModel ,以及 3个 LiveData 来驱动刷新对应的 UI 。


    class HomeViewModel() : ViewModel() {

private val _newsViewState = MutableLiveData<String>()
val newsViewState: LiveData<String>
get() = _newsViewState

private val _weatherState = MutableLiveData<String>()
val weatherState: LiveData<String>
get() = _weatherState

private val _imageOfTheDayState = MutableLiveData<String>()
val imageOfTheDayState: LiveData<String>
get() = _imageOfTheDayState

fun getNews(){}
fun getWeather(){}
fun getImage(){}

}

这样的实现会有个缺点,就是随着业务的迭代,页面的逻辑变得复杂,这里的 ViewModel 类代码会变复杂,变得臃肿。


这个时候,就可能需要考虑进行拆分 ViewModel


一种实现方法,就是直接简单地拆分为3个 ViewModel,每个 ViewModel 处理对应的业务。但是这样会带来其他问题,就是在 View 层使用的时候,要判断当前是什么业务,然后再去获取对应的ViewModel,使用起来会比较麻烦。


优化实现


目标:



  • 将 ViewModel 拆分成多个子 ViewModel,每个子 ViewModel 只关注处理自身的业务逻辑

  • 尽量考虑代码的可维护性、可扩展性


Kotlin 委托



  • 委托(Delegate)是 Kotlin 的一种语言特性,用于更加优雅地实现代理模式

  • 本质上就是使用了 by 语法后,编译器会帮忙生成相关代码。

  • 类委托: 一个类的方法不在该类中定义,而是直接委托给另一个对象来处理。

  • 基础类和被委托类都实现同一个接口,编译时生成的字节码中,继承自 Base 接口的方法都会委托给BaseImpl 处理。


// 基础接口
interface Base {
fun print()
}

// 基础对象
class BaseImpl(val x: Int) : Base {
override fun print() { print(x) }
}

// 被委托类
class Derived(b: Base) : Base by b

fun main(args: Array<String>) {
val b = BaseImpl(10)
Derived(b).print() // 最终调用了 Base#print()
}

具体实现


定义子 ViewModel 的接口,以及对应的实现类


    interface NewsViewModel {
companion object {
fun create(): NewsViewModel = NewsViewModelImpl()
}

val newsViewState: LiveData<String>

fun getNews()
}

interface WeatherViewModel {
companion object {
fun create(): WeatherViewModel = WeatherViewModelImpl()
}

val weatherState: LiveData<String>

fun getWeather()
}

interface ImageOfTheDayStateViewModel {
companion object {
fun create(): ImageOfTheDayStateViewModel = ImageOfTheDayStateImpl()
}

val imageState: LiveData<String>

fun getImage()
}

class NewsViewModelImpl : NewsViewModel, ViewModel() {
override val newsViewState = MutableLiveData<String>()

override fun getNews() {
newsViewState.postValue("测试")
}
}

class WeatherViewModelImpl : WeatherViewModel, ViewModel() {
override val weatherState = MutableLiveData<String>()

override fun getWeather() {
weatherState.postValue("测试")
}
}

class ImageOfTheDayStateImpl : ImageOfTheDayStateViewModel, ViewModel() {
override val imageState = MutableLiveData<String>()

override fun getImage() {
imageState.postValue("测试")
}
}


  • 把一个大模块,划分成若干个小的业务模块,由对应的 ViewModel 来进行处理,彼此之间尽量保持独立。

  • 定义接口类,提供需要对外暴漏的字段和方法

  • 定义接口实现类,内部负责实现 ViewModel 的业务细节,修改对应字段值,实现相应方法。

  • 这种实现方式,就不需要像上面的例子一样,每次都要多声明一个带划线的私有变量。并且可以对外隐藏更多 ViewModel 的实现细节,封装性更好


组合 ViewModel


image.png


    interface HomeViewModel : NewsViewModel, WeatherViewModel, ImageOfTheDayStateViewModel {
companion object {
fun create(activity: FragmentActivity): HomeViewModel {
return ViewModelProviders.of(activity, object : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return if (modelClass == HomeViewModelImpl::class.java) {
@Suppress("UNCHECKED_CAST")

val newsViewModel = NewsViewModel.create()
val weatherViewModel = WeatherViewModel.create()
val imageOfTheDayStateImpl = ImageOfTheDayStateViewModel.create()

HomeViewModelImpl(
newsViewModel,
weatherViewModel,
imageOfTheDayStateImpl
) as T
} else {
modelClass.newInstance()
}

}
}).get(HomeViewModelImpl::class.java)
}
}
}

class HomeViewModelImpl(
private val newsViewModel: NewsViewModel,
private val weatherViewModel: WeatherViewModel,
private val imageOfTheDayState: ImageOfTheDayStateViewModel
) : ViewModel(),
HomeViewModel,
NewsViewModel by newsViewModel,
WeatherViewModel by weatherViewModel,
ImageOfTheDayStateViewModel by imageOfTheDayState {

val subViewModels = listOf(newsViewModel, weatherViewModel, imageOfTheDayState)

override fun onCleared() {
subViewModels.filterIsInstance(BaseViewModel::class.java)
.forEach { it.onCleared() }
super.onCleared()
}
}


  • 定义接口类 HomeViewModel,继承了多个子 ViewModel 的接口

  • 定义实现类 HomeViewModelImpl,组合多个子 ViewModel,并通过 Kotlin 类委托的形式,把对应的接口交给相应的实现类来处理

  • 通过这种方式,可以把对应模块的业务逻辑,拆分到对应的子 ViewModel 中进行处理

  • 如果后续需要新增一个新业务数据,只需新增相应的子模块对应的 ViewModel,而无需修改其他子模块对应的 ViewModel。

  • 自定义 ViewModelFactory,提供 create 的静态方法,用于外部获取和创建 HomeViewModel。


使用方式



  • 对于 View 层来说,只需要获取 HomeViewModel 就行了。

  • 调用暴露的方法,最后会委托给对应子 ViewModel 实现类进行处理。


        val viewModel = HomeViewModel.create(this)

viewModel.getNews()
viewModel.getWeather()
viewModel.getImage()

viewModel.newsViewState.observe(this) {

}
viewModel.weatherState.observe(this) {

}
viewModel.imageState.observe(this) {

}

扩展



  • 上面的例子,HomeViewModel 下面,可以由若干个子 ViewMdeol 构成。

  • 随着业务拓展,NewsViewModel、WeatherViewModel、ImageOfTheDayStateViewModel,也可能是分别由若干个子 ViewModel 构成。那也可以参照上面的方式,进行实现,最后会形成一棵”ViewModel 树“,各个节点的 ViewModel 负责处理对应的业务逻辑。


image.png


总结


这里只是提供一种拆分 ViewModel 的思路,在项目中进行应用的话,可以根据需要进行改造。


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

【自定义 View】一个易用且好看的阴影控件

前言 在 Android 的界面绘制中,控件的阴影是我们经常会处理的一种界面元素,尤其会出现在按钮 Button 这种需要吸引用户关注点的控件上。Android 原生提供了控件的 Z 轴属性即 elevetion 供阴影效果,但是这个效果嘛,但凡是有一点想法的...
继续阅读 »

前言


Android 的界面绘制中,控件的阴影是我们经常会处理的一种界面元素,尤其会出现在按钮 Button 这种需要吸引用户关注点的控件上。Android 原生提供了控件的 Z 轴属性即 elevetion 供阴影效果,但是这个效果嘛,但凡是有一点想法的 UI 都不会满意的,比如我司的,就坚决不接受。


常见的问题比如不支持特定的阴影形状或大小,或不允许完全自定义阴影的颜色或透明度,切图是一种方式,但是自定义 View 绘制的效果会更好,毕竟切图会实实在在的造成 apk 包体积的增大,而且屏幕适配也会是一个潜藏的问题隐患。



结合我的经验,简单封装了一下,分享我目前使用的 ShadowView


使用


圆角矩形阴影




  1. 普通阴影


    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.randalldev.shadowview.ShadowView
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:layout_constraintBottom_toBottomOf="@id/btn_target"
    app:layout_constraintEnd_toEndOf="@id/btn_target"
    app:layout_constraintStart_toStartOf="@id/btn_target"
    app:layout_constraintTop_toTopOf="@id/btn_target"
    app:shadowBottomHeight="16dp"
    app:shadowCardColor="#FF7043"
    app:shadowColor="#FFEE58"
    app:shadowLeftHeight="16dp"
    app:shadowRadius="16dp"
    app:shadowRightHeight="16dp"
    app:shadowRound="8dp"
    app:shadowTopHeight="16dp" />

    <Button
    android:id="@+id/btn_target"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@android:color/transparent"
    android:paddingStart="40dp"
    android:paddingEnd="40dp"
    android:paddingTop="20dp"
    android:paddingBottom="20dp"
    android:text="target button"
    android:textColor="@color/purple_700"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>

    image.png


    抛开配色不谈,这个效果还可以吧




  2. 普通阴影 + 偏移


    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout
    ···
    app:shadowLeftHeight="16dp"
    app:shadowOffsetX="8dp"
    app:shadowOffsetY="4dp"
    app:shadowRadius="16dp"
    ···
    </androidx.constraintlayout.widget.ConstraintLayout>

    image.png




圆形阴影


圆形阴影也可以认为是一种特殊的圆角矩形阴影,可以继续沿用圆角矩形的方式,或者添加 shadowShape 属性。


如果要使用圆角矩形的方式,需要事先确定目标控件的尺寸,这可能会遇到屏幕适配问题,所以我这里就直接演示使用 shadowShape 属性的方式




  1. 普通阴影


    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout
    ···
    app:shadowCardColor="#FF7043"
    app:shadowColor="#FFEE58"
    app:shadowRadius="16dp"
    app:shadowShape="1" />

    <Button
    android:id="@+id/btn_target"
    android:layout_width="wrap_content"
    android:layout_height="0dp"
    android:background="@android:color/transparent"
    android:padding="20dp"
    android:text="target button"
    android:textColor="@color/purple_700"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintDimensionRatio="1:1"
    ···

    </androidx.constraintlayout.widget.ConstraintLayout>

    image.png


    很简单吧,相比圆角矩形的配置,多了一个 shadowShape 但是少了很多尺寸的设置,只需要设置一个 shadowRaduis 即可。


    需要注意的是,我这里使用了 ConstrainLayoutratio 属性设置为 1:1 来实现一个正方形的目标控件,因为在绘制圆形时,是以控件的中心作为圆心来绘制的,如果不是正方形就可能出现问题了。




  2. 普通阴影 + 偏移


    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout
    ···
    app:shadowCardColor="#FF7043"
    app:shadowColor="#FFEE58"
    app:shadowRadius="16dp"
    app:shadowOffsetX="4dp"
    app:shadowOffsetY="4dp"
    app:shadowShape="1" />

    ···
    </androidx.constraintlayout.widget.ConstraintLayout>

    image.png





这个使用起来还是比较方便的吧,只需要目标控件设置 padding 留出足够的空间绘制阴影效果即可。


并且不需要再写 drawable 文件设置控件的背景了。


当然也不是没有缺陷,目前还是只能兼容圆角矩形和圆形。异形的暂时没用到,可能也不会去做支持。



实现


什么是阴影


首先,阴影是什么?


在真实世界中,阴影是物体遮挡住光源的光路出现的现象;在 Android View 体系中则是 Z 轴高度,Z 轴高度越高,阴影范围越大,颜色越深。


但是仅仅通过 elevetion 属性设置 Z 轴高度实现的阴影视效上往往只能说满足有无的问题,毕竟国内谁按照 MD 风格去设计界面啊。


image.png


那么,阴影是什么?


当我们自定义 View 去绘制阴影的时候,其实也可以是一圈从边缘向四周放射式扩散的渐变色层,从而造成一种视觉的阴影效果。


那偏移又是什么?


偏移其实就是表达光源的位置,偏移为 0,即光源在正中心光线直射,阴影效果是从边缘均匀的向四周逐渐变淡。


X 偏移为正,则光源在中心偏右,Y 偏移为正,则光源在中心偏下。 若为负数则相反。视觉上则会出现某一或两轴方向上的阴影区域偏少。


上代码


初始化


这段很简单,就是读取 attrs 属性,设置硬件加速


init {
initView(context, attrs)
//设置软件渲染类型,跟绘制阴影相关,后边会说
setLayerType(View.LAYER_TYPE_SOFTWARE, null)
}

绘制阴影


这里创建了一个画笔 Paint 的实例,画笔的颜色是目标控件的背景色;绘制模式设置的是 FILL 表示填充模式,还有 STROKE 描边模式,FILL_AND_STROKE 描边加填充模式;AntiAlias 设置为 true 标识开启抗锯齿。


这里就是使用 PaintsetShadowLayer() 方法创建阴影效果,其中:



  • radius:阴影半径,值越大阴影越模糊,值为0时阴影消失。

  • dx:阴影在水平方向的偏移量,正值表示向右偏移,负值表示向左偏移。

  • dy:阴影在垂直方向的偏移量,正值表示向下偏移,负值表示向上偏移。

  • shadowColor:阴影颜色。


Canvas 可以理解为画布,基于 shadowShape 属性在画布上对应的绘制圆角矩形和圆形两种不同形状。



  • drawRoundRect() 用于在 Canvas 上绘制一个圆角矩形。该方法需要传递四个参数,分别是矩形左上角的 X 坐标,矩形左上角的 Y 坐标,矩形右下角的 X 坐标和矩形右下角的 Y 坐标。此外还需要提供两个额外参数,分别是圆角的 X 半径和 Y 半径。

  • canvas.drawCircle() 用于在 Canvas 上绘制一个圆形。该方法需要传递三个参数,分别是圆心的 X 坐标,圆心的 Y 坐标以及圆的半径。


创建一个 RectF,也就是一个矩形对象,表示一个浮点数精度的矩形。在绘制操作,比如指定绘制区域、裁剪画布等经常会用到。其构造函数包含4个浮点型成员变量:left、top、right、bottom,分别表示矩形左边界、上边界、右边界和下边界的坐标值。


override fun dispatchDraw(canvas: Canvas) {
// 配置画笔
val shadowPaint = Paint()
shadowPaint.color = shadowCardColor
shadowPaint.style = Paint.Style.FILL
shadowPaint.isAntiAlias = true
val left = shadowLeftHeight.toFloat()
val top = shadowTopHeight.toFloat()
val right = (width - shadowRightHeight).toFloat()
val bottom = (height - shadowBottomHeight).toFloat()
// 配置阴影的范围,偏移,颜色
shadowPaint.setShadowLayer(shadowRadius.toFloat(), shadowOffsetX.toFloat(), shadowOffsetY.toFloat(), shadowColor)
if (shadowShape == 0) {
// 如果绘制圆角矩形的阴影,用 drawRoundRect
val rectF = RectF(left, top, right, bottom)
canvas.drawRoundRect(rectF, shadowRound.toFloat(), shadowRound.toFloat(), shadowPaint)
} else {
// 如果绘制圆形的阴影,用 drawCircle
val radius = measuredHeight.toFloat() / 2 - shadowRadius
canvas.drawCircle(measuredHeight.toFloat() / 2, measuredHeight.toFloat() / 2, radius, shadowPaint)
}
shadowPaint.utilReset()
canvas.save()
}

总结


Android 界面绘制中,阴影是常见的 UI 元素之一,而 Android 原生提供的 elevation 属性虽然可以实现阴影效果,但往往不能满足 UI 设计的要求。因此,自定义 View 绘制阴影的方式更为灵活和实用。本文介绍了 ShadowView,它可以方便地绘制圆角矩形和圆形的阴影,且支持颜色、透明度和阴影形状的自定义。此外,本文还提供了使用 ShadowView 绘制阴影的示例代码,可供读者参考和使用。通过使用 ShadowView,可以更加方便地实现复杂、美观的阴影效果,提高 Android 应用的用户体验。


参考文章


Android进阶:快速实现自定义阴影效果


ShadowView


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

写给 Android 开发者的系统基础知识科普

与我以往的风格不同,本文为科普类文章,因此不会涉及到太过高深难懂的知识。但这些内容可能 Android 应用层开发者甚至部分 framework 层开发者都不了解,因此仍旧高能预警。 另外广东这两天好冷啊,大家注意保暖~ 虚拟机与运行时 对象的概念 假设 g...
继续阅读 »

与我以往的风格不同,本文为科普类文章,因此不会涉及到太过高深难懂的知识。但这些内容可能 Android 应用层开发者甚至部分 framework 层开发者都不了解,因此仍旧高能预警。


另外广东这两天好冷啊,大家注意保暖~



虚拟机与运行时


对象的概念


假设 getObjectAddress(Object) 是一个获取对象内存地址的方法。


第一题:


考虑如下代码:


public static void main(String[] args) {
Object o = new Object();
long address1 = getObjectAddress(o);
// .......
long address2 = getObjectAddress(o);
}

main 方法中,创建了一个 Object 对象,随后两次调用 getObjectAddress 获取该对象的地址。两次获取到的对象地址是否有可能不同?换句话说,对象的地址是否有可能变更?










答:有可能。JVM 中存在 GC 即“垃圾回收”机制,会回收不再使用的对象以腾出内存空间。GC 可能会移动对象。


第二题:


考虑如下代码:


private static long allocate() {
Object o = new Object();
return getObjectAddress(o);
}

public static void main(String[] args) {
long address1 = allocate();
// ......
long address2 = allocate();
}

allocate() 创建了一个 Object 对象,然后获取它的对象地址。
main 方法中调用两次 allocate(),这两个对象的内存地址是否有可能相同?










答:有可能。在 allocate() 方法中创建的对象在该方法返回后便失去所有引用成为“不再需要的对象”,如果两次方法调用之间,第一次方法调用中产生的临时对象被上文中提到的 GC 机制回收,对应的内存空间就变得“空闲”,可以被其他对象占用。


第三题:


哎呀,既然上面说同一个对象的内存地址可能不相同,两个不同对象也有可能有相同的内存地址,而java 里的 == 又是判断对象的内存地址,那么


Object o = new Object();
if (o != o)

还有


Object o1 = new Object();
Object o2 = new Object();
if (o1 == o2)

这里的两个 if 不是都有可能成立?










答:不可能。== 操作符比较的确实是对象地址没错,但是这里其实还隐含了两个条件:



  1. 这个操作符比较的是 “那一刻” 两个对象的地址。

  2. 比较的两个对象都位于同一个进程内。


上述提到的两种情况都不满足“同一时间”这一条件,因此这两条 if 永远不会成立。


类与方法


第四题:


假设 Framework 是 Android Framework 里的一个类,App 是某个 Android App 的一个类:


public class Framework {
public static int api() {
return 0;
}
}

public class App {
public static void main(String[] args) {
Framework.api();
}
}

编译 App,然后将 Frameworkapi 方法的返回值类型从 int 改为 long,编译 Framework 但不重新编译 App,App 是否可以正常调用 Framework 的 api 方法?










答:不能。Java 类内存储的被调用方法的信息里包含返回值类型,如果返回值类型不对在运行时就找不到对应方法。将方法改为成员变量然后修改该变量的类型也同理。


第五题:


考虑如下代码:


class Parent {
public void call() {
privateMethod();
}
private void privateMethod() {
System.out.println("Parent method called");
}
}

class Child extends Parent {
private void privateMethod() {
System.out.println("Child method called");
}
}

new Child().call();

Child 里的 privateMethod 是否重写了 Parent 里的?call 中调用的 privateMethod() 会调用到 Parent 里的还是 Child 里的?










答:不构成方法重写,还是会调用到 Parent 里的 privateMethod。private 方法是 direct 方法,direct 方法无法被重写。


操作系统基础


多进程与虚拟内存


假设有进程 A 和进程 B。


第六题:


进程 A 里的对象 a 和进程 B 里的对象 b 拥有相同的内存地址,它们是同一个对象吗?










答:当然不是,上面才说过“对象相等”这个概念在同一个进程里才有意义,不认真听课思考是会被打屁屁的~


第七题:


进程 A 内有一个对象 a 并将这个对象的内存地址传递给了 B,B 是否可以直接访问(读取、写入等操作)这个对象?










答:不能,大概率会触发段错误,小概率会修改到自己内存空间里某个冤种对象的数据,无论如何都不会影响到进程 A。作为在用户空间运行的进程,它们拿到的所谓内存地址全部都是虚拟地址,进程访问这些地址的时候会先经过一个转换过程转化为物理地址再操作。如果转换出错(人家根本不认识你给的这个地址,或者对应内存的权限不让你执行对应操作),就会触发段错误。


第八题:


还是我们可爱的进程 A 和 B,但是这次 B 是 A 的子进程,即 A 调用 fork 产生了 B 这个新的进程:


void a() {
int* p = malloc(sizeof(int));
*p = 1;
if (fork() > 0) {
// 进程 A 也即父进程
// 巴拉巴拉巴拉一堆操作
} else {
// 进程 B 也即子进程
*p = 2;
}
}

(fork 是 Posix 内创建进程的 API,调用完成后如果仍然在父进程则返回子进程的 pid 永远大于 0,在子进程则返回 0)


(还是理解不了就把 A 想象为 Zygote 进程,B 想象为任意 App 进程)


这一段代码分配了一段内存,调用 fork 产生了一个子进程,然后在子进程里将预先分配好的那段内存里的值更改为 2。
问:进程 B 做出的更改是否对进程 A 可见?










答:不可见,进程 A 看见的那一段内存的值依然是 1。Linux 内核有一个叫做“写时复制”(Copy On Write)的技术,在进程 B 尝试写入这一段内存的时候会偷偷把真实的内存给复制一份,最后写入的是这份拷贝里的值,而进程 A 看见的还是原来的值。


跨进程大数据传递


已知进程 A 和进程 B,进程 A 暴露出一个 AIDL 接口,现在进程 B 要从 A 获取 10M 的数据(远远超出 binder 数据大小限制),且禁止传递文件路径,只允许调用这个 AIDL 接口一次,请问如何实现?










答:可以传递文件描述符(File Descriptor)。别以为这个玩意只能表示文件!举个例子,作为应用层开发者我们可以使用共享内存的方法,这样编写 AIDL 实现类把数据传递出去:


@Override public SharedMemory getData() throws RemoteException {
int size = 10 * 1024 * 1024;
try {
SharedMemory sharedMemory = SharedMemory.create("shared memory", size);
ByteBuffer buffer = sharedMemory.mapReadWrite();
for (int i = 0;i < 10;i++) {
// 模拟产生一堆数据
buffer.put(i * 1024 * 1024, (byte) 114);
buffer.put(i * 1024 * 1024 + 1, (byte) 51);
buffer.put(i * 1024 * 1024 + 2, (byte) 4);
buffer.put(i * 1024 * 1024 + 3, (byte) 191);
buffer.put(i * 1024 * 1024 + 4, (byte) 98);
buffer.put(i * 1024 * 1024 + 5, (byte) 108);
buffer.put(i * 1024 * 1024 + 6, (byte) 93);
}
SharedMemory.unmap(buffer);
sharedMemory.setProtect(OsConstants.PROT_READ);
return sharedMemory;
} catch (ErrnoException e) {
throw new RemoteException("remote create shared memory failed: " + e.getMessage());
}
}

然后在进程 B 里这样拿:


IRemoteService service = IRemoteService.Stub.asInterface(binder);
try {
SharedMemory sharedMemory = service.getData();
ByteBuffer buffer = sharedMemory.mapReadOnly();

// 模拟处理数据
int[] temp = new int[10];
for (int i = 0;i < 10;i++) {
for (int j = 0;j < 10;j++) {
temp[j] = buffer.get(i * 1024 * 1024 + j);
}
Log.e(TAG, "Large buffer[" + i + "]=" + Arrays.toString(temp));
}
SharedMemory.unmap(buffer);
sharedMemory.close();
} catch (Exception e) {
throw new RuntimeException(e);
}

这里使用的 SharedMemory 从 Android 8.1 开始可用,在 8.1 之前的系统里也有一个叫做 MemoryFile 的 API 可以用。
打开 SharedMemory 里的源码,你会发现其实它内部就是创建了一块 ashmem (匿名共享内存),然后将对应的文件描述符传递给 binder。内核会负责将一个可用的文件描述符传递给目标进程。
你可以将它理解为可以跨进程传递的 File Stream(只要能通过权限检查),合理利用这个小玩意有奇效哦 :)


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

一个简单的自定义输入框

Hello啊各位老铁,今天还是一篇关于自定义View相关的,带来一个大众的,常见的一个输入框,很多的场合下都能遇到,比如验证码,密码框等等,配置了很多常见的属性,可以满足不同场合下的需求,矩形框,圆角框,下划线等等均可满足,长度设置,光标选择,背景选择,均可控...
继续阅读 »

Hello啊各位老铁,今天还是一篇关于自定义View相关的,带来一个大众的,常见的一个输入框,很多的场合下都能遇到,比如验证码,密码框等等,配置了很多常见的属性,可以满足不同场合下的需求,矩形框,圆角框,下划线等等均可满足,长度设置,光标选择,背景选择,均可控制,废话不多数,我们直接进入正题。


今天的内容大致如下:


1、效果及代码具体调用。


2、具体实现过程。


3、开源地址。


4、总结及注意事项。


一、效果及代码具体调用。


效果展示


边框黑圆圈展示



边框文字展示



纯色背景文字展示



纯色背景黑圆圈展示



纯色背景星号展示



下划线文字展示



下划线黑圆圈展示



纯色背景横向光标展示



能实现的效果还有很多,大家可以根据属性来动态配置即可。


关于使用方式,大家可以下载源码,直接复制即可,毕竟只有一个类,如果懒得下载源码,使用我给大家准备的远程Maven也是可以的,也是非常的方便,远程Maven具体使用如下。


Maven具体调用


1、在你的根项目下的build.gradle文件下,引入maven。


allprojects {
repositories {
maven { url "https://gitee.com/AbnerAndroid/almighty/raw/master" }
}
}

2、在你需要使用的Module中build.gradle文件下,引入依赖。


dependencies {
implementation 'com.vip:edit:1.0.3'
}

代码使用


   <com.vip.edit.InputBoxView
android:id="@+id/ib_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginTop="30dp"
android:layout_marginRight="10dp"
app:input_background="#f5f5f5"
app:input_canvas_type="rect"
app:input_length="6"
app:input_text_size="16sp"
app:input_text_type="round"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />


属性介绍











































































































属性类型概述
input_canvas_typeenum绘制类型,目前有三种,线(line),矩形(rect),圆角(round),圆角需要结合属性input_radius使用
input_canvas_styleenum绘制画笔类型,空心还是实心,空心(stroke),实心(fill),实心和空心(fill_and_stroke)
input_backgroundcolor输入框的背景
input_select_backgroundcolor输入框的选择背景
input_radiusdimension输入框圆角度数
input_line_heightdimension输入框下划线的高度
input_lengthinteger输入框的长度
input_spacingdimension输入框的间距
input_text_colorcolor输入框的内容颜色
input_text_sizedimension输入框的文字大小
input_text_typeenum输入框的文字类型,普通文字(text),星号(asterisk),黑圆圈(round)
input_is_cursorboolean输入框是否有光标,默认展示光标
input_cursor_directionboolean输入框光标方向
input_cursor_widthdimension输入框光标宽度
input_cursor_colorcolor输入框光标颜色
input_cursor_spacingcolor输入框光标间距
input_cursor_is_twinkleboolean输入框的光标是否闪烁
input_is_android_keyboardboolean输入框是否弹起原生的软件盘,默认谈起,可以调用自定义的键盘
input_cursor_margin_bottomdimension横向的光标距离底部的距离

方法介绍










































方法参数概述
clearContent无参清空内容
setContentString设置内容
hideInputMethod无参隐藏软键盘,使用系统软键盘时
showKeyBoard回调函数需要弹起自己的软键盘时可以调用
inputChangeContent回调函数获取连续的输入结果
inputEndResult回调函数获取最终的输入内容,当等于你设置的length时进行回调

二、具体实现过程。


实现的过程也是非常的简单,大致可以分为五步走,1、绘制方格或下划线,2、绘制内容,3、绘制光标,4、实现光标闪动,5、软键盘控制,来,我们一步一步的来实现。


1、绘制方格或下划线


绘制方格或下划线,如下草图所示,需要根据传递的数量来进行绘制,首先要计算出每一格的宽度,也就是屏幕的宽度-格子之间的边距/格子的数量。



  //每个输入框的宽度=屏幕的宽-左右的边距-输入框直接的间距/输入框的个数
mRectWidth = (width - mSpacing * (mLength - 1)) / mLength

得到了每一格的宽度之后,就可以根据数量来进行动态的绘制了,无非就是遍历,根据属性input_canvas_type来绘制不同的效果,mSelectBackGroundColor变量为属性input_select_background设置的值,用来标记选择的输入框颜色,如下图所示:



如果,你想要改变选中的格子边框颜色,就可以进行设置颜色值,同样的需要搭配画笔的绘制样式,如下代码所示:


 /**
* AUTHOR:AbnerMing
* INTRODUCE:绘制输入框
*/
private fun canvasInputBox(canvas: Canvas?) {
mPaint!!.apply {
color = mBackGroundColor//设置背景颜色
strokeCap = Paint.Cap.ROUND//圆角线
}


for (a in 0 until mLength) {

val textLength = text.toString().length//当前输入的长度

if (mSelectBackGroundColor != 0) {
var paintStyle = Paint.Style.STROKE
when (mInputCanvasStyle) {
0 -> {
paintStyle = Paint.Style.STROKE
}
1 -> {
paintStyle = Paint.Style.FILL
}
2 -> {
paintStyle = Paint.Style.FILL_AND_STROKE
}
}
if (a == textLength) {
mPaint!!.apply {
style = paintStyle
color = mSelectBackGroundColor//设置选中背景颜色
}
} else {
mPaint!!.apply {
style = paintStyle
color = mBackGroundColor//设置背景颜色
}

}
}

val left = a * mRectWidth + a * mSpacing
val top = 0f
val right = (a + 1) * mRectWidth + a * mSpacing
val bottom = height.toFloat()

when (mInputCanvasType) {
0 -> {
//绘制下划线
canvas?.drawRoundRect(
left,
bottom - mLineHeight,
right,
bottom,
mRadius,
mRadius,
mPaint!!
)
}
1 -> {
//绘制矩形
canvas?.drawRect(left, top, right, bottom, mPaint!!)
}
2 -> {
//绘制圆角矩形
canvas?.drawRoundRect(left, top, right, bottom, mRadius, mRadius, mPaint!!)
}
}
}
}

绘制格子,最重要的就是计算每个格子的位置,其实只需要考虑X的坐标即可,Y可以直接充满View的高度。


2、绘制内容


绘制输入的内容,和绘制格子一样,重要的就是计算位置,有了格子的位置之后,计算内容就比较的简单了,只需要获取格子的中间坐标即可,计算如下:首先,拿到每个格子的右边X坐标点,再减去格子宽度的一半,就得到的中间的X坐标,但是,文字的绘制,还需要减去文字宽度的一半,这个一定要注意,否则,文字就是从中间点往右进行绘制的,就偏移了中间点。


文字的X轴计算如下:


  val textX = ((a + 1) * mRectWidth) + a * mSpacing - mRectWidth / 2 - w / 2

同理,Y的计算方式类似,全部代码如下,有一点需要注意下,就是星号,星号和文字以及圆圈还是有不一样的地方,那就比较小,那么就需要特殊的处理一下,都是基础的代码,没什么好说的。


/**
* AUTHOR:AbnerMing
* INTRODUCE:绘制内容
*/
private fun drawText(canvas: Canvas?) {
mPaint!!.apply {
style = Paint.Style.FILL
color = mTextColor//设置内容颜色
textSize = mTextSize
}

if (!TextUtils.isEmpty(text)) {
for (a in text!!.indices) {
val content = text!![a].toString()
var endContent = content

if (mTextType == 1) {
endContent = "*"
} else if (mTextType == 2) {
endContent = "●"
}

val rect = Rect()
mPaint!!.getTextBounds(endContent, 0, content.length, rect)
val w = mPaint!!.measureText(endContent)//获取文字的宽
//获取文字的X坐标
val textX = ((a + 1) * mRectWidth) + a * mSpacing - mRectWidth / 2 - w / 2
val h = rect.height()
//获取文字的Y坐标
var textY = (height + h) / 2.0f
//针对星号做特殊处理
if (mTextType == 1) {
textY += mTextSize / 3
}

canvas?.drawText(endContent, textX, textY, mPaint!!)
}
}
}

3、绘制光标


绘制光标就比较简单了,无非就是纵向还是横向,也是根据设置的属性来控制的,纵向计算出X坐标即可,横向就计算出Y的坐标即可。需要注意的是,距离左右或者上下的间距控制,代码如下:


 /**
* AUTHOR:AbnerMing
* INTRODUCE:绘制光标
*/
private fun drawCursor(canvas: Canvas?) {
mCursorPaint!!.apply {
strokeWidth = mCursorWidth
isAntiAlias = true
}
//需要根据当前输入的位置,计算光标的绘制位置
val len = text?.length
if (len!! < mLength) {
if (mCursorDirection) {
//纵向光标
val rectWidth = ((len + 1) * mRectWidth) + len * mSpacing - mRectWidth / 2
canvas?.drawLine(
rectWidth,
mCursorSpacing,
rectWidth,
height - mCursorSpacing,
mCursorPaint!!
)
} else {
val endX = ((len + 1) * mRectWidth) + len * mSpacing
val startX = endX - mRectWidth
//横向光标
canvas?.drawLine(
startX + mCursorSpacing,
height.toFloat() - mCursorMarginBottom,//减去距离底部的边距
endX - mCursorSpacing,
height.toFloat() - mCursorMarginBottom,
mCursorPaint!!
)
}
}

}

4、实现光标闪动


光标闪动,使用了一个属性动画,设置无限循环,然后控制画笔的颜色即可。


    private val cursorAnim: ValueAnimator = ValueAnimator.ofInt(0, 2).apply {
duration = 1000
repeatCount = ValueAnimator.INFINITE//无线循环
repeatMode = ValueAnimator.RESTART//正序
}

在onAttachedToWindow方法里做启动动画操作:mCursorTwinkle为是否需要光标,需要再启动,是通过属性input_cursor_is_twinkle来控制的。


if (mCursorTwinkle) {
//不在运行,开启动画
if (!cursorAnim.isRunning) {
cursorAnim.start()
}
cursorAnim.addUpdateListener {
val v = it.animatedValue as Int
if (v == 0) {
mCursorPaint?.color = Color.TRANSPARENT
} else {
mCursorPaint?.color = mCursorColor
}
postInvalidate()
}
}

同样的,当onDetachedFromWindow方法时,就需要结束。


if (mCursorTwinkle) {
if (cursorAnim.isRunning || cursorAnim.isStarted) {
cursorAnim.end()
}
cursorAnim.removeAllUpdateListeners()
}

判断在文字的输入时进行闪烁,这个是很重要的,也就是闪烁的位置,一定是当前的输入位置,未输入就是第一格闪烁,依次类推,输入完成,就结束闪烁。


override fun onTextChanged(
text: CharSequence?,
start: Int,
lengthBefore: Int,
lengthAfter: Int
) {
super.onTextChanged(text, start, lengthBefore, lengthAfter)
if (!mIsAttachedToWindows) return

//输入框的光标是否闪烁
if (mCursorTwinkle) {
if ((text?.length ?: 0) >= mLength) {
cursorAnim.takeIf { it.isStarted || it.isRunning }?.end()
} else if (!cursorAnim.isRunning) {
cursorAnim.start()
}
}

val endContent = text.toString()
if (endContent.length == mLength) {
//一样的话,进行回调
mEndContentResult?.invoke(endContent)
}

mChangeContent?.invoke(endContent)

}

5、软键盘控制


软件盘控制,有两种方式,一种是弹出系统的软键盘,一种是弹出自定义的软键盘,这个控制也是由传递的属性input_is_android_keyboard来操作的,默认为true,弹出系统的,否则就弹出自定义的,针对自定义的弹出,需要暴露出实现的方法,由使用者进行实现。


 /**
* AUTHOR:AbnerMing
* INTRODUCE:弹起软件盘
*/
private fun showKeyboard() {
if (mIsAndroidKeyBoard) {
isFocusable = true
isFocusableInTouchMode = true
requestFocus()
val im =
context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
?: return
im.showSoftInput(this, InputMethodManager.SHOW_FORCED)
} else {
//启用自定义的软键盘
if (mKeyBoard != null) {
mKeyBoard?.invoke()
}
}
}

mKeyBoard为弹出自定义软键盘回调函数,代码如下:


    /**
* AUTHOR:AbnerMing
* INTRODUCE:显示自己定义的软件盘
*/
private var mKeyBoard: (() -> Unit?)? = null
fun showKeyBoard(block: () -> Unit) {
mKeyBoard = block
}

隐藏软键盘操作,可以在页面隐藏时进行触发,目前在自定义View中onDetachedFromWindow方法里进行了调用,当然,你可以自己选择性调用。


    /**
* AUTHOR:AbnerMing
* INTRODUCE:隐藏软件盘
*/
fun hideInputMethod() {
if (mIsAndroidKeyBoard) {
val imm: InputMethodManager =
context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(windowToken, 0) //强制隐藏
}
}

三、开源地址。


目前项目已经开源,需要的朋友可以查看:github.com/AbnerMing88…


四、总结及注意事项。


1、触摸输入框,默认是弹出系统自带的软键盘的,如果想使用自定义的,设置属性input_is_android_keyboard为false即可,并调用showKeyBoard回调函数,在showKeyBoard方法里进行书写弹起自定义软键盘即可。


2、如果绘制类型属性input_canvas_type为round,也就是圆角时,需要结合input_radius这个属性,来实现圆角的角度大小。


3、光标的方向属性input_cursor_direction是一个boolean类型的值,默认是纵向的,false是为横向的。


4、当输入框的选择背景input_select_background不为空时,画笔属性input_canvas_style才会生效。


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

职场上有什么谎言?

努力干活就能赚多点钱 职场中最大的谎言可能是“工作越忙就能赚到更多的钱”。虽然在某些情况下这是真实的,但很多时候这只是一种误解。长时间超负荷工作可能会导致身体和心理健康问题,甚至影响到家庭生活和人际关系。此外,很多公司并不愿意在工作强度过高的员工身上支付额外的...
继续阅读 »

努力干活就能赚多点钱


职场中最大的谎言可能是“工作越忙就能赚到更多的钱”。虽然在某些情况下这是真实的,但很多时候这只是一种误解。长时间超负荷工作可能会导致身体和心理健康问题,甚至影响到家庭生活和人际关系。此外,很多公司并不愿意在工作强度过高的员工身上支付额外的报酬,而是更倾向于平衡员工的工作与生活,提高员工的幸福感和满意度。因此,新进职场人士应该认识到,在职场中坚持适度的工作量、良好的工作习惯和优秀的职业素养才是取得成功的重要因素。


我都是为你好


“我都是为你好”可能是一种常见的谎言,在不同情境下被使用。在某些情况下,这可能真诚地表达出对他人的关心和照顾,但在其他情况下,这也可能成为掩盖自己私人动机或者行为错误的借口。因此,在职场和日常生活中,我们需要学会审视这句话所蕴含的背后意图,并判断其是否真实可信。同时,我们也应该秉持着开放、坦诚、尊重和理解的态度,与他人进行良好的沟通和相处,以建立健康、和谐的人际关系。


他做得比你好,向他好好学习


“他做得比你好,向他好好学习”是一句非常有益的建议,可以让人们从成功的经验中汲取营养,不断提高自己的能力和水平。在职场中,人们面对不同的工作任务和挑战,而且每个人的工作方式、思维模式和经验都不同,因此,我们应该善于借鉴他人的优点和长处,吸取别人的经验和教训,不断完善自己的职业素养和技能。然而,这并不意味着要完全依赖和模仿别人,而是应该在合适的时机,根据自身实际情况和需要,加以改进和创新,开拓自己的专业视野和发展空间。


在职场中,有些人可能会通过拍马屁、拉关系等不正当手段来获取自己的利益或者提高自己的地位。然而,这种做法可能会导致负面后果和损失,例如破坏工作团队的合作氛围、损害自己的职业形象和信誉等。因此,我们应该始终保持清醒和冷静的头脑,不受拍马屁等诱惑,专注于自己的工作和职责,努力提高自己的专业水平和职业素养。同时,我们也应该与他人建立良好的人际关系,以合理、公正、透明的方式展示自己的才华和成果,赢得别人的尊重和信任,并在适当的时刻借助他人的力量来实现共同的目标。


公司不怎么赚钱,理解一下,行情好了加工资


如果公司在过去设定了一些目标和承诺,但无法兑现或者没有达到预期的结果,那么这就是一种失信行为。画饼充当推销手段,可能会对员工、客户和利益相关方造成误导和不良影响,并破坏公司的商誉和形象。因此,公司应该根据市场实际情况和自身能力水平,制定合理、可行的计划和策略,避免过于浮夸和虚幻的承诺,注重落实和执行,加强与员工、客户和社会各方的沟通和互动,建立坦诚、透明的企业文化和价值观念。同时,员工也应该保持客观、谨慎、理性的态度,不盲目追求高回报或者虚假宣传,始终以个人职业道德和职责为先,为公司和自

作者:象骑士
来源:juejin.cn/post/7213636024102469693
己的未来发展负责任。

收起阅读 »

写给迷茫的 1-3 年前端人的思考

写给迷茫的 1-3 年前端人的思考 前言 今年由于大环境差,加之大家都会遇到的职场困惑期,让很多人觉得很无助,不知道要向何处去努力。 我有两三位朋友正面临类似的困境,向我寻求意见。虽然我不是什么大厂的架构师,也没有多牛,但也有一些自己的思考和实践,希望能帮助到...
继续阅读 »

写给迷茫的 1-3 年前端人的思考


前言


今年由于大环境差,加之大家都会遇到的职场困惑期,让很多人觉得很无助,不知道要向何处去努力。


我有两三位朋友正面临类似的困境,向我寻求意见。虽然我不是什么大厂的架构师,也没有多牛,但也有一些自己的思考和实践,希望能帮助到他们,也希望与大家一起探讨这个话题。


思考


学好英语


原因



  • 获得更广泛的就业机会

  • 拥有更高的技术天花板


获得更广泛的就业机会

1680090655136.png


如果你了解过外国的就业机会,就会发现除了母语为英语的国家,像德国、西班牙、瑞典、荷兰这样的英语为第二语言的国家,只要英语过关,找工作方面也是完全没问题的。


当你学好英语时,就不仅局限于国内这个环境,而是在全世界范围内找工作。此外,国外远程工作机会也不少,在英语不错的情况下,完全可以拿着美元在国内享福。


image.png


更高的技术天花板

我们不得不承认英语在技术领域处于绝对统治地位。如果你的英语水平不够,那么想学习更加新和深入的技术都将面临巨大的困难。我相信每个人都能明白这点。


如何学好英语?


学习英语的资料汗牛充栋,方法也不胜枚举,但知易行难,我个人也在努力中,所以就交给大家自行探索。


找到并深入特定领域


除了日常的页面和组件开发,与前端相关的技术还有许多特定领域,例如:AI、低代码、图形学、数据大屏、serverless、构建、错误监控、行为分析、Web3、编辑器、微前端、服务端、云原生、游戏、组件库、ab测试等等。我个人建议至少找到一个领域深入研究。


原因



  • 形成职业亮点

  • 增加职业壁垒


形成职业亮点

在面试和简历筛选中,面试官最看重的是是否具有亮点。没有亮点的人基本上很难达到高级别,顶多只能达到中级别。而亮点可以从许多方面切入,其中之一就是在某个特定领域有自己的见解和产出,能够为人所不能。


增加职业壁垒

如果你拥有某个领域的知识,那么你不仅是一个切图仔,即使老板想要裁员,他也会掂量是否能够找到更合适的人才。


如何找到自己的领域?


工作

大多数人的专业领域都是从工作中找到的。每个人在职业生涯中都会遇到一些技术难点,此时你可以分析业界各种解决方案的优缺点和实现原理,并思考是否有更好的解决方案或者在社区方案的基础上能否进行更优化、更便捷化,或者更加适合国内市场的特殊需求。


许多人不愿意在工作之外的时间思考与工作相关的问题,但对于自己有利的事情,我认为可以适当花时间深入研究或有意识地增加这部分时间的分配。


主动寻找

如果日常工作就是纯纯的切图仔,那我们就需要主动出击,找到一个自己感兴趣的点并进行深入研究。


例如,如果你想找一份 Web3 相关的工作,那么你现在就应该开始学习 Web3 知识,并将你的学习过程记录下来并发布到各个文章平台,扩大你的影响力。


面向工作和薪资学习


如果你现在的工作只是切图,下班后也没有什么事做,不知道该学习什么,那么这个问题就很简单了。


你可以看一下 BOSS 直聘上前端工程师的工作描述中都需要什么技能以及哪些行业薪资高,什么技能、行业薪资高,你就学什么就对了。当然最好的方法是主动面试尝试毒打。


例如 1:如果你想找远程工作,发现国外许多人使用 tailwindcss,工作描述中也常常要求掌握这个技能,那么你应该毫不犹豫地开始学习 tailwindcss。


例如 2:如果你发现 Web3 相关工作的薪资比普通前端工作高很多,而你又想要高薪,那么你应该立即开始学习 Web3 知识。


关注前端趋势


了解当前前端技术的现状,关注前端技术未来的发展趋势。如果其他几项知易行难,我建议先从这项内容开始,每天花费时间不需要超过 20 分钟即可完成。


如何做到了解目前前端发展的趋势呢?



  • GitHub Trending:每天早晨到公司先打开 GitHub Trending 看看社区中哪些项目正在受到关注(10 分钟)

  • MDH 前端周刊:umijs 作者开设的前端周刊,关注最新的前端发展

  • 两个油管博主

    • t3dotgg:拥有 11 万 YouTube 频道订阅者,基本上当社区一项技术开始火时就会跟进讲解

    • Web Dev Simplified:拥有 120 万 YouTube 频道订阅者,除了讲解前端基础知识外,还会跟进前端的最新发展




当你看到某个东西变得流行起来,你需要思考它为什么会火,之前的技术方案有哪些问题,新的方案怎么解决之前的问题的等等,这样的思考会带领你走向更深的前端知识区。


做开源项目


原因



  • 满足自我实现需求

  • 提高技术水平上限

  • 更好地找工作

  • 更多可能性


满足自我实现需求

当你的开源项目受到关注和讨论时,相当于你被需要、被认可,这是一种很好的正向反馈,会使你身心愉悦。


提高技术水平上限

当你开发出一个新的东西时,代表着你对这个事物有着自己的思考和见解,人与人之间的差异在于思想上。如果社区内有新的反馈,会促使你不断完善和学习相关知识,无形中提升了你的个人能力。


更好地找工作

一个好的开源项目是一个亮点,面试一般都会询问相关问题,此时的主动权是掌握在你手里的。


更多可能性

如果你做过一个成功的开源项目,你就会发现有人开始找你写小册、出课程甚至出版社会找你写书。此外你还可以在文档中有自己的赞助、广告等收入,虽然不多,但是这都代表了一种不同于上班的可能性。


如何开始自己的开源项目?


做开源有两大难点,首先是如何开始一个开源项目,其次是如何让它变得受欢迎。


如何开始一个开源项目,已经在以前的文章里阐述了 juejin.cn/post/719877… ,这里不再赘述。


至于如何让项目受欢迎,所涉及的东西就比较多了,这里就先挖个坑,看是否有人感兴趣,如果有的话,可以再写一篇文章。


基础知识


前端是离端很近的岗位,它所依附的知识变化也很快,昨天还是 jQuery,今天变成了 React、Vue、Webpack、Jest,明天就是 Vite、TurboPack、vitest, SolidJS、Qwik、Astro、Svelte、Remix,后天呢?。


虽然上层的知识变化很快,但是仔细观察就会发现底层的知识却保持不变,比如编程思想、编译原理、算法等。


在这里,我推荐以下三本书作为入门:



  • 《重构——改善既有的代码设计》

  • 《程序员修炼之道》

  • 《黑客与画家》


软技能


除了编程技能外,软技能也很重要。例如这篇 文章 中列举的 8 个能力:



  • 人际交往能力

  • 结构化思维能力

  • 沟通能力

  • 写作能力

  • 自我营销能力

  • 演讲能力

  • 协同/领导能力

  • 抗压能力


如果你可以逐步提高所有这些方面,那当然最好。如果不能全面发展,其中我认为 写作能力 可以先发育,推荐以下内容:



当然关键还是多写多练,毕竟纸上得来终觉浅。


人生方向


编程行业仍然是普通人最好的出路。毕竟没有哪个专业刚毕业的大学生(没背景)就能拿到 7、8 千,甚至脉脉上刚毕业就能拿到 2、30k。


但如果你真的对技术不感兴趣或者做的很痛苦,可以思考是否要继续从事这个行业,是否有其他出路。这是留给你自己的思考题。


后记


实际上,本文已经写完并放置了几周了。随着人工智能井喷式发展、国内经济实行的下行以及各种裁员消息,让我觉得上面的这些努力似乎已没有意义,普通人的命运是否会像《人类简史》中描述的那样,成为无用阶层。


但我又想起一句话:“悲观者正确,乐观者成功”。因此,我还是决定与大家分享这篇文章。


当然,知易行难。如果你在一家 996 公司,建议你骑驴找马,背背八股文,并且在上班前花 10 分钟看一下 GitHub Trending 就行了;


如果是在 965 公司,除了看看 GitHub Trending 建议再至少学一样其他东西。


最后,无论往什么方向努力,希望最重要,希望大家都能怀揣希望,继续前行。


作者:超杰_
来源:juejin.cn/post/7215908160019824698
收起阅读 »

00后的我,大四即将毕业了

前言:   我写这篇文章的初衷主要是记录我自己作为00后面临毕业的总结,算是对大学四年的总结吧,这是第一次写文章总结,也算是00后从小学到大学的学生生涯的总结吧!主要就是写的一名从小听家长的话,做一个乖孩子;听老师的话做一个好学生从小学到大学毕业的感触吧!差不...
继续阅读 »

前言:


  我写这篇文章的初衷主要是记录我自己作为00后面临毕业的总结,算是对大学四年的总结吧,这是第一次写文章总结,也算是00后从小学到大学的学生生涯的总结吧!主要就是写的一名从小听家长的话,做一个乖孩子;听老师的话做一个好学生从小学到大学毕业的感触吧!差不多就是讲的我自己了,哈哈哈哈,我知道自己是这么想的,但我还是想知道大部分00后面临毕业是怎么样的心情🤣


road-3396764__480.jpg


00后还年轻吗?


  00后,听起来很年轻吧,我记得当年上学的时候,国际上有一个词语“千禧一代”,我记得好像当时知道这个词语还是TFBoy火起来了,记得都是未成年然后刚刚出道的时候,以前都还不清楚“千禧一代”一词代表着什么,现在我们00的作为最后一代千禧,也已经面临大学毕业了,我不知道其他地方,我这普遍还是00的同学,大学毕业意味着
进入社会,所以00后其实已经不年轻了。



“千禧一代”是指出生于20世纪时未成年,在跨入21世纪(即2000年)以后达到成年年龄的一代人,这代人的成长时期几乎同时和互联网/计算机科学的形成与高速发展时期相吻合。



大学四年


  大学之大不在大楼,在于大师,对啊大学就是该是大学云集的地方,但是作为大学过来的人,我经历到的大学绝非家长口中的大学,也绝非高中老师口中的大学,这个我不说大家懂得都懂,可能就是这种类似“潜规则”样的教育,我不知道怎么去描述,写着写着又跑题了,我不知道是故意的还是不小心的 🤣


大一


  大一学校安排还是挺多,晚上晚自习,早上晨读晨跑,似乎高三的感觉又回来了,但是那种日子还是比较充实的,因为不需要你思考自己的道路,只需要依照着安排完成感觉就很满足了,大一下学期记得是疫情爆发了,也不能返校在家上网课。在家学习效率其实挺低的,即使我每天还会准时上课但是总是觉得学不进去,后面侥幸没有挂科,相信好多同学的因为在家上网课挂科了吧,第一次报的英语四级因此还被延期了,不过也是裸考,后面侥幸过了😁


大二


  下学期疫情没有那么严峻了,开始返校上课了,我感觉隔了半个学期像是第一次新生来这学校一样,不过已经是大二的老油条了,后面也是自己每个学期选看起来更有趣的课,就像矮个子里挑高个子,我学的软件工程专业,其实所有课都是又难又枯燥的,不仅仅是老师那落后十年的ppt还有那十几年没有改版的课本,考试大家都是临时抱佛脚了,快说你也是🤣


大三


  大三在我印象中是没有什么印象的,因为除了上课还是上课,课超级多,晚饭都没时间不知不觉大三就过了大四的时候在学校的安排下去培训三个月,不知道其他学校有没有这种,交了大四一年的学费(1W),软件工程专业不知道学费贵在哪里。。


  但是从大三结束就没去过学校了,暑假在和学校合作的培训公司培训了3个月,后面直接秋招,这个我觉得是最气人的,我们选择就业的被送去深圳培训公司培训,那些考研考公的就在学校,学校里的校招根本与我们出来的无缘,不管是路途远还是学校把我们宿舍都直接撤了。大三就让把东西寄回家啊,我还以为是读的三年专科(这里不是看不起专科的意思),这不是明显给考研和考公的更多机会了,我们出来就业的只能在网上各种海投
BOSS直拒、前程堪忧、失联招聘、58不成、裂聘……


现在


  我算是运气还好,面试虽然寥寥无几,最后也是拿到两个offer,选择一个相比之下好一点的就签三方了至于班里其他人就只能等了,等到年末,刺骨的不是天气的寒冷,而是就业的寒潮,班里三分之二的人只能空手回家过年,我虽然拿到offer但也决定回家休整几个月,毕竟当时疫情还没有结束呢,毕竟签了三方
就决定明年再去了。


2023我即将毕业了


university-student-1872810__480.jpg
  希望大家能够在大学早点找到学习的意义,不管是为了找工作还是为了充实自己,看到过一个视频内容说的是年龄其实都是虚无的最重要的还是自己的经历,换句话说我们人的最终都是各种经历形成的,和你多大年龄多大岁数是没有关系的,所以说我们需要增加自己的经历,不管是好的还是不好的,都是需要去经历的,最后祝福大家前程似锦!


作者:过了三分之二年河东
来源:juejin.cn/post/7215886869200306237
收起阅读 »

提高你工作效率的10条建议

最近看到一个关于工作效率的问题,这里系统整理下自己总结的一些经验。 有一个跟工作效率有点像的词汇:生产效率。 生产效率指的是单位时间内的有效产出,想要生产效率高,要么做事的“质”和“量”更高,要么缩短所花费的时间。 工作效率和生产效率比较类似,很多都可以借鉴。...
继续阅读 »

最近看到一个关于工作效率的问题,这里系统整理下自己总结的一些经验。


有一个跟工作效率有点像的词汇:生产效率。


生产效率指的是单位时间内的有效产出,想要生产效率高,要么做事的“质”和“量”更高,要么缩短所花费的时间。


工作效率和生产效率比较类似,很多都可以借鉴。


有些工作效率高的,三年经验可以顶别人五年工作经验。


找到自己精力最旺盛的时间段


有人喜欢早自习时候睡觉,有人喜欢晨读。每个人的作息规律不同,可以在自己正常运行一段时间后,找到最佳节奏。在这个精力最旺盛的时间段,更容易进入心流,可以集中精力处理优先级比较高的事情。另外,大段时间尽量不要被大段。


掌握通用技能


掌握基础的电脑办公软件技能、沟通能力、时间管理能力、快速学习一项技能的能力等等。能够使用软件等解决日常工作中遇到的问题,提高工作效率。


如何提高解决问题的能力?


掌握工作必备的基础技能


基础知识扎实的话,就可以避免在一些低级错误上花费很多时间。如果基础不好,而工作任务又比较重,就类似于每天都在考试,但是却没有时间学习新知识,这样学习成绩也无法提升。


单位时间内不断给自己施压


一小时干别人两三个小时干的活,同样的任务,第二次、第三次做的时候就有意识地提高效率。



像训练肌肉一样的训练自己的大脑。同等时间内,从明天起,让自己思考学习双倍的工作量。注意我加粗加重的关键词,不许增加时间,一个小时还是那一个小时,时间不变,内容翻番。


一开始一定有些疲劳感,但只要不生病,那说明你的大脑就能够适应。坚持半个月,习惯它。再加倍。再坚持半个月,习惯它。


一直加倍加倍坚持到你感觉要生病了为止,把速度降下来,降低20%。把这个效率维持终身。当训练成为习惯的时候,你会越来越轻松,越来越惬意。全力以赴的思考也是一样的。


任何一个人,只要你肯,你都能这么去训练自己的工作效率。而当你的效率提升到别人的4倍,8倍。你会发现生活很惬意,不是因为压力变小了,而是因为你习惯了。


——记忆承载《韦小宝绝境》



定期复盘


每周大致回顾下自己本周做了什么,有什么需求改进的。可以自己给自己写周报。


充分利用碎片化时间


可以在上班大致想下今天要做的内容,在下班路上回顾下今天都做了些什么,哪些做得好,哪些还有待改进。


多出妙招不如减少失误


尽量少出岔子,可以避免因为失误而带来的对已有时间的占用。


做最重要的事情


领会领导意图,抓住重点。细枝末节可以在大的事情基本上确认无误的时候再做。


不会就搜


总有些问题是自己措手不及的,不会就搜,不行就换一个搜索引擎,或者换一个关键词。


适当摸鱼


该休息休息会,劳逸结合。休息时间可以整理下文档,换换思路也行,有时候现在百思不得其解的问题,出去溜达一圈回来就豁然开朗了。

作者:江湖人称向前兄
来源:juejin.cn/post/7216671329188937787

收起阅读 »

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

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



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


千人呼吁暂停AI训练


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


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



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

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

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

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


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


代表人物分析


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


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


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


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


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


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


image.png


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


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


反对原因分析


信息安全


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


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


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


人身安全


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


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


失业


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


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


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


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


利益


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


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


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


总结


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


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


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

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

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

介绍


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


自定义字体


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


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


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



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

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


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

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

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


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


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


AutoLink


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


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


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


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


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


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


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


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


对齐模式


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


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


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


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

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


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


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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

上联是:这个需求很简单

下联是:怎么实现我不管

横批是:明天上线

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

会议太多,占用时间

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

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

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

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

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

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

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

技术需求,各种丐版轮子

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

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

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

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

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

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

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

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

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

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

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

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

总结

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

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

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

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

一、序


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

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

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

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


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

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

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

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


二、 需求描述


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

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

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


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


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



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

  • 支持单选/多选;

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

  • 支持自定义筛选条件;

  • 支持自定义目录排序;

  • 支持“原图”选项;

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

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


效果如图:


easy_album_cn.jpg


三、API设计


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

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

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


3.1 全局设置


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

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


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

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

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

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

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


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

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

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


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

出个思考题:

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

改如何定义Comparator?


3.2 启动相册


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


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

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


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

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

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

// ...其他参数..

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

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

有两种思路:



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

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


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

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

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

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

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

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

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


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

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

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

四、媒体文件加载


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


4.1 MediaStore API


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


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

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


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


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

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


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

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

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


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

以下是比较常见的字段:


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

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

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

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

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


4.2 数据包装


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


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

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

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

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

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

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

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

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

// ......
}

4.2.1 数据共享


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

同理,mime也可以复用。


截取部分查询的代码:


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

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

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

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


4.2.2 处理无效数据


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

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

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


也有比较折中的做法:



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

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


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

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


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

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


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

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

4.3 数据加载


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

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


4.3.1 缓存MediaData


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

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

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

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

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


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

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


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

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


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

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

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


4.3.2 组装结果


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

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

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


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


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

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

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

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

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

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

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

MediaFilter的定义如下:


public interface MediaFilter {
boolean accept(MediaData media);

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

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

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

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

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


4.3.3 加载流程


流程图如下:

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



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

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

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


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

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

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

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

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

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

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


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

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


五、相册列表


5.1 媒体缩略图


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

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

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


5.2 列表布局


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

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


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

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

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

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

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

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

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

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

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

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

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

效果如下图:



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


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

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

效果如下图:



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

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

例如:

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

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

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


六、后序


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

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

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


项目已发布到Github和Maven Central:


Githun地址:
github.com/BillyWei01/…


下载方式:


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

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

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

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

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


一、我的背景


1. 大学之路


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


2. 初入职场


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

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

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


3. 不破不立


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


4. 渐入佳境


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


4. 愈感迷茫


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

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


二、回顾 2022


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


1. 身份的转变


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


2. 重拾旧代码


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


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


image.png

image.png



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



3. 开启新篇章


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


三、展望 2023


1. 不忘初心


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


2. 重新开始


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


结语


相聚掘金,与君共勉。


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

怎么去选择一个公司?

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

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


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



  • 企业文化和价值观

  • 行业势头

  • 工资待遇

  • 公司规模

  • 人才水平


企业文化和价值观


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


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


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


行业势头


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



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

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

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


工资待遇


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


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


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


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


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


公司规模


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


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


人才水平


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


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




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

手册》课程。

收起阅读 »

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

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

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


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


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


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


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


一、三种人格状态


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


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


三种人格状态


1.1 儿童自我状态


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


适应性儿童自我状态


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


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


自由型儿童自我状态


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


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


1.2 父母自我状态


控制型父母自我状态


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


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


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


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


营养型父母自我状态


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


1.3 成人自我状态


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


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


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


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


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


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


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


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


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


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


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


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


二、人生坐标


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


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




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




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




人生坐标


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


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


2.1 我好,你不好


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


表现为:



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

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

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


2.2 我不好,你好


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


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


表现为:



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

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

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


2.3 我不好,你不好


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


2.4 我好,你好


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


表现为:



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

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

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


三、心理游戏


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


3.1 受害者游戏


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


我真不幸


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


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


可怜弱小的我(PLOM)


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


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


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


不论做什么都要爱我


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


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


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


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


3.2 施虐者游戏


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


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


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


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


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


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


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


猜猜我在想什么


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


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


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


四、活得真实


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


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


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


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


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


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


4.2 成为一个高情商的人


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


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


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


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


共勉!




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



推介阅读:



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



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

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

收起阅读 »

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

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

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


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


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


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


1. 福托尔(Fotor)


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


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


主要特点:



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

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

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

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

  • 6种作品可供选择。


Fotor AI 绘画生成器


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


AI绘画生成器达勒2


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


主要特点:



  • 高度逼真的图像。

  • 创建插图。

  • 设计产品。

  • 可定制的多层图像。

  • 编辑和修饰功能。

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


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


火锅AI绘画生成器


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


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


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


主要特点:



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

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

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


4. 夜间咖啡厅(NightCafe)


爱画生成器夜咖啡厅


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


主要特点:



  • 信用赚取系统。

  • 视频生成工具。

  • 有用的社交功能。

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

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


5. 深度人工智能(DeepAI)


深度AI绘画生成器


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


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


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


主要特点:



  • 始终免费使用。

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

  • 开源软件。

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

  • 使用卡通 GAN 创建卡通


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


深梦生成器


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


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


主要特点:



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

  • 不同的绘画风格。

  • 图像的分类。

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


7. 克雷永(Craiyon)


克雷永爱绘画生成器


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



  • 易于使用。

  • 无需注册或注册。

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

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


8. 星空人工智能(StarryAI)


星空AI绘画生成器


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


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


主要特点:



  • 自动图像生成器。

  • 无需用户输入。

  • 免费的 NFT 生成器。

  • 文本到图像


结论


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


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





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

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

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

一、设计思路


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



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

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


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



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

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

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

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


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


动态图演示:


登录认证


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



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



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


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

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


二、登录与注销


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


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

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



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

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

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

  4. Token 注入到请求上下文

  5. 等等其它工作……


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


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


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

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


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



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

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


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


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


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

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

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

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


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


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

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


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

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

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

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



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

三、会话查询


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

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

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

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

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

四、Token 查询


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

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

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

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

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

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


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

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


新建 LoginAuthController,复制以下代码


package com.pj.cases.use;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

}

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


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




参考资料



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

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

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

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


1.gif


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


实现方法


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


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


html 结构


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

css 样式


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

值得注意的地方有几点;



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

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

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


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


Js 实现


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



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

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

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


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


初始化数据


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

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

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


鼠标按下时


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

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

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

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


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


鼠标移动时


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

 data.distanceX = events.clientX - data.clientX

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

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

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


鼠标释放时


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

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

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

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

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


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


高度自适应


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


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

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

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


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

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

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


可以在线预览一下效果。


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

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

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

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



  • 配置手机代理:


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


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



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

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

  • 勾选显示高级选项

  • 代理 -> 手动

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

  • 服务器端口 -> 8888

  • 保存


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



  • 设置 -> 无线局域网

  • 点击当前连接的 WiFi

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

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

  • 端口 -> 8888

  • 存储


核心功能


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



  1. Charles 设置



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

  • 勾选 Support HTTP/2

  • 勾选 Enable transparent HTTP proxying

  • OK




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




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




二、  过滤网络请求




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




  2. 在 Charles 的菜单栏选择




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



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


三、  分析 HTTPS 



  1. Mac 安装证书:


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


image.png


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


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




  1. Charles 设置请求允许 SSL proxying




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




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



  1. 移动端安装证书


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


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


Android tips


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


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


IOS tips


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


charles 安装&破解:


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


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



  • Proxy -> SSL Proxy Settings -> Add

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

  • Port:443

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


四、  模拟弱网




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




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




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



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


image.png



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




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

简析无感知刷新Token

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

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


Token认证的原理


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


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


什么是无感知刷新Token


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


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



  • 如何判断Token是否过期?

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

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


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


实现步骤


步骤一:获取Access Token和Refresh Token


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


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


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

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


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


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

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


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

步骤三:拦截401 Unauthorized响应


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


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


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

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


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

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

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

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


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


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


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


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

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


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

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


步骤五:设置定时刷新Token


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


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

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

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


安全性考虑


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


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



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

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

收起阅读 »

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

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


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


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


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


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


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


官网长这样:


1.png



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


2.png


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


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


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


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


3.png


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


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


我决定上点强度。



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



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



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


4.gif


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


5.gif


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


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



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



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


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


6.gif


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


7.gif


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


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


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


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



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



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



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


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


8.png


运行出来的打印结果为:


9.png


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



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


细思极恐。


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


https://www.cursor.so/

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

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

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

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


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



操作步骤:



1.打开Github项目




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

  2. 分支选择drive





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


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


3.运行stable-diffusion-webui


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


4.更换model


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


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




6.生成成果


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


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

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

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

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


图片


背景


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


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


图片


方案思路


1) 方案选型


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


图片



  • 传统离线包


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



  • SSR


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



  • 公司内相关工作


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


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


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


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


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


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



离线包SSRVasSonicCSR
资源加载
图片


图片

图片
数据拉取

图片

图片

图片
JS执行



WebView启动优化


图片

首屏FCP

图片

图片

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




2)方案架构


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


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


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


图片


图1 上屏流程


图片


图2 方案架构


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


图片


离线包加速


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


1)加载流程


图片


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



2)fallback机制


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


图片



3. 离线包生命周期


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


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


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


图片


预热提速


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


1)耗时分析


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


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



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

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


图片


图片


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


图片



2)渲染链路预热提速




  • 预热流程




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


图片



2)WebView预创建


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





  • 数据预拉取




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


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


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


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


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


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


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





  • JS预执行




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


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


图片


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


图片


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


图片


图片


进一步优化



1)离线包安全


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


2)稳定性


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


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


图片


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



3)内存泄露


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


图片


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


4)解决副作用


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


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


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


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


图片


图片


总结


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


图片


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



图片


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

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

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

前言


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


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


业务背景


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


我就简答举个栗子哈💬



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



思路


摇头版


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

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


image-20230327234216250


优雅版


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


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

客户端代码


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

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


image-20230327235125257


小总结


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


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


结语


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


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

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

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

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


image.png

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


线程池


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



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

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

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

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


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



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

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

  • Executors.newSingleThreadExecutor:创建单个线程数的线程池

  • Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池

  • Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池

  • Executors.newWorkStealingPool:创建一个抢占式执行的线程池

  • ThreadPoolExecutor:最原始的创建方式,以上六种方式的内部也是通过这个创建的线程池


虽然我们提倡使用线程池,但是有这么多的创建方式,我们如果不在项目当中做一下管理的话,那么各种各样的线程池都有可能被使用到,由于每种创建方式对于线程的管理方式都不一样,如果不合理创建的话,很可能会出现问题,所以我们需要有一个统一创建线程池的地方


统一管理线程


image.png

首先我们先创建一个线程池,使用Executors.newCachedThreadPool()去创建一个
ExecutorService,至于为什么选择newCachedThreadPool(),我们看下它的源码


image.png

从上面一大段英文注释中我们能知道,这是一个可缓存的线程池,并且corePoolSize为0说明这个线程池没有始终存活的线程,如果线程池中没有可用线程,会重新创建新线程,而线程池中如果有可用线程,那么这个线程会被再利用,一个线程如果60秒内没有被使用,那么将会从队列中移除并销毁,所以个人感觉对于并发要求不是特别高的移动端,从性能角度来讲使用这样的一个线程池是比较合适的,当然具体设计方案以业务性质来决定,现在我们可以将项目当中的线程放在我们的线程池里面运行了,再增加一个执行线程的函数


image.png

通过这个函数就可以有效的避免项目当中随意创建线程的现象发生,让项目当中的线程可以井然有序的运行,但是这还没完事,我们知道Runnable在任务执行完成之后是没有返回结果的,因为Runnable接口中的run方法的返回类型是个void,但实际开发当中,我们的确有需求,在执行一些比如查询数据库,读取文件之类的操作中,需要获取任务的执行结果,之前都是通过在线程当中手动添加一个handler将需要的数据传递出来,再专业一点使用RxJava或者Flow,但不管什么方式,这些都会造成代码耦合,我们还有更简单的方式


Callable和Future


这两个类是在java1.5之后推出来的,目的就是解决线程执行完成之后没有返回结果的问题,我们先来对比下Runnable与Callable


image.png

相比较于Runnable,Callable接口里面也有一个call的方法,这个方法是有返回值的,并且可以抛出异常,所以以后当我们需要获取任务的执行结果的时候,我们还可以使用Callable代替Runnable,那么如何使用并获取返回值呢?当然是使用我们已经创建好的ExecutorService,它里面提供了一个函数去执行Callable


image.png

使用submit函数就可以执行我们的Callable,返回值是一个Future,而如何去获取真正的返回结果,就在Future里面,我们看下


image.png

使用get方法就可以获取线程的执行结果,我们现在就来试试Callable和Future,在PoolManager里面再增加一个函数,用来执行Callable


image.png

我们这里有个简单的任务,就是返回一段文字,然后将这段文字显示在界面上,那么第一步,先在布局文件里面添加一个按钮


image.png

然后点击这个按钮,将任务里面返回的文字显示在按钮上,代码如下


image.png

得到的效果如下


aa2.gif


在这边特地把执行结果放在界面上而不是用日志打印出来的原因可能大家已经发现了,Callable在返回执行结果的同时,也帮我们把线程切回到了主线程,所以我们不用在特地去切换线程更新ui界面了


周期性任务


普通的单个任务我们讲完了,但是在项目当中往往会存在一些比较特殊的任务,可能需要你去周期性的去执行,举个常见的例子,在证券类的app里绘制k线图的时候,并不需要将服务器吐出来的数据统统拿出来绘制ui,这样对性能的开销是很大的,我们正确的做法是将数据都先存放在一个buffer里面,然后定时的去buffer里面拿最新数据就好,那这样一个定时刷新的功能如何在我们的线程池里面去实现呢,这里就要用到刚刚说到的另一种创建线程池的方式


image.png

这个函数创建的是一个ScheduledExecutorService对象,可周期性的执行任务,入参的corepoolSize表示可并发的线程数,现在我们在PoolManager里面添加上这个ScheduledExecutorService


image.png

而如何去执行任务,我们使用ScheduledExecutorService里面的scheduleAtFixedRate函数,我们先看下这个函数都有哪些入参


image.png

不用去看注释我们就能知道怎么使用这个函数,command就是执行的任务,第二,第三个参数分别表示延迟执行的时间以及任务执行的周期时间,第四个参数是时间的单位,在看返回值是一个ScheduleFuture,既然也是个Future,那是不是也可以通过它去获取任务执行的结果呢?答案是拿不到的,一个原因是command是一个Runnable而不是Callable,不会返回任务的执行结果,另外我们从注释上就能了解,这个ScheduleFuture只是用来当周期任务中有任务被取消了,或者被异常终止的时候,抛出异常用的,那ScheduledExecutorService一定有入参是Callable的函数的吧,找了找发现并没有,那只有一个办法了,我们在command里面去执行一个Callable任务,再将任务的执行结果回调出来就好了,代码设计如下


image.png

我们创建了一个函数叫executeScheduledJob,也有四个入参,job是一个Callable,用来执行我们的任务,callback是一个回调,用来将任务执行结果回调到上层去处理,后面两个刚刚已经介绍过了,这里设置了默认值,可自定义,现在我们就来实现一个简单的读秒功能,点击刚刚那个按钮,按钮初始值是1,然后每秒钟加一,代码实现如下


image.png

这边创建了一个CounterViewModel用来执行计数器的逻辑,dataState是一个StateFlow并且设置了初始值1,在onCallback里面接收到了任务执行结果并发送至上层展示,上层的代码逻辑如下


image.png

现在这个计时器功能完成了,我们来执行下代码看看效果如何


aa3.gif


我们这边使用StateFlow发送数据还有个好处,当接收的数据中有些数据需要过滤掉的时候,我们还可以使用StateFlow提供的操作符实现,比如这边我们只想展示奇数,那么代码可以改成如下所示


image.png

使用filter操作符将偶数的值过滤掉了,我们再看看效果


aa4.gif


总结


我们的这个线程管理工具到这里已经完成了,不是很复杂,但是项目当中存不存在这样一个工具明显会对整体开发效率,代码的可读性,维护成本,以及一个app的性能角度来讲都会有个很大的提升与改善,后面如果还做了其他优化工作,也会拿出来分享。


作者:Coffeeee
链接:https://juejin.cn/post/7215185077444345917
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Moshi 真正意义上的完美解决Gson在kotlin中默认值空的问题

Moshi Moshi是一个对Kotlin更友好的Json库,square/moshi: A modern JSON library for Kotlin and Java. (github.com) 依赖 implementation("com.square...
继续阅读 »

Moshi


Moshi是一个对Kotlin更友好的Json库,square/moshi: A modern JSON library for Kotlin and Java. (github.com)


依赖


implementation("com.squareup.moshi:moshi:1.8.0")
kapt("com.squareup.moshi:moshi-kotlin-codegen:1.8.0")

使用场景


基于kotlin-reflection反射需要额外添加 com.squareup.moshi:moshi-kotlin:1.13.0 依赖


// generateAdapter = true 表示使用codegen生成这个类的JsonAdapter
@JsonClass(generateAdapter = true)
// @Json 标识json中字段名
data class Person(@Json(name = "_name")val name: String, val age: Int)
fun main() {
   val moshi: Moshi = Moshi.Builder()
       // KotlinJsonAdapterFactory基于kotlin-reflection反射创建自定义类型的JsonAdapter
      .addLast(KotlinJsonAdapterFactory())
      .build()
   val json = """{"_name": "xxx", "age": 20}"""
   val person = moshi.adapter(Person::class.java).fromJson(json)
   println(person)
}


  • KotlinJsonAdapterFactory用于反射生成数据类的JsonAdapter,如果不使用codegen,那么这个配置是必要的;如果有多个factory,一般将KotlinJsonAdapterFactory添加到最后,因为创建Adapter时是顺序遍历factory进行创建的,应该把反射创建作为最后的手段




  • @JsonClass(generateAdapter = true)标识此类,让codegen在编译期生成此类的JsonAdapter,codegen需要数据类和它的properties可见性都是internal/public




  • moshi不允许需要序列化的类不是存粹的Java/Kotlin类,比如说Java继承Kotlin或者Kotlin继承Java


存在的问题


所有的字段都有默认值的情况


@JsonClass(generateAdapter = true)
data class DefaultAll(
  val name: String = "me",
  val age: Int = 17
)

这种情况下,gson 和 moshi都可以正常解析 “{}” json字符


部分字段有默认值


@JsonClass(generateAdapter = true)
data class DefaultPart(
  val name: String = "me",
  val gender: String = "male",
  val age: Int
)

// 针对以下json gson忽略name,gender属性的默认值,而moshi可以正常解析
val json = """{"age": 17}"""


产生的原因


Gson反序列化对象时优先获取无参构造函数,由于DefaultPart age属性没有默认值,在生成字节码文件后,该类没有无参构造函数,所有Gson最后调用了Unsafe.newInstance函数,该函数不会调用构造函数,执行对象初始化代码,导致name,gender对象是null。


Moshi 通过adpter的方式匹配类的构造函数,使用函数签名最相近的构造函数构造对象,可以是的默认值不丢失,但在官方的例程中,某些情况下依然会出现我们不希望出现的问题。


Moshi的特殊Json场景


1、属性缺失


针对以下类


@JsonClass(generateAdapter = true)
data class DefaultPart(
   val name: String,
   val gender: String = "male",
   val age: Int
)

若json = """ {"name":"John","age":18}""" Moshi可以正常解析,但如果Json=""" {"name":"John"}"""Moshi会抛出Required value age missing at $ 的异常,


2、属性=null


若Json = """{"name":"John","age":null} ”“”Moshi会抛出Non-null value age was null at $ 的异常


很多时候后台返回的Json数据并不是完全的统一,会存在以上情况,我们可以通过对age属性如gender属性一般设置默认值的方式处理,但可不可以更偷懒一点,可以不用写默认值,系统也能给一个默认值出来。


完善Moshi


分析官方库KotlinJsonAdapterFactory类,发现,以上两个逻辑的判断代码在这里


internal class KotlinJsonAdapter<T>(
 val constructor: KFunction<T>,
   // 所有属性的bindingAdpter
 val allBindings: List<Binding<T, Any?>?>,
   // 忽略反序列化的属性
 val nonIgnoredBindings: List<Binding<T, Any?>>,
   // 反射类得来的属性列表
 val options: JsonReader.Options
) : JsonAdapter<T>() {

 override fun fromJson(reader: JsonReader): T {
   val constructorSize = constructor.parameters.size

   // Read each value into its slot in the array.
   val values = Array<Any?>(allBindings.size) { ABSENT_VALUE }
   reader.beginObject()
   while (reader.hasNext()) {
       //通过reader获取到Json 属性对应的类属性的索引
     val index = reader.selectName(options)
     if (index == -1) {
       reader.skipName()
       reader.skipValue()
       continue
    }
       //拿到该属性的binding
     val binding = nonIgnoredBindings[index]
// 拿到属性值的索引
     val propertyIndex = binding.propertyIndex
     if (values[propertyIndex] !== ABSENT_VALUE) {
       throw JsonDataException(
         "Multiple values for '${binding.property.name}' at ${reader.path}"
      )
    }
// 递归的方式,初始化属性值
     values[propertyIndex] = binding.adapter.fromJson(reader)

       // 关键的地方1
       // 判断 初始化的属性值是否为null ,如果是null ,代表这json字符串中的体现为 age:null
     if (values[propertyIndex] == null && !binding.property.returnType.isMarkedNullable) {
         // 抛出Non-null value age was null at $ 异常
       throw Util.unexpectedNull(
         binding.property.name,
         binding.jsonName,
         reader
      )
    }
  }
   reader.endObject()

   // 关键的地方2
    // 初始化剩下json中没有的属性
   // Confirm all parameters are present, optional, or nullable.
     // 是否调用全属性构造函数标志
   var isFullInitialized = allBindings.size == constructorSize
   for (i in 0 until constructorSize) {
     if (values[i] === ABSENT_VALUE) {
         // 如果等于ABSENT_VALUE,表示该属性没有初始化
       when {
           // 如果该属性是可缺失的,即该属性有默认值,这不需要处理,全属性构造函数标志为false
         constructor.parameters[i].isOptional -> isFullInitialized = false
           // 如果该属性是可空的,这直接赋值为null
         constructor.parameters[i].type.isMarkedNullable -> values[i] = null // Replace absent with null.
           // 剩下的则是属性没有默认值,也不允许为空,如上例,age属性
           // 抛出Required value age missing at $ 异常
         else -> throw Util.missingProperty(
           constructor.parameters[i].name,
           allBindings[i]?.jsonName,
           reader
        )
      }
    }
  }

   // Call the constructor using a Map so that absent optionals get defaults.
   val result = if (isFullInitialized) {
     constructor.call(*values)
  } else {
     constructor.callBy(IndexedParameterMap(constructor.parameters, values))
  }

   // Set remaining properties.
   for (i in constructorSize until allBindings.size) {
     val binding = allBindings[i]!!
     val value = values[i]
     binding.set(result, value)
  }

   return result
}

 override fun toJson(writer: JsonWriter, value: T?) {
   if (value == null) throw NullPointerException("value == null")

   writer.beginObject()
   for (binding in allBindings) {
     if (binding == null) continue // Skip constructor parameters that aren't properties.

     writer.name(binding.jsonName)
     binding.adapter.toJson(writer, binding.get(value))
  }
   writer.endObject()
}


通过代码的分析,是不是可以在两个关键的逻辑点做以下修改



// 关键的地方1
// 判断 初始化的属性值是否为null ,如果是null ,代表这json字符串中的体现为 age:null
if (values[propertyIndex] == null && !binding.property.returnType.isMarkedNullable) {
   // 抛出Non-null value age was null at $ 异常
   //throw Util.unexpectedNull(
   //   binding.property.name,
   //   binding.jsonName,
   //   reader
   //)
   // age:null 重置为ABSENT_VALUE值,交由最后初始化剩下json中没有的属性的时候去初始化
values[propertyIndex] = ABSENT_VALUE
}

// 关键的地方2
// 初始化剩下json中没有的属性
// Confirm all parameters are present, optional, or nullable.
// 是否调用全属性构造函数标志
var isFullInitialized = allBindings.size == constructorSize
for (i in 0 until constructorSize) {
   if (values[i] === ABSENT_VALUE) {
       // 如果等于ABSENT_VALUE,表示该属性没有初始化
       when {
           // 如果该属性是可缺失的,即该属性有默认值,这不需要处理,全属性构造函数标志为false
           constructor.parameters[i].isOptional -> isFullInitialized = false
           // 如果该属性是可空的,这直接赋值为null
           constructor.parameters[i].type.isMarkedNullable -> values[i] = null // Replace absent with null.
           // 剩下的则是属性没有默认值,也不允许为空,如上例,age属性
           // 抛出Required value age missing at $ 异常
           else ->{
               //throw Util.missingProperty(
                   //constructor.parameters[i].name,
                   //allBindings[i]?.jsonName,
                   //reader
          //)
               // 填充默认
               val index = options.strings().indexOf(constructor.parameters[i].name)
               val binding = nonIgnoredBindings[index]
               val propertyIndex = binding.propertyIndex
// 为该属性初始化默认值
               values[propertyIndex] = fullDefault(binding)

          }
      }
  }
}



private fun fullDefault(binding: Binding<T, Any?>): Any? {
       return when (binding.property.returnType.classifier) {
           Int::class -> 0
           String::class -> ""
           Boolean::class -> false
           Byte::class -> 0.toByte()
           Char::class -> Char.MIN_VALUE
           Double::class -> 0.0
           Float::class -> 0f
           Long::class -> 0L
           Short::class -> 0.toShort()
           // 过滤递归类初始化,这种会导致死循环
           constructor.returnType.classifier -> {
               val message =
                   "Unsolvable as for: ${binding.property.returnType.classifier}(value:${binding.property.returnType.classifier})"
               throw JsonDataException(message)
          }
           is Any -> {
               // 如果是集合就初始化[],否则就是{}对象
               if (Collection::class.java.isAssignableFrom(binding.property.returnType.javaType.rawType)) {
                   binding.adapter.fromJson("[]")
              } else {
                   binding.adapter.fromJson("{}")
              }
          }
           else -> {}
      }
  }

最终效果


"""{"name":"John","age":null} ”“” age会被初始化成0,


"""{"name":"John"} ”“” age依然会是0,即使我们在类中没有定义age的默认值


甚至是对象


@JsonClass(generateAdapter = true)
data class DefaultPart(
   val name: String,
   val gender: String = "male",
   val age: Int,
   val action:Action
)
class Action(val ac:String)

最终Action也会产生一个Action(ac:"")的值


data class RestResponse<T>(
val code: Int,
val msg: String="",
val data: T?
) {
fun isSuccess() = code == 1

fun checkData() = data != null

fun successRestData() = isSuccess() && checkData()

fun requsetData() = data!!
}
class TestD(val a:Int,val b:String,val c:Boolean,val d:List<Test> ) {
}

class Test(val a:Int,val b:String,val c:Boolean=true)



val s = """
{
"code":200,
"msg":"ok",
"data":[{"a":0,"c":false,"d":[{"b":null}]}]}
""".trimIndent()

val a :RestResponse<List<TestD>>? = s.fromJson()



最终a为 {"code":200,"msg":"ok","data":[{"a":0,"b":"","c":false,"d":[{"a":0,"b":"","c":true}]}]}


作者:ChangJiahong
链接:https://juejin.cn/post/7209578803160465468
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

抓包神器 Charles 使用教程支持mac ios Android

本文以Mac 系统为例进行讲解 配置手机代理: 手机和 Mac 连接到同一个 WiFi 网络 1.1 Android 系统:「以华为 P20 手机为例」 设置 -> 无线和网络 -> WLAN 长按当前 WiFi -> 修改网络 勾选显...
继续阅读 »

本文以Mac 系统为例进行讲解



  • 配置手机代理:


手机和 Mac 连接到同一个 WiFi 网络


1.1 Android 系统:「以华为 P20 手机为例」



  • 设置 -> 无线和网络 -> WLAN

  • 长按当前 WiFi -> 修改网络

  • 勾选显示高级选项

  • 代理 -> 手动

  • 服务器主机名 -> 填写 Mac 的IP 地址「Mac IP 获取方法:Charles -> Help -> Local IP Address 」

  • 服务器端口 -> 8888

  • 保存


1.2 IOS 系统:「以 iPhone Xs Max 手机为例」



  • 设置 -> 无线局域网

  • 点击当前连接的 WiFi

  • 最底部 HTTP 代理 -> 配置代理 -> 勾选手动

  • 服务器 -> 填写 Mac 的IP 地址「Mac IP 获取方法:Charles -> Help -> Local IP Address 」

  • 端口 -> 8888

  • 存储


核心功能


一、  抓包「以 iPhone Xs Max 为例」



  1. Charles 设置



  • Proxy -> Proxy Settings -> Port -> 8888

  • 勾选 Support HTTP/2

  • 勾选 Enable transparent HTTP proxying

  • OK




  1. 手机设置代理如上「配置手机代理」步骤




  2. 打开手机上任意联网的应用,Charles 会弹出请求连接的确认菜单,点击“Allow“即可完成设置




二、  过滤网络请求



  1. 左侧底部 Filter 栏 -> 过滤关键字




  1. 在 Charles 的菜单栏选择


Proxy -> Recording Settings -> Include -> add「依次填入协议+主机名+端口号,即可只抓取目标网站的包」



  1. 切换到 Sequence,在想过滤的网络请求上右击,选择“Focus“,在 Filter 栏勾选上 Focused


三、  分析 HTTPS 



  1. Mac 安装证书:


Help -> SSL Proxying -> Install Charles Root Certificate -> 输入系统的帐号密码,即可在钥匙串中看到添加好的证书


image.png


如果遇到证书不被信任的问题,解决办法:


Mac本顶栏 前往 -> 实用工具 -> 打开钥匙串访问 -> 找到该证书 -> 双击或右键「显示简介」-> 点开「信任」-> 选择「始终信任」




  1. Charles 设置请求允许 SSL proxying




  2. Charles 默认并不抓取 HTTPS 网络通讯的数据,若想拦截所有 HTTPS 网络请求,需要进行设置:在请求上右击选择 Enable SSL proxying




image.png
2. Charles -> Proxy -> SSL Proxying Settings -> SSL Proxying「添加对应的域名和端口号,为方便也可端口号直接添加通配符*」



  1. 移动端安装证书


a. Charles 选择 Help -> SSL Proxying -> Install Charles Root Certificate on a Mobile Device or Remote Browser


b. 确保手机连上代理的情况下,在手机浏览器栏输入:chls.pro/ssl,下载证书,完成安装。


Android tips


1.1. 小米机型请注意,如果是 MIUI 9 以上的版本,请不要用自带浏览器下载证书,自带浏览器下载的证书文件格式不对,无法安装,uc 浏览器下载没有问题。


1.2. 若不能直接安装,需要下载下来,到手机设置 -> 安全 -> 从设备存储空间安装 -> 找到下载的证书 .pem 结尾的 -> 点击安装即可


IOS tips


IOS 需要设置手机信任证书,详见 官方文档。若不能直接安装,需在手机「设置」-> 通用 -> 描述文件与设备管理安装下载的证书,完成安装后 -> 找到关于本机 -> 证书信任设置,打开刚安装的证书的开关。


抓包内容遇到乱码,解决如下:



  • Proxy -> SSL Proxy Settings -> Add

  • Host:*「代表所有网站都拦截」

  • Port:443

  • 保存后,在抓包数据就会显示正常


四、  模拟弱网




  1. 选择 Proxy -> Throttle Settings -> 勾选 Enable Throttling -> 选择 Throttle Preset 类型
    image.png
    五、  Mock 数据




  2. 以 map local 为例,修改返回值




选择目标请求,右键选择 Save All保存请求的 response 内容到本地文件



  1. 配置 Charles Map Local,Tool -> Map Local -> 勾选 Enable Map Local -> Add 「添加目标请求及需要替换的response 文件地址」-> OK


image.png



  1. 用文本编辑器打开保存的 json 文件,修改内容,进行替换。打开客户端应用重新请求该接口,返回的数据就是本地的文件数据。



作者:CodeCiCi
链接:https://juejin.cn/post/7215105725387374650
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »