注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

🤭新的广告方式,很新很新!

哈哈 会爬树的金鱼,树上的金鱼呦😀 前言 老板:针对上半年业绩发表一下大家的看法,大家自由发言,已经定好晚餐和夜宵了有需要的自取୧(๑•̀⌄•́๑)૭ 产品A:根据大数据分析公司产品的广告收入较低拖后腿,建议扩宽曝光渠道! 产品B:对! 大数据分析公司产品的广...
继续阅读 »

哈哈 会爬树的金鱼,树上的金鱼呦😀


前言


老板:针对上半年业绩发表一下大家的看法,大家自由发言,已经定好晚餐和夜宵了有需要的自取୧(๑•̀⌄•́๑)૭


产品A:根据大数据分析公司产品的广告收入较低拖后腿,建议扩宽曝光渠道!


产品B:对! 大数据分析公司产品的广告大部分来自移动端,pc端曝光率较少,可以增加一下曝光率,据统计移动端大部分广告收入来自首屏广告,我觉得pc也可以加上


程序员: PC哪有首屏广告啊,行业都没有先例


老板:这个好,没有先例! 我们又可以申请专利了Ψ( ̄∀ ̄)Ψ 搞起!!今年公司专利指标有着落了


程序员: 这也太影响体验了


产品A:就说能不能做吧, 今天面试的那个应届生不错能倒背chromium源码,还能手写react源码并且指出优化方案


程序员: 我做!!! ╭( ̄m ̄*)╮


先来个全屏遮罩🤔


这还不简单,直接一个定位搞定

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>XX百货</title>
<style>
#ADBox {
background: #fff;
position: fixed;
width: 100%;
height: 100%;
display: none;
}
</style>
</head>
<body>
<div id="ADBox">广告</div>
</body>
</html>


搞定提测!


重来没接过这么简单的需求,送业绩这是╮(╯﹏╰)╭


第一次提测🤐


测试A: 送业绩这是? 产品说要和移动端一模一样,你这哪一样了?? 直系看需求文档!!


程序员: 需求文档就一句话, 和移动端一样的开屏广告, 这那不一样了?


测试A: 这哪一样了?? 你家移动端广告露个顶部出来?看看哪个app广告不是全屏的???


程序员: 啥? 还要全屏?? 行...


// 必须用点击事件触发才能全屏
document.addEventListener("click", async (elem) => {
const box = document.getElementById("ADBox");
if (box.requestFullscreen) {
box.requestFullscreen();
}
setTimeout(() => {
const state = !!document.fullscreenElement;
// 是否全屏状态
if (state) {
// 取消全屏
if (document.exitFullscreen) {
document.exitFullscreen();
}
}
}, 5 * 1000);
});

搞定提测


第二次提测🙄


产品A: 嗯...有点感觉了,这鼠标去掉都遮住广告了,万一广告商不满意投诉怎么办?


程序员: 鼠标这么小这么能遮住广告??


产品B: 看我鼠标? (大米老鼠标PC主题)


程序员: ...

<style>
#ADBox {
background: #fff;
position: fixed;
width: 100%;
height: 100%;
// 隐藏广告Box让用户点任意地方激活
opacity: 0;

}
</style>

提测...


第三次提测🤕


测试A: 为啥还有鼠标???


程序员: 怎么那可能还有?


测试A: 过来看,鼠标不动的话还是会显示鼠标哦,动一下才消失


程序员: ##..行, 那我直接锁指针

    <script>
let pointerLockElement = null;
// 指针锁定或解锁
document.addEventListener(
"pointerlockchange",
() => {
// 锁定的元素是否为当前的元素 -- 没啥也意义可以去掉
if (document.pointerLockElement === pointerLockElement) {
console.log("指针锁定成功了。");
} else {
pointerLockElement = null;
console.log("已经退出锁定。");
}
},
false
);
// 锁定失败事件
document.addEventListener(
"pointerlockerror",
() => {
console.log("锁定指针时出错。");
},
false
);

// 锁定指针,锁定指针的元素必须让用户点一下才能锁定
function lockPointer(elem) {
// 如果已经存锁定的元素则不操作
if (document.pointerLockElement) {
return;
}
if (elem) {
pointerLockElement = elem;
elem.requestPointerLock();
}
}

// 解除锁定
function unlockPointer() {
document.exitPointerLock();
}

// 必须用点击事件触发才能全屏
document.addEventListener("click", async () => {
const box = document.getElementById("ADBox");
if (box.requestFullscreen) {
box.requestFullscreen();
box.style.opacity = 1;
box.style.display = "block";
lockPointer(box);
}
// 5秒后解除锁定
setTimeout(() => {
const state = !!document.fullscreenElement;
// 是否全屏状态
if (state) {
// 取消全屏
if (document.exitFullscreen) {
document.exitFullscreen();
unlockPointer();
box.style.display = "none";
}
}
}, 5 * 1000);
});
</script>

提测...


第四次提测😤


测试A: Safari上失效哦


程序员: 额....

<script>

// requestFullscreen 方法兼容处理
function useRequestFullscreen(elem) {
const key = ['requestFullscreen', 'mozRequestFullScreen', 'webkitRequestFullscreen', 'msRequestFullscreen']
for (const value of key) {
if (elem[value]) {
elem[value]()
return true
}
}
return false
}

// document.exitFullscreen 方法兼容处理
document.exitFullscreenUniversal = document.exitFullscreen || document.webkitExitFullscreen || document.mozCancelFullScreen

// fullscreenElement 对象兼容处理
function getFullscreenElement() {
const key = ['fullscreenElement', 'webkitFullscreenElement']
for (const value of key) {
if (document[value]) {
return document[value]
}
}
return null
}

// fullscreenchange 事件兼容处理
addEventListener("fullscreenchange", endCallback);
addEventListener("webkitfullscreenchange", endCallback);

// requestPointerLock 方法在Safari下不可与 requestFullscreen 方法共用一个事件周期 暂无解决方法,必须让用户点两次鼠标,第一次全屏,第二次锁鼠标
// 同一事件周期内会出现的问题: 1.有小机率会正常执行, 2.顶部出现白条(实际上是个浏览器锁鼠标的提示语,但显示异常了) 3.锁定鼠标失败

</script>


结尾😩


产品A: 效果不错,但还有点小小的瑕疵,为啥要鼠标点一下才能弹广告,改成进入就弹窗吧


程序员: 要不还是找上次那个应届生来吧,改chromium源码应该能实现╭∩╮(︶︿︶)╭∩╮


效果预览: http://www.npmstart.top/BSOD.html


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

你知道什么是SaaS吗?

天天听SaaS,相信大家都知道什么叫SaaS系统!这不?领导安排下来了任务,说要去做SaaS系统,作为小白的我赶紧去看看什么是SaaS,大概收集整理(并非原创)了这部分内容,分享给大家。相信大家看了也会有很多收获。 本文从以下几个方面对SaaS系统召开介绍...
继续阅读 »

天天听SaaS,相信大家都知道什么叫SaaS系统!这不?领导安排下来了任务,说要去做SaaS系统,作为小白的我赶紧去看看什么是SaaS,大概收集整理(并非原创)了这部分内容,分享给大家。相信大家看了也会有很多收获。


  本文从以下几个方面对SaaS系统召开介绍:


  1. 云服务架构的三个概念

  2. SaaS系统的两大特征

  3. SaaS服务与传统服务、互联网服务的区别

  4. B2B2C

  5. SaaS系统的分类

  6. 如何SaaS化

  7. SaaS产品的核心组件

  8. SaaS多租户


一、云服务架构的三个概念


1.1 PaaS


英文就是 Platform-as-a-Service(平台即服务)


PaaS,某些时候也叫做中间件。就是把客户采用提供的开发语言和工具(例如Java,python, .Net等)开发的或收购的应用程序部署到供应商的云计算基础设施上去。
客户不需要管理或控制底层的云基础设施,包括网络、服务器、操作系统、存储等,但客户能控制部署的应用程序,也可能控制运行应用程序的托管环境配置。


PaaS 在网上提供各种开发和分发应用的解决方案,比如虚拟服务器和特定的操作系统。底层的平台3/4帮你铺建好了,你只需要开发自己的上层应用。这即节省了你在硬件上的费用,也让各类应用的开发更加便捷,不同的工作互相打通也变得容易,因为在同一平台上遵循的是同样的编程语言、协议和底层代码。


1.2 IaaS


英文就是 Infrastructure-as-a-Service(基础设施即服务)


IaaS 提供给消费者的服务是对所有计算基础设施的利用,包括处理 CPU、内存、存储、网络和其它基本的计算资源,用户能够部署和运行任意软件,包括操作系统和应用程序。
消费者不管理或控制任何云计算基础设施,但能控制操作系统的选择、存储空间、部署的应用,也有可能获得有限制的网络组件(例如路由器、防火墙、负载均衡器等)的控制。


IaaS 会提供场外服务器,存储和网络硬件,你可以租用。节省了维护成本和办公场地,公司可以在任何时候利用这些硬件来运行其应用。我们最熟悉的IaaS服务是我们服务器托管业务,多数的IDC都提供这样的服务,用户自己不想要再采购价格昂贵的服务器和磁盘阵列了,所有的硬件都由 IaaS 提供,你还能获得品质更高的网络资源。


1.3 SaaS


英文就是 Software-as-a-Service(软件即服务)


SaaS提供给客户的服务是运行在云计算基础设施上的应用程序,用户可以在各种设备上通过客户端界面访问,如浏览器。
消费者不需要管理或控制任何云计算基础设施,包括网络、服务器、操作系统、存储等等。


SaaS 与我们普通使用者联系可能是最直接的,简单地说任何一个远程服务器上的应用都可以通过网络来运行,就是SaaS了。国内的互联网巨头竭力推荐的 SaaS 应用想必大家已经耳熟能详了,比如阿里的钉钉,腾讯的企业微信,这些软件里面应用平台上的可供使用的各类SaaS小软件数不胜数,从OA,到ERP到CRM等等,涵盖了企业运行所需的几乎所用应用。


二、SaaS系统的两大特征



  1. 部署在供应商的服务器上,而不是部署在甲方的服务器上。

  2. 订购模式,服务商提供大量功能供客户选择,客户可以选择自己需要的进行组合,支付所需的价格,并支持按服务时间付费。


三、SaaS服务与传统服务、互联网服务的区别


3.1 SaaS服务


介于传统与互联网之间,通过租用的方式提供服务,服务部署在云端,任何用户通过注册后进行订购后获得需要的服务,可以理解成服务器及软件归供应商所有,用户通过付费获得使用权
image.png


3.2 传统软件


出售软件及配套设备,将软件部署在客户服务器或客户指定云服务器,出售的软件系统及运维服务为盈利来
image.png


3.3 互联网应用供应商


服务器部署在云端,所有用户可以通过客户端注册进行使用,广告及付费增值服务作为盈利来源
image.png


四、B2B2C


SaaS作为租户系统,需要为租户(C端)提供注册、购买、业务系统的入口,还得为B端(运营/运维)提供租户管理、流量监控、服务状态监控运维入口


五、SaaS系统的分类


5.1 业务型SaaS


定义:为客户的赚钱业务提供工具以及服务的SaaS,直面的是用户的生意,例如有赞微盟等电商SaaS以及销售CRM工具,为B2B2C企业;


架构以及商业模式:在产品的成长期阶段,为了扩充业务规模和体量,业务SaaS产品会拓展为“多场景+多行业”的产品模式,为不同行业或者不同场景提供适应的解决方案,例如做电商独立站的有赞,后期发展为“商城、零售、美业、教育”多行业的解决方案进行售卖。
image.png


5.2 效率型SaaS


定义:为客户效率提升工具的SaaS,如项目管理工具、Zoom等会议工具,提升办公或者生产效率,为B2B企业;


架构以及商业模式:不同于业务型的SaaS,效率SaaS思考得更多的是企业内存在一个大共性的效率的问题,不同的企业对于CRM销售系统的需求是不一样的,但都需要一个协同办公的产品来提升协作效率。对于效率类SaaS来说,从哪来到哪去是非常清晰的,就是要解决优化或者解决一个流程上的问题。
image.png


5.3 混合型SaaS


定义:即兼顾企业业务和效率效用SaaS,例如近几年在私域流量上大做文章的企业微信,其本身就是一个办公协同工具,但为企业提供了一整套的私域管理能力,实现业务的提升,同时也支持第三方服务。


架构以及商业模式:混合SaaS是业务和效率SaaS的结合体,负责企业业务以及企业管理流程的某类场景上的降本增效;因混合SaaS核心业务的使用场景是清晰且通用的,非核心业务是近似于锦上添花的存在,所以在中台产品架构上更接近为“1+X”组合方式——即1个核心业务+X个非核心功能,两者在产品层级上是属于同一层级的。
image.png


六、如何SaaS化



  1. 进行云化部署,性能升级,能够支持更大规模的用户访问

  2. 用户系统改造,支持2C用户登录(手机号一键登录、小程序登录、短信验证码登录)

  3. 网关服务,限流,接口防篡改等等

  4. 租户系统开发,包含租户基础信息管理、租户绑定资源(订购的功能)、租户服务期限等等

  5. 客户端改造(通常SaaS系统主要提供WEB端服务),页面权限控制,根据租户系统用户资源提供用户已购买的模块或页面

  6. 官网开发,功能报价单,功能试用、用户选购及支付

  7. 服务端接口数据权限改造、租户级别数据权限


七、SaaS产品的核心组件



  1. 安全组件:在SaaS产品中,系统安全永远是第一位需要考虑的事情

  2. 数据隔离组件:安全组件解决了用户数据安全可靠的问题,但数据往往还需要解决隐私问题,各企业之间的数据必须相互不可见,即相互隔离。

  3. 可配置组件:SaaS产品在设计之初就考虑了大多数通用的功能,让租户开箱即用,但任然有为数不少的租户需要定制服务自身业务需求的配置项,如UI布局、主题、标识(Logo)等信息

  4. 可扩展组件:SaaS产品应该具备水平扩展的能力。如通过网络负载均衡其和容器技术,在多个服务器上部署多个软件运行示例并提供相同的软件服务,以此实现水平扩展SaaS产品的整体服务性能

  5. 0停机时间升级产品:实现在不重启原有应用程序的情况下,完成应用程序的升级修复工作

  6. 多租户组件:SaaS产品需要同时容纳多个租户的数据,同时还需要保证各租户之间的数据不会相互干扰,保证租户中的用户能够按期望索引到正确的数据


八、SaaS多租户


8.1 多租户核心概念



  • 租户:一般指一个企业客户或个人客户,租户之间数据与行为是隔离的

  • 用户:在某个租户内的具体使用者,可以通过使用账户名、密码等登录信息,登录到SaaS系统使用软件服务

  • 组织:如果租户是一个企业客户,通常会拥有自己的组织架构

  • 员工:是指组织内部具体的某位员工。

  • 解决方案:为了解决客户的某类型业务问题,SaaS服务商将产品与服务组合在一起,为商家提供整体的打包方案。

  • 产品能力:指的是SaaS服务商对客户售卖的产品应用,特指能够帮助客户实现端到端场景解决方案闭环的能力。

  • 资源域:用来运行1个或多个产品应用的一套云资源环境

  • 云资源:SaaS产品一般都部署在各种云平台上,例如阿里云、腾讯云、华为云等。对这些云平台提供的计算、存储、网络、容器等资源,抽象为云资源。


8.2 三大模式


8.2.1 竖井隔离模式


image.png



  • 优势:



  1. 满足强隔离需求:一些客户为了系统和数据的安全性,可能提出非常严格的隔离需求,期望软件产品能够部署在一套完全独立的环境中,不和其他租户的应用实例、数据放在一起。

  2. 计费逻辑简单:SaaS服务商需要针对租户使用资源进行计费,对于复杂的业务场景,计算、存储、网络资源间的关系同样也会非常复杂,计费模型是很有挑战的,但在竖井模式下,计费模型相对来说是比较简单的。

  3. 降低故障影响面:因为每个客户的系统都部署在自己的环境中,如果其中一个环境出现故障,并不会影响其他客户使用软件服务。



  • 劣势:



  1. 规模化问题:由于租户的SaaS环境是独立的,所以每入驻一个租户,就需要创建和运营一套SaaS环境,如果只是少量的租户,还可能可以管理,但如果是成千上万的租户,管理和运营这些环境将会是非常大的挑战。

  2. 成本问题:每个租户都有独立的环境,花费在单个客户上的成本将非常高,会大幅削弱SaaS软件服务的盈利能力。

  3. 敏捷迭代问题:SaaS模式的一个优势是能够快速响应市场需求,迭代产品功能。但竖井隔离策略会阻碍这种敏捷迭代能力,因为更新、管理、支撑这些租户的SaaS环境,会变得非常复杂和低效。

  4. 统一管理与监控:在同一套环境中,对部署的基础设施进行管理与监控,是较为简单的。但每个租户都有独立的环境,在这种非中心化的模式下,对每个租户的基础设施进行管理与监控,同样也是非常复杂、困难的。


8.2.2 共享模式


image.png



  • 优势:



  1. 高效管理:在共享策略下,能够集中化地管理、运营所有租户,管理效率非常高。同时,对基础设施配置管理、监控,也将更加容易。相比竖井策略,产品的迭代更新会更快。

  2. 成本低:SaaS服务商的成本结构中,很大一块是基础设施的成本。在共享模型下,服务商可以根据租户们的实际资源负载情况,动态伸缩系统,这样基础设施的利用率将非常高。



  • 劣势:



  1. 租户相互影响:由于所有租户共享一套资源,当其中一个租户大量占用机器资源,其他租户的使用体验很可能受到影响,在这种场景下,需要在技术架构上设计一些限制措施(限流、降级、服务器隔离等),让影响面可控。

  2. 租户计费困难:在竖井模型下,非常容易统计租户的资源消耗。然而,在共享模型下,由于所有租户共享一套资源,需要投入更多的精力统计单个租户的合理费用。


8.2.3 分域隔离模式


image.png


8.3 多租户系统需要具备的能力



  1. 多个租户支持共享一套云资源,如计算、存储、网络资源等。单个租户也可以独占一套云资源。

  2. 多个租户间能够实现数据与行为的隔离,能够对租户进行分权分域控制。

  3. 租户内部能够支持基于组织架构的管理,可以对产品能力进行授权和管理。

  4. 不同的产品能力可以根据客户需求,支持运行在不同的云资源上。


8.4 多租户系统应用架构图


image.png

收起阅读 »

如何把一家创业公司搞垮

在拜读了耗子哥推荐的书《重来》之后,如何把一个创业公司搞垮,我得到了一些灵感。 追求完美的产品 我们都知道没有完美的产品,但是对于创业公司,想要做出完美的产品,至少需要付出很多的努力: 毫无 BUG:全面的产品交互设计、严格的编码过程、完整的用例测试等等。 ...
继续阅读 »

在拜读了耗子哥推荐的书《重来》之后,如何把一个创业公司搞垮,我得到了一些灵感。


追求完美的产品


我们都知道没有完美的产品,但是对于创业公司,想要做出完美的产品,至少需要付出很多的努力:



  1. 毫无 BUG:全面的产品交互设计、严格的编码过程、完整的用例测试等等。

  2. 大而全的功能:不要花时间去区分重要、次要,所有能力都得上,所有平台都得适配,所有功能都得支持。

  3. 延迟交付:追求完美产品,需要付出时间和精力,延期延期再延期。

  4. 沉默的反馈:开发完毕一个功能,要经过很久才能上线,迟迟得不到客户的真实反馈。


追求完美的产品,这个功能也要,那个功能也要,迟迟交付产品,磨灭团队信心,减少公司成功几率,钝刀子杀人


开会的技巧


会议是一种毒药,在开会的时候,我们要尽量扩大它的毒性:



  1. 没有明确的问题,没有确定的议程。

  2. 人员尽量扩大。不要精简会议人员,多增加无关人员。

  3. 每个人都发言。多听取一些低能儿的无效意见

  4. 去会议室。可以直接打断每个人的工作。

  5. 时长不限。不要限制开会的时间,时间越长越好。1 小时的会议,10 个人参加的话,就可以减少公司 10 小时的生产时间。


通过以上一些开会技巧,可以有效地增加会议的时长,多积累一些纸上谈兵的想法


做长期计划


做计划的本质是用过去指导未来,用以前的经验去圈套之后的变化。做长期计划,就可以把一个创业公司给套牢。


当创业公司按照一段时间去执行长期计划之后,如果发现事情不妙,可能会因为这么几个方面而硬着头皮继续执行:



  1. 沉没成本:我们都已经付出 4 个月的努力,不继续做下去很可惜吗?

  2. 傲慢自负:我们都已经定好目标了,再改变不是打脸吗?


满足客户


上线之后,要记住客户自上,来自客户的反馈都必须汲取,客户的要求都必须满足,让自己的产品成为一个臃肿的产品,成为一个臃肿产品的好处:



  1. 功能很多。用户所有的需求都能满足,意味着我们有对应处理需求的纷繁功能。

  2. 提高复杂性。每一个功能的增加,都需要对应交互,乱七八糟的功能可以让我们的产品交互变得复杂,界面花里胡哨。

  3. 拒绝新用户。通过提高产品复杂性,可以有效减少新的用户。

  4. 没有个性,平易近人。像一辆公交车一样,谁都可以上。


千万不要追求简洁,我们的目标是努力变成的微信,成就一款庞大、臃肿的垃圾产品


融资扩张,多招人手


当产品取得一定的成效,就需要马上融资,融资带来的好处太多:



  1. 更大的办公室,人数更多的公司

  2. 花别人的钱会上瘾

  3. 对公司失去控制权

  4. 投资人套现离场的风险

  5. 融资非常耗时耗力

  6. 产品可能偏向迎合投资人而不是客户


多招聘人手,新来的人:



  1. 对公司不了解

  2. 对项目不了解

  3. 互相谦让,互相客气

  4. 谁也不敢指出产品缺陷


集中力量办小事


人总是有限的,资源也总是有限的,我们需要正确地调用这些人力和资源,把他们都投入到小事中,如何做到呢?



  1. 不做取舍。在众多的事情中,不要去试图找到中心点,所有的任务都必须做,所有的需求都必须完成。

  2. 没有急事。把所有事情都当做急事,那也就没有急事。

  3. 唯唯诺诺。顺从永远比争锋相对容易,人们很容易同意添加一项新功能、接受一个过于乐观的最后期限、笑纳一个平庸的设计


不以盈利为目的


公司如何通过产品盈利的事情,尽量搁置,就像我们在设计神舟一号的时候,先假设地心引力不存在


一家企业不以盈利为目的,那么公司的可持续存活就有问题,可以给员工一些退出策略:



  • 如何破产清算,保障各位 n+1

  • 被其他公司收购


让员工少一点破釜沉舟的勇气,可以让公司早一点走向 Ending。






我是楷鹏,这是我阅读《重来》的读书笔记:wukaipeng.com/

read/rework

收起阅读 »

懂点心理学 - 曼德拉效应

最近在看电影 《消失的她》 ,里面提到了一个效应 - 曼德拉效应:修改他人记忆。 本文,我们来谈谈曼德拉效应。 什么是曼德拉效应 曼德拉效应,是指人们错误地记忆了某个特定的事件或情节的现象。产生的方式可以是让人们对新奇或者陌生事物的偏好会随着暴露的频率的增加...
继续阅读 »

最近在看电影 《消失的她》 ,里面提到了一个效应 - 曼德拉效应:修改他人记忆。


曼德拉效应.png


本文,我们来谈谈曼德拉效应


什么是曼德拉效应


曼德拉效应,是指人们错误地记忆了某个特定的事件或情节的现象。产生的方式可以是让人们对新奇或者陌生事物的偏好会随着暴露的频率的增加而增加。它表明通过重复和频繁的某种刺激,我们对于该刺激产生更积极的态度和更强烈的喜好。当然,也可以混淆/误导他人的思维 - 通常表示虚假的记忆。


总是穿着你老婆的衣服.png


跟在你身后.png


老公.png


那么这个酒店的工作人员.png


就会习惯性认为.png


她就是何太太.png


这个效应可以解析为什么人们倾向于更喜欢和接受他们熟悉的人、事物和概念。


如何应用曼德拉效应


曼德拉效应可以在广告、宣传和社交等领域中应用。


比如,上个星期笔者在京东 app 上浏览器一个牌子的茶壶🫖。然后,过了半个小时,自己刷朋友圈,微信推送了京东这个牌子的茶壶广告给我。给到我必须买这个牌子的错觉~


再比如,在工作中,你在现在这个公司遇到了一个问题。然后过了几天后,你将这个 issue 关闭掉。在某天,你向 leader 汇报工作演示操作的时候,却翻车了。因为你这个问题原来在上一家公司解决了,你却错误认为是在目前这家公司解决了。(大脑给到了错误的信号给你:这个问题你已经解决了,不必处理了)。当然,有一种很恐怖的职场现象:职场 PUA你什么都干不好,我什么都比你强...


如何避免曼德拉效应


曼德拉效应既然是错误的表象。那么我们可以:



  1. 检查自己的记忆:如果我们对某个情节的事件质疑,尝试回想并核对相关的证据。与他人交流,比较彼此的记忆。

  2. 养成记录重要的事项:对于比较重要的事情,比如借钱等,要写下日期、金额等重要信息,以减少记忆错误的风险。比如你朋友欠你 1000 块钱,期间他还了 100 块钱给你。然后过了几个月后,你问TA 还钱。TA 说:上一次,我不是全还给你了嘛。然后你会不会回忆下,期间确实还了一次,然后真以为他全还给你了。笔者也有记录的习惯,比如这篇文章 借点钱来“救急”【多图】

  3. 接受更正和反馈:如果他人提出了自己记忆不符的观点和事实,我们得深入了解事实,不仅要靠记忆和第三方证据,还要寻求更多来源的证据,比如录音等。了解了事实后,意识到自己的问题,要保持开放的心态接受讨论和反馈(这点要做到,太难了)。


参考


收起阅读 »

项目部署之后页面没有刷新怎么办?

web
最近项目部署成功之后,突然产品找我,上线之后,页面没有生效,这是怎么回事?我这是第一次部署这个项目,也不太清楚历史问题,接下来就慢慢寻找答案吧, 如果心急的可以直接看后面的总结,下面我们好好聊聊缓存的问题。 浏览器输入url之后,就会进行下面一系列判断,来实现...
继续阅读 »

最近项目部署成功之后,突然产品找我,上线之后,页面没有生效,这是怎么回事?我这是第一次部署这个项目,也不太清楚历史问题,接下来就慢慢寻找答案吧, 如果心急的可以直接看后面的总结,下面我们好好聊聊缓存的问题。


浏览器输入url之后,就会进行下面一系列判断,来实现页面渲染。



首先讲一下常见的http缓存~


HTTP缓存常见的有两类:



  • 强缓存:可以由这两个字段其中一个决定





    • expires

    • cache-control(优先级更高)





  • 协商缓存:可以由这两对字段中的一对决定





    • Last-Modified,If-Modified-Since

    • Etag,If--Match(优先级更高)




强缓存


使用的是express框架


expires


app.get('/login', function(req, res){
// 设置 Expires 响应头
const time = new Date(Date.now() + 300000).toUTCString()
res.header('Expires', time)
res.render('login');
});

然后我们在前端页面刷新,我们可以看到请求的资源的响应头里多了一个expires的字段, 取消Disable cache



刷新



勾选Disable cache



但是,Expires已经被废弃了。对于强缓存来说,Expires已经不是实现强缓存的首选。


因为Expires判断强缓存是否过期的机制是:获取本地时间戳,并对先前拿到的资源文件中的Expires字段的时间做比较。来判断是否需要对服务器发起请求。这里有一个巨大的漏洞:“如果我本地时间不准咋办?”


是的,Expires过度依赖本地时间,如果本地与服务器时间不同步,就会出现资源无法被缓存或者资源永远被缓存的情况。所以,Expires字段几乎不被使用了。现在的项目中,我们并不推荐使用Expires,强缓存功能通常使用cache-control字段来代替Expires字段。


cache-control


其实cache-control跟expires效果差不多,只不过这两个字段设置的值不一样而已,前者设置的是秒数,后者设置的是毫秒数


app.get('/login', function(req, res){
// 设置 Expires 响应头
// const time = new Date(Date.now() + 300000).toUTCString()
// res.header('Expires', time)
// 设置 Cache-Control 响应头
res.header('Cache-Control', 'max-age=300')
res.render('login');
});

前端页面响应头多了cache-control这个字段,且300s内都走本地缓存,不会去请求服务端



Cache-Control:max-age=N,N就是需要缓存的秒数。从第一次请求资源的时候开始,往后N秒内,资源若再次请求,则直接从磁盘(或内存中读取),不与服务器做任何交互。


Cache-control中因为max-age后面的值是一个滑动时间,从服务器第一次返回该资源时开始倒计时。所以也就不需要比对客户端和服务端的时间,解决了Expires所存在的巨大漏洞。


Cache-control的多种属性:developer.mozilla.org/zh-CN/docs/…


但是使用最多的就是no-cache和no-store,接下来就重点学习这两种


no-cache和no-store


no_cache是Cache-control的一个属性。它并不像字面意思一样禁止缓存,实际上,no-cache的意思是强制进行协商缓存。如果某一资源的Cache-control中设置了no-cache,那么该资源会直接跳过强缓存的校验,直接去服务器进行协商缓存。而no-store就是禁止所有的缓存策略了。


app.get('/login', function(req, res){
// 设置 Expires 响应头
// const time = new Date(Date.now() + 300000).toUTCString()
// res.header('Expires', time)
// 设置 Cache-Control 响应头
res.header('Cache-Control', 'no-cache')
res.render('login');
});

no-cache(进行协商缓存,下次再次请求,没有勾选控制台Disable cache,状态码是304)



app.get('/login', function(req, res){
// 设置 Cache-Control 响应头
res.header('Cache-Control', 'no-store')
res.render('login');
});

no-store(每次都请求服务器的最新资源,没有缓存策略)



强制缓存就是以上这两种方法了。现在我们回过头来聊聊,Expires难道就一点用都没有了吗?也不是,虽然Cache-control是Expires的完全替代品,但是如果要考虑向下兼容的话,在Cache-control不支持的时候,还是要使用Expires,这也是我们当前使用的这个属性的唯一理由。


协商缓存


与强缓存不同的是,强缓存是在时效时间内,不走服务端,只走本地缓存;而协商缓存是要走服务端的,如果请求某个资源,去请求服务端时,发现命中缓存则返回304,否则则返回所请求的资源,那怎么才算命中缓存呢?接下来讲讲


Last-Modified,If-Modified-Since


简单来说就是:



  • 第一次请求资源时,服务端会把所请求的资源的最后一次修改时间当成响应头中Last-Modified的值发到浏览器并在浏览器存起来

  • 第二次请求资源时,浏览器会把刚刚存储的时间当成请求头中If-Modified-Since的值,传到服务端,服务端拿到这个时间跟所请求的资源的最后修改时间进行比对

  • 比对结果如果两个时间相同,则说明此资源没修改过,那就是命中缓存,那就返回304,如果不相同,则说明此资源修改过了,则没命中缓存,则返回修改过后的新资源


基于last-modified的协商缓存实现方式是:



  1. 首先需要在服务器端读出文件修改时间,

  2. 将读出来的修改时间赋给响应头的last-modified字段。

  3. 最后设置Cache-control:no-cache


三步缺一不可。


app.get('/login', function(req, res){
// 设置 Expires 响应头
// const time = new Date(Date.now() + 300000).toUTCString()
// res.header('Expires', time)

const { mtime } = fs.statSync(path.join(__dirname, 'public/index.css')) // 读取最后修改时间
console.log(mtime.toUTCString(), '--------')
// 响应头的last-modified字段
res.header('last-modified', mtime.toUTCString())
// 设置 Cache-Control 响应头
res.header('Cache-Control', 'no-cache')
res.render('login');
});


当index.css发生改变再次请求时



终端输出的时间变化



服务端的时间跟last-modified的值是一致的



使用以上方式的协商缓存已经存在两个非常明显的漏洞。这两个漏洞都是基于文件是通过比较修改时间来判断是否更改而产生的。


1.因为是更具文件修改时间来判断的,所以,在文件内容本身不修改的情况下,依然有可能更新文件修改时间(比如修改文件名再改回来),这样,就有可能文件内容明明没有修改,但是缓存依然失效了。


2.当文件在极短时间内完成修改的时候(比如几百毫秒)。因为文件修改时间记录的最小单位是秒,所以,如果文件在几百毫秒内完成修改的话,文件修改时间不会改变,这样,即使文件内容修改了,依然不会 返回新的文件。


为了解决上述的这两个问题。从http1.1开始新增了一个头信息,ETag(Entity 实体标签)


Etag,If--Match


ETag就是将原先协商缓存的比较时间戳的形式修改成了比较文件指纹。


其实Etag,If--Match跟Last-Modified,If-Modified-Since大体一样,区别在于:



  • 后者是对比资源最后一次修改时间,来确定资源是否修改了

  • 前者是对比资源内容,来确定资源是否修改


那我们要怎么比对资源内容呢?我们只需要读取资源内容,转成hash值,前后进行比对就行了!


app.get('/login', function(req, res){
// 设置 Expires 响应头
// const time = new Date(Date.now() + 300000).toUTCString()
// res.header('Expires', time)

// const { mtime } = fs.statSync(path.join(__dirname, 'public/index.css')) // 读取最后修改时间
// console.log(mtime.toUTCString(), '--------')
// 响应头的last-modified字段
// res.header('last-modified', mtime.toUTCString())


// 设置ETag
const ifMatch = req.header['if-none-match']
const hash = crypto.createHash('md5')
const fileBuf = fs.readFileSync(path.join(__dirname, 'public/index.css'))
hash.update(fileBuf, 'utf8')
const etag = `"${hash.digest('hex')}"`
console.log(etag, '---etag----')
// 对比hash值
if (ifMatch === etag) {
res.status = 304
} else {
res.header('etag', etag)
// ctx.body = fileBuffer
}
// 设置 Cache-Control 响应头
res.header('Cache-Control', 'no-cache')
res.render('login');
});


当资源发生改变时,状态码变成200,更新缓存


比如更改css样式



ETag也有缺点



  • ETag需要计算文件指纹这样意味着,服务端需要更多的计算开销。如果文件尺寸大,数量多,并且计算频繁,那么ETag的计算就会影响服务器的性能。显然,ETag在这样的场景下就不是很适合。

  • ETag有强验证和弱验证,所谓将强验证,ETag生成的哈希码深入到每个字节。哪怕文件中只有一个字节改变了,也会生成不同的哈希值,它可以保证文件内容绝对的不变。但是,强验证非常消耗计算量。ETag还有一个弱验证,弱验证是提取文件的部分属性来生成哈希值。因为不必精确到每个字节,所以他的整体速度会比强验证快,但是准确率不高。会降低协商缓存的有效性。


值得注意的一点是,不同于cache-control是expires的完全替代方案(说人话:能用cache-control就不要用expiress)。ETag并不是last-modified的完全替代方案。而是last-modified的补充方案(说人话:项目中到底是用ETag还是last-modified完全取决于业务场景,这两个没有谁更好谁更坏)。


disk cache & memory cache


磁盘缓存+内存缓存,这两种缓存不属于http缓存,而是本地缓存了~


我们直接打开掘金官网,点击network,类型选择all



可以看的很多请求,这里请求包括了静态资源+接口请求


这里我们能够看的很多请求的size中有很多是disk cache(磁盘缓存)


也有一些图片是memory cache(内存缓存)



这两者有什么区别呢?


disk cache: 磁盘缓存,很明显将内容存储在计算机硬盘中,很明显,这种缓存可以占用比较大的空间,但是由于是读取硬盘,所以速度低于内存


memory cache: 内存缓存,速度快,优先级高,但是大小受限于计算机的内存大小,很大的资源还是缓存到硬盘中


上面的浏览器缓存已经有三个大点了,那它们的优先级是什么样的呢?


缓存的获取顺序如下:


1.内存缓存


2.磁盘缓存


3.强缓存


4.协商缓存


如果勾选了Disable cache,那磁盘缓存都不存在了,之还有内存缓存



我还发现,勾选了Disable cache,就base64图片一定会在内存缓存中,其他图片则会发起请求;而不勾选了Disable cache,则大多数图片都在内存缓存中




CDN缓存


CDN缓存是一种服务端缓存,CDN服务商可以将源站上的资源缓到其各地的边缘服务器节点上。当用户访问该资源时,CDN再通过负载均衡将用户的请求调度到最近的缓存节点上,有效减少了链路回源,提高了资源访问效率及可用性,降低带宽消耗。


如果客户端没有命中缓存,那接下来就要发起一次网络请求,根据网络环境,一般大型站点都会配置CDN,CDN会找一个最合适的服务节点接管网络请求。CDN节点都会在本地缓存静态文件数据,一旦命中直接返回,不会穿透去请求应用服务器。并且CDN会通过在不同的网络,策略性地通过部署边缘服务器和应用大量的技术和算法,把用户的请求指定到最佳响应节点上。所以会减少非常多的网络开销和响应延迟。


如果没有部署CDN或者CDN没有命中,请求最终才会落入应用服务器,现在的http服务器都会添加一层反向代理,例如nginx,在这一层同样会添加缓存层,代表技术是squid,varnish,当然nginx作为http服务器而言也支持静态文件访问和本地缓存技术,当然也可以使用远程缓存,如redis,memcache,这里缓存的内容一般为静态文件或者由服务器已经生成好的动态页面,在返回用户之前缓存。


如果前面的缓存机制全部失效,请求才会落入真正的服务器节点。


总结


1.如果页面是协商缓存,如何获取页面最新内容?


协商缓存比较好办,那就刷新页面,不过需要勾选Disable cache,但是用户不知道打开控制台怎么办?


那就右击页面的刷新按钮,然后选择硬性重新加载,或者清空缓存并硬性重新加载,页面就获取到最新资源了



2.如果页面没有设置cache-control,那默认的缓存机制是什么样的?



默认是协商缓存,这也符合浏览器设计,缓存可以减少宽度流量,加快响应速度


3.如果项目重新部署还是没有更新,怎么办?


在确定项目已经部署成功


这样子,可以去问一下公司的运维同事,这个项目是否有CDN缓存


如果项目的域名做了CDN缓存,就需要刷新CDN目录,来解决缓存问题了,不然就只能等,等CDN策略失效,来请求最新的内容


向如下配置的缓存策略,只有过30天才会去真正服务器去请求最新内容



当然你可以测试一下是否为CDN缓存,在url后面拼接一个参数,就能够获取到最新资源了,比如有缓存的链接是baidu.com/abc


你可以在浏览器中输入baidu.com/abc&t=1234来…


当然特定场景,我们不能随意给链接后面添加参数,所以这也只适用于测试一下是否有CDN缓存


所以最好的解决办法还是需要让运维同事去刷新目录,这样就能快速解决CDN缓存问题。


参考链接


juejin.cn/post/712719…


juejin.cn/post/717756…


xiaolincoding.com/

network/2_h…

收起阅读 »

我也惊呆了!关于数字广东对于 CEC-IDE 重大事件的道歉声明网友解读

喜大普奔 8 月 21 日,在某 gov.cn 官网上有一篇文章作出以下报告: 国内首款适配国产操作系统、自主可控的集成开发环境工具 CEC-IDE;国内首款数据安全极限生存保障产品——数据安全守护软硬件一体化产品;国内首款国密指纹认证鼠标…… 在《喜大...
继续阅读 »

喜大普奔


8 月 21 日,在某 gov.cn 官网上有一篇文章作出以下报告:



国内首款适配国产操作系统、自主可控的集成开发环境工具 CEC-IDE;国内首款数据安全极限生存保障产品——数据安全守护软硬件一体化产品;国内首款国密指纹认证鼠标……



image.png


《喜大普奔:全新自主研发的超强 CEC-IDE ,打破国外垄断》一文中有简要叙述。


网友挖掘


8 月 24 日,众多网友经过文件分析并在 vscode 官方仓库创建了编号为 #191279 和 #191229 的 issues,引来网友在该帖进行大量讨论。大量证据都在表明 CEC-IDE 涉嫌造假。


image.png


8 月 25 日,CEC-IDE 官网已无法访问。


image.png


import * as fs from "fs-extra";

const sourceExePath = "path/to/vscode.exe";
const iconFilePath = "path/to/new-icon.ico";

const sourceExeBuffer = fs.readFileSync(sourceExePath);
const iconFileBuffer = fs.readFileSync(iconFilePath);

const targetExeBuffer = replaceIconData(sourceExeBuffer, iconFileBuffer);

fs.writeFileSync("path/to/output.exe", targetExeBuffer);

function replaceIconData(sourceBuffer: Buffer, iconBuffer: Buffer): Buffer {
const targetBuffer = sourceBuffer.clone();
const iconDataOffset = 0x1234;
targetBuffer.fill(
iconBuffer,
iconDataOffset,
iconDataOffset + iconBuffer.length
);

return targetBuffer;
}

众所周知,对于软件开发,立项人是谁,目标是什么,开发人员是谁,测试人员是谁,验收人员是谁,这些都是很清楚的。


致歉声明


8 月 26 日,官方公众号发表致歉声明,这应该也侧面证实了此事。


CEC-IDE 道歉声明.png


声明解读



  • 8 月 24 日晚,我司获悉有网友发帖讨论我司 CEC-IDE 系统


在 8 月 21 日时各网络和电视媒体已进行 CEC-IDE 的宣传报告,表明在 21 日前按正常的开发流程来说,系统已经过测试、发布上线、验收。而我司获悉时是在 24 日,此前那么多时间都在做什么?流程都在做什么?众所周知,此类项目要走的时间和流程都是挺多的。



  • 公司管理层高度重视


从这个事件来看,至少在 26 日前是没有重视的。在 26 日后是不是真的重视?如何体现高度、体现重视,要采取什么样的措施,达到什么样的效果,只字未提。可能是保密调查?



  • 诚恳接受网友批评,并认真开展核查


image.png


诚恳接受网友批评这几个文字与未开启评论区形成鲜明对比。要知道发表当天 6 小时不到就有 6 万人阅读,但 0 评论。



  • CEC-IDE 系统由开发工具、后端系统和组件库组成...


作为一份致歉声明,有近 1/4 的内容是在讲述“列举工作”。



CEC-IDE 系统由开发工具、后端系统和组件库组成,其中开发工具使用开源 VSCode,进行了少量改造,增加了部分功能,后端系统开发了用户、权限、项目、需求等管理,以及任务协作和知识共享等功能,组件库中开发了公共能力组件。




  • 未用于商业用途


今年7月投入试运行,目前仍处在探索阶段,未用于商业用途。意思是不是在说:我们也才刚开始做就被发现了,所以问题不大。而且我们真的没有用于商业用途!但程序截图上的VIP登录和标志显得额外耀眼。


image.png



  • 因版本迭代更新中出现疏忽,近几个版本中缺失了 MIT 协议文件


出现疏忽导致近几个版本缺失 MIT 协议文件,疏忽一词避重就轻,表示我们只是不小心。但大家都知道从 近几个版本中缺失 来看,以前是有此文件(因为原仓库就有此文件)的,只是后面的版本中都被赤裸裸的删除了。


import * as fs from "fs-extra";
import * as yauzl from "yauzl";

const sourceExePath = "path/to/vscode.exe";
const targetExePath = "path/to/output.exe";
const mitLicenseText = "MIT License"; // 要删除的MIT协议文本

fs.copyFileSync(sourceExePath, targetExePath);

yauzl.open(targetExePath, { lazyEntries: true }, (error, zipfile) => {
if (error) throw error;

zipfile.readEntry();

zipfile.on("entry", (entry) => {
if (/\/$/.test(entry.fileName)) {
// 目录项,继续读取下一个entry
zipfile.readEntry();
} else {
// 文件项,处理文件内容
zipfile.openReadStream(entry, (error, readStream) => {
if (error) throw error;

let data = "";
readStream.on("data", (chunk) => {
data += chunk.toString("utf-8");
});

readStream.on("end", () => {
const updatedData = data.replace(mitLicenseText, "");
const writeStream = fs.createWriteStream(entry.fileName);
writeStream.write(updatedData, "utf-8");
writeStream.end();

zipfile.readEntry();
});
});
}
});

zipfile.on("end", () => {
console.log("MIT license removed successfully!");
});
});


  • 产品表述中“自主研发”等用语被网友质疑


被质疑,被 XX。等一系列的词,总让人有一种不能内省的感觉。而“自主研发”此类词语根本就不是单纯的自不自主那么简单。担忧从来不是自不自主开发,而是自信的磨灭、情怀的磨灭。



  • 数字广东公司向所有开源贡献者致以衷心



开源软件的使用极大提升了我司产品研发效率,开源项目为我司提供了巨大帮助,开源精神是程序员共同的同心圆,数字广东公司向所有开源贡献者致以衷心的感谢和崇高的敬意。



广大开源者可能不专门需要此敬意。但对 VSCODE 开发组应有,对默默真正投入自主研发的人应有此敬意。


相关链接


收起阅读 »

Android 时钟翻页效果

web
背景 今天逛掘金,发现了一个有意思的web view效果,想着Android能不能实现一下捏。 原文链接:juejin.cn/post/724435… 具体实现分析请看上文原文链接,那我们开始吧! 容器 val space = 10f //上下半间隔 val...
继续阅读 »

背景


今天逛掘金,发现了一个有意思的web view效果,想着Android能不能实现一下捏。


image.png
原文链接:juejin.cn/post/724435…


具体实现分析请看上文原文链接,那我们开始吧!


容器


val space = 10f //上下半间隔
val bgBorderR = 10f //背景圆角
//上半部分
val upperHalfBottom = height.toFloat() / 2 - space / 2
canvas.drawRoundRect(
0f,
0f,
width.toFloat(),
upperHalfBottom,
bgBorderR,
bgBorderR,
bgPaint
)
//下半部分
val lowerHalfTop = height.toFloat() / 2 + space / 2
canvas.drawRoundRect(
0f,
lowerHalfTop,
width.toFloat(),
height.toFloat(),
bgBorderR,
bgBorderR,
bgPaint
)

image.png


绘制数字


我们首先居中绘制数字4


val number4 = "4"
textPaint.getTextBounds(number4, 0, number4.length, textBounds)
//居中显示
val x = (width - textBounds.width()) / 2f - textBounds.left
val y = (height + textBounds.height()) / 2f - textBounds.bottom
canvas.drawText(number4, x, y, textPaint)

image.png


接下来我们将数字切分为上下两部分,分别绘制。


val number4 = "4"
textPaint.getTextBounds(number4, 0, number4.length, textBounds)
val x = (width - textBounds.width()) / 2f - textBounds.left
val y = (height + textBounds.height()) / 2f - textBounds.bottom
// 上半部分裁剪
canvas.save()
canvas.clipRect(
0f,
0f,
width.toFloat(),
upperHalfBottom
)
canvas.drawText(number4, x, y, textPaint)
canvas.restore()
// 下半部分裁剪
canvas.save()
canvas.clipRect(
0f,
lowerHalfTop,
width.toFloat(),
height.toFloat()
)
canvas.drawText(number4, x, y, textPaint)

image.png


翻转卡片


如何实现让其旋转呢?
而且还得是3d的效果了。我们选择Camera来实现。
我们先让数字'4'旋转起来。


准备工作,通过属性动画来改变旋转的角度。


private var degree = 0f //翻转角度
private val camera = Camera()
private var flipping = false //是否处于翻转状态
...
//动画
val animator = ValueAnimator.ofFloat(0f, 360f)
animator.addUpdateListener { animation ->
val animatedValue = animation.animatedValue as Float
setDegree(animatedValue)
}
animator.doOnStart {
flipping = true
}
animator.doOnEnd {
flipping = false
}
animator.duration = 1000
animator.interpolator = LinearInterpolator()
animator.start()
...

private fun setDegree(degree: Float) {
this.degree = degree
invalidate()
}

让数字'4'旋转起来:


  override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// 居中绘制数字4
val number4 = "4"
textPaint.getTextBounds(number4, 0, number4.length, textBounds)
val x = (width - textBounds.width()) / 2f - textBounds.left
val y = (height + textBounds.height()) / 2f - textBounds.bottom

if (!flipping) {
canvas.drawText(number4, x, y, textPaint)
} else {
camera.save()
canvas.translate(width / 2f, height / 2f)
camera.rotateX(-degree)
camera.applyToCanvas(canvas)
canvas.translate(-width / 2f, -height / 2f)
camera.restore()
canvas.drawText(number4, x, y, textPaint)
}
}

file.gif

我们再来看一边效果图:
我们希望将卡片旋转180度,并且0度-90度由上半部分完成,90度-180度由下半部分完成。


我们调整一下代码,先处理一下上半部分:


...
val animator = ValueAnimator.ofFloat(0f, 180f)
...
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val space = 10f //上下半间隔
//上半部分
val upperHalfBottom = height.toFloat() / 2 - space / 2
...
// 居中绘制数字4
val number4 = "4"
textPaint.getTextBounds(number4, 0, number4.length, textBounds)
val x = (width - textBounds.width()) / 2f - textBounds.left
val y = (height + textBounds.height()) / 2f - textBounds.bottom

if (!flipping) {
//上半部分裁剪
canvas.save()
canvas.clipRect(
0f,
0f,
width.toFloat(),
upperHalfBottom
)
canvas.drawText(number4, x, y, textPaint)
canvas.restore()
} else {
if (degree < 90) {
//上半部分裁剪
canvas.save()
canvas.clipRect(
0f,
0f,
width.toFloat(),
upperHalfBottom
)
camera.save()
canvas.translate(width / 2f, height / 2f)
camera.rotateX(-degree)
camera.applyToCanvas(canvas)
canvas.translate(-width / 2f, -height / 2f)
camera.restore()
canvas.drawText(number4, x, y, textPaint)
canvas.restore()
}
}
}

效果如下:


upper.gif

接下来我们再来看一下下半部分:


override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val space = 10f //上下半间隔
//下半部分
val lowerHalfTop = height.toFloat() / 2 + space / 2

// 居中绘制数字4
val number4 = "4"
textPaint.getTextBounds(number4, 0, number4.length, textBounds)
val x = (width - textBounds.width()) / 2f - textBounds.left
val y = (height + textBounds.height()) / 2f - textBounds.bottom

if (!flipping) {
// 下半部分裁剪
canvas.save()
canvas.clipRect(
0f,
lowerHalfTop,
width.toFloat(),
height.toFloat()
)
canvas.drawText(number4, x, y, textPaint)
canvas.restore()
} else {
if (degree > 90) {
canvas.save()
canvas.clipRect(
0f,
lowerHalfTop,
width.toFloat(),
height.toFloat()
)
camera.save()
canvas.translate(width / 2f, height / 2f)
val bottomDegree = 180 - degree
camera.rotateX(bottomDegree)
camera.applyToCanvas(canvas)
canvas.translate(-width / 2f, -height / 2f)
camera.restore()
canvas.drawText(number4, x, y, textPaint)
canvas.restore()
}
}
}

lower.gif

那我们将上下部分结合起来,效果如下:


all.gif

数字变化


好!我们完成了翻转部分,现在需要在翻转的过程中将数字改变:

我们还是举例说明:数字由'4'变为'5'的情况。我们思考个问题,什么时候需要改变数字?

上半部分在翻转开始的时候,上半部分底部显示的数字就应该由'4'变为'5',但是旋转的部分还是应该为'4',
下半部分开始旋转的时候底部显示的数字还是应该为'4',而旋转的部分该为'5'。


canvas.save()
canvas.clipRect(
0f,
0f,
width.toFloat(),
upperHalfBottom
)
canvas.drawText(number5, x, y, textPaint)
canvas.restore()
// 下半部分裁剪
canvas.save()
canvas.clipRect(
0f,
lowerHalfTop,
width.toFloat(),
height.toFloat()
)
canvas.drawText(number4, x, y, textPaint)
canvas.restore()
//=====⬆️=====上述的代码显示的上下底部显示的内容,即上半部分地步显示5,下半部分显示4
if (degree < 90) {
//上半部分裁剪
canvas.save()
canvas.clipRect(
0f,
0f,
width.toFloat(),
upperHalfBottom
)
camera.save()
canvas.translate(width / 2f, height / 2f)
camera.rotateX(-degree)
camera.applyToCanvas(canvas)
canvas.translate(-width / 2f, -height / 2f)
camera.restore()
canvas.drawText(number4, x, y, textPaint)
canvas.restore()
//=====⬆️=====上述的代码表示上半部分旋转显示的内容,即数字4
} else {
canvas.save()
canvas.clipRect(
0f,
lowerHalfTop,
width.toFloat(),
height.toFloat()
)
camera.save()
canvas.translate(width / 2f, height / 2f)
val bottomDegree = 180 - degree
camera.rotateX(bottomDegree)
camera.applyToCanvas(canvas)
canvas.translate(-width / 2f, -height / 2f)
camera.restore()
canvas.drawText(number5, x, y, textPaint)
canvas.restore()
//=====⬆️=====上述的代码表示下半部分旋转显示的内容,即数字5
}

效果图如下:大伙可以在去理一下上面数字的变化的逻辑。


a.gif

最后我们加上背景再看一下效果:


a.gif

小结


上述代码仅仅提供个思路,仅为测试code,正式代码可不

作者:蹦蹦蹦
来源:juejin.cn/post/7271518821809438781
能这么写哦 >..<

收起阅读 »

程序员应避免自我安慰式的无效学习

重复学习了很多年 从事前端开发已经超过5年,自诩也是一个坚持学习的程序猿。 今年工作不是很忙,并且职场的35岁槛已经到来,既有时间又有压力,于是更多的时间在思考成长这件事。 最近在做的一件特别重要的事情是:做减法。 从事开发的这些年,因为待过的公司不同,后端的...
继续阅读 »

重复学习了很多年


从事前端开发已经超过5年,自诩也是一个坚持学习的程序猿。
今年工作不是很忙,并且职场的35岁槛已经到来,既有时间又有压力,于是更多的时间在思考成长这件事。


最近在做的一件特别重要的事情是:做减法。


从事开发的这些年,因为待过的公司不同,后端的语言不同,业务不同,加上给自己制定每年都要学习一门有价值的课程这样一个目标。先后学习了C#,PHP,JAVA。我这可不是走马观花式的学习,我是要求自己学习后至少能够使用相应语言的框架做简单基础开发。结果是除了C#外,我学会了PHP的Yii做后端开发,学会了Springboot做开发,虽然仅仅是常规的开发,但走过了从0-1的过程。


当然除了后端语言,前端技术栈从Vue、react、微信小程序、RN开发、Nodejs都有涉及,且都能进行日常开发。当然这里面最熟练还是Vue还有nodejs。后来我觉得做前端就得做全套,又花钱专门学了android app开发。虽然android平时不会涉及,学习的具体时间也是3年前了,但是也度过了从0-1的阶段。


学习了这些知识点,最大的一个结果是有道笔记我的知识笔记里面记录了大量的笔记。


学了这么多,照理说我应该对自己很有信心。但扪心自问我没有,我感觉我自己始终找不到让我特别自信的点,我想做自己的产品,但是始终没有做成。有段时间,我一直很迷茫。


现在回忆起来大概是因为我读了一本书《财富自由之路》,至于具体哪段内容我忘记了,反正我后来开始做减法,多个方面做减法,如下



  1. 收拾买的书籍,常用的放在明面上,不常用的收藏起来

  2. 不再买书,因为我发现我其实有大量的书只是看了开头

  3. 整理电脑桌面和文件夹,尤其整理做过的大量开发练习,分门别类并删除大量早期的和无用的

  4. 整理手机桌面和文件夹,手机从4屏变为2屏

  5. 整理浏览器的书签栏,分门别类

  6. 整理关注的股票,整理自选分类,坚决去掉自己不熟悉的,最后只留下不到10只

  7. 整理有道笔记里面笔记:共删除150多篇,重新划分目录


这里面感触最深的是整理有道笔记。我发现很多知识点我学了一遍又一遍,记了一次又一次,我每一年都会起很多诸如JavaScript学习笔记,Vue学习笔记,nodejs学习笔记等标题的笔记,但工作内容并没有特别大的变化,以前记住的知识点因为不经常温习和使用被忘掉,再次用到时候我会重新搜索出来然后再次记录。就这样反复着向前。


然而这样存在一个很大的问题:我在原地踏步。这个词很形象的形容了我的状况,看似学习了:记了笔记,但实际上根本没有进步,都是自我安慰,是对年龄带来的焦虑的缓解,是对社会给予的压力的缓解。


做减法之后,我想到盛传已久的一句话:太阳底下没有新鲜事。学习同样如此,任何学科都是有边界的。有边界意味着边界里面一定是在重复着某些知识点。只要找到这些知识点,总结这些知识点,迭代这些知识点,就可以避免重复无效的学习,进而真正进步。


划分知识结构


划分的原则:同一级不可以超过5个分类,因为人同时管理好的数量上限是5个左右。下面是一部分划分截图


开发技术.png


之后就是对最下级分类内容的填充和迭代。我是从2018年开始做的笔记。划分分类之后,我开始整理过去五年多的笔记,将笔记当中有用的属于对应分类的内容拿出来,填充到对应部分,同时删除原来的笔记。


我也将日常做了划分:


日常笔记.png


日常工作主要是一些日常的记录。日常分类和上述的开发技术,同属一个级别。都归属于我的文件夹下。我的文件夹:


我的文件夹.png


毛主席说过:好脑筋不如烂笔头。笔记的好处就是拓展思维的里程。


当然就划分来说,每个人的经历和认知是不同的,不同人有不同的划分标准。但是我觉得这不是重要的,重要的是聚焦注意力,最重要的是找到自己的世界,找到自己的内生动力。


找到自己的世界


刘青云出演的电视剧《大时代》有台词:一个人要成功,就一定要找到自己的世界。


猫腻的《择天记》男主有这样一句话:我修的是顺心意。这个时代谁修的不是顺心意呢?只有找到自己的世界顺自己的心意才能真正登堂入室,避免无效

作者:通往自由之路
来源:juejin.cn/post/7270906612339884093
的学习。因为此时才真正有了动力。

收起阅读 »

人情世故职场社会生存实战篇(三)

人情人情世故职场社会生存实战篇(一)人情人情世故职场社会生存实战篇(二) 21、问:我是做装饰行业的,我在多年前认识一个朋友,他呢混的比较好。经常给我介绍一些装修活儿,几十万的,一百多万的都交给我做。对我像自己人一样。我们萍水相逢 真的很感激他。因为我的不善...
继续阅读 »

人情人情世故职场社会生存实战篇(一)

人情人情世故职场社会生存实战篇(二)



21、问:我是做装饰行业的,我在多年前认识一个朋友,他呢混的比较好。经常给我介绍一些装修活儿,几十万的,一百多万的都交给我做。对我像自己人一样。我们萍水相逢 真的很感激他。因为我的不善交际,我们的来往并不多。想请教一下,在日常交往中,我该怎样做,以表现对他的感谢,以及怎样送礼给他,毕竟人家什么都不缺。


答:你认为富人不缺钱,这都是假象,富人的开销也是很大的。加油不要钱吗,物业不要钱吗,孩子读书不要钱吗,培训班不要钱吗,他们比我们这些屌丝更需要钱。因为我们一个月可能几千块就搞定了,他们一个月可能几万都不够。


你能给他添砖加瓦,习惯性的给他反馈,他也会觉得你是同道中人,是理解他的。王宝强搞定冯小刚,是因为他每年都给冯小刚送米送面。不要觉得没反馈,也可以获得资源,这会让你的路越走越窄。


反馈:1、你帮我介绍业务了,你就是我的大恩人,以后你的事儿就是我的事儿,需要我干啥您尽管吩咐。


2、我这个业务虽然本小利微,你可能也看不上,但是你确实帮我太多了,这点东西不成敬意,你一定要收下,不然我每晚每晚睡不着。


3、我们萍水相逢,能认识您,结交您是我的福气。您放心,只要您介绍的业务,我会百分百的让客户满意,亏钱也无所谓,不能坏了您的名声。(私人薇:xxxx)


22、问:明天晚上我们老板和我现任直接领导都在 ,我只能敬酒一次,怎么说比较好?既表达了对老板的敬意,又不得罪现任领导(老板,是以前的老领导)。


答:1、他们俩不在一个桌,你要先敬你的老板。


2、他俩要是在一个桌,你要对着领导介绍你原来的领导,说他多厉害多厉害。这个多厉害,用一句话说:以前我什么都听他的 ,现在他把我培养出来了。现在我又成了您的兵。一个是我的导师,一个是我的将军,我干了,您们二老随意。然后收摊就行了。


23、问:我是一名管理者。请问对于有本事,有才华,但名声不好的人怎么管理好呢?


答:看过水浒传吗,这样的人就是水浒传里的时迁,宋江怎么对时迁,你就怎么对他。给待遇,但不给级别。就是厚而不尊,因为时迁是个贼,所以不能把他排进领导班子,但是他贡献大,宋江给他丰厚的待遇和奖励,但不提拔他,这叫厚而不尊。你不厚,他不给你干;你要尊,队伍的名气就坏了。虽然时迁排名很靠后,不受尊重,没地位,但是时迁的个人待遇、工资奖金水平都很高。


24、问:我们处内有个女孩,小我10岁左右,她想提干,但是她没有得到上级认可,业务水平一般,但是家里有一定背景。她看到我要被提拔,就很嫉妒,拉拢处里另外一个刚入职不久的女孩, 孤立我。我现在必须要处理好同事关系,真要提拔,也要找处里同事谈话,不能出现反对的声音。现在我该怎么办?怎么和她相处呢?


答:你平时跟领导搞好关系,这事领导说了算,她说了不算,她那里你就示弱,在新来的那个姑娘那里多说她的好话,夸她能力强,情商高,说你们以前的一些事情,就是夸她,慢慢她就不好意思了,如果她再说你的坏话,新来的姑娘都鄙视她。


25、问:我们领导以前是财政局的,现在让我们每个口子都报表,因为我们下属也不只归我们管,所以他有气,我就成了夹板,下属公司这个月的报表已经报了5次了,他都不满意,每次都发火,而且每次标准不一样。怎么办?


答:先去拜师学艺,问问他标准是什么。按照他的标准去做,自然通过。买一条烟,去办公室请教∶领导,我这个人啊就是愚笨,还请您多指导指导,这个表到底是哪里不对,您指导一次我记住了,以后就按这个标准了,也不再惹您生气了,您指点指点我。


26、问:我们单位一把手要调走,我算是在他在位期间招入的和提起来的,去他办公室,如何表达感恩更完美?因为突然,我也没准备什么礼物,还有什么补救措施吗?


答:3个点,感恩+愧疚


1、没有你,就没有我的今天……


2、吃水不忘挖井人,我能帮你做点儿什么,我一定帮你去做……


3、我哪儿做的不对,你一定要说,我比较笨……


27、问:我是一个部门副职,遇到了这么个情况,我刚来任职,需要从下面的人那里获取数据信息,我才能开展我接下来的工作,然总是不给我主动汇报,总是要我催他们,怎么办呢?


答:你就问他们一个问题,就是上一届领导是怎么带他的,那么你就怎么带他们。也就是说你不要变动性太大,你要是变动性太大的话,大家都不屌你。最好的交接方式,大家问你问题的时候,你说以前你们怎么弄的,现在还怎么弄,我什么也不懂,我听大家的。那么这个问题就很快就解决了。新官上任三把火,第一把火,请员工吃饭,第二把火,表扬员工,第三个,你说我会为你们争取利益的。先把局面打开就OK了。


28、问:今天早上领导安排我去做工作份外的事,我就说我现在有事情做只做了一半,我就说我不去,领导就说叫你做什么就做什么,不做就叫我马上滚蛋,可是想一想,好我去做,我做了一上午才做完,问下我的做法有问题吗,接下来该怎么操作?


答:领导安排任务的时候不要拒绝,否则就是给领导难堪,先接受,对领导说:好的领导。你要是手头有事,可以问领导:领导,我现在手头上还有点事,那我现在是先做 A 还是先做 B,把皮球踢给老板, 领导如果问:你想做哪个?你说:我服从领导的安排。这样就是不得罪领导,自己做的慢也不受罚。你现在得罪了领导,领导生气了,去道歉就可以,说自己年轻不懂事,以后有不对的地方还请领导多多指教。


29、问:我们领导分配给我一个辅警辅助我工作,有时候他会和我使小脾气,他好像就是这性格习惯了,我有工作给他做,他不愿意做或者做的不仔细,我还得给他擦屁股。有时候我就不给他布置任务,我去跟领导说我忙不过来,某项工作给他做,然后他才会好好做。其实我特别想跟领导打他小报告,但是忍了。这该怎么办呢?


答:这个好办啊,你每天把工作分工,简单做个记录,你负责什么他负责什么。早上上班你跟他说:领导让我去汇报咱两的工作分工,您看一下没问题吧?你们两商量完了,写在本子上,然后你去跟领导请示汇报:领导,这是我们今天一天的工作,您还有没有其他的指示?回来你跟搭档说:领导说分工很好,晚上汇报完成情况。


30、问:老师 我最近走上了管理岗位。但是在人情世故方面还是有些胆怯。举个例子:被提拔了想五一想拜访领导,总是怕被拒绝,感觉踩不到领导在家的空闲点,时机如何把握呢?


答:这个太简单了,他的司机,你请了吗?他的秘书,你请了吗?先搞定他身边的人,让他身边的人帮你搞定领导,你+领导的司机+领导的秘书=你的队伍。你们3个人吃领导一个人,别心疼在小人物身上花钱,小人物有时候比大人物更有价值...

作者:公z号_纵横潜规则
来源:juejin.cn/post/7268260762401095699

收起阅读 »

虚拟列表 or 时间分片

前言 最近在做一个官网,原本接口做的都是分页的,但是客户提出不要分页,之前看过虚拟列表这个东西,所以进行一下了解。 为啥要用虚拟列表呢! 在日常工作中,所要渲染的也不单单只是一个li那么简单,会有很多嵌套在里面。但数据量过多,同时渲染式,会在 渲染样式 跟 布...
继续阅读 »

前言


最近在做一个官网,原本接口做的都是分页的,但是客户提出不要分页,之前看过虚拟列表这个东西,所以进行一下了解。


为啥要用虚拟列表呢!


在日常工作中,所要渲染的也不单单只是一个li那么简单,会有很多嵌套在里面。但数据量过多,同时渲染式,会在 渲染样式 跟 布局计算上花费太多时间,体验感不好,那你说要不要优化嘛,不是你被优化就是你优化它。


进入正题,啥是虚拟列表?


可以这么理解,根据你视图能显示多少就先渲染多少,对看不到的地方采取不渲染或者部分渲染。




这时候你完成首次加载,那么其他就是在你滑动时渲染,就可以通过计算,得知此时屏幕应该显示的列表项。


怎么弄?


备注:很多方案对于动态不固定高度、网络图片以及用户异常操作等形式处理的也并不好,了解下原理即可。


虚拟列表的实现,实际上就是在首屏加载的时候,只加载可视区域内需要的列表项,当滚动发生时,动态通过计算获得可视区域内的列表项,并将非可视区域内存在的列表项删除。


1、计算当前可视区域起始数据索引(startIndex)

2、计算当前可视区域结束数据索引(endIndex)

3、计算当前可视区域的数据,并渲染到页面中

4、计算startIndex对应的数据在整个列表中的偏移位置startOffset并设置到列表上


由于只是对可视区域内的列表项进行渲染,所以为了保持列表容器的高度并可正常的触发滚动,将Html结构设计成如下结构:

<div class="infinite-list-container">
<div class="infinite-list-phantom"></div>
<div class="infinite-list">
<!-- item-1 -->
<!-- item-2 -->
<!-- ...... -->
<!-- item-n -->
</div>
</div>


  • infinite-list-container 为可视区域的容器

  • infinite-list-phantom 为容器内的占位,高度为总列表高度,用于形成滚动条

  • infinite-list 为列表项的渲染区域

    接着,监听infinite-list-containerscroll事件,获取滚动位置scrollTop

  • 假定可视区域高度固定,称之为screenHeight

  • 假定列表每项高度固定,称之为itemSize

  • 假定列表数据称之为listData

  • 假定当前滚动位置称之为scrollTop

  •   则可推算出:

    • 列表总高度listHeight = listData.length * itemSize
    • 可显示的列表项数visibleCount = Math.ceil(screenHeight / itemSize)
    • 数据的起始索引startIndex = Math.floor(scrollTop / itemSize)
    • 数据的结束索引endIndex = startIndex + visibleCount
    • 列表显示数据为visibleData = listData.slice(startIndex,endIndex)

      当滚动后,由于渲染区域相对于可视区域已经发生了偏移,此时我需要获取一个偏移量startOffset,通过样式控制将渲染区域偏移至可视区域中。

    • 偏移量startOffset = scrollTop - (scrollTop % itemSize);

时间分片


那么虚拟列表是一方面可以优化的方式,另一个就是时间分片。


先看看我们平时的情况


1.直接开整,直接渲染。




诶???我们可以发现,js运行时间为113ms,但最终 完成时间是 1070ms,一共是 js 运行时间加上渲染总时间。

PS:

  • 在 JS 的 EventLoop中,当JS引擎所管理的执行栈中的事件以及所有微任务事件全部执行完后,才会触发渲染线程对页面进行渲染
  • 第一个 console.log的触发时间是在页面进行渲染之前,此时得到的间隔时间为JS运行所需要的时间
  • 第二个 console.log是放到 setTimeout 中的,它的触发时间是在渲染完成,在下一次 EventLoop中执行的

那我们改用定时器


上面看是因为我们同时渲染,那我们可以分批看看。

let once = 30
let ul = document.getElementById('testTime')
function loopRender (curTotal, curIndex) {
if (curTotal <= 0) return
let pageCount = Math.min(curTotal, once) // 每页最多20条
setTimeout(_ => {
for (let i=0; i<pageCount;i++) {
let li = document.createElement('li')
li.innerHTML = curIndex + i
ul.appendChild(li)
}
loopRender(curTotal - pageCount, curIndex + pageCount)
}, 0)
}
loopRender(100000, 0)

这时候可以感觉出来渲染很快,但是如果渲染复杂点的dom会闪屏,为什么会闪屏这就需要清楚电脑刷新的概念了,这里就不详细写了,有兴趣的小朋友可以自己去了解一下。

可以改用 requestAnimationFrame 去分批渲染,因为这个关于电脑自身刷新效率的,不管你代码的事,可以解决丢帧问题。

let once = 30
let ul = document.getElementById('container')
// 循环加载渲染数据
function loopRender (curTotal, curIndex) {
if (curTotal <= 0) return
let pageCount = Math.min(curTotal, once) // 每页最多20条
window.requestAnimationFrame(_ => {
for (let i=0; i<pageCount;i++) {
let li = document.createElement('li')
li.innerHTML = curIndex + i
ul.appendChild(li)
}
loopRender(curTotal - pageCount, curIndex + pageCount)
})
}
loopRender(100000, 0)

还可以改用 DocumentFragment


什么是 DocumentFragment



DocumentFragment,文档片段接口,表示一个没有父级文件的最小文档对象。它被作为一个轻量版的 Document使用,用于存储已排好版的或尚未打理好格式的XML片段。最大的区别是因为 DocumentFragment不是真实DOM树的一部分,它的变化不会触发DOM树的(重新渲染) ,且不会导致性能等问题。

可以使用 document.createDocumentFragment方法或者构造函数来创建一个空的 DocumentFragment

ocumentFragments是DOM节点,但并不是DOM树的一部分,可以认为是存在内存中的,所以将子元素插入到文档片段时不会引起页面回流。



当 append元素到 document中时,被 append进去的元素的样式表的计算是同步发生的,此时调用 getComputedStyle 可以得到样式的计算值。而 append元素到 documentFragment 中时,是不会计算元素的样式表,所以 documentFragment 性能更优。当然现在浏览器的优化已经做的很好了, 当 append元素到 document中后,没有访问 getComputedStyle 之类的方法时,现代浏览器也可以把样式表的计算推迟到脚本执行之后。

let once = 30 
let ul = document.getElementById('container')
// 循环加载渲染数据
function loopRender (curTotal, curIndex) {
if (curTotal <= 0) return
let pageCount = Math.min(curTotal, once) // 每页最多20条
window.requestAnimationFrame(_ => {
let fragment = document.createDocumentFragment()
for (let i=0; i<pageCount;i++) {
let li = document.createElement('li')
li.innerHTML = curIndex + i
fragment.appendChild(li)
}
ul.appendChild(fragment)
loopRender(curTotal - pageCount, curIndex + pageCount)
})
}
loopRender(100000, 0)

其实同时渲染十万条数据这个情况还是比较少见的,就当做个了解吧。


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

iOS小技能: 抽奖轮盘跑马灯边框的实现

iOS
携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第11天,点击查看活动详情 前言 跑马灯的应用场景:iOS 抽奖轮盘边框动画 原理: 用NSTimer无限替换背景图片1和背景图片2,达到跑马灯的效果 - (void)touchesBega...
继续阅读 »

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第11天,点击查看活动详情


前言


跑马灯的应用场景:

  1. iOS 抽奖轮盘边框动画


原理: 用NSTimer无限替换背景图片1和背景图片2,达到跑马灯的效果


- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

[self rotate:_rotaryTable];

}

/**

iOS翻牌效果

*/
- (void)rotate:(id)sender {

[UIView beginAnimations:@"View Filp" context:nil];
[UIView setAnimationDelay:0.25];
[UIView setAnimationCurve:UIViewAnimationCurveLinear];
[UIView setAnimationTransition:UIViewAnimationTransitionFlipFromLeft forView:sender
cache:NO];
[UIView commitAnimations];

}


2. 在待办界面或者工作台界面,往往需要应用到跑马灯的地方


原理:利用QMUIMarqueeLabel 进行cell封装简易的跑马灯 label 控件


文章:kunnan.blog.csdn.net/article/det…





如用户登陆未绑定手机号,进行提示。



简易的跑马灯 label 控件,在文字超过 label 可视区域时会自动开启跑马灯效果展示文字,文字滚动时是首尾连接的效果 



I iOS 抽奖轮盘边框动画


1.1 原理


用NSTimer无限替换UIImageView的Image为互为错位的bg_horse_race_lamp_1或者bg_horse_race_lamp_2,达到跑马灯的效果



应用场景: iOS 抽奖轮盘边框动画



审核注意事项:



  1. 在抽奖页面添加一句文案“本活动与苹果公司无关”
    2, 在提交审核时修改分级至17+



1.2 实现代码

//
// ViewController.m
// horse_race_lamp
//
// Created by mac on 2021/4/7.
#import <Masonry/Masonry.h>


#import "ViewController.h"
NSString *const bg_horse_race_lamp_1=@"bg_horse_race_lamp_1";
NSString *const bg_horse_race_lamp_2=@"bg_horse_race_lamp_2";

@interface ViewController ()
/**

用NSTimer无限替换bg_horse_race_lamp_1和bg_horse_race_lamp_2,达到跑马灯的效果

应用场景: iOS 抽奖轮盘边框动画
*/
@property (nonatomic,strong) UIImageView *rotaryTable;
@property (nonatomic,strong) NSTimer *itemBordeTImer;
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.


//通过以下两张图片bg_lamp_1 bg_lamp_2,用NSTimer无限替换,达到跑马灯的效果
_rotaryTable = [UIImageView new];
_rotaryTable.tag = 100;

[_rotaryTable setImage:[UIImage imageNamed:bg_horse_race_lamp_1]];

[self.view addSubview:_rotaryTable];

[_rotaryTable mas_makeConstraints:^(MASConstraintMaker *make) {

make.center.offset(0);

}];



_itemBordeTImer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(itemBordeTImerEvent) userInfo:nil repeats:YES];


[[NSRunLoop currentRunLoop] addTimer:_itemBordeTImer forMode:NSRunLoopCommonModes];







}
// 边框动画
- (void)itemBordeTImerEvent
{
if (_rotaryTable.tag == 100) {
_rotaryTable.tag = 101;
[_rotaryTable setImage:[UIImage imageNamed:bg_horse_race_lamp_2]];
}else if (_rotaryTable.tag == 101){
_rotaryTable.tag = 100;
[_rotaryTable setImage:[UIImage imageNamed:bg_horse_race_lamp_1]];
}
}




@end


1.3 下载Demo


从CSDN下载Demo:https://download.csdn.net/download/u011018979/16543761



private :https://github.com/zhangkn/horse_race_lamp


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

搞明白什么是零拷贝,就是这么简单

我们总会在各种地方看到零拷贝,那零拷贝到底是个什么东西。 接下来,让我们来理一理啊。 拷贝说的是计算机里的 I/O 操作,也就是数据的读写操作。计算机可是一个复杂的家伙,包括软件和硬件两大部分,软件主要指操作系统、驱动程序和应用程序。硬件那就多了,CPU、内存...
继续阅读 »

我们总会在各种地方看到零拷贝,那零拷贝到底是个什么东西。


接下来,让我们来理一理啊。


拷贝说的是计算机里的 I/O 操作,也就是数据的读写操作。计算机可是一个复杂的家伙,包括软件和硬件两大部分,软件主要指操作系统、驱动程序和应用程序。硬件那就多了,CPU、内存、硬盘等等一大堆东西。


这么复杂的设备要进行读写操作,其中繁琐和复杂程度可想而知。


传统I/O的读写过程


如果要了解零拷贝,那就必须要知道一般情况下,计算机是如何读写数据的,我把这种情况称为传统 I/O。


数据读写的发起者是计算机中的应用程序,比如我们常用的浏览器、办公软件、音视频软件等。


而数据的来源呢,一般是硬盘、外部存储设备或者是网络套接字(也就是网络上的数据通过网口+网卡的处理)。


过程本来是很复杂的,所以大学课程里要通过《操作系统》、《计算机组成原理》来专门讲计算机的软硬件。


简化版读操作流程


那么细的没办法讲来,所以,我们把这个读写过程简化一下,忽略大多数细节,只讲流程。



上图是应用程序进行一次读操作的过程。

  1. 应用程序先发起读操作,准备读取数据了;
  2. 内核将数据从硬盘或外部存储读取到内核缓冲区;
  3. 内核将数据从内核缓冲区拷贝到用户缓冲区;
  4. 应用程序读取用户缓冲区的数据进行处理加工;

详细的读写操作流程


下面是一个更详细的 I/O 读写过程。这个图可好用极了,我会借助这个图来厘清 I/O 操作的一些基础但非常重要的概念。



先看一下这个图,上面红粉色部分是读操作,下面蓝色部分是写操作。


如果一下子看着有点儿迷糊的话,没关系,看看下面几个概念就清楚了。


应用程序


就是安装在操作系统上的各种应用。


系统内核


系统内核是一些列计算机的核心资源的集合,不仅包括CPU、总线这些硬件设备,也包括进程管理、文件管理、内存管理、设备驱动、系统调用等一些列功能。


外部存储


外部存储就是指硬盘、U盘等外部存储介质。


内核态

  • 内核态是操作系统内核运行的模式,当操作系统内核执行特权指令时,处于内核态。
  • 在内核态下,操作系统内核拥有最高权限,可以访问计算机的所有硬件资源和敏感数据,执行特权指令,控制系统的整体运行。
  • 内核态提供了操作系统管理和控制计算机硬件的能力,它负责处理系统调用、中断、硬件异常等核心任务。

用户态


这里的用户可以理解为应用程序,这个用户是对于计算机的内核而言的,对于内核来说,系统上的各种应用程序会发出指令来调用内核的资源,这时候,应用程序就是内核的用户。

  • 用户态是应用程序运行的模式,当应用程序执行普通的指令时,处于用户态。
  • 在用户态下,应用程序只能访问自己的内存空间和受限的硬件资源,无法直接访问操作系统的敏感数据或控制计算机的硬件设备。
  • 用户态提供了一种安全的运行环境,确保应用程序之间相互隔离,防止恶意程序对系统造成影响。

模式切换


计算机为了安全性考虑,区分了内核态和用户态,应用程序不能直接调用内核资源,必须要切换到内核态之后,让内核来调用,内核调用完资源,再返回给应用程序,这个时候,系统在切换会用户态,应用程序在用户态下才能处理数据。


上述过程其实一次读和一次写都分别发生了两次模式切换。



内核缓冲区


内核缓冲区指内存中专门用来给内核直接使用的内存空间。可以把它理解为应用程序和外部存储进行数据交互的一个中间介质。


应用程序想要读外部数据,要从这里读。应用程序想要写入外部存储,要通过内核缓冲区。


用户缓冲区


用户缓冲区可以理解为应用程序可以直接读写的内存空间。因为应用程序没法直接到内核读写数据, 所以应用程序想要处理数据,必须先通过用户缓冲区。


磁盘缓冲区


磁盘缓冲区是计算机内存中用于暂存从磁盘读取的数据或将数据写入磁盘之前的临时存储区域。它是一种优化磁盘 I/O 操作的机制,通过利用内存的快速访问速度,减少对慢速磁盘的频繁访问,提高数据读取和写入的性能和效率。


PageCache

  • PageCache 是 Linux 内核对文件系统进行缓存的一种机制。它使用空闲内存来缓存从文件系统读取的数据块,加速文件的读取和写入操作。
  • 当应用程序或进程读取文件时,数据会首先从文件系统读取到 PageCache 中。如果之后再次读取相同的数据,就可以直接从 PageCache 中获取,避免了再次访问文件系统。
  • 同样,当应用程序或进程将数据写入文件时,数据会先暂存到 PageCache 中,然后由 Linux 内核异步地将数据写入磁盘,从而提高写入操作的效率。

再说数据读写操作流程


上面弄明白了这几个概念后,再回过头看一下那个流程图,是不是就清楚多了。


读操作
  1. 首先应用程序向内核发起读请求,这时候进行一次模式切换了,从用户态切换到内核态;
  2. 内核向外部存储或网络套接字发起读操作;
  3. 将数据写入磁盘缓冲区;
  4. 系统内核将数据从磁盘缓冲区拷贝到内核缓冲区,顺便再将一份(或者一部分)拷贝到 PageCache;
  5. 内核将数据拷贝到用户缓冲区,供应用程序处理。此时又进行一次模态切换,从内核态切换回用户态;

写操作
  1. 应用程序向内核发起写请求,这时候进行一次模式切换了,从用户态切换到内核态;
  2. 内核将要写入的数据从用户缓冲区拷贝到 PageCache,同时将数据拷贝到内核缓冲区;
  3. 然后内核将数据写入到磁盘缓冲区,从而写入磁盘,或者直接写入网络套接字。

瓶颈在哪里


但是传统I/O有它的瓶颈,这才是零拷贝技术出现的缘由。瓶颈是啥呢,当然是性能问题,太慢了。尤其是在高并发场景下,I/O性能经常会卡脖子。


那是什么地方耗时了呢?


数据拷贝


在传统 I/O 中,数据的传输通常涉及多次数据拷贝。数据需要从应用程序的用户缓冲区复制到内核缓冲区,然后再从内核缓冲区复制到设备或网络缓冲区。这些数据拷贝过程导致了多次内存访问和数据复制,消耗了大量的 CPU 时间和内存带宽。


用户态和内核态的切换


由于数据要经过内核缓冲区,导致数据在用户态和内核态之间来回切换,切换过程中会有上下文的切换,如此一来,大大增加了处理数据的复杂性和时间开销。


每一次操作耗费的时间虽然很小,但是当并发量高了以后,积少成多,也是不小的开销。所以要提高性能、减少开销就要从以上两个问题下手了。


这时候,零拷贝技术就出来解决问题了。


什么是零拷贝


问题出来数据拷贝和模态切换上。


但既然是 I/O 操作,不可能没有数据拷贝的,只能减少拷贝的次数,还有就是尽量将数据存储在离应用程序(用户缓冲区)更近的地方。


而区分用户态和内核态有其他更重要的原因,不可能单纯为了 I/O 效率就改变这种设计吧。那也只能尽量减少切换的次数。


零拷贝的理想状态就是操作数据不用拷贝,但是显示情况下并不一定真的就是一次复制操作都没有,而是尽量减少拷贝操作的次数。


要实现零拷贝,应该从下面这三个方面入手:

  1. 尽量减少数据在各个存储区域的复制操作,例如从磁盘缓冲区到内核缓冲区等;
  2. 尽量减少用户态和内核态的切换次数及上下文切换;
  3. 使用一些优化手段,例如对需要操作的数据先缓存起来,内核中的 PageCache 就是这个作用;

实现零拷贝方案


直接内存访问(DMA)


DMA 是一种硬件特性,允许外设(如网络适配器、磁盘控制器等)直接访问系统内存,而无需通过 CPU 的介入。在数据传输时,DMA 可以直接将数据从内存传输到外设,或者从外设传输数据到内存,避免了数据在用户态和内核态之间的多次拷贝。




如上图所示,内核将数据读取的大部分数据读取操作都交个了 DMA 控制器,而空出来的资源就可以去处理其他的任务了。


sendfile


一些操作系统(例如 Linux)提供了特殊的系统调用,如 sendfile,在网络传输文件时实现零拷贝。通过 sendfile,应用程序可以直接将文件数据从文件系统传输到网络套接字或者目标文件,而无需经过用户缓冲区和内核缓冲区。


如果不用sendfile,如果将A文件写入B文件。



  1. 需要先将A文件的数据拷贝到内核缓冲区,再从内核缓冲区拷贝到用户缓冲区;

  2. 然后内核再将用户缓冲区的数据拷贝到内核缓冲区,之后才能写入到B文件;


而用了sendfile,用户缓冲区和内核缓冲区的拷贝都不用了,节省了一大部分的开销。


共享内存


使用共享内存技术,应用程序和内核可以共享同一块内存区域,避免在用户态和内核态之间进行数据拷贝。应用程序可以直接将数据写入共享内存,然后内核可以直接从共享内存中读取数据进行传输,或者反之。



通过共享一块儿内存区域,实现数据的共享。就像程序中的引用对象一样,实际上就是一个指针、一个地址。


内存映射文件(Memory-mapped Files)


内存映射文件直接将磁盘文件映射到应用程序的地址空间,使得应用程序可以直接在内存中读取和写入文件数据,这样一来,对映射内容的修改就是直接的反应到实际的文件中。


当文件数据需要传输时,内核可以直接从内存映射区域读取数据进行传输,避免了数据在用户态和内核态之间的额外拷贝。


虽然看上去感觉和共享内存没什么差别,但是两者的实现方式完全不同,一个是共享地址,一个是映射文件内容。


Java 实现零拷贝的方式


Java 标准的 IO 库是没有零拷贝方式的实现的,标准IO就相当于上面所说的传统模式。只是在 Java 推出的 NIO 中,才包含了一套新的 I/O 类,如 ByteBufferChannel,它们可以在一定程度上实现零拷贝。


ByteBuffer:可以直接操作字节数据,避免了数据在用户态和内核态之间的复制。


Channel:支持直接将数据从文件通道或网络通道传输到另一个通道,实现文件和网络的零拷贝传输。


借助这两种对象,结合 NIO 中的API,我们就能在 Java 中实现零拷贝了。


首先我们先用传统 IO 写一个方法,用来和后面的 NIO 作对比,这个程序的目的很简单,就是将一个100M左右的PDF文件从一个目录拷贝到另一个目录。

public static void ioCopy() {
try {
File sourceFile = new File(SOURCE_FILE_PATH);
File targetFile = new File(TARGET_FILE_PATH);
try (FileInputStream fis = new FileInputStream(sourceFile);
FileOutputStream fos = new FileOutputStream(targetFile)) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
}
System.out.println("传输 " + formatFileSize(sourceFile.length()) + " 字节到目标文件");
} catch (IOException e) {
e.printStackTrace();
}
}

下面是这个拷贝程序的执行结果,109.92M,耗时1.29秒。



传输 109.92 M 字节到目标文件
耗时: 1.290 秒



FileChannel.transferTo() 和 transferFrom()


FileChannel 是一个用于文件读写、映射和操作的通道,同时它在并发环境下是线程安全的,基于 FileInputStream、FileOutputStream 或者 RandomAccessFile 的 getChannel() 方法可以创建并打开一个文件通道。FileChannel 定义了 transferFrom() 和 transferTo() 两个抽象方法,它通过在通道和通道之间建立连接实现数据传输的。


这两个方法首选用 sendfile 方式,只要当前操作系统支持,就用 sendfile,例如Linux或MacOS。如果系统不支持,例如windows,则采用内存映射文件的方式实现。


transferTo()


下面是一个 transferTo 的例子,仍然是拷贝那个100M左右的 PDF,我的系统是 MacOS。

public static void nioTransferTo() {
try {
File sourceFile = new File(SOURCE_FILE_PATH);
File targetFile = new File(TARGET_FILE_PATH);
try (FileChannel sourceChannel = new RandomAccessFile(sourceFile, "r").getChannel();
FileChannel targetChannel = new RandomAccessFile(targetFile, "rw").getChannel()) {
long transferredBytes = sourceChannel.transferTo(0, sourceChannel.size(), targetChannel);

System.out.println("传输 " + formatFileSize(transferredBytes) + " 字节到目标文件");
}
} catch (IOException e) {
e.printStackTrace();
}
}

只耗时0.536秒,快了一倍。



传输 109.92 M 字节到目标文件
耗时: 0.536 秒



transferFrom()


下面是一个 transferFrom 的例子,仍然是拷贝那个100M左右的 PDF,我的系统是 MacOS。

public static void nioTransferFrom() {
try {
File sourceFile = new File(SOURCE_FILE_PATH);
File targetFile = new File(TARGET_FILE_PATH);

try (FileChannel sourceChannel = new RandomAccessFile(sourceFile, "r").getChannel();
FileChannel targetChannel = new RandomAccessFile(targetFile, "rw").getChannel()) {
long transferredBytes = targetChannel.transferFrom(sourceChannel, 0, sourceChannel.size());
System.out.println("传输 " + formatFileSize(transferredBytes) + " 字节到目标文件");
}
} catch (IOException e) {
e.printStackTrace();
}
}

执行时间:



传输 109.92 M 字节到目标文件
耗时: 0.603 秒



Memory-Mapped Files


Java 的 NIO 也支持内存映射文件(Memory-mapped Files),通过 FileChannel.map() 实现。


下面是一个 FileChannel.map()的例子,仍然是拷贝那个100M左右的 PDF,我的系统是 MacOS。

    public static void nioMap(){
try {
File sourceFile = new File(SOURCE_FILE_PATH);
File targetFile = new File(TARGET_FILE_PATH);

try (FileChannel sourceChannel = new RandomAccessFile(sourceFile, "r").getChannel();
FileChannel targetChannel = new RandomAccessFile(targetFile, "rw").getChannel()) {
long fileSize = sourceChannel.size();
MappedByteBuffer buffer = sourceChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileSize);
targetChannel.write(buffer);
System.out.println("传输 " + formatFileSize(fileSize) + " 字节到目标文件");
}
} catch (IOException e) {
e.printStackTrace();
}
}

执行时间:



传输 109.92 M 字节到目标文件
耗时: 0.663 秒



推荐阅读


我的第一个 Chrome 插件上线了,欢迎试用!


前端同事最讨厌的后端行为,看看你中了没有


RPC框架的核心到底是什么


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

27岁程序媛未来的出路到底在哪里?

不太聪明的脑子的思考原因 最近回老家面试了一个工作,发现老家的思想的底层逻辑是:到了这个年纪女性就应该相夫教子,不愿意给女性与男性同等的工资标准或对女性进行培养, 看到他们这种嘴脸真的不想回去,但是目前互联网环境也不好,对未来开始变得迷茫不安, 不过作为i型...
继续阅读 »

不太聪明的脑子的思考原因


最近回老家面试了一个工作,发现老家的思想的底层逻辑是:到了这个年纪女性就应该相夫教子,不愿意给女性与男性同等的工资标准或对女性进行培养,

看到他们这种嘴脸真的不想回去,但是目前互联网环境也不好,对未来开始变得迷茫不安,

不过作为i型人格真的很喜欢这种沉浸式工作,暂时没有换行业的打算,所以还是先从目前做程序出发,去提升自己的能力,争取能再多干个几年,然后回东北老家花几万块买个小房子,开始我的摆烂养老人生
(人生终极目标)


在此总结一下今年上半年的成果和下半年的目标吧~


上半年成果


1.刷力扣拿到排名


摆烂人生是在去年感知到危机的时候结束的,于是开始疯狂刷LeetCode,学习算法,最终的结果是对待代码问题脑子变得灵光了但生活中越发糊涂了,但是目前困难的题还是基本摸不到头绪的状态,好多数学公式也不知道,位运算符也不咋会用,就目前感觉自己还是很差,提升的空间还是非常非常高的

(今年四月拿到排名时截的图)



2.开始准备软考


年初的时候开始考虑考一个专业资格证,于是开始做一些功课,上半年从bilibili上看了一些公开的课先做了初步了解,六月份买了一套课开始进行系统的学习,备战11月的考试



3.涨薪


很幸运自己能在目前经济环境下行的情况下没有失业,并且领导对我还算认可,给我们在竞争中留下来的人涨了工资,但说是涨薪,其实最终结果我们未必拿到的多了,因为目前公司效益不景气,如果公司效益持续低迷,年底的14薪必定要打水漂,但是还能稳定的存活下来也算是比较满意了,真心希望公司越来越好,因为我们的老板人真的非常不错(虽然我不接受pua但是发自内心感谢公司)


4.买了自行车开始骑行健身


其实早就想买个自行车,可以骑行上班,周末也可以当运动,不过身边的好多人都不赞同,因为像夏天太热、冬天太冷、刮风下雨都骑不出去,但是最终我还是买了,嘎嘎开心,不过确实影响因素很多最终也没骑过几次哈哈(主要是本人太懒总是找借口不骑车出门)



下半年目标


1.软考通过!


最近还是按照规划的持续学习,每个月给自己定一个总体的目标,然后分到每一天里去,现在距离考试还有两个多月,还是要加油的!


2.争取换一个更高的平台


感觉目前的公司体量还是太小了,做了很多微信小程序,工作对自己的提升已经到达了极限,但是就目前的情况来说,还是对年底的14薪抱有一丝丝幻想,所以这个目标可能在今年年底或者明年去达成


3.持续精进算法


还是在有条不紊的刷LeetCode,给自己的最低要求是每周至少一道中级,保持一个持续学习的状态


4.做一个开源项目


这个规划应该会在11月份开始实施,或者如果突然来了灵感可以立马启动,也是给以后面试提供一个优势条件吧


最后希望还在圈子中的同行们也能越来越好,不管这些努力会不会给自己带来实质性的收益,本质上都是在提升自己,目的其实很简单,就是不被这日新月异的时代所淘汰,
就好像一句金句里描述的那样:我们所做的一切,不是为了改变世界,而是不让世界改变我们!



最后,大家有什么能提升自己的点子也可以给我留言,让我们一起努力吧,加油!




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

[译] 2021 年的 SwiftUI: 优势、劣势和缺陷

iOS
2021 年的 SwiftUI: 优势、劣势和缺陷 在生产环境使用 SwiftUI?仍然不可行。 过去的 8 个月,我一直在用 SwiftUI 开发复杂的应用程序,其中就包括最近在 App Store 上架的 Fave。期间遇到了很多限制,也找到了大多数...
继续阅读 »

2021 年的 SwiftUI: 优势、劣势和缺陷



在生产环境使用 SwiftUI?仍然不可行。



由 Maxwell Nelson 在 Unsplash 发布


过去的 8 个月,我一直在用 SwiftUI 开发复杂的应用程序,其中就包括最近在 App Store 上架的 Fave。期间遇到了很多限制,也找到了大多数问题的解决方法。


简而言之,SwiftUI 是一个很棒的框架,并且极具前景。我认为它就是未来。但是要达到和 UIKit 同等的可靠性和健壮性,可能还需要 3-5 年。但是这并不意味着现在不应该使用 SwiftUI。我的目的是帮助你理解它的利弊,这样你可以就 SwiftUI 是否适合下一个项目做出更明智的决定。


SwfitUI 的优势


1. 编写 SwiftUI 是一件乐事,而且你可以快速构建用户界面


使用 addSubviewsizeForItemAtIndexPath,小心翼翼地计算控件的大小与位置,应对烦人的约束问题,手动构建视图层次结构,这样的日子已经一去不复返了。SwiftUI 的声明式和响应式设计模式使得创建响应式布局和 React 一样简单,同时它还背靠 Apple 强大的 UIKit。用它构建、启动并运行视图快到不可思议。


2. SwiftUI 简化了跨平台开发


我最兴奋的事情就是只需要编写一次 SwiftUI 代码,就可以在 iOS (iPhone 和 iPad),WatchOS 和 macOS 上使用。同时开发和维护 Android 和 Windows 各自的代码库已经很困难了,所以在减少不同代码库的数量这方面,每一个小的改变都很有帮助。当然还是有一些缺点,我将会在 “劣势” 章节分享。


3. 你可以免费获取漂亮的转场效果,动画和组件


你可以把 SwiftUI 当作一个 UI 工具箱,这个工具箱提供了开发专业应用程序所需的所有构建块。另外,如果你熟悉 CSS 的 Transition 属性,你会发现 SwiftUI 也有一套类似的方法,可以轻松创建优雅的交互过程。声明式语法的魅力在于你只需要描述你需要什么样的效果,效果就实现了,这看上去像魔法一样,但是也有不好的一面,我之后将会介绍。


4. UI 是完全由状态驱动并且是响应式的


如果你熟悉 React 的话,SwiftUI 在这一点上完全类似。当你监听整个 UI 的”反应“,动画和所有一切的时候,你只需要修改 @State@Binding 以及 @Published 属性,而不是使用多达几十层的嵌套回调函数。使用 SwiftUI,你可以体会到 CombineObservableObject 以及 @StateObject 的强大。这方面是 SwiftUI 和 UIKit 最酷的区别之一,强大到不可思议。


5. 社区正在拥抱 SwiftUI


几乎每个人都在因为 SwiftUI 而兴奋。SwiftUI 有许多学习资源可供获取,从 WWDC 到书,再到博客 —— 资料就在那里,你只需要去搜索它。如果不想搜索的话,我这里也汇总了一份最佳社区资源列表。


拥有一个活跃且支持度高的社区可以加速学习,开发,并且大量的新库会使得 SwiftUI 用途更加广泛。


劣势


1. 不是所有组件都可以从 SwiftUI 中获取到


在 SwiftUI 中有许多缺失、不完整或者过于简单的组件,我将在下面详细介绍其中一部分。


使用 UIViewRepresentableUIViewControllerRepresentableUIHostingController 协议可以解决这一问题。前两个让你可以在 SwiftUI 视图层中嵌入 UIKit 视图和控制器。最后一个可以让你在 UIKit 中嵌入 SwiftUI 视图。在 Mac 开发中也存在类似的三种协议 (NSViewRepresentable 等)。


这些协议是弥补 SwiftUI 功能缺失的权宜之计,但并不是一直天衣无缝。而且,尽管 SwiftUI 的跨平台承诺很好,但是如果某些功能不可用的话,你仍然需要为 iOS 和 Mac 分别实现协议代码。


2. NavigationView 还没有真正实现


如果你想在隐藏导航栏的同时仍然支持滑动手势,这是不可能的。我最终参考一些找到的代码创建了一个 UINavigationController wrapper。尽管可以起作用,但这不是一个长远的解决方案。


如果你想要在 iPad 上拥有一个 SplitView,但目前你还不能以纵向模式同时展示主视图和详情视图。他们选择用一个简陋的按钮展示默认关闭的抽屉。显然,你可以通过添加 padding 来解决这个问题,它可以突出显示你在使用 SwiftUI 时必须做的事情。


当你想使用编程式导航的时候,NavigationLink 是一种流行的解决方案。这里有一个有趣的讨论


3. 文本输入十分受限


TextFieldTextEditor 现在都太简单了,最终你还是会退回到 UIKit。所以我不得不为 UITextFieldUITextView 构建自己的 UIViewRepresentable 协议(以实现文本行数的自动增加)。


4. 编译器困境


当视图开始变得笨重,并且你已经竭尽所能去提取分解,编译器仍然会冲着你咆哮:



The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions.



这个问题已经多次拖慢进度。由于这个问题,我已经很擅长注释代码定位到引起问题的那一行,但是 2021 年了还在用这种方法调试代码感觉非常落后。


5. matchedGeometryEffect


我第一次发现这个的时候,感觉很神奇。它目的是通过匹配一隐一现的几何形状,帮助你更加流畅地转换两个不同标识的视图。我觉得这有助于从视图 A 优雅地转场到 B 视图。


我一直想让它起作用。但最终还是放弃了,因为它并不完美。此外,在包含大量列表项的 ListScrollView 中使用它会导致项目瘫痪。我只推荐在同一视图中使用这个做简单的转换过渡。当你在多个不同的视图中共享一个命名空间的时候(包括转场期间的视图剪裁在内),事情就会开始变得奇怪。


6. 对手势的支持有限


SwiftUI 提供了一系列新的手势(即 DragGestureLongPressGesture)。这些手势可以通过 gesture 修饰符(如 tapGesturelongPressGesture)添加到视图中。它们都能正常工作,除非你想要做更复杂的交互。


比如,DragGestureScrollView 交互就不是很好。即使有了 simultaneousGesture 修饰符,在 ScrollView 中放一个 DragGesture 还是会阻止滚动。在其他情况下,拖动手势可以在没有任何通知的情况下被取消,使得手势处于不完整状态。


为了解决这个问题,我构建了自己的 GestureView,它可以在 SwiftUI 中使用 UIKit 手势。我会在下一篇关于最佳 SwiftUI 库和解决方案的文章中分享这部分内容。


7. 分享扩展中的 SwiftUI


我可能是错的,但是分享扩展还是使用 UIKit 吧。我通过 UIHostingController 用 SwiftUI 构建了一个分享扩展,当分享扩展加载完毕后,有一个非常明显的延迟,用户体验较差。你可以尝试通过在视图中添加动画去掩盖它,但是仍然有 500 毫秒左右的延迟。


值得一提的点

  • 无法访问状态栏 (不能修改颜色或拦截点击)
  • 由于缺少 App,我们仍然需要 @UIApplicationDelegateAdaptor
  • 不能向后兼容
  • UIVisualEffectsView 会导致滚动延迟(来源于推特:@AlanPegoli

缺陷


1. ScrollView


这是迄今为止最大的缺点之一。任何一个构建过定制化 iOS 应用的人都知道我们有多依赖 ScrollView 去支持交互。

  • 主要的障碍:视图中的 LazyVStack 导致卡顿、抖动和一些意外的行为LazyVStack 对于需要滚动的混合内容(如新闻提要)的长列表至关重要。仅凭这一点,SwiftUI 就还没准备好投入生产环境: Apple 已经证实,这是 SwiftUI 自身的漏洞。尚未清楚他们什么时候会修复,但是一旦修复了,这将是一个巨大的胜利。
  • 滚动状态:原生不支持解析滚动的状态(滚动视图是否正在被拖拽?滚动?偏移多少?)。尽管有一些解决方案,但是还是很繁琐且不稳定。
  • 分页:原生不支持分页滚动视图。所以打消实现类似于可滑动的媒体库的念头吧(但是如果你想要关闭一些东西的时候,可以使用 SwiftUIPager)。在技术上你可以使用 TabView 加 PageTabViewStyle,但是我认为它更适合少部分的元素,而不是大的数据集。
  • 性能:使用 List 是性能最好的,并且避免了 LazyVStack 的卡顿问题,但由于工作方式的转换,它仍然不适合显示可变大小的内容。例如,在构建聊天视图时,其过渡很奇怪,会裁剪子视图,并且无法控制插入的动画样式。

结论


毫无疑问我觉得应该学习 SwiftUI ,自己去理解它,并享受乐趣。但是先别急着全盘采用。


SwiftUI 已经为简单的应用程序做好了准备,但是在写这篇文章的时候(iOS 15,beta 4 版本),我不认为它已经适合复杂应用程序的生产环境,主要是由于 ScrollView 的问题和对 UIViewRepresentable 的严重依赖。我很遗憾,尤其是像即时通信产品,新闻摘要,以及严重依赖复杂视图或者想要创建手势驱动的定制体验产品,目前还不适合使用 SwiftUI。


如果你想要精细的控制和无限的可能性,我建议在可预见的未来坚持使用 UIKit。你可以在一些视图(如设置页)里通过使用 UIHostingController 包装 SwiftUI 视图以获得 SwiftUI 的好处。


未来会发生什么?


当开始着手我们项目的下一次大迭代的时候。我知道这个新项目的交互范围不在 SwiftUI 目前支持的范围之内。即使当我知道 SwiftUI 在某些关键方面存在不足的时候,我的心都碎了,但是我还是不打算退回到 UIKit,因为我知道当 SwiftUI 运行起来时,构建它是一件多么快乐的事情。它的速度如此之快。


SwiftUI 会兼容 UIKit 么?如果这样的话,我们可能需要等待 SwiftUI 使用 3-5 年的时间来移植所有必要的 UIKit API。如果 SwiftUI 不准备兼容 UIkit,那你也能通过 SwiftUI 封装的方式使用 UIKit。


我好奇的是 Apple 会在 SwiftUI 上投入多少。他们是否有让所有的开发者采用 SwiftUI 的长期计划,或者说 SwiftUI 只是另一个界面构建器而已?我希望不是,也希望他们能全心投入 SwiftUI,因为它的前景是非常诱人的。


更多看法


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

iOS crash 报告分析系列 - 看懂 crash 报告的内容

iOS
在日常工作中,开发者最怕的应该就是线上的崩溃了。线上的崩溃不像我们开发中遇到的崩溃,可以在 Xcode 的 log 中直观的看到崩溃信息。 不过,线上的崩溃也并不是线索全无,让我们卖虾的不拿秤 -- 抓瞎。 每当 App 发生崩溃时,系统会自动生成一个后缀 i...
继续阅读 »

在日常工作中,开发者最怕的应该就是线上的崩溃了。线上的崩溃不像我们开发中遇到的崩溃,可以在 Xcode 的 log 中直观的看到崩溃信息。


不过,线上的崩溃也并不是线索全无,让我们卖虾的不拿秤 -- 抓瞎。


每当 App 发生崩溃时,系统会自动生成一个后缀 ips 的崩溃报告。我们可以通过崩溃报告来进行问题定位。但崩溃报告的内容繁多,新手看很容易一脸懵。所以本文先讲解一下报告中各字段的含义,后面再说报告符号化。


废话不多说,让我们开始吧!


前期准备


首先,报告解读我们需要先生成一个 crash 报告。


1、新建一个项目,在 ViewController 中写下面的代码:

NSString *value;
NSDictionary *dict = @{@"key": value}; // 字典的 value 不可为 nil,所以会崩溃

2、在真机上运行项目,然后去设置 - 隐私与安全性 - 分析与改进 - 分析数据,拿去生成的 crash 报告(报告的名字与项目名字一致,比如我的项目名为:CrashDemo,崩溃报告的名则为:CrashDemo-2023-05-30-093930.ips)。


注意:连着 Xcode 运行时不会产生崩溃报告,需要真机拔掉数据线再次运行 app 才会生成崩溃报告。


拿到报告,接下来就是解读了。


报告内容解读


官网的示例图:




Header


首先来看 Header:

Incident Identifier: 9928A955-FE71-464F-A2AF-A4593A42A26B
CrashReporter Key: 7f163d1c67c5ed3a6be5c879936a44f10b50f0a0
Hardware Model: iPhone14,5
Process: CrashDemo [45100]
Path: /private/var/containers/Bundle/Application/6C9D4CF7-4C16-4B50-A4A5-389BED62C699/CrashDemo.app/CrashDemo
Identifier: cn.com.fengzhihao.CrashDemo
Version: 1.0 (1)
Code Type: ARM-64 (Native)
Role: Foreground
Parent Process: launchd [1]
Coalition: cn.com.fengzhihao.CrashDemo [3547]

Date/Time: 2023-05-30 09:39:29.6418 +0800
Launch Time: 2023-05-30 09:39:28.5579 +0800
OS Version: iPhone OS 16.3.1 (20D67)
Release Type: User
Baseband Version: 2.40.01
Report Version: 104

Header 主要描述了目标设备的软硬件环境。比如上图可以看出:是 iphone 14 的设备,系统版本是16.3,发生崩溃的事件是 2023-05-30 09:39:29 等等。


需要注意的是 Incident Identifier 相当于当前报告的 id,报告和 Incident Identifier 是一一对应的关系,绝对不会存在两份不同的报告 Incident Identifier 相同的情况。


Exception information

Exception Type:  EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000

这一部分主要是告诉我们 app 是因为什么错误而导致的崩溃,但不会包含完整的信息。


可以看到当前的 Type 为:EXC_CRASH (SIGABRT),这代表当前进程因收到了 SIGABRT 信号而导致崩溃,这是一个很常见的类型,字典 value 为nil或者属于越界等都会是此类型。更多的 Exception Type 解释请参见此处


Diagnostic messages

Application Specific Information:
abort() called

操作系统有时包括额外的诊断信息。此信息使用多种格式,具体取决于崩溃的原因,并且不会出现在每个崩溃报告中。


本次的崩溃原因是因为调用了 abort() 函数。


接下来,就是报告的重点了。


Backtraces


这部分记录了当前进程的线程的函数调用栈,我们可以通过调用栈来定位出问题的代码。


崩溃进程的每一条线程都会被捕获成回溯。回溯会展示当前线程被中断时的线程的函数调用栈。如果崩溃是由于语言异常造成的,会额外有一个Last Exception Backtrace,位于第一个线程之前。关于 Last Exception Backtrace 的详细介绍请看这里


比如我们示例中的崩溃就是由于语言异常造成的,所以崩溃报告中会有 Last Exception Backtrace。

Last Exception Backtrace:
0 CoreFoundation 0x191560e38 __exceptionPreprocess + 164
1 libobjc.A.dylib 0x18a6f78d8 objc_exception_throw + 60
2 CoreFoundation 0x191706078 -[__NSCFString characterAtIndex:].cold.1 + 0
3 CoreFoundation 0x1917113ac -[__NSPlaceholderDictionary initWithCapacity:].cold.1 + 0
4 CoreFoundation 0x19157c2b8 -[__NSPlaceholderDictionary initWithObjects:forKeys:count:] + 320
5 CoreFoundation 0x19157c158 +[NSDictionary dictionaryWithObjects:forKeys:count:] + 52
6 CrashDemo 0x104a69e0c -[ViewController touchesBegan:withEvent:] + 152
.... 中间内容省略
25 CrashDemo 0x104a6a0c4 main + 120
26 dyld 0x1afed0960 start + 2528

以下是上述每一列元素的含义:

  • 第一列:栈帧号。堆栈帧按调用顺序排列,其中帧 0 是在执行暂停时正在执行的函数。第 1 帧是调用第 0 帧函数的函数,依此类推
  • 第二列:包含正在执行函数的二进制包名
  • 第三列:正在执行的机器指令的地址
  • 第四列:在完全符号化的崩溃报告中,正在执行的函数的名称。出于隐私原因,函数名称有时限制为前 100 个字符
  • 第五列(+ 号后面的数字):函数入口点到函数中当前指令的字节偏移量

通过第 6 行我们可以推断出问题是由 NSDictionary 引起的。


但大部分时候我们得到的报告都是未符号化的,我们需要对报告进行符号化来获得更多的信息。关于符号化的相关内容可以看这里


Thread state

Thread 0 crashed with ARM Thread State (64-bit):
x0: 0x0000000000000000 x1: 0x0000000000000000 x2: 0x0000000000000000 x3: 0x0000000000000000
...中间内容省略
far: 0x00000001e4d30560 esr: 0x56000080 Address size fault

崩溃报告的线程状态部分列出了应用程序终止时崩溃线程的 CPU 寄存器及其值。


Binary images

0x1cf074000 -        0x1cf0abfeb libsystem_kernel.dylib arm64e  <c76e6bed463530c68f19fb829bbe1ae1> /usr/lib/system/libsystem_kernel.dylib
...中间内容省略
0x18b8ca000 - 0x18c213fff Foundation arm64e <e5f615c7cc5e3656860041c767812a35> /System/Library/Frameworks/Foundation.framework/Foundation

以下是上述每一列元素的含义:

  • 第一列:二进制镜像在进程中的地址范围
  • 第二列:二进制镜像的名称
  • 第三列:操作系统加载到进程中的二进制映像中的 CPU 架构
  • 第四列:唯一标识二进制映像的构建 UUID。符号化崩溃报告时使用此值定位相应的 dSYM 文件
  • 第五列:二进制文件在磁盘上的路径

至此,报告上的所有 section 都已经解读完。希望大家看完这篇文章后,再分析崩溃日志的时候能更加得心应手。


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

开发工具 2.0 的时代已经来临

AI 正在变革软件工程:开发工具 2.0 时代 生成式 AI 的爆发已经开始改变了很多行业的工作方式,但对于软件工程来说,转型才刚刚开始。 从 Copilot 说起 Github Copilot 的成功引发了一场 AI 编程工具的浪潮,《Research: q...
继续阅读 »

AI 正在变革软件工程:开发工具 2.0 时代


生成式 AI 的爆发已经开始改变了很多行业的工作方式,但对于软件工程来说,转型才刚刚开始。


从 Copilot 说起


Github Copilot 的成功引发了一场 AI 编程工具的浪潮,《Research: quantifying GitHub Copilot’s impact on developer productivity and happiness》这份报告研究了 Copilot 对开发者效率和幸福感的提升,如下

  • 使用 GitHub Copilot 的开发人员比不使用 GitHub Copilot 的开发人员完成任务的速度快 55%
  • 使用 GitHub Copilot 的小组完成任务的比例为 78%,而没有使用 Copilot 的小组为 70%
  • 88% 的使用者认为自己生产力提高了
  • 96% 的使用者认为自己处理重复性的工作更快了
  • 88% 的使用者认为自己可以更加专注于更喜欢的工作上了


原文地址:github.blog/2022-09-07-…





从数据上来看,Copilot 已经是非常成功了,我们会认为这已经是一个大的变革,但是当我们把眼光放到整个软件工程行业的时候,才发现 Copilot 可能只是 AI 改变软件工程师工作方式的开端。



我曾经写了一篇 Copilot 的体验文章,有兴趣可以看看 # 与 AI 结对编程,好搭档 Copilot



开发工具 2.0 与现状


红衫资本在《Developer Tools 2.0》中定义了”开发工具 2.0“ :通过 AI 改变软件创造方式的工具。


还整理了一张图用以展示现有的开发工具在不同的软件研发阶段的应用。




这图本质上是一个表格,每一行从左到右代表了软件在当前市场的占用水平,分为

  • Incumbents:当前主流使用的标准工具
  • Challengers:挑战者,一些加入了 AI 特性的创新型工具
  • Dev Tools 2.0:通过 AI 改变软件创造方式的工具

列的话从上到下代表了软件开发的各个阶段,或者说生命周期,分别为

  • Deployment:部署阶段,包括 CI/CD、云、监控等
  • Implementation:实现阶段,包括 code review 工具、文档工具、代码编写维护工具等
  • Setup:配置阶段,包括 IDE、终端、ISSUE 记录工具等

接下来我们从上往下来分析。


Deployment 所属区域中,软件还是集中在 Incumbents(主流) 和 Challengers(挑战者) 中,这里可以看到很多熟悉的产品,比如 Datadog、Grafana、Aws、Jenkins 等。


但 Deployment 目前还没有 Dev Tools 2.0 的工具




Implementation 中,目前已有很多 Dev Tools 2.0 了,比如 AI code review 工具 Codeball、DEBUG 和对话工具 ChatGPT、AI 文档工具 Mintlify、以及 AI 代码补全工具 Copilot 和 Tabnine。


注意看细分的 write docs(文档编写) 和 write & maintain code (代码编写维护)中,在主流中这些都是人力维护,这说明当前的软件工程已经处于一个分水岭了:从人工到 AI。


对比 Deployment 的话,Implementation 的 2.0 工具可谓是百花齐放。




最后就是 Setup 了,目前只有 Cursor (一款集成了 ChatGPT 4 的代码编辑器)被完全定义为 Dev Tools 2.0




这里比较意外的是 warp 和 fig 居然没有被定义为 2.0 工具,因为我前段时间刚试用了 warp 终端,有兴趣的可以看看我发的视频


其实回顾一下红衫资本对 Dev Tools 2.0 的定义就能理解了:通过 AI 改变软件创造方式的工具。


warp 和 fig 只是带了 AI 的特性,还没有改变软件的创造规则,所以就被列入了 challenger 里。


从目前世面上的工具来看,AI 已经有了巨大的机会改变软件工程,并且这是一个关于“谁”,而不是“是与否”的问题。


开发工具 2.0 的共同点


再再再次啰嗦一下红衫资本对 Dev Tools 2.0 的定义:通过 AI 改变软件创造方式的工具。


我考察了 5 个图中被归类为 2.0 的软件,看看它们是如何改变软件的创作方式的



首先是 Cursor,我们可以用自然语言来写新的代码、维护既有代码,从这点来看它是超越了 Copilot (这不是指下一代 Copilot X )。




然后是 Codeball,它主要是用 AI 来自动执行 code review,它可以为每一个 PR 进行评分(检查代码规范、Bug 等)并自动合并,大量节省功能特性因 PR 被 Block 的时间,而且用机器代替人做检查也能避免 Review 成为形式主义的流程。




ChatGPT 此处就不做演示了,直接看一下 Grit 吧。虽然下面展示的动图只是将代码片段的优化,但 Grit 给自己的定位是通过 AI 自动化完成整个项目的代码迁移和升级,比如从 JavaScript 到 TypeScript、自动处理技术债等




最后就是 Adrenaline 了,它是一个 AI Debuger(调试器?),我输入了一段会导致 NullPointerException 的代码,但是因为服务器请求的数量太多无法运行。所以我直接在对话框里问了一句:Is there anything wrong with this code?(这段代码有问题吗?)。Adrenaline 不仅回答了会出问题,还详细分析了这段代码的功能




再来对比一下这几个场景下传统的处理方式



基于以上工具的特点,我们也可以畅想一下 Deployment 2.0 工具的特点

  1. 首先肯定是通过自然语言进行交互,比如:帮我在阿里云上部署一下 xxx 项目;也可以说帮我创建一个项目,这项目叫熔岩巨兽,需要使用到 mysql、redis,需要一个公网域名等…
  2. 然后是能够自动分析并配置项目的依赖,比如:部署 xxx 项目需要 mysql 数据库、redis 缓存
  3. 如果能够为我使用最优(成本、性能等多方面)的解决方案更好

其实随着云平台的成熟、容器化的普及,我相信这样的 Deployment 2.0 工具肯定不会太遥远。


事实上在写这篇文章的时候我就发现了 Github 上的一个项目叫 Aquarium,它已经初步基于 AI 的能力实现了部署,它给 AI 输入了以下的前提提示:



你现在控制着一个Ubuntu Linux服务器。你的目标是运行一个Minecraft服务器。不要回答任何批判、问题或解释。你会发出命令,我会回应当前的终端输出。 回答一个要给服务器的Linux命令。



然后向 AI 输入要执行的部署,比如:”Your goal is to run a minecraft server“。


接着 AI 就会不断的输出命令,Aquarium 负责在程序执行命令并将执行结果返回给 AI,,不断重复这个过程直到部署结束。


对开发者的影响


作为一名软件开发者,我们经常会自嘲为 CV 工程师,CV 代表了 ctrl + cctral + v ,即复制粘贴工程师。


这是因为大多数的代码都是通过搜索引擎查询获得,开发者可以直接复制、粘贴、运行,如果运行失败就把错误信息放进搜索引擎再次搜索,接着又复制、粘贴、运行……


但基于开发工具 2.0,这个流程就产生了变化:搜索、寻找答案、检查答案的过程变成了询问、检查答案,直接省去了最费时间的寻找答案的过程。




还有就是开发模式的改变,以前是理解上游的需求并手写代码,而现在是理解上游的需求并用自然语言描述需求,由 AI 写代码。


也就是说在代码上的关注会降低,需要将更多的注意力集中在需求上




也许你发现了,其实可以直接从产品到 AI,因为程序员极有可能是在重复的描述产品需求。


这个问题其实可以更大胆一点假设:如果 AI 可以根据输入直接获得期望的输出,那么老板可以直接对接 AI 了,80% 的业务人员都不需要。


既然已经谈到了对”人“的影响,那不如就接着说两点吧

  • 这些工具会让高级开发者的技能经验价值打折扣,高级和初级的编码能力会趋于拟合,因为每个人都拥有一个收集了全人类知识集的 AI 助手
  • 会编程的人多了,但是适合以编程为工作的人少了

很多开发者对此产生焦虑,其实也不必,因为这是时代的趋势,淹没的也不止你一个,浪潮之下顺势而为指不定也是一个机遇。


如果光看软件工具 2.0,它给软件工程带来的是一次转型,是一次人效的变革,目前来看还没有达到对软件工程的颠覆,那什么时候会被颠覆呢?



有一天有一个这样的游戏出现了,每个人在里面都是独一无二的,系统会为每个人的每个行为动态生成接下来的剧情走向,也就是说这个游戏的代码是在动态生成,并且是为每一个人动态生成。这个游戏的内存、存储空间等硬件条件也是动态在增加。 这就是地球 Online



短期来看,AI 还不会代替程序员,但会替代不会用 AI 的程序员。


AI 正在吞噬软件


最后就用两位大佬的话来结束本文吧。


原 Netscape(网景公司)创始人 Marc Andreessen 说过一句经典的话:软件正在吞噬世界。


人工智能领域知名科学家 Andrej Karpathy 在 2017 年为上面的话做了补充:软件(1.0)正在吞噬世界,现在人工智能(软件2.0)正在吞噬软件



Software (1.0) is eating the world, and now AI (Software 2.0) is eating software.



所以,你准备好了吗?


参考

  1. http://www.sequoiacap.com/article/ai-…
  2. karpathy.medium.com/software-2-…
  3. github.blog/2022-09-07-…
  4. github.com/fafrd/aquar…

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

CSS命名太头疼?这个Vite插件自动生成,让你解放双手!

web
CSS样式一直以来都是一个让前端开发者头疼的问题,随着前端工程化的发展,使用原子CSS进行样式开发正变得越来越流行。相比传统的CSS样式书写方式,原子CSS可以让我们以更模块化和可复用的方式进行样式的编码。但是手动编写大量原子类样式也比较烦琐。有没有办法自动生...
继续阅读 »

CSS样式一直以来都是一个让前端开发者头疼的问题,随着前端工程化的发展,使用原子CSS进行样式开发正变得越来越流行。相比传统的CSS样式书写方式,原子CSS可以让我们以更模块化和可复用的方式进行样式的编码。但是手动编写大量原子类样式也比较烦琐。有没有办法自动生成原子CSS类呢?今天我要介绍的Vite插件atom-css-generator就可完美实现这一功能。


原子CSS简介


原子CSS(Atomic CSS)将传统的CSS类拆分成一个个独立的、原子级的类,每个类仅包含一个CSS属性,例如:



.p-10 {
padding: 10px;
}

.bg-red {
background: red;
}

相比于传统的CSS类,原子类具有以下特点:



  • 原子:每个类只包含一个CSS属性,拆分到最小粒度

  • 独立:类名语义明确,可以任意组合使用而不会产生冲突

  • 可复用:一个原子类可以重复使用在不同的组件中


使用原子CSS的优势在于:



  • 更模块化:样式属性高内聚、解耦

  • 更可维护:不同类名称、不同文件,避免影响

  • 更灵活:组件样式由原子类组合,更容易扩展和维护


但是编写大量原子类也比较麻烦,多达几千个类定义都可能出现。有没有自动生成的方式呢?


atom-css-generator插件介绍


atom-css-generator是一个Vite插件,它可以通过解析Vue组件中的class,自动生成对应的原子CSS定义


安装和配置


使用npm或yarn安装:


Copy code

npm install atom-css-generator

在vite.config.js中引入插件:


js

Copy code

import atomCssGenerator from 'atom-css-generator';

export default {
plugins: [
atomCssGenerator({
outputPath: 'assets/styles'
})
]
}

主要的配置项有:



  • outputPath:指定生成的CSS文件输出目录,默认为public


使用方式



  1. 在Vue组件的template中,使用特定格式的class,例如:


html

Copy code

<template>
<div class="bg-red fc-white p-20">
<!-- ... -->
</div>
</template>


  1. 构建项目时,插件会自动生成对应的原子CSS类定义:


css

Copy code

.bg-red {
background-color: red;
}

.fc-white {
color: white;
}

.p-20 {
padding: 20px;
}


  1. style.css会被自动生成到指定的outputPath中,并注入到HTML文件头部。


支持的类名格式


插件支持多种格式的类名规则生成,包括:



  • 颜色类名:bg-red、fc-333

  • 间距类名:p-20、ml-10

  • 尺寸类名:w-100、h-200

  • Flexbox类名:jc-center、ai-stretch

  • 边框类名:bc-333、br-1-f00-solid

  • 布局类名:p-relative、p-fixed

  • 文字类名:fs-14、fw-bold


等等,非常全面。


而且也内置了一些预设的实用样式类,比如文字截断类te-ellipsis。


原理简析


插件主要通过以下处理流程实现自动生成原子CSS:



  1. 使用@vue/compiler-sfc解析Vue文件,获取模板内容

  2. 通过正则表达式提取模板中的class名称

  3. 根据特定类名规则,生成对应的CSS定义

  4. 将CSS写入style.css文件中,并注入到HTML中


同时,插件还会在热更新时自动检查新添加的类名,从而动态更新style.css。


总结


通过atom-css-generator这个插件,我们可以非常轻松地在Vue项目中使用原子CSS样式,而不需要手动编写。它省去了我们大量重复的工作,使得样式的维护和扩展更加简单。


如果你也想尝试在自己的项目中引入原子CSS,不妨试试这个插件。相信它能给你带来意想不到的便利!
GitHub地址

收起阅读 »

扒一扒uniapp是如何做ios app应用安装的

iOS
为何要扒 因为最近有移动端业务的需求,用uniapp做了ios、Android双端的app应用,由于没有资质上架AppStore和test flight,所以只能使用苹果的超签(需要ios用户提供uuid才能加入测试使用,并且只支持100人安装使用)。打包出来...
继续阅读 »

为何要扒


因为最近有移动端业务的需求,用uniapp做了ios、Android双端的app应用,由于没有资质上架AppStore和test flight,所以只能使用苹果的超签(需要ios用户提供uuid才能加入测试使用,并且只支持100人安装使用)。打包出来生成的是一个ipa包,并不能直接安装,要通过爱思助手这类的应用装一下ipa包。但交付到客户手上就有问题了,还需要电脑连接助手才能安装,那岂不是每次安装新版什么的,都要打开电脑搞一下。因此,才有了这次的扒一扒,目标就是为了解决只提供一个下载链接用户即可下载,不用再通过助手类应用安装ipa包。




开干


官方模板




先打开uniapp云打包一下项目看看


image-20230824112232275.png




复制地址到移动端浏览器打开看看


image-20230824112410817.png


这就对味了,都知道ios是不能直接打开ipa文件进行安装的,接下来就研究下这个页面的执行逻辑。




开扒




F12打开choromdevtools,ctrl+s保存网页html。


image.png


保存成功,接下来看看html代码(样式代码删除了)


    <!DOCTYPE html>
<!-- saved from url=(0077)https://ide.dcloud.net.cn/build/download/2425a4b0-4229-11ee-bd1b-67afccf2f6a7 -->
<html>
   <head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no, width=device-width">
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
 </head>

<body>
<br><br>
   <center>
       <a class="button" href="itms-services://?action=download-manifest&amp;url=https://ide.dcloud.net.cn/build/ipa-xxxxxxxxxxx.plist">点击安装</a>
   </center>
   <br><br>
   <center>注意:只提供包名为io.dcloud.appid的包的直接下载安装,如果包名不一致请自行搭建下载服务器</center>
</body>
</html>



解析




从上面代码可以看出,关键代码就一行也就是a标签的href地址("itms-services://?action=download-manifest&url=ide.dcloud.net.cn/build/ipa-x…")


先看看itms-services是什么意思,下面是代码开发助手给的解释


image-20230824113418246.png


大概意思就是itms-services是苹果提供给开发者一个的更新或安装应用的协议,用来做应用分发的,需要指向一个可下载的plist文件地址。




什么又是plist呢,这里再请我们的代码开发助手解释一下


image-20230824113748570.png


对于没接触过ios相关开发的,连plist文件怎么写都不知道,既然如此,那接下来就来扒一下dcloud的pilst文件,看看官方是怎么写的吧。




打开浏览器,copy一下刚刚扒下来的html文件下a标签指向的地址,复制url后面plist文件的下载地址粘贴到浏览器保存到桌面。


image-20230824114108792.png


访问后会出现


image-20230824115354028.png




别担心,这时候直接按ctrl+s可以直接保存一个plist.xml文件,也可以打开devtools查看网络请求,找到ipa开头的请求


image-20230824115609551.png


直接新建一个plist文件,cv一下就好,我这里就选择保存它的plist.xml文件,接下来康康文件里到底是什么


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>items</key>
<array>
<dict>
<key>assets</key>
<array>
<dict>
<key>kind</key>
<string>software-package</string>
<key>url</key>
<string>https://bdpkg-aliyun.dcloud.net.cn/20230824/xxxxx/Pandora.ipa?xxxxxxxx</string>
</dict>
      <dict>
<key>kind</key>
<string>display-image</string>
<key>needs-shine</key>
<false/>
<key>url</key>
<string>https://qiniu-web-assets.dcloud.net.cn/unidoc/zh/uni.png</string>
</dict>
</array>
<key>metadata</key>
<dict>
<key>kind</key>
<string>software</string>
<key>bundle-identifier</key>
<string>xxxxx</string>
<key>title</key>
<string>HBuilder手机应用</string>
</dict>
</dict>
</array>
</dict>
</plist>

直接抓重点,这里存你存放ipa包的地址


image-20230824120013828.png


这里改你应用的昵称


image-20230824120453368.png


这里改图标


image-20230824120509797.png


因篇幅限制,想了解plist的自行问代码助手或者搜索引擎。




为我所用


分析完了,如何为我所用呢,首先按照分析上扒下来的plist文件修改下自身应用的信息,并且需要服务器存放ipa文件,这里我选择了unicloud,开发者可以申请一个免费的空间(想了解更多的自己去dcloud官网看看,说多了有打广告嫌疑),替换好大概如下:


image-20230824155040313.png


将plist文件放到服务器上后,拿到plist的下载地址,打开扒下来的html,将a标签上的url切换成plist文件的下载地址,如图:


image-20230824155306228.png


可以把页面上没用的信息都删掉,保存,再把html放到服务器上,用户访问这个地址,就可以直接下载描述文件安装ipa包应用了(记得需要添加用户的uuid到开发者账号上),其实至此需求已经算是落幕了,但转念想想还是有点麻烦,于是又优化了一下,将a标签中的href信息,直接加载到二维码上供用户扫描便可直接下载,相对来说更方便一点,于是我直接打开草料,生成了一个二维码,至

作者:廿一c
来源:juejin.cn/post/7270799565963149324
此,本次扒拉过程结束,需求落幕!

收起阅读 »

iOS - 上手AR

iOS
前言 随着 Apple Vision Pro 的发布,势必掀起新一波的Ar潮,简单了解一下来个小Demo 开始 要在iOS中创建一个的AR物体,你可以使用 ARKit 和 SceneKit 来实现 首先,确保你的项目已经导入了 ARKit 和 SceneKit...
继续阅读 »

前言


随着 Apple Vision Pro 的发布,势必掀起新一波的Ar潮,简单了解一下来个小Demo


开始


要在iOS中创建一个的AR物体,你可以使用 ARKitSceneKit 来实现


首先,确保你的项目已经导入了 ARKit 和 SceneKit 框架。你可以在 Xcode 中的项目设置中添加 ARKit.framework 和 SceneKit.framework 到 "Frameworks, Libraries, and Embedded Content" 部分




然后,在你的程序文件中,导入 ARKit 和 SceneKit

import UIKit
import ARKit

接下来,创建一个 ARSCNView,并将其添加到你的视图层次结构中:

// 创建 ARSCNView 实例
sceneView = ARSCNView(frame: view.bounds)
view.addSubview(sceneView)
sceneView.delegate = self

// 创建 SCNScene 实例,并设置为 sceneView 的场景
let scene = SCNScene()
sceneView.scene = scene

然后,在视图控制器的生命周期方法中,配置 ARSession 并启动 AR 会话:

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

// 配置 AR 会话并启动
let configuration = ARWorldTrackingConfiguration()
sceneView.session.run(configuration)
}

override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)

// 暂停 AR 会话
sceneView.session.pause()
}

现在,已经设置好 ARKit 和 AR 会话已经开始运行。接下来,我们将创建3D 模型:

func createBallNode() {
// 创建球体几何体
let ballGeometry = SCNSphere(radius: 0.1)
let ballMaterial = SCNMaterial()
ballMaterial.diffuse.contents = UIImage(named: "cxkj.webp") // 使用纹理图片
ballGeometry.materials = [ballMaterial]

// 在屏幕范围内生成随机位置坐标
let randomX = Float.random(in: -1.0...1.0) // 在屏幕宽度范围内生成随机 X 坐标
let randomY = Float.random(in: -1.0...1.0) // 在屏幕高度范围内生成随机 Y 坐标
let randomZ = Float.random(in: -3.0...0.0) // 在屏幕深度范围内生成随机 Z 坐标
ballNode = SCNNode(geometry: ballGeometry)
ballNode.position = SCNVector3(randomX, randomY, randomZ)

// 将球体节点添加到场景的根节点上
sceneView.scene.rootNode.addChildNode(ballNode)
}

最后我们通过点击事件将3D模型添加到场中

@objc func handleTap(_ gesture: UITapGestureRecognizer) {
if gesture.state == .ended {
createBallNode() // 创建球体节点
}
}

效果




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

iOS 快速复习GCD

iOS
多线程-串行、并行队列,同步、异步任务 1、创建串行队列和并行队列 //并行队列 dispatch_queue_t queue = dispatch_queue_create("com.lg.cooci.cn", DISPATCH_QUEUE_C...
继续阅读 »

多线程-串行、并行队列,同步、异步任务


1、创建串行队列和并行队列

    //并行队列
dispatch_queue_t queue = dispatch_queue_create("com.lg.cooci.cn", DISPATCH_QUEUE_CONCURRENT);
//串行队列
dispatch_queue_t queue = dispatch_queue_create("com.lg.cooci.cn", DISPATCH_QUEUE_SERIAL);

  • 每次只有一个任务被执行。让任务一个接着一个地执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)
  • 可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务),并发队列 的并发功能只有在异步(dispatch_async)方法下才有效。

2、同步异步任务

//同步
dispatch_sync(queue, ^{
        NSLog(@"1");
    });
//异步
dispatch_async(queue, ^{
        NSLog(@"1");
    });

同步执行:

  • 同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。
  • 只能在当前线程中执行任务,不具备开启新线程的能力。

异步执行:

  • 异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。
  • 可以在新的线程中执行任务,具备开启新线程的能力。

异步执行(async) 虽然具有开启新线程的能力,但是并不一定开启新线程。这跟任务所指定的队列类型有关。

默认全局并发队列:dispatch_get_global_queue

第一个参数表示队列优先级,一般用 DISPATCH_QUEUE_PRIORITY_DEFAULT

第二个参数暂时没用,用 0 即可。


信号量 dispatch_semaphore_t



GCD中的信号量dispatch_semaphore_t中主要有三个函数:

  • dispatch_semaphore_create:创建信号
  • dispatch_semaphore_wait:等待信号
  • dispatch_semaphore_signal:释放信号

1、dispatch_semaphore_create
参数为int,表示信号量初始值,需大于等于0,否则创建失败,返回一个dispatch_semaphore_t


2、dispatch_semaphore_wait
参数1:

需传递一个 dispatch_semaphore_t 类型对象,对信号进行减1,然后判断信号量大小

参数2:

传递一个超时时间:dispatch_time_t 对象

  • 减1后信号量小于0,则阻塞当前线程,直到超时时间到达或者信号量大于等于0后继续执行后面代码
  • 减1后信号量大于等于0,对dispatch_semaphore_t 进行赋值,并返回dispatch_semaphore_t对象,继续执行后面代码

3、dispatch_semaphore_signal
参数:dispatch_semaphore_t

进行信号量加1操作,如果加1后结果大于等于0,则继续执行,否则继续等待。


用法:

- (void)startAsync{
//创建信号量 值为0
    self.sem = dispatch_semaphore_create(0);
//开启异步并发线程执行
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"dispatch_semaphore 2\n");
        sleep(5);
//发送信号,信号量值+1
        dispatch_semaphore_signal(self.sem);
        NSLog(@"dispatch_semaphore 3\n");
    });
    NSLog(@"dispatch_semaphore 0\n");
//信号量 值-1 小于0 等待信号。。。
    dispatch_semaphore_wait(self.sem, DISPATCH_TIME_FOREVER);
    NSLog(@"dispatch_semaphore 1\n");

}
执行顺序0 2 1 3 1和3不确定顺序
如果初始化创建是信号量值为1
执行顺序0 1 2 3

常用总结:

1、异步并发线程顺序执行

2、异步并发线程控制最大并发数,比如下载功能控制最大下载数


调度组 dispatch_group_t


主要API:

  • dispatch_group_create:创建组

  • dispatch_group_async:进组任务

  • dispatch_group_notify:组任务执行完毕的通知

  • dispatch_group_enter:进组

  • dispatch_group_leave:出组

  • dispatch_group_wait:等待组任务时间


组合用法1:

- (void)dispatchGroupAsync{
//创建调度组
    dispatch_group_t group = dispatch_group_create();
//获取全局并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//开启异步线程
    dispatch_group_async(group, queue, ^{
        sleep(2);
        NSLog(@"11");
    });
    dispatch_group_async(group, queue, ^{
        sleep(1);
        NSLog(@"12");
    });
    dispatch_group_async(group, queue, ^{
        sleep(3);
        NSLog(@"13");
    });
    NSLog(@"14");
    dispatch_group_notify(group, queue, ^{
//收到执行完成的通知后执行
        NSLog(@"15");
    });
//等待调度组执行完成
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
调度组执行完成后执行
    NSLog(@"16");
}

用法2:

- (void)dispatchSyncEnterGroup{
//创建调度组
    dispatch_group_t group = dispatch_group_create();
//获取全局并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//进入调度组
    dispatch_group_enter(group);
//执行异步任务
    dispatch_async(queue, ^{
        sleep(2);
        NSLog(@"21");
//执行完成后立刻调度组
        dispatch_group_leave(group);
    });
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        sleep(1);
        NSLog(@"22");
        dispatch_group_leave(group);
    });
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        sleep(3);
        NSLog(@"23");
        dispatch_group_leave(group);
    });
    NSLog(@"24");
    dispatch_group_notify(group, queue, ^{
//执行完后回调
        NSLog(@"25");
    });
    NSLog(@"26");
//等待调度组执行完成
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    NSLog(@"27");
}

总结:

1、dispatch_group_async 是对dispatch_group_enter和dispatch_group_leave的封装

2、dispatch_group_enter和dispatch_group_leave的须成双成对的出现


事件源 dispatch_source_t


主要API:

  • dispatch_source_create :创建源

  • dispatch_source_set_event_handler: 设置源的回调

  • dispatch_source_merge_data: 源事件设置数据

  • dispatch_source_get_data: 获取源事件的数据

  • dispatch_resume:恢复继续

  • dispatch_suspend:挂起

  • uintptr_t dispatch_source_get_handle(dispatch_source_t source) //得到dispatch源创建,即调用dispatch_source_create的第二个参数

  • unsignedlong dispatch_source_get_mask(dispatch_source_t source); //得到dispatch源创建,即调用dispatch_source_create的第三个参数


源的类型dispatch_source_type_t

1. DISPATCH_SOURCE_TYPE_DATA_ADD:用于ADD合并数据
2. DISPATCH_SOURCE_TYPE_DATA_OR:用于按位或合并数据
3.DISPATCH_SOURCE_TYPE_DATA_REPLACE:跟踪通过调用dispatch_source_merge_data获得的数据的分派源,新获得的数据值将替换 尚未交付给源处理程序 的现有数据值
4. DISPATCH_SOURCE_TYPE_MACH_SEND:用于监视Mach端口的无效名称通知的调度源,只能发送没有接收权限
5. DISPATCH_SOURCE_TYPE_MACH_RECV:用于监视Mach端口的挂起消息
6. DISPATCH_SOURCE_TYPE_MEMORYPRESSURE:用于监控系统内存压力变化
7.DISPATCH_SOURCE_TYPE_PROC:用于监视外部进程的事件
8. DISPATCH_SOURCE_TYPE_READ:监视文件描述符以获取可读取的挂起字节的分派源
9. DISPATCH_SOURCE_TYPE_SIGNAL:监控当前进程以获取信号的调度源
10. DISPATCH_SOURCE_TYPE_TIMER:基于计时器提交事件处理程序块的分派源
11. DISPATCH_SOURCE_TYPE_VNODE:用于监视文件描述符中定义的事件的分派源
12. DISPATCH_SOURCE_TYPE_WRITE:监视文件描述符以获取可写入字节的可用缓冲区空间的分派源。

1、dispatch_source_create 参数:

  • dispatch_source_type_t 要创建的源类型
  • uintptr_t 句柄 用于和其他事件并定,很少用,通常为0
  • uintptr_t mask 很少用,通常为0
  • dispatch_queue_t 事件处理的调度队列

用法:

self.sourceAdd = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_global_queue(0, 0));

2、dispatch_source_set_event_handler 设置回调函数,当触发源事件时执行

//需要注意循环引用
dispatch_source_set_event_handler(self.sourceAdd, ^{
需要执行的代码
});
//启动
dispatch_resume(self.sourceAdd);
//挂起,即暂停
dispatch_suspend(self.sourceAdd);
这两个API需要成对使用,不可多次挂起或者多次恢复

3、dispatch_source_cancel 取消事件源,取消后不可再恢复或挂起,需要再次创建
4、dispatch_source_set_timer 当事件源类型为定时器类型(DISPATCH_SOURCE_TYPE_TIMER)时,设置开始时间、重复时间、允许时间误差


定时器实现比较简单容易,网上教程也多,这里主要介绍一下:DISPATCH_SOURCE_TYPE_DATA_ADD、DISPATCH_SOURCE_TYPE_DATA_OR、DISPATCH_SOURCE_TYPE_DATA_REPLACE。


先说下结果:

  • DISPATCH_SOURCE_TYPE_DATA_ADD 会把事件源累加 可以记录总共发送多少次事件进行合并
  • DISPATCH_SOURCE_TYPE_DATA_OR 会把事件源合并,最终得到的数据源数为1
  • DISPATCH_SOURCE_TYPE_DATA_REPLACE 会用最新事件源替换旧有未处理事件,最终得到的数据源数为1
  • 循环10000次实际跑处理回调事件次数 add315 or275 replace 284

从结果上来看,当需要把快速频繁的重复事件进行合并,最好的选择是DISPATCH_SOURCE_TYPE_DATA_OR,使用场景,监听消息时,多消息频繁下发需要刷新UI,如果不进行合并处理,会导致UI太过频繁的刷新,影响最终效果,且对性能开销过大。


当然,类似的场景也可使用其他方式处理,比如建立消息池,接收消息后标记消息池状态及变化,然后定时从消息池中取消息。诸如此类的方法较多,如果只是简单的处理,上面的DISPATCH_SOURCE_TYPE_DATA_OR模式应该满足使用。


代码:

//创建源
self.sourceAdd = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_global_queue(0, 0));
//弱引用
__weak typeof(self) weakifySelf = self;
//设置回调事件
dispatch_source_set_event_handler(self.sourceAdd, ^{
//强引用
__strong typeof(self) strongSelf = weakifySelf;
//获取接收到的源数据
strongSelf.handleData = dispatch_source_get_data(strongSelf.sourceAdd);
NSLog(@"dispatch_source1 %ld\n",strongSelf.handleData);
//需要执行的代码
[strongSelf sourceHandle];

        });
//开启源
dispatch_resume(self.sourceAdd);
for (int i = 0; i<10000; i ++) {

[self dispatchSource];
}
- (void)dispatchSource{

    NSLog(@"dispatch_source2 %ld\n",self.handleData);
//发送源信号
    dispatch_source_merge_data(self.sourceAdd, 1);
}

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

自建”IT兵器库”,你值得一看!

web
现在市面的组件库,我们用的越发熟练,越发爆嗨,只需cv就能完成需求,为何不爆嗨!!! 常用库有 element、antd、iView、antd pro,这些组件库都是在以往的需求开发当中进行总结提炼,一次次符合我们的需求,只要有文档,只要有示例,页面开发都是小...
继续阅读 »

现在市面的组件库,我们用的越发熟练,越发爆嗨,只需cv就能完成需求,为何不爆嗨!!!


常用库有 element、antd、iView、antd pro,这些组件库都是在以往的需求开发当中进行总结提炼,一次次符合我们的需求,只要有文档,只要有示例,页面开发都是小问题,对吧,各位优秀开发前端工程师。


接下来,根据开发需求,一步步完成一个组件的开发,当然可能自己的思路,并不是最好的,欢迎大家留言讨论,一起进步


需求:


动态列表格,就是一个表格,存在默认列,但是支持我们操控,实现动态效果


实现效果


默认表格配置


image.png


默认列配置


image.png


动态列组件支持查询


image.png


动态列组件支持勾选


image.png


动态列组件支持清空


image.png


动态列组件支持一键全选


image.png


动态列组件支持一键清空


image.png


功能点划分



  • 表格默认列和动态列组件默认选中项 实现双向绑定

  • 动态列组件 增删改与表格 实现双向绑定

  • 动态列组件 实现搜索

  • 动态列组件 实现单点控制 添加与删除

  • 动态列组件 实现一键控制功能 全选与清空

  • 动态列组件 实现恢复初始态


使用到组件(Antd 组件库哈)



  • Table

  • Pagination

  • Modal

  • Input

  • Button

  • Checkbox


动态列组件区域划分



  • 头部标题

  • 头部提示语

  • 核心内容区



    • 核心区域头部功能按钮





    • 搜索区域





    • 左边所有内容项





    • 待选内容项




动态列组件最终可支持配置项


  open?: boolean // Modal状态
setOpen?: React.Dispatch> // 控制Modal状态
modalTitle?: string | React.ReactNode
modalWidth?: number
modalHeadContent?: React.ReactNode
leftHeadContent?: React.ReactNode | string
rightHeadContent?: React.ReactNode | string
modalBodyStyle?: any
searchPlaceholder?: string
modalOk?: (val, isUseDefaultData?: boolean) => void // 第二个参数 内部数据处理支持
enableSelectAll?: boolean // 是否开启全选功能
selectData: SelectItem[] // 下拉框数据
isOutEmitData?: boolean
defaultSelectKeyList?: string[] // 默认选中的key(当前表格列)自定义数据(外部做逻辑处理)
initSelectKey?: string[] // 初始表格选中的key 自定义数据(外部做逻辑处理)
curColumns?: any[] // 当前表格列 内部做逻辑处理
originColumns?: any[] // 原始表格列 内部做逻辑处理
isDelHeadCol?: boolean // 删除头部columnKey 例如序号 只有内部处理数据时生效
isDelTailCol?: boolean // 删除尾部columnKey 例如操作 只有内部处理数据时生效
isDelHeadAndTail?: boolean // 删除头尾部columnKey 只有内部处理数据时生效

动态列组件布局



    

// 头部内容区
{modalHeadContent}

// 以下维核心区


// 核心区-左边

// 核心区-功能按钮 - 一键全选
{enableSelectAll && (

onCheckBoxChange([], e)} checked={checkAll} indeterminate={indeterminate}>
全选


)}
{leftHeadContent || ''}



// 核心区-左搜索
{childSearchRender({ curData: leftSelectData })}

// 核心区-左列表区域
{selectItemRender(leftSelectData)}



// 核心区-右边


{rightHeadContent || ''}

// 核心区-功能按钮 - 一键清空
handleRightClearSelectData()}>
清空



// 核心区-右搜索
{childSearchRender({ curData: rightSelectData }, true)}

// 核心区-右列表区域
{selectItemRender(rightSelectData, true)}






动态列组件-列表渲染


const selectItemRender = (listArr = [], isRight = false) => {
return (


// 数据遍历形式
{listArr?.map(({ label, value, disabled = false }) => (

{!isRight && (

{label}

)}
// 判断是否是 右边列表区域 添加删除按钮
{isRight && {label}}
{isRight && (



)}

))}


)
}

动态列组件-搜索渲染


const childSearchRender = (childSearchProps: any, isRight = false) => {
// eslint-disable-next-line react/prop-types
const { curData } = childSearchProps
return (
{
onSearch(e, curData, isRight)
}}
allowClear
/>
)
}

动态列组件样式


.content-box {
width: 100%;
height: 550px;
border: 1px solid #d9d9d9;
}
.content-left-box {
border-right: 1px solid #d9d9d9;
}
.content-left-head {
padding: 16px 20px;
background: #f5f8fb;
height: 48px;
box-sizing: border-box;
}
.content-right-head {
padding: 16px 20px;
background: #f5f8fb;
height: 48px;
box-sizing: border-box;

&-clear {
color: #f38d29;
cursor: pointer;
}
}
.content-right-box {
}
.content-left-main {
padding: 10px 20px 0 20px;
height: calc(100% - 50px);
box-sizing: border-box;
}
.content-right-main {
padding: 10px 20px 0 20px;
height: calc(100% - 50px);
box-sizing: border-box;
}
.right-head-content {
font-weight: 700;
color: #151e29;
font-size: 14px;
}
.modal-head-box {
color: #151e29;
font-size: 14px;
height: 30px;
}
.icon-box {
color: #f4513b;
}
.ant-checkbox-group {
flex-wrap: nowrap;
}
.left-select-box {
height: 440px;
padding-bottom: 10px;
}
.right-select-box {
height: 440px;
padding-bottom: 10px;
}
.ant-checkbox-wrapper {
align-items: center;
}
.display-box {
height: 22px;
}

功能点逐一拆解实现


点1:表格默认列和动态列组件默认选中项 实现双向绑定



  • 首先,先写一个表格啦,确定列,这个就不用代码展示了吧,CV大法

  • 其次,把表格原始列注入动态列组件当中,再者注入当前表格列当前能选择的所有项

  • 当前能选择所有项内容参数示例


[
{ label: '项目编码', value: 'projectCode' },
{ label: '项目名称', value: 'projectName' },
{ label: '项目公司', value: 'company' },
{ label: '标段', value: 'lot' },
]




  • 动态组件内部默认选中当前表格列

  • 这里需要把表格列数据 进行过滤 映射成 string[]


   内部是通过checkbox.Grop 实现选中 我们只需要 通过一个状态去控制即可 `selectKey`
<Checkbox.Group onChange={onCheckChange} className="flex-col h-full flex" value={selectKey}>
...
Checkbox.Group>

动态列组件 单点控制 增删改



  • 增,删,改就是实现 左右边列表的双向绑定

  • 监听 左边勾选事件 + 右边删除事件 + 一键清空事件

  • 通过左右两边的状态 控制数据即可

  • 状态


  const [originSelectData, setOriginSelectData] = useState([]) // 下拉原始数据 搜索需要
const [originRightSelectData, setOriginRightSelectData] = useState([])
const [rightSelectData, setRightSelectData] = useState([])
const [selectKey, setSelectKey] = useState([])
const [transferObj, setTransferObj] = useState({})
const [indeterminate, setIndeterminate] = useState(false)
const [checkAll, setCheckAll] = useState(false)
const [leftSelectData, setLeftSelectData] = useState([])
const [defaultSelectKey, setDefaultSelectKey] = useState([])
const [originSelectKey, setOriginSelectKey] = useState([])

const onCheckChange = checkedValues => {
// 往右边追加数据
const selectResArr = checkedValues?.map(val => transferObj[val])
setSelectKey(checkedValues) // 我们选中的key (选项)
setRightSelectData(selectResArr) // 右边列表数据
setOriginRightSelectData(selectResArr) // 右边原数据(搜索时候需要)
}

const deleteRightData = key => {
const preRightData = rightSelectData
const filterResKeyArr = preRightData?.filter(it => it.value !== key).map(it => it.value) // 数据处理 只要你不等于删除的key 保留
const filterResItemArr = preRightData?.filter(it => it.value !== key)
setRightSelectData(filterResItemArr) // 更新右边数据 即可刷新右边列表
setOriginRightSelectData(filterResItemArr) // 更新右边数据
setSelectKey(filterResKeyArr) // 更新选中的key 即可刷新左边选中项
}

  const handleRightClearSelectData = () => {
// 这就暴力了塞
setSelectKey([])
setRightSelectData([])
setOriginRightSelectData([])
}

动态列组件 实现搜索



  • 搜索,就是改变一下数据 视觉上看起来有过滤的效果塞

  • 刚才我们不是多存了一份数据源嘛

  • 出来见见啦~


const onSearch = (val, curData, isRight = false) => {
const searchKey = val
// 这个是同时支持 左右两边
// 做个判断
if (!isRight) {
// 在判断一下是否有搜索内容 因为也需要清空的啦
if (searchKey) {
// 有,我就过滤呗
const searchResArr = curData?.filter(item => item.label.includes(searchKey))
setLeftSelectData(searchResArr)
}
if (!searchKey) {
// 没有 我就把原本数据还给你呗
setLeftSelectData(originSelectData)
}
}
// 右边 一样
if (isRight) {
if (searchKey) {
const searchResArr = curData?.filter(item => item.label.includes(searchKey))
setRightSelectData(searchResArr)
}
if (!searchKey) {
setRightSelectData(originRightSelectData)
}
}
}

动态列组件 增删改与表格 实现数据绑定



  • 里面的数据 处理好了 直接再关闭的时候 丢给外面嘛

  • 把右边的内容(也就是选中的key)返回给表格

  • 表格再自己构造


  const handleOk = (colVal, isUseDefaultCol) => {
`colVal` : 选中的列key
`isUseDefaultCol`:是否使用默认列
// table column字段组装
const normalColConstructor = (
title,
dataIndex,
isSort = true,
colWidth = 150,
isEllipsis = false,
render = null
) => {
const renderObj = render ? { render } : {}
return {
title,
dataIndex,
sorter: isSort,
width: colWidth,
ellipsis: isEllipsis,
key: dataIndex,
...renderObj,
}
}
const statusRender = text => approvalStatusRender(text)
const dateRender = (text, record) => {dayjs(text).format('YYYY-MM-DD')}
const newColArr = []
// 定制化处理 (其实还有2.0)
colVal?.forEach(({ label, value }, index) => {
let isSort = false
let renderFn = null
const isSubmissionAmount = value === 'submissionAmount'
const isApprovalAmount = value === 'approvalAmount'
const isReductionRate = value === 'reductionRate'
const isInitiationTime = value === 'initiationTime'

// 特定的业务场景 特殊状态渲染
const isStatus = value === 'status'
// 特定的业务场景 时间类型 加上排序
if (isApprovalAmount || isInitiationTime || isReductionRate || isSubmissionAmount) {
isSort = true
}
if (isStatus) {
renderFn = statusRender
}
// 普通列 已就绪
// 普通列 标题 拿label就ok
newColArr.push(normalColConstructor(label, value, isSort, 100, true, renderFn))
})

// 最后在头部追加一个序号
newColArr.unshift({
title: '序号',
dataIndex: 'orderCode',
width: 45,
render: (_: any, record: any, index) => tablePageSize * (tablePage - 1) + index + 1,
})
// 最后在尾部部追加一个操作
newColArr.push({
title: '操作',
dataIndex: 'action',
fixed: 'right',
width: 50,
render: (text, row: DataType) => (



),
})

if (colVal?.length) {
if (isUseDefaultCol) {
setColumns([...originColumns])
} else {
setColumns([...newColArr])
}
} else {
setColumns([...originColumns])
}
}

// 解决表格存在子列 -- 先搞定数据结构 -- 在解决表格列内容填充 -- 无需捆绑
// 遍历拿到新增列数组 匹配上代表 需要子数组 进行转换
// eslint-disable-next-line consistent-return
colVal?.forEach(({ label, value }, index) => {
// DesignHomeDynamicCol[value] 返回的值能匹配到 说明存在 嵌套列
const validVal = DesignHomeDynamicClassify[DesignHomeDynamicCol[value]]
const isHasChild = newColChildObj[validVal]
const titleText = DesignHomeDynamicLabel[value]
if (validVal) {
// 如果已经有孩子 追加子列
if (isHasChild) {
newColChildObj[validVal] = [...isHasChild, normalColConstructor(titleText, value)]
} else {
// 则 新增
newColChildObj[validVal] = [normalColConstructor(titleText, value)]
}
} else {
// 普通列 已就绪
// 普通列 标题 拿label就ok
newColArr.push(normalColConstructor(label, value, false, 100, true))
}
})

动态列组件 实现恢复初始态 实现双向绑定



  • 这个就更简单啦 再点击确定的时候 传一个 isUseDefaultData:true

  • 只是这个isUseDefaultData 的逻辑判断问题

  • 当动态列组件 点击恢复默认列 我们只需把 当初传进来的 原始列数据 更新到 selectKey 状态即可


const handleDefaultCol = () => {
// 这里是考虑到组件灵活性 数据可由自己处理好在传入
if (isOutEmitData) {
setSelectKey(initSelectKey)
} else {
// 这里是使用 内部数据处理逻辑
setSelectKey(originSelectKey)
}
}

const handleOk = () => { 
// 数据比对 是否使用默认校验
// originColumnMapSelectKey 源数据与传出去的数据 进行比对
const originRightMapKey = originRightSelectData?.map(it => it.value)
// 采用 lodash isEqual 方法
const isSame = isEqual(originSelectKey, originRightMapKey)
// 判断外部是否有传 确定事件 handleOk
if (modalOk) {
modalOk(originRightSelectData, isSame)
}
setOpen(false)
}

const handleOk = (colVal, isUseDefaultCol) => {
... 一堆代码
// 当用户清空以后 还是恢复表格默认状态
if (colVal?.length) {
// 恢复默认列
if (isUseDefaultCol) {
setColumns([...originColumns])
} else {
// 否则就拿新数据更新
setColumns([...newColArr])
}
} else {
setColumns([...originColumns])
}
}

动态列组件 实现一键控制功能 全选与清空



  • 这就是Vip版本的噻

  • 但是也简单 无非就是操作多选框 无非多选框就三种态

  • 未选 半选 全选

  • 既然我们下面的逻辑已处理好 这个其实也很快的锅

  • 首先,就是下面数据变化的时候 我们上面需要去感应

  • 其次就是 上面操作的时候 下面也需要感应

  • 最后 双向数据绑定 就能搞定 没有那么神秘

  • 一步一步来 先分别把 上下事件处理好


const onCheckBoxChange = (dataArr = [], e = null) => {
// 判断所有数据长度
const allLen = originSelectData?.length
// 根据当前选中数据长度 判断是 多选框三种状态当中的哪一种
const checkLen = e ? selectKey?.length : dataArr?.length // 全选
const isAllSelect = allLen === checkLen // 半选
const isHalfSelect = allLen > checkLen
// 然后再判断一下是点击一键全选事件 触发还是 点击下面选项的时候触发
// 点击一键全选 能拿到事件的 e.target 从而来判断
// 这里是操作下面按钮的时候 触发
if (!e) {
// 如果没有选中
if (checkLen === 0) {
// 恢复未选状态
setCheckAll(false)
setIndeterminate(false)
return ''
}
if (isAllSelect) {
// 如果是全选 改为全选态
setCheckAll(true)
setIndeterminate(false)
}
if (isHalfSelect) {
// 半选态
setIndeterminate(true) // 这个控制 多选框的半选态
setCheckAll(false)
}
}
// 这个就是用户操作 一键全选按钮触发
if (e) {
// 如果当前长度为0 那么应该更新为全选
if (checkLen === 0) {
setCheckAll(true)
setIndeterminate(false)
setSelectKey(originSelectData?.map(it => it.value))
}
// 如果已经全选 就取消全选
if (isAllSelect) {
setCheckAll(false)
setIndeterminate(false)
setSelectKey([])
}
// 如果是半选态 就全选
if (isHalfSelect) {
setCheckAll(true)
setIndeterminate(false)
setSelectKey(originSelectData?.map(it => it.value))
}
}
}

const onCheckChange = checkedValues => {
// 往右边追加数据
const selectResArr = checkedValues?.map(val => transferObj[val]) setSelectKey(checkedValues)
setRightSelectData(selectResArr)
setOriginRightSelectData(selectResArr)
}


  • 我们两个事件都处理好 那么开始进行联动

  • 意思就是 我们拿什么去控制这两个机关 核心是不是就是 选中的选项啊

  • 有两种解法,第二种可能有点绕



    • 一、就是在下面每个选项改变的时候 都去调一下 上面一键全选事件;然后上面变化的时候 也去调一下 下面选项的事件 (常规)缺点:容易漏 一变多改





    • 二、监听选项key的变化 去触发开关 避免我们第一种缺点 (优解)同时解决了确保外面传进来的 表格列 使得下拉框选中这些项 生效




// 这里通过 useEffect() 实现监听 确保外面传进来的 表格列 使得下拉框选中这些项 生效 
useEffect(() => {
onCheckBoxChange(selectKey)
onCheckChange(selectKey)
// eslint-disable-next-line react-hooks/exhaustive-deps },
[selectKey]
)

结束


都看到这里了,不留点痕迹,是怕我发现么?

作者:造更多的轮子
来源:juejin.cn/post/7266463919139684367

收起阅读 »

论如何在Android中还原设计稿中的阴影

每当设计稿上注明需要添加阴影时,Android上总是显得比较棘手,因为Android的阴影实现方式与Web和iOS有所区别。 一般来说阴影通常格式是有: X: 在X轴的偏移度 Y: 在Y轴偏移度 Blur: 阴影的模糊半径 Color: 阴影的颜色 何为阴影 ...
继续阅读 »

每当设计稿上注明需要添加阴影时,Android上总是显得比较棘手,因为Android的阴影实现方式与Web和iOS有所区别。


一般来说阴影通常格式是有:


X: 在X轴的偏移度


Y: 在Y轴偏移度


Blur: 阴影的模糊半径


Color: 阴影的颜色


何为阴影


但是在Android中却比较单一,只有一个度量单位:Elevation,作为在Android5.0的material2引入的概念,用一个图来形象的描绘一下,其实本质上就是虚拟的Z轴坐标。


image.png


那好,高度差有了,还差个光源,这样才能形成阴影,在material2中,光源不是单一的位于屏幕正上方的,而且有两组光源,分为主光源(Key light)和环境光源(Ambient light)如下图所示:
image.png


最终形成的效果是一种复合光源下更自然的阴影。


image.png


其中环境光源,在屏幕空间中没有实际的位置,但是主光源是有实际的位置的,具体的参数见:


frameworks/base/core/res/res/values/dimens.xml - Android Code Search
image.png


好,既然知道了阴影本身的机制,那下一步现在则是如何自定义控制阴影,这也是本文的目的。


从SDK 21开始,提供了Elevation可以实现类似于阴影的模糊半径的效果,但是毕竟尺度过于单一,往往有时候无法满足所需的效果,所以,还需要控制阴影的颜色。


在SDK 28之后,可以通过outlineSpotShadowColoroutlineAmbientShadowColor来分别设置Key light和Ambient light投射的阴影颜色,但是说实话,这两个属性基本用不到或者说比较鸡肋。


不过这里引入了一个概念:Outline。


四种常见方案


Elevation + Outline


Outline其实是View的边框(轮廓),通过OutlineProvider可以自定义一个View的Outline从而影响View本身在elevation下的投影,比如定义以实现一个圆角ImageView为例:


<ImageView
android:id="@+id/image"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@color/material_dynamic_primary90" />


image.clipToOutline = true
image.outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View?, outline: Outline?) {
view ?: return
outline?.setRoundRect(0, 0, view.width, view.height, 32f)
}

}

效果基本没啥问题:
image.png


同样的,既然View的轮廓变化了,阴影自然也会跟着随之变化,所以outline也可以改变阴影:


image.elevation = 32f
image.outlineAmbientShadowColor = Color.RED
image.outlineSpotShadowColor = Color.BLUE
image.clipToOutline = true
image.outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View?, outline: Outline?) {
view ?: return
outline?.setRoundRect(0, 0, view.width, view.height, 32f)
}

}

效果如下:(不过outlineAmbientShadowColoroutlineSpotShadowColor仅支持SDK 28及以上)


image.png


通常,到这一步通过调整elevation的数值和outline以及高版本可用的shadowColor大体上可以满足设计师的阴影需求。
而且通常来说shadowColor都是Color.Black以及alpha的区别,所以你也可以这样:


outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View?, outline: Outline?) {
view ?: return
outline?.alpha = 0.5f
outline?.setRoundRect(0, 0, view.width, view.height, 32f)
}
}

但是,还记着前面提到的两个光源吗?其中有一个光源是位于屏幕斜上方的,这就带来了另外一个问题,同一个View设置相同的Elevation在不同的Y轴坐标它的阴影效果是不一样的,如下图所示:



总之,阴影的Blur和Color参数勉强是可以得到满足的。


优点:原生的阴影效果


缺点:设置阴影的颜色需要SDK>=28,需要配合使用outline来实现对阴影的轮廓控制


下面我们先来引申一下Android中了解过的阴影实现方式。


LayerDrawable


我相信大家肯定见过这种实现方式,通过绘制一层层渐变色来模拟阴影,其实官方也有通过该方式实现的阴影:MaterialShapeDrawable,示例如下:


val drawable = MaterialShapeDrawable(
ShapeAppearanceModel.builder()
.setAllCornerSizes(16.dp)
.build()
)
drawable.fillColor = ColorStateList.valueOf(getColor(com.google.android.material.R.color.material_dynamic_primary90))
drawable.setShadowColor(Color.RED)
drawable.shadowVerticalOffset = 8.dp.toInt()
drawable.elevation = 32f
drawable.shadowCompatibilityMode = MaterialShapeDrawable.SHADOW_COMPAT_MODE_ALWAYS
image.background = drawable

效果如图:
image.png


只能说很一般,毕竟是模拟的阴影模糊效果,而且目前只支持Y轴的offset。


优点:几乎是开箱即用的Drawable且自带圆角


缺点:模拟的阴影效果,展示效果不够精细且效率不高


NinePatchDrawable


说实话想在Android上实现一个简单的阴影太折腾了,什么奇怪的技巧都来了,比如.9图,至于什么是.9图这里便不再过多介绍。
通过这个网站:Android Shadow Generator (inloop.github.io)


image.png
你可以直接生成一个CSS Style的阴影效果,几乎可以完美还原Figma的阴影效果,效果如下:
image.png


其实还是很还原的,但是它有一个致命的缺点,就是圆角,因为是一张图片,所以圆角的单位本质上是px而非Android上的dp,如果你需要一个带圆角弧度的阴影是达不到预期的。


优点:参数完全可控的阴影,可以做到1:1还原设计稿


缺点:因为是图片,所以阴影的圆角无法跟随像素密度缩放(非常致命的缺点)


Paint.setShadowLayer/BlurMaskFilter


这两个我之所以放在一起本质上是因为实现起来都是类似的,
如:


paint.setShadowLayer(radius, offsetX, offsetY, shadowColor)
// 或者使用maskFilter然后通过paint.color以及绘制的区域进行offset来变相控制阴影的Color及offset
paint.maskFilter = BlurMaskFilter(field, BlurMaskFilter.Blur.NORMAL)

相比之下更推荐使用setShadowLayer,最终效果如下,基本上没啥问题:
image.png


但是值得注意的是,其绘制的阴影本质上等价于BlurMaskFilter,是占位的,而且是需要留出空间来展示的,所以必要时需要对父布局设置android:clipChildren="false"或者预留出足够的空间。


优点:


1. 参数完全可控的阴影,可以做到1:1还原设计稿


2. 参数的自定义程度及可控性强


缺点:


1. 阴影占位,需要通过clipChildren=false来或者预留空间规避


2. 需要自定义View或者Drawable,写起来较为麻烦。


总的来说,上面介绍了4种可能常见的阴影实现方式,其中按我的经验来说,较为推荐采用Outline或者setShadowLayer的方式来实现,如果可以的话原生Elevation配合Outline基本可以满足大部分需求场景。


当然还有部分实现方式比如用RenderScriptBlur等等,我没提是因为是前几种方式较为复杂,性价比不高。


Paint.setShadowLayer 扩展内容


下面则重点讲一下Paint.setShadowLayer/BlurMaskFilter这种方式,为什么说这两种方式实现的阴影都是一致的呢?这个就需要深入到C++层。
首先直接跳到paint.setShadowLayer的native实现类:
frameworks/base/libs/hwui/jni/Paint.cpp


Paint.cpp - Android Code Search


    static void setShadowLayer(CRITICAL_JNI_PARAMS_COMMA jlong paintHandle, jfloat radius,
jfloat dx, jfloat dy, jlong colorSpaceHandle,
jlong colorLong)
{
SkColor4f color = GraphicsJNI::convertColorLong(colorLong);
sk_sp<SkColorSpace> cs = GraphicsJNI::getNativeColorSpace(colorSpaceHandle);

Paint* paint = reinterpret_cast<Paint*>(paintHandle);
if (radius <= 0) {
paint->setLooper(nullptr);
}
else {
SkScalar sigma = android::uirenderer::Blur::convertRadiusToSigma(radius);
paint->setLooper(BlurDrawLooper::Make(color, cs.get(), sigma, {dx, dy}));
}
}


里面将我们传入的阴影radius参数转为Sigma并创建了BlurDrawLooper,我们来看看其实现


#include "BlurDrawLooper.h"
#include <SkMaskFilter.h>

namespace android {

BlurDrawLooper::BlurDrawLooper(SkColor4f color, float blurSigma, SkPoint offset)
: mColor(color), mBlurSigma(blurSigma), mOffset(offset) {}

BlurDrawLooper::~BlurDrawLooper() = default;

SkPoint BlurDrawLooper::apply(Paint* paint) const {
paint->setColor(mColor);
if (mBlurSigma > 0) {
paint->setMaskFilter(SkMaskFilter::MakeBlur(kNormal_SkBlurStyle, mBlurSigma, true));
}
return mOffset;
}

sk_sp<BlurDrawLooper> BlurDrawLooper::Make(SkColor4f color, SkColorSpace* cs, float blurSigma,
SkPoint offset)
{
if (cs) {
SkPaint tmp;
tmp.setColor(color, cs); // converts color to sRGB
color = tmp.getColor4f();
}
return sk_sp<BlurDrawLooper>(new BlurDrawLooper(color, blurSigma, offset));
}

} // namespace android

内容不多,可以看到本质上还是利用了setMaskFilter来实现的。


然后还剩下一个点就是通过SkMaskFilter::MakeBlur生成的模糊是占位的,如果能知道模糊具体需要多大的空间,就可以方便的进行预留以免实际展示时阴影被裁剪。
MakeBlur最终返回的是一个SkBlurMaskFilterImpl对象,我们可以先看一下其父类SkMaskFilterBase的虚函数:重点关注computeFastBounds函数


SkMaskFilterBase.h - Android Code Search


    /**
* The fast bounds function is used to enable the paint to be culled early
* in the drawing pipeline. This function accepts the current bounds of the
* paint as its src param and the filter adjust those bounds using its
* current mask and returns the result using the dest param. Callers are
* allowed to provide the same struct for both src and dest so each
* implementation must accommodate that behavior.
*
* The default impl calls filterMask with the src mask having no image,
* but subclasses may override this if they can compute the rect faster.
*/

virtual void computeFastBounds(const SkRect& src, SkRect* dest) const;

可以看到该函数的作用便是计算MaskFiter的bounds,看一下子类的SkBlurMaskFilterImpl的实现


void SkBlurMaskFilterImpl::computeFastBounds(const SkRect& src,
SkRect* dst)
const
{
// TODO: if we're doing kInner blur, should we return a different outset?
// i.e. pad == 0 ?

SkScalar pad = 3.0f * fSigma;

dst->setLTRB(src.fLeft - pad, src.fTop - pad,
src.fRight + pad, src.fBottom + pad);
}

其中fSigme便是最开始通过convertRadiusToSigma(radius)获取到的返回值,其计算方式如下:
SkBlurMask.cpp - Android Code Search


// This constant approximates the scaling done in the software path's
// "high quality" mode, in SkBlurMask::Blur() (1 / sqrt(3)).
// IMHO, it actually should be 1: we blur "less" than we should do
// according to the CSS and canvas specs, simply because Safari does the same.
// Firefox used to do the same too, until 4.0 where they fixed it. So at some
// point we should probably get rid of these scaling constants and rebaseline
// all the blur tests.
static const SkScalar kBLUR_SIGMA_SCALE = 0.57735f;

SkScalar SkBlurMask::ConvertRadiusToSigma(SkScalar radius) {
return radius > 0 ? kBLUR_SIGMA_SCALE * radius + 0.5f : 0.0f;
}

这样,我们可以得到一个模糊的近似Bound,虽然不是一个准确的值但是至少可以保证绘制的阴影不会被裁剪。
当然,如果无法预留Padding也可以通过clipChildren=false来实现。


总结


最后我也是针对setShadowLayer提供了一个自定义View的实现方式:


Lowae/Shadows: A simple and customizable library on Android to implement CSS style shadows (github.com)


感兴趣的可以尝试使用,有任何兼容性问题欢迎提issue~



(我十分清楚会有很多兼容性问题,没办法,这种Api就是这样,不,准确来说,Android就是这样)



所以,想在Android上1:1还原设计稿上的阴影是比较困难的,但是如果不去追求参数的还原只是寻求视觉的略显一致,那还是可以做到的,简单点的通过第一种方式(Elevation + Outline),如果设置到阴影颜色或者offset这种便可以尝试最后一种方式(

作者:Lowae
来源:juejin.cn/post/7270503053358874664
setShadowLayer)。

收起阅读 »

卸下if-else 侠的皮衣!- 适配器模式

web
🤭当我是if-else侠的时候 😶怕出错 给我一个功能,我总是要写很多if-else,虽然能跑,但是维护起来确实很难受,每次都要在一个方法里面增加逻辑,生怕搞错,要是涉及到支付功能,分分钟炸锅 😑难调试 我总是不知道之前写的逻辑在哪里,一个方法几百行逻辑,而且...
继续阅读 »

🤭当我是if-else侠的时候


😶怕出错


给我一个功能,我总是要写很多if-else,虽然能跑,但是维护起来确实很难受,每次都要在一个方法里面增加逻辑,生怕搞错,要是涉及到支付功能,分分钟炸锅


😑难调试


我总是不知道之前写的逻辑在哪里,一个方法几百行逻辑,而且是不同功能点冗余在一起!这可能让我牺牲大量时间在这查找调试中


🤨交接容易挨打


当你交接给新同事的时候,这个要做好新同事的白眼和嘲讽,这代码简直是坨翔!这代码简直是个易碎的玻璃,一碰就碎!这代码简直是个世界十大奇迹!


🤔脱下if-else侠的皮衣


先学习下开发的设计原则


单一职责原则(SRP)



就一个类而言,应该仅有一个引起他变化的原因



开放封闭原则(ASD)



类、模块、函数等等应该是可以扩展的,但是不可以修改的



里氏替换原则(LSP)



所有引用基类的地方必须透明地使用其子类的对象



依赖倒置原则(DIP)



高层模块不应该依赖底层模块



迪米特原则(LOD)



一个软件实体应当尽可能的少与其他实体发生相互作用



接口隔离原则(ISP)



一个类对另一个类的依赖应该建立在最小的接口上



在学习下设计模式


大致可以分三大类:创建型结构型行为型

创建型:工厂模式 ,单例模式,原型模式

结构型:装饰器模式,适配器模式,代理模式

行为型:策略模式,状态模式,观察者模式


上篇文章学习了 策略模式,有兴趣可以过去看看,下面我们来学习适配器模式


场景:将一个接口返回的数据,转化成列表格式,单选框数据格式,多选框数据格式


用if-else来写,如下


let list = []
let selectArr = []
let checkedArr = []

http().then(res =>{
//处理成列表格式
this.list = this.handel(res,0)
//处理成下拉框模式
this.selectArr = this.handel(res,1)
//处理成多选框模式
this.checkedArr = this.handel(res,2)
})
handel(data,num){
if(num == 0){
....
}else if(num ==1){
....
}else if(num ==2){
....
}
}

分下下问题,假如我们多了几种格式,我们又要在这个方法体里面扩展,违反了开放封闭原则(ASD),所以我们现在来采用适配器模式来写下:


//定义一个Adapter
class Adpater {
data = [];
constructor(){
this.data = data
}
toList(col){
//col代表要的字段,毕竟整个data的字段不是我们都想要的
//转列表
return this.data.map(item =>{
const obj = {}
for(let e of col){
const f = e.f
obj[f] = item[f];
}
return obj
})
}
//用了map的省略写法,来处理转化单选的格式
toSelect(opt){
const {label,value} = opt
return this.data.map(item => ({
label,
value
}))
}
//同上处理转化多选的格式
toChecked(opt){
const {field} = opt
return this.data.map(item => ({
checked:false,
value:item[field]
}))
}
}

//下面是调用这个适配类
let list = []
let selectArr = []
let checkedArr = []
http.then(data =>{
const adapter = new Adatpter(data)
//处理列表
list = adapter.toList(['id','name','age'])
//处理下拉
selectArr = adapter.toSelect({
label:'name'
value:'id'
})
//处理多选
checkedArr = adapter.toChecked({
field:'id'
})
})

这个扩展性就能大大提高,看着也会好看很多,可以通过继承等等方式来扩展,继承方式下次有机会再来写代码,文章先到这里!


结尾


遵守设计规则,脱掉if-else的皮衣,善用设计模式,加

作者:向乾看
来源:juejin.cn/post/7265694012962537513
油,骚年们!给我点点赞,关注下!

收起阅读 »

当文字成为雨滴:HTML、CSS、JS创作炫酷的"文字雨"动画!

web
简介 大家好,今天要给大家带来一个Super Cool的玩意儿😎! 在本篇技术文章中,将介绍如何使用HTML、CSS和JavaScript创建一个独特而引人注目的"文字(字母&数字)"雨🌨️动画效果。通过该动画,展现出的是一系列随机字符将从云朵中下落...
继续阅读 »

简介


大家好,今天要给大家带来一个Super Cool的玩意儿😎!


rain-preview.gif


在本篇技术文章中,将介绍如何使用HTML、CSS和JavaScript创建一个独特而引人注目的"文字(字母&数字)"雨🌨️动画效果。通过该动画,展现出的是一系列随机字符将从云朵中下落像是将文字变成雨滴从天而降,营造出与众不同的视觉效果;


HTML


创建一个基本的HTML结构,这段HTML代码定义了一个容器,其中包含了"云朵"和"雨滴"(即文字元素)。基本结构如下:



  • 首先是类名为container的容器,表示整个动画的容器;

  • 其次是类名为cloud的容器,表示云朵的容器;

  • 接着是cloud容器中的文字元素,表示雨滴(即文字元素);
    然后引入外部创建的css和js文件,可以先定义几个text容器,用于调整样式;


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Text Rain Animation</title>

<link rel="stylesheet" href="./css/style.css">
</head>
<body>
<div class="container">
<div class="cloud">
<!-- <div class="text">a</div> -->
<!-- <div class="text">b</div> -->
<!-- <div class="text">c</div> -->
<!-- 雨滴将会在这里出现 -->
</div>
</div>

<script src="./js/main.js"></script>
</body>
</html>

CSS


CSS是为文字雨效果增色添彩的关键,使动画效果更加丰富,关于一些 CSS 样式:



  • 使用了自定义的颜色变量来为背景色和文本颜色提供值,有助于使代码易于维护和修改;

  • 利用CSS的阴影效果和动画功能,创造逼真的"云朵"和流畅的"雨滴"动画;


* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

:root {
--body-color: #181c1f;
--primary-color: #ffffff;
}

body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: var(--body-color);
}

.container {
width: 100%;
height: 400px;
display: flex;
justify-content: center;
border-bottom: 1px solid rgba(255, 255, 255, .1);
/* 添加一个从下往上线性渐变的镜像效果,增加视觉层次感 */
-webkit-box-reflect: below 1px linear-gradient(transparent, transparent, transparent, transparent, #0005);
}

.cloud {
position: relative;
top: 50px;
z-index: 100;

/* 横向云朵 */
width: 320px;
height: 100px;
background-color: var(--primary-color);
border-radius: 100px;

/* drop-shadow函数将阴影效果应用于投影图像 */
filter: drop-shadow(0 0 30px var(--primary-color));
}
.cloud::before {
content: "";
/* 左侧小云朵 */
width: 110px;
height: 110px;
background-color: var(--primary-color);
border-radius: 50%;
position: absolute;
top: -50px;
left: 40px;

/* 右侧大云朵 */
box-shadow: 90px 0 0 30px var(--primary-color);
}

.cloud .text {
position: absolute;
top: 40px;
height: 20px;
line-height: 20px;

text-transform: uppercase;
color: var(--primary-color);
/* 为文字添加阴影,看上去发光,增加视觉效果 */
text-shadow: 0 0 5px var(--primary-color), 0 0 15px var(--primary-color), 0 0 30px var(--primary-color);
transform-origin: bottom;
animation: animate 2s linear forwards;
}

@keyframes animate {
0% {
transform: translateX(0);
}

70% {
transform: translateY(290px);
}

100% {
transform: translateY(290px);
}
}

通过关键帧动画 @keyframes animate 定义文字运动的过程,在这里是垂直移动290px,也就是向下移动,模拟下雨的状态。当然为了让文字雨效果更加好看,还可以引入一下字体库;



Warning


-webkit-box-reflect:可将元素内容在特定方向上进行轴对称反射;


但是该特性是非标准的,请尽量不要在生产环境中使用它!


目前只有webkit内核的浏览器支持,如:谷歌浏览器、Safari浏览器。在火狐浏览器中是不支持的;



JavaScript


最后,使用JavaScript来实现文字雨的效果。通过动态生成并随机选择字符,可以实现让这些字符(雨滴)从.cloud(云朵)中降落的效果。JavaScript 脚本逻辑:



  • 首先,定义函数 generateText() 并创建字符集,定义函数 randomText() 通过从给定的字符集中随机选择一个字符返回;

  • 接下来,编写 rain() 函数,在函数内部,首先选取 .cloud 元素同时创建一个新的 <div>元素作为字符节点,设置元素文本内容为函数返回的字符,并添加类名;

  • 然后,利用 Math.random() 方法生成一些随机值,将这些随机值应用到创建的 <div> 元素上,包括:

    • 字符距离左侧位置,在 .cloud 容器的宽度区间;

    • 字体大小,最大不超过32px;

    • 动画周期所需的时间(动画持续时间),2s内;



  • 最后,将其<div>添加到 .cloud 元素中,使用 setTimeout() 函数在2秒后将文字节点从 .cloud 元素中移除,模拟雨滴落地消失效果;


定时器: 为了让字符(雨滴)持续下落,使用 setInterval 函数和一个时间间隔值来调用 rain() 函数。这样就是每20毫秒就会生成一个新的字符(雨滴)节点并添加到云朵中。


// 生成字母和数字数组
function generateText() {
const letters = [];
const numbers = [];

const a = "a".charCodeAt(0);

for (let i = 0; i < 26; i++) {
letters.push(String.fromCharCode(a + i));

if (i < 9) {
numbers.push(i + 1);
}
};

return [...letters, ...numbers];
};

// 从生成的数组中随机取出一个字符
function randomText() {
const texts = generateText();
const text = texts[Math.floor(Math.random() * texts.length)];

return text;
};

function rainEffect() {
const cloudEle = document.querySelector(".cloud");
const textEle = document.createElement("div");

textEle.innerText = randomText();
textEle.classList.add("text");

const left = Math.floor(Math.random() * 310);
const size = Math.random() * 1.5;
const duration = Math.random();
const styleSheets = {
left: `${left}px`,
fontSize: `${0.5 + size}em`,
animationDuration: `${1 + duration}s`,
};
Object.assign(textEle.style, styleSheets);

cloudEle.appendChild(textEle);
setTimeout(() => {
cloudEle.removeChild(textEle);
}, 2000);
};

// 每隔20ms创建一个雨滴元素
setInterval(() => rainEffect(), 20);

结论


通过HTML、CSS和JS的紧密合作,成功创建了一个炫酷的"文字雨"动画效果,这个动画可以增加网页的吸引力! 不要犹豫🖐️,动手尝试一下,或者甚至你也可以根据自己的需求对文字、样式和动画参数进行调整,进一步改善和扩展这个效果;


希望这篇文章对你在开发类似动画效果时有所帮助!另外如果你对这个案例还有任何问题,欢迎在评论区留

作者:掘一
来源:juejin.cn/post/7270648629378367528
言或联系(私信)我。谢谢阅读!🎉

收起阅读 »

大厂产品为何集体下架

快手一个 30 多人的产品团队每月开销上百万,其中技术、产品和运营是开支大头。如果碰上产品商业化能力不行,那这就是铁亏。 昨天看了Tech星球发表的一篇文章,说的是各互联网大厂集中下架 60 多款 App 的事儿,有些感想。 不可否认,互联网已经过了最激进的时...
继续阅读 »

快手一个 30 多人的产品团队每月开销上百万,其中技术、产品和运营是开支大头。如果碰上产品商业化能力不行,那这就是铁亏。


昨天看了Tech星球发表的一篇文章,说的是各互联网大厂集中下架 60 多款 App 的事儿,有些感想。


不可否认,互联网已经过了最激进的时期,尤其是过去十年以移动互联网为代表的这一轮技术周期。


接下来,几乎所有的互联网企业都会关注成本和效益。


说白了,要赚稳当的钱并好好活下去。


而那些烧钱的产品、业务、部门,则大概率都会关停并转。


光是今年,腾讯就已经下架了包括游戏在内的 40 多款 App,其中不乏 QQ 堂这样的元老级产品。


除此之外,其他大厂也都在进行同样的操作,产品下架意味着团队解散,对应就会出现人员缩减。


我知道,很多人看到这样的信息一定会产生焦虑,认为互联网是不是不行了?找工作是不是更难了?会不会面临裁员失业了?


这里,我想说几个内在的逻辑关系。


先说很多人关心的裁员和失业问题,以及应对策略。


对于企业来说,他们会优先从自身立场出发进行决策,在面对发展危机时,开源节流、壮士断腕、集中火力就会成为优先考虑。


这种影响首先会辐射到边缘业务和产品以及探索型项目上,直接关停就是解决方案,以此释放出来的资源可以投入到主营业务上,而多余的资源则对外释放。


这里说的对外释放,其实也就是裁员。



被裁员的对象通常符合这么几个特征,一线螺丝钉、高成本中层、多余的高层。


因此,要想避免自己陷入这样的困境,至少有这么几件事可以做。


第一,选择公司和业务时尽量避开边缘部门和探索创新性业务,比如去主营电商业务的公司做社交产品。


可能有人会说了,没办法啊,有时候选择权不在自己,能找到工作就不错了,哪还能挑三拣四。


不过我想说,这种破罐子破摔的思想一定会害人不浅。


没选择的前提一定是没本事,而没本事的原因就是学习和实践能力不行。


不管是公司还是社会,都是典型金字塔结构,越往上,能容纳的空间就越小。


当别人奋力往上爬的时候,你还在原地踏步,更好的工作机会自然就不会属于你,这是现实。


所以,别再抱怨工作不好找,这是从外界找原因。先看看自己有什么,从内在找问题。


浑浑噩噩过下去是一种选择,精明筹划过下去也是一种选择。选择不同,结果不同。


第二,尽早建立自己的职业标签,尽快找到自己的关键位置,尽力打造自己的不可替代性。


职业标签、关键位置、不可替代性,这三点是一个成熟专业且有独特价值职场人的标配。


职业标签是专业能力的体现,SaaS 产品专家、电商供应链产品专家、数据产品专家、搜索和策略产品专家等,这都是职业标签。


什么都会,什么都不精,是不可能形成职业标签的,也就无法获得第二个优势,关键位置。


关键位置是你所在部门和公司的岗位,不一定是领导,但一定是一个不可或缺的角色。


比如,负责搭建公司后台业务系统的产品经理,对整体结构和细节是最了解的那个人。


获得了关键位置的前提是职业标签,而获得关键位置后就能进而获得第三个优势,不可替代性。


遇上公司裁员,可能会裁掉一线大头兵,但不会裁掉处于关键位置的人。铁打的营盘,流水的兵,只要核心还在,就可以继续生长。


职业标签、关键位置、不可替代性,评估一下自己有没有,缺不缺。


问题找到了,才有形成解决方案的可能性。


第三,不断为自己打造优质的信息和人脉渠道,你可以不喜欢圈子,但你不得不混圈子。


任何一个领域都会有对应的圈子,那里聚集了人、信息、机会,越是专业的圈子,这三方面的质量就越高。


应届生找工作靠校招,工作几年后找工作靠社招,工作多年后找工作靠朋友,理解了这一点,你就知道信息和人脉渠道有多重要了。


不知道你们有没有一种感觉,这几年世界变化很快,这种变化带来了太多的不确定性,让原本的稳定得以打破。


有的人认为这是危机,还有的人认为这是机会。要我看,越是不确定性增强的危机时刻,可探索的机会其实就更多。


原因很简单,稳定格局的建立意味着机会窗口的关闭,而稳定被打破,说明机会窗口的重新打开。


我对未来还是持有乐观态度的,即便寒气逼人,我们依旧可以拥有一颗火热向前的内心。



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

如果你的退路有1亿人在卷了,阁下又该如何应对?

你所认为的退路,其实已经有1亿人在卷了,"那么阁下将如何应对?" 引子 最近进了一些技术交流群,群友们也是来自五湖四海,有些碰巧还在找工作,自嘲 「“大不了去送外卖”、“大不了去开滴滴”」 这句话在工作中有时候大家也经常会有这样的调侃。 其实这几年互联网头部公...
继续阅读 »

你所认为的退路,其实已经有1亿人在卷了,"那么阁下将如何应对?"


引子


最近进了一些技术交流群,群友们也是来自五湖四海,有些碰巧还在找工作,自嘲 「“大不了去送外卖”、“大不了去开滴滴”」 这句话在工作中有时候大家也经常会有这样的调侃。


其实这几年互联网头部公司都比较难做,在21年 2记重拳打向这个行业:打击打击资本无序扩张与规范平台经济发展、反垄断。阿里和美团前年也都收到了182亿和32亿的天价罚单,这不最近蚂蚁不是又被罚了71亿,降本增效、开源节流成为各大公司的主旋律,毕竟_"地主家也没有余粮啦"_,再加上坊间传闻35岁的潜规则,大家的危机感还是挺强的。


送快递、送外卖、跑滴滴,这些职业因为没有什么门槛,也没有什么年龄限制,只要你愿意吃苦,就能稳定赚到钱,可是事实上,这些真的是不错的退路吗?真相可能跟你想象的完全不一样。




卷起来


在今年3月份时候,中华总工会发布了一个统计:目前全国职工总数4.02亿人,而从 「事外卖、快递、网约车等新就业形态的群体,已达8400万人」





「也就是说:你以为的退路,实际上已经有一亿人在卷了」



网约车


记得在五月份的时候流传过一个 段子“报名滴滴网约车由于饱和被劝退到美团外卖 ”,网约车行业被视为一个门槛较低的行业,只要会开车,身体健康没有犯罪记录,就可以轻松成为网约车司机,无论专职还是兼职,都是一个不错的选择。所以,最近几年,网约车成为我国一个相当重要的灵活就业市场,也在很大程度上减轻了我国的就业压力。「司机们纷纷涌入网约车行业,但是网约车的订单数量却在持续萎缩。」



数据显示,2022年,我国网约车用户规模为4.37亿人,同比减少了3%,网约车市场规模约为3100多亿元,同比下降1.4%,



今年5月,中国多个城市,发布了 「网约车饱和预警,劝大家谨慎入行」「长沙、三亚等城市,已暂停受理网约车运输证新增业务」。 考虑到同期司机数量的激增,那么势必会造成 网约车司机能够接到的订单迅速减少。很多城市的网约车司机日均接单量不到10单,和前几年相比大概下降了一半。「过去很多网约车司机可以月收入轻松过万元,而现在想要这个目标难度加了不止一点点。」


外卖



 今年3月份广州出现有史以来第一次外卖骑手“爆满”!想当外卖骑手还得托关系。外卖已经成为知识密集型行业,广州外卖骑手本科率接近30%,毕竟全国人口的本科率也只有4.4%左右。情况变化之快令人咂舌,前几年还有篇文章 “被困在系统里面出不去的美团骑手”,今年却出现了 “想要到系统里面却进不去”。 美团财报显示,2021年美团骑手的数量为527万,2022年,美团骑手的数量变成了624万,2022年,美团骑手的数量变成了624万,其中81.6%是来自县域乡村地区的农村转移劳动力,平台一年新增骑手的人数达到了97万 


 而行业进入饱和期之后,不可避免的就是降薪了。这对于本不富裕的外卖小哥来说无疑是雪上加霜 


做好当下


其实呢 也不需要气馁或者过于焦虑,了解一些事实,有助于我们更好的把握好当下。看清楚事实,坦诚的面对自己,“知人者智,自知者明”。面对当前互联网的大环境,可以主动思考一下自己的“护城河”是什么,高端的学历?大厂的履历?扎实的技术?殷实的家境?甚至是人脉广情商高。如果暂时想不起的来话,还是得行动起来去有意识的构建属于自己的“护城河”。每个人都是独一无二的,至少我们可以从认真做好当下的本职工作做起,最后跟大家共享一句我比较喜欢的一句话



yesterday is history,tomorrow is a mystery,but today is a gift



做好当下,尽人事听天命~

作者:Android茶话会

来源:juejin.cn/post/7270871863160700984

收起阅读 »

人情世故职场社会生存实战篇(二)

这个专栏文章都是群友问的实际问题,我在这里分享出来,希望可以帮助到大家,本篇是本专栏的第二篇,欢迎大家踊跃发言 人情人情世故职场社会生存实战篇(一)人情人情世故职场社会生存实战篇(二)11、问:如何改善和领导的关系? 答:1、工作上有能力。现在社会变化快,三、...
继续阅读 »

这个专栏文章都是群友问的实际问题,我在这里分享出来,希望可以帮助到大家,本篇是本专栏的第二篇,欢迎大家踊跃发言


人情人情世故职场社会生存实战篇(一)

人情人情世故职场社会生存实战篇(二)

11、问:如何改善和领导的关系?


答:1、工作上有能力。现在社会变化快,三、五年组织环境,内部的领导可能都会有大的变化,自己不能无所作为的等着,要努力提升自己的工作能力,业余时间学习工作的各个环节的东西,毕竟职场还是要有人干活的,领导再差劲也不能只用小人,还需要具体的做事的人,有真本事不怕。


2、权力上能维护。有工作能力的人很多,但是越是有工作能力的人,越是要处处维护领导的权力,早请示晚汇报,让他觉得一切都在掌控之中,有功归上。这样的工作才能得到领导的认可。如果没有权力意识,工作能力越强,就让领导越是感到威胁,很多有能力的人,对领导毫不尊重,自然备受打击。


3、生活上凑交集。现在人分的很清楚,工作是工作,生活是生活,下班了也就没有什么交集了,工作关系如果未能产生私交,两个人的关系不会稳定,私交是关系的粘合剂。所以一定要有私下的交集,这个交集要偶然,也就是要凑机会,多了领导烦。把握好度,比如孩子在一个培训班,妻子在一个瑜伽馆。


12、问:我是刚毕业两年的研究生,在国企工作。我得知一个消息,我有一个老乡任命为我们单位的财务副总,下个月就会到岗,我想请他提携我一下。我该怎么做呢?


答:首先打铁必须自身硬,他下个月到岗,我建议你不要一来就仗着自己是他老乡就张口请人帮你,没有这样的道理。你可以仔细的写一下你了解的单位的现状、存在的问题,以及你预想的以后未来发展的方向,形成一份书面材料。等他到岗后,你借向他汇报工作的时候交给他,让他看到你不是废铁一块,这样子人家自然更乐意帮你。其次,职场中关系也非常重要的,有关系就有一切,你呀,先组个局,请老乡吃个饭,祝贺一下,把原来不温不火的关系回温一下,关系到位了,你不用求,别人自然会帮。


13、问:老师我实习快结束了,需要转正,想给领导送礼,请问怎么送比较好?


答:很多人给领导送礼,送了一条烟,马上说:我转正的事儿,麻烦您了。你转正的事儿,又不是他一个人说了算,所以你的东西,他收了,反而睡不着,甚至会梦到自己进去了。你一直送,领导说,转正的事儿,你争取下。你说:争取个啥啊,在哪儿不是做贡献啊。领导说,你先走个程序嘛,你勉为其难说:哎,那就走个吧。失败了,咱说:我说我不行吧,你非要让我上,我啥水平我不知道吗,谢谢您高看我。领导说:别急,下次再试试,你说不试了,我对我目前的待遇非常满意。


你没野心,他反而帮你了,也就是你不能让领导有压力。有人说了,马勒戈壁,我都送礼了,为什么还忍着,我只能说对不起兄弟,你别忍了,你去爆炸吧。领导让你试试,你成功了,你要说:哎呦,又是你帮我使劲了。一句话,去送就好了,习惯性送有价值的东西 ,如果领导拒绝你,你就说,这个是自己的一点小心意,在这实习期期间,你在公司学习到了很多,也跟领导学习了很多,一直感恩就好了。


14、问:因为我工作失误,害的我部门领导被大领导骂,还要帮我各方协调挽回损失。我想买点礼物感谢一下我部门领导,但是从来没有过给领导送东西的经验,请问我什么时候送合适啊 ?是现在事情刚过就送 ?还是过年过节有合适机会了送啊。


答:你傻呀!肯定是现在马上去办了,此时过去,你名正言顺的感谢领导帮你挽回了工作上的失误,并表示,如果没有他,你可能就丢了这份工作,或者造成更大的损失,然后感激之情,无以言表,送上一点小心意(烟酒+红包),请领导一定要收下,不然自己寝食难安。人呐,都喜欢懂得感恩的人。


15、问:老师,您好!单位中层调整,之前托领导前辈已给老大打过招呼,礼数走过了,现事已到跟前,私下和老总见面,老大说竞争很激烈,走程序,请老师指点下一步怎么走?


答:老大的话,其实意思也比较明显了,“竞争很激烈”=打招呼的人很多,表心意的人很多,“走程序”=你送的不够,咱就走程序,你送的够,咱就插队,走后门,老大的意思,就是现在 你这边没啥竞争力,你看看要不要加码。


如何破局?你找前辈领导在出一次面,你把心意给前辈领导,让他帮你给老大。很多事儿,可以通过送梨搞定。愿送,永远都有机会。不愿送,只能走程序……在咱们这儿,领导说,该走的程序,咱必须走。这就意味着有戏。要是领导说,走正常程序吧,意味着这个事儿啊,100%搞不定。


16、问:老师请问找老大办事,给老大送礼,老大说送礼破坏感情,说你的心意我领了,我会找机会帮你办的,我该怎么办?


答:心意我领了,我会找机会帮你办的,这个就是典型套话,你要是当真了,你的事就会一直杳无音讯,没有下文。这个时候,你要说,领导,你帮我太多了,然后列举123件事,一直感恩就好了。你说,领导,没有你的关照,就没有我的今天,可能这些帮助,都是您随手为之的,但是,对我的帮助是巨大的,这个恩我一直记得,这点心意您要是不收下,我真的问心有愧,以后都不敢找你指点工作,指点人生了。领导这心意我就放着了,我还有事我先走了,我那个事不打紧,如果麻烦的话,领导您千万别为难。


17、问:老师我身边有个同事,嘴有点J,喜欢乱开玩笑,我不知道怎么怼他,每次虽然我没说啥,但心里是很不高兴的。比如我昨天穿了一件新衣服,他说你新衣服真的是不得了,像个新郎官,怎么着又想做一次新郎官,那样可不行。那是犯了生活作风问题,不过你可以偷偷摸摸的夜夜做新郎。


答:有时候你让他一小步,他就会前进一大步。由刚开始不怀好意的开玩笑,慢慢变成欺负你。对于这种毫无顾忌的玩笑,必须给予反击。你可以这样说:我是既没那贼心也没那贼胆,老婆管得严,不像哥哥你在哪里都没人敢管,你能夜夜做新郎,我只能做一回新郎。


18、问:单位风气不正,很多老员工总是不干活,同事也是甩活给我,我该怎么办?我是一个合同工,单位的关系错综复杂,一杆子打下去,他们都是关系户,很多老员工仗着资历老,都不干活,同事也是照猫画虎,喜欢把活甩给我,请问,我是要学会拒绝,还是继续接受?


答:一般来说,单位里面就几种人老是被针对。


1、新人,太嫩,总是希望通过表达自己的热情和善意,以期希望得到对方的帮助,其实没必要!你够强,你有价值,别人就会尊重你,你一来就暴露底牌,别人就知道怎么拿捏你了。


2、软人,性格太软,别人听你说话就知道你是一个可以欺负的人,你要学会提高自己的气场。


3、孤立的人,这种人不会抱团,狮子落单了,鬣狗都要去掏肛,更别提小绵羊了,狮子、野狗,狼群都会群殴它。


4、情商低,不会跟领导搭关系,没有领导撑腰你是合同工,你也可以很硬气,谁知道单位的大领导不会是你舅舅?你硬气一点,别人还会觉得你有关系,反倒是不敢欺负你,这里,你会拒绝就行了,会推,会找借口,很好办的。人性本贱,这是我在群内一直讲的,你好说话,别人就喜欢欺负你你不好说话,别人反而尊重你。


19、问:我有个领导超级傻叉,工作上很多事情的流程和解决办法,她都不清楚,遇到问题她也搞不明白,就比如这两天,其他部门的员工都知道的消息,就只有我们部门的员工不知道,问题是她也不知道,搞的我们乱成一团麻,然后还瞎喷瞎指挥。后面我自己去找其他部门问情况,才得知最新消息和工作流程!


我是非常不明白,为什么像这种蠢H一样的货色,都能当领导?能力那么差就不要出来嚯嚯别人了好吗!请问如何对待这种领导?是不是能力不行的领导都喜欢通过折腾别人,在一些细枝末节而没有意义的事情是精益求精,才能彰显自己的存在感?


答:没有能力,但是却做了你的领导,那么她必然有她优势的一面,比如关系!不然,她又如何到了这个位置?一将无能,累死三军,主将无能,其实就是一个很大的机会,篡位的宰相,为什么都喜欢皇帝昏庸无能?因为他只要控制了这个蠢货,他就控制了整个朝堂!一人之下,万人之上,甚至他就是无冕之王。


所以,这里你的答案也很简单,你只要会向上管理,想办法帮她消化所有的问题,让自己成为她离不开的人。每次遇到问题了,你就带着通盘的方案去跟她讨论该怎么办,流程如何,细节如何,无形之中把你的方案植入他的脑子里面,然后以后遇到事情,她跟任何人都搭不来,但是跟你特别有缘,你说是不是很有趣。这还只是初步的,你要知道她的背后一定有更强的关系,想办法融入进去,在职场中你一步一高升,不是更简单?问题就是机遇,就看你拥有多宽广的视野了!


20、问:老师您好,我认识一个退休的老领导(不在本地),他跟现在我们市局一把手关系好。最近我们要有一波提拔,我看中一个位置。我想通过这个老领导给我们一把手送礼,我该怎么办?我该怎么说让这个老领导引荐一下比较合适?如果他愿意帮忙的话,怎么给我们一把手送礼合适?


答:退休了,人走茶凉。他有没有能量,就一个判断标准∶他的儿女立起来了吗?要是他的儿女都没立起来。你想立,他也爱莫能助。人,只要退休了,说话跟放屁一样,无人接招。他跟你一把手关系好,这个关系好必须建立在有共同利益的基础上,没有这个基础,这个关系就是假的。再说,动用他的资源帮你,他的回报率难道就是烟酒茶?他缺这点儿东西?一句话, 你送礼没戏..想想你能为他提供什么其他价值......


欢迎同学们关注本专栏,会持续更新社会上的人情世故,有问题的同学也欢迎留言~


作者:公z号_纵横潜规则
来源:juejin.cn/post/7268050036474232851

收起阅读 »

继copilot之后,又一款免费帮你写代码的插件

写在前面 在之前的文章中推荐过一款你写注释,它就能帮你写代码的插件copilot。 copilot写代码的能力没得说,但是呢copilot试用没几天之后就收费了。 按理说这么好用,又可以提高效率的工具,收点费也理所当然 但是秉承白嫖一时爽,一直白嫖一直爽的原则...
继续阅读 »

写在前面


在之前的文章中推荐过一款你写注释,它就能帮你写代码的插件copilot。


copilot写代码的能力没得说,但是呢copilot试用没几天之后就收费了。


按理说这么好用,又可以提高效率的工具,收点费也理所当然


但是秉承白嫖一时爽,一直白嫖一直爽的原则(主要是我穷穷穷),又发现了一款可以平替的插件CodeGeex


一、CodeGeex简介


① 来自官方的介绍



CodeGeeX is a powerful intelligent programming assistant based on LLMs. It provides functions such as code generation/completion, comment generation, code translation, and AI-based chat, helping developers significantly improve their work efficiency. CodeGeeX supports multiple programming languages.



翻译过来大概是



CodeGeeX是一个功能强大的基于llm的智能编程助手。它提供了代码生成/完成、注释生成、代码翻译和基于ai的聊天等功能,帮助开发人员显著提高工作效率。CodeGeeX支持多种编程语言。



GitHub地址


github.com/THUDM/CodeG…


目前在GitHub上 2.6k star 最近更新是2周前




③ 下载量

  • vscode 目前已有129k下载量
  • idea 目前已有58.7k 下载量

二、插件安装


① vscode




②idea


注: idea低版本的搜不到这个插件,小编用的是2023.01 这个版本的




安装完成后,注册一个账号即可使用


三、帮你写代码

① 我们只需要输入注释回车,它就可以根据注释帮你写代码

② tab接受一行代码 ctrl+space 接受一个单词








四、帮你添加注释



有时候,我们拿到同事没有写注释的代码,或者翻看一周前自己写的代码时。


这写得啥,完全看不懂啊,这时候就可以依靠它来帮我们的代码添加注释了



操作方法:

① 选中需要添加注释的代码

② 鼠标右键选择Add Comment

③ 选择中文或者英文






这是没加注释的代码

public class test02 {
   public static void main(String[] args) {
       int count=0;
       for(int i=101;i<200;i+=2) {
           boolean flag=true;
           for(int j=2;j<=Math.sqrt(i);j++) {
               if(i%j==0) {
                   flag=false;
                   break;
              }
          }
           if(flag==true) {
               count++;
               System.out.println(i);
          }
      }
       System.out.println(count);
  }
}

这是CodeGeex帮加上的注释

public class test02 {
   //主方法,用于执行循环
   public static void main(String[] args) {
       //定义一个变量count,初始值为0
       int count=0;
       //循环,每次循环,计算101到200之间的值,并判断是否是因子
       for(int i=101;i<200;i+=2) {
           //定义一个变量flag,初始值为true
           boolean flag=true;
           //循环,每次循环,计算i的值,并判断是否是因子
           for(int j=2;j<=Math.sqrt(i);j++) {
               //如果i的值不是因子,则flag设置为false,并跳出循环
               if(i%j==0) {
                   flag=false;
                   break;
              }
          }
           //如果flag为true,则count加1,并打印出i的值
           if(flag==true) {
               count++;
               System.out.println(i);
          }
      }
       //打印出count的值
       System.out.println(count);
  }
}

基本上每一行都加上了注释,这还怕看不懂别人写的代码


五、帮你翻译成其他语言



除了上面功能外,CodeGeeX 还可以将一种语言的代码转换成其他语言的代码



操作方法:

① 选中需要转换的代码

② 鼠标右键选择Translation mode

③ 在弹出的侧边栏中选择需要转换成的语言,例如C++、 C#、Javascript 、java、Go、Python、C 等等

④ 选择转换按钮进行转换






六 小结


试用了一下,CodeGeeX 还是可以基本可以满足需求的,日常开发中提高效率是没得说了


作为我这样的穷逼,完全可以用来平替copilot,能白嫖一天是一天~


也不用当心哪天不能用了,等用不了了再找其他的呗




本期内容到此就结束了


希望对你有所帮助,我们下期再见~ (●'◡'●)


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

小米备案 mios.cn 网站域名

8月22日消息,近期有关小米科技备案新域名和自研操作系统的消息引起了广泛关注。根据最新披露的信息,小米科技有限责任公司于去年11月11日备案了mios.cn网站域名。然而,当前访问该域名时遇到了403错误,意味着资源暂时不可用,同时网站也尚未设置HTTPS安全...
继续阅读 »

8月22日消息,近期有关小米科技备案新域名和自研操作系统的消息引起了广泛关注。根据最新披露的信息,小米科技有限责任公司于去年11月11日备案了mios.cn网站域名。然而,当前访问该域名时遇到了403错误,意味着资源暂时不可用,同时网站也尚未设置HTTPS安全证书。



关于自研操作系统的消息更是引发了热议。有博主在今年8月15日爆料称,小米正在积极打磨一款自研操作系统,并已取得了阶段性进展。然而,这一爆料后来却被删除,导致外界对其真实性产生了疑问。值得注意的是,同一博主在今日上午发布了新消息,表示小米的自研系统将实现全端覆盖,并且兼容了AOSP(Android开放源代码项目)。尽管有人曾称MIUI 15将更名为mios,但博主在最新消息中否认了这一说法。

此外,一些人或许会对小米为何没有采用更通用的mios.com域名产生疑惑。事实上,据ITBEAR科技资讯了解,该域名已经被他人注册,目前被用于一个国外的物联网设备自动化网站。

尽管目前关于小米备案域名和自研操作系统的信息仍存一定的不确定性,然而这些信息的传播已经引发了业界和用户的广泛关注。未来,我们可以期待小米在科技领域带来更多令人期待的创新。

作者:ITBEAR科技资讯

来源:www.itbear.com.cn/html/2023-08/469514.html

收起阅读 »

App本地配置持久化方案

iOS
概述 在App开发过程中,会遇到很多简单配置项的持久化需求。比如App最近一次启动的时间,App最后一次登陆的用户ID,用户首次使用功能的判断条件。并且随着业务的扩展,零碎的配置还会不断增加。 UserDefaults Apple提供了UserDefault框...
继续阅读 »

概述


在App开发过程中,会遇到很多简单配置项的持久化需求。比如App最近一次启动的时间,App最后一次登陆的用户ID,用户首次使用功能的判断条件。并且随着业务的扩展,零碎的配置还会不断增加。


UserDefaults


Apple提供了UserDefault框架来帮助我们存储离散的配置,UserDefaults将以plist文件的形式存储在沙盒环境中。在不引入NoSql数据库的情况下,这是首推的方案。


注意事项


为了提升读取速度,App在启动时会将UserDefaults Standard对应的plist加载到内存中,如果文件过大就会增加App在启动时的加载时间,同时提高一定的内存消耗。


所以在Standard中,我们应该存放需要在App启动阶段立即获取的信息,比如用户最近登录的ID,App远程配置缓存的版本。


我们可以通过分表来缩减Standard的数据量。使用UserDefaults的suiteName模式创建不同的配置表,这样配置项将存储到各自的plist文件中,这些独立的plist不会在启动时被自动加载。


配置管理的常见问题

  1. 使用硬编码的String Key将配置存储到UserDefaults中,通过复制粘贴Key的字符串来存取数据。

  2. 零散的使用UserDefaults,缺少中心化管理方案。比如需要存储“开启通知功能”的配置,Key通常会直接被放在业务相关代码中维护。


方案 1.0


管理UserDefaults


创建一个UserDefault的管理类,主要用途是对UserDefault框架使用的收口,统一使用策略。

public class UserDefaultsManager {
public static let shared = UserDefaultsManager()
private init() {}
public var suiteName:String? {
didSet {
/**
根据传入的 suiteName的不同会产生四种情况:
传入 nil:跟使用UserDefaults.standard效果相同;
传入 bundle id:无效,返回 nil;
传入 App Groups 配置中 Group ID:会操作 APP 的共享目录中创建的以Group ID命名的 plist 文件,方便宿主应用与扩展应用之间共享数据;
传入其他值:操作的是沙箱中 Library/Preferences 目录下以 suiteName 命名的 plist 文件。
*/
userDefault = UserDefaults(suiteName: suiteName) ?? UserDefaults.standard
}
}
public var userDefault = UserDefaults.standard
}


创建常量表

  1. 对配置项的Key进行中心化的注册与维护
struct UserDefaultsKey {
static let appLanguageCode = "appLanguageCode"
static let lastLaunchSaleDate = "resetLastLaunchSaleDate"
static let lastSaleDate = "lastSaleDate"
static let lastSaveRateDate = "lastSaveRateDate"
static let lastVibrateTime = "lastVibrateTime"
static let exportedImageSaveCount = "exportedImageSaveCount"

static let onceFirstLaunchDate = "onceFirstLaunchDate"
static let onceServerUserIdStr = "onceServerUserIdStr"
static let onceDidClickCanvasButton = "onceDidClickCanvasButton"
static let onceDidClickCanvasTips = "onceDidClickCanvasTips"
static let onceDidClickEditBarGuide = "onceDidClickEditBarGuide"
static let onceDidClickEditFreestyleGuide = "onceDidClickEditFreestyleGuide"
static let onceDidClickManualCutoutGuide = "onceDidClickManualCutoutGuide"
static let onceDidClickBackgroundBlurGuide = "onceDidClickBackgroundBlurGuide"
static let onceDidTapCustomStickerBubble = "onceDidTapCustomStickerBubble"
static let onceDidRequestHomeTemplatesFromAPI = "onceDidRequestHomeTemplatesFromAPI"

static let firSaveExportTemplateKey = "firSaveExportTemplateKey"
static let firSaveTemplateDateKey = "firSaveTemplateDateKey"
static let firShareExportTemplateKey = "firShareExportTemplateKey"
static let firShareTemplateDateKey = "firShareTemplateDateKey"
}

2. 提供CURD API
private let appConfigUserDefaults = UserDefaultsManager(suiteName: "com.pe.config").userDefaults

var exportedImageSaveCount: Int {
return appConfigUserDefaults.integer(forKey: key)
}

func increaseExportedImageSaveCount() {
let key = UserDefaultsKey.exportedImageSaveCount
var count = appConfigUserDefaults.integer(forKey: key)
count += 1
appConfigUserDefaults.setValue(count, forKey: key)
}

我们对UserDefaults数据源进行了封装,String Key的注册也统一到常量文件中。当我们要查找或修改时,可以从配置表方便的查到String Key。


随着业务的膨胀,配置项会越来越多,我们会需要根据业务功能的分类,重新整理出多个分表。


随后我们会发现一些问题:

  1. String Key的注册虽然不麻烦,但Key中无法体现出Key归属与哪个UserDefaults。

  2. CURD API的数量会膨胀的更快,需要更多的维护成本。那么能不能将配置的管理更加面向对象,实现类似ORM的方式来管理呢?


方案2.0


根据上述的问题,来演化下方案2.0,我们来创建一个协议,用来规范UserDefaults的使用类。


它将包含CURD API的默认实现,初始化关联UserDefaults,自动生成String Key。

/// UserDefaults存储协议,建议用String类型的枚举去实现该协议
public protocol UserDefaultPreference {

var userDefaults: UserDefaults { get }
var key: String { get }

var bool: Bool { get }
var int: Int { get }
var float: Float { get }
var double: Double { get }

var string: String? { get }
var stringValue: String { get }

var dictionary: [String: Any]? { get }
var dictionaryValue: [String: Any] { get }

var array: [Any]? { get }
var arrayValue: [Any] { get }

var stringArray: [String]? { get }
var stringArrayValue: [String] { get }

var data: Data? { get }
var dataValue: Data { get }

var object: Any? { get }
var url: URL? { get }

func codableObject<T: Decodable>(_ as:T.Type) -> T?

func save<T: Encodable>(codableObject: T) -> Bool

func save(string: String)
func save(object: Any?)
func save(int: Int)
func save(float: Float)
func save(double: Double)
func save(bool: Bool)
func save(url: URL?)
func remove()
}

定义完协议后,我们再添加一些默认实现,降低使用成本。

// 生成默认的String Key
public extension UserDefaultPreference where Self: RawRepresentable, Self.RawValue == String {
var key: String { return "\(type(of: self)).\(rawValue)" }
}

public extension UserDefaultPreference {
// 默认使用 standard UserDefaults,可以在实现类中配置
var userDefaults: UserDefaults { return UserDefaultsManager.shared.userDefaults }

func codableObject<T: Decodable>(_ as:T.Type) -> T? {
return UserDefaultsManager.codableObject(`as`, key: key, userDefaults: userDefaults)
}

@discardableResult
func save<T: Encodable>(codableObject: T) -> Bool {
return UserDefaultsManager.save(codableObject: codableObject, key: key, userDefaults: userDefaults)
}

var object: Any? { return userDefaults.object(forKey: key) }

func hasKey() -> Bool { return userDefaults.object(forKey: key) != nil }

var url: URL? { return userDefaults.url(forKey: key) }

var string: String? { return userDefaults.string(forKey: key) }
var stringValue: String { return string ?? "" }

var dictionary: [String: Any]? { return userDefaults.dictionary(forKey: key) }
var dictionaryValue: [String: Any] { return dictionary ?? [String: Any]() }

var array: [Any]? { return userDefaults.array(forKey: key) }
var arrayValue: [Any] { return array ?? [Any]() }

var stringArray: [String]? { return userDefaults.stringArray(forKey: key) }
var stringArrayValue: [String] { return stringArray ?? [String]() }

var data: Data? { return userDefaults.data(forKey: key) }
var dataValue: Data { return userDefaults.data(forKey: key) ?? Data() }

var bool: Bool { return userDefaults.bool(forKey: key) }
var boolValue: Bool? {
guard hasKey() else { return nil }
return bool
}

var int: Int { return userDefaults.integer(forKey: key) }
var intValue: Int? {
guard hasKey() else { return nil }
return int
}

var float: Float { return userDefaults.float(forKey: key) }
var floatValue: Float? {
guard hasKey() else { return nil }
return float
}

var double: Double { return userDefaults.double(forKey: key) }
var doubleValue: Double? {
guard hasKey() else { return nil }
return double
}

func save(object: Any?) { userDefaults.set(object, forKey: key) }
func save(string: String) { userDefaults.set(string, forKey: key) }
func save(int: Int) { userDefaults.set(int, forKey: key) }
func save(float: Float) { userDefaults.set(float, forKey: key) }
func save(double: Double) { userDefaults.set(double, forKey: key) }
func save(bool: Bool) { userDefaults.set(bool, forKey: key) }
func save(url: URL?) { userDefaults.set(url, forKey: key) }

func remove() { userDefaults.removeObject(forKey: key) }
}

OK,我们来看下使用的案例

// MARK: - Launch
enum LaunchEventKey: String {
case didShowLaunchGuideOnThisLaunch
case launchGuideIsAlreadyShow
}
extension LaunchEventKey: UserDefaultPreference { }

func checkIfNeedLaunchGuide() -> Bool {
return !LaunchEventKey.launchGuideIsAlreadyShow.bool
}
func launchContentView() {
LaunchEventKey.launchGuideIsAlreadyShow.save(bool: true)
}

// MARK: - Language
enum LanguageEventKey: String {
case appLanguageCode
}
extension LanguageEventKey: UserDefaultPreference { }

static var appLanguageCode: String {
get {
let code = LanguageEventKey.appLanguageCode.string ?? ""
return code
}
set {
LanguageEventKey.appLanguageCode.save(codableObject: newValue)
}
}

// MARK: - Purchase
enum PurchaseStatusKey: String {
case iapSubscribeExpireDate
}
extension PurchaseStatusKey: UserDefaultPreference { }

func handle() {
let expirationDate: Date = Entitlement.expirationDate
PurchaseStatusKey.iapSubscribeExpireDate.save(object: expirationDate)
}

func getValues() {
let subscribeExpireDate = PurchaseStatusKey.iapSubscribeExpireDate.object as? Date
}

// MARK: - GlobalConfig
enum AppConfig: String {
case globalConfig
}

private let appConfigUserDefaults = UserDefaultsManager(suiteName: "com.pe.AppConfig").userDefaults

extension AppConfig: UserDefaultPreference {
var userDefaults: UserDefaults { return appConfigUserDefaults }
}

// 自定义类型
public class GlobalConfig: Codable {
/// 配置版本号
let configVersion: Int
/// 用户初始试用次数
let userInitialTrialCount: Int
/// 生成时间 如:2022-09-19T02:58:31Z
let createDate: String

enum CodingKeys: String, CodingKey {
case configVersion = "version"
case userInitialTrialCount = "user_initial_trial_count"
case createDate = "create_date"
}
...
}

lazy var globalConfig: GlobalConfig = {
guard let config = AppConfig.globalConfig.codableObject(GlobalConfig.self) else {
return GlobalConfig()
}
return config
}() {
didSet { AppConfig.globalConfig.save(codableObject: globalConfig) }
}

从上述案例可以看出,在配置项的注册和维护成本相对方案1.0有了大幅度的降低,对UserDefaults的使用进行了规范性的约束,提供了更方便的CURD API,使用方式也更加符合面向对象的习惯。


同时为了满足复杂结构体的存储需求,我们可以扩展实现Codable对象的存取逻辑。


总结


本方案的目的是解决乱象丛生的UserDefaults的使用情况,分析后向两个方向进行了优化:

  1. 提供中心化的配置方式,关联UserDefaults、维护String Key。
  2. 提供类ORM的管理方式,减少业务的接入成本。


针对更复杂的、类缓存集合的,或者有查询需求的配置项管理,请尽快用NoSQL替换,避免数据量上升带来的效率下降。


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

七夕节那天,我被裁员了

前言 先祝jym昨天七夕节快乐吧,有情人终成眷属 一直想在掘金上发文但迟迟未行动,上学时在b乎写了一段时间一直切换不来平台。看着b乎仅存的几百号关注者到这边只能从0开始很是不情愿( ̄Д ̄)ノ,也行吧就从零开始了,我的工作似乎也得从零开始了。该死的仪式感... ...
继续阅读 »

前言


先祝jym昨天七夕节快乐吧,有情人终成眷属


一直想在掘金上发文但迟迟未行动,上学时在b乎写了一段时间一直切换不来平台。看着b乎仅存的几百号关注者到这边只能从0开始很是不情愿( ̄Д ̄)ノ,也行吧就从零开始了,我的工作似乎也得从零开始了。该死的仪式感...




难忘的七夕


如标题所见,七夕节被通知裁员了。单身狗今天受到双重痛击。说的是公司迫于压力需要节流,部门会裁1/4。而我恰好就是那个“幸运儿”。其实从前些时间就早有风向,先是ui,到测试,终于到开发人员了,而到了开发最先动刀的果然还就是鼠鼠前端。


23届真的太惨了。在这里从实习到转正也快干了1年了,但才刚拿了一个月转正工资就遇到调整,实属难绷。


应届生身份也没了,工作年限也达不到,双重叠buff了属于是,老实说刚听到这消息的时候自己还是懵的,也就和往常一样上班怎么今天就这么突然开这样的会议呢?


而后是无奈,但是又有点兴奋,正好可以逼自己再出去外面探探,看看市场如何。

回家路上,打开手机刷了刷boss,要么招24的要么一到两年工作经验的,一下子又把我难住了。。。我怎么这么背啊5555


整个晚上我都在思考人生,我在想假如自己不是读计算机会如何?我是不是能没负担的尝试各种工作,自己真的要在程序员这条赛道一条路走到黑吗?


程序员的工作又何尝不是围墙,技术类的工作是建立起了行业壁垒,在外行人看来能将代码变成程序是一件很酷的事,而正是这道壁垒,让外行的人想进来,里面的人想走却又鼓不起勇气。




是啊 我除了写代码还能干什么呢?难道真的放弃学了这么久在外行人看来很厉害的技术去做其他工作吗?(脱不下的长衫又穿上了)


总结


我一直认为无论发生什么事,一切都是上天最好的安排,世上无所谓输赢祸福,关键是从中有所收获,写几点感悟吧:

  • 拥抱不确定性是踏入社会后最需要学会的。读书时每个人都是按部就班,除了学习剩下的挑战就是安排到日程上的考试,而步入社会每天遇到的事情都不一样,so be water my friend 随机应变 随遇而安
  • 负能量可以有,适当摆烂一下,但睡一觉起来就得调整好心态了,享受生活不要被蛋疼的事影响
  • 不要畏手畏脚,敞开自己而不是压抑自己,一辈子很短,很多事情不去尝试就来不及了
  • 灵活就业或许才是这个时代下的答案?不知道
  • 行动起来最重要,如前面说的想在掘金发文却拖延了很久,而正是这回吐槽却让我奋笔疾书了起来,命运的齿轮或许就此转动,后面我会督促自己继续更开发过程中的杂记、生活记录
  • 接下来的安排:先编辑个简历再出发,然后整理一下这一年来开发的东西,记录一下

结语


我也不知道后面会如何,走一步是一步吧,通知还没正式下来。如果读到这里的你觉得我我是个有趣的人,那么点个关注吧,这也是我更文的动力


失败并不存在,关键是故事仍在继续......


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

iOS 拖拽式控件:QiDragView

iOS
首先,我们先看一下QiDragView的效果图:  一、QiDragView整体架构设计 话不多说,上架构图~ QiDragView(QiDragSortView)是一种可选择可拖拽的自定义控件,可以满足一些拖拽排序的业务需求场景。 二、如何自定义...
继续阅读 »

首先,我们先看一下QiDragView的效果图: 


一、QiDragView整体架构设计


话不多说,上架构图~



QiDragView(QiDragSortView)是一种可选择可拖拽的自定义控件,可以满足一些拖拽排序的业务需求场景。


二、如何自定义使用QiDragView?


在上Demo之前,先介绍几个可以自定义的UI配置属性:



以及一些逻辑配置属性:



使用起来也很方便:

  • 直接设置titles即可创建出对应title的Buttons。
  • 通过dragSortEnded的block方法回调,处理拖拽后的业务逻辑:按钮的排序、按钮是否选择等属性

默认配置用法:

QiDragSortView *dragSortView = [[QiDragSortView alloc] initWithFrame:CGRectMake(.0, 100.0, self.view.bounds.size.width, .0)];
dragSortView.backgroundColor = [[UIColor lightGrayColor] colorWithAlphaComponent:.5];

dragSortView.titles = @[@"首页推荐", @"奇舞周刊", @"众成翻译", @"QiShare", @"HULK一线杂谈", @"Qtest之道"];//!< 初始的Buttons(必填)
[self.view addSubview:dragSortView];

//! 拖拽方法回调:能拿到Button数组的排序和选择状态
dragSortView.dragSortEnded = ^(NSArray<UIButton *> * _Nonnull buttons) {
for (UIButton *button in buttons) {
NSLog(@"title: %@, selected: %i", button.currentTitle, button.isSelected);
}
};

自定义配置用法:

QiDragSortView *dragSortView = [[QiDragSortView alloc] initWithFrame:CGRectMake(.0, 100.0, self.view.bounds.size.width, .0)];
dragSortView.backgroundColor = [[UIColor lightGrayColor] colorWithAlphaComponent:.5];
dragSortView.rowHeight = 50.0;
dragSortView.rowMargin = 30.0;
dragSortView.rowPadding = 20.0;
dragSortView.columnCount = 3;
dragSortView.columnMargin = 30.0;
dragSortView.columnPadding = 20.0;
dragSortView.normalColor = [UIColor redColor];
dragSortView.selectedColor = [UIColor purpleColor];
dragSortView.enabledTitles = @[@"奇舞周刊", @"众成翻译", @"QiShare", @"HULK一线杂谈", @"Qtest之道"];//!< 可以点击选择的Buttons(选填,默认全选)
dragSortView.selectedTitles = @[@"首页推荐", @"HULK一线杂谈", @"Qtest之道"];//!< 初始选择的Buttons(选填,默认全选)
dragSortView.titles = @[@"首页推荐", @"奇舞周刊", @"众成翻译", @"QiShare", @"HULK一线杂谈", @"Qtest之道"];//!< 初始的Buttons(必填)
[self.view addSubview:dragSortView];

//! 拖拽方法回调:能拿到Button数组的排序和选择状态
dragSortView.dragSortEnded = ^(NSArray<UIButton *> * _Nonnull buttons) {
for (UIButton *button in buttons) {
NSLog(@"title: %@, selected: %i", button.currentTitle, button.isSelected);
}
};

三、QiDragView的技术点


3.1 长按手势:


长按手势分别对应三种状态:UIGestureRecognizerStateBeganUIGestureRecognizerStateChangedUIGestureRecognizerStateEnded

//! 长按手势
- (void)longPress:(UILongPressGestureRecognizer *)gesture {

UIButton *currentButton = (UIButton *)gesture.view;

if (gesture.state == UIGestureRecognizerStateBegan) {

[self bringSubviewToFront:currentButton];

[UIView animateWithDuration:.25 animations:^{
self.originButtonCenter = currentButton.center;
self.originGesturePoint = [gesture locationInView:currentButton];
currentButton.transform = CGAffineTransformScale(currentButton.transform, 1.2, 1.2);
}];
}
else if (gesture.state == UIGestureRecognizerStateEnded) {

[UIView animateWithDuration:.25 animations:^{
currentButton.center = self.originButtonCenter;
currentButton.transform = CGAffineTransformIdentity;
} completion:^(BOOL finished) {
if (self.dragSortEnded) {
self.dragSortEnded(self.buttons);
}
}];
}
else if (gesture.state == UIGestureRecognizerStateChanged) {

CGPoint gesturePoint = [gesture locationInView:currentButton];
CGFloat deltaX = gesturePoint.x - _originGesturePoint.x;
CGFloat deltaY = gesturePoint.y - _originGesturePoint.y;
currentButton.center = CGPointMake(currentButton.center.x + deltaX, currentButton.center.y + deltaY);

NSInteger fromIndex = currentButton.tag;
NSInteger toIndex = [self toIndexWithCurrentButton:currentButton];

if (toIndex >= 0) {
currentButton.tag = toIndex;

if (toIndex > fromIndex) {
for (NSInteger i = fromIndex; i < toIndex; i++) {
UIButton *nextButton = _buttons[i + 1];
CGPoint tempPoint = nextButton.center;
[UIView animateWithDuration:.5 animations:^{
nextButton.center = self.originButtonCenter;
}];
_originButtonCenter = tempPoint;
nextButton.tag = i;
}
}
else if (toIndex < fromIndex) {
for (NSInteger i = fromIndex; i > toIndex; i--) {
UIButton *previousButton = self.buttons[i - 1];
CGPoint tempPoint = previousButton.center;
[UIView animateWithDuration:.5 animations:^{
previousButton.center = self.originButtonCenter;
}];
_originButtonCenter = tempPoint;
previousButton.tag = i;
}
}
[_buttons sortUsingComparator:^NSComparisonResult(UIButton *obj1, UIButton *obj2) {
return obj1.tag > obj2.tag;
}];
}
}
}

3.2 配置按钮:


设计思路:在属性titles的setter方法中,初始化并配置好各个Buttons。

- (void)setTitles:(NSArray<NSString *> *)titles {

_titles = titles;

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSInteger differCount = titles.count - self.buttons.count;

if (differCount > 0) {
for (NSInteger i = self.buttons.count; i < titles.count; i++) {
[self.buttons addObject:[self buttonWithTag:i]];
}
}
else if (differCount < 0) {
NSArray *extraButtons = [self.buttons subarrayWithRange:(NSRange){titles.count, self.buttons.count - titles.count}];
[self.buttons removeObjectsInArray:extraButtons];
for (UIButton *button in extraButtons) {
[button removeFromSuperview];
}
}

self.enabledTitles = self.enabledTitles ?: titles;//!< 如果有,就传入,否则传入titles
self.selectedTitles = self.selectedTitles ?: titles;

for (NSInteger i = 0; i < self.buttons.count; i++) {
[self.buttons[i] setTitle:titles[i] forState:UIControlStateNormal];
[self.buttons[i] addGestureRecognizer:[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPress:)]];//!< 长按手势
[self selectButton:self.buttons[i] forStatus:[self.selectedTitles containsObject:titles[i]]];
if ([self.enabledTitles containsObject:titles[i]]) {
[self.buttons[i] addTarget:self action:@selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside];
}
}

for (NSInteger i = 0; i < self.buttons.count; i++) {
NSInteger rowIndex = i / self.columnCount;
NSInteger columnIndex = i % self.columnCount;
CGFloat buttonWidth = (self.bounds.size.width - self.columnMargin * 2 - self.columnPadding * (self.columnCount - 1)) / self.columnCount;
CGFloat buttonX = self.columnMargin + columnIndex * (buttonWidth + self.columnPadding);
CGFloat buttonY = self.rowMargin + rowIndex * (self.rowHeight + self.rowPadding);
self.buttons[i].frame = CGRectMake(buttonX, buttonY, buttonWidth, self.rowHeight);
}

CGRect frame = self.frame;
NSInteger rowCount = ceilf((CGFloat)self.buttons.count / (CGFloat)self.columnCount);
frame.size.height = self.rowMargin * 2 + self.rowHeight * rowCount + self.rowPadding * (rowCount - 1);
self.frame = frame;
});
}

源码地址:QiDragView


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

第三方库并不是必须的

iOS
前言 我在Lyft的八年间,很多产品经理以及工程师经常想往我们 app 里添加第三方库。有时候集成一个特定的库(比如 PayPal)是必须的,有时候是避免去开发一些非常复杂的功能,有时候仅仅只是避免c重复造轮子。 虽然这些都是合理的考量,但使用第三方库的风险和...
继续阅读 »

前言


我在Lyft的八年间,很多产品经理以及工程师经常想往我们 app 里添加第三方库。有时候集成一个特定的库(比如 PayPal)是必须的,有时候是避免去开发一些非常复杂的功能,有时候仅仅只是避免c重复造轮子。


虽然这些都是合理的考量,但使用第三方库的风险和相关成本往往被忽视或误解。在某些情况下,风险是值得的,但是在决定冒险之前,首先要能够明确的定义风险。为了使风险评估更加的透明和一致,我们制定了一个流程来衡量我们将其集成到app有多大的风险。


风险


大多数大型组织,包括我们,都有某种形式的代码审查,作为开发实践的一部分。对这些团队来说,添加一个第三方库就相当于添加了一堆由不属于团队成员开发,未经审查的代码。这破坏了团队一直坚持的代码审查原则,交付了质量未知的代码。这给app的运行方式以及长期开发带来了风险,对于大型团队而言,更是对整体业务带来了风险。


运行时风险


库代码通常来说,对于系统资源,和app拥有相同级别的访问权限,但它们不一定应用团队为管理这些资源而制定的最佳实践。这意味着它们可以在没有限制的情况下访问磁盘,网络,内存,CPU等等,因此,它们可以(过度)将文件写入磁盘,使用未优化的代码占用内存或CPU,导致死锁或主线程延迟,下载(和上传!)大量数据等等。更糟糕的是他们会导致崩溃,甚至崩溃循环


其中许多情况直到 app 已经上架才被发现,在这种情况下,修复它需要创建一个新版本,并通过审核,这通常需要大量时间和成本。这种风险可以通过一个变量控制是否调用来进行一定程度的控制,但是这种方法也并非万无一失(看下文)。


开发风险


引用一个同事的话:“每一行代码都是一种负担”,对不是你自己写的代码而言,这句话更甚。库在适配新技术或API时可能很慢,这阻碍了代码开发,或者太快,导致开发的版本过高。


库在采用新技术或API时可能很慢,阻碍了代码库,或者太快,导致部署目标太高。每当 Apple 和 Google 每年发布一个新 OS 版本时,他们通常要求开发人员根据SDK的变化更新代码,库开发人员也必须这样做。这需要协调一致的努力、优先事项的一致性以及及时完成工作的能力。


随着移动平台的不断变化,以及团队(成员)也不是一成不变,这将会成为一个持续不断的风险。当被集成的库不存在了,而库又需要更新时,会花很多时间来决定谁来做。事实证明一旦一个库存在,就很少也很难被移除,因此我们将其视为长期维护成本。


商业风险


如同我上面所说,现代的操作系统并没有对 app 代码和库代码进行区分,因此除了系统资源之外,它们还可以访问用户信息。作为 app 的开发者,我们负责恰当的使用这部分信息,也需要为任何第三方库负责。


如果用户给了 Lyft app 地理位置授权,任何第三方库也将自动得获得授权。他们可以将那些(地理位置)数据上传到自己服务器,竞对服务器,或者谁知道还有什么地方。当一个库需要我们没有的权限时,那问题就更大了。


同样,一个系统的安全取决于其最薄弱的环节,但如果其中包含未经审核的代码,那么你就不知道它到底有多安全。你精心设计的安全编码实践可能会被一个行为不当的库所破坏。苹果和谷歌实施的任何政策都是如此,例如“你不得对用户追踪”。


减少风险


当对一个库(是否)进行使用评估时,我们首先要问几个问题,以了解对库的需求。


我们内部能做么?


有时候我们只需要简单的粘贴复制真正需要的部分。在更复杂的场景中,库与自定义后端通信,我们对该API进行了逆向,并自己构建了一个迷你SDK(同样,只构建了我们需要的部分)。在90%的情况下,这是首选,但在与非常特定的供应商或需求集成时并不总是可行。


有多少用户从该库中受益?


在一种情况下,我们正在考虑添加一个风险很大的库(根据下面的标准),旨在为一小部分用户提供服务,同时将我们的所有用户都暴露在该库中。 对于我们认为会从中受益的一小部分客户,我们冒了为我们所有用户带来问题的风险。


这个库有什么传递依赖?


我们还需要评估库的所有依赖项的以下标准。


退出标准是什么?


如果集成成功,是否有办法将其转移到内部? 如果不成功,是否有办法删除?


评价标准


如果此时团队仍然希望集成库,我们要求他们根据一组标准对库进行“评分”。下面的列表并不全面,但应该能很好地说明我们希望看到的。


阻断标准


这些标准将阻止我们从技术上或者公司政策上集成此库,在进行下一步之前,我们必须解决:


过高的 deployment target/target SDKs。 我们支持过去4年主流的操作系统(版本),所以第三方库至少也需要支持一样多。


许可证不正确/缺失。 我们将许可文件与应用捆绑在一起,以确保我们可以合法使用代码并将其归属于许可持有人。


没有冲突的传递依赖关系。 一个库不能有一个我们已经包含但版本不同的传递依赖项。


不显示它自己的 UI 。 我们非常小心地使我们的产品看起来尽可能统一,定制用户界面对此不利。


它不使用私有 API 。 我们不愿意冒 app 因使用私有 API 而被拒绝的风险。


主要关注点


闭源。 访问源代码意味着我们可以选择我们想要包含的库的哪些部分,以及如何将该源代码与应用程序的其余部分捆绑在一起。 对于我们来说,一个封闭源代码的二进制发行版更难集成。


编译时有警告。 我们启用了“警告视为错误”,具有编译警告的库是库整体质量(下降)的良好指示。


糟糕的文档。 我们希望有高质量的内联文档,外部”如何使用“文档,以及有意义的更新日志。


二进制体积。 这个库有多大?一些库提供了很多功能,而我们只需要其中的一小部分。尤其是在没有访问源码权限的情况下,这通常是一个全有或全无的情况。


外部的网络流量。 与我们无法控制的上游服务器/端点通信的库可能会在服务器关闭、错误数据被发回等时关闭整个应用程序。这也与我上面提到的隐私问题相同。


技术支持。 当事情不能正常工作时,我们需要能够报告/上报问题,并在合理的时间内解决问题。开源项目通常由志愿者维护,也很难有一个时间线,但至少我们可以自己进行修改。这在闭源项目是不可能的。


无法禁用。 虽然大多数库特别要求我们初始化它,但有些库在实例化时更“主动”,并且在我们不调用它的情况下可以自己执行工作。这意味着当库导致问题时,我们无法通过功能变量或其他机制将其关闭。


我们为所有这些(和其他一些)标准分配了点数,并要求工程师为他们想要集成的库汇总这些点数。虽然默认情况下,低分数并不难被拒绝,但我们通常会要求更多的理由来继续前进。


最后


虽然这个过程看起来非常严格,在许多情况下,潜在风险是假设的,但我们有我在这篇博文中描述的每个场景的实际例子。将评估记录下来并公开,也有助于将相对风险传达给不熟悉移动平台工作方式的人,并证明我们没有随意评估风险。


此外,我不想声称每一个第三方库本质上都是坏的。事实上,我们在Lyft使用了很多:RxSwiftRxJavaBugsnagSDKGoogle MapsTensorflow,以及一些较小的用于非常特定的用例。但所有这些要么都经过了充分审查,要么我们已经决定风险值得收益,同时对这些风险和收益的真正含义有了清晰的认识。


最后,作为一个专业开发人员提示:始终在库的API之上创建自己的抽象,不要直接调用它们的API。这使得将来替换(或删除)底层库更加容易,再次减轻了与长期开发相关的一些风险。


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

所有开发者注意,苹果审核策略有变

iOS
这里每天分享一个 iOS 的新知识,快来关注我吧 访问敏感数据的 App 新规 苹果最近在 Apple Developer 上发了篇新闻公告,对需要访问用户敏感数据的 App 增加了审核要求。 这件事的缘由是苹果发现有一小部分 API 可能会被开发者滥用,通过...
继续阅读 »


这里每天分享一个 iOS 的新知识,快来关注我吧


访问敏感数据的 App 新规


苹果最近在 Apple Developer 上发了篇新闻公告,对需要访问用户敏感数据的 App 增加了审核要求。


这件事的缘由是苹果发现有一小部分 API 可能会被开发者滥用,通过信息指纹收集有关用户设备的信息。


早在今年 6 月的 WWDC23 上苹果就宣布,开发人员需要在其应用程序的隐私清单中声明使用某些 API 的原因,目前正式放出了这份需要声明的 API 列表。


新规详情


从今年(2023年)秋天开始,大概是 9 月中旬左右,如果你将你的 App 上传到 App Store Connect,你的应用程序使用到了需要声明原因的 API(也包括你引入的第三方 SDK),但是你没有在隐私清单文件中添加原因,那么 Apple 会给你发送一封警告性的邮件。


从 2024 年春季开始,大概是 3 月左右,没有在隐私清单文件中说明使用原因的 App 将会被拒审核。


需要声明原因的 API 有哪些?


1、NSUserdefaults 相关 API


这个 API 是被讨论最多争议最大的,因为几乎每个 App 都会用到,而且因为有沙盒保护,每个 app 的存储空间是隔离的,这都要申报理由,的确十分荒谬。


2、获取文件时间戳相关的 API

  • creationDate

  • modificationDate

  • fileModificationDate

  • contentModificationDateKey

  • creationDateKey

  • getattrlist(::::_:)

  • getattrlistbulk(::::_:)

  • fgetattrlist(::::_:)

  • stat

  • fstat(::)

  • fstatat(::::)

  • lstat(::)

  • getattrlistat(::::::)


3、获取系统启动时间的 API


大多数衡量 App 启动时间的 APM 库会用到这个 API。

  • systemUptime

  • mach_absolute_time()


4、磁盘空间 API

  • volumeAvailableCapacityKey

  • volumeAvailableCapacityForImportantUsageKey

  • volumeAvailableCapacityForOpportunisticUsageKey

  • volumeTotalCapacityKey

  • systemFreeSize

  • systemSize

  • statfs(::)

  • statvfs(::)

  • fstatfs(::)

  • fstatvfs(::)

  • getattrlist(::::_:)

  • fgetattrlist(::::_:)

  • getattrlistat(::::::)


5、活动键盘 API


这个 API 可以来确定当前用户文本输入的主要语言,有些 App 可能会用来标记用户。

  • activeInputModes

如何在 Xcode 中配置


由于目前 Xcode 15 正式版还没有发布,下边的操作是在 Beta 版本进行的。


在 Xcode 15 中隐私部分全部归类到了一个后缀为 .xcprivacy 的文件中,创建项目时默认没有生成这个文件,我们先来创建一下。


打开项目后,按快捷键 Command + N 新建文件,在面板中搜索 privacy,选择 App Pirvacy 点击下一步创建这个文件。



这个文件是个 plist 格式的面板,默认情况下长这样:



然后点击加号,创建一个 Privacy Accessed API TypesKey,这是一个数组,用来包含所有你 App 使用到需要申明原因的 API。



在这个数组下继续点击加号,创建一个 Item,会看到两个选项:

  • Privacy Accessed API Type:用到的 API 类型

  • Privacy Accessed API Reasons:使用这个 API 的原因(也是个数组,因为可能包含多个原因)



这两个 Key 都创建出来,然后在 Privacy Accessed API Type 一栏点击右侧的菜单,菜单中会列出上边提到的所有 API,选择你需要申报的 API,我这里就拿 UserDefault 来举例:



然后在 Privacy Accessed API Reasons 一览中点击加号,在右侧的选项框中选择对应的原因,每个 API 对应的原因都会列出来,可以到苹果的官方文档上查看这个 API 的原因对应的是哪个,比如 UserDefault 对应的是 CA92.1,我这里就选择这个:



到此,申报原因就完成了,原因不需要自己填写,直接使用苹果给出的选项就可以了,还是蛮简单的。


参考资料


[1]


公告原文: developer.apple.com/news/?id=z6…


[2]


需要在 App 内声明的 API 列表: developer.apple.com/documentati…


[3]


API 列表对应的原因: developer.apple.com/documentati…


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

Java仿抽奖系统

Java仿抽奖系统 前言 今天也是刚看完最近挺火的电影《孤注一掷》,也是亲眼的看到了,一个完整的家庭,是如何因为赌,而导致分崩离析,最后导致走向破碎的。 一旦涉及到电子的东西,很多东西都是变得可以控制的。这个作为程序员的我们是最清楚的,同时现在的反诈宣传,做的...
继续阅读 »

Java仿抽奖系统


前言


今天也是刚看完最近挺火的电影《孤注一掷》,也是亲眼的看到了,一个完整的家庭,是如何因为赌,而导致分崩离析,最后导致走向破碎的。


一旦涉及到电子的东西,很多东西都是变得可以控制的。这个作为程序员的我们是最清楚的,同时现在的反诈宣传,做的也是非常的到位,当时剧中哪位女警说的话,影响也非常的深刻。人都有贪心和不甘心,这也就是赌能真正抓住人的东西


好了不说那么多了,下面看一个简易的程序的代码实现


代码实现


首先我们定义一些常量


private static final int PRIZE_LEVELS = 4; // 奖品级别数量
private static final int[] PRIZE_AMOUNTS = {1, 10, 100, 1000}; // 奖品金额
private static final double[] WINNING_RATES = {10, 0, 0, 0}; // 中奖率

public static void main(String[] args) {
       // 设定中奖率
       double winningRate = 0.1;

       // 抽奖
       int prize = drawLottery(winningRate);

       // 发放奖品
       if (prize > 0) {
           System.out.println("恭喜你中奖了!奖金:" + prize + "元");
      } else {
           System.out.println("很遗憾,未中奖");
      }
  }

   // 抽奖方法
   private static int drawLottery(double winningRate) {
       Random random = new Random();
       int prize = 0;

       // 根据奖品级别逐级判断中奖
       for (int i = 0; i < PRIZE_LEVELS; i++) {
           // 生成0到1之间的随机数,判断是否中奖
           if (random.nextDouble() < winningRate * WINNING_RATES[i]) {
               prize = PRIZE_AMOUNTS[i];
               break;
          }
      }

       return prize;
  }
}

一个简单的抽奖程序。我们根据这个进行一些修改,更加的客观真实,我们加上已经有的金额和权重,让他更像是真正的赌。


我们加入权重以及自己的现金


private static double[] WEIGHTS;
// 自己的现金余额
static int cashBalance = 1000;

之后我们进行这样设计


   public static void main(String[] args) {
       // 计算权重
       calculateWeights();

       // 自己的现金余额
       int cashBalance = 1000;

       // 抽奖一次
       drawLottery(cashBalance);
  }

public static void calculateWeights() {
   WEIGHTS = new double[WINNING_RATES.length];
   double totalWeight = 0;

   // 计算总权重
   for (double rate : WINNING_RATES) {
       totalWeight += rate;
  }

   // 计算每个奖品级别的权重
   for (int i = 0; i < WEIGHTS.length; i++) {
       WEIGHTS[i] = WINNING_RATES[i] / totalWeight;
  }
}

public static void drawLottery() {
   Random random = new Random();
   double randomValue = random.nextDouble();

   int prizeIndex = 0;
   double cumulativeWeight = 0;

   // 根据随机值选择对应的奖品级别
   for (int i = 0; i < WEIGHTS.length; i++) {
       cumulativeWeight += WEIGHTS[i];
       if (randomValue <= cumulativeWeight) {
           prizeIndex = i;
           break;
      }
  }

   // 判断是否中奖
   if (random.nextDouble() <= WINNING_RATES[prizeIndex]) {
       int prizeAmount = PRIZE_AMOUNTS[prizeIndex];
       System.out.println("恭喜您中奖了!获得奖金:" + prizeAmount + "元");
       cashBalance += prizeAmount;
  } else {
       System.out.println("很遗憾,未中奖。");
  }

   // 更新现金余额
   cashBalance -= COST_PER_DRAW;
   System.out.println("抽奖后的现金余额:" + cashBalance + "元");
}

可以看出,我们这里规定的是20元抽奖一次,最高能达到1000元。


image-20230821164510692


运行一次后发现从原来的升值到了5000,


可是当你一旦陷入进去的话,只要我们稍微修改一下中奖率


image-20230821164625143


就会不断的去输。



赌博有害健康,需要我们每个人去

作者:小u
来源:juejin.cn/post/7270173541457723452
制止


收起阅读 »

别再用unload了:拥抱浏览器生命周期API

web
以前的前端页面很少有涉及页面的生命周期相关的话题,我们最熟悉的还是这几个load, beforeunload, unload 事件,用来监听页面加载完、离开页面之前、页面卸载事件并做一些相应的处理,比如保存需要持久化的数据,或者上报一些埋点数据。首先我们先理下...
继续阅读 »

以前的前端页面很少有涉及页面的生命周期相关的话题,我们最熟悉的还是这几个load, beforeunload, unload 事件,用来监听页面加载完、离开页面之前、页面卸载事件并做一些相应的处理,比如保存需要持久化的数据,或者上报一些埋点数据。首先我们先理下现有问题:


unload事件的问题


上面的这些事件(特别是unload)会有以下一些问题:



  • 用户主动触发的事件,无系统自动触发的一些状态监控

  • 无法稳定触发,特别是在手机端

  • 前进/后退时无法进入浏览器缓存(back/forward cache),导致用户来回切换时加载慢

  • 用户离开时埋点数据可能无法稳定上报

  • 无法追踪用户在页面上的完整生命周期


虽然现在计算机的算力硬件越来越强,但是现在我们同时需要处理的事情越来越多,开的Tab也就越来越多,一直被人诟病的比如chrome的疯狂吃内存的问题也很是头疼,接近顶配的电脑可能很快就被chrome给耗尽😂,所以现在的chrome版本会根据系统资源消耗程度来对不活动的Tab冻结或直接销毁来释放内存及减少电量的消耗。


针对这些问题现代浏览器提供了多个生命周期相关的事件来让开发者可以监听,并在触发时做相应处理。这里主要介绍几个最常用的而且对埋点相对比较重要的节点的生命周期事件。


页面是否可见: visibilitychange


visibilitychange事件涉及的场景会比较多,主要有用户切换tab导航到其他页面关闭Tab最小化窗口手机端切换app等,可在document上添加该事件,回调里通过document.visibilityState可以知道当前Tab是否隐藏了:


document.addEventListener(
'visibilitychange',
(e) => {
const isTabVisible = document.visibilityState === 'visible'
console.log('tab is visible: ', isTabVisible)
},
{
capture: true,
}
)

以上事件需要在捕获阶段进行监听(包括下面讲到的其他相关事件),避免被业务代码阻止冒泡,而且有些是window层面的,没有冒泡的阶段,所以需要在capture阶段执行。


页面的加载与离开: pageshow/pagehide


首先,这事件名是真的很容易让人理解错😭(pageshow/pagehide感觉才是上面visibilitychange所表示的意义)。


pageshow主要在页面新加载或被浏览器冻结后重新解冻时触发, 可通过e.persisted来确定是如何加载的:


window.addEventListener(
'pageshow',
(e) => {
// true 为之前冻结现在解冻,false相当于重新加载或新加载
console.log('e.persisted: ', e.persisted)
},
{
capture: true,
}
)

pagehide本质上是对unload事件的真正替换且具有更稳定的触发时机,我们可以在这里对一些埋点事件或者其他一些小批量的数据进行上报(sendBeacon或fetch, 下面讲到),这里还有个需要注意的是visibilitychange触发范围更广,也就是页面进来和离开时也会触发,和pageshow/pagehide同时触发(都触发时在这2个之前),所以如果业务需要区分页面离开还是用户仅仅是切换tab时,需要在不同的事件回调里做不同的处理。


页面离开时的数据上报


当需要在用户离开页面时(pagehide触发时)稳定的上报一些数据,我们一般会使用navigator.sendBeacon()方法:


// queued = true 说明浏览器接收请求,马上会上报出去,false的话可能业务要做其他处理
const queued = navigator.sendBeacon('/api', data)

当然sendBeacon有大小限制,一般在64KB以下,超出很可能会失败,所以我们在这里上报时要控制大小,如果数据量较大,建议提前上报一部分,比如在visibilitychange时(用户切换tab时)先上报一部分,确保只留64KB以下的数据放到最后。
除了sendBeacon之外我们还可以用fetch上报,通过设置keepalive: true来达到sendBeacon一样的效果,当然也有一样的大小限制,这里可以做兼容处理:


function send(body) {
if (navigator.sendBeacon) {
navigator.sendBeacon('/api', body)
return
}
fetch('/api', {
body,
method: 'POST',
keepalive: true
})
}

其他生命周期事件


除了上面常用的生命周期之外,浏览器的生命周期API还提供了页面的其他状态如:freeze, resume,freeze一般在用户导航到其他页面并且满足进入back/forward cache的条件时,或者用户在其他tab,浏览器根据系统资源自动使其进入冻结状态,当用户通过浏览器的返回按钮或重新切回到该Tab时,会触发resume事件说明页面继续,紧接着会触发pageshow事件,说明页面又进来了。关于更多的说明可以参考Page Lifecycle API


电脑息屏、休眠时


还有一些场景是电脑休眠或者只是关闭屏幕时,页面的生命周期以及页面里的一些定时器会有哪些变化?


休眠比较简单,定时器、网络连接这些都会被暂停,如果不想丢失数据,需要在pagehide做处理;息屏时可在document.visibilityState !== 'visible'时做处理,相当于页面不可见了,而且页面里的定时器不会被停掉但是可能会被浏览器延时处理,比如正常代码里是5s执行一次,此时可能会变成30s或者1min执行一次来节省资源。


总结


以上主要介绍了如何使用现代浏览器提供的生命周期API来在页面的不同阶段做相应的处理,pagehide事件主要用来替换unload, 关于beforeunload事件在有些应用中还是需要的,但是我们应该有选择性的添加该事件及在合适的时间移除监听该事件,比如未保存的数据已经保存完毕时可以移除,当又有新改动时再监听。


还有很多其他的生命周期事件可以让开发者能对用户在页面里/外的整个生命周期有更好的理解,以此来分析并提升网站的整体体验。笔者后面会写一篇如何跟踪用户在浏览器里的同一tab/不同tab之间的来回操作、记录并上报,并能够在后台进行session回放的文章,通过这些扩展能力相较于纯粹的埋点能更好地理

作者:jasonboy7
来源:juejin.cn/post/7269983642155663419
解用户行为以此来优化产品体验🍻。

收起阅读 »

一个写了3年半flutter的小伙,突然写了2个月uniapp的感悟!

前言 因为某些原因,在过去的三年半时间,我除了flutter之外,很少接触其他的框架,期间除了学习了Android(主要是Kotlin、jetpack)、GoLang Gin之外基本上很少接触其他的框架。而在最近的两个月,突然来了一个要求用uniapp实现的项...
继续阅读 »

前言


因为某些原因,在过去的三年半时间,我除了flutter之外,很少接触其他的框架,期间除了学习了Android(主要是Kotlin、jetpack)、GoLang Gin之外基本上很少接触其他的框架。而在最近的两个月,突然来了一个要求用uniapp实现的项目,在接下这个前,我是有些抵触的。第一点是觉得自己短期内去学一个新的框架,学到的东西不足以完成整个项目,第二点是不想脱离舒适圈。当然,最后我还是选择了直面困难,不然您也看不到这篇文章了🤣。


本文更多的是帮助您解决是否要学习uni-app或flutter框架的这一问题,以及两个框架的一些代码对比。如果您想要判断的是一个新项目该使用哪个框架,那么本文就不是很合适了~


跨平台层面的对比感悟


在Flutter刚出来的这几年,经常会在各种跨平台框架对比的文章下,看到将其与uni-app进行比较。当时我也没有在意太多,以为uni-app也是个差不多的“正经”跨平台框架,但当我打开uni-app官网的时候,我震惊了,因为我看到了这样一句话:一套代码编到15个平台,这不是梦想。我瞬间就傻眼了,这么nb?Flutter不也才横跨六大平台 ?在仔细一想,不对啊,这哪来的15个平台?再仔细一看,然后我的心中只剩下一万个省略号了,横跨一堆小程序平台是吧...



学习成本的对比感悟


1. 开发语言的不同

Flutter,要求开发者学习dart,了解dart和flutter的API,最好还会写点原生。而uni-app只需要学Vue.js,没有附加专有技术。所以从学习一个框架来看,很明显uni-app的学习成本很低。而从我个人的角度去分析,当年我只是一个刚入编程世界的菜鸡中的菜鸡,只学了半年的html+css+js和半年的java。抛开学了1个月的SpringBoot,Flutter可以算是我学习的第一个框架,当时我是直接上手学的Flutter,没有去单独学习dart,因为和java很相似。个人觉得学习成本也还好,如果你喜欢这个框架的话~而最近两个月学习uni-app,我也确实是感受到了学习成本很低,基本上看了看文档,就直接上手了,很多组件的名字也是和flutter大差不差。就是写css有点难受🤣,好在flex布局和flutter的rowcolumn用法一样,基本上半小时就能把基本的、简单的页面布局写好了。


2. 第三方插件&社区氛围

截至目前2023.7,flutter在github上有155K的star,uni-app有着38.4K的star。从star的数量也可以看出一个框架的热度,很明显,flutter是远高于uni-app的(毕竟uni-app的主要使用场景还是在国内小程序中)。对于第三方插件呢Flutter有着pub.dev,uni-app有插件市场,但相比Flutter呢可能略显不足。


3. 开发工具的使用

Flutter可以选择vscode或者android studio等来进行开发,uni-app可以选择HBuilderX,当然也可以使用vscode,用什么开发工具其实大差不差,如果你一直使用vscode,那么你对工具的使用会更加的熟悉,而如果你和我一样,用的是android studio,再去使用HBuilderX,说实话,有点点难受...例如我最常用的Alt+回车(提示),crtl+alt+l(代码格式化)。当然,反过来也是一样的(●'◡'●)


编码实现对比


1. 布局区别


  • 代码整体结构:Flutter使用Widget层级嵌套来构建用户界面,也是被很多人所不喜欢的嵌套地狱(这一点因人而异,根据自己的习惯和代码风格)。 uni-app 使用 Vue.js 的组件化布局方式,templatestylescripttemplate 定义了组件的 HTML 结构,style 定义了组件的样式,script 定义了组件的行为。

  • 布局原理区别:Flutter 中的布局是基于约束的,可以使用Constraints来控制小部件的最大和最小尺寸,并根据父级小部件的约束来确定自身的尺寸。uni-app则是,可以使用类似于 CSS 中 Flex 弹性布局的方式来控制组件的排列和布局。通过设置组件的样式属性,如 display: flexflexjustify-content 等,可以实现垂直和水平方向上的灵活布局。当然flutter也有和flex差不多的rowcolumn

  • 自定义布局:Flutter支持自定义布局,可以通过继承 SingleChildLayoutDelegateMultiChildLayoutDelegate 来实现自定义布局,而uni-app目前并没有直接提供类似的专门用于自定义布局的机制,不过uni-app常见的做法是创建一个自定义组件,并在该组件的 template 中使用各种布局方式、样式和组件组合来实现特定的布局效果。


2. 状态管理的区别

Flutter 提供了内置的状态管理机制,最常见的就是通过setState来管理小部件的状态,uni-app是利用Vue.js的响应式数据绑定和状态管理,通过 data 属性来定义和管理组件的状态。


3. 开发语言的区别与联系

区别:众所周知,JavaScript 是一门弱类型的语言,而 Dart 是强类型的语言(dart也支持一些弱类型,Dart中弱类型有var, Object 以及dynamic)。Dart有类和接口的概念,并支持面向对象编程,如果你喜欢 OOP 概念,那么你会喜欢使用 Dart 进行开发,此外,它还支持接口、Mixin、抽象类和静态类型等,这一点对写过java的朋友很友好,而JavaScript则支持基于原型的面向对象编程。Dart和JavaScript还有一个重要的区别就是:Dart是类型安全的,使用AOT和JIT编译器编译。


联系:从一个学习这个两个语言的角度去看, 两者都支持异步编程模型,如 Dart 的 async/await和 JavaScript 的 Promiseasync/await,这就非常友好了。


4. 一个简单的计数器例子,更好的理解他们直接的区别以及相关的地方:


Flutter代码:


import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
)
;
}
}

class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State {
int _counter = 0;

void _incrementCounter() {
setState(() {
_counter++;
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'
You have pushed the button this many times:',
)
,
Text(
'$_counter',
style:
Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton:
FloatingActionButton(
onPressed: _incrementCounter,
tooltip: '
Increment',
child: const
Icon(Icons.add),
),
)
;
}
}

uniapp代码:


<template>
<view class="container">
<text class="count">{{ count }}text>
<view class="buttons">
<button class="btn" @tap="incrementCounter">+button>
view>
view>
template>

<script>
export default {
data() {
return {
count: 0,
};
},
methods: {
incrementCounter() {
this.count++;
},
},
};
script>

<style>
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
height: 100vh;
background-color: #f0f0f0;
}

.count {
display: flex;
justify-content: center;
align-items: center;
font-size: 48px;
font-weight: bold;
height: 100%;
}

.buttons {
display: flex;
width: 100vw;
flex-direction: row;
justify-content: flex-end;
}

.btn {
width: 108rpx;
height: 108rpx;
font-size: 24px;
display: flex;
justify-content: center;
align-items: center;
margin: 8px;
background-color: #2196F3;
color: #fff;
border-radius: 50%;
}
style>

总结


从App开发的角度来看,uni-app的最大价值在于让国内庞大的Vue开发群体也能够轻松地开发“高性能”的App,不用去承担flutter或react native的学习成本,短时间内开发一款简单的偏展示类的app的话,uni-app肯定是首选,小公司应该挺受益的。再加上uni-app可以同时开发多端小程序,就足以保证在国内有足够的市场。但是稍微有点动效或者说有video、map之类的app,那么要慎重考虑,个人觉得挺限制的。不过很多时候技术并不是一个项目选型第一标准,适合才是,uni-app很适合国内,毕竟试错成本低...


注:本文仅为一个写了几年flutter小伙,突然写了2个月uniapp的感悟,存在一定个人主观,有错误欢迎指出😘

作者:编程的平行世界
来源:juejin.cn/post/7261162911615926331

收起阅读 »

优化重复冗余代码的8种方式

前言 大家好,我是田螺。好久不见啦~ 日常开发中,我们经常会遇到一些重复代码。大家都知道重复代码不好,它主要有这些缺点:可维护性差、可读性差、增加错误风险等等。最近呢,我优化了一些系统中的重复代码,用了好几种的方式。感觉挺有用的,所以本文给大家讲讲优化重复代码...
继续阅读 »

前言


大家好,我是田螺。好久不见啦~


日常开发中,我们经常会遇到一些重复代码。大家都知道重复代码不好,它主要有这些缺点:可维护性差、可读性差、增加错误风险等等。最近呢,我优化了一些系统中的重复代码,用了好几种的方式。感觉挺有用的,所以本文给大家讲讲优化重复代码的几种方式。

  • 抽取公用方法
  • 抽个工具类
  • 反射
  • 泛型
  • 继承和多态
  • 设计模式
  • 函数式
  • AOP

1. 抽取公用方法


抽取公用方法,是最常用的代码去重方法~


比如这个例子,分别遍历names列表,然后各自转化为大写和小写打印出来:


public class TianLuoExample {

public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "TianLuo");

System.out.println("Uppercase Names:");
for (String name : names) {
String uppercaseName = name.toUpperCase();
System.out.println(uppercaseName);
}

System.out.println("Lowercase Names:");
for (String name : names) {
String lowercaseName = name.toLowerCase();
System.out.println(lowercaseName);
}
}
}

显然,都是遍历names过程,代码是重复的,只不过转化大小写不一样。我们可以抽个公用方法processNames,优化成这样:


public class TianLuoExample {

public static void processNames(List<String> names, Function<String, String> nameProcessor, String processType) {
System.out.println(processType + " Names:");
for (String name : names) {
String processedName = nameProcessor.apply(name);
System.out.println(processedName);
}
}

public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "TianLuo");

processNames(names, String::toUpperCase, "Uppercase");
processNames(names, String::toLowerCase, "Lowercase");
}
}

2. 抽工具类


我们优化重复代码,抽一个公用方法后,如果发现这个方法有更多共性,就可以把公用方法升级为一个工具类。比如这样的业务场景:我们注册的时候,修改邮箱,重置密码等,都需要校验邮箱


实现注册功能时,用户会填邮箱,需要验证邮箱格式


public class RegisterServiceImpl implements RegisterService{
private static final String EMAIL_REGEX =
"^[A-Za-z0-9+_.-]+@(.+)$";

public boolean registerUser(UserInfoReq userInfo) {
String email = userInfo.getEmail();
Pattern pattern = Pattern.compile(EMAIL_REGEX);
Matcher emailMatcher = pattern.matcher(email);
if (!emailMatcher.matches()) {
System.out.println("Invalid email address.");
return false;
}

// 进行其他用户注册逻辑,比如保存用户信息到数据库等
// 返回注册结果
return true;
}
}

密码重置流程中,通常会向用户提供一个链接或验证码,并且需要发送到用户的电子邮件地址。在这种情况下,也需要验证邮箱格式合法性


public class PasswordServiceImpl implements PasswordService{

private static final String EMAIL_REGEX =
"^[A-Za-z0-9+_.-]+@(.+)$";

public void resetPassword(PasswordInfo passwordInfo) {
Pattern pattern = Pattern.compile(EMAIL_REGEX);
Matcher emailMatcher = pattern.matcher(passwordInfo.getEmail());
if (!emailMatcher.matches()) {
System.out.println("Invalid email address.");
return false;
}
//发送通知修改密码
sendReSetPasswordNotify();
}
}

我们可以抽取个校验邮箱的方法出来,又因为校验邮箱的功能在不同的类中,因此,我们可以抽个校验邮箱的工具类


public class EmailValidatorUtil {
private static final String EMAIL_REGEX =
"^[A-Za-z0-9+_.-]+@(.+)$";

private static final Pattern pattern = Pattern.compile(EMAIL_REGEX);

public static boolean isValid(String email) {
Matcher matcher = pattern.matcher(email);
return matcher.matches();
}
}

//注册的代码可以简化为这样啦
public class RegisterServiceImpl implements RegisterService{

public boolean registerUser(UserInfoReq userInfo) {
if (!EmailValidatorUtil.isValid(userInfo.getEmail())) {
System.out.println("Invalid email address.");
return false;
}

// 进行其他用户注册逻辑,比如保存用户信息到数据库等
// 返回注册结果
return true;
}
}

3. 反射


我们日常开发中,经常需要进行PO、DTO和VO的转化。所以大家经常看到类似的代码:


    //DTO 转VO
public UserInfoVO convert(UserInfoDTO userInfoDTO) {
UserInfoVO userInfoVO = new UserInfoVO();
userInfoVO.setUserName(userInfoDTO.getUserName());
userInfoVO.setAge(userInfoDTO.getAge());
return userInfoVO;
}
//PO 转DTO
public UserInfoDTO convert(UserInfoPO userInfoPO) {
UserInfoDTO userInfoDTO = new UserInfoDTO();
userInfoDTO.setUserName(userInfoPO.getUserName());
userInfoDTO.setAge(userInfoPO.getAge());
return userInfoDTO;
}

我们可以使用BeanUtils.copyProperties() 去除重复代码BeanUtils.copyProperties()底层就是使用了反射


    public UserInfoVO convert(UserInfoDTO userInfoDTO) {
UserInfoVO userInfoVO = new UserInfoVO();
BeanUtils.copyProperties(userInfoDTO, userInfoVO);
return userInfoVO;
}

public UserInfoDTO convert(UserInfoPO userInfoPO) {
UserInfoDTO userInfoDTO = new UserInfoDTO();
BeanUtils.copyProperties(userInfoPO,userInfoDTO);
return userInfoDTO;
}

4.泛型


泛型是如何去除重复代码的呢?给大家看个例子,我有个转账明细和转账余额对比的业务需求,有两个类似这样的方法:


private void getAndUpdateBalanceResultMap(String key, Map<String, List> compareResultListMap,
List balanceDTOs
) {
List<TransferBalanceDTO> tempList = compareResultListMap.getOrDefault(key, new ArrayList<>());
tempList.addAll(balanceDTOs);
compareResultListMap.put(key, tempList);
}

private void getAndUpdateDetailResultMap(String key, Map<String, List> compareResultListMap,
List detailDTOS
) {
List<TransferDetailDTO> tempList = compareResultListMap.getOrDefault(key, new ArrayList<>());
tempList.addAll(detailDTOS);
compareResultListMap.put(key, tempList);
}

这两块代码,流程功能看着很像,但是就是不能直接合并抽取一个公用方法,因为类型不一致。单纯类型不一样的话,我们可以结合泛型处理,因为泛型的本质就是参数化类型.优化为这样:


private  void getAndUpdateResultMap(String key, Map<String, List> compareResultListMap, List accountingDTOS) {
List tempList = compareResultListMap.getOrDefault(key, new ArrayList<>());
tempList.addAll(accountingDTOS);
compareResultListMap.put(key, tempList);
}

5. 继承与多态


假设你正在开发一个电子商务平台,需要处理不同类型的订单,例如普通订单和折扣订单。每种订单都有一些共同的属性(如订单号、购买商品列表)和方法(如计算总价、生成订单报告),但折扣订单还有特定的属性和方法


在没有使用继承和多态的话,会写出类似这样的代码:


//普通订单
public class Order {
private String orderNumber;
private List products;

public Order(String orderNumber, List products) {
this.orderNumber = orderNumber;
this.products = products;
}

public double calculateTotalPrice() {
double total = 0;
for (Product product : products) {
total += product.getPrice();
}
return total;
}

public String generateOrderReport() {
return "Order Report for " + orderNumber + ": Total Price = $" + calculateTotalPrice();
}
}

//折扣订单
public class DiscountOrder {
private String orderNumber;
private List products;
private double discountPercentage;

public DiscountOrder(String orderNumber, List products, double discountPercentage) {
this.orderNumber = orderNumber;
this.products = products;
this.discountPercentage = discountPercentage;
}

public double calculateTotalPrice() {
double total = 0;
for (Product product : products) {
total += product.getPrice();
}
return total - (total * discountPercentage / 100);
}
public String generateOrderReport() {
return "Order Report for " + orderNumber + ": Total Price = $" + calculateTotalPrice();
}
}

显然,看到在OrderDiscountOrder类中,generateOrderReport() 方法的代码是完全相同的。calculateTotalPrice()则是有一点点区别,但也大相径庭。


我们可以使用继承和多态去除重复代码,让DiscountOrder去继承Order,代码如下:


public class Order {
private String orderNumber;
private List products;

public Order(String orderNumber, List products) {
this.orderNumber = orderNumber;
this.products = products;
}

public double calculateTotalPrice() {
double total = 0;
for (Product product : products) {
total += product.getPrice();
}
return total;
}

public String generateOrderReport() {
return "Order Report for " + orderNumber + ": Total Price = $" + calculateTotalPrice();
}
}

public class DiscountOrder extends Order {
private double discountPercentage;

public DiscountOrder(String orderNumber, List products, double discountPercentage) {
super(orderNumber, products);
this.discountPercentage = discountPercentage;
}

@Override
public double calculateTotalPrice()
{
double total = super.calculateTotalPrice();
return total - (total * discountPercentage / 100);
}
}

6.使用设计模式


很多设计模式可以减少重复代码、提高代码的可读性、可扩展性.比如:



  • 工厂模式: 通过工厂模式,你可以将对象的创建和使用分开,从而减少重复的创建代码

  • 策略模式: 策略模式定义了一族算法,将它们封装成独立的类,并使它们可以互相替换。通过使用策略模式,你可以减少在代码中重复使用相同的逻辑

  • 模板方法模式:模板方法模式定义了一个算法的骨架,将一些步骤延迟到子类中实现。这有助于避免在不同类中重复编写相似的代码


我给大家举个例子,模板方法是如何去除重复代码的吧,业务场景:



假设你正在开发一个咖啡和茶的制作流程,制作过程中的热水和添加物质的步骤是相同的,但是具体的饮品制作步骤是不同的



如果没有使用模板方法模式,实现是酱紫的:


public class Coffee {
public void prepareCoffee() {
boilWater();
brewCoffeeGrinds();
pourInCup();
addCondiments();
}

private void boilWater() {
System.out.println("Boiling water");
}

private void brewCoffeeGrinds() {
System.out.println("Brewing coffee grinds");
}

private void pourInCup() {
System.out.println("Pouring into cup");
}

private void addCondiments() {
System.out.println("Adding sugar and milk");
}
}

public class Tea {
public void prepareTea() {
boilWater();
steepTeaBag();
pourInCup();
addLemon();
}

private void boilWater() {
System.out.println("Boiling water");
}

private void steepTeaBag() {
System.out.println("Steeping the tea bag");
}

private void pourInCup() {
System.out.println("Pouring into cup");
}

private void addLemon() {
System.out.println("Adding lemon");
}
}

这个代码例子,我们可以发现,烧水和倒入杯子的步骤代码,在CoffeeTea类中是重复的。


使用模板方法模式,代码可以优化成这样:


abstract class Beverage {
public final void prepareBeverage() {
boilWater();
brew();
pourInCup();
addCondiments();
}

private void boilWater() {
System.out.println("Boiling water");
}

abstract void brew();

private void pourInCup() {
System.out.println("Pouring into cup");
}

abstract void addCondiments();
}

class Coffee extends Beverage {
@Override
void brew() {
System.out.println("Brewing coffee grinds");
}

@Override
void addCondiments() {
System.out.println("Adding sugar and milk");
}
}

class Tea extends Beverage {
@Override
void brew() {
System.out.println("Steeping the tea bag");
}

@Override
void addCondiments() {
System.out.println("Adding lemon");
}
}

在这个例子中,我们创建了一个抽象类Beverage,其中定义了制作饮品的模板方法 prepareBeverage()。这个方法包含了烧水、倒入杯子等共同的步骤,而将制作过程中的特定步骤 brew() 和 addCondiments() 延迟到子类中实现。这样,我们避免了在每个具体的饮品类中重复编写相同的烧水和倒入杯子的代码,提高了代码的可维护性和重用性。


7.自定义注解(或者说AOP面向切面)


使用 AOP框架可以在不同地方插入通用的逻辑,从而减少代码重复。


业务场景:


假设你正在开发一个Web应用程序,需要对不同的Controller方法进行权限检查。每个Controller方法都需要进行类似的权限验证,但是重复的代码会导致代码的冗余和维护困难


public class MyController {
public void viewData() {
if (!User.hasPermission("read")) {
throw new SecurityException("Insufficient permission to access this resource.");
}
// Method implementation
}

public void modifyData() {
if (!User.hasPermission("write")) {
throw new SecurityException("Insufficient permission to access this resource.");
}
// Method implementation
}
}

你可以看到在每个需要权限校验的方法中都需要重复编写相同的权限校验逻辑,即出现了重复代码.我们使用自定义注解的方式能够将权限校验逻辑集中管理,通过切面来处理,消除重复代码.如下:


@Aspect
@Component
public class PermissionAspect {

@Before("@annotation(requiresPermission)")
public void checkPermission(RequiresPermission requiresPermission) {
String permission = requiresPermission.value();

if (!User.hasPermission(permission)) {
throw new SecurityException("Insufficient permission to access this resource.");
}
}
}

public class MyController {
@RequiresPermission("read")
public void viewData() {
// Method implementation
}

@RequiresPermission("write")
public void modifyData() {
// Method implementation
}
}

就这样,不管多少个Controller方法需要进行权限检查,你只需在方法上添加相应的注解即可。权限检查的逻辑在切面中集中管理,避免了在每个Controller方法中重复编写相同的权限验证代码。这大大提高了代码的可读性、可维护性,并避免了代码冗余。


8.函数式接口和Lambda表达式


业务场景:



假设你正在开发一个应用程序,需要根据不同的条件来过滤一组数据。每次过滤的逻辑都可能会有些微的不同,但基本的流程是相似的。



没有使用函数式接口和Lambda表达式的情况:


public class DataFilter {
public List<Integer> filterPositiveNumbers(List numbers) {
List<Integer> result = new ArrayList<>();
for (Integer number : numbers) {
if (number > 0) {
result.add(number);
}
}
return result;
}

public List<Integer> filterEvenNumbers(List numbers) {
List<Integer> result = new ArrayList<>();
for (Integer number : numbers) {
if (number % 2 == 0) {
result.add(number);
}
}
return result;
}
}

在这个例子中,我们有两个不同的方法来过滤一组数据,但是基本的循环和条件判断逻辑是重复的,我们可以使用使用函数式接口和Lambda表达式,去除重复代码,如下:


public class DataFilter {
public List<Integer> filterNumbers(List numbers, Predicate predicate) {
List<Integer> result = new ArrayList<>();
for (Integer number : numbers) {
if (predicate.test(number)) {
result.add(number);
}
}
return result;
}
}


我们将过滤的核心逻辑抽象出来。该方法接受一个 Predicate函数式接口作为参数,以便根据不同的条件来过滤数据。然后,我们可以使用Lambda表达式来传递具体的条件,这样最终也达到去除重复代码的效果啦.


最后


我是捡田螺的小男孩,大家如果觉得看了本文有帮助的话,麻烦给个三连(点赞、分享、转发)支持一下哈。最近我在工作中,用了其中的几种方式,去优化重复代码。下一篇文章,我打算出一篇后端思维系列的文章,基于业务代码,手把手教大家去除重复代码哈。一起加油~~

作者:捡田螺的小男孩
来源:juejin.cn/post/7270026656663322685

收起阅读 »

消息太大,kafka受不了

前言 上周在进行自测的时候,kafka抛出一个RecordTooLargeException异常,从名字我们可以直接看出是消息太大了,导致发不出去而抛出异常,那么怎么应该怎么解决这个问题呢,其实很简单,要么将消息拆分得小一点,要么调节kafka层面的参数,依然...
继续阅读 »

前言


上周在进行自测的时候,kafka抛出一个RecordTooLargeException异常,从名字我们可以直接看出是消息太大了,导致发不出去而抛出异常,那么怎么应该怎么解决这个问题呢,其实很简单,要么将消息拆分得小一点,要么调节kafka层面的参数,依然它抛出这个异常,那么就证明超过了某个参数的阈值,由此我们可以有两种方式来处理这个问题,但是一切还要从我们的业务背景和数据结构去看这个问题。


业务背景


我们这边会将数据写入文件,通过FTP的方式,没产生数据,就往FTP里面追加,而这些数据都是需要保证不丢失的,由于业务的发展,我这边需要专门去处理这些文件,然后通过kafka投递给下游系统,所以自然需要解析文件,还得一条一条的解析后发送。


问题出现


一开始我看到文件都比较小,所以处理方式是只有这个文件的数据全部解析完成并成功投递kafka,那么我这边才记录这个文件处理成功,但是处理了很多个大文件过后,发现数据条数对不上,看日志是RecordTooLargeException异常,因为上面的处理方式是文件处理完成并全部投递到kafka才记录文件解析完成,所以这是有问题的,一个大文件可能有即使上百万条数据,难免会遇到很大的数据,所以只要一条没解析成功,那么后面的数据就不去解析了,这个文件就不算解析成功,所以应该要设计容错,并对数据进行监控和补偿。


处理问题


在得知是某些数据过大的问题,我就DEBUG去看源码,在kafka生产端的KafkaProducer类中,发现问题出在下面这方法中。



ensureValidRecordSize方法就是对消息的大小进行判断的,参数size就是我们所发送的消息的字节数,maxRequestSize就是允许消息的最大字节,因为没有进行设置,所以这个值使用的是默认值,默认为1M,所以就应该将maxRequestSize这个参数进行重新设置。


因为我们使用的是SpringBoot开发,于是通过yml方式配置,但是发现spring-kafka没提示这个属性,于是只有写一个Kafka的配置类,然后再读取yml文件内容进行配置


配置类


yml文件



通过上面的配置后,我们看到我将max.request.size参数的值设置为10M,这需要根据实际情况来,因为我在处理的过程中发现像比较大的数据行也只有个别。


如果在实际使用过程中数据比较大,那么可能需要拆分数据,不过如果数据不能拆分,那么我们应该考虑消息压缩方式,将数据压缩后再发送,然后在消费者进行解压,不过这种压缩是我们自己实现的,并不是kafka层面的压缩,kafka本身也提供了压缩功能,有兴趣可以了解一下。


扩展


上面设置了max.request.size参数,我们在上面的截图代码中看到第二个判断中有一个参数totalMemorySize,这个值是缓冲区大小,我们发送的消息并不会马上发送kafka服务端,而是会先放在内存缓冲区,然后kafka通过一个线程去取,然后发送,可通过buffer.memory设置,这个值的默认值为32M,所以我们在设置max.request.size的时候也要考虑一下这个值。


总结


有必要对kafka进行比较深一点的学习,这样在出现问题的时候能够快速定位,并且合理解决,当然,在业务处理的时候要充分考虑可能出现的问题,做好容错和相应的补偿方案。


今天的分享就到这里,感谢你的观

作者:刘牌
来源:juejin.cn/post/7269745800178286627
看,我们下期见

收起阅读 »

从读《微信背后的产品观》到思考前端工程师的“35岁”

我是一名前端开发者,同时兼任pm职责,近半年在公司负责升级一直在开发与运营的一个B端的Saas商城系统; 在对需求剖析、需求抽象、每个字段含义的推敲的时候,我越发对微信这样简洁、自然的产品产生兴趣与共鸣。 也会情不自禁的赞叹微信清晰明了、谨慎内敛的结构化产品思...
继续阅读 »


我是一名前端开发者,同时兼任pm职责,近半年在公司负责升级一直在开发与运营的一个B端的Saas商城系统;


在对需求剖析、需求抽象、每个字段含义的推敲的时候,我越发对微信这样简洁、自然的产品产生兴趣与共鸣。


也会情不自禁的赞叹微信清晰明了、谨慎内敛的结构化产品思路。


虽然微信这么多年上线了如此多功能与特性,但是产品简洁而克制的灵魂从来没有改变,哪怕对于60岁的人来说,也几乎不存在用不好微信的情况。


而我做为微信的使用者,广义技术上的开发者,无论是产品还是技术维度,都让我越加佩服与引起共鸣(恶心的小程序开发除外)。


昨天终于行动了起来,把这本知名的 《微信背后的产品观》 找出来并读了起来,与其叫一本书,不如说是一个演讲的记录,很短,大约2小时就读完了,主要内容是2012年微信4.0发布时候,张小龙长达8个多小时的公开演讲的内容;


不同于其他产品经理的书籍,他们会告诉你各种方法论、科学分析方法,张小龙截然相反的采用了一种极其浪漫的方式去看待产品,去理解所谓的用户需求,我认为这种产品思路的领先是微信这么多年在社交领域立于不败之地的根本。


虽然在最后,张小龙看似补刀似的说:我所说的,都是错的;但是这恰恰就是他的产品理念,所谓产品没有任何科学方法,完全来自于对人,对人类群体的理解,对自己的拷问与质疑。


我也斗胆推荐大家看看这本书,很快就能看完,人类之所以存在信息差,就是因为不知道,或许看完这本书,你会打开新的思路,某些问题也能豁然开朗。


借此机会,我也想与大家分享一下,这几年我作为一名前端开发者的迷茫与努力


业务前端开发者的困境



我只是一名普通学历,普通的业务前端开发者,所以以下仅是我的个人感觉,不代表所有前端开发者。



目前在一个小型互联网公司的saas电商部门下,主要职责是前端开发组长,我们公司的主营业务不是saas电商,所以这几年算不上受到到很强的市场冲击,平时会管理几人的小团队,我18年毕业至今,一直在这家公司。


而我大学毕业之后一直从事前端开发方向,在我工作1-3年的阶段,我都保持着对技术的热情,主要是因为尝到了学习技术的甜头,那时候我坚定的认为下一个阶段是全栈,在工作之余我花费了大量的精力学习技术;但是后面我就发现了一个很现实的问题,公司需要大家更好分工协作,所以高级别的项目后端是绝对轮不到一个前端去开发开发;并不是说能力不行,而是人的精力有限,前后端都干,还要管团队,是忙不过来的。


在这样的环境下,关于全栈技术的学习,我也越发疑虑,逐渐走到了大多数业务前端开发者的临界点。



  • 业务前端的35岁危机在普通人身上是存在的,我们的年纪、精力、外部压力都不允许你永远征战一线,并始终保持高竞争力。

  • 业务前端的上限很低,大多数努力的前端开发者可以在3 - 5年内触摸到业务前端的上限,职位也就是前端组长。

  • 技术纵深是很好的选择,但是受限于综合实力(英语、计算机基础、天分),普通人可以达到的纵深远比想象的要浅。

  • 技术学会了,但是用不上,也会慢慢被遗忘;demo级别的应用无法让开发者对某一项技术有深刻理解。

  • 在业务开发场景下的前端,永远是没有灵魂的大头兵,上限低就意味着待遇相对较低、可替代性相对较强。

  • 代码写的越多,与人交流的机会越少,对于几十年的人生而言,这是一件很没安全的事情。


总结一下,就是因为兴趣而走的前端技术路径开始越来越窄,前路开始越来越看不清,时间推着我向前,这不禁让我低头沉思,下一步究竟要怎么走?


说回产品



到目前为止我也依旧不确定前路怎么走,接下来的一些结论,只是我的一些探索。



在大学后期,我隐约感觉技术路线并非我所擅长的时候,我有目的的学习了微观经济学、企业管理、竞争战略相关的知识,从而间接接触到了互联网产品,也就是pm。


我很快就感受到了pm的魅力:创造,我恰好是一个喜欢新鲜事物的人,在技术上总喜欢优化、迭代、升级,亲手构建出优美并且有价值的产品极具满足感,而前端开发者与用户的距离比任何一个岗位都要近,甚至可以说:前端开发者决定了用户体验。


如果一个人同时具备前端开发 + 产品的能力是不是还不错?



  • 消除技术与产品的认知壁垒,后续我们做到了,在我们公司,产品和技术从来不吵架

  • 如果有能力决定产品走向,开发者就可以是一名有灵魂的大头兵,甚至晋升军衔。


也就是说,我逐渐不再下探技术,而是走向用户


走向用户,并不意味着开发者要放弃对技术的学习,技术很重要,技术能力依旧是核心竞争力,而是随着我对技术的理解逐渐深入,开始越发清晰的了解到,究竟什么样的能力是前端开发者最需要的,什么样的能力边际收益是最高的。



在我从事前端开发第2年至今,我一直都有在产品这方面作出努力与尝试。


关于这几年产品的结果,大概可以用这句话来形容:


100个想法中,80个想法死在在调研与分析阶段,15个想法死在在demo阶段,最后落地5个想法,其中4个反响平平,只有1个还算成功。


虽然绝大部分都是失败,但是站在此刻回头来看,这几年的产品的学习将我的思维高度提升了很多,综合能力也提升了很多,因为很多想法初期是没有团队介入的,凡事都需要亲力亲为,需要思考需求、写最小demo、UI设计、沟通,而上线后,有需要又要为产品负责,就需要进行数据分析,线上数据的观察,等等....,这其实比写代码累多了。


这些进步不像程序的学习有一个可量化的指标,这样的软实力很多的是一种感觉,虽然依旧是时而迷茫,但是也偶尔会有一些收获。


虽然我做的产品决策越来越多,公司与同事给予的信任也越来越多,所以在产品上的舞台也是越来越大,而看了《微信背后的产品观》,里面的想法非常符合我对产品的理解,当然我的理解是相对浅显与张小龙没法比的,不过张小龙对产品的理念,以及他对需求的理解,这样一套浪漫的方法论,真的非常有魅力,这也是为什么,我看完最后决定写这篇文章。


跨越“技术”思维


这几年,我的老板经常会找我聊天,因为我和他提过对产品很有兴趣,在前两年,他反复和我提一句话技术为业务服务,我当时觉得我理解这句话了。


我想,这不是废话,写代码最终都是为了公司的项目,为了更好更快的完成公司需求,我要狠狠的学习技术。


之后来随着我写的代码越来越多,我对这句话逐渐有了新的理解。


之前过于执着于技术,总是站在写代码的角度去理解,而这句话的侧重点是业务,或者我们换个词交付


并不是技术推动交付,而是在推动交付的因素中,技术是其中之一,我们可以衍生出很多类似的话;设计为业务服务、产品为业务服务....


所以技术的目的并非技术,而是交付价值。


人们总是不自知的放大自己在团队中的价值,这样只会蒙蔽大家的视野,走到更高处,对很多事物将会有不一样的理解。



低纬度的技术思维,走向高纬度的业务(交付)思维。


最后


其实我本来只是觉得读了还不错的一本书,不复盘、不留下点什么反思会达不到学习的目的,写着写着就想到了自己的职业,想到了这几年的经历;


总结性的话不说太多,希望可以帮助到屏

作者:狗阿木
来源:juejin.cn/post/7270331527506100264
幕前你,我们共同成长,共同进步。

收起阅读 »

人情世故职场社会生存实战篇(一)

这个专栏文章都是群友问的实际问题,我在这里分享出来,希望可以帮助到大家,欢迎大家踊跃发言 1、问:老师,我们领导今天请我吃饭,我送了两瓶梦之蓝,一条中华,临走他问了我一下这酒多少钱,有点奇怪,他问酒多少钱是什么意思?另外走的时候非要给我一箱枣,我感觉去了人家家...
继续阅读 »

这个专栏文章都是群友问的实际问题,我在这里分享出来,希望可以帮助到大家,欢迎大家踊跃发言


1、问:老师,我们领导今天请我吃饭,我送了两瓶梦之蓝,一条中华,临走他问了我一下这酒多少钱,有点奇怪,他问酒多少钱是什么意思?另外走的时候非要给我一箱枣,我感觉去了人家家里提东西很正常,拿人家的枣是不礼貌的,何况不值两个钱,有点奇怪。


答:领导问你这两瓶酒多少钱?他想听到的答案是:这两瓶酒是朋友送的,我也不喜欢喝酒,麻烦你帮我消化一下吧。领导想听这句话,醉翁之意不在酒嘛。临走的时候他把这个大枣送给你,这个枣肯定是别人送给他的,这个大枣他不想要,他想让你帮他消化一 下,那这个枣你是要接住的,你说太好了,我老婆最喜欢吃枣了,这样的话,你就满足了领导的精神需求,他就感觉到你特别懂事儿。也就是领导想跟你做一个良性的循环和互动。


2、问:老大经常分给我费时又费力的垃圾项目,别的同事都是简单高效的赚钱项目,我认为是随机分的,我该怎么拒绝领导的安排啊?


答:你手里有2个项目,一个可以轻轻松松赚10万,一个累死才能赚8000。现在有人找你要项目?有人送了你阿弥陀佛,有人送了你烟酒,有人送了你美女,有人送了你5W真金白银,有人送了你烟酒茶美女+五W。最后你把项目给谁啊?你觉得是随机的,你急什么啊?下次能不能轮到你啊?找人算一卦试试,比如有些考试啊,面试和笔试都是私人订制,然后拉一群沙雕陪着走个形式,这样显得更公平。公平是相对而言,不是绝对的。


3、问:下周即将和我们公司大老板开会,参会的有同级别的其他中心经理,还有我们的直属总监,要求我们经理都说一下各自中心今年的工作计划。请教您,在会议上说工作计划,有没有什么技巧?有需要注意的地方吗?


答:1、领导安排的工作,我全落实了。


2、领导的目标也是我的目标,领导安排什么,我做什么,我力求每一步工作都做扎实 ,都做到位。


3、我会跟领导搞好关系,我会跟同事搞好关系,在领导的帮助下,还有同事的帮助下,去为公司创 造更多的利润。


就是用这三句话,在里面转圈圈就行了。


4、问:请教一个事,我是一个老师,我平时很注重提升自己,会不断学习各种课程,所以经常会在办公室学到很晚,然后我感觉有同事在背后不断嘲讽我,她们经常问我,你不是晚自习没有课程吗,怎么还在这里呀,就是类似这种,我感觉很反感。


答:同事为什么会在背后嘲讽你呀,因为你不懂人性。你要进步没有错,但你不能给身边的人压力。你要进步,在家里你学到凌晨2点都无所谓,但在公共场合,不要展现你的野心,如此会给别人造成心理上的压迫感。其次你要学会回话,会示弱。人家问你:晚上不是没课程吗?怎么还在这里呀?你说,我笨鸟先飞,下周的课我要提前准备,XX老师你的教案可以借我参考吗,听其他老师说,你的教案做的很好。你这样子给她回话,以后,她就不会在背后说你坏话了。


5、问:在单位,直接大领导一直很挺我,因为各种原因并没有走得很近,单位副职一直不太认可我。这次选上了大领导的秘书(有竞争对手的情况下),我是否应该送礼给大领导?


答:在这种情况下,你不要送礼,这时候送礼对你来说根本不是加分,他反而减分。万一被你的竞争对手看到呢,把你给举报了,肯定对你是不利的,那你这个时候应该怎么样做呢,非常简单。以前怎么样,现在你还怎么样,你现在不要乱了这个方寸。你要是送礼表态的话,你早都应该送了,你非要等到最后再送礼,那人家觉得你临时抱佛脚,你这个人怎么那么势利啊?这个时候,你要做的,你想一想,你能够帮他们做点什么,为他们创造一些什么价值,这些价值有可能是物质方面的,有可能是精神层面的,就是你要一直旺他们,替他们排忧解难,你就顺着这个点往前跑,你100%的能立起来。


6、问:如果领导和你聊骚,你又不想牺牲自己,你该怎么办?


答:只要你在一个位阶和势差比你大的人手下工作,他们总能创造各种有利的机会。男人最懂男人,狗改不了吃屎,你只要被得手了,对于他来说,你的结局大致已经有一个雏形了。如果他和你聊骚,你就把他对你的聊骚内容截图,他发一次,你截图一次,找个隐秘的空间保存起来。如果他不微信,打电话怎样?录音啊,傻呀!以后有需要动动或者求到他的地方,没有什么比这个更有分量了。或者,你直接点他一下,你说,领导,给我发的信息,我好多都截图(录音)保存了,以后我不想再存了,到此为止吧。领导我相信您是一个睿智的人,希望能跟您多聊点正面的,积极的东西。


7、问:如何搞关系?


关系是跑出来的,跑关系三个字,点出了人际交往的秘密。关系不是等出来的,是天天跑,多多跑,关系就有了。好处有多到位,机会就有多到位。很多傻子被君子之交淡如水严重洗脑,脑子坏掉了,不知道君子只出现在书里面,忘记了人情往来中的尊重和礼仪。


处处感恩,处处逢源,处处遇贵人。时时送出好处,处处遇见活雷锋。不要空手套白狼、四两拨千斤、以小博大,谁这么教你谁就是把你当成傻X呢。有求于人的的时候,就要舍得投入,舍得砸钱,舍得亲近。这个时候不舍不得,等事情过去了,再怎么投入,也意义不大了。


求人办事,最忌讳临时抱佛脚。如果遇到这种情况,最好找个中间人帮忙。备好双份礼物,一份给引荐人,一份给办事人。但是一定要明白,如果事情事能够办下来,并不是因为礼物,而是中间人的关系网与情面的作用。所以啊,对咱有用的人,必须要细水长流地去维护。人生在世,需要有靠山。


天生的靠山可遇不可求,可求的靠山都是自己主动经营来的。你打牌、抽烟、喝酒,不是因为喜欢,而是因为别人喜欢。只做自己喜欢的事,只能和自己玩,只有做了别人喜欢的事,才能和别人玩到一块。


8、问:老师,为什么你说能不送礼就尽量不要送礼?


答:能不送礼就不送礼。尤其是职场。送了,就要持续送。送着送着,你不送了,收礼的人觉得你看不起他了,你不重视他了,你过桥拆河。以后他会弄你。你是怎么死的,都不知道。咱不想送礼了,咱遇到对方了,咱彬彬有礼就行了。不要做啥深度链接。你用不上他,为什么给他送礼呀,难道你贱啊?职场中,很多人,一开始觉得好玩,也想锻炼一下自己,就去给领导送礼了,送了一年,发现自己的福利没啥变化,就不想送了,第二年,他发现他过得比以前更不好了,他也不知道为什么!关于送礼,第一,我的看法就是,能不送,就不送,送了,就不能断,你送着送着不送了,领导觉得你是不是看不起他了,还是觉得他没用?啥,既然觉得我没用了,那我就要拿出点手段让你重新认识我!人心难测!


9、问:我们公司现在在搞改革,很多岗位会涉及到变动,甚至被安排分配到下一级公司干活。现在新来的上司是我四年前的老领导,他之前被调去别的单位了。我跟他以前关系蛮好,期间就过节短信问候,现在我想找他,让他帮我继续留在现在这个层级干活,你觉得他会帮我嘛?要怎么开口?


答:这个帮不帮暂时还不知道,你需要带上心意,烧香拜神 投石问路。你现在也犯了多数人都会犯的错误,以至于到了关键时刻非常被动。


第一个就是临时抱佛脚,看的不够远,总以为太阳可以一直照耀着你,不懂的提前去布局,铺设你日后能用得到的关系站点,导致你现在关键性的节点就缺乏了很重要的助力。


第二个,那就是不懂的主动去搭关系,你的老领导调回来的那天,你为什么不去拜访、祝贺?为什么不提这猪头去拜庙门?现在才想到人家。不过,如果他之前帮助过你的,一般还会帮助你第二次,第三次,所以,你带上心意,去拜访一下他,事到临头,见面了把礼物放下,你先回望过去,把彼此的关系拉进,然后谈谈自己的想法,你说 老领导,我的事情劳烦您费心了,这事成不成,没有关系,很久没有见过领导了,这点小心意,请领导不要嫌弃。


10、问:我找一个领导帮忙和单位打招呼,因为这一次有大调整,请求帮忙调整一下,之前与帮忙这位领导都是过节礼仪性的人情,这次找这个领导帮忙,领导答应帮忙,简历也给了,但是一直没给反馈,我第二次找,他正好周末和我们单位的领导们一起吃饭,他说会帮我说,现在都没给反馈,是不是我要去意思意思,我又担心送了,万一这领导也没帮上忙咋办?


答:我送你一句话吧:送礼是从承担风险开始的,而不是从刻意追求回报率开始的!其实,在我看来,你不适合送礼,因为你的的觉悟还没有到。觉悟不到的东西,你是拿不住也做不好的。


送礼,原本就是风险投资。咱不是说送礼了,100%有回报率。但送礼有回报率的概率至少在50%以上。上211/985的概率有多少呀?听说985全国只有2%,211占5%(是我粘贴复制过来的,数据真假不知道啊)。都说读书改变命运,但通过读过改变命运的概率有多大?3%。通过送礼改变命运的概率有多大?50%。


生活中,开窍的人非常少。开窍的人,都是直接给我送红砖,说领导辛苦了,事成不成,遇到您认识您,就是我最大的福气。这样的人,都是开窍的人。习惯性承担风险,习惯性承担责任。可惜这样的人实在太少了。


送礼,是从习惯性承担风险开始的。而不是说我送了,对方必须100%给我一个说法。建议你带上心意,去拜访一下他,他收了,会告诉你一两句有用的话,自然,你就知道下一步怎么样了。


欢迎同学们关注本专栏,会持续更新社会上的人情世故,有问题的同学也欢迎留言~


作者:公z号_纵横潜规则
来源:juejin.cn/post/7267576629295022080
>如果对你有帮助的话给个关注吧~

收起阅读 »

七夕节那天,我被裁员了

前言 先祝jym昨天七夕节快乐吧,有情人终成眷属 一直想在掘金上发文但迟迟未行动,上学时在b乎写了一段时间一直切换不来平台。看着b乎仅存的几百号关注者到这边只能从0开始很是不情愿( ̄Д ̄)ノ,也行吧就从零开始了,我的工作似乎也得从零开始了。该死的仪式感... ...
继续阅读 »

前言


先祝jym昨天七夕节快乐吧,有情人终成眷属


一直想在掘金上发文但迟迟未行动,上学时在b乎写了一段时间一直切换不来平台。看着b乎仅存的几百号关注者到这边只能从0开始很是不情愿( ̄Д ̄)ノ,也行吧就从零开始了,我的工作似乎也得从零开始了。该死的仪式感...


image.png


难忘的七夕


如标题所见,七夕节被通知裁员了。单身狗今天受到双重痛击。说的是公司迫于压力需要节流,部门会裁1/4。而我恰好就是那个“幸运儿”。其实从前些时间就早有风向,先是ui,到测试,终于到开发人员了,而到了开发最先动刀的果然还就是鼠鼠前端。


23届真的太惨了。在这里从实习到转正也快干了1年了,但才刚拿了一个月转正工资就遇到调整,实属难绷。


应届生身份也没了,工作年限也达不到,双重叠buff了属于是,老实说刚听到这消息的时候自己还是懵的,也就和往常一样上班怎么今天就这么突然开这样的会议呢?


而后是无奈,但是又有点兴奋,正好可以逼自己再出去外面探探,看看市场如何。

回家路上,打开手机刷了刷boss,要么招24的要么一到两年工作经验的,一下子又把我难住了。。。我怎么这么背啊5555


整个晚上我都在思考人生,我在想假如自己不是读计算机会如何?我是不是能没负担的尝试各种工作,自己真的要在程序员这条赛道一条路走到黑吗?


程序员的工作又何尝不是围墙,技术类的工作是建立起了行业壁垒,在外行人看来能将代码变成程序是一件很酷的事,而正是这道壁垒,让外行的人想进来,里面的人想走却又鼓不起勇气。


image.png


是啊 我除了写代码还能干什么呢?难道真的放弃学了这么久在外行人看来很厉害的技术去做其他工作吗?(脱不下的长衫又穿上了)


总结


我一直认为无论发生什么事,一切都是上天最好的安排,世上无所谓输赢祸福,关键是从中有所收获,写几点感悟吧:



  • 拥抱不确定性是踏入社会后最需要学会的。读书时每个人都是按部就班,除了学习剩下的挑战就是安排到日程上的考试,而步入社会每天遇到的事情都不一样,so be water my friend 随机应变 随遇而安

  • 负能量可以有,适当摆烂一下,但睡一觉起来就得调整好心态了,享受生活不要被蛋疼的事影响

  • 不要畏手畏脚,敞开自己而不是压抑自己,一辈子很短,很多事情不去尝试就来不及了

  • 灵活就业或许才是这个时代下的答案?不知道

  • 行动起来最重要,如前面说的想在掘金发文却拖延了很久,而正是这回吐槽却让我奋笔疾书了起来,命运的齿轮或许就此转动,后面我会督促自己继续更开发过程中的杂记、生活记录

  • 接下来的安排:先编辑个简历再出发,然后整理一下这一年来开发的东西,记录一下


结语


我也不知道后面会如何,走一步是一步吧,通知还没正式下来。如果读到这里的你觉得我我是个有趣的人,那么点个关注吧,这也是我更文的动力


失败并不存在

作者:前端咖啡豆
来源:juejin.cn/post/7270152997166252071
,关键是故事仍在继续......

收起阅读 »

iOS 陀螺仪技术的应用探究

iOS
本文源自本人的学习记录整理与理解,其中参考阅读了部分优秀的博客和书籍,尽量以通俗简单的语句转述。引用到的地方如有遗漏或未能一一列举原文出处还望见谅与指出,另文章内容如有不妥之处还望指教,万分感谢。 前言 陀螺仪是一种硬件传感器,能够感知设备的旋转和方向变化。...
继续阅读 »

本文源自本人的学习记录整理与理解,其中参考阅读了部分优秀的博客和书籍,尽量以通俗简单的语句转述。引用到的地方如有遗漏或未能一一列举原文出处还望见谅与指出,另文章内容如有不妥之处还望指教,万分感谢。



前言


陀螺仪是一种硬件传感器,能够感知设备的旋转和方向变化。它通常通过MEMS(微机电系统)技术来实现,内部包含了微小但高精度的陀螺仪器件、加速度计和磁力计等传感器,可以实时地感知设备在空间中的旋转角度和方向。


在iOS系统中,可以通过CoreMotion框架来访问陀螺仪的数据。在开发iOS应用程序时,可以使用CoreMotion框架提供了一个CMMotionManager类,该类可以用来获取设备的运动数据,包括陀螺仪数据、加速度计数据等。


iOS陀螺仪的精度和灵敏度通常比较高,可以实现比加速度计更加准确的姿态估计和方向识别,也可以帮助开发者实现更加真实的虚拟现实和增强现实应用。同时,iOS陀螺仪的实时响应和低功耗特性,也使得它在移动应用程序开发中得到了广泛的应用和认可。


基础知识


在开发前,有几个基础的知识点,我们需要事先了解,这对我们后期开发会有更好的帮助


三轴方向


在 iOS 中,陀螺仪传感器的三轴方向通常遵循右手系的规则。具体来说:

  • x 轴:表示设备绕着横轴旋转。当设备的屏幕朝上时,x 轴指向设备的右侧;当设备的屏幕朝下时,x 轴指向设备的左侧
  • y 轴:表示设备绕着纵轴旋转。当设备的屏幕朝上时,y 轴指向设备的顶部;当设备的屏幕朝下时,y 轴指向设备的底部
  • z 轴:表示设备绕着竖轴旋转。当设备的屏幕朝上时,z 轴指向设备的正面;当设备的屏幕朝下时,z 轴指向设备的背面



姿态信息


陀螺仪用于侦测设备沿三个轴为中线所旋转时的角速度,故有了三个姿态信息,分别为 pitch (纵倾), roll (横倾) 和 yaw (横摆)

  • pitch(俯仰角):表示设备绕着 x 轴旋转的角度,也称为纵倾角。当设备正面朝上时,俯仰角为 0°;当设备向上仰起时,俯仰角为正值;当设备向下倾斜时,俯仰角为负值
  • roll(横滚角):表示设备绕着 y 轴旋转的角度,也称为横倾角。当设备正面朝上时,横滚角为 0°;当设备向右侧倾斜时,横滚角为正值;当设备向左侧倾斜时,横滚角为负值
  • yaw(偏航角):表示设备绕着 z 轴旋转的角度,也称为横摆角。当设备正面朝北时,偏航角为 0°;当设备逆时针旋转时,偏航角为正值;当设备顺时针旋转时,偏航角为负值
  • CMRotationMatrix 结构体表示设备绕X、Y、Z轴的旋转矩阵,可用于描述设备在三维空间中的方向和旋转状态, 这里再细讲该结构体中9个元素所代表的含义


陀螺仪的使用

import CoreMotion

let motionManager = CMMotionManager()
if motionManager.isGyroAvailable {
motionManager.gyroUpdateInterval = 0.1
motionManager.startGyroUpdates(to: OperationQueue.main) { (data, error) in
if let gyroData = data {
let rotationRateX = gyroData.rotationRate.x
let rotationRateY = gyroData.rotationRate.y
let rotationRateZ = gyroData.rotationRate.z

print("Rotation Rate X: \(rotationRateX)")
print("Rotation Rate Y: \(rotationRateY)")
print("Rotation Rate Z: \(rotationRateZ)")
}
}
} else {
print("Gyroscope is not available.")
}

关键类解析


CMDeviceMotion


CMDeviceMotion 是一个 Core Motion 框架中的类,用于表示设备的运动和姿态信息。通过 CMDeviceMotion 类,可以获取到设备在三维空间中的加速度、旋转速度、重力加速度、旋转矩阵以及设备的姿态信息等,以便进一步进行处理和计算。


下面是 CMDeviceMotion 类中常用的属性和方法:

  • attitude 属性:表示设备的姿态信息,包括俯仰角(pitch)、横滚角(roll)和偏航角(yaw)等信息。
  • userAcceleration 属性:表示设备在三维空间中的加速度,即不包括重力加速度的加速度
  • rotationRate 属性:表示设备在三维空间中的旋转速度
  • gravity 属性:表示设备在三维空间中的重力加速度,即不包括设备加速度的重力加速度

需要注意的是,在使用 CMDeviceMotion 类时,需要首先创建一个 CMMotionManager 对象,并设置其属性和回调函数,以便获取设备的运动和姿态信息。此外,由于设备运动和姿态信息的获取涉及到多个传感器的协同工作,因此在使用时需要考虑传感器的准确性和稳定性,以避免误差和不良体验。


CMAttitude


CMAttitude 表示设备在三维空间中的姿态信息,包括设备的旋转、倾斜、方向等信息。在 iOS 开发中,可以通过 CMMotionManager 获取设备的姿态信息,然后将其保存为 CMAttitude 对象,并使用其中的各个属性来进行相应的处理和计算。


CMAttitude 类中的主要属性如下:

  • pitch:设备绕 x 轴的旋转角度,单位为弧度
  • roll:设备绕 y 轴的旋转角度,单位为弧度
  • yaw:设备绕 z 轴的旋转角度,单位为弧度
  • quaternion:设备的四元数表示,用于表示设备的旋转状态,包括旋转角度和旋转轴等信息
  • rotationMatrix:设备的旋转矩阵表示,用于表示设备在三维空间中的旋转状态

其中,pitchroll 和 yaw 属性是最基本的属性,用于表示设备绕 x、y、z 轴的旋转角度。一般来说,可以通过这三个属性来进行设备的姿态检测和相应的处理。其余的属性包括四元数、旋转矩阵,都可以用于更加精确和复杂的姿态检测和处理。


应用场景

  • 姿态估计和方向识别:通过陀螺仪获取设备旋转的角度和方向,可以实现设备的姿态估计和方向识别,广泛应用于游戏、导航、运动感知等领域
  • 图像校正和稳定:通过将陀螺仪中的旋转信息应用于图像处理,可以实现图像校正和稳定,提高图像质量和用户体验
  • 虚拟现实和增强现实:通过与其他传感器的结合和数据处理,可以实现更加真实的虚拟现实和增强现实应用,如3D游戏、AR导航、AR应用等
  • 运动检测和姿态跟踪:通过结合加速度计和地磁计等传感器的信息,可以实现设备的运动检测和姿态跟踪,如步数统计、运动轨迹记录、体感游戏等
  • 安全防护和权限控制:通过使用陀螺仪的数据,可以实现设备的安全防护和权限控制,如设备锁定、用户身份验证、数据加密等

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

iOS:NSNotification.Name从OC到Swift的写法演进

iOS
前言 在闲来无事的时候,我会抽时间看看Foundation、UIKit等相关库的Swift代码说明与注释。说实话,有的时候看起来真的很乏味,也不容易理解。 不过有的时候也会觉得Apple这么设计API真是书写的简单漂亮,一脸佩服,今天给大家分享的就是从NSNo...
继续阅读 »

前言


在闲来无事的时候,我会抽时间看看Foundation、UIKit等相关库的Swift代码说明与注释。说实话,有的时候看起来真的很乏味,也不容易理解。


不过有的时候也会觉得Apple这么设计API真是书写的简单漂亮,一脸佩服,今天给大家分享的就是从NSNotification.Name学习一种代码编写方式,并且已经在我自己的项目中进行类似这种写法的落地实战分享。


OC时代的通知名写法


NSNotification想必大家都使用过,在iOS中处理跨非相邻页面、一对多的数据处理的时候,我们通常就是通过发通知传参,告之相关页面处理逻辑。


一般情况下,如果可以避免NSNotification的时候,我都会尽量避免使用,当然,既然系统给你了这个方法,那么在合适的场景使用也会妙手生花。


当然,对于NSNotification的通知名的管理,其实是一个看似简单,实际上可以做得非常优雅的事情。


特别是从OC过渡到Swift的过程,这段简单的代码,其实进行了很大的演变,我们不妨来看看。


下面这个是我早期写OC代码的时候,发送一个通知:

[[NSNotificationCenter defaultCenter] postNotificationName:@"CancelActivateSuccessNotification" object:nil];

大家注意看,通知名,我就是非常简单的使用硬编码字符串@"CancelActivateSuccessNotification" 来表示,硬编码的缺点就不用我多说了,编译器是不会给提示的,写错了,甚至连通知事件都没法收到,总之,这种写法是不好的。


于是,看看系统代码以及AFNetworking,我们会看见这样一种写法:


系统通知名:

UIKIT_EXTERN NSNotificationName const UIApplicationDidFinishLaunchingNotification;

AFNetworking的通知名,也是学习系统通知名的写法进行的扩展:


.h文件




.m文件 



看起来并不是太高明?也许确实如此,只不过通过.h与.m的分隔,将一个硬编码字符串变成了一个全局可以引用、IDE可以快速键入的方式,但是它至少让调用变得简单与安全,这样就足够了。


于是乎,OC时代通知名的写法,我们基本上都会用以上这种方式进行编写:


.h

UIKIT_EXTERN NSString *const CancelActivateSuccessNotification;

.m

NSString *const CancelActivateSuccessNotification = @"CancelActivateSuccessNotification";

Swift时代还是这么写吗?


Swift时代的通知名写法


其实Swift的早期,基本上还是沿用着OC的这一套写法来写通知名,不过在Swift4.2之后就迎来比较大的改变,让我们来看看调用的API与源码:

open func post(name aName: NSNotification.Name, object anObject: Any?)

open func post(name aName: NSNotification.Name, object anObject: Any?, userInfo aUserInfo: [AnyHashable : Any]? = nil)

发通知的时候,通知名被一个NSNotification.Name类型代替了,我们进去追着NSNotification.Name看:




大家一定要记住这种编码的书写方式,先送上结论:

  • 可以在一个类型里面再定义一个类型,大家可以自己尝试。
  • 什么时候嵌套?为何要这么写?当嵌套的类型与外层定义的类型有着较强关联的时候可以这么写。

说完了这些,我们可以看到在Swift中,发通知,通知名不再是一个字符串了,而是一个NSNotification.Name类型了。


那么在开发过程中,我们如何使用呢?我们不妨还是从系统提供的API开始找:




因为Swift可以随处编写一个类的分类,于是在一个类的分类中定义好该类的通知名这种书写方式随处可见,这样的好处就是通知名与类紧紧联系在一起,一来便于查找,二来便于绑定业务类型。

NotificationCenter.default.post(name: UIApplication.didFinishLaunchingNotification, object: nil)

上面这个通知一发出,通过通知名我就知道是涉及UIApplication的操作行为。


说完了系统提供的API,我们再来看看一些知名第三方库是怎么定义吧,这里以Alamofire为例:




Alamofire保持了和系统API一样的风格来定义通知名。


我们再来看看Kingfisher




Kingfisher是在NSNotification.Name分类中扩展了通知名。


顺带说一下,我自己管理与编写通知名是这样的:

extension Notification.Name {
    enum LoginService {
        /// 退出
        static let logoutNotification = NSNotification.Name("logoutNotification")
    }
}
NotificationCenter.default.post(name: .LoginService.logoutNotification, object: nil)

通过在NSNotification.Name分类中进行二级业务扩展,细化通知名。


至于大家更喜欢哪一种写法,那就是仁者见仁智者见智的事情了。


总结


本篇文章从NotificationCenter发通知的通知名开始,对OC到Swift的写法演进进行梳理与说明,举了系统API和著名第三方库的例子,给大家讲解如何写好并管理好NSNotification.Name


吐槽


掘金的这个编辑器,我直接从Xcode里面复制粘贴代码的体验真的很不友好,导致我比较长的代码都是截图,只有较少的代码使用的代码块。


自己写的项目,欢迎大家star⭐️


RxStudy:RxSwift/RxCocoa框架,MVVM模式编写wanandroid客户端。


GetXStudy:使用GetX,重构了Flutter wanandroid客户端。


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

Swift 中async/await 简单使用

iOS
在 Swift 5.5 中,终于加入了语言级别的异步处理 async/await,这应该会让用回调闭包写异步调用方法时代彻底结束了! 这篇文章就简单总结一下这个功能使用吧。 异步函数 所谓异步,是相对于同步而言,这是一种执行任务的方式,同步的执行任务,任务需...
继续阅读 »

在 Swift 5.5 中,终于加入了语言级别的异步处理 async/await,这应该会让用回调闭包写异步调用方法时代彻底结束了!



这篇文章就简单总结一下这个功能使用吧。


异步函数


所谓异步,是相对于同步而言,这是一种执行任务的方式,同步的执行任务,任务需要一个一个的顺序执行,前边的好了,后边的才能运行。而异步就不是这样,它不需要等待当前任务执行完成,其他任务就可以执行。


在 Swift 5.5 中,添加了 async 关键字,标记这个函数是一个异步函数。

func getSomeInfo() async -> String { ... }
/// 可以抛出错误的异步函数
func getSomeInfoWithError() async throws -> String { ... }


这里需要注意的是,如果我们想调用异步函数,就必须在其他异步函数或者闭包里面使用 await关键字。

func runAsyncFunc() async {
let info = await getSomeInfo()
...
}

func runAsyncErrorFunc() async throws {
let info = try await getSomeInfoWithError()
...
}

实际使用异步函数的时候,我们是无法在同步函数里使用的,这时Swift会报错。要使用的话,就需要我们要提供了一个异步执行的环境 Task

func someFunc() {
Task {
runAsyncFunc()
}
}


异步序列


如果一个序列中的每个信息都是通过异步获取的,那么就可以使用异步序列的方式遍历获取。前提是序列是遵守AsyncSequence协议,只要在for in 中添加 await关键字。

let asyncItems = [asyncItem1, asyncItem2, asyncItem3]
for await item in asyncItems { ... }

多个异步同时运行


这个可以使用叫做异步绑定的方式,就是在每个存储异步返回信息的变量前边添加async

async let a = getSomeInfo()
async let b = getSomeInfo()
async let c = getSomeInfo()
let d = await [a, b, c]
...

这时运行的情况就是 a b c 是同时执行的,也就是所说的并行执行异步任务,即并发。


结构化并发


上边在提到在同步函数中使用异步函数,我们需要添加一个Task,来提供异步运行的环境。 每个 Task 都是一个单独任务,里面执行一些操作,这操作可以是同步也可以是异步。多个任务执行时,可以把它们添加到一个任务组TaskGroup中,那么这些任务就有了相同的父级任务,而这些任务Task又可以添加子任务,这样下来,任务之间就有了明确的层级关系,这也就是所谓的结构化并发


任务和任务组


任务组可以更为细节的处理结构化并发,使用方式如下,就在任务组中添加单个任务即可。

func someTasksFunc() {
Task {
await withTaskGroup(of: String.self) { group in
group.addTask {
let a = await getSomeInfo()
...
}
group.addTask {
let b = await getSomeInfo()
...
}
}
}
}

从运行的方式来说,这种使用任务组的情况和异步绑定的效果一样,简单的异步任务,完全可以使用异步绑定的方式。而任务和任务组是为更为复杂的并发情况提供支持,比如任务的优先级,执行和取消等。


如果异步函数是可抛出错误的,使用withThrowingTaskGroup就行。


解决数据竞争的Actor


在并发过程中,对于同一属性数据的读取或者写入,有时会有奇怪的结果,这些由于在不同的线程,同时进行了操作。消除这种问题的方式,就是使用 Swift 提供的 Actor类型。 一个和类差不多的类型,但是对自身可变内容进行了隔离。

actor SomeInfo {
var info: String
}

外部在访问其info属性时,actor 做了隔离,每次只能一个线程访问,直接访问就会报错。而且外部不能进行修改,只有内部才能修改。


外部访问方式就是需要异步执行,在异步环境中,添await

let content = SomeInfo(info: "abc")
let info = await content.info)
...

总结


以上就是Swift 5.5 async/await的简单使用了。了解了这些,就可以日常开发中替代闭包回调,告别回调地狱和处理不完的 completionHandler了。😎
目前官方已经在已有闭包回调处理异步的地方,都增加async异步版本,自行查看文档了解吧。。


另外附上一些很有帮助的文章地址,这些地方都有更为详尽的说明,参考学习起来!


Swift 5.5 有哪些新功能?


http://www.hackingwithswift.com/articles/23…


Swift 并发初步


onevcat.com/2021/07/swi…


并发


swiftgg.gitbook.io/swift/swift…


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

人走茶凉?勾心斗角?职场无友谊?

你和同事之间存在竞争关系 要不要把工作关系维护成伙伴关系 明枪暗箭防不胜防 背后捅刀子往往最不设防 大家是否在职场上交友是有也遇到过以上困扰呢? 不要在职场上交“朋友”,而是要寻找“盟友”。 这两者的区别在于应对策略: 我们会愿意为“朋友”牺牲自己的利益,像是...
继续阅读 »

你和同事之间存在竞争关系


要不要把工作关系维护成伙伴关系


明枪暗箭防不胜防


背后捅刀子往往最不设防


大家是否在职场上交友是有也遇到过以上困扰呢?


不要在职场上交“朋友”,而是要寻找“盟友”。


这两者的区别在于应对策略:


我们会愿意为“朋友”牺牲自己的利益,像是一张年卡。


而结交“盟友”就是为了一起争取更多利益,《孔乙己》说得好:“这次是现钱,酒要好。”


所以,在职场上的“受欢迎”和社交场、朋友圈上的“受欢迎”之间有着本质的区别:


你和你的同事未必真心喜欢彼此,但在日常相处当中能够客气、友善地交往。


大家需要寻找盟友时会第一个想到你,在争斗冲突时会尽量绕开你,这就是一种非常理想的“受欢迎”状态。 不要在职场上寻求友谊和爱,这件事是不对的。


在这里给大家列出一个在职场上受欢迎的清单。


1.实力在及格线以上


这是一切的前提。职场新人要“先活下来,再做兄弟”,稳住了工作能力这个基本面,才有资格和同事谈交情。


实力不够的人会拖累整个团队、增加所有人的工作量,大家恨都来不及,绝对不会和他称兄道弟。


实力强可以表现为实力本身,在初级职位上,也可以表现为潜力。


极少数特别强大的人可能从一开始就能很好地完成工作,但是大部分人在新加入一个团队时都需要经过一段时间的磨合,在这个过程中有欠缺和不足都是正常的,你所表现出来的敬业精神、学习能力和进步的速度才是大家对你进行评价的关键。


刚入职的新人,对于要做的事情完全没有概念,但是为人极勤奋又上进,给他布置的任务会完成得特别扎实,每一天都在飞快地进步。这样的人在职场上永远都能收获一大把来自他人的橄榄枝。


2.比较高的自尊水平


高自尊的人对自己评价高,要求也高,又能够带着欣赏的眼光去看周围的人,他们不光是很好的父母、伴侣和朋友,同时也是职场上最好的结盟对象。


高自尊的人往往拥有很多优秀的品质,同时他们也能够理解“大局”,和他们合作不用在鸡毛蒜皮的细节上纠缠推诿,可以把精力全部用来开疆拓土,极大地降低团队的内耗。


如果你是一个高自尊的人,在日常生活中表现出了自律和很好的品行,就会收获高自尊同类的赞赏。有些低自尊的人可能会认为你的言行是在“装X”,别犹豫,把他们从你的结交名单当中划掉,高自尊会帮你筛掉一批最糟糕的潜在合作者。


如果你是一个部门的领导者,记得要维护高自尊的下属,他们都是潜在的优秀带队者,给他们一个位子就可以坐上来自己动,给他们一点精神鼓励和支持,他们就会变得无所不能。


即使高自尊的手下可能某些地方让你感到嫉妒或者冒犯(这是常见的,嫉妒是每个人都一定会有的情感),也绝对不要默许或者纵容低自尊的妄人跑去伤害他们,否则会伤了大家的心,事业就难以成功了。


“朕可以敲打丞相,但你算什么东西”就是对这种低自尊妄人最好的态度。


3.嘴严,可靠


在任何一个群体当中,多嘴多舌的人都不会受到尊重,而在职场上,嘴不严尤其危险。


如果你是一个爱说是非的人,围绕在你周围的只会是一帮同样没正事、低级趣味的家伙。你会被打上“不可靠”的标记,愿意和你交流的人越来越少,大家等着看你什么时候因为多嘴闯祸,而强者根本不会和你为伍。


有些同学曾经给我留言说,自己很内向,不知道如何跟同事拉近关系。内向的人最适合强调自己的“嘴严”和“可靠”,在职场上,这两项品质远比“能说会道”更让人喜欢。


4.随和,有分寸


体面的人不传闲话,也不会轻易对旁人发表议论。


“思想可以特立独行,生活方式最好随大流”,这是对自己的要求,而他人的生活方式是不是合理,不是我们能评价的。


哪怕是最亲近的人,都未必能知晓对方的全部经历和心里藏着的每一件小事。在职场上大家保持着客气有礼的距离,就更不可能了解每个人做事的出发点和逻辑,“看不懂”是正常的,但是完全没有必要“看不惯”。如果还要大发议论,把自己的“看不惯”到处传播,你的伙伴就只会越来越少。


有人说在北上广深这样的大城市,人和人之间距离遥远,缺人情味,太冷漠。


这不是冷漠,而是对“和自己不一样”的宽容,这份宽容就是我们在向文明社会靠拢的标志。


5.懂得如何打扮


还记得斯大林的故事吗?在他离开校园之后,从头到脚都经过精心设计,不是为了精神好看,而是要让自己看起来就像一位投身革命事业的进步青年。


有句老话叫做“先敬罗衣后敬人”,本意是讽刺那些根据衣饰打扮来评价一个人的现象。我们自己在做判断的时候要尽量避免受到这类偏见的影响,但是对他人可能存在的偏见一定要心中有数。人是视觉动物,穿着打扮是“人设(人物设定)”的一部分,在我们开口说话之前,衣饰鞋袜就已经传达了无数信息。


想要成为职场当中受欢迎的人,穿着打扮的风格就要和公司的调性保持一致,最安全的做法是向你的同事靠拢。


在一个风格统一的群体当中,“与众不同”这件事自带攻击性。如果在事业单位之类的上年纪同事比较多的地方上班,马卡龙色的衣服和颜色夸张的口红,最好等到下班时间再上身。


这不是压抑天性,而是自我保护和职业精神。


6.和优秀的人站在一起


在职场上,优秀的人品质都是相似的:勤奋,自律,不断精进。如果发现了这样的同事,就要尽量和他们保持良好关系。


但是,单纯的日常沟通并不足以让你们成为盟友,正式结盟往往是通过利益交换和分享:当你遇到棘手的工作任务,就可以主动邀请对方共同跟进,同时将一部分利益让出去。愉快的合作是关系飞跃的最好契机。


优秀的人能认可的,通常也都是自己的同类。如果你能获得他们的称许和背书,在同事当中的地位自然会有所提升。


7.知道如何求助


前两天有一位关系户同学留言说,自己即将去实习,因为家人的关系可以得到一些行业资深专家的指点,问自己应该如何表现,是不是不懂就要问,像“好奇宝宝”一样,对方就会觉得自己好学上进。


我告诉她说,不要上去就问,有任何疑惑都先用搜索引擎找一下答案,如果找不出来,再带着你搜到的细节去询问那些资深前辈。


互联网时代有个很大的变化,就是人们获取信息的成本大大降低。善用搜索引擎寻找答案,就能更快、更精准、更全面地找到自己想要的东西,这种方式比跑到对方工位边用嘴问效率高得多。


凡事都问,只会让人觉得你的文字阅读能力有限,同时既不把自己的时间当回事,也不尊重别人的时间。尤其对方还是行业中的专家,他们的时间一定比实习生的宝贵多了。如果网上找不到答案,再带着细节去仔细咨询,这样的请教才是高效的,才能证明你是一个“好学上进”的人。


职场不是校园,不会再有一群老师专门负责手把手地教你,不轻易占用其他同事的时间会让你成为一个自立、有分寸、受尊重的人。毕业之后,你取得进步的速度、最终的上升空间,都和使用搜索引擎寻找答案的能力呈正相关。


8.技巧地送出小恩小惠


小恩小惠带两个“小”字,并不意味着这是一种微末小技。事实上,即使是最普通的零食,只要讲究得法,都可以送到人心里。


你的同事当中有没有因为宗教信仰而忌口的情况?


甲和乙爱吃辣,丙和丁爱吃甜,是不是两种口味都来上一点?


要留心同事的自我暴露,最好是用一个小本本记下来,关键时刻可能派上大用场。大家都是成年人,不会像孩子一样轻易被小恩小惠打动,打动我们的往往是“你把我放在心上”的温暖。


9.良好的情绪管理能力


很多时候这是个隐藏特征,但是自带“一票否决”属性:平时表现得沉着稳重,周围同事们不会有特别明显的感觉,然而歇斯底里和失控只要有一次,之前苦心经营的人设就会全面崩塌。情绪不稳定的人一般没人敢惹,但是也没人会在意了:你会被视为一个“病人”,很难再有大的发展。


已经发泄出去的情绪不能收回来,这个时候不要反复陷入纠结和悔恨,待在情绪里不出来,钱花出去了就不要去想,不要去比价。


如果情绪失控了,应该立刻做到的是原谅自己,然后考虑如何不再有下一次失控。要知道大多数人一辈子都至少会换三四次工作,了不起是换个地方,重新再来。


有的人特别幸运,天生长得好看,容易被人喜欢。


如果不是让人眼前一亮的高颜值人士,就不要太心急了。


成为一个自律、行为可以预期的人,也能慢慢地被别人喜欢。


人生很长,

作者:程序员小高
来源:juejin.cn/post/7255589558996992059
被人喜欢这件事,我们不用赶时间。

收起阅读 »

开发者不需要成为 K8s 专家!!!

之前有一篇文章 “扯淡的DevOps,我们开发者根本不想做运维!” 得到了许多开发者的共鸣,每一个开发人员,都希望能够抛却运维工作,更专注于自己开发的代码,将创意转化为令人惊叹的应用。然而事不尽如人意,到了云原生时代,开发者的运维工作似乎并没有减少,而是变成了...
继续阅读 »

之前有一篇文章 “扯淡的DevOps,我们开发者根本不想做运维!” 得到了许多开发者的共鸣,每一个开发人员,都希望能够抛却运维工作,更专注于自己开发的代码,将创意转化为令人惊叹的应用。然而事不尽如人意,到了云原生时代,开发者的运维工作似乎并没有减少,而是变成了在 K8s 上的应用部署和管理。


对运维人员来说,只需要维护好底层的 K8s,便可以在弹性、便捷性上得到巨大提升。然而 K8s 对于我们开发者而言还是太复杂了,我们还需要学习如何打包镜像以及 K8s 相关知识。许多时间都浪费在了应用部署上,我们真的需要成为 K8s 专家吗?我只是想部署一个应用至于那么复杂吗?你是否曾想过,能否有平台或方法,让我们不必成为 K8s 专家,甚至都不需要懂 K8s 就能部署好你的应用,轻松管理应用?


实际面临的问题


对于我们开发者而言,总会遇到以下不同的场景,也许是公司层面的问题、又或是业务层面的问题,也许现在用传统部署方式很简单,但随着业务增长,又不得不迁移。而面对这些问题,我们也要发出自己的声音。




  • 身处小公司,没有专门的运维。需要程序员自己负责写 Dockerfile + YAML + Kustomize 然后部署到 k8s 上面。除了工作量以外,还面临 K8s 自身的复杂性,对于多套业务,Dockerfie、Yaml、CI、CD 脚本占据了绝大部分的工作量。不写这些行不行?




  • 公司内的微服务越来越复杂,在写代码的基础上还得考虑各个服务之间的通信、依赖和部署问题,毕竟除了我们开发者以外,运维人员也不会比你更熟悉微服务之间的复杂依赖。也许已经开始尝试 Helm ,但是编写一个完整的 Chart 包依然是如此复杂,还可能面临格式问题、配置解耦不完全导致的换个环境无法部署问题。时间全写 Yaml 了。不额外编写 Helm Chart,直接复制应用行不行?




  • 在大型企业内部,正处于在传统应用迁移到云环境的十字路口。面对多种集群的需求、现有应用的平稳迁移、甚至一些公共的模块的复用如何做都将成为我们需要解决的问题。不要每次都重新开发,把现有的应用或模块积累下来行不行?




在这些场景下,我们大量的时间都消耗在额外的 Dockerfile、Yaml、Helm Chart 这些编写上了。K8s 很好,但似乎没解决我们开发者的问题,我们开发者用 K8s 反而变得更加复杂。不说需要额外编写的这些文件或脚本,单单是掌握 K8s 的知识就需要耗费大量时间和精力。
这些问题真的绕不过去吗?我觉得不是。来了解下 Rainbond 这个不需要懂 K8s 的云原生应用管理平台吧。谁说只有成为 K8s 专家后,才能管理好你的应用?


为什么是 Rainbond?


Rainbond 是一个不需要懂 K8s 的应用管理平台。不用在服务器上进行繁琐操作,也不用深入了解 K8s 的相关知识。Rainbond 遵循“以应用为中心”的设计理念。在这里只有你的业务模块和应用。每一个业务模块都可以从你的代码仓库直接部署并运行,你不是 K8s 专家也可以管理应用的全生命周期。同时利用 Rainbond 的模块化拼装能力,你的业务可以灵活沉淀为独立的应用模块,这些模块可以随意组合、无限拼装,最终构建出多样化的应用系统。


1. 不懂 K8s 部署 K8s 行不行?


行! 对于很多初学者或者开发人员来说,如果公司内部已经搭建好了可用的 K8s 平台,那么这一步不会是需要担心的问题。但是对于一些独立开发者而言,却很难有这样的环境,而 Rainbond 就提供了这样的解决方案,在 Linux 服务器上,只需要先运行一个 Docker 容器,访问到 Rainbond 控制台后,再输入服务器的 IP 地址,即可快速部署一套完整的 K8s 集群。


add_cluster


如果这还是太复杂,那么可以尝试使用 Rainbond 的快速安装,只需要一个容器和 5 分钟时间,就能为你启动一个带 K8s 集群的平台,而你在平台上部署的业务也都会部署到这个集群中。


2. 不想或不会写 Dockerfile、Yaml 等文件能不能部署应用?


能! Rainbond 支持自动识别各类开发语言,不论你是使用哪种开发语言,如Java、Python、Golang、NodeJS、Dockerfile、Php、.NetCore等,通过简单的向导式流程,无需配置或少量配置,Rainbond 都能够将它们识别并自动打包为容器镜像,并将你的业务快速部署到 K8s 集群中进行高效管理。你不再需要编写任何与代码无关的文件。只需要提供你的代码仓库地址即可。


source_code_build


3. 各类业务系统如何拼装?


在 Rainbond 中,不同的业务程序可以通过简单的连线方式进行快速编排。如果你需要前端项目依赖后端,只需打开编排模式,将它们连接起来,便能迅速建立依赖关系,实现模块化的拼装。这为你的应用架构带来了极大的灵活性,无需复杂的配置和操作,即可快速构建复杂的应用系统。


同时如果你已经实现了完整的业务程序,它可能包含多个微服务模块,你还可以将其发布到本地的组件库实现模块化的积累。下次部署时可以直接即点即用,且部署后可以与你其他应用程序再次进行拼装。实现无限拼装组合的能力。


component_assembly


4. 不会 K8s 能不能管理部署好的应用?


没问题! Rainbond 提供了面向应用的全生命周期管理运维,不需要学习 Kubectl 命令,也不需要知道 K8s 内复杂的概念,即可在页面上一键管理应用内各个业务模块的批量启动、关闭、构建、更新、回滚等关键操作,同时还支持应用故障时自动恢复,以及应用自动伸缩等功能。同时还支持应用 http 和 tcp 策略的配置,以及相应的证书管理。


app_manage


如何使用?


在 Linux 终端执行以下命令, 5 分钟之后,打开浏览器,输入 http://<你的IP>:7070 ,即可访问 Rainbond 的 UI 了。


curl -o install.sh https://get.rainbond.com && bash ./install.sh

作者:Rainbond开源
来源:juejin.cn/post/7268539925086519353

收起阅读 »

Amazon SageMaker 让机器学习轻松“云上见”!

最近,“上云探索实验室”活动 正在如火如荼进行,亚马逊云、“大厂”背景加持,来看看它们有什么新鲜技术/产品? 本篇带来对 shouAmazon SageMaker 的认识~ 闻名不如一见 首先,开宗明义,shouAmazon SageMaker 是什么? 从...
继续阅读 »

最近,“上云探索实验室”活动 正在如火如荼进行,亚马逊云、“大厂”背景加持,来看看它们有什么新鲜技术/产品?


本篇带来对 shouAmazon SageMaker 的认识~


闻名不如一见


首先,开宗明义,shouAmazon SageMaker 是什么?




官网我们可以了解到:Amazon SageMaker 是一项帮助开发人员快速构建、训练和部署机器学习 (ML) 模型的托管服务。


其实,类比于:就像咱们开发人员经常把代码部署 Github Page 上,自动构建、运行我们的代码服务。


当下,我们可以把 AIGC 领域火爆的模型共享平台 —— Hugging Face 中的模型放到 Amazon SageMaker 中去部署运行、进行微调等~


如果你还不了解 HuggingFace?那抓紧快上车!




就这张小黄脸,目前已经估值 20 亿美元,它拥有 30K+ 的模型,5K+ 的数据集,以及各式各样的 Demo,用于构建、训练最先进的 NLP (自然语言处理)模型。


是的,如果你:


1、不想关心硬件、软件和基础架构等方面的问题


2、想简化操作机器学习模型的开发流程


3、想灵活选择使用自己的算法和框架以满足不同业务需求


就可以 把目光投向 Amazon SageMaker,用它的云服务来部署你想要用的 HuggingFace 模型等~




百思不如一试


Amazon SageMaker 可以全流程的帮助我们构建机器学习模型,这样真的会省下很多心力(少掉几根头发)~


具体实践中,我们知道在数据预处理过程中,在训练模型之前,需要做一系列操作,比如:缺失值处理、数据归一化和特征选择等,Amazon SageMaker 提供了很好的预处理和转换数据的工具,助力快速完成这些工作。


在模型选择环节,Amazon SageMaker 提供多种内置的机器学习算法和框架,你可以根据数据集和任务类型选择合适的模型。


还有,提高模型性能是我们需要特别关注的,Amazon SageMaker 让你可以指定调优的目标和约束条件,系统会自动搜索最优的参数组合,这就很智能、很舒服。


模型训练完后,Amazon SageMaker 也自带易用的模型部署和监控功能;


一整套下来,训练模型感觉就像呼吸一样自然~


官网教程写的很清晰,还有很多视频讲解:# Amazon SageMaker - 入门手册


这里不赘述,仅看实战中代码表示,感受一二:


如何在Amazon SageMaker 上部署Hugging Face 模型?




1、首先,在 Amazon SageMaker 中创建一个 Notebook 实例。可以使用以下代码在 Amazon SageMaker 中创建 Notebook 实例:

import sagemaker from sagemaker
import get_execution_role

role = get_execution_role()

sess = sagemaker.Session()

# 创建 Notebook 实例
notebook_instance_name = 'YOUR_NOTEBOOK_INSTANCE_NAME'
instance_type = 'ml.t2.medium'
sagemaker_session = sagemaker.Session()
sagemaker_session.create_notebook_instance(notebook_instance_name=notebook_instance_name,instance_type=instance_type,role=role)


2、其次,下载 Hugging Face 模型,你可以使用以下代码下载 Hugging Face 模型:

!pip install transformers

from transformers import AutoTokenizer, AutoModelForSequenceClassification

# 下载 Hugging Face 模型
model_name = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSequenceClassification.from_pretrained(model_name)

3、创建一个推理规范,以指定部署的模型。可以参照以下代码创建推理规范:

from sagemaker.predictor import Predictor
from sagemaker.serializers import JSONSerializer
from sagemaker.deserializers import JSONDeserializer
from sagemaker.tensorflow.serving import Model

# 创建推理规范
class HuggingFacePredictor(Predictor):
def __init__(self, endpoint_name, sagemaker_session):
super().__init__(endpoint_name, sagemaker_session, serializer=JSONSerializer(),
deserializer=JSONDeserializer())

model_name = "huggingface"
model_data = "s3://YOUR_S3_BUCKET/YOUR_MODEL.tar.gz"
entry_point = "inference.py"
instance_type = "ml.m5.xlarge"
role = sagemaker.get_execution_role()

model = Model(model_data=model_data,
image_uri="763104351884.dkr.ecr.us-east-1.amazonaws.com/huggingface-pytorch-inference:1.6.0-transformers4.0.0",
role=role,
predictor_cls=HuggingFacePredictor,
entry_point=entry_point)

predictor = model.deploy(initial_instance_count=1, instance_type=instance_type)


4、测试部署模型

data = {"text": "I love using Amazon SageMaker!"}
response = predictor.predict(data)

print(response)

过程就是,创建 NoteBook => 下模型 => 指定模型、设定推理脚本 => 测试部署,足够简洁~


人人都能上云,人人都能训练机器模型~


不知道大家发现没有,其实现在的编程开发都逐渐在“云”化,不仅是机器学习,普通开发也是;类似低代码平台,开发不再是一点点复制代码、拼凑代码、修改代码,更多是通过自动化平台“点点点”就能构建自己想要的服务了!拥抱“云”平台,省心又省力,也在拥抱未来~


防守不如亮剑


目前市面上同类型的产品也有一些,比如:


1、Google Cloud AI Platform:Google提供的全托管的机器学习平台,可以帮助用户构建、训练和部署机器学习模型。


2、Microsoft Azure Machine Learning:微软提供的一款全托管的机器学习平台,可以帮助用户构建、训练和部署机器学习模型。


3、IBM Watson Studio:IBM提供的一款机器学习工具和服务平台,可以帮助用户构建、训练和部署机器学习模型。此外它还提供了数据可视化、模型解释和协作等功能。


它们与Amazon SageMaker 类似,具有全托管和自动化的特点,同时提供了多种算法和框架供用户选择。但尺有所长、寸有所短,咱们不妨用表格来一眼对比它们各自的优缺点:



可以看到,Amazon SageMaker 的配置更简单;


至于谈到:“需掌握 AWS 服务”,其实它也好上手,类比于阿里云,AWS 是亚马逊云服务,全球顶流、百万客户、加速敏捷,即使不用来开发机器模型,也建议体验、上手其它云服务产品~具体参见aws.amazon.com




活动福利环节


刚好,最近正在进行“上云探索实验室”活动;6月27-28日亚马逊云科技中国峰会上海现场,展区5楼【开发者会客厅】有互动活动:


现场为开发者提供了部分 Amazon codewhisperer 账号,开发者可以到现场直接进行代码体验,参与体验问卷回复可获得社区定制周边礼品一份。


同时开发者也可于现场进行专业版账号注册,完成注册即可获得定制周边礼品一份或免费 Serverlesspresso 咖啡券一张。


欢迎大家注册~~


话不多少,先冲为敬~




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

什么?同一张图在Flutter和iOS原生渲染出来效果不一样?

iOS
最近遇到了一个很奇怪的现象,同样的一张图在iOS原生能正常显示,但是在Flutter就显得很暗 具体往下看 现象 可以很清晰的看到iOS对于原图的还原度要比Flutter好太多 思考 看到这种场景顿时懵逼,明明是一张图,不至于Flutter这么拉胯吧,但是这...
继续阅读 »

最近遇到了一个很奇怪的现象,同样的一张图在iOS原生能正常显示,但是在Flutter就显得很暗
具体往下看


现象


截屏2023-08-18 16.49.37.png


可以很清晰的看到iOS对于原图的还原度要比Flutter好太多


思考


看到这种场景顿时懵逼,明明是一张图,不至于Flutter这么拉胯吧,但是这个现象是做了这么久的Flutter第一次遇见(或者是之前没太仔细看小的icon,这次大图渲染更加直观。。),所以觉得这个事情或许没那么复杂。于是开始怀疑是Impeller的问题,但是我尝试了@恋猫de小郭的文章里的
在iOS工程中修改info.plist


<key>FLTEnableImpeller</key>
<true />

尝试关闭Impeller,但是并没有什么效果,所以初步怀疑Impeller的思路或许不太对,所以就开始看之前的在Flutter显示正常的图,最后发现了一个微妙之处,在图片的简介里有一个我不懂的地方不一样,就是颜色描述文件


WechatIMG394.jpg


这里询问chatgpt颜色描述文件:sRGB IEC61966-2.1Display P3有什么区别,得到的回答是



sRGB IEC61966-2.1和Display P3是两种不同的颜色描述文件,它们主要用于不同的显示设备和应用场景。它们的区别主要在于颜色范围和应用领域。



  1. sRGB IEC61966-2.1:sRGB是一种标准的RGB颜色空间,广泛用于互联网、数字摄影和普通的显示设备上。sRGB IEC61966-2.1的颜色范围相对较窄,适合一般图像显示,它的色域较小,能够在大多数常见显示设备上提供相对一致的颜色显示。sRGB通常是图像文件的默认颜色描述文件。

  2. Display P3:Display P3是一种更宽广的颜色空间,偏向于更鲜明和逼真的色彩展示。它主要用于高级的显示设备,如苹果公司的Retina显示器。相对于sRGB,Display P3能够呈现更宽广的颜色范围,对于鲜艳和饱和度较高的颜色效果更为明显。 总结来说,sRGB IEC61966-2.1适合一般的互联网应用和常见显示设备,而Display P3则适用于高级显示设备,如高分辨率显示器和专业图形处理工作。在选择使用哪一种颜色描述文件时,需要考虑图像的应用场景和目标设备的能力来做出合适的选择。



呃。。好像看不出来咋回事,后来看到一篇文章说到Flutter对于Display P3的支持问题,具体意思就是原因就是 Flutter 直接把 Display P3 色域当做 sRGB 色域的图像处理了,而没有做色域转换,这一下就真相大白了~。


解决办法


文章中提到可以让原生来处理图片


CGImageSourceRef src = CGImageSourceCreateWithData((__bridge CFDataRef) imageData, NULL);
NSUInteger frameCount = CGImageSourceGetCount(src);
if (frameCount > 0) {
NSDictionary *options = @{(__bridge NSString *)kCGImageSourceShouldCache : @YES,
(__bridge NSString *)kCGImageSourceShouldCacheImmediately : @NO
};
NSDictionary *props = (NSDictionary *) CFBridgingRelease(CGImageSourceCopyPropertiesAtIndex(src, (size_t) 0, (__bridge CFDictionaryRef)options));
NSString *profileName = [props objectForKey:(NSString *) kCGImagePropertyProfileName];
if ([profileName isEqualToString:@"Display P3"]) {

NSMutableData *data = [NSMutableData data];
CGImageDestinationRef destRef = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)data, kUTTypePNG, 1, NULL);

NSMutableDictionary *properties = [NSMutableDictionary dictionary];
properties[(__bridge NSString *)kCGImageDestinationLossyCompressionQuality] = @(1);
properties[(__bridge NSString *)kCGImageDestinationEmbedThumbnail] = @(0);

properties[(__bridge NSString *)kCGImagePropertyNamedColorSpace] = (__bridge id _Nullable)(kCGColorSpaceSRGB);
properties[(__bridge NSString *)kCGImageDestinationOptimizeColorForSharing] = @(YES);

CGImageDestinationAddImageFromSource(destRef, src, 0, (__bridge CFDictionaryRef)properties);

CGImageDestinationFinalize(destRef);
CFRelease(destRef);
return data;

}
}

return imageData;

这里偷懒了,因为找UI小姐姐让她切图的时候调整一下就可以了~,最后的解决方案是UI根据设计稿导出sRGB IEC61966-2.1类型的图片,同时这个图片的色值是向Display P3

作者:Jerry815
来源:juejin.cn/post/7268539503907307520
code>靠拢的,至此问题解决。

收起阅读 »

新一代前端工具链Rome:革新前端开发

web
在前端开发领域,每时每刻都在涌现着各种新的工具和框架。而 Rome,作为一款新一代前端工具链,引起了广泛的关注和热议。它不仅提供了卓越的性能,还整合了各种强大的功能,使前端开发变得更加高效。本文将深入介绍 Rome,并为你提供一些代码示例,帮助你更好地了解和使...
继续阅读 »

在前端开发领域,每时每刻都在涌现着各种新的工具和框架。而 Rome,作为一款新一代前端工具链,引起了广泛的关注和热议。它不仅提供了卓越的性能,还整合了各种强大的功能,使前端开发变得更加高效。本文将深入介绍 Rome,并为你提供一些代码示例,帮助你更好地了解和使用这个令人激动的工具。


image.png
image.png


Rome 是什么?


Rome 是一个全新的前端工具链,旨在重新定义前端开发体验。它是一个一站式解决方案,涵盖了许多前端开发中常见的问题和任务。Rome 的主要目标是提供一致性和高性能,以加速前端开发流程。它的核心特点包括:


1. 依赖管理


Rome 提供了强大的依赖管理系统,可用于管理你的项目中的依赖关系。它支持 JavaScript、TypeScript 和 Flow,并能够准确地分析和处理依赖项,以确保你的项目始终保持一致。


// 安装依赖
rome deps add react

// 查看依赖树
rome deps list

这个特性非常有用,因为它将所有的依赖关系都纳入统一管理,无需依赖其他工具。


2. 代码格式化


Rome 自带了一个先进的代码格式化工具,可帮助你统一项目中的代码风格。无需争论缩进或分号,Rome 将自动处理这些问题。


# 格式化整个项目
rome format

代码格式化对于团队协作和维护项目非常重要,它可以消除代码风格的争议,使代码更易读。


3. 静态类型检查


Rome 集成了强大的静态类型检查器,可以在编码过程中捕获潜在的类型错误,提高代码质量和可维护性。它支持多种类型系统,包括 TypeScript、Flow 等。


// 检查类型
rome check

这个特性有助于减少运行时错误,提前发现潜在的问题。


4. 构建工具


Rome 提供了一套强大的构建工具,可用于将你的代码编译成浏览器可执行的代码。这有助于优化性能并减小最终部署包的大小。


# 构建项目
rome build

Rome 的构建工具支持多种目标,你可以轻松地将项目构建成不同的输出格式。


5. 包管理


Rome 不仅支持 JavaScript 包管理,还可以处理 CSS、图片、字体等多种资源。这意味着你可以在一个地方管理所有资源,而无需额外的工具。


// 导入 CSS 文件
import './styles.css';

这个特性使得资源管理变得更加一致和方便。


Rome 的安装和配置


现在,让我们来看看如何安装 Rome 并进行基本配置。


步骤 1:安装 Rome


你可以使用 npm 或 yarn 安装 Rome。这里以 npm 为例:


npm install -g rome

安装完成后,你可以在终端中运行 rome -v 来确认 Rome 是否成功安装。


步骤 2:初始化项目


在你的项目目录中,运行以下命令来初始化一个 Rome 项目:


rome init

这将创建一个 .romerc.js 文件,其中包含了


项目的配置信息。


步骤 3:配置选项


你可以编辑 .romerc.js 文件来配置 Rome 的选项,以满足你的项目需求。例如,你可以指定项目的目标环境、依赖管理方式、构建选项等。


// .romerc.js
module.exports = {
target: 'browser',
module: {
type: 'commonjs',
},
build: {
minify: true,
},
};

使用 Rome


一旦你的项目配置好了,就可以开始使用 Rome 提供的工具来进行开发。以下是一些常用的命令和示例:


运行 linter 来检查代码风格和潜在问题。


rome check

运行格式化程序来自动格式化你的代码。


rome format

运行类型检查以确保类型安全。


rome typecheck

构建你的项目。


rome build

运行你的应用程序。


rome run

自定义和插件


Rome 还支持自定义插件和配置。你可以编写自己的插件,或者将现有的插件集成到你的项目中,以满足特定需求。


// .romerc.js
module.exports = {
custom: {
myPlugin: {
enabled: true,
options: {
// 自定义选项
},
},
},
};

在这个示例中,我们启用了一个名为 "myPlugin" 的自定义插件,并提供了一些自定义选项。


Rome发展前景




  1. 性能优化: Rome 的核心目标之一是提供高性能。未来,它将不断进行性能优化,以确保更快的构建和更快的开发体验。随着前端项目变得越来越复杂,性能优化将成为前端开发的重要问题,Rome 在这方面有望发挥重要作用。




  2. 更好的类型检查: Rome 集成了静态类型检查器,帮助开发者在编码阶段捕获潜在的类型错误。未来,这个类型系统可能会进一步增强,提供更多的类型推断和错误检查功能。




  3. 更多的插件和扩展: Rome 的自定义插件和配置功能使得开发者可以根据自己的需求扩展 Rome。随着社区的不断壮大,预计会有更多的插件和扩展出现,为开发者提供更多的选择和解决方案。




  4. 更广泛的应用领域: Rome 目前主要用于前端开发,但未来可能会扩展到其他领域,如后端开发或跨平台开发。这将使 Rome 成为一个更加通用的工具链,适用于各种不同类型的项目。




  5. 更丰富的文档和教程: 随着 Rome 的发展,预计会有更多的文档、教程和学习资源出现,帮助开发者更好地掌握和使用 Rome。这将有助于扩大 Rome 的用户群体。




  6. 更强大的生态系统: Rome 的生态系统将继续扩大,包括各种开发工具、编辑器插件和整合解决方案。这将使 Rome 成为一个完整的开发生态系统,为开发者提供一站式的解决方案。




总之,Rome作为一个新一代前端工具链,充满了潜力,未来有望在前端开发领域取得更多的成功和影响力。开发者可以继续关注 Rome 的发展,利用它来提高前端开发效率和质量。


结语


Rome 是一个前端工具链的新星,它为前端开发者提供了许多强大的功能,以提高开发效率和代码质量。虽然 Rome 还在不断发展和改进中,但它已经展现出了巨大的潜力。无论你是新手还是经验丰富的前端开发者,都值得一试 Rome,看看它如何改变你的前端开发流程。在使用 Rome 时,记得查阅官方文档以获取更多详细信息和示例代码。希望这篇文章能帮助你入门 Ro

作者:侠名风
来源:juejin.cn/post/7269745800178925603
me 并开始在你的项目中使用它。

收起阅读 »