注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Awesome metaverse projects (元宇宙精选资源汇总)

Awesome Metaverse 关于 Metaverse 的精彩项目和信息资源列表。由于关于 Metaverse 是什么存在许多相互竞争的想法,请随时以拉取请求、问题和评论的形式留下反馈。WebXRWebXR Explainer - 什么是 WebXR,有...
继续阅读 »



Awesome Metaverse

关于 Metaverse 的精彩项目和信息资源列表。

由于关于 Metaverse 是什么存在许多相互竞争的想法,请随时以拉取请求、问题和评论的形式留下反馈。

WebXR

社交虚拟现实(Social VR)

开源

非免费

  • Cryptovoxels - 用户拥有的虚拟世界

  • Somnium Space - 基于区块链的持久虚拟世界

  • NeosVR - 旨在加速社交 VR 应用程序开发的引擎

  • VRChat

    - 最大的社交 VR 平台,拥有世界和头像的 UGC

    • Awesome VRChat - 有兴趣为 VRchat 开发内容的人的一站式商店

  • Roblox - 大型在线多人 UGC 游戏平台

  • Omniverse - 3D 生产流水线的实时模拟和协作平台

  • dot bigbang - 基于 Web 的多人 UGC 游戏平台,内置工具和 Typescript 脚本

  • Helios - 基于虚幻引擎的 UGC 世界、头像和游戏平台

  • Meta - The Metaverse 的 Meta(前 Facebook)公告视频

头像提供者

协议和标准

书籍

科幻

  • Neuromancer - (80s) 定义了赛博朋克流派和赛博空间一词

  • Snow Crash - (90 年代) 创造术语 Metaverse 作为互联网的继承者

  • Ready Player One - (2011) 后来成为斯皮尔伯格电影的热门书

  • Ready Player Two - (2020) 准备好的玩家一的续集

  • Rainbows End - (2006) 越来越多的数字/虚拟世界与无处不在的计算

  • Idoru - 虚拟名人和分散的虚拟世界,桥梁三部曲中的第 2 册

非小说

  • 空间网络 - Web 3.0 将如何连接人类、机器和人工智能以改变世界

电影

  • The Matrix 矩阵

  • The Thirteenth Floor 十三楼

  • Existenz 存在

  • Free Guy 自由人

  • Tron 创

  • Wreck it Ralph 2 破坏它拉尔夫 2

  • Ready Player One 准备好球员一

文章和博客

加密

白皮书

链接

翻译文章

元节入门

元节入门

后期 ROAD-MAP

  • 整理相关的资源到当前仓库。

  • 白皮书翻译

作者:houbb
来源:https://github.com/houbb/awesome-metaverse-zh

收起阅读 »

到2030年将存在的10个元宇宙工作

还记得2016年么? 宝可梦GO(Pokémon GO)席卷全球,当时许多人认为我们正站在增强现实技术(AR)革命的风口上。 显然, 这并没有实现。快进到今天,我们再次就Facebook/Meta 疯狂投资为创建一个元宇宙(Metaverse)——人人都生活在...
继续阅读 »



还记得2016年么? 宝可梦GO(Pokémon GO)席卷全球,当时许多人认为我们正站在增强现实技术(AR)革命的风口上。 显然, 这并没有实现。快进到今天,我们再次就Facebook/Meta 疯狂投资为创建一个元宇宙(Metaverse)——人人都生活在其中的完全沉浸式数字世界,展开了类似的谈论。

来自如此多参与者的这种投资往往会创造一个自我实现的预言:无论我们喜欢与否,我们都可能很快就拥有一个运转的元宇宙,只因科技霸主想这样做。

因为我的工作是规划(而非预测)未来的工作,所以我研究了这种可能性并自我发问:“元宇宙将创造什么类型的工作?” 以下是一些初步想法。

*快速定义

  • 虚拟现实 (VR):全人工环境;完全沉浸在虚拟环境中。

  • 增强现实 (AR):虚拟物体与真实世界环境重叠;数字物体增强了真实世界。

  • 混合现实 (MR):虚拟环境融合真实世界;现实世界和虚拟环境皆可交互。

扩展现实 (XR) / 元宇宙:上述一切的混合。


1. 元宇宙研究科学家

AR 和 VR 研究科学家已经是顶尖大学和大型科技公司的主要人才。但在元宇宙(或者您对“物质世界和数字世界的无缝交织”的任意称呼) 慢慢成为一个被广泛接受的观点同时,我们需要更多的智慧

开发一些真实世界的基本数字模型,让企业将能够在这些模型中吸引客户和合作伙伴,元宇宙研究科学家的工作可不仅仅是如此。这已经存在了。未来会有更大的变化。元宇宙研究科学家需要构建的是一种类似于万物理论的东西,其中整个世界都是可见的和可数字化操作。(想象一下没有乐趣的《头号玩家》)。该架构将是所有其他用例构建的基础;游戏、广告、工厂质量控制、互联健康、DeFi……等等。

这是一项极度复杂的任务,元宇宙研究科学家们需要能够使用计算机视觉算法融合的技术构建并缩放原型,用于3D计算摄影神经渲染场景重构计算成像视觉惯性里程计状态估计传感器融合,绘图与定位……这些原型将随着时间推移而变大

  • 如何成为元宇宙研究科学家:获得深度学习、计算机视觉、计算机图形学或计算成像的博士学位。你还需要了解c++,祝你好运。

2. 元宇宙规划师

一步实际行动比一打纲领更重要。一旦我们有了一个运行着的元宇宙,将所有功能规划和实施到一个完全虚拟的世界中的能力绝对是大多数公司的关键。在这个不断扩张的数字世界中选择正确的事情亦是如此。

这就是元宇宙规划师的用武之地。随着CEO为创造和增长其公司的元宇宙收入而制定愿景和战略时,规划师们需要推动从概念验证到试点再到部署的战略性机遇组合。这意指识别市场机会、建立商业案例、影响工程路线图、制定关键指标等……

你知道的…有趣的事物

这似乎并不吸引人,但您如何决定汽车公司是应该专注于创建虚拟驾驶测试,还是应实施一个数字孪生业务来预测故障?我不知道,但是规划师们肯定会给出答案。

  • 如何成为元宇宙规划师:拥有多年的管理经验,了解硬件/软件/SaaS/PaaS营销和商业模式,以及良好的创业心态。

3. 生态系统开发者

元宇宙不会靠扎克伯格的意志自行实现,需要围绕它建立一个健全的生态系统。传感器、CPU、GPU、KYC流程、数据湖(data-lakes)、绿色电能生产、边缘计算、法律、法规……世界是复杂的,因此进一步数字化(比现在更复杂)并非易事。我们可以将这一困难同目前汽车行业向电动汽车转型所面临的困难对比。电动汽车就在那里,但它们被采用的最大障碍是街道和道路上缺乏广泛分布的充电站,以及电池容量的不断变化。同样,我们可能拥有实现元宇宙的软件和硬件,但是仍然缺乏……其他一切

元宇宙生态系统开发者将负责协调合作伙伴和政府,以确保创建的各种功能能够大规模实现。他们将推动政府对基础建设的投资,并激发大型社区的活力。

元宇宙生态系统开发者们需要关注的一个关键问题是互操作性,以确保元宇宙客户能够在不同的体验中使用他们的虚拟道具。毕竟,如果你不能在商场里也穿着它,那么在迷你游戏中获得炫酷皮肤又有什么意义呢?其他游说努力将面向金融机构,它们需要支持分布式账本技术,以及在平台上交换商品和服务的智能合约。

  • 如何成为生态系统开发者:拥有多年的政务/游说经验以及对蓬勃发展的XR行业的深刻理解。

4. 元宇宙安全经理

您确信互联网对每个人来说都很安全吗? 是的,我也不。任何声称元宇宙会更安全的人都是在自欺欺人。当然,它有很多机会成为一个更安全、包容的地方,但是这不会自己发生。

隐私。虚拟身份认证。安全帽。足够的传感器……我们需要能在设计、验证和量产阶段提供指导和监督的人员,确保我们的数字世界是安全的,满足或超过适用的监管安全要求。显然,这一切都未牺牲尖端功能或设计,或削尖收入。这个人员即是元宇宙安全经理。

这也不是一个简单活。他们需要准确地预测元宇宙功能将如何被使用或者被滥用,并识别与这些预测相关的安全关键组件(safety-critical components)、系统和制造步骤。这十足的复杂性和移动部件的数量光是想想就足以让我头晕目眩。

  • 如何成为XR安全经理:拥有工程学位和消费电子产品/制造经验。

5. 元宇宙硬件制造商

元宇宙不会(仅仅)构建在代码之上,它(也)将构建在传感器、摄像头和耳机上。 这样的传感器会让你感受到被触摸就仿佛有人在网上挤压你的手臂。摄像头可以查看你是否心情不好,这样AI就不会过多打扰你。而耳机则可以感受到你周围的太阳,并在数字世界中投射出夏日,以增加真实感。这甚至还没有涉及到无聊的东西,譬如用以辅助跟踪、绘图和定位的惯性测量单元、视觉光相机、深度相机……

目前,最好的传感器是为工业作业和汽车行业制造的。这些都是拥有大量资本的行业。因此,作为一个额外的挑战,无论谁制造元宇宙的硬件,都需要确保他们足够廉价且安全,以便元宇宙就不会成为富人们的专属玩物。

  • 如何成为传感器制造商:拥有一家能够制造复杂消费电子产品的工厂。嘿,我可从没说过这很容易

6. 元宇宙作家(storyteller)

随着体验经济和游戏化概念的不断发展,我们要求我们的扩展现实体验具有很棒的并可汲取教训的故事情节,这是非常符合逻辑的。我们想笑;我们想哭;我们想要学习;我们想要从数字世界中看到一些稍微稀奇古怪点的东西。这就是元宇宙作家的用武之地.

元宇宙作家将负责为用户设计沉浸式的任务以探索元宇宙,如军事训练方案、以公司叙述的方式进行难以发现的营销机会、心理学会议(为什么杀死内心的恶魔当你可以在数字世界中模拟杀他们)……例子不胜枚举。

他们不会得到很好的报酬,除非你把为他们打call也算作报酬,但你没有。但至少他们会出售能够吸引数百万人来追的故事线,以助其摆脱枯燥的日常生活。这难道不是元宇宙作家们的梦想吗?

  • 如何成为元宇宙作家:主修文学,辅修市场营销,在游戏公司开始你的职业生涯,然后转向科技公司。

7. 世界建造者

在我们建立了我们的架构、我们的硬件和我们的故事情节后,我们仍需要创建整个世界(想想《盗梦空间》中的Ellen Page —或者是Elliot,老实说当时她仍在使用她的弃名)。我的意思不是为世界编码,我是指,想象世界

这个角色需要许多与电子游戏设计师相同的技能,尽管规则可能云泥之别。世界建设者将需要具有前瞻性并迎接未来,因为他们梦想的许多东西还没有以技术或产品解决方案的形式存在。

他们还需要考虑规章和道德。当数字世界变得真实时,在其中杀害可以吗?犯下战争罪行?我们已经自我发问这些问题了,但讨论还离得出结论很远呢。

  • 如何成为世界建造者:半战争诗人,半平面设计师。擅长我的世界(Minecraft)也没有什么坏处。

8. 广告拦截专家

Facebook…噢不好意思,是Meta,它如何赚钱?通过向它的虚假信息工厂出售订阅?通过摘取和出售器官?通过接受独裁者的捐款?当然不是(?)。他们卖广告。让我告诉你,元宇宙可能会以非常相似的方式运行。我想我们可以称之为DNA。你认为Instagram的广告很具有针对性且很烦人吗?那是因为你还没有看到他们可以用整个数据集做什么事,并且能够真正地在无处不在地关注到你。

想象一下,你在一个数字空间中走来走去,此时在现实世界的你很饿。不知不觉中,你盯着沿途的数字咖啡厅和餐馆看的时间长了很久。然后您猜怎么着,一分钟后,你开始收到食物的广告。 一开始听起来挺有趣的,但是从长远角度来看,这很干扰。因此,一旦我们丧失了新鲜感,我们就会希望广告拦截软件足够先进以发现嵌入现实中的广告,这就是广告拦截专家发挥作用的地方。

很像AdBlock Plus模式,我猜他们会开发插件来阻止广告弹出。他们不会得到太多报酬,但通过捐赠和获取数据,他们或许能维持生计。

  • 如何成为广告拦截专家:拥有基础编程知识,并能访问元宇宙的源代码。

9. 元宇宙网络安全

元宇宙是网络攻击和诈骗的完美目标:被黑的虚拟形象(avatars)、NFT 盗窃、生物识别/生理数据泄露(脑电波模式,有人在吗?),被黑的耳机……出现问题的可能几乎是无数的。

这就是我们需要元宇宙网络安全专家的原因。他们将实时阻止攻击,并确保法律和协议被重新考虑和修改,甚至再创造法律和协议,以囊括元宇宙所有可能的风险。

笔者对网络安全不甚了解,所以在这一段给出开放式的想法:虚拟世界违规成为现实世界的法案只是时间问题

  • 如何成为元宇宙网络安全专家:具有常规网络安全知识和/或具有技术倾向的法学学位。

10. 无薪实习生

笔者知道,为了得到一个漂亮整数而凑数,有点不光彩。但是请听我说完,虽然这个角色已经存在,但需要强调的是,它将是元宇宙未来的核心。

无薪实习生不仅能喝咖啡,他们研磨数据;他们制作风险投资的附录(VC’s decks’ appendices);他们为岩石和树木写代码;他们是科技帝国赖以建立的必要素材。

我们应该向他们的牺牲致敬。

而且,我们应该付钱给他们。


安全、舒适、引人入胜的叙事曲线、可定制的发型、家长控制、智力游戏、附加组件、小部件、通知、优化、意识形态一致性、娱乐和惩罚门户以及发出低沉隆隆声的传感器(beta版)……我们将在元宇宙中拥有一切!

但这不是一蹴而就的,元宇宙将需要无数新技术、协议、公司、创新和发现才能运作。并且,根据定义,它需要无数人从事不可量计的工作才能实现其宏伟目标。

如果您有幸在不久的将来得到上述之一的工作,只要记住一件事:: 世界是异常黯淡的,请你不要将其黑暗带入元宇宙


作者:用户8309268629399
来源:https://juejin.cn/post/7032552320483524645

收起阅读 »

如果将元宇宙逐层拆解,你会发现内核是“云”

2021 最火的新概念,莫过于元宇宙。2021 年 10 月 29 日,Facebook 宣布改名 Meta;2021 年 11 月 1 日,“元宇宙第一股” Roblox 经过短暂调整,宣布重新上线。接下来关于元宇宙的线下 / 线上讨论如火如荼,元宇宙概...
继续阅读 »

2021 最火的新概念,莫过于元宇宙。2021 年 10 月 29 日,Facebook 宣布改名 Meta;2021 年 11 月 1 日,“元宇宙第一股” Roblox 经过短暂调整,宣布重新上线。接下来关于元宇宙的线下 / 线上讨论如火如荼,元宇宙概念的热度可见一斑。

逐层拆解元宇宙

清华大学新闻学院教授、博士生导师沈阳教授在一场活动中分享道,元宇宙,英文是 Metaverse,从字面来理解,由 Meta(超越) 和 Universe(宇宙) 两部分组成。而沈阳教授团队也给元宇宙下了一个相对精确的定义:

元宇宙是整合多种新技术而产生的新型虚实相融的互联网应用和社会形态,它基于扩展现实技术提供沉浸式体验,基于数字孪生技术生成现实世界的镜像,基于区块链技术搭建经济体系,将虚拟世界与现实世界在经济系统、社交系统、身份系统上密切融合,并且允许每个用户进行内容生产和世界编辑。

Beamable 公司创始人 Jon Radoff 则在产业层面对元宇宙的概念做了拆解:“元宇宙构造的七个层面:体验;发现;创作者经济;空间计算;去中心化;人机互动;基础设施。”

体验层面相对最容易理解,目前我们常见到的游戏、社交等领域企业,都是在体验层面开展工作。著名游戏 《Second Life》 尤为经典。在这个游戏里,用户叫做"居民",可以通过可运动的虚拟化身互相交互。这套程序还在一个通常的元宇宙的基础上提供了一个高层次的社交网络服务。居民们可以四处逛逛,会碰到其他的居民,社交,参加个人或集体活动,制造和相互交易虚拟财产和服务。而典型《头号玩家》则是人们对于元宇宙在体验层面的自由畅想。

发现层面是用户了解到体验层的重要途径,其中包括各种应用商店,主要参与者是大型互联网公司;

创作者经济层 (Creator Economy): 帮助元宇宙创作者的成果货币化,其中包括设计工具 、 货币化技术、动画系统、图形工具等;

空间计算层 (Spital Computing): 对创作者经济层的赋能,具体包括 3D 引擎、手势识别、空 间映射和人工智能等,主要参与者是 3D 软硬件厂商;

去中心化层 (Decentralization): 这个层面的公司主要是帮助元宇宙生态系统构建分布式架 构,从而形成民主化结构;

人机交互层 (Human Interface): 人机交互层主要是大众接触元宇宙的媒介工具,主要体现在 触觉、姿势、声音、神经等层面,其中产品包括 AR/VR、手机、电脑、汽车、智能眼镜等可穿戴设备,主要参与者是 3D 软硬件厂商 ;

基础设施层 (Infrastructure):5G、半导体芯片、新型材料、云计算和电信网络等。基础设施层大概率是巨头之间的游戏,大部分是基础硬件公司。

可以说,元宇宙是整个人类经济体未来需求的一个集中出口,包含了用户对新体验的渴望,资本对新出口的渴望,技术对新领域的渴望,它是科技发展到一定阶段的必然新构想。即便 2021 没有出现“元宇宙”,可能也会出现“元世界”、“元矩阵”等其他概念。

元宇宙的关键支撑技术

以上关于元宇宙概念和产业分层方面的定义,是最近被很多人所熟知的概念,但这仍然没有解释元宇宙的实现路径,说到底,我们最想搞清楚的是,究竟该如何实现梦想中的元宇宙。

从技术维度来看,元宇宙的各部分关键支撑可以简称为:“HNCBD”,分别是硬件体验设备 (Hardware)、网络与算力 (Networking and Computing)、内容及应用生态 (Content)、区块链和 NFT(Blockchain),数字孪生(Digital Twin)。当然,这些核心技术在不同人眼里可能有细微区别,但总体相差不大。

在“HNCBD”中,H 属于硬件,不在软件开发者的常规讨论范围内;C 依赖百花齐放的应用社区;而网络与算力、区块链和 NFT、数字孪生,其实都存在一个统一的承载形式,就是云计算。

明眼人早已看出,如果排除因商业竞争而重复造轮子的问题,其实实现元宇宙最好的通路就是云。某种意义上讲,云不光承载的是元宇宙对于算力和基础设施的空前庞大的需求,更是各类在基础设施之上的 PaaS 、SaaS 服务。在元宇宙的发展过程中,如果每一家应用提供商、内容提供商,都要重构基础设施,包括基础的数据湖仓服务、数字孪生服务、机器学习服务,那成本将是不可想象的。

而当下阶段的云计算,除了提供基础的算力支撑,最关键的就是在游戏、AI 算法及 VR 三个方向上,提供了足够成熟的技术产品,其中最具代表性的就是亚马逊云科技。

回顾《头号玩家》的电影画面,演员们戴上眼镜,即进入了游戏世界,这其实是典型的云游戏场景。

目前大型游戏采用服务器 + 客户端的实现模式,对客户端硬件要求比较高,尤其是 3D 图形的渲染,基本完全依赖于终端运算。随着 5G 时代的到来,游戏将会在云端 GPU 上完成大规模渲染,游戏画面压缩后通过 5G 高速网络传送给用户。

在客户端,用户的游戏设备不需要任何高端处理器和显卡,只需要基本的视频解压能力。从游戏开发角度来看,游戏平台可以更加快速地部署新游戏功能,减少启动游戏所需的构建和测试工作量,满足玩家需求。

2020 年 9 月,亚马逊云科技就推出了自己的云游戏平台 Luna,兼容 PC、Mac、Fire TV、iPad 和 iPhone 和 Android 系统,知名游戏和平台厂商 Epic Games 也在利用 Amazon EC2 等 亚马逊云科技 服务及时扩展容量并支持远程创建者。Amazon G4 实例就是通过 GPU 来驱动云游戏渲染,通过 NVIDIA Video Codec SDK 传输最复杂的云游戏。Amazon G4 实例所搭载的 NVIDIA T4 GPU,也是云上第一款提供了 RT 核心、支持 NVIDIA RTX 实时光线追踪的 GPU 实例。

而元宇宙的体验又不仅限于云游戏,云游戏只是场景,VR 才是路径。

传统 VR 应用的局限性主要体现在四个方面,其中包括:购置主机和终端硬件成本高、设备使用率低、内容分散、移动性受限。

云计算和 VR 的结合,可以将 GPU 渲染功能从本地迁移到云端,从而使得终端的设计变得更加轻便与高性价比,降低了用户购买硬件设备的成本。VR 开发者可以 在云上进行快速的内容迭代发布,用户即点即玩、无需下载,解决内容不集中问题。

以 Amazon Sumerian 为例,开发者可以轻松创建 3D 场景并将其嵌入到现有网页中。Amazon Sumerian 编辑器则提供了现成的场景模板和 直观的拖放工具,使内容创建者、设计师和开发人员都可以构建交互式场景。Amazon Sumerian 采用最新的 WebGL 和 WebXR 标准,可直接在 Web 浏览器中创建沉浸式体验,并可通过简单的 URL 在几秒钟内进行访 问,同时能够在适用于 AR/VR 的主要硬件平台上运行。

除了云游戏和 VR,元宇宙的实现还有一个关键变量,就是 AI。AI 可以缩短数字创作时间,为元宇宙提供底层支持,主要体现在计算机视觉、智能语音语义、机器学习。三者都需要巨大的算力和存储,云计算为人工智能提供了无限的算力和存储支持。有一家叫做 GE Healthcare 的公司,就是使用 Amazon P4d 实例,将定制化 AI 模型处理时间从几天缩短为几小时,使训练模型的速度提高了两三倍,从而提供各类远程医疗、诊断服务。

AI 在虚拟形象上的价值更明显,亚马逊云科技的 AI 服务在此领域有很多的应用实践包括图像 AI 生成(自动上色、场景调整、图像二次元化)、模型自动生成(动画自动生成、场景道具生成)、游戏机器人(游戏 AI NPC、文本交互、语音驱动口型动画、动作补抓、表情迁移)、偶像营销运营(聊天观察、流行搭配、反外挂)等。

云计算企业在元宇宙领域的核心工作

如果说,以上拆解更多还属于理论分析,那么,如果我们仔细看看头部云计算企业的近期动态,就会发现关于元宇宙的种种技术支撑正在云端成为现实。

2021 亚马逊云科技 re:Invent,亚马逊云科技发布了 Amazon IoT TwinMaker 与 Amazon Private 5G。

前者让开发人员可以轻松汇集来自多个来源(如设备传感器、摄像机和业务应用程序)的数据,并将这些数据结合起来创建一个知识图谱,对现实世界环境进行建模,是实现工业元宇宙的组成技术之一。

后者则可自动设置和部署企业专有 5G 网络,并按需扩展容量以支持更多设备和网络流量,重点服务了以工业 4.0 为主的庞大传感器和端侧设备集群,前文提到的工业元宇宙、车联网自然也在同一序列。

更不用说 Amazon SageMaker Canvas,用无代码理念构建机器学习模型,做模型预测,保证在脱离数据工程团队的情况下,依然可以提供服务,进一步降低了未来元宇宙内容生产的门槛,保证了内容的多样性。

同样在 2021 亚马云科技 re:Invent 全球大会期间,元宇宙公司 Meta 宣布深化与亚马逊云科技的合作,将亚马逊云科技作为其战略云服务提供商。

据介绍,Meta 使用亚马逊云科技可靠的基础设施和全面的功能,补充其现有的本地基础设施,并将使用更多亚马逊云科技的计算、存储、数据库和安全服务,获得云端更好的隐私保护、可靠性和扩展性,包括将在亚马逊云科技上运行第三方合作应用,并使用云服务支持其收购的已经在使用亚马逊云科技的企业。

Meta 还将使用亚马逊云科技的计算服务来加速 Meta AI 部门人工智能项目的研发工作。另外,亚马逊云科技和 Meta 双方还将合作帮助客户提高在亚马逊云科技上运行深度学习计算框架 PyTorch 的性能,并助力开发人员加速构建、训练、部署和运行人工智能和机器学习模型的机制。

亚马逊全球副总裁、亚马逊云科技大中华区执行董事张文翊认为,这是云计算可以大量赋能的一个领域。她表示:“我们认为元宇宙一定是云计算可以大量赋能的一个领域。元宇宙本身需要的就是计算、存储、机器学习等,这些都离不开云计算。”

未来仍在描绘中

未来元宇宙的技术栈是否会扩展,元宇宙的呈现形式是否会出现大幅变化?

答案几乎是肯定的,就像在 4G 手机普及以前,我们完全无法想象 4G 生态下主要的应用类型。从建设到成熟,仅在算力层面,元宇宙也至少还有十余年时间的路程要走。

但万变不离其宗,关注云计算领域关于元宇宙支撑技术的更新迭代,可能是我们抛开泡沫,观察元宇宙生态进展的重要方法。


作者:SegmentFault思否
来源:https://juejin.cn/post/7041125661465346062

收起阅读 »

前端 4 种渲染技术的计算机理论基础

前端可用的渲染技术有 html + css、canvas、svg、webgl,我们会综合运用这些技术来绘制页面。有没有想过这些技术有什么区别和联系,它们和图形学有什么关系呢?本文我们就来谈一下网页渲染技术的计算机理论基础。渲染的理论基础人眼的视网膜有视觉暂留机...
继续阅读 »

前端可用的渲染技术有 html + css、canvas、svg、webgl,我们会综合运用这些技术来绘制页面。有没有想过这些技术有什么区别和联系,它们和图形学有什么关系呢?

本文我们就来谈一下网页渲染技术的计算机理论基础。

渲染的理论基础

人眼的视网膜有视觉暂留机制,也就是看到的图像会继续保留 0.1s 左右,图形界面就是根据这个原理来设计的一帧一帧刷新的机制,要保证 1s 至少要渲染 10 帧,这样人眼看到画面才是连续的。

每帧显示的都是图像,它是由像素组成的,是显示的基本单位。不同显示器实现像素的原理不同。

我们要绘制的目标是矩形、圆形、椭圆、曲线等各种图形,绘制完之后要把它们转成图像。图形的绘制有一系列的理论,比如贝塞尔曲线是画曲线的理论。图形转图像的过程叫做光栅化。这些图形的绘制和光栅化的过程,都是图形学研究的内容。

图形可能做缩放、平移、旋转等变化,这些是通过矩阵计算来实现的,也是图形学的内容。

除了 2D 的图形外,还要绘制 3D 的图形。3D 的原理是把一个个三维坐标的顶点连起来,构成一个一个三角形,这是造型的过程。之后再把每一个三角形的面贴上图,叫做纹理。这样组成的就是一个 3D 图形,也叫 3D 模型。

3D 图形也同样需要经历光栅化变成二维的图像,然后显示出来。这种三维图形的光栅化需要找一个角度去观察,就像拍照一样,所以一般把这个概念叫做相机。

同时,为了 3D 图形更真实,还引入了光线的概念,也就是一束光照过来,3D 图形的每个面都会有什么变化,怎么反射等。不同材质的物体反射的方式不同,比如漫反射、镜面反射等,也就有不同的计算公式。一束光会照射到一些物体,到物体的反射,这个过程需要一系列跟踪的计算,叫做光线追踪技术。

我们也能感受出来,3D 图形的计算量比 2D 图形大太多了,用 CPU 计算很可能达不到 1s 大于 10 帧,所以后面出现了专门用于 3D 渲染加速的硬件,叫做 GPU。它是专门用于这种并行计算的,可以批量计算一堆顶点、一堆三角形、一堆像素的光栅化,这个渲染流程叫做渲染管线。

现在的渲染管线都是可编程的,也就是可以控制顶点的位置,每个三角形的着色,这两种分别叫做顶点着色器(shader)、片元着色器。

总之,2D 或 3D 的图形经过绘制和光栅化就变成了一帧帧的图像显示出来。

变成图像之后其实还可以做一些图像处理,比如灰度、反色、高斯模糊等各种滤镜的实现。

所以,前端的渲染技术的理论基础是计算机图形学 + 图像处理。

不同的渲染技术的区别和联系

具体到前前端的渲染技术来说,html+css、svg、canvas、webgl 都是用于图形和图像渲染的技术,但是它们各有侧重:

html + css

html + css 是用于图文布局的,也就是计算文字、图片、视频等的显示位置。它提供了很多计算规则,比如流式布局很适合做图文排版,弹性布局易于做自适应的布局等。但是它不适合做更灵活的图形绘制,这时就要用其他几种技术了。

canvas

canvas 是给定一块画布区域,在不同的位置画图形和图像,它没有布局规则,所以很灵活,常用来做可视化或者游戏的开发。但是 canvas 并不会保留绘制的图形的信息,生成的图像只能显示在固定的区域,当显示区域变大的时候,它不能跟随一起放缩,就会失真,如果有放缩不失真的需求就要用其他渲染技术了。

svg

svg 会在内存中保留绘制的图形的信息,显示区域变化后会重新计算,是一个矢量图,常用于 icon、字体等的绘制。

webgl

上面的 3 种技术都是用于 2D 的图形图像的绘制,如果想绘制 3D 的内容,就要用 webgl 了。它提供了绘制 3D 图形的 api,比如通过顶点构成 3D 的模型,给每一个面贴图,设置光源,然后光栅化成图像等的 api。它常用于通过 3D 内容增强网站的交互效果,3D 的可视化,3D 游戏等,再就是虚拟现实中的 3D 交互。

所以,虽然前端渲染技术的底层原理都是图形学 + 图像处理,但上层提供的 4 种渲染技术各有侧重点。

不过,它们还是有很多相同的地方的:

  • 位置、大小等的变化都是通过矩阵的计算

  • 都要经过图形转图像,也就是光栅化的过程

  • 都支持对图像做进一步处理,比如各种滤镜

  • html + css 渲染会分不同图层分别做计算,canvas 也会根据计算量分成不同的 canvas 来做计算

因为他们底层的图形学原理还是一致的。

除此以外,3D 内容,也就是 webgl 的内容会通过 GPU 来计算,但 css 其实也可以通过 GPU 计算,这叫做 css 的硬件加速,有四个属性可以触发硬件加速:transform、opacity、filter、will-change。(更多的 GPU 和 css 硬件加速的内容可以看这篇文章:这一次,彻底搞懂 GPU 和 css 硬件加速

编译原理的应用

除了图形学和图像技术外,html+css 还用到了编译技术。因为 html、css 是一种 DSL( domin specific language,领域特定语言),也就是专门为界面描述所设计的语言。用 html 来表达 dom 结构,用 css 来给 dom 添加样式都只需要很少的代码,然后运行时解析 html 和 css 来创建 dom、添加样式。

DSL 可以让特定领域的逻辑更容易表达,前端领域还有一些其他技术也用到了 DSL,比如 graphql。

总结

因为人眼的视觉暂留机制,只要每帧绘制不超过 0.1s,人看到的画面就是连续的,这是显示的原理。每帧的绘制要经过图形绘制和图形转图像的光栅化过程,2D 和 3D 图形分别有不同的绘制和光栅化的算法。此外,转成图像之后还可以做进一步的图像处理。

前端领域的四种渲染技术:html+css、canvas、svg、webgl 各有侧重点,分别用于不同内容的渲染:

  • html+ css 用于布局

  • canvas 用于灵活的图形图像渲染

  • svg 用于矢量图渲染

  • webgl 用于 3D 图形的渲染

但他们的理论基础都是计算机图形学 + 图像处理。(而且,html+css 为了方便逻辑的表达,还设计了 DSL,这用到了编译技术)

这四种渲染技术看似差别很大,但在理论基础层面,很多东西都是一样的。这也是为什么我们要去学计算机基础,因为它可以让我们对技术有一个更深入的更本质的理解。


作者:zxg_神说要有光
来源:https://juejin.cn/post/7041157165024804895

收起阅读 »

如何用 docker 打造前端开发环境

用 docker 做开发环境的好处 保持本机清爽 做开发的都知道,电脑一买回来就要安装各种各样的环境,比如前端开发需要安装 node、yarn、git 等,为了使用某些工具或者包,可能还需要安装 python 或者 java 等(比如 jenkins 就依赖了...
继续阅读 »

用 docker 做开发环境的好处


保持本机清爽


做开发的都知道,电脑一买回来就要安装各种各样的环境,比如前端开发需要安装 node、yarn、git 等,为了使用某些工具或者包,可能还需要安装 python 或者 java 等(比如 jenkins 就依赖了 java),久而久之,本机环境会非常乱,对于一些强迫证患者或者有软件洁癖的人来说多少有点不爽。


使用 docker 后,开发环境都配置在容器中,开发时只需要打开 docker,开发完后关闭 docker,本机不用再安装乱七八糟的环境,非常清爽。


隔离环境


不知道大家在开发时有没有遇到这种情况:公司某些项目需要在较新的 node 版本上运行(比如 vite,需要在 node12 或以上),某些老的项目需要在较老的 node 版本上运行,切换起来比较麻烦,虽然可以用 nvm 来解决,但使用 docker 可以更方便的解决此问题。


快速配置环境


买了新电脑,或者重装了系统,又或者换了新的工作环境,第一件事就是配置开发环境。下载 node、git,然后安装一些 npm 的全局包,然后下载 vscode,配置 vscode,下载插件等等……


使用 docker 后,只需从 docker hub 中拉取事先打包好的开发环境镜像,就可以愉快的进行开发了。


安装 docker


到 docker 官网(http://www.docker.com)下载 docker desktop 并安装,此步比较简单,省略。


安装完成,打开 docker,待其完全启动后,打开 shell 输入:


docker images

截屏2021-09-29 22.16.13.png


显示上述信息即成功!


配置开发环境


假设有一个项目,它必须要运行在 8.14.0 版本的 node 中,我们先去 docker hub 中将这个版本的 node 镜像拉取下来:


docker pull node:8.14.0

拉取完成后,列出镜像列表:


docker images

截屏2021-09-29 23.30.16.png


有了镜像后,就可以使用镜像启动一个容器:


docker run -it --name my_container 3b7ecd51 /bin/bash

上面的命令表示以命令行交互的模式启动一个容器,并将容器的名称指定为 my_container。


截屏2021-10-08 23.16.11.png


此时已经新建并进入到容器,容器就是一个 linux 系统,可以使用 linux 命令,我们尝试输入一些命令:


截屏2021-10-08 23.18.11.png


可以看到这个 node 镜像除了预装了 node 8.14.0,还预装了 git 2.11.0。



镜像和容器的的关系:镜像只预装了最基本的环境,比如上面的 node:8.14.0 镜像可以看成是预装了 node 8.14.0 的 linux 系统,而容器是基于镜像克隆出来的另一个 linux 系统,可以在这个系统中安装其它环境比如 java、python 等,一个镜像可以建立多个容器,每个容器环境都是相互隔离的,互不影响(比如在容器 A 中安装了 java,容器 B 是没有的)。



使用命令行操作项目并不方便,所以我们先退出命令行模式,使用 exit 退出:


截屏2021-10-08 23.20.49.png


借助 IDE 可以更方便的玩 docker,这里我们选择 vscode,打开 vscode,安装 Remote - Containers 扩展,这个扩展可以让我们更方便的管理容器:


截屏2021-10-08 23.03.53.png


安装成功后,左下角会多了一个图标,点击:


截屏2021-10-08 23.23.00.png


在展开菜单中选择“Attach to Running Container”:


截屏2021-10-08 23.25.02.png


此时会报一个错“There are no running containers to attach to.”,因为我们刚刚退出了命令行交互模式,所以现在容器是处理停止状态的,我们可以使用以下命令来查看正在运行的容器:


docker ps

# 或者
docker container ls

截屏2021-10-08 23.30.51.png


发现列表中并没有正在运行的容器,我们需要找到刚刚创建的容器并将其运行起来,先显示所有容器列表:


# -a 可以显示所有容器,包括未运行的
docker ps -a

# 或者
docker container ls -a

截屏2021-10-08 23.29.25.png


运行指定容器:


# 使用容器名称
docker start my_container

# 或者使用容器 id,id 只需输入前几位,docker 会自动识别
docker start 8ceb4

再次运行 docker ps 命令后,就可以看到已运行的容器了。然后回到 vscode,再次选择"Attach to Running Container",就会出现正在运行的容器列表:


截屏2021-10-08 23.36.14.png


选择容器进入,添加一个 bash 终端,就可以进入我们刚刚的命令行模式:


截屏2021-10-08 23.40.14.png


我们安装 vue-cli,并在 /home 目录下创建一个项目:


# 安装 vue-cli
npm install -g @vue/cli

# 进入到 home 目录
cd /home

# 创建 vue 项目
vue create demo

在 vscode 中打开目录,发现打开的不再是本机的目录,而是容器中的目录,找到我们刚刚创建的 /home/demo 打开:


截屏2021-10-09 00.01.13.png


输入 npm run serve,就可以愉快的进行开发啦:


截屏2021-10-09 00.03.32.png


上面我们以 node 8.14.0 镜像为例创建了一个开发环境,如果想使用新版的 node 也是一样的,只需要将指定版本的 node 镜像 pull 下来,然后使用这个镜像创建一个容器,并在容器中创建项目或者从 git 仓库中拉取项目进行开发,这样就有了两个不同版本的 node 开发环境,并且可以同时进行开发。


使用 ubuntu 配置开发环境


上面这种方式使用起来其实并不方便,因为 node 镜像只安装了 node 和 git,有时我们希望镜像可以内置更多功能(比如预装 nrm、vue-cli 等 npm 全局包,或者预装好 vscode 的扩展等),这样用镜像新建的容器也包含这些功能,不需要每个容器都要安装一次。


我们可以使用 ubuntu 作为基础自由配置开发环境,首先获取 ubuntu 镜像:


# 不输入版本号,默认获取 latest 即最新版
docker pull ubuntu

新建一个容器:


docker run -itd --name fed 597ce /bin/bash

这里的 -itd 其实是 -i -t -d 的合写,-d 是在后台中运行容器,相当于新建时一并启动容器,这样就不用使用 docker start 命令了。后面我们直接用 vscode 操作容器,所以也不需要使用命令行模式了。


我们将容器命名为 fed(表示 front end development),建议容器的名称简短一些,方便输入。


截屏2021-10-10 00.11.40.png


ubuntu 镜像非常纯净(只有 72m),只具备最基本的能力,为了后续方便使用,我们需要更新一下系统,更新前为了速度快一点,先换到阿里的源,用 vscode 打开 fed 容器,然后打开 /etc/apt/sources.list 文件,将内容改为:


deb http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse

截屏2021-10-10 00.18.57.png


在下面的终端中依次输入以下命令更新系统:


apt-get update
apt-get upgrade

安装 sudo:


apt-get install sudo

安装 git:


apt-get install git

安装 wget(wget 是一个下载工具,我们需要用它来下载软件包,当然也可以选择 axel,看个人喜好):


apt-get install wget

为了方便管理项目与软件包,我们在 /home 目录中创建两个文件夹(projects 与 packages),projects 用于存放项目,packages 用于存放软件包:


cd /home
mkdir projects
mkdir packages

由于 ubuntu 源中的 node 版本比较旧,所以从官网中下载最新版,使用 wget 下载 node 软件包:


# 将 node 放到 /home/packages 中
cd /home/packages

# 需要下载其它版本修改版本号即可
wget https://nodejs.org/dist/v14.18.0/node-v14.18.0-linux-x64.tar

解压文件:


tar -xvf node-v14.18.0-linux-x64.tar

# 删除安装包
rm node-v14.18.0-linux-x64.tar

# 改个名字,方便以后切换 node 版本
mv node-v14.18.0-linux-x64 node

配置 node 环境变量:


# 修改 profile 文件
echo "export PATH=/home/packages/node/bin:$PATH" >> /etc/profile

# 编译 profile 文件,使其生效
source /etc/profile

# 修改 ~.bashrc,系统启动时编译 profile
echo "source /etc/profile" >> ~/.bashrc

# 之后就可以使用 node 和 npm 命令了
node -v
npm -v

安装 nrm,并切换到 taobao 源:


npm install nrm -g
nrm use taobao

安装一些 vscode 扩展,比如 eslint、vetur 等,扩展是安装在容器中的,在容器中会保留一份配置文件,到时打包镜像会一并打包进去。当我们关闭容器后再打开 vscode,可以发现本机的 vscode 中并没有安装这些扩展。


至此一个简单的前端开发环境已经配置完毕,可以根据自己的喜好自行添加一些包,比如 yarn、nginx、vim 等。


打包镜像


上面我们通过 ubuntu 配置了一个简单的开发环境,为了复用这个环境,我们需要将其打包成镜像并推送到 docker hub 中。


第一步:先到 docker 中注册账号。


第二步:打开 shell,登录 docker。


截屏2021-10-10 01.44.26.png


第三步:将容器打包成镜像。


# commit [容器名称] [镜像名称]
docker container commit fed fed

第四步:为镜像打 tag,因为镜像推送到 docker hub 中,要用 tag 来区分版本,这里我们先设置为 latest。tag 名称加上了用户名做命名空间,防止与 docker hub 上的镜像冲突。


docker tag fed huangzhaoping/fed:latest

第五步:将 tag 推送至 docker hub。


docker push huangzhaoping/fed:latest

第六步:将本地所有关于 fed 的镜像和容器删除,然后从 docker hub 中拉取刚刚推送的镜像:


# 拉取
docker pull huangzhaoping/fed

# 创建容器
docker run -itd --name fed huangzhaoping/fed /bin/bash

用 vscode 打开容器,打开命令行,输入:


node -v
npm -v
nrm -V
git --version

然后再看看 vscode 扩展,可以发现扩展都已经安装好了。


如果要切换 node 版本,只需要下载指定版本的 node,解压替换掉 /home/packages/node 即可。


至此一个 docker 开发环境的镜像就配置完毕,可以在不同电脑,不同系统中共享这个镜像,以达到快速配置开发环境的目的。


注意事项



  • 如果要将镜像推送到 docker hub,不要将重要的信息保存到镜像中,因为镜像是共享的,避免重要信息泄露。

  • 千万不要在容器中存任何重要的文件或信息,因为容器一旦误删这些文件也就没了。

  • 如果在容器中开发项目,记得每天提交代码到远程仓库,避免容器误删后代码丢失。

作者:Rise_
链接:https://juejin.cn/post/7017129520649994253

收起阅读 »

【手写代码】面试官:请你手写防抖和节流

一、前言当用户高频触发某一事件时,如窗口的resize、scroll,输入框内容校验等,此时这些事件调用函数的频率如果没有限制,可能会导致响应跟不上触发,出现页面卡顿,假死现象。此时,我们可以采用 防抖(debounce) 和 节...
继续阅读 »

一、前言

当用户高频触发某一事件时,如窗口的resize、scroll,输入框内容校验等,此时这些事件调用函数的频率如果没有限制,可能会导致响应跟不上触发,出现页面卡顿,假死现象。此时,我们可以采用 防抖(debounce) 和 节流(throttle) 的方式来减少调用频率,同时又不影响实际效果。

二、防抖

假设你用手压住一个弹簧,那么弹簧不会弹起来,除非你松手。

函数防抖,就是指触发事件后,函数在 n 秒后只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数的执行时间。

简单的说,当一个函数连续触发,只执行最后一次。

函数防抖一般用在什么情况之下呢?一般用在,连续的事件只需触发一次回调的场合。具体有:

  1. 搜索框搜索输入。只需用户最后一次输入完,再发送请求;
  2. 用户名、手机号、邮箱输入验证;
  3. 浏览器窗口大小改变后,只需窗口调整完后,再执行resize事件中的代码,防止重复渲染。

代码实现

在下面这段代码中,我们实现了最简单的一个防抖函数,我们设置一个定时器,你重复调用一次函数,我们就清除定时器,重新定时,直到在设定的时间段内没有重复调用函数。

// fn是你要调用的函数,delay是防抖的时间
function debounce(fn, delay) {
// timer是一个定时器
let timer = null;
// 返回一个闭包函数,用闭包保存timer确保其不会销毁,重复点击会清理上一次的定时器
return function () {
// 调用一次就清除上一次的定时器
clearTimeout(timer);
// 开启这一次的定时器
timer = setTimeout(() => {
fn();
}, delay)
}
}

代码优化

仔细一想,上面的代码是不是有什么问题?

问题一: 我们返回的fn函数,如果需要事件参数e怎么办?事件参数被debounce函数保存着,如果不把事件参数给闭包函数,若fn函数需要e我们没给,代码毫无疑问会报错。

问题二: 我们怎么确保调用fn函数的对象是我们想要的对象?你发现了吗,在上面这段代码中fn()函数的调用者是fn所定义的环境,这里涉及this指向问题,想要了解为什么可以去了解下js中的this。

为了解决上述两个问题,我们对代码优化如下

// fn是你要调用的函数,delay是防抖的时间
function debounce(fn, delay) {
// timer是一个定时器
let timer = null;
// 返回一个闭包函数,用闭包保存timer确保其不会销毁,重复点击会清理上一次的定时器
return function () {
// 保存事件参数,防止fn函数需要事件参数里的数据
let arg = arguments;
// 调用一次就清除上一次的定时器
clearTimeout(timer);
// 开启这一次的定时器
timer = setTimeout(() => {
// 若不改变this指向,则会指向fn定义环境
fn.apply(this, arg);
}, delay)
}
}

三、节流

当水龙头的水一直往下流,这十分的浪费水,所以我们可以把龙头关小一点,让水一滴一滴往下流,每隔一段时间掉下来一滴水。

节流就是限制一个函数在一段时间内只能执行一次,过了这段时间,在下一段时间又可以执行一次。应用场景如:

  1. 输入框的联想,可以限定用户在输入时,只在每两秒钟响应一次联想。
  2. 搜索框输入查询,如果用户一直在输入中,没有必要不停地调用去请求服务端接口,等用户停止输入的时候,再调用,设置一个合适的时间间隔,有效减轻服务端压力。
  3. 表单验证
  4. 按钮提交事件。

代码实现1(时间戳版)

// 方法一:时间戳
function throttle(fn, delay = 1000) {
// 记录第一次的调用时间
var prev = null;
console.log(prev);
// 返回闭包函数
return function () {
// 保存事件参数
var args = arguments;
// 记录现在调用的时间
var now = Date.now();
// console.log(now);
// 如果间隔时间大于等于设置的节流时间
if (now - prev >= delay) {
// 执行函数
fn.apply(this, args);
// 将现在的时间设置为上一次执行时间
prev = now;
}
}
}

触发事件时立即执行,以后每过delay秒之后才执行一次,并且最后一次触发事件若不满足要求不会被执行

代码实现2(定时器版)

// 方法二:定时器
function throttle(fn, delay) {
// 重置定时器
let timer = null;
// 返回闭包函数
return function () {
// 记录事件参数
let args = arguments;
// 如果定时器为空
if (!timer) {
// 开启定时器
timer = setTimeout(() => {
// 执行函数
fn.apply(this, args);
// 函数执行完毕后重置定时器
timer = null;
}, delay);
}
}
}

第一次触发时不会执行,而是在delay秒之后才执行,当最后一次停止触发后,还会再执行一次函数。

代码实现3(时间戳 & 定时器)

// 方法三:时间戳 & 定时器
function throttle(fn, delay) {
// 初始化定时器
let timer = null;
// 上一次调用时间
let prev = null;
// 返回闭包函数
return function () {
// 现在触发事件时间
let now = Date.now();
// 触发间隔是否大于delay
let remaining = delay - (now - prev);
// 保存事件参数
const args = arguments;
// 清除定时器
clearTimeout(timer);
// 如果间隔时间满足delay
if (remaining <= 0) {
// 调用fn,并且将现在的时间设置为上一次执行时间
fn.apply(this, args);
prev = Date.now();
} else {
// 否则,过了剩余时间执行最后一次fn
timer = setTimeout(() => {
fn.apply(this, args)
}, delay);
}
}
}


原文链接:https://juejin.cn/post/7040633388625035272


收起阅读 »

vue工程师必须学会封装的埋点指令思路

vue
前言 最近项目中需要做埋点功能,梳理下产品的埋点文档,发现点击埋点的场景比较多。因为使用的是阿里云sls日志服务去埋点,所以通过使用手动侵入代码式的埋点。定好埋点的形式后,技术实现方法也有很多,哪种比较好呢? 稍加思考... 决定封装个埋点指令,这样使用起来...
继续阅读 »

前言


最近项目中需要做埋点功能,梳理下产品的埋点文档,发现点击埋点的场景比较多。因为使用的是阿里云sls日志服务去埋点,所以通过使用手动侵入代码式的埋点。定好埋点的形式后,技术实现方法也有很多,哪种比较好呢?


稍加思考...



决定封装个埋点指令,这样使用起来会比较方便,因为指令的颗粒度比较细能够直击要害,挺适合上面所说的业务场景。


指令基础知识


在此之前,先来复习下vue自定义指令吧,这里只介绍常用的基础知识。更全的介绍可以查看官方文档


钩子函数




  • bind:只调用一次,指令第一次绑定到元素时调用。




  • inserted:被绑定元素插入父节点时调用。




  • update:所在组件的 VNode 更新时调用。




钩子函数参数



  • el:指令所绑定的DOM元素。

  • binding:一个对象,包含以下 property:

    • value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2。

    • arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"。



  • vnode:指令所绑定的当前组件vnode。


在这里分享个小技巧,钩子函数参数中没有可以直接获取当前实例的参数,但可以通过 vnode.context 获取到,这个在我之前的vue技巧文章中也有分享到,有兴趣可以去看看。


正文


进入正题,下面会介绍埋点指令的使用,内部是怎么实现的。


用法与思路


一般我在封装一个东西时,会先确定好它该怎么去用,然后再从用法入手去封装。这样会令整个思路更加清晰,在定义用法时也可以思考下易用性,不至于封装完之后因为用法不理想而返工。


埋点上报的数据会分为公共数据(每个埋点都要上报的数据)和自定义数据(可选的额外数据,和公共数据一起上报)。那么公共数据在内部就进行统一处理,对于自定义数据则需要从外部传入。于是有了以下两种用法:



  • 一般用法


<div v-track:clickBtn></div>


  • 自定义数据


<div v-track:clickBtn="{other:'xxx'}"></div>

可以看到埋点事件是通过 arg 的形式传入,在此之前也看到有些小伙伴封装的埋点事件是在 value 传入。但我个人比较喜欢 arg 的形式,这种更能让人一目了然对应的埋点事件是什么。


另外上报数据结构大致为:


{   
eventName: 'clickBtn'
userId: 1,
userName: 'xxx',
data: {
other: 'xxx'
}
}

eventName 是埋点对应的事件名,与之同级的是公共数据,而自定义数据放在 data 内。


实现


定义一个 track.js 的文件


import SlsWebLogger from 'js-sls-logger'

function getSlsWebLoggerInstance (options = {}) {
return new SlsWebLogger({
host: '***',
project: '***',
logstore: `***`,
time: 10,
count: 10,
...options
})
}

export default {
install (Vue, {baseData = {}, slsOptions = {}) {
const slsWebLogger = getSlsWebLoggerInstance(slsOptions)
// 获取公共数据的方法
let getBaseTrackData = typeof baseData === 'function' ? baseData : () => baseData
let baseTrackData = null
const Track = {
name: 'track',
inserted (el, binding) {
el.addEventListener('click', () => {
if (!binding.arg) {
console.error('Track slsWebLogger 事件名无效')
return
}
if (!baseTrackData) {
baseTrackData = getBaseTrackData()
}
baseTrackData.eventName = binding.arg
// 自定义数据
let trackData = binding.value || {}
const submitData = Object.assign({}, baseTrackData, {data: trackData})
// 上报
slsWebLogger.send(submitData)
if (process.env.NODE_ENV === 'development') {
console.log('Track slsWebLogger', submitData)
}
})
}
}
Vue.directive(Track.name, Track)
}
}

封装比较简单,主要做了两件事,首先是为绑定指令的 DOM 添加 click 事件,其次处理上报数据。在封装埋点指令时,公共数据通过baseData传入,这样可以增加通用性,第二个参数是上报平台的一些配置参数。


在初始化时注册指令:


import store from 'src/store'
import track from 'Lib/directive/track'

function getBaseTrackData () {
let userInfo = store.state.User.user_info
// 公共数据
const baseTrackData = {
userId: userInfo.user_id, // 用户id
userName: userInfo.user_name // 用户名
}
return baseTrackData
}

Vue.use(track, {baseData: getBaseTrackData})

Vue.use 时会自动寻找 install 函数进行调用,最终在全局注册指令。


加点通用性


除了点击埋点之外,如果有停留埋点等场景,上面的指令就不适用了。为此,可以增加手动调用的形式。


export default {
install (Vue, {baseData = {}, slsOptions = {}) {
// ...
Vue.directive(Track.name, Track)
// 手动调用
Vue.prototype.slsWebLogger = {
send (trackData) {
if (!trackData.eventName) {
console.error('Track slsWebLogger 事件名无效')
return
}
const submitData = Object.assign({}, getBaseTrackData(), trackData)
slsWebLogger.send(submitData)
if (process.env.NODE_ENV === 'development') {
console.log('Track slsWebLogger', submitData)
}
}
}
}

这种挂载到原型的方式可以在每个组件实例上通过 this 方便进行调用。


export default {
// ...
created () {
this.slsWebLogger.send({
//...
})
}
}

总结


本文分享了封装埋点指令的过程,封装并不难实现。主要有两种形式,点击埋点通过绑定 DOM click 事件监听点击上报,而其他场景下提供手动调用的方式。主要是想记录下封装的思路,以及使用方式。埋点实现也是根据业务做了一些调整,比如注册埋点指令可以接受上报平台的配置参数。毕竟人是活的,代码是死的。只要能满足业务需求并且能维护,怎么使用舒服怎么来嘛。


作者:出来吧皮卡丘
链接:https://juejin.cn/post/7040649951923142687

收起阅读 »

Android中的类加载器

类的生命周期加载阶段加载阶段可以细分如下加载类的二进制流数据结构转换,将二进制流所代表的静态存储结构转化成方法区的运行时的数据结构生成java.lang.Class对象,作为方法区这个类的各种数据的访问入口加载类的二进制流的方法从zip包中读取。我们常见的JA...
继续阅读 »

类的生命周期

image.png

加载阶段

加载阶段可以细分如下

  • 加载类的二进制流
  • 数据结构转换,将二进制流所代表的静态存储结构转化成方法区的运行时的数据结构
  • 生成java.lang.Class对象,作为方法区这个类的各种数据的访问入口

加载类的二进制流的方法

  • 从zip包中读取。我们常见的JAR、AAR依赖
  • 运行时动态生成。我们常见的动态代理技术,在java.reflect.Proxy中就是用ProxyGenerateProxyClass来为特定的接口生成代理的二进制流
验证

验证是连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

  1. 文件格式验证:如是否以魔数 0xCAFEBABE 开头、主、次版本号是否在当前虚拟机处理范围之内、常量合理性验证等。
    此阶段保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个 Java类型信息的要求。
  2. 元数据验证:是否存在父类,父类的继承链是否正确,抽象类是否实现了其父类或接口之中要求实现的所有方法,字段、方法是否与父类产生矛盾等。
    第二阶段,保证不存在不符合 Java 语言规范的元数据信息。
  3. 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。例如保证跳转指令不会跳转到方法体以外的字节码指令上。
  4. 符号引用验证:在解析阶段中发生,保证可以将符号引用转化为直接引用。

可以考虑使用 -Xverify:none 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

准备

类变量分配内存并设置类变量初始值,这些变量所使用的内存都将在方法区中进行分配。

解析

虚拟机将常量池内的符号引用替换为直接引用的过程。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行

初始化

到初始化阶段,才真正开始执行类中定义的 Java 程序代码,此阶段是执行 () 方法的过程。

类加载的时机

虚拟机规范规定了有且只有 5 种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始)

  1. 遇到new、getstatic 和 putstatic 或 invokestatic 这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。对应场景是:使用 new 实例化对象、读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)、以及调用一个类的静态方法。

  2. 对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

  3. 当初始化类的父类还没有进行过初始化,则需要先触发其父类的初始化。(而一个接口在初始化时,并不要求其父接口全部都完成了初始化)

  4. 虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),
    虚拟机会先初始化这个主类。

  5. 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

注意:

  1. 通过子类引用父类的静态字段,不会导致子类初始化。
  2. 通过数组定义来引用类,不会触发此类的初始化。MyClass[] cs = new MyClass[10];
  3. 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

类加载器

把实现类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作的代码模块称为“类加载器”。

将 class 文件二进制数据放入方法区内,然后在堆内(heap)创建一个 java.lang.Class 对象,Class 对象封装了类在方法区内的数据结构,并且向开发者提供了访问方法区内的数据结构的接口。

类的唯一性

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。

即使两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类也不相等。
这里所指的“相等”,包括代表类的 Class 对象的 equals() 方法、 isAssignableFrom() 方法、isInstance() 方法的返回结果,也包括使用 instanceof 关键字做对象所属关系判定等情况

双亲委托机制

image.png

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

    protected Class loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
//先从缓存中加没加载这个类
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
//从parent中加载
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
//加载不到,就自己加载
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}


好处
  • 避免重复加载,当父加载器已经加载了该类的时候,就没有必要子ClassLoader再加载一次。
  • 安全性考虑,防止核心API库被随意篡改。

Android中ClassLoader

image.png

  • ClassLoader是一个抽象类,定义了ClassLoader的主要功能
  • BootClassLoader是ClassLoader的子类(注意不是内部类,有些材料上说是内部类,是不对的),用于加载一些系统Framework层级需要的类,是Android平台上所有的ClassLoader的最终parent
  • SecureClassLoader扩展了ClassLoader类,加入了权限方面的功能,加强了安全性
  • URLClassLoader继承SecureClassLoader,用来通过URI路径从jar文件和文件夹中加载类和资源,在Android中基本无法使用
  • BaseDexClassLoader是实现了Android ClassLoader的大部分功能
  • PathClassLoader加载应用程序的类,会加载/data/app目录下的dex文件以及包含dex的apk文件或者java文件(有些材料上说他也会加载系统类,我没有找到,这里存疑)
  • DexClassLoader可以加载自定义dex文件以及包含dex的apk文件或jar文件,支持从SD卡进行加载。我们使用插件化技术的时候会用到
  • InMemoryDexClassLoader用于加载内存中的dex文件

ClassLoader的加载流程源码分析

-> ClassLoader.java 类

protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized(this.getClassLoadingLock(name)) {
//先查找class是否已经加载过,如果加载过直接返回
Class c = this.findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();

try {
if (this.parent != null) {
//委托给parent加载器进行加载 ClassLoader parent;
c = this.parent.loadClass(name, false);
} else {
//当执行到顶层的类加载器时,parent = null
c = this.findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException var10) {
}

if (c == null) {
long t1 = System.nanoTime();
c = this.findClass(name);
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
//如果parent加载器中没有找到,
PerfCounter.getFindClasses().increment();
}
}

if (resolve) {
this.resolveClass(c);
}

return c;
}
}

由子类实现

protected Class findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}

BaseDexClassLoader类中findClass方法

protected Class findClass(String name) throws ClassNotFoundException {
List suppressedExceptions = new ArrayList();
// pathList是DexPathList,是具体存放代码的地方。
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException(
"Didn't find class "" + name + "" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}

public Class findClass(String name, List suppressed) {
for (Element element : dexElements) {
Class clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}

if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}

public Class findClass(String name, ClassLoader definingContext,
List suppressed) {
return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed) : null;
}

public Class loadClassBinaryName(String name, ClassLoader loader, List suppressed) {
return defineClass(name, loader, mCookie, this, suppressed);
}

private static Class defineClass(String name, ClassLoader loader, Object cookie,
DexFile dexFile, List suppressed) {
Class result = null;
try {
result = defineClassNative(name, loader, cookie, dexFile);
} catch (NoClassDefFoundError e) {
if (suppressed != null) {
suppressed.add(e);
}
} catch (ClassNotFoundException e) {
if (suppressed != null) {
suppressed.add(e);
}
}
return result;
}

// 调用 Native 层代码
private static native Class defineClassNative(String name, ClassLoader loader, Object cookie, DexFile dexFile)

image.png

本文转自 juejin.cn/post/703847…,如有侵权,请联系删除。


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

收起阅读 »

Android 多线程-IntentService详解

IntentService 一、IntentService概述   上一篇我们聊到了HandlerThread,本篇我们就来看看HandlerThread在IntentService中的应用,看本篇前建议先看看上篇的HandlerThread,有助于我们更好掌...
继续阅读 »

IntentService


一、IntentService概述


  上一篇我们聊到了HandlerThread,本篇我们就来看看HandlerThread在IntentService中的应用,看本篇前建议先看看上篇的HandlerThread,有助于我们更好掌握IntentService。同样地,我们先来看看IntentService的特点:



  • 它本质是一种特殊的Service,继承自Service并且本身就是一个抽象类

  • 它可以用于在后台执行耗时的异步任务,当任务完成后会自动停止

  • 它拥有较高的优先级,不易被系统杀死(继承自Service的缘故),因此比较适合执行一些高优先级的异步任务

  • 它内部通过HandlerThread和Handler实现异步操作

  • 创建IntentService时,只需实现onHandleIntent和构造方法,onHandleIntent为异步方法,可以执行耗时操作


二、IntentService的常规使用套路


  大概了解了IntentService的特点后,我们就来了解一下它的使用方式,先看个案例:

IntentService实现类如下:


package com.zejian.handlerlooper;

import android.app.IntentService;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.IBinder;
import android.os.Message;

import com.zejian.handlerlooper.util.LogUtils;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;

/**
* Created by zejian
* Time 16/9/3.
* Description:
*/
public class MyIntentService extends IntentService {
public static final String DOWNLOAD_URL="download_url";
public static final String INDEX_FLAG="index_flag";
public static UpdateUI updateUI;


public static void setUpdateUI(UpdateUI updateUIInterface){
updateUI=updateUIInterface;
}

public MyIntentService(){
super("MyIntentService");
}

/**
* 实现异步任务的方法
* @param intent Activity传递过来的Intent,数据封装在intent中
*/
@Override
protected void onHandleIntent(Intent intent) {

//在子线程中进行网络请求
Bitmap bitmap=downloadUrlBitmap(intent.getStringExtra(DOWNLOAD_URL));
Message msg1 = new Message();
msg1.what = intent.getIntExtra(INDEX_FLAG,0);
msg1.obj =bitmap;
//通知主线程去更新UI
if(updateUI!=null){
updateUI.updateUI(msg1);
}
//mUIHandler.sendMessageDelayed(msg1,1000);

LogUtils.e("onHandleIntent");
}
//----------------------重写一下方法仅为测试------------------------------------------
@Override
public void onCreate() {
LogUtils.e("onCreate");
super.onCreate();
}

@Override
public void onStart(Intent intent, int startId) {
super.onStart(intent, startId);
LogUtils.e("onStart");
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
LogUtils.e("onStartCommand");
return super.onStartCommand(intent, flags, startId);

}

@Override
public void onDestroy() {
LogUtils.e("onDestroy");
super.onDestroy();
}

@Override
public IBinder onBind(Intent intent) {
LogUtils.e("onBind");
return super.onBind(intent);
}


public interface UpdateUI{
void updateUI(Message message);
}


private Bitmap downloadUrlBitmap(String urlString) {
HttpURLConnection urlConnection = null;
BufferedInputStream in = null;
Bitmap bitmap=null;
try {
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024);
bitmap= BitmapFactory.decodeStream(in);
} catch (final IOException e) {
e.printStackTrace();
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
try {
if (in != null) {
in.close();
}
} catch (final IOException e) {
e.printStackTrace();
}
}
return bitmap;
}

}

  通过代码可以看出,我们继承了IntentService,这里有两个方法是必须实现的,一个是构造方法,必须传递一个线程名称的字符串,另外一个就是进行异步处理的方法onHandleIntent(Intent intent) 方法,其参数intent可以附带从activity传递过来的数据。这里我们的案例主要利用onHandleIntent实现异步下载图片,然后通过回调监听的方法把下载完的bitmap放在message中回调给Activity(当然也可以使用广播完成),最后通过Handler去更新UI。下面再来看看Acitvity的代码:


activity_intent_service.xml


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">

<ImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

IntentServiceActivity.java


package com.zejian.handlerlooper.util;

import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.widget.ImageView;

import com.zejian.handlerlooper.MyIntentService;
import com.zejian.handlerlooper.R;

/**
* Created by zejian
* Time 16/9/3.
* Description:
*/
public class IntentServiceActivity extends Activity implements MyIntentService.UpdateUI{
/**
* 图片地址集合
*/
private String url[] = {
"https://img-blog.csdn.net/20160903083245762",
"https://img-blog.csdn.net/20160903083252184",
"https://img-blog.csdn.net/20160903083257871",
"https://img-blog.csdn.net/20160903083257871",
"https://img-blog.csdn.net/20160903083311972",
"https://img-blog.csdn.net/20160903083319668",
"https://img-blog.csdn.net/20160903083326871"
};

private static ImageView imageView;
private static final Handler mUIHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
imageView.setImageBitmap((Bitmap) msg.obj);
}
};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_intent_service);
imageView = (ImageView) findViewById(R.id.image);

Intent intent = new Intent(this,MyIntentService.class);
for (int i=0;i<7;i++) {//循环启动任务
intent.putExtra(MyIntentService.DOWNLOAD_URL,url[i]);
intent.putExtra(MyIntentService.INDEX_FLAG,i);
startService(intent);
}
MyIntentService.setUpdateUI(this);
}

//必须通过Handler去更新,该方法为异步方法,不可更新UI
@Override
public void updateUI(Message message) {
mUIHandler.sendMessageDelayed(message,message.what * 1000);
}
}

  代码比较简单,通过for循环多次去启动IntentService,然后去下载图片,注意即使我们多次启动IntentService,但IntentService的实例只有一个,这跟传统的Service是一样的,最终IntentService会去调用onHandleIntent执行异步任务。这里可能我们还会担心for循环去启动任务,而实例又只有一个,那么任务会不会被覆盖掉呢?其实是不会的,因为IntentService真正执行异步任务的是HandlerThread+Handler,每次启动都会把下载图片的任务添加到依附的消息队列中,最后由HandlerThread+Handler去执行。好~,我们运行一下代码:



每间隔一秒去更新图片,接着我们看一组log:



从Log可以看出onCreate只启动了一次,而onStartCommand和onStart多次启动,这就证实了之前所说的,启动多次,但IntentService的实例只有一个,这跟传统的Service是一样的,最后任务都执行完成后,IntentService自动销毁。以上便是IntentService德使用方式,怎么样,比较简单吧。接着我们就来分析一下IntentService的源码,其实也比较简单只有100多行代码。


三、IntentService源码解析


我们先来看看IntentService的onCreate方法:


@Override
public void onCreate() {
// TODO: It would be nice to have an option to hold a partial wakelock
// during processing, and to have a static startService(Context, Intent)
// method that would launch the service & hand off a wakelock.

super.onCreate();
HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
thread.start();

mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);
}

  当第一启动IntentService时,它的onCreate方法将会被调用,其内部会去创建一个HandlerThread并启动它,接着创建一个ServiceHandler(继承Handler),传入HandlerThread的Looper对象,这样ServiceHandler就变成可以处理异步线程的执行类了(因为Looper对象与HandlerThread绑定,而HandlerThread又是一个异步线程,我们把HandlerThread持有的Looper对象传递给Handler后,ServiceHandler内部就持有异步线程的Looper,自然就可以执行异步任务了),那么IntentService是怎么启动异步任务的呢?其实IntentService启动后还会去调用onStartCommand方法,而onStartCommand方法又会去调用onStart方法,我们看看它们的源码:


@Override
public void onStart(Intent intent, int startId) {
Message msg = mServiceHandler.obtainMessage();
msg.arg1 = startId;
msg.obj = intent;
mServiceHandler.sendMessage(msg);
}

/**
* You should not override this method for your IntentService. Instead,
* override {@link #onHandleIntent}, which the system calls when the IntentService
* receives a start request.
* @see android.app.Service#onStartCommand
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
onStart(intent, startId);
return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
}

  从源码我们可以看出,在onStart方法中,IntentService通过mServiceHandler的sendMessage方法发送了一个消息,这个消息将会发送到HandlerThread中进行处理(因为HandlerThread持有Looper对象,所以其实是Looper从消息队列中取出消息进行处理,然后调用mServiceHandler的handleMessage方法),我们看看ServiceHandler的源码:


private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}

@Override
public void handleMessage(Message msg) {
onHandleIntent((Intent)msg.obj);
stopSelf(msg.arg1);
}
}

  这里其实也说明onHandleIntent确实是一个异步处理方法(ServiceHandler本身就是一个异步处理的handler类),在onHandleIntent方法执行结束后,IntentService会通过 stopSelf(int startId)方法来尝试停止服务。这里采用stopSelf(int startId)而不是stopSelf()来停止服务,是因为stopSelf()会立即停止服务,而stopSelf(int startId)会等待所有消息都处理完后才终止服务。最后看看onHandleIntent方法的声明:


protected abstract void onHandleIntent(Intent intent);

  到此我们就知道了IntentService的onHandleIntent方法是一个抽象方法,所以我们在创建IntentService时必须实现该方法,通过上面一系列的分析可知,onHandleIntent方法也是一个异步方法。这里要注意的是如果后台任务只有一个的话,onHandleIntent执行完,服务就会销毁,但如果后台任务有多个的话,onHandleIntent执行完最后一个任务时,服务才销毁。最后我们要知道每次执行一个后台任务就必须启动一次IntentService,而IntentService内部则是通过消息的方式发送给HandlerThread的,然后由Handler中的Looper来处理消息,而Looper是按顺序从消息队列中取任务的,也就是说IntentService的后台任务时顺序执行的,当有多个后台任务同时存在时,这些后台任务会按外部调用的顺序排队执行,我们前面的使用案例也很好说明了这点。最后贴一下到IntentService的全部源码,大家再次感受一下:


/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package android.app;

import android.annotation.WorkerThread;
import android.content.Intent;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;

/**
* IntentService is a base class for {@link Service}s that handle asynchronous
* requests (expressed as {@link Intent}s) on demand. Clients send requests
* through {@link android.content.Context#startService(Intent)} calls; the
* service is started as needed, handles each Intent in turn using a worker
* thread, and stops itself when it runs out of work.
*
* <p>This "work queue processor" pattern is commonly used to offload tasks
* from an application's main thread. The IntentService class exists to
* simplify this pattern and take care of the mechanics. To use it, extend
* IntentService and implement {@link #onHandleIntent(Intent)}. IntentService
* will receive the Intents, launch a worker thread, and stop the service as
* appropriate.
*
* <p>All requests are handled on a single worker thread -- they may take as
* long as necessary (and will not block the application's main loop), but
* only one request will be processed at a time.
*
* <div>
* <h3>Developer Guides</h3>
* <p>For a detailed discussion about how to create services, read the
* <a href="{@docRoot}guide/topics/fundamentals/services.html">Services</a> developer guide.</p>
* </div>
*
* @see android.os.AsyncTask
*/
public abstract class IntentService extends Service {
private volatile Looper mServiceLooper;

private volatile ServiceHandler mServiceHandler;
private String mName;
private boolean mRedelivery;

private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}

@Override
public void handleMessage(Message msg) {
onHandleIntent((Intent)msg.obj);
stopSelf(msg.arg1);
}
}

/**
* Creates an IntentService. Invoked by your subclass's constructor.
*
* @param name Used to name the worker thread, important only for debugging.
*/
public IntentService(String name) {
super();
mName = name;
}

/**
* Sets intent redelivery preferences. Usually called from the constructor
* with your preferred semantics.
*
* <p>If enabled is true,
* {@link #onStartCommand(Intent, int, int)} will return
* {@link Service#START_REDELIVER_INTENT}, so if this process dies before
* {@link #onHandleIntent(Intent)} returns, the process will be restarted
* and the intent redelivered. If multiple Intents have been sent, only
* the most recent one is guaranteed to be redelivered.
*
* <p>If enabled is false (the default),
* {@link #onStartCommand(Intent, int, int)} will return
* {@link Service#START_NOT_STICKY}, and if the process dies, the Intent
* dies along with it.
*/
public void setIntentRedelivery(boolean enabled) {
mRedelivery = enabled;
}

@Override
public void onCreate() {
// TODO: It would be nice to have an option to hold a partial wakelock
// during processing, and to have a static startService(Context, Intent)
// method that would launch the service & hand off a wakelock.

super.onCreate();
HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
thread.start();

mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);
}

@Override
public void onStart(Intent intent, int startId) {
Message msg = mServiceHandler.obtainMessage();
msg.arg1 = startId;
msg.obj = intent;
mServiceHandler.sendMessage(msg);
}

/**
* You should not override this method for your IntentService. Instead,
* override {@link #onHandleIntent}, which the system calls when the IntentService
* receives a start request.
* @see android.app.Service#onStartCommand
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
onStart(intent, startId);
return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
}

@Override
public void onDestroy() {
mServiceLooper.quit();
}

/**
* Unless you provide binding for your service, you don't need to implement this
* method, because the default implementation returns null.
* @see android.app.Service#onBind
*/
@Override
public IBinder onBind(Intent intent) {
return null;
}

/**
* This method is invoked on the worker thread with a request to process.
* Only one Intent is processed at a time, but the processing happens on a
* worker thread that runs independently from other application logic.
* So, if this code takes a long time, it will hold up other requests to
* the same IntentService, but it will not hold up anything else.
* When all requests have been handled, the IntentService stops itself,
* so you should not call {@link #stopSelf}.
*
* @param intent The value passed to {@link
* android.content.Context#startService(Intent)}.
*/
@WorkerThread
protected abstract void onHandleIntent(Intent intent);
}

此IntentService的源码就分析完了,嗯,本篇完结。


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

Android onSaveInstanceState/onRestoreInstanceState 原来要这么理解

前些天,有位小伙伴兴匆匆地跑过来给我展示一个现象:Activity 里有个EditText,点击该EditText 输入一些文字。此时,转动手机方向,Activity 变成横屏了,而EditText 上的文字依然保留。 问我:为啥EditText上文字能够恢复...
继续阅读 »

前些天,有位小伙伴兴匆匆地跑过来给我展示一个现象:Activity 里有个EditText,点击该EditText 输入一些文字。此时,转动手机方向,Activity 变成横屏了,而EditText 上的文字依然保留。

问我:为啥EditText上文字能够恢复?

我说:你Activity 配置了横竖屏切换时不重建Activity。

他立马给我展示了:Activity 重建的日志。

我说:系统会在重建Activity 的时候恢复整个ViewTree吧。

他又给我展示了:ImageView 横竖屏时没有恢复之前的图像。

我:...

不服输的我开始了默默地研究,于是有了这篇总结以解心中困惑。

通过本篇文章,你将了解到:



1、onSaveInstanceState/onRestoreInstanceState 作用。

2、onSaveInstanceState/onRestoreInstanceState 原理分析

3、onSaveInstanceState/onRestoreInstanceState 触发场景。

4、onSaveInstanceState/onRestoreInstanceState 为啥不能存放大数据?

5、与Jetpack ViewModel 区别。



1、onSaveInstanceState/onRestoreInstanceState 作用


EditText/ImageView 横竖屏地表现



tt0.top-423136.gif


可以看出,从竖屏到横屏再恢复到竖屏,EditText 内容没有变化。而从竖屏到横屏时,ImageView 内容已经丢失了。

都是系统控件,咱们也没有进行其它的额外区别处理,为啥表现不一致呢?

View.java 里有两个方法:


#View.java
protected Parcelable onSaveInstanceState() {...}

protected void onRestoreInstanceState(Parcelable state){...}

官方注释上写的比较清楚了:



1、onSaveInstanceState 是个钩子方法,View.java 的子类可以重写该方法,在方法里面存储一些子类的内部状态,用以下次重建时恢复。

2、onRestoreInstanceState 也是个钩子方法,用以恢复在onSaveInstanceState 里保存的状态。



既然是View的方法,分别查看EditText 与ImageView 对它们的重写情况:


#TextView.java
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
...
if (freezesText || hasSelection) {
SavedState ss = new SavedState(superState);

if (freezesText) {
if (mText instanceof Spanned) {
final Spannable sp = new SpannableStringBuilder(mText);
...
ss.text = sp;
} else {
//将TextView 内容存储在SavedState里
ss.text = mText.toString();
}
}
...
return ss;
}

//返回存储的对象
return superState;
}

public void onRestoreInstanceState(Parcelable state) {
if (!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}

SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());

if (ss.text != null) {
//取出TextView 内容,并设置
setText(ss.text);
}
...
}

由此可见,TextView 重写这俩方法,先是在onSaveInstanceState 里存储文本内容,再在onRestoreInstanceState 里恢复文本内容。

而通过查看ImageView 发现它并没有重写这俩方法,当然就不能恢复了。其实这也比较容易理解,毕竟对于ImageView,Bitmap 是它的内容,暂存这个Bitmap 很耗内存。


需要注意的是:想要onSaveInstanceState 被调用,则需要给该控件设置id。因为系统是根据View id将状态存储在SparseArray 里


Activity 横竖屏的处理


现在的问题是:谁调用了View 的onSaveInstanceState/onRestoreInstanceState ? 在前一篇分析过Activity 和View的关系:Android Activity 与View 的互动思考

可知,Activity 通过Window 控制View,我们子类继承自EditText,并重写 onSaveInstanceState/onRestoreInstanceState,然后在横竖屏切换时查看这俩方法的调用栈:



image.png


第一个红色框表示EditText子类里的方法(onSaveInstanceState),而第二个红框表示Activity 子类里重写的方法(onSaveInstanceState)。

由此可知,当横竖屏切换时调用了Activity.onSaveInstanceState(xx) 方法。


#Activity.java
protected void onSaveInstanceState(@NonNull Bundle outState) {
//saveHierarchyState 调用整个ViewTree 的onSaveInstanceState 方法
outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());
...
//告知生命周期回调方法状态已保存
dispatchActivitySaveInstanceState(outState);
}

同样的对于onRestoreInstanceState:


#Activity.java
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
if (mWindow != null) {
Bundle windowState = savedInstanceState.getBundle(WINDOW_HIERARCHY_TAG);
if (windowState != null) {
//恢复整个ViewTree 状态
mWindow.restoreHierarchyState(windowState);
}
}
}


当横竖屏切换时,会调用到Activity onSaveInstanceState/onRestoreInstanceState 方法,进而会调用整个ViewTree onSaveInstanceState/onRestoreInstanceState 方法来保存与恢复必要的状态。



Activity 数据保存与恢复


Activity 的onSaveInstanceState/onRestoreInstanceState 方法 除了触发View 的状态保存与恢复外,还可以将Activity 用到的一些重要的数据保存下来,待下次Activity 重建时恢复。

重写两者:


    @Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("say", "hello world");
}

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
String restore = savedInstanceState.getString("say");
Log.d("fish", restore);
}

此时我们注意到onSaveInstanceState 的入参是Bundle 类型,往outState 写入数据,在onRestoreInstanceState 将数据取出,outState/savedInstanceState 必然不为空。


总结 onSaveInstanceState/onRestoreInstanceState 作用



1、保存与恢复View 的状态。

2、保存与恢复Activity 自定义数据。



2、onSaveInstanceState/onRestoreInstanceState 原理分析。


onSaveInstanceState 调用时机


之前 Android Activity 生命周期详解及监听 有详细分析了Activity 各个阶段的调用情况,现在结合生命周期来分析onSaveInstanceState(xx)在生命周期中的哪个阶段被调用的。

调用栈如下:



image.png


看看上图标黄色的方法,这方法很眼熟,在Activity 生命周期中分析过,它是Activity.onStop()方法的调用者:


#ActivityThread.java
private void callActivityOnStop(ActivityClientRecord r, boolean saveState, String reason) {
// Before P onSaveInstanceState was called before onStop, starting with P it's
// called after. Before Honeycomb state was always saved before onPause.
//这句话翻译过来:
//如果目标设备是Android 9之前,那么onSaveInstanceState 在onStop 之前调用
//如果在Android 9 之后,那么onSaveInstanceState 在onStop 之后调用
//Honeycomb 指的是Android 3.0 现在基本可以忽略了。
//r.activity.mFinished 表示Activity 是否即将被销毁
final boolean shouldSaveState = saveState && !r.activity.mFinished && r.state == null
&& !r.isPreHoneycomb();
final boolean isPreP = r.isPreP();
//Android p 之前先于onStop 之前执行
if (shouldSaveState && isPreP) {
callActivityOnSaveInstanceState(r);
}
try {
//最终执行到Activity.onStop()方法
r.activity.performStop(r.mPreserveWindow, reason);
} catch (SuperNotCalledException e) {
...
}
//标记Stop状态
r.setState(ON_STOP);

if (shouldSaveState && !isPreP) {
//调用onSave 保存
callActivityOnSaveInstanceState(r);
}
}

以上注释比较详细了,小结一下:



1、在Android 9之前,onSaveInstanceState 在onStop 之前调用(至于在onPause 之前还是之后调用,这个时机不确定);在Android 9(包含)之后,onSaveInstanceState 在onStop 之后调用。

2、如果Activity 即将被销毁,则onSaveInstanceState 不会被调用。



对于第二句的理解,举个简单例子:



Activity 在前台时,此时按Home键回到桌面,会执行onSaveInstanceState;若是按back键/主动finish,此时虽然会执行到onStop,但是不会执行onSaveInstanceState。



onRestoreInstanceState 调用时机


现在已经弄清楚onSaveInstanceState 调用时机,接着来分析 onRestoreInstanceState 什么时候执行。

调用栈如下:



image.png


黄色部分的方法也很眼熟,它是Activity.onStart()方法的调用者:


    public void handleStartActivity(ActivityClientRecord r,
PendingTransactionActions pendingActions) {
final Activity activity = r.activity;
...
//最终执行到Activity.onStart()
activity.performStart("handleStartActivity");
r.setState(ON_START);
...
if (pendingActions.shouldRestoreInstanceState()) {
if (r.isPersistable()) {
//从持久化存储里恢复数据
if (r.state != null || r.persistentState != null) {
mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state,
r.persistentState);
}
} else if (r.state != null) {
//从内存里恢复数据
mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state);
}
}
...
}

小结:



1、onRestoreInstanceState 在onStart()方法之后执行。

2、pendingActions.shouldRestoreInstanceState() 返回值是执行 onRestoreInstanceState()方法的关键,它是在哪赋值的呢?接下来会分析。

3、r.state 不能为空,毕竟没数据无法恢复。



通过以上分析,结合Activity生命周期,onSaveInstanceState /onRestoreInstanceState 调用时机如下:



image.png


onRestoreInstanceState 与onCreate 参数差异


onCreate参数也是Bundle类型,实际上这个参数就是onSaveInstanceState里保存的Bundle,这个Bundle分别传递给了onCreate和onRestoreInstanceState,而onCreate里的Bundle可能为空(新建非重建的情况下),onRestoreInstanceState 里的Bundle必然不为空。

官方注释也说了在onRestoreInstanceState里处理数据的恢复更灵活。


3、onSaveInstanceState/onRestoreInstanceState 触发场景


横竖屏触发的场景


在前面的分析中,与Activity 生命周期关联可能会让人有种印象:

onSaveInstanceState 调用之后onRestoreInstanceState 就会被调用。

而事实并非如此,举个简单例子:

Activity 处在前台时,此时退回到桌面,onSaveInstanceState 会被执行。而后再让Activity 回到前台,onStart()方法执行后,发现onRestoreInstanceState 并没有被调用。



也就是说onSaveInstanceState/onRestoreInstanceState 的调用不一定是成对出现的。



还记得在分析onRestoreInstanceState 遗留了个问题: pendingActions.shouldRestoreInstanceState() 返回值如何确定的 ?

在横竖屏切换时,onRestoreInstanceState 被调用了,说明 pendingActions.shouldRestoreInstanceState() 在横竖屏切换时返回了true,接着来看看其来龙去脉:


#PendingTransactionActions.java
//判断是否需要执行onRestoreInstanceState 方法
public boolean shouldRestoreInstanceState() {
return mRestoreInstanceState;
}

//设置标记
public void setRestoreInstanceState(boolean restoreInstanceState) {
mRestoreInstanceState = restoreInstanceState;
}

只需要找到setRestoreInstanceState()在何处调用即可。

直接说结论:



ActivityThread.handleLaunchActivity() 里设置了setRestoreInstanceState(true)



而handleLaunchActivity()在两种情况下被调用:



image.png


横竖屏时属于重建 Activity,因此onRestoreInstanceState 能被调用。

而从后台返回到前台,并没有新建Activity也没有重建Activity,因此onRestoreInstanceState 不会被调用。

又引申出另一个问题:为啥新建Activity 时onRestoreInstanceState 没被调用?

答案:因为新建Activity 时,ActivityClientRecord 是全新的对象,它所持有的Bundle state 对象为空,因此不会调用到onRestoreInstanceState。


其它配置项更改的场景


除了横竖屏切换时会重建Activity,还有以下配置项更改会重建Activity:



image.png


当然,还有一些不常涉及的配置项,比如所在地区更改等。


重建Activity 的细节



image.png


当需要重建Activity 时,AMS 发出指令,会执行到 ActivityThread.handleRelaunchActivity()方法。


#ActivityThread.java
public void handleRelaunchActivity(ActivityClientRecord tmp,
PendingTransactionActions pendingActions) {
...
//从Map 里获取缓存的ActivityClientRecord
ActivityClientRecord r = mActivities.get(tmp.token);
...
//将ActivityClientRecord 传递下去
handleRelaunchActivityInner(r, configChanges, tmp.pendingResults, tmp.pendingIntents,
pendingActions, tmp.startsNotResumed, tmp.overrideConfig, "handleRelaunchActivity");
...
}

mActivities 定义如下:


final ArrayMap<IBinder, ActivityClientRecord> mActivities = new ArrayMap<>();

以IBinder 为key,存储ActivityClientRecord。

当新建Activity 时,存入ActivityClientRecord,当销毁Activity 时,移除 ActivityClientRecord。


再来分析handleRelaunchActivityInner():


#ActivityThread.java
private void handleRelaunchActivityInner(...) {
...
if (!r.paused) {
//最终执行到onPause
performPauseActivity(r, false, reason, null /* pendingActions */);
}
if (!r.stopped) {
//最终执行到onStop
callActivityOnStop(r, true /* saveState */, reason);
}
//最终执行到onDestroy
handleDestroyActivity(r.token, false, configChanges, true, reason);
//创建新的Activity 实例
handleLaunchActivity(r, pendingActions, customIntent);
}

通过分析Activity 重建的细节,有以下结论:



1、Activity 重建过程中,先将原来的Activity 进行销毁(从onResume--onStop-->onDestroy 的生命周期)。

2、虽然是不同的Activity 对象,但重建时使用的ActivityClientRecord 却是相同的,而ActivityClientRecord 最终是被ActivityThread 持有,它是全局的。这也是 onSaveInstanceState/onRestoreInstanceState 能够存储与恢复数据的本质原因。



当然也可以通过配置告诉系统在配置项变更时不重建Activity:


<activity android:name=".viewmodel.ViewModelActivity" android:configChanges="orientation|screenSize"></activity>

比如以上配置,当横竖屏切换时,不会重建Activity,而配置项的变更会通过 Activity.onConfigurationChanged()方法回调。


4、onSaveInstanceState/onRestoreInstanceState 为啥不能存放大数据?


onSaveInstanceState/onRestoreInstanceState 的参数都是Bundle 类型,思考一下为什么需要定义为Bundle类型呢?

Android IPC 精讲系列 中有提到过,Android 进程间通信方式大多时候使用的是Binder,而要想自定义数据能够通过Binder传输则需要实现Parcelable 接口,Bundle 实现了Parcelable 接口。


由此我们推测,onSaveInstanceState/onRestoreInstanceState 可能涉及到进程间通信,才会用Bundle 来修饰形参。但之前说的ActivityClientRecord是存储在当前进程的啊,貌似和其它进程没有关联呢?

要分析这个问题,实际上只需要在onSaveInstanceState 存储一个比较大的数据,看看报错时的堆栈。


    protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("say", "hello world");
//存储2M 数据
outState.putByteArray("big", new byte[1024*1024*2]);
}

保存2M 的数据,通常来说这是超出了Binder的限制,当调用onSaveInstanceState 时会有报错信息:



image.png


果然还是crash了。

找到 PendingTransactionActions ,它实现了Runnable 接口,在其run方法里:


#PendingTransactionActions.java
public void run() {
try {
//提交给ActivityTaskManagerService 处理,属于进程间通信
//mState 即是onSaveInstanceState 保存的数据
ActivityTaskManager.getService().activityStopped(
mActivity.token, mState, mPersistentState, mDescription);
} catch (RemoteException ex) {
...
}
}

而在ActivityThread.java 里有个方法:


    public void reportStop(PendingTransactionActions pendingActions) {
mH.post(pendingActions.getStopInfo());
}

该方法用于告知系统,咱们的Activity 已经变为Stop状态了,最终会执行到PendingTransactionActions.run()方法。

小结一下:



onSaveInstanceState 存储的数据,在onStop执行后,当前进程需要将Stop状态传递给ATM(ActivityTaskManagerService 运行在system_server进程),因为跨进程传递(Binder)有大小限制,因此onSaveInstanceState 不能传递大量数据。



5、与Jetpack ViewModel 区别


onSaveInstanceState 与 ViewModel 都是将数据放在ActivityClientRecord 的不同字段里。



image.png



1、onSaveInstanceState 用Bundle存储数据便于跨进程传递,而ViewModel 是Object存储数据,不需要跨进程,因此它没有大小限制。

2、onSaveInstanceState 在onStop 之后调用,比较频繁。而ViewModel 存储数据是onDestroy 之后。

3、onSaveInstanceState 可以选择是否持久化数据到文件里(该功能由ATM 实现,存储到xml里),而ViewModel 没有这功能。



更多的区别后续分析 ViewModel 时会提到。


本文基于Android 10.0。


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

Spring Boot + Redis 解决重复提交问题,还有谁不会??

前言 在实际的开发项目中,一个对外暴露的接口往往会面临很多次请求,我们来解释一下幂等的概念:任意多次执行所产生的影响均与一次执行的影响相同。按照这个含义,最终的含义就是 对数据库的影响只能是一次性的,不能重复处理。如何保证其幂等性,通常有以下手段: 1、数据库...
继续阅读 »

前言


在实际的开发项目中,一个对外暴露的接口往往会面临很多次请求,我们来解释一下幂等的概念:任意多次执行所产生的影响均与一次执行的影响相同。按照这个含义,最终的含义就是 对数据库的影响只能是一次性的,不能重复处理。如何保证其幂等性,通常有以下手段:


1、数据库建立唯一性索引,可以保证最终插入数据库的只有一条数据。


2、token机制,每次接口请求前先获取一个token,然后再下次请求的时候在请求的header体中加上这个token,后台进行验证,如果验证通过删除token,下次请求再次判断token。


3、悲观锁或者乐观锁,悲观锁可以保证每次for update的时候其他sql无法update数据(在数据库引擎是innodb的时候,select的条件必须是唯一索引,防止锁全表)


4、先查询后判断,首先通过查询数据库是否存在数据,如果存在证明已经请求过了,直接拒绝该请求,如果没有存在,就证明是第一次进来,直接放行。


redis 实现自动幂等的原理图:



搭建 Redis 服务 API


1、首先是搭建redis服务器。


2、引入springboot中到的redis的stater,或者Spring封装的jedis也可以,后面主要用到的api就是它的set方法和exists方法,这里我们使用springboot的封装好的redisTemplate。


推荐一个 Spring Boot 基础教程及实战示例:
github.com/javastacks/…


/**
* redis工具类
*/
@Component
public class RedisService {

@Autowired
private RedisTemplate redisTemplate;

/**
* 写入缓存
* @param key
* @param value
* @return
*/
public boolean set(finalString key, Object value) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}

/**
* 写入缓存设置时效时间
* @param key
* @param value
* @return
*/
public boolean setEx(finalString key, Object value, Long expireTime) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}

/**
* 判断缓存中是否有对应的value
* @param key
* @return
*/
public boolean exists(finalString key) {
return redisTemplate.hasKey(key);
}

/**
* 读取缓存
* @param key
* @return
*/
public Objectget(finalString key) {
Object result = null;
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
result = operations.get(key);
return result;
}

/**
* 删除对应的value
* @param key
*/
public boolean remove(finalString key) {
if (exists(key)) {
Boolean delete = redisTemplate.delete(key);
return delete;
}
returnfalse;

}

}

自定义注解 AutoIdempotent


自定义一个注解,定义此注解的主要目的是把它添加在需要实现幂等的方法上,凡是某个方法注解了它,都会实现自动幂等。后台利用反射如果扫描到这个注解,就会处理这个方法实现自动幂等,使用元注解ElementType.METHOD表示它只能放在方法上,etentionPolicy.RUNTIME表示它在运行时。


@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoIdempotent {

}

token 创建和检验


token服务接口:我们新建一个接口,创建token服务,里面主要是两个方法,一个用来创建token,一个用来验证token。创建token主要产生的是一个字符串,检验token的话主要是传达request对象,为什么要传request对象呢?主要作用就是获取header里面的token,然后检验,通过抛出的Exception来获取具体的报错信息返回给前端。


publicinterface TokenService {

/**
* 创建token
* @return
*/
public String createToken();

/**
* 检验token
* @param request
* @return
*/
public boolean checkToken(HttpServletRequest request) throws Exception;

}

token的服务实现类:token引用了redis服务,创建token采用随机算法工具类生成随机uuid字符串,然后放入到redis中(为了防止数据的冗余保留,这里设置过期时间为10000秒,具体可视业务而定),如果放入成功,最后返回这个token值。checkToken方法就是从header中获取token到值(如果header中拿不到,就从paramter中获取),如若不存在,直接抛出异常。这个异常信息可以被拦截器捕捉到,然后返回给前端。


@Service
publicclass TokenServiceImpl implements TokenService {

@Autowired
private RedisService redisService;

/**
* 创建token
*
* @return
*/
@Override
public String createToken() {
String str = RandomUtil.randomUUID();
StrBuilder token = new StrBuilder();
try {
token.append(Constant.Redis.TOKEN_PREFIX).append(str);
redisService.setEx(token.toString(), token.toString(),10000L);
boolean notEmpty = StrUtil.isNotEmpty(token.toString());
if (notEmpty) {
return token.toString();
}
}catch (Exception ex){
ex.printStackTrace();
}
returnnull;
}

/**
* 检验token
*
* @param request
* @return
*/
@Override
public boolean checkToken(HttpServletRequest request) throws Exception {

String token = request.getHeader(Constant.TOKEN_NAME);
if (StrUtil.isBlank(token)) {// header中不存在token
token = request.getParameter(Constant.TOKEN_NAME);
if (StrUtil.isBlank(token)) {// parameter中也不存在token
thrownew ServiceException(Constant.ResponseCode.ILLEGAL_ARGUMENT, 100);
}
}

if (!redisService.exists(token)) {
thrownew ServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200);
}

boolean remove = redisService.remove(token);
if (!remove) {
thrownew ServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200);
}
returntrue;
}
}

拦截器的配置


web配置类,实现WebMvcConfigurerAdapter,主要作用就是添加autoIdempotentInterceptor到配置类中,这样我们到拦截器才能生效,注意使用@Configuration注解,这样在容器启动是时候就可以添加进入context中。


@Configuration
publicclass WebConfiguration extends WebMvcConfigurerAdapter {

@Resource
private AutoIdempotentInterceptor autoIdempotentInterceptor;

/**
* 添加拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(autoIdempotentInterceptor);
super.addInterceptors(registry);
}
}

拦截处理器:主要的功能是拦截扫描到AutoIdempotent到注解到方法,然后调用tokenService的checkToken()方法校验token是否正确,如果捕捉到异常就将异常信息渲染成json返回给前端。


/**
* 拦截器
*/
@Component
publicclass AutoIdempotentInterceptor implements HandlerInterceptor {

@Autowired
private TokenService tokenService;

/**
* 预处理
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

if (!(handler instanceof HandlerMethod)) {
returntrue;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
//被ApiIdempotment标记的扫描
AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class);
if (methodAnnotation != null) {
try {
return tokenService.checkToken(request);// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示
}catch (Exception ex){
ResultVo failedResult = ResultVo.getFailedResult(101, ex.getMessage());
writeReturnJson(response, JSONUtil.toJsonStr(failedResult));
throw ex;
}
}
//必须返回true,否则会被拦截一切请求
returntrue;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

}

/**
* 返回的json值
* @param response
* @param json
* @throws Exception
*/
private void writeReturnJson(HttpServletResponse response, String json) throws Exception{
PrintWriter writer = null;
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html; charset=utf-8");
try {
writer = response.getWriter();
writer.print(json);

} catch (IOException e) {
} finally {
if (writer != null)
writer.close();
}
}

}

测试用例


模拟业务请求类,首先我们需要通过/get/token路径通过getToken()方法去获取具体的token,然后我们调用testIdempotence方法,这个方法上面注解了@AutoIdempotent,拦截器会拦截所有的请求,当判断到处理的方法上面有该注解的时候,就会调用TokenService中的checkToken()方法,如果捕获到异常会将异常抛出调用者,下面我们来模拟请求一下:


@RestController
publicclass BusinessController {

@Resource
private TokenService tokenService;

@Resource
private TestService testService;

@PostMapping("/get/token")
public String getToken(){
String token = tokenService.createToken();
if (StrUtil.isNotEmpty(token)) {
ResultVo resultVo = new ResultVo();
resultVo.setCode(Constant.code_success);
resultVo.setMessage(Constant.SUCCESS);
resultVo.setData(token);
return JSONUtil.toJsonStr(resultVo);
}
return StrUtil.EMPTY;
}

@AutoIdempotent
@PostMapping("/test/Idempotence")
public String testIdempotence() {
String businessResult = testService.testIdempotence();
if (StrUtil.isNotEmpty(businessResult)) {
ResultVo successResult = ResultVo.getSuccessResult(businessResult);
return JSONUtil.toJsonStr(successResult);
}
return StrUtil.EMPTY;
}
}

使用postman请求,首先访问get/token路径获取到具体到token:



利用获取到到token,然后放到具体请求到header中,可以看到第一次请求成功,接着我们请求第二次:



第二次请求,返回到是重复性操作,可见重复性验证通过,再多次请求到时候我们只让其第一次成功,第二次就是失败:



总结


本篇博客介绍了使用springboot和拦截器、redis来优雅的实现接口幂等,对于幂等在实际的开发过程中是十分重要的,因为一个接口可能会被无数的客户端调用,如何保证其不影响后台的业务处理,如何保证其只影响数据一次是非常重要的,它可以防止产生脏数据或者乱数据,也可以减少并发量,实乃十分有益的一件事。而传统的做法是每次判断数据,这种做法不够智能化和自动化,比较麻烦。而今天的这种自动化处理也可以提升程序的伸缩性。


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

FlutterWeb初体验

FlutterWeb初体验 [toc] 背景 因为最近业务需求的变动,在APP的某一部分页面会经常性发生变动,一般情况下来说,这种不稳定的页面不应该由原生来承担,修改发版的成本太大了,最合理的做法是由H5来承担,由原生提供必要的bridge来调用原生方法,但是...
继续阅读 »

FlutterWeb初体验


[toc]


背景


因为最近业务需求的变动,在APP的某一部分页面会经常性发生变动,一般情况下来说,这种不稳定的页面不应该由原生来承担,修改发版的成本太大了,最合理的做法是由H5来承担,由原生提供必要的bridge来调用原生方法,但是由于种种历史债务,还是没有如此实现,经历了痛苦的发版以及等待审核后,我在想flutterWeb是不是可以解决这个问题?


想法


页面进入流程


screenshot-20211210-211936.png


项目架构想法


整个项目转为支持FlutterWeb


整个项目转为flutterweb,可以打包成web文件直接部署在服务器,而app依旧打包成apk和ipa,但是在路由监听处留下开关,当有页面需要紧急修复或者紧急更改的情况下,下发配置,跳转的时候根据路由配置跳转WebView或者原生页面。


抽离出某个模块,单个模块支持web


抽离出一个module,由一个壳工程引用,这个壳工程用于把该module打包成web;同时该模块依然被app工程引用,作为一个功能模块,而部署的时候只部署了这个模块的web产物。


因为目前app集成了一定数量的原生端的第三方sdk,直接支持flutterweb工程量较大,所以先尝试第二个方法。


壳工程结构图

1924616-18f5d8ee85f0f330.png


其中


flutter_libs 是基础的lib库,封装了基础的网络请求,持久化存储,状态管理等基础,壳工程和app工程也会引用


ly_income是功能module,也是我们主要开发需求的模块,它会被壳工程引用作为web的打包内容,也会被app工程引用作为原生的页面展示。


实践


打包问题处理


因为是新建的项目工程,打包成flutterWeb并不会有那么多障碍。


开启web支持


执行 flutter config查看目前的配置信息,如果看到


Settings:
enable-web: true
enable-macos-desktop: true

那就是已经开启了,如果还没,可以使用flutter config --enable-web开启配置


打包模式选择

而flutterWeb打包也有两种模式可以选择:html模式和CanvasKit模式


它们两者各自的特别是:


html模式


flutter build web --web-renderer html


当我们采用html渲染模式时,flutter会采用HTML的custom element,CSS,Canvas和SVG来渲染UI元素


优点是:体积比较小


缺点是:渲染性能比较差,跨端一致性可能不受保障



CanvasKit模式


flutter build web --web-renderer canvaskit


当我们采用canvaskit渲染模式时,flutter将 Skia 编译成 WebAssembly 格式,并使用 WebGL 渲染。应用在移动和桌面端保持一致,有更好的性能,以及降低不同浏览器渲染效果不一致的风险。但是应用的大小会增加大约 2MB。


优点是:跨端一致性受保障,渲染性能更好


缺点是:体积比较大,load页面时间会更久



跨域问题处理


之前一直是做app开发,跨域这个词只听过,还没见识过。


了解跨域

跨域是指浏览器的不执行其他网站脚本的,由于浏览器的同源策略造成,是对JavaScript的一种安全限制


说白点理解,当你通过浏览器向其他服务器发送请求时,不是服务器不响应,而是服务器返回的结果被浏览器限制了。


而什么是同源策略的同源



同源指的是协议、域名、端口 都要保持一致


http://www.123.com:8080/index.html (http协议,http://www.123.com 域名、8080 端口 ,只要这三个有一项不一样的都是跨域,这里不一一举例子)


http://www.123.com:8080/matsh.html(…


http://www.123.com:8081/matsh.html(…


注意:localhost 和127.0.0.1 虽然都指向本机,但也属于跨域。



而跨域的解决方法也暂时不适用我:



  1. JSONP方式 (我们项目的请求都是post请求)

  2. 反向代理,ngixn (ngixn小白)

  3. 配置浏览器 (好像不太适用,应该,大概,也许,可能,或许)

  4. 项目配置跨域 (因为只是尝试项目,需要后台和运维支持的话,需要跨部门沟通,太麻烦了)


摘自网络 什么是跨域,侵删歉


常规做法



  1. 本地调试的时候修改代码,支持跨域请求


    在上图红框中添加代码--disable-web-security




1924616-e444ef62f7776b1e.png


1924616-fddf6a72c3a43965.png


然后删除以下两个文件,执行flutter doctor生成新的一份,再尝试run起来,你会发现浏览器已经支持跨域了,你可以很开心地在浏览器run接口了。但是仅支持本地调试!!!



  1. ngixn做转发,但是这个... 我没有怎么用过ngixn,而且需要在周末做完调研给出可行性报告,也没有时间去学习,先搁置,后续再拿起来看看

  2. 后端和运维同学帮忙调试跨域,因为是尝试而已,没有必要用到其他部门的资源,先搁置,后续如果可实际应用,再要求他们协助。


骚操作


保命前提:



  1. 这个其实就是配置转发的做法,但是这块我没什么经验,时间紧任务重所以就先这么尝试做了

  2. 其实这个就是类似于openfeign之类的想法,但是我并不知道后台开发的FeignClient,而且也有点危险,还是调用开发的接口更加稳妥

  3. 纯个人做法,肯定还会有更好的方法,但是这个是我当时最快的达成方案,勿喷。



如果说我要求不了后台服务做跨域,那可不可以我自己要求我自己做跨域呢?


比如:


我请求我的服务器,我的服务器再去请求后台服务,我访问后台服务跨域而已,我的服务器访问后台服务可不跨域,我的服务器跨域又咋样,自己的东西随便拿捏。



  1. 新建一个springboot项目

  2. 搭建一个controller,参数是url全路径以及参数json字符串,配置好header之后请求后台服务并返回信息


@CrossOrigin
@RestController
@RequestMapping("api/home")
public class GatewayController {

@PostMapping("/gatewayApi")
public String gatewayApi(@RequestParam("url") String url, @RequestParam("params") String json) {
try {
JSONObject jsonObject = JSONObject.parseObject(json);
JSONObject result = doPost(jsonObject, url);
if (result != null) {
return result.toString();
} else {
return errMsg().toString();
}
} catch (Exception e) {
return errMsg(e.getMessage()).toString();
}
}
}


  1. 配置跨域信息


@SpringBootConfiguration
public class WebGlobalConfig {

@Bean
public CorsFilter corsFilter() {

//创建CorsConfiguration对象后添加配置
CorsConfiguration config = new CorsConfiguration();
//设置放行哪些原始域
config.addAllowedOriginPattern("*");
//放行哪些原始请求头部信息
config.addAllowedHeader("*");
//暴露哪些头部信息
config.addExposedHeader("*");
//放行哪些请求方式
config.addAllowedMethod("GET"); //get
config.addAllowedMethod("PUT"); //put
config.addAllowedMethod("POST"); //post
config.addAllowedMethod("DELETE"); //delete
//corsConfig.addAllowedMethod("*"); //放行全部请求

//是否发送Cookie
config.setAllowCredentials(true);

//2. 添加映射路径
UrlBasedCorsConfigurationSource corsConfigurationSource =
new UrlBasedCorsConfigurationSource();
corsConfigurationSource.registerCorsConfiguration("/**", config);
//返回CorsFilter
return new CorsFilter(corsConfigurationSource);
}
}


  1. 打包后部署到服务器

  2. module里的接口不再请求后台服务,而是请求我的服务器,因为只是转发,所以没有改动任何数据结构,只需要请求地址改动下

  3. 可以跨域了


与原生交互问题


设想中web的页面可以有三种方式:



  1. 集成在app里面作为原生页面,这个的交互没什么好说的。

  2. 打包成web项目,通过webview进行加载,那需要额外处理持久化信息的获取与写入,以及与原生页面的跳转交互

  3. 只有url,测试人员可以通过url路径传参之类的切换账号,方便测试


针对业务来说,页面的加载流程应该是这样的:


screenshot-20211210-211914.png


不同场景做不同的操作

原生

通过持久化工具类获取用户基础信息,然后读取接口判断身份,根据身份去做不同展示,点击跳转时间也是直接的通过路由跳转


通过webview加载

通过js交互,从原生模块拿到用户基础信息(存疑,是否直接读接口?,这样避免对原生api的依赖,如果有需求修改的话可以尽量不依赖),然后读取接口判断身份,根据身份不同去做不同展示,如果是dialog之类的交互可以直接实现,如果是跳转页面之类的,可以通过js交互进行原生操作


通过url加载的

通过url的参数串获取到对应的用户id,读取接口获取用户信息,其他操作如上,但是页面没有跳转之类的交互


实现

从链接上面获取参数

比如url为:```xxx.yyy.zzz/value


要如何拿到value值?


因为项目里刚好使用了Get做状态管理,而刚好Get已经实现了这一块,世间上的事情就是这么刚好。(好像navigator2已经支持这个了,不过还没仔细看过)




  1. 配置路由表



    class RouterConf {
    static const String appIncomeArgs = '/app/inCome/:fromApp';
    static const String appIncome = '/app/inCome/';
    static List<GetPage> _getPages = [];
    static List<GetPage> get getPages {
    _getPages = [
    GetPage(name: appIncomeArgs, page: () => const StoreKeeperInComePage()),
    ];
    return _getPages;
    }
    }

    这里appIncome配置了两个路由名


    但是实际使用时以没带**:fromApp为准的,fromApp我觉得可以理解成一个占位符,也就是fromApp=value**




  2. 获取对应的value


    在base类里面定义一个bool值,在init的回调里面去做获取操作


      bool ifFromApp = false;
    Map<String, String?> _args = Get.parameters;
    if (_args.isNotEmpty && _args.containsKey('fromApp')) {
    String? _fromAppFlag = Get.parameters['fromApp'];
    if ((_fromAppFlag?.isNotEmpty ?? false)) {
    ifFromApp = _fromAppFlag == "1";
    }
    }



根据不同情景做操作

以在webview打开为例,在页面加载时通过js交互获取用户信息,拿到用户信息后替换cache类里缓存的id,token之类的,因为拦截器里面会读取这些值用于拼接通用参数


  @override
void onReady() {
if (ifFromApp) {
initUserInfo();
js.context['getUserInfoCallback'] = getUserInfoCallback;
}else{
_loadInterface();
}

super.onReady();
}

void initUserInfo() {
js.context.callMethod("callFlutterMethod", [
json.encode({
"api": "getUserInfo",
"data": {
"name": 'getUserInfo',
"needCallback": true,
"needToken": true,
"callbackName": 'getUserInfoCallback',
"callbackArgs": 'info'
},
})
]);
}

void getUserInfoCallback(msg, info) {
Map<String, dynamic> _args = {};
if (info != null) {
if (info is String) {
_args = jsonDecode(info);
} else {
_args = info;
}
if (_args.containsKey("info")) {
dynamic _realInfo = _args['info'];
if (_realInfo is String) {
_args = jsonDecode(_realInfo);
} else {
_args = _realInfo;
}
}
if (_args.containsKey('name')) {
debugPrint(' _args[name]---------${_args['name']}');
CacheManager.instance.oName = _args['name'];
}
if (_args.containsKey('uId')) {
debugPrint(' _args[uId]---------${_args['uId']}');

CacheManager.instance.userId = _args['uId'];
}
if (_args.containsKey('oId')) {
debugPrint(' _args[oId]---------${_args['oId']}');
CacheManager.instance.userOId = _args['oId'];
}
if (_args.containsKey('token')) {
debugPrint(' _args[token]---------${_args['token']}');

CacheManager.instance.userToken = _args['token'];
}
if (_args.containsKey('headImg')) {
debugPrint(' _args[headImg]---------${_args['headImg']}');
CacheManager.instance.headImgUrl = _args['headImg'];
}
state.userName = CacheManager.instance.oName;
state.userHeaderImg = CacheManager.instance.headImgUrl;
_loadInterface();
}
}

每次都做这个判断是真的恶心,应该把这些东西抽离出来,通过中间件去实现,避免页面上耦合了这个判断。


接下去就是正常的请求接口渲染页面的流程了。


与原生的交互

这里借鉴的是这位大佬的文章 flutterweb与flutter的交互 侵删歉


唯一需要注意的就是在web项目里面增加一个js


1924616-af650f09d9300f88.png
在app里面也要做一点操作:


class NativeBridge implements JavascriptChannel {
BuildContext context; //来源于当前widget, 便于操作UI
Future<WebViewController> _controller; //当前webView 的 controller

NativeBridge(this.context, this._controller);

// api 与具体函数的映射表,可通过 _functions[key](data) 调用函数
get _functions => <String, Function>{
"getUserInfo": _getUserInfo,
"incomeDetail": _incomeDetail,
"incomeHistory": _incomeHistory,
};

@override
String get name =>
"nativeBridge"; // js 通过 nativeBridge.postMessage(msg); 调用flutter

// 处理js请求
@override
get onMessageReceived => (msg) async {
// 将收到的string数据转为json
Map<String, dynamic> message = json.decode(msg.message);
// 异步是因为有些api函数实现可能为异步,如inputText,等待UI相应
// 根据 api 字段,调用具体函数
final data = await _functions[message["api"]](message["data"]);
};

//拿token
_getUserInfo(data) async {
handlerCallback(data);
} //拿token

_incomeDetail(data) async {
Get.toNamed(RouterConf.OLD_STOREKEEPER_INCOME_LIST);
}

_incomeHistory(data) async {
Get.toNamed(RouterConf.STORE_KEEPER_INCOME_HISTORY);
}

handlerCallback(data) async {
LoginModel? _login = await UserManager.getLoginModel();
UserInfoModel? _user = await UserManager.getUserInfo();
String? _name = _user?.resultData?.organization?.organizationName;
String? _uId = _user?.resultData?.user?.userId?.toString() ?? "";
String? _oId =
_user?.resultData?.organization?.organizationId?.toString() ?? "";
String? _token = _login?.resultData?.xAUTHTOKEN;
String? _img = _user?.resultData?.user?.portraitUrl;
_img = ImgSize.getImgUrlThumbnail(_img);
Map<String, dynamic> _infos = {
"name": _name,
"uId": _uId,
"oId": _oId,
"token": _token,
"headImg": _img,
};

if (data['needCallback']) {
var args = data['callbackArgs'];
if (data['needToken']) {
args = "'${data['callbackArgs']}','${jsonEncode(_infos)}'";
}
doCallback(data['callbackName'], args);
}
}

doCallback(name, args) {
_controller.then((value) => value.evaluateJavascript("$name($args)"));
}
}

在webview里面设置channels:


 javascriptChannels: <JavascriptChannel>[
NativeBridge(context, widget.controller!.future)
].toSet(),

结尾


目前来说好像这个方案是可行的,把一个app页面通过网页跑起来确实是挺爽的,但是慢也是真的慢,


也可能因为我的服务器是丐版中的丐版,加载起来是真的慢:


1924616-5860a92dde710996.png


1924616-71b2de0857dcbc1e.png


但是挺好玩的,虽然代码很烂,但是开心就是了。


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

一步一步完成Flutter应用开发-掘金App文章详情, 悬浮,标题动画

这边文章主要将掘金app的文章详情界面的内容构造和效果的实现,也是完结篇,或者有兴趣的同学们可以谈论想要去实现哪个页面的或者功能都可以谈论起来。一起进步。一个人终究没有一群人会走得远。 标题部分 看了一下掘金app文章详情的效果,我的思路是自定义一个appba...
继续阅读 »

这边文章主要将掘金app的文章详情界面的内容构造和效果的实现,也是完结篇,或者有兴趣的同学们可以谈论想要去实现哪个页面的或者功能都可以谈论起来。一起进步。一个人终究没有一群人会走得远。


标题部分


看了一下掘金app文章详情的效果,我的思路是自定义一个appbar然后,左半部分是一个返回按钮,右部分是点击弹出分享的悬浮窗口,中间部分根据内容列表的滑动进行改变,大体思路就是通过pageView构建中间部分,禁止手势滑动,使用主动触发滑动效果,触发机制是内容滑动改变的距离
效果如下:


tutieshi_640x1343_4s.gif


import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';

class DetailPage extends StatefulWidget {
DetailPage({Key key}) : super(key: key);

@override
_DetailPageState createState() => _DetailPageState();
}

class _DetailPageState extends State<DetailPage> {
PageController controller = new PageController();
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Container(
width: Get.width,
height: 80,
decoration: BoxDecoration(color: Colors.white),
padding: EdgeInsets.only(
top: Get.context.mediaQueryPadding.top, left: 10, right: 10),
child: Row(
children: [
InkWell(
child: Icon(
CupertinoIcons.back,
color: Color.fromRGBO(38, 38, 40, 1),
),
onTap: () {
print('点击了返回');
Get.back();
},
),
Expanded(
child: PageView.builder(
itemBuilder: (context, index) {
if (index == 0) {
return Container(
alignment: Alignment.center,
child: Text('一步一步完成Flutter应用开发-掘金文章详情页面'),
);
}
return Container(
child: Row(
children: [
Container(
height: 20,
width: 20,
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(10)),
)
],
),
);
},
itemCount: 2,
scrollDirection: Axis.vertical,
physics: NeverScrollableScrollPhysics(),
controller: controller,
)),
InkWell(
child: Icon(
Icons.list,
color: Color.fromRGBO(38, 38, 40, 1),
),
onTap: () {
controller.animateTo(40,
duration: Duration(milliseconds: 500),
curve: Curves.ease);
},
),
],
),
)
],
));
}
}

基于这个思想接下来完成下面的内容


内容部分的构建


这部分想要知道掘金内容的返回格式是什么,是markdown内容或者是html内容,如果是html内容传送门,可以参考一下。
效果:


tutieshi_640x1343_5s.gif
这块主要是通过markdown形式展示详情内容引入


flutter_markdown: ^0.5.2

在上述代码中加入详情内容代码


ScrollController _scrollController = new ScrollController();

@override
void initState() {
super.initState();
_scrollController
..addListener(() {
setState(() {
if (_scrollController.offset > 88 && _scrollController.offset < 100) {
controller.animateTo(30,
duration: Duration(milliseconds: 500), curve: Curves.ease);
} else if (_scrollController.offset <= 0) {
controller.animateTo(0,
duration: Duration(milliseconds: 500), curve: Curves.ease);
}
});
});
}

@override
Widget build(BuildContext context) {
...省略上述代码
//_markdownData为内容常量
Expanded(
child: Markdown(
data: _markdownData,
controller: _scrollController,
))
}


悬浮弹窗


使用getX的Get.dialog进行展示悬浮弹窗
效果:


tutieshi_640x1343_4s.gif


代码如下:


renderItem(title) {
return Column(
children: [
Container(
height: 40,
width: 40,
margin: EdgeInsets.only(top: 10),
decoration: BoxDecoration(
color: Colors.red, borderRadius: BorderRadius.circular(20)),
),
Padding(padding: EdgeInsets.only(top: 8)),
Material(
child: Text(title),
)
],
);
}

//调用方法
Get.dialog(
UnconstrainedBox(
alignment: Alignment.bottomCenter,
child: Container(
width: Get.width,
height: Get.height * 0.6,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20))),
child: Column(
children: [
Padding(padding: EdgeInsets.only(top: 40)),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
renderItem('卡片分享'),
renderItem('微信分享'),
renderItem('朋友圈分享'),
renderItem('微博分享'),
],
),
Padding(padding: EdgeInsets.only(top: 20)),
Divider(
height: 2,
color: Colors.grey,
),
Padding(padding: EdgeInsets.only(top: 20)),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
renderItem('卡片分享'),
renderItem('微信分享'),
renderItem('朋友圈分享'),
renderItem('微博分享'),
],
),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
renderItem('卡片分享'),
renderItem('微信分享'),
renderItem('朋友圈分享'),
renderItem('微信分享'),
],
),
Expanded(child: Container()),
GestureDetector(
onTap: () {
Get.back();
},
child: Container(
margin: EdgeInsets.only(
bottom: Get
.context.mediaQueryPadding.bottom),
width: Get.width,
height: 40,
alignment: Alignment.center,
child: Material(
child: Text(
'取消',
),
),
),
),
],
)),
),
useRootNavigator: false,
useSafeArea: false);

over ~~~~


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

SwiftUI与Swift的区别

iOS
引言 SwiftUI 于 2019 年度 WWDC 全球开发者大会上发布,它是基于 Swift 建立的声明式框架。该框架可以用于 watchOS、tvOS、macOS、iOS 等平台的应用开发,等于说统一了苹果生态圈的开发工具。 本人最早开始 iOS 开发时选...
继续阅读 »

引言


SwiftUI 于 2019 年度 WWDC 全球开发者大会上发布,它是基于 Swift 建立的声明式框架。该框架可以用于 watchOS、tvOS、macOS、iOS 等平台的应用开发,等于说统一了苹果生态圈的开发工具。


本人最早开始 iOS 开发时选择了 OC(Objective-C,一种编程语言),当时 OC 不但拥有各种知名的第三方库和完善的社区支持,同时 Swift 语言本身都还在不断颠覆性改进中。但当我看了 2020 年 WWDC 关于 SwiftUI 一系列课程之后,便从 Swift 语言的学习开始,逐步了解并掌握 SwiftUI,并果断抛弃了OC,将新项目全部迁移到了 SwiftUI 框架。


SwiftUI 到底有没有苹果宣传的那么理想化?资深的 iOS 开发者是否有必要转型,以及如何转型?SwiftUI 在实际使用的过程中真实体验如何?这些问题就是这篇文章希望探讨的话题。


在最后,我会分享一些自己的学习心得和材料顺序,借开源的精神与大家共同进步。


什么是 SwiftUI


对于 Swift UI,官方的定义很好找到,也写得非常明确:



SwiftUI is a user interface toolkit that lets us design apps in a declarative way.

可以理解为 SwiftUI 就是⼀种描述式的构建 UI 的⽅式。



单单通过描述,大部分人其实很难对抽象的编程方法,和其中的改进有直观的认识。这篇文章也希望通过尽量口语化的叙述,减少专业词汇和代码的出现来降低阅读门槛,让更多人了解计算机科学,了解程序的世界。


下面是我手头正在做的一个项目,定位是一个原生全平台的电子阅读应用,正在使用 SwiftUI 构建用户界面。


为什么苹果要推出 SwiftUI


SwiftUI 的两个组成部分,Swift + UI,即是这个问题的答案。


Swift:编程语言和体验的一致性


Swift 代表苹果推出的一种现代编程语言


很多苹果用户之所以喜欢苹果的产品,其中一个原因,是不同产品之间由内而外的统一感和协调感。这一点在硬件层面的感知是最明显的,从早期开始苹果出的硬件就是「果味十足」的。即使是新品迭代或者是开发全新的品类,也一定会带有烙印很深的「果味」工业设计。


仅仅外观的统一还不够,苹果真正追求的是内外一致,也就是体验的统一。


然而工业设计可以交给自家精英团队,系统可以相互借鉴,但用户使用的软件是由广大的开发者自由创造的。让人意外的是,这一点苹果做的也很不错,与别家相比,质量精良是很多人对苹果系统上软件的印象。


为了实现这一目标苹果做了大量不为普通消费者所感知的工作。


在设计上,苹果提供了一整套不断在更新的 Human Interface Guidelines,详细规定了与视觉相关的各个方面。在完成开发,准备上架分发之前,苹果的审核团队会对每一款应用进行审核,根据 App Store Review Guidelines 的条款判断应用是否允许上架 App Store,即使是知名的应用违反规定也是说下架就下架,绝不含糊。对于不越狱的移动设备而言,App Store 是唯一可以安装应用的途径,控制了其中的准入也就等于替整个平台做了筛选。


除了控制终端以外,苹果也在想方设法增加开发者的数量,提升单个应用质量。方式也非常符合第一性思维原则——降低开发的难度。所以先有了Swift,紧接着又推出了 SwiftUI。苹果希望直接优化语言本身,并统一所有设备的开发体验,让开发者更容易上手,也更容易将心里的想法转化为运行的程序。


虽说在 2015 年推出 Swift2.0 的时候就进行了开源。但这些年 Swift 在后端或是跨平台的发展上并不是非常顺利。提起 Swift,圈内还是会被默认为特指苹果平台内使用的编程语言,地位有些类似 OC 的接班者。其实为了推广这门语言,苹果本身也做了非常多的工作,像是推出SwiftUI,基本就可以看做苹果在推广新语言的过程中一个里程碑式的节点。


SwiftUI 使用了大量 Swift 的语言特性,特别是 5.0 之后新增的特性。Swift 5.1 的很多特性几乎可以说都是为了 SwiftUI 量身定制的,比如 Opaque return types、Property Delegate 和 Function builder 等。


UI:开发的困局


在 SwiftUI 出现之前,苹果不同的设备之前的开发框架并不互通,移动端的⼯程师和桌⾯端的⼯程师需要掌握的知识,有很⼤⼀部分是差异化的。


从 iOS SDK2.0 开始,移动端的开发者⼀直使⽤ UIKit 进⾏⻚⾯部分的开发。UIKit 的思想继承了成熟的 AppKit 和MVC(Model-View-Controller)模式,作出了⼀些改进,但本质上改动不⼤。UI 包括了⽤⼾能看到的⼀切,包括静⽌的显⽰和动态的动画。


再到后来苹果推出了Apple Watch,在这块狭小屏幕上,又引入了一种新的布局方式。这种类似堆叠的逻辑,在某种程度上可以看做 SwiftUI 的未完全体。


截止此时,macOS 的开发需要使用 AppKit,iOS 的开发需要使用 UIKit,WatchOS 的开发需要使用堆叠,这种碎片化的开发体验无疑会大大增加开发者所需消耗的时间精力,也不利于构建跨平台的软件体验。


即使单看 iOS 平台,UIKit 也不是完美的开发⽅案。


UIKit 的基本思想要求 ViewController 承担绝⼤部分职责,它需要协调 model,view 以及⽤⼾交互。这带来了巨⼤的 sideeffect 以及⼤量的状态,如果没有妥善安置,它们将在 ViewController 中混杂在⼀起,同时作⽤于 view 或者逻辑,从⽽使状态管理愈发复杂,最后甚⾄不可维护⽽导致项⽬崩溃。换句话说,在不断增加新的功能和⻚⾯后,同⼀个ViewControlle r会越来越庞杂,很容易在意想不到的地⽅产⽣ bug。⽽且代码纠缠在⼀起后也会⼤⼤降低可读性,伤害到维护和协作的效率。


SwiftUI的特点


在很多地方都能看到 SwiftUI 针对现有问题的一些解决思路,而且现在的编程思想经过不断以来的演化,也一直就软件工程在开发过程中的各种问题在寻找答案。


近年来,随着编程技术和思想的进步,使用声明式或者函数式的方式来进行界面开发,已经越来越被接受并逐渐成为主流。SwiftUI 不是第一个,也不会是最后一个使用声明式界面开发的框架。


声明式的界面开发方式


在计算机科学的世界内,抽象是一个很重要的概念。从底层的二进制逻辑门,到人类可以阅读和理解的编程语言之间,是由很多层的抽象将它们关联起来的。所谓抽象,简单解释就是通过封装组件,将底层细节打包并隐藏起来,从而明确逻辑降低复杂度。就像把晶体管打包成逻辑门,以及软件工程中的函数对象。在软件开发的过程中,工程师只需负责某个具体功能的实现,而其他人则通过开放的 api 使用该功能。


与曾经的布局方式相比,声明式的页面开发无疑又加了一层抽象。


在 UIKit 框架中,界面上的每一个元素都需要开发者进行布置,期间有不少计算工作,例如长宽的改变或是屏幕可视面积的变化等。这种线性的方式被称为指令式 (imperative) 编程。以一行文字为例,放置在哪个坐标、宽度多少、在哪里换行、怎么断句、字形字号是多少、最终高度多少、是否需要缩小字号来完全显示等,这些都是开发者在制作界面时要考虑和计算妥当的问题。到了第二年,用户可能会换更大屏幕的手机,系统支持动态字体调节等新功能,此时原先的程序不进行适配就可能出现显示问题,开发者就需要回头进行程序的重新调试。


换做 SwiftUI 之后,上述的很多变量就被系统接管了。开发者要做的就是直观的告诉系统放置一个图像,上面加一行文字,右边加一个按钮。系统会根据屏幕大小、方向等自动渲染这个界面,开发者也不再需要像素级的进行计算。这被称为声明式 (declarative) 编程


对比同一个场景界面的实现


作为常用的列表视图,在UIKit中被称为 TableView,而在 SwiftUI 中被叫做 List 或 Form。同样是实现一个列表,在 SwiftUI 仅需要声明这里有一个 List 以及每个单元的内容即可。而在UIKit 中,需要使用委托代理的方式定制每个单元的内容,还需要事无巨细的设置行和组的数量、对触摸的反应、点击过程等各方面。


在我的另一个早期项目 Amos 时间志中就可以看到,为了绘制主页就需要几千行代码。


智能适配不同尺寸的屏幕


除了不同尺寸的屏幕,SwiftUI 还能根据运行平台的区别,将按钮、对话框、设置项等渲染成匹配的样式。由于声明的留白是很大的,当开发者不需要事无巨细的安排好每一个细节时,系统可操作的空间也会变大。


可以想象,假如苹果推出新品例如眼镜,或许同样的界面代码会被展示成与 iPhone 中完全不同的样式。


提高了解决问题时所需要着手的层级,这可以让开发者可以将更多的注意力集中到更重要的创意方面。


链式调用修改属性


链式调用是 Swift 语言的一种特性,就是用来使用函数方法的一种方式。可以像链条那样不断地调用函数,中间不需要断开。使用这种方式开发者可以给界面元素添加各种属性,只要愿意,同样能够事无巨细的安排页面元素的各种细节。


除了系统预制的属性可以调节外,开发者也可以进行自定义。例如将不同字体、字号、行间距、颜色等属性统合起来,可以组合成为一个叫「标题」的文字属性。之后凡是需要将某一行文字设置成标题,直接添加这个自定义的属性即可。


使用这种方式进行开发无疑能够极大的避免无意义的重复工作,更快的搭建应用界面框架。


界面元素组件化


理论上来讲,每一个复杂的视图,都是由大量简单的单元视图构成。但是函数方法可以包装起来,做到仅在有需要的时候进行调取使用。在 UIKit 框架下的页面元素解耦却不太容易,一般都是针对某种特定情境,很难进行移植。有时候可能手机横屏就会让页面元素混乱,就更别论页面元素的组件化了。


不过 SwiftUI 在布局上的特点,却可以便捷的拆分复杂的视图组件。单一的组件不仅可以自由组合,而且在苹果的任意平台上都可以使用该组件,达到跨平台的实现。


一般我个人会将视图组件区分为基础组件、布局组件和功能组件。因为 SwiftUI 的界面不再像 UIKit 那样,用 ViewController 承载各种 UIVew 控件,而是一切都是视图。这种视图的拼装方式提高了界面开发的灵活性和复用性。


响应式编程框架 Combine


在构建复杂界面的过程中,数据的流通一直是指令式编程中相当让人头疼的部分。


在 UIKit 框架下时,会配合 Target-Action 或者 protocol-delegate 模式来交换信息,使用 Key-Value Observing (KVO) 或者 Key-Value Coding (KVC) 来监测变化和读写属性。但即便开发者熟练地使用这些工具,面对日益增长的应用复杂性,掉坑里的可能性还是非常大。因为有太多需要开发者妥善处理的数据流动,例如数据改动后需要通知相关的页面进行刷新,或是让关联数据重新计算等。


像是 React Native 和 Flutter 这样的移动端跨平台方案,由于采用了声明式 UI 的编写方式和严格的数据流动方向,就能够大幅减轻开发者的思考负担。


SwiftUI 很明显也吸收了这些现代的编程思想,在另一个重量级系统框架 Combine 的协助下,实现了单一数据源的管理。


响应式编程的核心是将所有事件转化成为异步的数据流,这刚好就是 Combine 的主要功能。Combine 采用观察者模式,对应多个观察者,可以分别订阅感兴趣的内容。在 SwiftUI 的界面布局过程中,不同的 View 就是观察者,分别订阅了相关联的属性,并在数据发生变化之后就能够自动的重新渲染。


单一数据源


在 WWDC 的介绍视频中,「Source of truth」这个词反复出现,中文可以将这个词理解为单一数据源。


一直以来复杂的UI结构都会创造更为复杂的数据和逻辑管理需求,每次在用户交互,或是数据来源发生变化的时候,能否及时更新相关界面组件,不然就会引起显示问题。


但是在 SwiftUI 中,只要在属性声明时加上 @State 等关键词,就可以将该属性和界面元素联系起来,在每次数据改动后,都有机会决定是否更新视图。这样就可以将所有的属性都集中到一起进行管理和计算,也不再需要手写刷新的逻辑。因为在 SwiftUI 中,页面渲染前会将开发者描述的界面状态储存为结构体,更新界面就是将之前状态的结构体销毁,然后生成新的状态。而在绘制界面的过程中,会自动比较视图中各个属性是否有变化,如果发生变化,便会更新对应的视图,避免全局绘制和资源浪费。


使用这种方式,读和写都集中在一处,开发者就能够更好地设计数据结构,比较方便的增减类型和排查问题。而不用再考虑线程、原子状态、寻找最新数据等各种细节,再决定通知相关的界面进行刷新


与UIKit彼此相容


一般开发者学习新技术有一个最大的障碍就是原先的项目怎么办。但 SwiftUI 在这一点上做的相当不错。由于是一个新发布的框架,UI 组件并不齐全,当 SwiftUI 中并没有提供类似的功能时,就可以把 UIKit 中已有的部分进行封装,提供给 SwiftUI 使用。需要做的仅仅是遵循UIViewRepresentable协议即可。相反,在已有的项目中,也可以仅用 SwiftUI 制作一部分的 UI 界面。


当然两种代码的风格是截然不同的,但在使用上却基本没有性能的损失。到最终成品时,用户也无法分辨出两种界面框架的不同。


从开发者的⻆度看 SwiftUI


回到开头的问题:SwiftUI 到底有没有苹果宣传的那么理想化?


在 WWDC 发布 SwiftUI 时,有一句话让我印象深刻:「不论多复杂,原先布局的 99% 现在都可以使用 SwiftUI 进行构建」。当我查询 SwiftUI 是否可以承担大型项目开发时,又一次从资深开发者那里看到了这句话。


在我实际体验一段时间,并最终将一款全 SwiftUI 开发的应用上架后,认为这句话并没有什么问题,但前提是对编程这件事需要有比较基础且深入的理解。


这有点像我们学习一些优秀的第三方库时候的感受,同样是用 Xcode 写代码,有些人写出来就是白开水,而另一些人就是黑魔法。学习同样的语言特性,但由于理解的深刻程度不同,在使用时也会大不一样。仅仅依靠一些标准的自带组件无法做出一款出色的应用,即使如 UIKit 那样拥有如此丰富的组件也不行。很多时候还是要根据业务需要,或者是一些独特的脑洞做出最合适的界面。


对于个⼈开发者而言,意味着什么?


SwiftUI 的上限有多高,还要看未来一年一度的更新。但与之前的 UIKit 相比,下限被大大拉低已是不争的事实。这里的所谓下限,指的是学习的难度。由于描述性的布局方式与我们平时的阅读习惯非常接近,告诉系统在页面中间放一个图片就像告诉别人在桌子中间放一个苹果那么直观。我认识的好多 UI 设计师就通过短时间的自学掌握了 SwiftUI,并且搭建起可以直接在真机上使用的 Demo。


降低学习成本这件事是非常有意义的,不仅可以增加开发者数量,降低学习门槛,而且就学习本身而言,让初学者感受到成就感和明确学习方向,长久而言是比短时间的学习效率更重要的事情。只有开始的时候培养足够的兴趣,在后期才可能自主自发的研究更深入更困难的问题。


SwiftUI 和 Combine 大量借助了 Swift 的语法特性,尤其是 5.0 之后的几个更新,新特性就仿佛是为了这两个系统及框架量身定做的一般。虽然将 Swift 开源,但苹果无疑还是牢牢地把握着这门语言的发展。这两个框架和编程语言之间的配合默契,也仿佛让开发者体会到了软硬件一体带来的发展潜力。无论 Swift 出圈后的成果能有多少,在苹果的体系内,无疑是能够将各种消费层面的软件体验整合统一。


只要使用 SwiftUI,系统会默认支持白天和黑夜模式的自动切换;在各种尺寸的屏幕间自动适配;为任意控件添加 Haptic Touch 或是动画;在 Apple Watch 上带来独立而完整的体验;将iOS 的应用转换为 macOS 的原生应用,会以最快的速度支持第一方的各种新特性。这种对苹果硬件的深入支持是那些跨平台方案无论如何无法实现的。可以看一些采用第三方框架的知名应用,像是横屏、黑夜模式、小组件等基础的特性,到现在都迟迟没有适配。


所以 SwiftUI 对小工作室或是独立开发者来说是件好事,可以让新的想法快速落地并且接受市场验证,真正的做到敏捷开发。以这种方式在市场中的细分领域获得一席之地,也能让更多人体会到编写程序的感受,甚至是创造财富。


开源我的学习心得


在最后的部分我会分享一些自己学习 SwiftUI 的过程和介绍相关的资源,给一些也对开发感兴趣的小伙伴们做个参考。


首先要学习的是 Swift 编程语言,它与 OC 之间的差别还是挺大的,学习也没有什么捷径,直接阅读官方教程,对照着实例自己写一遍就行。国内有几个非常好的汉化网站,可以一起对照学习。基本上没有必要特意买书,反而不如直接电脑上看了就敲来的方便。



  1. 官网

  2. SwiftGG

  3. GitHub 汉化库


对语言有了大概的了解后,就可以开始对 SwiftUI 的学习,假如遇到问题可以反复回去查看之前的资料。很多被忽略的细节,或是当时初看没概念的部分,结合具体的案例就能够有比较透彻的理解。



  1. 斯坦福公开课 CS193P·2020 年春:该课程强推,我当年学习 OC 看的就是它,现在到SwiftUI了还是先看这个,系统且细致,结合案例和编程过程中的小技巧介绍,是很好的入门课程。

  2. 苹果官方 SwiftUI 课程:打开Xcode,照着官方的教学,从头到尾学着做一遍应用。

  3. Hacking with swift:这是国外一个程序员用业余时间搭建的分享网站,有大量的文章可以阅读,还有推荐初学者跟着做的「100 Days of SwiftUI」课程。

  4. 苹果官方文档:文档是必读的,虽然很多文档缺乏工程细节,但是文档涉及很多概念性的内容,你可以知道官方是怎么思考的,并且有很多具体的机制参数。我本人有一个习惯,要是工程涉及某个框架,会把相关的文档都翻译一遍。

  5. Stack Overflow:有问题查询专用,在谷歌中搜索错误代码或者关键词基本都会由该网站给答案。

  6. 阅读 SwiftUI 库的源代码。


基本到此假如能够顺利完成下来,就可以开启自己的项目。开发想要提高的关键就是亲自写代码和不断地阅读学习。初期学习的关键能力就是英语,而到后期需要的就是真正的兴趣和一些数学能力。


作者:洋仔
链接:https://juejin.cn/post/6997313521067229214
收起阅读 »

Swift:基石库——R.swift

iOS
这是我参与更文挑战的第4天,活动详情查看: 更文挑战何为基石库?做一个App无外乎两大要素:获取数据通过数据驱动页面也许你的App没有网络请求或者网络请求少,你可以不需要Alamofire。也许你的App的UI不是特别复杂,简单的xib和storyb...
继续阅读 »

这是我参与更文挑战的第4天,活动详情查看: 更文挑战

何为基石库?

做一个App无外乎两大要素:

  • 获取数据

  • 通过数据驱动页面

也许你的App没有网络请求或者网络请求少,你可以不需要Alamofire。

也许你的App的UI不是特别复杂,简单的xib和storyboard就可以胜任。

但是在当下一个App中,图片资源、字符串资源等,作为一个App开发者,你是不得不用的。

举个栗子,传统的获取一个image资源我们都是这么写:

let image = UImage(named: "saber")

这么写的最大弊端就是saber这是一个字符串硬编码,靠的的是纯手工敲打,一旦出错,界面就会出现异常。

在开发中,需要尽量避免这种硬编码,如何高效将这种硬编码的表达方式更换为高效安全的方式,就由本次的主角出场了--R.swift

统和所有的资源,以现代化的方式引用资源,项目中使用它,虽然不会让你的App上升一个层次,不过却给你的编码极度舒适。

let image = R.image.saber()

同样是Ex咖喱棒,味道却完全不同,哈哈。

基石库就是那些你无法避免不得不用的库,而R.swift恰恰就是。

R.swift

何为R,即Resource的缩写,我们先看看官方给出的一些例子:

使用R.swift函数前:

let icon = UIImage(named: "settings-icon")
let font = UIFont(name: "San Francisco", size: 42)
let color = UIColor(named: "indicator highlight")
let viewController = CustomViewController(nibName: "CustomView", bundle: nil)
let string = String(format: NSLocalizedString("welcome.withName", comment: ""), locale: NSLocale.current, "Arthur Dent")

使用R.swift函数后:

let icon = R.image.settingsIcon()
let font = R.font.sanFrancisco(size: 42)
let color = R.color.indicatorHighlight()
let viewController = CustomViewController(nib: R.nib.customView)
let string = R.string.localizable.welcomeWithName("Arthur Dent")

所有的资源都函数化后,编写过程想出错都难,特别需要注意的是最后一个涉及国际化的函数R.string.localizable.welcomeWithName("Arthur Dent"),Arthur Dent这个字符串需要自己具体制定,可以通过在做国际化时,通过info.strings进行处理。

R.swift目前支持下面这些资源文件管理:

  • Images
  • Fonts
  • Resource files
  • Colors
  • Localized strings
  • Storyboards
  • Segues
  • Nibs
  • Reusable cells

基本上覆盖了绝大多数的App中的资源管理。

安装和使用

安装

R.swift使用其他特别舒服,不过它的安装确实比其他的第三方库稍微麻烦一点,正所谓工欲善其事必先利其器,这一点麻烦是值得的。

1.添加'R.swift' 在工程的Podfile文件中,并运行pod install。 2.如下图所示。添加脚本:

image.png

3.如下图所示,移动脚本的位置,让它在Compile Sources phase和Check Pods Manifest.lock之间:

image.png

4.添加脚本:

image.png

在shell,下面这一栏添加"$PODS_ROOT/R.swift/rswift" generate "$SRCROOT/R.generated.swift"

在input Files通过+号添加$TEMP_DIR/rswift-lastrun

在Output Files通过+号添加$SRCROOT/R.generated.swift

5.运行添加R.generated.swift:

完成第4步后,进行command + B编译,然后在工程的根目录下面会找到R.generated.swift文件: image.png

将这个文件拖入到工程中,并且不要勾选Copy items if needed

image.png

这样,R.swift就安装完成啦。

使用

每一次添加了新的资源文件,就运行一次command + B一次,这样R.generated.swift文件就将新加入的资源文件更新,使用使用的时候只用通过R.来进行引用了。

更多用法,参考上面写的例子,以及官方文档

明天周末怎么破?

最怕周末更文,因为作为一个奶爸,休息都不是自己的,我争取做到不水文,至少讲一些知识点,明日继续,大家加油。


收起阅读 »

Swift:解包的正确姿势

iOS
嗯,先来一段感慨 在掘金里面看见iOS各路大神各种底层与runtime,看得就算工作了好几年的我也一脸蒙圈,于是只好从简单的入手。 文章最初发布在简书上面,有段时间了,考虑以后大部分时间都会在掘金学习,于是把文章搬过来了。稍微做了点润色与排版。 对于Swift...
继续阅读 »

嗯,先来一段感慨


在掘金里面看见iOS各路大神各种底层与runtime,看得就算工作了好几年的我也一脸蒙圈,于是只好从简单的入手。


文章最初发布在简书上面,有段时间了,考虑以后大部分时间都会在掘金学习,于是把文章搬过来了。稍微做了点润色与排版。


对于Swift学习而言,可选类型Optional是永远绕不过的坎,特别是从OC刚刚转Swift的时候,可能就会被代码行间的?与!,有的时候甚至是??搞得稀里糊涂的。


这篇文章会给各位带来我对于可选类型的一些认识以及如何进行解包,其中会涉及到Swift中if let以及guard let的使用以及思考,还有涉及OC部分的nullablenonnull两个关键字,以及一点点对两种语言的思考。


var num: Int? 它是什么类型?


在进行解包前,我们先来理解一个概念,这样可能更有利于对于解包。


首先我们来看看这样一段代码:



var num: Int?

num = 10

if num is Optional<Int> {

print("它是Optional类型")

}else {

print("它是Int类型")

}



请先暂时不要把这段代码复制到Xcode中,先自问自答,num是什么类型,是Int类型吗?


好了,你可以将这段代码复制到Xcode里去了,然后在Xcode中的if上一定会出现这样一段话:



'is' test is always true



num不是Int类,它是Optional类型


那么Optional类型是啥呢--可选类型,具体Optional是啥,Optional类型的本质实际上就是一个带有泛型参数的enum类型,各位去源码中仔细看看就能了解到,这个类型和Swift中的Result类有异曲同工之妙。


var num: Int?这是一个人Optional的声明,意思不是“我声明了一个Optional的Int值”,而是“我声明了一个Optional类型,它可能包含一个Int值,也可能什么都不包含”,也就是说实际上我们声明的是Optional类型,而不是声明了一个Int类型!


至于像Int!或者Int?这种写法,只是一种Optional类型的糖语法写法。


以此类推String?是什么类型,泛型T?是什么类型,答案各位心中已经明了吧。


正是因为num是一个可选类型。所以它才能赋值为nil, var num: Int = nil。这样是不可能赋值成功的。因为Int类型中没有nil这个概念!


这就是Swift与OC一个很大区别,在OC中我们的对象都可以赋值为nil,而在Swift中,能赋值为nil只有Optional类型!


解包的基本思路,使用if let或者guard let,而非强制解包


我们先来看一个简单的需求,虽然这个需求在实际开发中意义不太大:


我们需要从网络请求获取到的一个人的身高(cm为单位)以除以100倍,以获取m为单位的结果然后将其结果进行返回。


设计思路:


由于实际网络请求中,后台可能会返回我们的身高为空(即nil),所以在转模型的时候我们不能定义Float类型,而是定义Float?便于接受数据。


如果身高为nil,那么nil除以100是没有意义的,在编译器中Float?除以100会直接报错,那么其返回值也应该为nil,所以函数的返回值也是Float?类型


那么函数应该设计成为这个样子是这样的:



func getHeight(_ height: Float?) -> Float?



如果一般解包的话,我们的函数实现大概会写成这样:



func getHeight(_ height: Float?) -> Float? {

if height != nil {

return height! / 100

}

return nil

}



使用!进行强制解包,然后进行运算。


我想说的是使用强制解包固然没有错,不过如果在实际开发中这个height参数可能还要其他用途,那么是不是每使用一次都要进行强制解包?


强制解包是一种很危险的行为,一旦解包失败,就有崩溃的可能,也许你会说这不是有if判断,然而实际开发中,情况往往比想的复杂的多。所以安全的解包行为应该是通过if let 或者guard let来进行。



func getHeight(_ height: Float?) -> Float? {

if let unwrapedHeight = height {

return unwrapedHeight / 100

}

return nil

}



或者:



func getHeight(_ height: Float?) -> Float? {

guard let unwrapedHeight = height else {

return nil

}

return unwrapedHeight / 100

}



那么if let和guard let 你更倾向使用哪个呢?


在本例子中,其实感觉二者的差别不大,不过我个人更倾向于使用guard let。




原因如下:


在使用if let的时候其大括号类中的情况才是正常情况,而外部主体是非正常情况的返回的nil;


而在使用guard let的时候,guard let else中的大括号是异常情况,而外部主体返回的是正常情况。


对于一个以返回结果为目的的函数,函数主体展示正常返回值,而将异常抛出在判断中,这样不仅逻辑更清晰,而且更加易于代码阅读。




解包深入


有这么一个需求,从本地路径获取一个json文件,最终将其转为字典,准备进行转模型操作。


在这个过程中我们大概有这么几个步骤:


1. 获取本地路径 


func path(forResource name: String?, ofType ext: String?) -> String?


2. 将本地路径读取转为Data 


init(contentsOf url: URL, options: Data.ReadingOptions = default) throws


3. JSON序列化


class func jsonObject(with data: Data, options opt: JSONSerialization.ReadingOptions = []) throws -> Any


4. 是否可以转为字典类型


我们可以看到以上几个函数中,获取路径获取返回的路径结果是一个可选类型而转Data的方法是抛出异常,JSON序列化也是抛出异常,至于最后一步的类型转换是使用as? [Sting: Any]这样的操作


这个函数我是这来进行设计与步骤分解的:


函数的返回类型为可选类型,因为下面的4步中都有可能失败进而返回nil。


虽然有人会说第一步获取本地路径,一定是本地有的才会进行读取操作,但是作为一个严谨操作,凡事和字符串打交道的书写都是有隐患的,所以我这里还是用了guard let进行守护。


这个函数看起来很不简洁,每一个guard let 后面都跟着一个异常返回,甚至不如使用if let看着简洁


但是这么写的好处是:在调试过程中你可以明确的知道自己哪一步出错



func getDictFromLocal() -> [String: Any]? {

/// 1 获取路径

guard let path = Bundle.main.path(forResource: "test", ofType:"json") else {

return nil

}

/// 2 获取json文件里面的内容

guard let jsonData = try? Data.init(contentsOf: URL.init(fileURLWithPath: path)) else {

return nil

}

/// 3 解析json内容

guard let json = try? JSONSerialization.jsonObject(with: jsonData, options:[]) else {

return nil

}

/// 4 将Any转为Dict

guard let dict = json as? [String: Any] else {

return nil

}

return dict

}



当然,如果你要追求简洁,这么写也未尝不可,一波流带走



func getDictFromLocal() -> [String: Any]? {

guard let path = Bundle.main.path(forResource: "test", ofType:"json"),

let jsonData = try? Data.init(contentsOf: URL.init(fileURLWithPath: path)),

let json = try? JSONSerialization.jsonObject(with: jsonData, options:[]),

let dict = json as? [String: Any] else {

return nil

}

return dict

}



guard let与if let不仅可以判断一个值的解包,而且可以进行连续操作


像下面这种写法,更加追求的是结果,对于一般的调试与学习,多几个guard let进行拆分,未尝不是好事。


至于哪种用法更适合,因人而异。


可选链的解包


至于可选链的解包是完全可以一步到位,假设我们有以下这个模型。



class Person {

var phone: Phone?

}

class Phone {

var number: String?

}



Person类中有一个手机对象属性,手机类中有个手机号属性,现在我们有位小明同学,我们想知道他的手机号。


小明他不一定有手机,可能有手机而手机并没有上手机号码。



let xiaoming = Person()

guard let number = xiaoming.phone?.number else {

return

}

print(number)



这里只是抛砖引玉,更长的可选链也可以一步到位,而不必一层层进行判断,因为可选链中一旦有某个链为nil,那么就会返回nil。


nullable和nonnull


我们先来看这两个函数,PHImageManager在OC与Swift中通过PHAsset实例获取图片的例子



[[PHImageManager defaultManager] requestImageForAsset:asset targetSize:size contentMode:PHImageContentModeDefault options:options resultHandler:^(UIImage * _Nullable result, NSDictionary * _Nullable info) {

//、 非空才进行操作 注意_Nullable,Swift中即为nil,注意判断

if (result) {

}

}];




PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .default, options: options, resultHandler: { (result: UIImage?, info: [AnyHashable : Any]?) in

guard let image = result else { return }

})



在Swift中闭包返回的是两个可选类型,result: UIImage?与info: [AnyHashable : Any]? 


而在OC中返回的类型是 UIImage * _Nullable result, NSDictionary * _Nullable info


注意观察OC中返回的类型UIImage * 后面使用了_Nullable来修饰,至于Nullable这个单词是什么意思,我想稍微有点英文基础的应该一看就懂--"可以为空",这不恰恰和Swift的可选类型呼应吗?


另外还有PHFetchResult遍历这个函数,我们再来看看在OC与Swift中的表达



PHFetchResult *fetchResult;

[fetchResult enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

}];




let fetchResult: PHFetchResult

fetchResult.enumerateObjects({ (obj, index, stop) in

})



看见OC中Block中的回调使用了Nonnull来修饰,即不可能为空,不可能为nil,一定有值,对于使用这样的字符修饰的对象,我们就不必为其做健壮性判断了。


这也就是nullable与nonnull两个关键字出现的原因吧--与Swift做桥接使用以及显式的提醒对象的状态


一点点Swift与OC的语言思考


我之前写过一篇文章,是说有关于一个字符串拼接函数的


从Swift来反思OC的语法


OC函数是这样的:



- (NSString *)stringByAppendingString:(NSString *)aString;



Swift中函数是这样的:



public mutating func append(_ other: String)



仅从API来看,OC的入参是很危险的,因为类型是NSString *


那么nil也可以传入其中,而传入nil的后果就是崩掉,我觉得对于这种传入参数为nil会崩掉的函数需要特别提醒一下,应该写成这样:



- (NSString *)stringByAppendingString:(NSString * _Nonnull)aString;

/// 或者下面这样

- (NSString *)stringByAppendingString:(nonnull NSString *)aString;



以便告诉程序员,入参不能为空,不能为空,不能为空,重要的事情说三遍!!!


反观Swift就不会出现这种情况,other后面的类型为String,而不是String?,说明入参是一个非可选类型。


基于以上对于代码的严谨性,所以我才更喜欢使用Swift进行编程。


当然,Swift的严谨使得它失去部分的灵活性,OC在灵活性上比Swift卓越。


作者:season_zhu
链接:https://juejin.cn/post/6931154052776460302

收起阅读 »

iOS 无感知上拉

iOS
本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!RxSwift编写wanandroid客户端现已开源目前RxSwift编写wanandroid客户端已经开源了——项目链接。记得给个star喔!附上一张效果图片:本篇文章是从6月更...
继续阅读 »

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

RxSwift编写wanandroid客户端现已开源

目前RxSwift编写wanandroid客户端已经开源了——项目链接。记得给个star喔!

附上一张效果图片:

RPReplay_Final1625472730.2021-07-05 16_13_58.gif

本篇文章是从6月更文中热心网友的留言中进行的开发与探索:

Snip20210709_1.png

6月确实因为日更的原因,这个功能没有实现,趁着7月的时候,解决了。

废话了这么多,那么我们进入主题吧。

什么是无感知上拉加载更多

什么是无感知,这个这样理解:在网络情况正常的情况下,用户对列表进行连续的上拉时,该列表可以无卡顿不停出现新的数据。

如果要体验话,Web端很多已经做到了,比如掘金的首页,还有比如掘金iOS的App,列表都是无感知上拉加载更多。

说来惭愧,写了这久的代码,还真的没有认真思考这个功能怎么实现。

如何实现无感知上拉加载更多

我在看见这位网友留言的时候,就开始思考了。

在我看来,有下面几个着手点:

  • 列表滑动时候的是如何知道具体滑动的位置以触发接口请求,添加更多数据?

  • 从UIScrollView的代理回调中去找和scrollView的位置(contentOffset)大小(contentSize)关系密切的回调。

  • 网络上有没有比较成熟的思路?

顺着这条线,我先跑去看了UIScrollViewDelegate的源码:

public protocol UIScrollViewDelegate : NSObjectProtocol {


@available(iOS 2.0, *)
optional func scrollViewDidScroll(_ scrollView: UIScrollView) // any offset changes

@available(iOS 3.2, *)
optional func scrollViewDidZoom(_ scrollView: UIScrollView) // any zoom scale changes

.
.
.
.
.
.
/// 代码很多,这里就不放上来,给大家压力了。
}

直接上结论吧:看了一圈,反正没有和contentSize或者位置相关的回调代理。scrollViewDidScroll这个回调里面虽然可以回参scrollView,但是对于我们需要的信息还不够具体。

思考:既然UIScrollViewDelegate的代理没有现成的代理回调,自己使用KVO去监听试试?

网上的思路(一)

就在我思考的同时,我也在网络上需求实现这个功能的答案,让后看到这样一个思路:

实现方法很简单,需要用到tableView的一个代理方法,就可轻松实现。- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath就是这个方法,自定义显示cell。这个方法不太常用。但是这个方法可在每个cell将要第一次出现的时候触发。然后我们可设置当前页面第几个cell将要出现时,触发请求加载更多数据。

我看了之后,心想着,多写一个TableView的代理,总比写KVO的代码少,先试试再说,于是代码撸起:

extension SwiftCoinRankListController: UITableViewDelegate {

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let row = indexPath.row
let distance = dataSource.count - 25
print("row: \(row), distance:\(distance) ")
if row == distance {
loadMore()
}
}
}

本代码可以在开源项目中的SwiftCoinRankListController.swift文件查看具体的逻辑,其主要就是通过cell显示的个数去提前请求加载数据,然后我们看看效果:

620A94AE4920C54C6E1B85E1776AC83C.2021-07-09 17_47_45.gif

Gif可能看起来还好,我说我调试的感受:

虽然做到了上拉无感知,但是当手滑的速度比较快的时候,到底了新的数据没有回来,就会在底部等一段时间。

功能达到了,但是感受却不理想,果然还是监听的细腻程度不够。

网上的思路(二)

然后在继续的搜索中,我看到了另外一个方案:

很多时候我们上拉刷新需要提前加载新数据,这时候利用MJRefreshAutoFooter的属性triggerAutomaticallyRefreshPercent就可以实现,该属性triggerAutomaticallyRefreshPercent默认值为1,然后改成0的话划到底部就会自动刷新,改成-1的话,在快划到底部44px的时候就会自动刷新。

MJRefresh?使用MJRefreshAutoFooter,这个简单,我直接把基类的footer给替换掉就可以了,本代码可以在开源项目中的BaseTableViewController.swift文件查看:

/// 设置尾部刷新控件,更新为无感知加载更多
let footer = MJRefreshAutoFooter()
footer.triggerAutomaticallyRefreshPercent = -1
tableView.mj_footer = footer

再来看看效果:

992BC78FBAC7B8CB36A6DC679897DA21.2021-07-09 18_04_09.gif

直接说感受:

代码改动性少,编写简单,达到预期效果,爽歪歪。比方案一更丝滑,体验好。

到此,功能就实现,难道就完了?

当然,不会,我们去看看源码吧。

MJRefresh代码的追根朔源

首先我们看看MJRefreshAutoFooter.h文件:

image.png

这里有个专门的属性triggerAutomaticallyRefreshPercent去做自动刷新,那么我们去MJRefreshAutoFooter.m中去看看吧:

image.png

注意看喔,这个.m文件有一个- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change方法,并且还调用了super,从这个方法名中我们可以明显的得到当scrollView的contentOffset变化的时候进行回调的监听。,我们顺藤摸瓜,看看super是什么,会不会有新的发现:

image.png

稍微跟着一下源代码,MJRefreshAutoFooter的继承关系如下:

MJRefreshAutoFooter => MJRefreshFooter => MJRefreshComponent

所以这个super的调用我们就去MJRefreshComponent.m里面去看看吧:

image.png

通过上面的截图我们可以得到下面的一些信息与结论:

  • MJRefreshComponent是通过KVO去监听scrollView的contentOffset变化,思路上我们对齐一致了。

  • 该类并没有实现其具体方法,而是将其交由其子类去实现,这一点通过看MJRefreshComponent.h的注释可以得到:

image.png

  • MJRefreshComponent从本质上更像虚基类。

总结

如果不是掘友提出这个问题,我可能都不会太仔细的去研究这个功能,也许继续普普通通的使用一般的上拉加载更多就够了。

这次的实践,其实是从思路到寻找方法,最后再到源码阅读。

思路也许不困难,但是真正一点点实现并完善功能,每一步都并不容易,这次我也仅仅是继续使用了MJRefresh这个轮子。

想起有一天,在群里吹水看见的一张图:

云程序员来了.jpeg

灵魂拷问,直击人心,大部分时间我们不也是云程序员呢?

知行合一方能开拓新的天地。


收起阅读 »

JVM整体结构

JVM结构图类加载子系统加载连接初始化使用卸载运行时数据区域栈帧数据结构动态链接内存管理Java内存区域从逻辑上分为堆区和非堆区,Java8以前,非堆区又称为永久区,Java8以后统一为原数据区,堆区按照分代模型分为新生代和老年代,其中新生代分为Eden、so...
继续阅读 »

Java虚拟机主要负责自动内存管理、类加载与执行、主要包括执行引擎、垃圾回收器、PC寄存器、方法区、堆区、直接内存、Java虚拟机栈、本地方法栈、及类加载子系统几个部分,其中方法区与Java堆区由所有线程共享、Java虚拟机栈、本地方法栈、PC寄存器线程私有,宏观的结构如下图所示:

JVM结构图

类加载子系统

从文件或网络中加载Class信息,类信息存放于方法区,类的加载包括加载->验证->准备->解析->初始化->使用->卸载几个阶段,详细流程后续文章会介绍。

  • 加载

从文件或网络中读取类的二进制数据、将字节流表示的静态存储结构转换为方法区运行时数据结构、并于堆中生成Java对象实例,类加载器既可以使用系统提供的加载器(默认),也可以自定义类加载器。

  • 连接

连接分为验证、准备、解析3个阶段,验证阶段确保类加载的正确性、准备阶段为类的静态变量分配内存,并将其初始化为默认值、解析阶段将类中的符号引用转换为直接引用

  • 初始化

初始化阶段负责类的初始化,Java中类变量初始化的方式有2种,声明类变量时指定初始值、静态代码块指定初始化,只有类被主动使用时才会触发类的初始化,类的初始化会先初始化父类,然后再初始化子类。

  • 使用

类访问方法区内的数据结构的接口,对象是堆区的数据

  • 卸载

程序执行了System.exit()、程序正常执行结束、JVM进程异常终止等

运行时数据区域

程序从静态的源代码到编译成为JVM执行引擎可执行的字节码,会经历类的加载过程,并于内存空间开辟逻辑上的运行时数据区域,便于JVM管理,其中各数据区域如下,其中垃圾回收JVM会自动自行管理

栈帧数据结构

Java中方法的执行在虚拟机栈中执行,为每个线程所私有,每次方法的调用和返回对应栈帧的压栈和出栈,其中栈帧中保存着局部变量表、方法返回地址、操作数栈及动态链接信息。

动态链接

Java中方法执行过程中,栈帧保存方法的符号引用,通过动态链接,将解析为符号引用。

内存管理

  • 内存划分(逻辑上)

Java内存区域从逻辑上分为堆区和非堆区,Java8以前,非堆区又称为永久区,Java8以后统一为原数据区,堆区按照分代模型分为新生代和老年代,其中新生代分为Eden、so、s1,so和s1是大小相同的2块区域,生产环境可以根据具体的场景调整虚拟机内存分配比例参数,达到性能调优的效果。

堆区是JVM管理的最大内存区域,由所有线程共享,采用分代模型,堆区主要用于存放对象实例,堆可以是物理上不连续的空间,逻辑上连续即可,其中堆内存大小可以通过虚拟机参数-Xmx、-Xms指定,当堆无法继续扩展时,将抛出OOM异常。

  • 运行时实例

假设存在如下的SimpleHeap测试类,则SimpleHeap在内存中的堆区、Java栈、方法区对应的映射关系如下图所示:

public class SimpleHeap {
   /**
    * 实例变量
    */
   private int id;

   /**
    * 构造器
    *
    * @param id
    */
   public SimpleHeap(int id) {
       this.id = id;
  }

   /**
    * 实例方法
    */
   private void displayId() {
       System.out.println("id:" + id);
  }

   public static void main(String[]args){
       SimpleHeap heap1=new SimpleHeap(1);
       SimpleHeap heap2=new SimpleHeap(2);
       heap1.displayId();
       heap2.displayId();
  }

}
复制代码


同理,建设存在Person类,则创建对象实例后,内存中堆区、Java方法栈、方法区三者关系如下图:

直接内存

Java NIO库允许Java程序使用直接内存,直接内存是独立于Java堆区的一块内存区域,访问内存的速度优于堆区,出于性能考虑,针对读写频繁的场景,可以直接操作直接内存,它的大小不受Xmx参数的限制,但堆区内存和直接内存总和必须小于系统内存。

PC寄存器

线程私有空间,又称之为程序计数器,任意时刻,Java线程总在执行一个方法,正在执行的方法,我们称之为:"当前方法”,如果当前方法不是本地方法,则PC寄存器指向当前正在被执行的指令;若当前方法是本地方法,则PC寄存器的值是undefined。

垃圾回收系统

垃圾回收系统是Java虚拟机中的重要组成部分,其主要对方法区、堆区、直接内存空间进行回收,与C/C++不同,Java中所有对象的空间回收是隐式进行的,其中垃圾回收会根据GC算法自动完成内存管理。


作者:洞幺幺洞
来源:https://juejin.cn/post/7040742081236566029

收起阅读 »

Apache Log4j 漏洞(JNDI注入 CVE-2021-44228)

本周最热的事件莫过于 Log4j 漏洞,攻击者仅需向目标输入一段代码,不需要用户执行任何多余操作即可触发该漏洞,使攻击者可以远程控制用户受害者服务器,90% 以上基于 java 开发的应用平台都会受到影响。通过本文特推项目 2 你也能近距离感受这个漏洞的“魅力...
继续阅读 »



本周最热的事件莫过于 Log4j 漏洞,攻击者仅需向目标输入一段代码,不需要用户执行任何多余操作即可触发该漏洞,使攻击者可以远程控制用户受害者服务器,90% 以上基于 java 开发的应用平台都会受到影响。通过本文特推项目 2 你也能近距离感受这个漏洞的“魅力”,而特推 1 则是一个漏洞检测工具,能预防类似漏洞的发生。

除了安全相关的 2 个特推项目之外,本周 GitHub 热门项目还有高性能的 Rust 运行时项目,在你不知道用何词时给你参考词的反向词典 WantWords,还有可玩性贼高的终端模拟器 Tabby。

以下内容摘录自微博@HelloGitHub 的 GitHub Trending 及 Hacker News 热帖(简称 HN 热帖),选项标准:新发布 | 实用 | 有趣,根据项目 release 时间分类,发布时间不超过 14 day 的项目会标注 New,无该标志则说明项目 release 超过半月。由于本文篇幅有限,还有部分项目未能在本文展示,望周知 🌝

  • 本文目录

      1. 本周特推

      • 1.1 JNDI 注入测试工具:JNDI-Injection-Exploit

      • 1.2 Apache Log4j 远程代码执行:CVE-2021-44228-Apache-Log4j-Rce

      1. GitHub Trending 周榜

      • 2.1 塞尔达传说·时之笛反编译:oot

      • 2.2 终端模拟器:Tabby

      • 2.3 反向词典:WantWords

      • 2.4 CPU 性能分析和调优:perf-book

      • 2.5 高性能 Rust Runtime:Monoio

      1. 往期回顾

1. 本周特推

1.1 JNDI 注入测试工具:JNDI-Injection-Exploit

本周 star 增长数: 200+

JNDI-Injection-Exploit 并非是一个新项目,它是一个可用于 Fastjson、Jackson 等相关漏洞的验证的工具,作为 JNDI 注入利用工具,它能生成 JNDI 链接并启动后端相关服务进而检测系统。

GitHub 地址→github.com/welk1n/JNDI…

1.2 Apache Log4j 远程代码执行:CVE-2021-44228-Apache-Log4j-Rce

本周 star 增长数: 1,500+

New CVE-2021-44228-Apache-Log4j-Rce 是 Apache Log4j 远程代码执行,受影响的版本 < 2.15.0。项目开源 1 天便标星 1.5k+ 可见本次 Log4j 漏洞受关注程度。

GitHub 地址→github.com/tangxiaofen…

2. GitHub Trending 周榜

2.1 塞尔达传说·时之笛反编译:oot

本周 star 增长数:900+

oot 是一个反编译游戏塞尔达传说·时之笛的项目,目前项目处于半成品状态,会有较大的代码变更,项目从 scratch 中重新构建代码,并用游戏中发现的信息以及静态、动态分析。如果你想通过这个项目了解反编译知识,建议先保存好个人的塞尔代资产。

GitHub 地址→github.com/zeldaret/oo…

2.2 终端模拟器:Tabby

本周 star 增长数:1,800+

Tabby(曾叫 Terminus)是一个可配置、自定义程度高的终端模拟器、SSH 和串行客户端,适用于 Windows、macOS 和 Linux。它是一种替代 Windows 标准终端 conhost、PowerShell ISE、PuTTY、macOS terminal 的存在,但它不是新的 shell 也不是 MinGW 和 Cygwin 的替代品。此外,它并非一个轻量级工具,如果你注重内存,可以考虑 ConemuAlacritty

GitHub 地址→github.com/Eugeny/tabb…

2.3 反向词典:WantWords

本周 star 增长数:1,000+

WantWords 是清华大学计算机系自然语言处理实验室(THUNLP)开源的反向字词查询工具,反向词典并非是查询反义词的词典,而是基于目前网络词官广泛使用导致部分场景下,我们表达某个意思未能找到精准的用词,所以它可以让你通过想要表达的意思来找寻符合语境的词汇。你可以在线体验反向词典:wantwords.thunlp.org/ 。下图分别为项目 workflow 以及查询结果。

GitHub 地址→github.com/thunlp/Want…

2.4 CPU 性能分析和调优:perf-book

本周 star 增长数:1,300+

perf-book 是书籍《现代 CPU 的性能分析和调优》开源版本,你可以通过 python.exe export_book.py && pdflatex book.tex && bibtex book && pdflatex book.tex && pdflatex book.tex 命令导出 pdf。

GitHub 地址→github.com/dendibakh/p…

2.5 高性能 Rust Runtime:Monoio

本周 star 增长数:1,250+

New Monoio 是字节开源基于 io-uring 的 thread-per-core 模型高性能 Rust Runtime,旨在为高性能网络中间件等场景提供必要的运行时。详细项目背景可以阅读团队的文章Monoio:基于 io-uring 的高性能 Rust Runtime

GitHub 地址→github.com/bytedance/m…

3. 往期回顾

以上为 2021 年第 50 个工作周的 GitHub Trending 🎉如果你 Pick 其他好玩、实用的 GitHub 项目,记得来 HelloGitHub issue 区和我们分享下哟 🌝

最后,记得你在本文留言区留下你想看的主题 Repo(限公众号),例如:AI 换头。👀 和之前的送书活动类似,留言点赞 Top5 的小伙伴(),小鱼干会努力去找 Repo 的^^

作者:HelloGitHub
来源:https://juejin.cn/post/7040980646423953439

收起阅读 »

又到公祭日,App快速实现“哀悼主题”方案

今天是南京大屠杀死难者国家公祭日,很多APP已经变成哀悼主题,怎么实现的呢?看看这个Android方案! 4月4日,今天是国家,为了纪念和哀悼为新冠疫情做出努力和牺牲的烈士以及在新冠中逝去的同胞“举行全国哀悼”的日子! 今天10时全国停止一切娱乐活动,并默...
继续阅读 »


今天是南京大屠杀死难者国家公祭日,很多APP已经变成哀悼主题,怎么实现的呢?看看这个Android方案!

原标题:App快速实现“哀悼主题”方案

4月4日,今天是国家,为了纪念和哀悼为新冠疫情做出努力和牺牲的烈士以及在新冠中逝去的同胞“举行全国哀悼”的日子!
今天10时全国停止一切娱乐活动,并默哀3分钟!至此,各大app(爱奇艺、腾讯、虎牙...)响应号召,统一将app主题更换至“哀悼色”...,本篇文章简谈Android端的一种实现方案。

系统api:saveLayer

saveLayer可以为canvas创建一个新的透明图层,在新的图层上绘制,并不会直接绘制到屏幕上,而会在restore之后,绘制到上一个图层或者屏幕上(如果没有上一个图层)。为什么会需要一个新的图层,例如在处理xfermode的时候,原canvas上的图(包括背景)会影响src和dst的合成,这个时候,使用一个新的透明图层是一个很好的选择

public int saveLayer(@Nullable RectF bounds, @Nullable Paint paint, @Saveflags int saveFlags) {
       if (bounds == null) {
           bounds = new RectF(getClipBounds());
      }
       checkValidSaveFlags(saveFlags);
       return saveLayer(bounds.left, bounds.top, bounds.right, bounds.bottom, paint,
               ALL_SAVE_FLAG);
  }

public int saveLayer(@Nullable RectF bounds, @Nullable Paint paint) {
       return saveLayer(bounds, paint, ALL_SAVE_FLAG);
  }

ColorMatrix中setSaturation设置饱和度,给布局去色(0为灰色,1为原图)

/**
    * Set the matrix to affect the saturation of colors.
    *
    * @param sat A value of 0 maps the color to gray-scale. 1 is identity.
    */
   public void setSaturation(float sat) {
       reset();
       float[] m = mArray;

       final float invSat = 1 - sat;
       final float R = 0.213f * invSat;
       final float G = 0.715f * invSat;
       final float B = 0.072f * invSat;

       m[0] = R + sat; m[1] = G;       m[2] = B;
       m[5] = R;       m[6] = G + sat; m[7] = B;
       m[10] = R;      m[11] = G;      m[12] = B + sat;
  }

1.在view上的实践

自定义MourningImageVIew

public class MourningImageView extends AppCompatImageView {
   private Paint mPaint;
   public MourningImageView(Context context) {
       this(context,null);
  }
   public MourningImageView(Context context, @Nullable AttributeSet attrs) {
       this(context, attrs,0);
  }
   public MourningImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
       super(context, attrs, defStyleAttr);
       createPaint();
  }
   private void createPaint(){
       if(mPaint == null) {
           mPaint = new Paint();
           ColorMatrix cm = new ColorMatrix();
           cm.setSaturation(0);
           mPaint.setColorFilter(new ColorMatrixColorFilter(cm));
      }
  }
   @Override
   public void draw(Canvas canvas) {
       createPaint();
       canvas.saveLayer(null, mPaint, Canvas.ALL_SAVE_FLAG);
       super.draw(canvas);
       canvas.restore();
  }
   @Override
   protected void dispatchDraw(Canvas canvas) {
       createPaint();
       canvas.saveLayer(null, mPaint, Canvas.ALL_SAVE_FLAG);
       super.dispatchDraw(canvas);
       canvas.restore();
  }
}

和普通的ImageView对比效果:

举一反三在其他的view上也是可以生效的,实际线上项目不可能去替换所有view,接下来我们在根布局上做文章。

自定义各种MourningViewGroup实际替换效果:

xml version="1.0" encoding="utf-8"?>
<com.example.sample.MourningLinearlayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity"
   android:orientation="vertical"
   >

   <ImageView
       android:layout_width="wrap_content"
       android:src="@mipmap/ic_launcher"
       android:layout_margin="20dp"
       android:layout_height="wrap_content"/>

   <com.example.sample.MourningImageView
       android:layout_width="wrap_content"
       android:src="@mipmap/ic_launcher"
       android:layout_margin="20dp"
       android:layout_height="wrap_content"/>
com.example.sample.MourningLinearlayout>

也是可以生效的,那接下来的问题就是如何用最少的代码替换所有的页面根布局的问题了。在Activity创建的时候同时会创建一个 Window,其中包含一个 DecoView,在我们Activity中调用setContentView(),其实就是把它添加到decoView中,可以统一拦截decoview并对其做文章,降低入侵同时减少代码量。

2.Hook Window DecoView

application中注册ActivityLifecycleCallbacks,创建hook点

public class MineApplication extends Application {

   @Override
   protected void attachBaseContext(Context base) {
       super.attachBaseContext(base);
       registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
           @Override
           public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
              //获取decoview
               ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
               if(decorView!= null && decorView.getChildCount() > 0) {
                   //获取设置的contentview
                   View child = decorView.getChildAt(0);
                   //从decoview中移除contentview
                   decorView.removeView(child);
                   //创建哀悼主题布局
                   MourningFramlayout mourningFramlayout = new MourningFramlayout(activity);
                   //将contentview添加到哀悼布局中
                   mourningFramlayout.addView(child);
                   //将哀悼布局添加到decoview中
                   decorView.addView(mourningFramlayout);
              }
...
}

找个之前做的项目试试效果:

效果还行,暂时没发现什么坑,但是可能存在坑....😄😄😄

作者:code_balance
来源:https://www.jianshu.com/p/abdebc2c508e


收起阅读 »

jsonp的原理是什么?它是怎么实现跨域的?

写在前面一说到javascript的跨域,很多人第一时间想到的就是jsonp(JSON with Padding),那么这种跨域方式的实现原理是什么? 我承认我使用了很长时间,但是还是现在才知道,原来是这样....问题,如果我在 本地 访问 api.com下面...
继续阅读 »



写在前面

一说到javascript的跨域,很多人第一时间想到的就是jsonp(JSON with Padding),那么这种跨域方式的实现原理是什么? 我承认我使用了很长时间,但是还是现在才知道,原来是这样....

问题,如果我在 本地 访问 api.com下面的接口,会出现跨域请求的问题,为什么jsonp能解决这个?

  • 1、script标签是用来加载什么的?

加载js脚本的,src写上一个脚本的地址,然后浏览器就能加载啊!

  • 2、那么本地jsonp.html的script标签可以加载api.com的域名下面的脚本文件吗?

可以啊!要不那些用CDN方式优化网页加载速度的,是不可能成功的如

<script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
复制代码
  • 3、那么script能加载别的域名下面的脚本文件,与jsonp何干?

我们都知道,加载api.com的域名下面的js脚本是可以的,此时,api.com下面的js脚本文件为真实存在的静态资源。那么如果这个脚本文件是由后端语言生成的呢?实例使用 php ==>jsonp.php

<?php
echo 'alert("Hello world")';
?>
  • 4、那么问题来了,我们生成js脚本的文件为.php文件啊,怎么加载这个脚本?

答案是:我们的 script标签是能够加载.php文件的,也就是

<script type="text/javascript" src='http://localhost/jsonp.php'></script>

运行结果

以上证明,我们完全可以在服务器端生成一段脚本,然后html页面用script标签去加载然后执行脚本。

那么,我们可以在生成的脚本中执行html中定义的方法吗?我们来试一下

index.html

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="utf-8">
   <title>jsonp</title>
</head>
<body>
</body>
<script type="text/javascript">
 function execWithJsonp(para){
   console.log('我被jsonp.php中的脚本执行了,传递过来的参数是==>'+para);
   console.log(para)
}
</script>
<script type="text/javascript" src='http://localhost/jsonp.php'></script>
</html>

jsonp.php

<?php
echo "execWithJsonp({status:'ok'})";
?>

运行结果

是的,我们发现完全没问题,我们平常调用接口就是要的后端返回的数据,上面的例子,后端生成脚本时已然给我们传递了参数,拿到数据之后,我们可以做任何我们想做的事。

问题:如果后端接口这么写,那么前端所有调用这个接口的地方,岂不是都要定义一个 execWithJsonp方法?

如果页面调用两次,处理逻辑还不一样,那么我们岂不是要区分是哪一次?我希望每次访问接口调用不同的处理数据函数,每次我来告诉后端用哪个函数来处理返回的数据。 当然可以,我们可以这么做

index.html

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="utf-8">
   <title>jsonp</title>
</head>
<body>
</body>
<script type="text/javascript">
 function execWithJsonp(para){
   console.log('我被jsonp.php中的脚本执行了,我是execWithJsonp,传递过来的参数是==>'+para);
   console.log(para)
}
 function doExecJsonp(para){
   console.log('我被jsonp.php中的脚本执行了,我是doExecJsonp,传递过来的参数是==>'+para);
   console.log(para)
}
</script>
<script type="text/javascript" src='http://localhost/jsonp.php?callback=doExecJsonp'></script>
<script type="text/javascript" src='http://localhost/jsonp.php?callback=execWithJsonp'></script>
</html>

jsonp.php

<?php
 $callback=$_GET['callback'];
 echo $callback."({status:'ok'})";
?>

运行结果

说到这儿,我好像还是没说原理是啥,其实你看完上面的也就理解了

jsonp实际上就是

  • 1、前端调用后端时传递给后端数据的处理函数callback

  • 2、后端收到处理函数callback之后,进行数据库查询等操作,将后端要传递给前端的数据(一般为json格式)放入callback函数的()中并返回【实际上就是由后端动态生成一个前端可用的js脚本】,

  • 3、html页面在脚本文件加载后,自动执行脚本

  • 4、完成了整个jsonp请求。

优缺点

优点:它不像XMLHttpRequest对象实现的Ajax请求那样受到同源策略的限制;它的兼容性更好,在更加古老的浏览器中都 可以运行,不需要XMLHttpRequest或ActiveX的支持;并且在请求完毕后可以通过调用callback的方式回传结果。

缺点:它只支持GET请求而不支持POST等其它类型的HTTP请求;它只支持跨域HTTP请求这种情况,不能解决不同域的两个页面之间如何进行JavaScript调用的问题,切很明显的需要后端工程师配合才能完成。

后记,发挥自己的想象吧,看这东西该怎么操作好

 function execWithJsonp(para){
   console.log('我被jsonp.php中的脚本执行了,我是execWithJsonp,传递过来的参数是==>'+para);
   console.log(para)
}
 function doExecJsonp(para){
   console.log('我被jsonp.php中的脚本执行了,我是doExecJsonp,传递过来的参数是==>'+para);
   console.log(para)
}

doJsonp('doExecJsonp')

function doJsonp(callbackName){
 var script=document.createElement('script');
 script.src='http://localhost/jsonp.php?callback='+callbackName;
 document.body.appendChild(script);
}


作者:小枫学幽默
来源:https://juejin.cn/post/7040730836156547086

收起阅读 »

Vite为什么快呢?快在哪?说一下我自己的理解吧

前言大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心。由于这几个月使用了Vue3 + TS + Vite进行开发,并且是真的被Vite强力吸粉了!!!Vite最大的优点就是:快!!!非常快!!!说实话,使用Vite开发...
继续阅读 »



前言

大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心。

由于这几个月使用了Vue3 + TS + Vite进行开发,并且是真的被Vite强力吸粉了!!!Vite最大的优点就是:快!!!非常快!!!

说实话,使用Vite开发之后,我都有点不想回到以前Webpack的项目开发了,因为之前的项目启动项目需要30s以上,修改代码更新也需要2s以上,但是现在使用Vite,差不多启动项目只需要1s,而修改代码更新也是超级快!!!

那到底是为什么Vite可以做到这么快呢?官方给的解释,真的很官方。。所以今天我想用比较通俗易懂的话来讲讲,希望大家能看一遍就懂。

问题现状

ES模块化支持的问题

咱们都知道,以前的浏览器是不支持ES module的,比如:

// index.js

import { add } from './add.js'
import { sub } from './sub.js'
console.log(add(1, 2))
console.log(sub(1, 2))

// add.js
export const add = (a, b) => a + b

// sub.js
export const sub = (a, b) => a - b

你觉得这样的一段代码,放到浏览器能直接运行吗?答案是不行的哦。那怎么解决呢?这时候打包工具出场了,他将index.js、add.js、sub.js这三个文件打包在一个bundle.js文件里,然后在项目index.html中直接引入bundle.js,从而达到代码效果。一些打包工具,都是这么做的,例如webpack、Rollup、Parcel

项目启动与代码更新的问题

这个不用说,大家都懂:

  • 项目启动:随着项目越来越大,启动个项目可能要几分钟

  • 代码更新:随着项目越来越大,修改一小段代码,保存后都要等几秒才更新

解决问题

解决启动项目缓慢

Vite在打包的时候,将模块分成两个区域依赖源码

  • 依赖:一般是那种在开发中不会改变的JavaScript,比如组件库,或者一些较大的依赖(可能有上百个模块的库),这一部分使用esbuild来进行预构建依赖,esbuild使用的是 Go 进行编写,比 JavaScript 编写的打包器预构建依赖快 10-100倍

  • 源码:一般是哪种好修改几率比较大的文件,例如JSX、CSS、vue这些需要转换且时常会被修改编辑的文件。同时,这些文件并不是一股脑全部加载,而是可以按需加载(例如路由懒加载)。Vite会将文件转换后,以es module的方式直接交给浏览器,因为现在的浏览器大多数都直接支持es module,这使性能提高了很多,为什么呢?咱们看下面两张图:

第一张图,是以前的打包模式,就像之前举的index.js、add.js、sub.js的例子,项目启动时,需要先将所有文件打包成一个文件bundle.js,然后在html引入,这个多文件 -> bundle.js的过程是非常耗时间的。

第二张图,是Vite的打包方式,刚刚说了,Vite是直接把转换后的es module的JavaScript代码,扔给支持es module的浏览器,让浏览器自己去加载依赖,也就是把压力丢给了浏览器,从而达到了项目启动速度快的效果。

解决更新缓慢

刚刚说了,项目启动时,将模块分成依赖源码,当你更新代码时,依赖就不需要重新加载,只需要精准地找到是哪个源码的文件更新了,更新相对应的文件就行了。这样做使得更新速度非常快。

Vite 同时利用 HTTP 头来加速整个页面的重新加载(再次让浏览器为我们做更多事情):源码模块的请求会根据 304 Not Modified 进行协商缓存,而依赖模块请求则会通过 Cache-Control: max-age=31536000,immutable 进行强缓存,因此一旦被缓存它们将不需要再次请求。

生产环境

刚刚咱们说的都是开发环境,也说了,Vite在是直接把转化后的es module的JavaScript,扔给浏览器,让浏览器根据依赖关系,自己去加载依赖。

那有人就会说了,那放到生产环境时,是不是可以不打包,直接在开个Vite服务就行,反正浏览器会自己去根据依赖关系去自己加载依赖。答案是不行的,为啥呢:

  • 1、你代码是放在服务器的,过多的浏览器加载依赖肯定会引起更多的网络请求

  • 2、为了在生产环境中获得最佳的加载性能,最好还是将代码进行tree-shaking、懒加载和 chunk 分割、CSS处理,这些优化操作,目前esbuild还不怎么完善

所以Vite最后的打包是使用了Rollup


作者:Sunshine_Lin
来源:https://juejin.cn/post/7040750959764439048

收起阅读 »

轻量级安卓水印框架,支持隐形数字水印 AndroidWM

一个轻量级的 Android 图片水印框架,支持隐形水印和加密水印。 English version下载与安装Maven:<dependency>  <groupId>com.huangyz0918groupId> &n...
继续阅读 »



AndroidWM

一个轻量级的 Android 图片水印框架,支持隐形水印和加密水印。 English version

img

下载与安装

Gradle:

implementation 'com.huangyz0918:androidwm:0.1.9'

Maven:

<dependency>
 <groupId>com.huangyz0918groupId>
 <artifactId>androidwmartifactId>
 <version>0.1.9version>
 <type>pomtype>
dependency>

Lvy:

<dependency org='com.huangyz0918' name='androidwm' rev='0.1.9'>
 <artifact name='androidwm' ext='pom' >artifact>
dependency>

快速入门

新建一个水印图片

在下载并且配置好 androidwm 之后,你可以创建一个 WatermarkImage 或者是 WatermarkText 的实例,并且使用内置的诸多Set方法为创建一个水印做好准备。

    WatermarkText watermarkText = new WatermarkText(editText)
          .setPositionX(0.5)
          .setPositionY(0.5)
          .setTextColor(Color.WHITE)
          .setTextFont(R.font.champagne)
          .setTextShadow(0.1f, 5, 5, Color.BLUE)
          .setTextAlpha(150)
          .setRotation(30)
          .setTextSize(20);

对于具体定制一个文字水印或者是图片水印, 我们在接下来的文档中会仔细介绍。

当你的水印(文字或图片水印)已经准备就绪的时候,你需要一个 WatermarkBuilder来把水印画到你希望的背景图片上。 你可以通过 create 方法获取一个 WatermarkBuilder 的实例,注意,在创建这个实例的时候你需要先传入一个 Bitmap 或者是一个 Drawable 的资源 id 来获取背景图。

    WatermarkBuilder
          .create(context, backgroundBitmap)
          .loadWatermarkText(watermarkText) // use .loadWatermarkImage(watermarkImage) to load an image.
          .getWatermark()
          .setToImageView(imageView);

选择绘制模式

你可以在 WatermarkBuilder.setTileMode() 中选择是否使用铺满整图模式,默认情况下我们只会添加一个水印。

    WatermarkBuilder
          .create(this, backgroundBitmap)
          .loadWatermarkText(watermarkText)
          .setTileMode(true) // select different drawing mode.
          .getWatermark()
          .setToImageView(backgroundView);

咚! 带水印的图片已经绘制好啦:

img

获取输出图片

你可以在 WatermarkBuilder 中同时加载文字水印和图片水印。 如果你想在绘制完成之后获得带水印的结果图片,可以使用 Watermark.getOutputImage() 方法:

    Bitmap bitmap = WatermarkBuilder
          .create(this, backgroundBitmap)
          .getWatermark()
          .getOutputImage();

创建多个水印

你还可以一次性添加多个水印图片,通过创建一个WatermarkText 的列表 List<> 并且在水印构建器的方法 .loadWatermarkTexts(watermarkTexts)中把列表传入进去(图片类型水印同理):

    WatermarkBuilder
          .create(this, backgroundBitmap)
          .loadWatermarkTexts(watermarkTexts)
          .loadWatermarkImages(watermarkImages)
          .getWatermark();

加载资源

你还可以从系统的控件和资源中装载图片或者是文字资源,从而创建一个水印对象:

WatermarkText watermarkText = new WatermarkText(editText); // for a text from EditText.
WatermarkText watermarkText = new WatermarkText(textView); // for a text from TextView.
WatermarkImage watermarkImage = new WatermarkImage(imageView); // for an image from ImageView.
WatermarkImage watermarkImage = new WatermarkImage(this, R.drawable.image); // for an image from Resource.

WatermarkBuilder里面的背景图片同样可以从系统资源或者是 ImageView 中装载:

    WatermarkBuilder
          .create(this, backgroundImageView) // .create(this, R.drawable.background)
          .getWatermark()

如果在水印构建器中你既没有加载文字水印也没有加载图片水印,那么处理过后的图片将保持原样,毕竟你啥也没干 :)

隐形水印 (测试版)

androidwm 支持两种模式的隐形水印:

  • 空域 LSB 水印

  • 频域叠加水印

你可以通过WatermarkBuilder 直接构造一个隐形水印,为了选择不同的隐形方式,可以使用布尔参数 isLSB 来区分它们 (注:频域水印扔在开发中),而想要获取到构建成功的水印图片,你需要添加一个监听器:

     WatermarkBuilder
          .create(this, backgroundBitmap)
          .loadWatermarkImage(watermarkBitmap)
          .setInvisibleWMListener(true, 512, new BuildFinishListener<Bitmap>() {
               @Override
               public void onSuccess(Bitmap object) {
                   if (object != null) {
                      // do something...
                  }
              }

               @Override
               public void onFailure(String message) {
                  // do something...
              }
          });

setInvisibleWMListener 方法的第二个参数是一个整数,表示输入图片最大尺寸,有的时候,你输入的可能是一个巨大的图片,为了使计算算法更加快速,你可以选择在构建图片之前是否对图片进行缩放,如果你让这个参数为空,那么图片将以原图形式进行添加水印操作。无论如何,注意一定要保持背景图片的大小足以放得下水印图片中的信息,否则会抛出异常。

同理,检测隐形水印可以使用类WatermarkDetector,通过一个create方法获取到实例,同时传进去一张加过水印的图片,第一个布尔参数代表着水印的种类,true 代表着检测文字水印,反之则检测图形水印。

     WatermarkDetector
          .create(inputBitmap, true)
          .detect(false, new DetectFinishListener() {
               @Override
               public void onImage(Bitmap watermark) {
                   if (watermark != null) {
                        // do something...
                  }
              }

               @Override
               public void onText(String watermark) {
                   if (watermark != null) {
                       // do something...
                  }
              }

               @Override
               public void onFailure(String message) {
                      // do something...
              }
          });

LSB 隐形空域水印 Demo 动态图:

imgimg
隐形文字水印 (LSB)隐形图像水印 (LSB)

好啦!请尽情使用吧 😘

使用说明

水印位置

我们使用 WatermarkPosition 这个类的对象来控制具体水印出现的位置。

   WatermarkPosition watermarkPosition = new WatermarkPosition(double position_x, double position_y, double rotation);
  WatermarkPosition watermarkPosition = new WatermarkPosition(double position_x, double position_y);

在函数构造器中,我们可以设定水印图片的横纵坐标,如果你想在构造器中初始化一个水印旋转角度也是可以的, 水印的坐标系以背景图片的左上角为原点,横轴向右,纵轴向下。

WatermarkPosition 同时也支持动态调整水印的位置,这样你就不需要一次又一次地初始化新的位置对象了, androidwm 提供了一些方法:

     watermarkPosition
            .setPositionX(x)
            .setPositionY(y)
            .setRotation(rotation);

在全覆盖水印模式(Tile mode)下,关于水印位置的参数将会失效。

imgimg
x = y = 0, rotation = 15x = y = 0.5, rotation = -15

横纵坐标都是一个从 0 到 1 的浮点数,代表着和背景图片的相对比例。

字体水印的颜色

你可以在 WatermarkText 中设置字体水印的颜色或者是其背景颜色:

    WatermarkText watermarkText = new WatermarkText(editText)
          .setPositionX(0.5)
          .setPositionY(0.5)
          .setTextSize(30)
          .setTextAlpha(200)
          .setTextColor(Color.GREEN)
          .setBackgroundColor(Color.WHITE); // 默认背景颜色是透明的
imgimg
color = green, background color = whitecolor = green, background color = default

字体颜色的阴影和字体

你可以从软件资源中加载一种字体,也可以通过方法 setTextShadow 设置字体的阴影。

    WatermarkText watermarkText = new WatermarkText(editText)
          .setPositionX(0.5)
          .setPositionY(0.5)
          .setTextSize(40)
          .setTextAlpha(200)
          .setTextColor(Color.GREEN)
          .setTextFont(R.font.champagne)
          .setTextShadow(0.1f, 5, 5, Color.BLUE);
imgimg
font = champagneshadow = (0.1f, 5, 5, BLUE)

阴影的四个参数分别为: (blur radius, x offset, y offset, color).

字体大小和图片大小

水印字体和水印图片大小的单位是不同的:
- 字体大小和系统布局中字体大小是类似的,取决于屏幕的分辨率和背景图片的像素,您可能需要动态调整。
- 图片大小是一个从 0 到 1 的浮点数,是水印图片的宽度占背景图片宽度的比例。

imgimg
image size = 0.3text size = 40

方法列表

对于 WatermarkTextWatermarkImage 的定制化,我们提供了一些常用的方法:

方法名称备注默认值
setPosition水印的位置类 WatermarkPositionnull
setPositionX水印的横轴坐标,从背景图片左上角为(0,0)0
setPositionY水印的纵轴坐标,从背景图片左上角为(0,0)0
setRotation水印的旋转角度0
setTextColor (WatermarkText)WatermarkText 的文字颜色Color.BLACK
setTextStyle (WatermarkText)WatermarkText 的文字样式Paint.Style.FILL
setBackgroundColor (WatermarkText)WatermarkText 的背景颜色null
setTextAlpha (WatermarkText)WatermarkText 文字的透明度, 从 0 到 25550
setImageAlpha (WatermarkImage)WatermarkImage 图片的透明度, 从 0 到 25550
setTextSize (WatermarkText)WatermarkText 字体的大小,单位与系统 layout 相同20
setSize (WatermarkImage)WatermarkImage 水印图片的大小,从 0 到 1 (背景图片大小的比例)0.2
setTextFont (WatermarkText)WatermarkText 的字体default
setTextShadow (WatermarkText)WatermarkText 字体的阴影与圆角(0, 0, 0)
setImageDrawable (WatermarkImage)WatermarkImage的图片资源null

WatermarkImage 的一些基本属性和WatermarkText 的相同。

项目地址:https://github.com/huangyz0918/AndroidWM

作者:huangyz0918
来源:https://www.wanandroid.com/blog/show/2346

收起阅读 »

优秀的react框架的开源ui库 -- Pile.js

Pile.js是滴滴出行企业级前端组开发的一套基于 react 的移动端组件库,Pile.js组件库在滴滴企业级产品中极大提高了开发效率,也希望我们的产出能给广大前端开发者带来便捷。特性质量可靠 由滴滴企业级业务精简提炼而来,经历了一年多的考验,提供质量保障标...
继续阅读 »

Pile.js是滴滴出行企业级前端组开发的一套基于 react 的移动端组件库,Pile.js组件库在滴滴企业级产品中极大提高了开发效率,也希望我们的产出能给广大前端开发者带来便捷。

特性

质量可靠
由滴滴企业级业务精简提炼而来,经历了一年多的考验,提供质量保障

标准规范
代码规范严格按照eslint Airbnb编码规范,增加代码的可读性

优势

相对于同类型的移动端组件库,Pile.js有哪些优势?

组件数量多、体积小
Pile.js组件库包含52个组件,体积只有236k(未压缩),并且我们支持单个组件引用,除了常用的基础组件(比如:Button、Alert、Toast、Tip、Content等)外,我们还包含更为丰富的日期、时间、城市、车型组件,包括雷达图、环形加载、刻度尺组件等以及canvas动画图表等

样式定制
Pile.js设计规范上支持一定程度的样式定制,以满足业务和品牌上多样化的视觉需求

多语言
组件内文案提供统一的国际化支持,配置LocaleProvider组件,运用React的context特性,只需在应用外围包裹一次即可全局生效。

啰嗦一句,如果你有兴趣,不妨也参与到这个项目中来。

项目地址:https://github.com/didi/pile.js

Pile Issues:https://github.com/didi/pile.js/issues

文档: https://didi.github.io/pile.js/docs/

demo: https://didi.github.io/pile.js/demo/#/?_k=klfvmd

组件分类

作者:闫森
来源: https://www.cnblogs.com/yansen/p/9083173.html


收起阅读 »

又到年会抽奖的时候,这是你要的抽奖程序

原标题:公司年会用了我的抽奖程序,然后我中奖了…… 这是我去年写的代码和文章,眼看又到年底抽奖季了,翻出来洗洗还能再用背景临近年末,又到了各大公司举办年会的时候了。对于年会,大家最关心的应该就是抽奖了吧?虽然中奖概率通常不高,但总归是个机会,期待一下也是好...
继续阅读 »

原标题:公司年会用了我的抽奖程序,然后我中奖了……

这是我去年写的代码和文章,眼看又到年底抽奖季了,翻出来洗洗还能再用

背景

临近年末,又到了各大公司举办年会的时候了。对于年会,大家最关心的应该就是抽奖了吧?虽然中奖概率通常不高,但总归是个机会,期待一下也是好的。

最近,我们部门举办了年会,也有抽奖环节。临近年会的前几天,Boss 突然找到我,说要做一个抽奖程序,部门年会要用。我当时都懵了:就三天时间,万一做的程序有bug,岂不是要被现场百十号人的唾沫给淹死?没办法,Boss 看起来对我很有信心,我也只能硬着头皮上了。

需求

  1. 要一个设置页面,包括设置奖项、参与人员名单等。

  2. 如果单个奖项中奖人数过多,可分批抽取,每批人数可设置。

  3. 默认按奖项顺序抽奖,也可选定某个奖项开始。

  4. 可删除没到场的中奖者,同时可再次抽取以作替补。

  5. 可在任意奖项之间切换,可查中奖记录名单

  6. 支持撤销当前轮次的抽奖结果,重新抽取。

实现

身为Web前端开发,自然想到用Web技术来实现。本着不重复造轮子的原则,首先求助Google,Github。搜了一圈好像没有找到特别好用的程序能直接用的。后来看到一个Github上的一个项目,用 TagCanvas 做的抽奖程序,界面挺好,就是逻辑有问题,点几次就崩溃了。代码是不能拿来用了,标签云这种抽奖形式倒是可以借鉴。于是找来文档看了下基本用法,很快就集成到页面里了。

由于设置页面涉及多种交互,纯手写太费时间了,直接用框架。平时 Element UI 用得比较多,自然就用它了。考虑到年会现场可能没有网络,就把框架相关的JS和CSS都下载到本地,直接引用。为了快速开发,也没搭建webpack构建工具了,直接在浏览器里引入JS。

    <link rel="stylesheet" href="css/reset.css" />
  <link
    rel="stylesheet"
    href="js/element-ui@2.4.11/lib/theme-chalk/index.css"
  />
  <script src="js/polyfill.min.js"></script>
  <script src="js/vue.min.js"></script>
  <script src="js/element-ui@2.4.11/lib/index.js"></script>
  <script src="js/member.js"></script>
1.先设计数据结构。 奖项列表 awards
[{
  "name": "二等奖",
  "count": 25,
  "award": "办公室一日游"
}, {
  "name": "一等奖",
  "count": 10,
  "award": "BMW X5"
}, {
  "name": "特等奖",
  "count": 1,
  "award": "深圳湾一号"
}]
2.参与人列表 members
[{
"id": 1,
"name": "张三"
}, {
"id": 2,
"name": "李四"
}]
3.待抽奖人员列表players,是members 的子集
[{
"id": 1,
"name": "张三"
}]
4.抽奖结果列表result,按奖项顺序索引
[[{
  "id": 1,
  "name": "张三"
}], [{
  "id": 2,
  "name": "李四"
}]]
5.设置页面 包括奖项设置和参与人员列表。

6.抽奖页面

具体代码可以去我的Github项目 查看,方便的话可以点个 star。也可以现在体验一下。由于时间仓促,代码写得比较将就。

年会当天抽中了四等奖:1000元购物卡。我是不是该庆幸自己没中特等奖……

作者:KaysonLi
来源:https://juejin.cn/post/6844904033652572174



收起阅读 »

Hi~ 这将是一个通用的新手引导解决方案

本组件已开源,源码可见:github.com/bytedance/g…组件背景不管是老用户还是新用户,在产品发布新版本、有新功能上线、或是现有功能更新的场景下,都需要一定的指导。功能引导组件就是互联网产品中的指示牌,它旨在带领用户参观产品,帮助用户熟悉新的界面...
继续阅读 »



本组件已开源,源码可见:github.com/bytedance/g…

组件背景

不管是老用户还是新用户,在产品发布新版本、有新功能上线、或是现有功能更新的场景下,都需要一定的指导。功能引导组件就是互联网产品中的指示牌,它旨在带领用户参观产品,帮助用户熟悉新的界面、交互与功能。与 FAQs、产品介绍视频、使用手册、以及 UI 组件帮助信息不同的是,功能引导组件与产品 UI 融合为一体,不会给用户割裂的交互感受,并且不需要用户主动进行触发操作,就会展示在用户眼前。

图片比文字更加具象,以下是两种典型的新手引导组件,你是不是一看就明白功能引导组件是什么了呢?

img

img

功能简介

分步引导

Guide 组件以分步引导为核心,像指路牌一样,一节一节地引导用户从起点到终点。这种引导适用于交互流程较长的新功能,或是界面比较复杂的产品。它带领用户体验了完整的操作链路,并快速地了解各个功能点的位置。

img

img

呈现方式

蒙层模式

顾名思义,蒙层引导是指在产品上用一个半透明的黑色进行遮罩,蒙层上方对界面进行高亮,旁边配以弹窗进行讲解。这种引导方式阻断了用户与界面的交互,让用户的注意力聚焦在所圈注的功能点上,不被其他元素所干扰。

img

弹窗模式

很多场景下,为了不干扰用户,我们并不想使用蒙层。这时,我们可以使用无蒙层模式,即在功能点旁边弹出一个简单的窗口引导。

img

精准定位

初始定位

Guide 提供了 12 种对齐方式,将弹窗引导加载到所选择的元素上。同时,还允许自定义横纵向偏差值,对弹窗的位置进行调整。下图分别展示了定位为 top-left 和 right-bottom 的弹窗:

img

img

并且当用户缩放或者滚动页面时,弹窗的定位依然是准确的。

自动滚动

在很多情境中,我们都需要对距离较远的几个页面元素进行功能说明,串联成一个完整的引导路径。当下一步要圈注的功能点不在用户视野中时,Guide 会自动滚动页面至合适的位置,并弹出引导窗口。

1.gif

键盘操作

当 Guide 引导组件弹出时,我们希望用户的注意力被完全吸引过来。为了让使用辅助阅读器的用户也能够感知到 Guide 的出现,我们将页面焦点移动到弹窗上,并且让弹窗里的每一个可读元素都能够聚焦。同时,用户可以用键盘(tab 或 tab+shift)依次聚焦弹窗里的内容,也可以按 escape 键退出引导。

下图中,用户用 tab 键在弹窗中移动焦点,被聚焦的元素用虚线框标识出来。当聚焦到“下一步”按钮时,敲击 shift 键,便可跳至下一步引导。

2.gif

技术实现

总体流程

在展示组件的步骤前我们会先判断是否过期,判断是否过期的标准有两个:一个是该引导组件在localStorage中存储唯一 key 是否为 true,为 true 则为该组件步骤执行完毕。第二个是组件接收一个props.expireDate,如果当前时间大于expireDate则代表组件已经过期则不会继续展示。

img

当组件没有过期时,会展示传入的props.steps相应的内容,steps 结构如下:

interface Step {
   selector: string;
   title: string;
   content: React.Element | string;
   placement: 'top' | 'bottom' | 'left' | 'right'
       | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right',
   offset: Record<'top' | 'bottom' | 'left' | 'right', number>
}

const steps = Step[]

根据 step.selector 获取高亮元素,再根据 step.placement 将弹窗展示到高亮元素相关的具体位置。点击下一步会按序展示下个 step,当所有步骤展示完毕之后我们会将该引导组件在 localStorage 中存储唯一 key 置为 true,下次进来将不再展示。

下面来看看引导组件的具体细节实现吧。

蒙层模式

当前的引导组件支持有无蒙层两种模式,有蒙层的展示效果如下图所示。

img

蒙层很好实现,就是一个撑满屏幕的 div,但是我们怎么才能让它做到高亮出中间的 selector 元素并且还支持圆角呢?🤔 ,真相只有一个,那就是—— border-width

img

我们拿到了 selector 元素的offsetTop, offsetRight, offsetBottom, offsetLeft,并相应地设置为高亮框的border-width,再把border-color设置为灰色,一个带有高亮框的蒙层就实现啦!在给这个高亮框 div 加个pseudo-element ::after 来赋予它 border-radius,完美!

弹窗的定位

用户使用 Guide 时,传入了步骤信息,每一步都包括了所要进行引导说明的界面元素的 CSS 选择器。我们将所要标注的元素叫做“锚元素”。Guide 需要根据锚元素的位置信息,准确地定位弹窗。

每一个 HTML 元素都有一个只读属性 offsetParent,它指向最近的(指包含层级上的最近)包含该元素的定位元素或者最近的 table,td,th,body元素。每个元素都是根据它的 offsetParent 元素进行定位的。比如说,一个 absolute 定位的元素,是根据它最近的、非 static 定位的上级元素进行偏移的,这个上级元素,就是其的 offsetParent。

所以我们想到将弹窗元素放进锚元素的 offsetParent 中,再对其位置进行调整。同时,为了不让锚元素 offsetParent 中的其它元素产生位移,我们设定弹窗元素为 absolute 绝对定位。

定位步骤

弹窗的定位计算流程大致如下:

img

步骤 1. 得到锚元素

通过传给 Guide 的步骤信息中的 selector,即 CSS selector,我们可以由下述代码拿到锚元素:

const anchor = document.querySelector(selector);

如何拿到 anchor 的 offsetParent 呢?这一步其实并没有想象中那么简单。下面我们就来详细地讲一讲这一步吧。

步骤 2. 获取 offsetParent

一般来说,拿到锚元素的 offsetParent,也只需要简单的一行代码:

const parent = anchor.offsetParent;

但是这行代码并不能涵盖所有的场景,我们需要考虑一些特殊的情况。

场景一: 锚元素为 fixed 定位

并不是所有的 HTMLElement 都有 offsetParent 属性。当锚元素为 fixed 定位时,其 offsetParent 返回 null。这时,我们就需要使用其 包含块(containing block) 代替 offsetParent 了。

包含块是什么呢?大多数情况下,包含块就是这个元素最近的祖先块元素的内容区,但也不是总是这样。一个元素的包含块是由其 position 属性决定的。

  • 如果 position 属性是 fixed,包含块通常是 document.documentElement

  • 如果 position 属性是 fixed,包含块也可能是由满足以下条件的最近父级元素的内边距区的边缘组成的:

    • transformperspective的值不是none

    • will-change 的值是 transformperspective

    • filter 的值不是 nonewill-change 的值是 filter(只在 Firefox 下生效).

    • contain 的值是 paint (例如: contain: paint;)

因此,我们可以从锚元素开始,递归地向上寻找符合上述条件的父级元素,如果找不到,那么就返回 document.documentElement

下面是 Guide 中用来寻找包含块的代码:

const getContainingBlock = node => {
 let currentNode = getDocument(node).documentElement;

 while (
   isHTMLElement(currentNode) &&
   !['html', 'body'].includes(getNodeName(currentNode))
) {
   const css = getComputedStyle(currentNode);

   if (
     css.transform !== 'none' ||
     css.perspective !== 'none' ||
    (css.willChange && css.willChange !== 'auto')
  ) {
     return currentNode;
  }
   currentNode = currentNode.parentNode;
}

 return currentNode;
};
场景二:在 iframe 中使用 Guide

在 Guide 的代码中,我们常常用到 window 对象。比如说,我们需要在 window 对象上调用 getComputedStyle()获取元素的样式,我们还需要 window 对象作为元素 offsetParent 的兜底。但是我们并不能直接使用 window 对象,为什么呢?这时,我们需要考虑 iframe 的情况。

想象一下,如果我们在一个内嵌了 iframe 的应用中使用 Guide 组件,Guide 组件代码在 iframe 外面,而被引导的功能点在 iframe 里面,那么在使用 Window 对象提供的方法是,我们一定是想在所圈注的功能点所在的 Window 对象上进行调用,而非当前代码运行的 Window。

因此,我们通过下面的 getWindow 方法,确保拿到的是参数 node 所在的 Window。

// Get the window object using this function rather then simply use `window` because
// there are cases where the window object we are seeking to reference is not in
// the same window scope as the code we are running. (https://stackoverflow.com/a/37638629)
const getWindow = node => {
 // if node is not the window object
 if (node.toString() !== '[object Window]') {
   // get the top-level document object of the node, or null if node is a document.
   const { ownerDocument } = node;
   // get the window object associated with the document, or null if none is available.
   return ownerDocument ? ownerDocument.defaultView || window : window;
}

 return node;
};

在 line 8,我们看到一个属性 ownerDocument。如果 node 是一个 DOM Element,那么它具有一个属性 ownerDocument,此属性返回的 document 对象是在实际的 HTML 文档中的所有子节点所属的主对象。如果在文档节点自身上使用此属性,则结果是 null。当 node 为 Window 对象时,我们返回 window;当 node 为 Document 对象时,我们返回了 ownerDocument.defaultView 。这样,getWindow 函数便涵盖了参数 node 的所有可能性。

步骤 3. 挂载弹窗

如下代码所示,我们常常遇到的使用场景是,在组件 A 中渲染 Guide,让其去标注的元素却在组件 B、组件 C 中。

 // 组件A
const A = props => (
   <>
       <Guide
           steps={[
              {
                   ......
                   selector: '#btn1'
              },
              {
                   ......
                   selector: '#btn2'
              },
              {
                   ......
                   selector: '#btn3'
              }
          ]}
       />
       <button id="btn1">Button 1</button>
   </>
)

// 组件B
const B = props => (<button id="btn2">Button 2</button>)

// 组件C
const C = props => (<button id="btn3">Button 3</button>)

上述代码中,Guide 会自然而然地渲染在 A 组件 DOM 结构下,我们怎样将其挂载到组件 B、C 的 offsetParent 中呢?这时候就要给大家介绍一下强大却少为人知的 React Portals 了。

React Portals

当我们需要把一个组件渲染到其父节点所在的 DOM 树结构之外时, 我们首先应该考虑使用 React Portals。Portals 最适用于这种需要将子节点从视觉上渲染到其父节点之外的场景了,在 Antd 的 Modal、Popover、Tooltip 组件实现中,我们也可以看到 Portal 的应用。

我们使用 ReactDOM.createPortal(child, container)创建一个 Portal。child 是我们要挂载的组件,container 则是 child 要挂载到的容器组件。

虽然 Portal 是渲染在其父元素 DOM 结构之外的,但是它并不会创建一个完全独立的 React DOM 树。一个 Portal 与 React 树中其它子节点相同,都可以拿到父组件的传来的 props 和 context,也都可以进行事件冒泡。

另外,与 ReactDOM.render 所创建的 React DOM 树不同,ReactDOM.createPortal 是应用在组件的 render 函数中的,因此不需要手动卸载。

在 Guide 中,每跳一步,上一步的弹窗便会卸载掉,新的弹窗会被加载到这一步要圈注的元素的 offsetParent 里。伪代码如下:

const Modal = props => (
ReactDOM.createPortal(
<div>
......
</div>,
offsetParent);
)

将弹窗渲染进 offsetParent 后,Guide 的下一步工作便是计算弹窗相对于 offsetParent 的偏移量。这一步非常复杂,并且要考虑一些特殊情况。下面就让我们就仔细地讲解这部分计算吧。

步骤 4. 偏移量计算

以一个 placement = left ,即需要在功能点左侧展示的弹窗引导为例。如果我们直接把弹窗通过 React Portal 挂载到锚元素的 offsetParent 中,并赋予其绝对定位,其位置会如下图所示——左上角与 offsetParent 的左上角对齐。

_下图中,用蓝色框表示的考拉图片是 Guide 需要标注的元素,即锚元素;红色框则标识出这个锚元素的 offsetParent 元素。

img

而我们预想的定位结果如下:

img

参考下图,将弹窗从初始位置移动至预期位置,我们需要在 y 轴上向下移动弹窗 offsetTop + h1/2 - h2/2 px。其中,h1 为锚元素的高度,h2 为弹窗的高度。

img

但是,上述计算依然忽略了一种场景,那就是当锚元素定位为 fixed 时。若锚元素定位为 fixed,那么无论锚元素所在的界面怎样滑动,锚元素相对于屏幕视口(viewport)的位置是固定的。自然,用来对 fixed 锚元素进行引导的弹窗也需要具有这些特性,即同样需要为 fixed 定位。

Arrow 实现及定位

arrowmodal 的子元素且相对于 modal 绝对定位,如下图所示有十二种展示位置,我们把十二种定位分为两类情况:

  1. 紫色的四种居中情况;

  2. 黄色的其余八种斜角。

img

对于第一类情况

箭头始终是相对弹窗边缘居中的位置,出对于 top、bottom,箭头的 right 值始终是(modal.width - arrow.diagonalWidth)/2 ,而 top 或 bottom 值始终为-arrow.diagonalWidth/2

对于 left、right,箭头的 top 值是(modal.height - arrow.diagonalWidth)/2 ,而 left 或 right 为-arrow.diagonalWidth/2

img

注:diagonalWidth为对角线宽度,getReversePosition\(placement\)为获取传入参数的 reverse 位置,top 对应 bottom,left 对应 right。

伪代码如下:

const placement = 'top' | 'bottom' | 'left' | 'right';
const diagonalWidth = 10;

const style = {
right: ['bottom', 'top'].includes(placement)
? (modal.width - diagonalWidth) / 2
: '',
top: ['left', 'right'].includes(placement)
? (modal.height - diagonalWidth) / 2
: '',
[getReversePosition(placement)]: -diagonalWidth / 2,
};

对于第二类情况

对于 A-B 的位置,通过下图可以发现,B 的位移总是固定值。比如对于 placement 值为 top-left 的弹窗,箭头 left 值总是固定的,而 bottom 值为-arrow.diagonalWidth/2

img

以下为伪代码:

const [firstPlacement, lastPlacement] = placement.split('-');
const diagonalWidth = 10;
const margin = 24;

const style = {
[lastPlacement]: margin,
[getReversePosition(placement)]: -diagonalWidth / 2,
}

Hotspot 实现及定位

引导组件支持 hotspot 功能,通过给一个 div 元素加上动画改变其 box-shadow 大小实现呼吸灯的效果,效果如下图所示,其中热点的定位是相对箭头的位置计算的,这里便不赘述了。

img

结语

在 Guide 的开发初期,我们并没有想到这样一个小组件需要考虑到以上这些技术点。可见,再小的组件,让其适用于所有场景,做到足够通用都是件难事,需要不断地尝试与反思。

作者:字节前端
来源:https://juejin.cn/post/6960493325061193735

收起阅读 »

领域驱动设计(DDD)能给前端带来什么

为什么需要 DDD在回答这个问题之前,我们先看下大部分软件都会经历的发展过程:频繁的变更带来软件质量的下降而这又是软件发展的规律导致的:软件是对真实世界的模拟,真实世界往往十分复杂人在认识真实世界的时候总有一个从简单到复杂的过程因此需求的变更是一种必然,并且总...
继续阅读 »



为什么需要 DDD

在回答这个问题之前,我们先看下大部分软件都会经历的发展过程:频繁的变更带来软件质量的下降

而这又是软件发展的规律导致的:

  • 软件是对真实世界的模拟,真实世界往往十分复杂

  • 人在认识真实世界的时候总有一个从简单到复杂的过程

  • 因此需求的变更是一种必然,并且总是由简单到复杂演变

  • 软件初期的业务逻辑非常清晰明了,慢慢变得越来越复杂

可以看到需求的不断变更和迭代导致了项目变得越来越复杂,那么问题来了,项目复杂性提高的根本原因是需求变更引起的吗?

根本原因其实是因为在需求变更过程中没有及时的进行解耦和扩展。

那么在需求变更的过程中如何进行解耦和扩展呢? DDD 发挥作用的时候来了。

什么是 DDD

DDD(领域驱动设计)的概念见维基百科:zh.wikipedia.org/wiki/\%E9\%…

可以看到领域驱动设计(domin-driven design)不同于传统的针对数据库表结构的设计,领域模型驱动设计自然是以提炼和转换业务需求中的领域知识为设计的起点。在提炼领域知识时,没有数据库的概念,亦没有服务的概念,一切围绕着业务需求而来,即:

  • 现实世界有什么事物 -> 模型中就有什么对象

  • 现实世界有什么行为 -> 模型中就有什么方法

  • 现实世界有什么关系 -> 模型中就有什么关联

在 DDD 中按照什么样的原则进行领域建模呢?

单一职责原则(Single responsibility principle)即 SRP:软件系统中每个元素只完成自己职责内的事,将其他的事交给别人去做。

上面这句话有没有什么哪里不清晰的?有,那就是“职责”两个字。职责该怎么理解?如何限定该元素的职责范围呢?这就引出了“限界上下文”的概念。

Eric Evans 用细胞来形容限界上下文,因为“细胞之所以能够存在,是因为细胞膜限定了什么在细胞内,什么在细胞外,并且确定了什么物质可以通过细胞膜。”这里,细胞代表上下文,而细胞膜代表了包裹上下文的边界。

我们需要根据业务相关性耦合的强弱程度分离的关注点对这些活动进行归类,找到不同类别之间存在的边界,这就是限界上下文的含义。上下文(Context)是业务目标,限界(Bounded)则是保护和隔离上下文的边界,避免业务目标的不单一而带来的混乱与概念的不一致。

如何 DDD

DDD 的大体流程如下:

  1. 建立统一语言

统一语言是提炼领域知识的产出物,获得统一语言就是需求分析的过程,也是团队中各个角色就系统目标、范围与具体功能达成一致的过程。

使用统一语言可以帮助我们将参与讨论的客户、领域专家与开发团队拉到同一个维度空间进行讨论,若没有达成这种一致性,那就是鸡同鸭讲,毫无沟通效率,相反还可能造成误解。因此,在沟通需求时,团队中的每个人都应使用统一语言进行交流。

一旦确定了统一语言,无论是与领域专家的讨论,还是最终的实现代码,都可以通过使用相同的术语,清晰准确地定义领域知识。重要的是,当我们建立了符合整个团队皆认同的一套统一语言后,就可以在此基础上寻找正确的领域概念,为建立领域模型提供重要参考。

举个例子,不同玩家对于英雄联盟(league of legends)的称呼不尽相同;国外玩家一般叫“League”,国内玩家有的称呼“撸啊撸”,有的称呼“LOL”等等。那么如果要开发相关产品,开发人员和客户首先需要统一对“英雄联盟”的语言模型。

  1. 事件风暴(Event Storming)

事件风暴会议是一种基于工作坊的实践方法,它可以快速发现业务领域中正在发生的事件,指导领域建模及程序开发。 它是 Alberto Brandolini 发明的一 种领域驱动设计实践方法,被广泛应用于业务流程建模和需求工程,基本思想是将软件开发人员和领域专家聚集在一起,相互学习,类似头脑风暴。

会议一般以探讨领域事件开始,从前向后梳理,以确保所有的领域事件都能被覆盖。

什么是领域事件呢?

领域事件是领域模型中非常重要的一部分,用来表示领域中发生的事件。一个领域事件将导致进一步的业务操作,在实现业务解耦的同时,还有助于形成完整的业务闭环。

领域事件可以是业务流程的一个步骤,比如投保业务缴费完成后,触发投保单转保单的动作;也可能是定时批处理过程中发生的事件,比如批处理生成季缴保费通知单,触发发送缴费邮件通知操作;或者一个事件发生后触发的后续动作,比如密码连续输错三次,触发锁定账户的动作。

  1. 进行领域建模,将各个模型分配到各个限界上下文中,构建上下文地图。

领域建模时,我们会根据场景分析过程中产生的领域对象,比如命令、事件等之间关系,找出产生命令的实体,分析实体之间的依赖关系组成聚合,为聚合划定限界上下文,建立领域模型以及模型之间的依赖。

上面我们大体了解了 DDD 的作用,概念和一般的流程,虽然前端和后端的 DDD 不尽相同,但是我们仍然可以将这种思想应用于我们的项目中。

DDD 能给前端项目带来什么

通过领域模型 (feature)组织项目结构,降低耦合度

很多通过 react 脚手架生成的项目组织结构是这样的:

-components
   component1
   component2
-actions.ts
...allActions
-reducers.ts
...allReducers

这种代码组织方式,比如 actions.ts 中的 actions 其实没有功能逻辑关系;当增加新的功能的时候,只是机械的往每个文件夹中加入对应的 component,action,reducer,而没有关心他们功能上的关系。那么这种项目的演进方向就是:

项目初期:规模小,模块关系清晰 ---> 迭代期:加入新的功能和其他元素 ---> 项目收尾:文件结构,模块依赖错综复杂。

因此我们可以通过领域模型的方式来组织代码,降低耦合度。

  1. 首先从功能角度对项目进行拆分。将业务逻辑拆分成高内聚松耦合的模块。从而对 feature 进行新增,重构,删除,重命名等变得简单 ,不会影响到其他的 feature,使项目可扩展和可维护。

  1. 再从技术角度进行拆分,可以看到 componet, routing,reducer 都来自等多个功能模块

可以看到:

  • 技术上的代码按照功能的方式组织在 feature 下面,而不是单纯通过技术角度进行区分。

  • 通常是由一个文件来管理所有的路由,随着项目的迭代,这个路由文件也会变得复杂。那么可以把路由分散在 feature 中,由每个 feature 来管理自己的路由。

通过 feature 来组织代码结构的好处是:当项目的功能越来越多时,整体复杂度不会指数级上升,而是始终保持在可控的范围之内,保持可扩展,可维护。

如何组织 componet,action,reducer

文件夹结构该如何设计?

  • 按 feature 组织组件,action 和 reducer

  • 组件和样式文件在同一级

  • Redux 放在单独的文件

  1. 每个 feature 下面分为 redux 文件夹 和 组件文件

  1. redux 文件夹下面的 action.js 只是充当 loader 的作用,负责将各个 action 引入,而没有具体的逻辑。 reducer 同理

  1. 项目的根节点还需要一个 root loader 来加载 feature 下的资源

如何组织 router

组织 router 的核心思想是把每个路由配置分发到每个 feature 自己的路由表中,那么需要:

  • 每个 feature 都有自己专属的路由配置

  • 顶层路由(页面级别的路由)通过 JSON 配置 1,然后解析 JSON 到 React Router

  1. 每个 feature 有自己的路由配置

  1. 顶层的 routerConfig 引入各个 feature 的子路由

import { App } from '../features/home';
import { PageNotFound } from '../features/common';
import homeRoute from '../features/home/route';
import commonRoute from '../features/common/route';
import examplesRoute from '../features/examples/route';

const childRoutes = [
 homeRoute,
 commonRoute,
 examplesRoute,
];

const routes = [{
   path: '/',
   componet: App,
   childRoutes: [
       ... childRoutes,
      { path:'*', name: 'Page not found', component: PageNotFound },
  ].filter( r => r.componet || (r.childRoutes && r.childRoutes.length > 0))
}]

export default routes
  1. 解析 JSON 路由到 React Router

import React from 'react';
import { Switch, Route } from 'react-router-dom';
import { ConnectedRouter } from 'connected-react-router';
import routeConfig from './common/routeConfig';

function renderRouteConfig(routes, path) {
   const children = []        // children component list
     const renderRoute = (item, routeContextPath) => {
   let newContextPath;
   if (/^\//.test(item.path)) {
     newContextPath = item.path;
  } else {
     newContextPath = `${routeContextPath}/${item.path}`;
  }
   newContextPath = newContextPath.replace(/\/+/g, '/');
   if (item.component && item.childRoutes) {
     const childRoutes = renderRouteConfigV3(item.childRoutes, newContextPath);
     children.push(
       <Route
         key={newContextPath}
         render={props => <item.component {...props}>{childRoutes}</item.component>}
         path={newContextPath}
       />,
    );
  } else if (item.component) {
     children.push(
       <Route key={newContextPath} component={item.component} path={newContextPath} exact />,
    );
  } else if (item.childRoutes) {
     item.childRoutes.forEach(r => renderRoute(r, newContextPath));
  }
};
   routes.forEach(item => renderRoute(item,path))
   return <Switch>children</Switch>
}


function Root() {
 const children = renderRouteConfig(routeConfig, '/');
 return (
     <ConnectedRouter>{children}</ConnectedRouter>
);
}

reference

Rekit:帮助创建遵循一般的最佳实践,可拓展的 Web 应用程序 rekit.js.org/


作者:字节前端
来源:https://juejin.cn/post/7007995442864586766

收起阅读 »

面试官对不起!我终于会了Promise...(一面凉经泪目)

面试题CSS 实现水平垂直居中flex的属性CSS transition的实现效果和有哪些属性CSS 实现沿Y轴旋转360度 (直接自爆了 CSS不行....麻了)好,那来点JS 基本数据类型有哪些 用什么判断数组怎么判断引用类型和基本类型的区别什么是栈?什么...
继续阅读 »

面试题

  • CSS 实现水平垂直居中
  • flex的属性
  • CSS transition的实现效果和有哪些属性
  • CSS 实现沿Y轴旋转360度 (直接自爆了 CSS不行....麻了)
  • 好,那来点JS 基本数据类型有哪些 用什么判断
  • 数组怎么判断
  • 引用类型和基本类型的区别
  • 什么是栈?什么是堆?
  • 手写 翻转字符串
  • 手写 Sum(1,2,3)的累加(argument)(我以为是柯里化,面试官笑了一下,脑筋不要这么死嘛)
  • 箭头函数和普通函数的区别(上题忘记了argument,面试官特意问这个问题提醒我,奈何基础太差救不起来了...泪目)
  • 数组去重的方法
  • 图片懒加载
  • 跨域产生的原因,同源策略是什么
  • 说说你了解的解决办法(只说了JSONP和CORS)
  • Cookie、sessionStorage、localStorage的区别
  • get 和 post 的区别 (只说了传参方式和功能不同,面试官问还有吗 其他的不知道了...)
  • 问了一下项目,react
  • 对ES6的了解 (Promise果真逃不了....)
  • let var const的区别
  • 知道Promise嘛?聊聊对Promise的理解?(说了一下Promise对象代表一个异步操作,有三种状态,状态转变为单向...)
  • 那它是为了解决什么问题的?(emmm当异步返回值又需要等待另一个异步就会嵌套回调,Promise可以解决这个回调地狱问题)
  • 那它是如何解决回调地狱的?(Promise对象内部是同步的,内部得到内部值后进行调用.then的异步操作,可以一直.then .then ...)
  • 好,你说可以一直.then .then ...那它是如何实现一直.then 的?(emmm... 这个.then链式调用就是...额这个...)
  • Promise有哪些方法 all和race区别是什么
  • 具体说一下 .catch() 和 reject (...我人麻了...)


结束环节

  • 问了面试官对CSS的理解(必须但非重要,前端的核心还是尽量一比一还原设计稿,只有写好了页面才能考虑交互)

  • 如何学习(基础是最重要的,CSS和JS要注重实践,盖房子最重要的还是地基,所有的框架源码,组件等都基于CSS和JS)

  • 曾经是如何度过这个过程的(多做项目,在项目中学习理解每个细节,再次告诫我基础的重要性)



Promise概述


Promise是ES6新增的引用类型,可以通过new来进行实例化对象。Promise内部包含着异步的操作。



new Promise(fn)




Promise.resolve(fn)



这两种方式都会返回一个 Promise 对象。



  • Promise 有三种状态: 等待态(Pending)、执行态(Fulfilled)和拒绝态(Rejected),且Promise 必须为三种状态之一只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。

  • 状态只能由 Pending 变为 Fulfilled 或由 Pending 变为 Rejected ,且状态改变之后不会在发生变化,会一直保持这个状态。

  • Pending 变为 Fulfilled 会得到一个私有value,Pending 变为 Rejected会得到一个私有reason,当Promise达到了Fulfilled或Rejected时,执行的异步代码会接收到这个value或reason。


知道了这些,我们可以得到下面的代码:


实现原理


class Promise {
constructor() {
this.state = 'pending' // 初始化 未完成状态
// 成功的值
this.value = undefined;
// 失败的原因
this.reason = undefined;
}
}

基本用法


Promise状态只能在内部进行操作,内部操作在Promise执行器函数执行。Promise必须接受一个函数作为参数,我们称该函数为执行器函数,执行器函数又包含resolve和reject两个参数,它们是两个函数。



  • resolve : 将Promise对象的状态从 Pending(进行中) 变为 Fulfilled(已成功)

  • reject : 将Promise对象的状态从 Pending(进行中) 变为 Rejected(已失败),并抛出错误。


使用栗子


let p1 = new Promise((resolve,reject) => {
resolve(value);
})
setTimeout(() => {
console.log((p1)); // Promise {<fulfilled>: undefined}
},1)

let p2 = new Promise((resolve,reject) => {
reject(reason);
})
setTimeout(() => {
console.log((p2)); // Promise {<rejected>: undefined}
},1)

实现原理

  • p1 resolve为成功,接收参数value,状态改变为fulfilled,不可再次改变。
  • p2 reject为失败,接收参数reason,状态改变为rejected,不可再次改变。
  • 如果executor执行器函数执行报错,直接执行reject。


所以得到如下代码:


class Promise{
constructor(executor){
// 初始化state为等待态
this.state = 'pending';
// 成功的值
this.value = undefined;
// 失败的原因
this.reason = undefined;
let resolve = value => {
console.log(value);
if (this.state === 'pending') {
// resolve调用后,state转化为成功态
console.log('fulfilled 状态被执行');
this.state = 'fulfilled';
// 储存成功的值
this.value = value;
}
};
let reject = reason => {
console.log(reason);
if (this.state === 'pending') {
// reject调用后,state转化为失败态
console.log('rejected 状态被执行');
this.state = 'rejected';
// 储存失败的原因
this.reason = reason;
}
};
// 如果 执行器函数 执行报错,直接执行reject
try{
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
}

检验一下上述代码咯:


class Promise{...} // 上述代码

new Promise((resolve, reject) => {
console.log(0);
setTimeout(() => {
resolve(10) // 1
// reject('JS我不爱你了') // 2
// 可能有错误
// throw new Error('是你的错') // 3
}, 1000)
})

  • 当执行代码1时输出为 0 后一秒输出 10 和 fulfilled 状态被执行
  • 当执行代码2时输出为 0 后一秒输出 我不爱你了 和 rejected 状态被执行
  • 当执行代码3时 抛出错误 是你的错

.then方法



promise.then(onFulfilled, onRejected)

  • 初始化Promise时,执行器函数已经改变了Promise的状态。且执行器函数是同步执行的。异步操作返回的数据(成功的值和失败的原因)可以交给.then处理,为Promise实例提供处理程序。
  • Promise实例生成以后,可以用then方法分别指定resolved状态rejected状态的回调函数。这两个函数onFulfilled,onRejected都是可选的,不一定要提供。如果提供,则会Promise分别进入resolved状态rejected状态时执行。
  • 而且任何传给then方法的非函数类型参数都会被静默忽略。
  • then 方法必须返回一个新的 promise 对象(实现链式调用的关键)


实现原理

  • Promise只能转换最终状态一次,所以onFulfilledonRejected两个参数的操作是互斥
  • 当状态state为fulfilled,则执行onFulfilled,传入this.value。当状态state为rejected,则执行onRejected,传入this.reason

class Promise {
constructor(executor) {
this.state = 'pending' // 初始化 未完成状态
// 成功的值
this.value = undefined;
// 失败的原因
this.reason = undefined;

// .then 立即执行后 state为pengding 把.then保存起来
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];

// 把异步任务 把结果交给 resolve
let resolve = (value) => {
if (this.state === 'pending') {
console.log('fulfilled 状态被执行');
this.value = value
this.state = 'fulfilled'
// onFulfilled 要执行一次
this.onResolvedCallbacks.forEach(fn => fn());
}
}
let reject = (reason) => {
if (this.state === 'pending') {
console.log('rejected 状态被执行');
this.reason = reason
this.state = 'rejected'
this.onRejectedCallbacks.forEach(fn => fn());
}
}
try {
executor(resolve, reject)
}
catch (e) {
reject(err)
}
}
// 一个promise解决了后(完成状态转移,把控制权交出来)
then(onFulfilled, onRejected) {
if (this.state == 'pending') {
this.onResolvedCallbacks.push(() => {
onFulfilled(this.value)
})
this.onRejectedCallbacks.push(() => {
onRejected(this.reason)
})
}
console.log('then');
// 状态为fulfilled 执行成功 传入成功后的回调 把执行权转移
if (this.state == 'fulfiiied') {
onFulfilled(this.value);
}
// 状态为rejected 执行失败 传入失败后的回调 把执行权转移
if (this.state == 'rejected') {
onRejected(this.reason)
}
}
}
let p1 = new Promise((resolve, reject) => {
console.log(0);
setTimeout(() => {
// resolve(10)
reject('JS我不爱你了')
console.log('setTimeout');
}, 1000)
}).then(null,(data) => {
console.log(data, '++++++++++');
})

0
then
rejected 状态被执行
JS我不爱你了 ++++++++++
setTimeout


当resolve在setTomeout内执行,then时state还是pending等待状态 我们就需要在then调用的时候,将成功和失败存到各自的数组,一旦reject或者resolve,就调用它们。



现可以异步实现了,但是还是不能链式调用啊?
为保证 then 函数链式调用,then 需要返回 promise 实例,再把这个promise返回的值传入下一个then中。


链式调用及后续实现源码


这部分我也不会,还没看懂。后续再更。
先贴代码:


class Promise{
constructor(executor){
this.state = 'pending';
this.value = undefined;
this.reason = undefined;
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];
let resolve = value => {
if (this.state === 'pending') {
this.state = 'fulfilled';
this.value = value;
this.onResolvedCallbacks.forEach(fn=>fn());
}
};
let reject = reason => {
if (this.state === 'pending') {
this.state = 'rejected';
this.reason = reason;
this.onRejectedCallbacks.forEach(fn=>fn());
}
};
try{
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
then(onFulfilled,onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };
let promise2 = new Promise((resolve, reject) => {
if (this.state === 'fulfilled') {
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
};
if (this.state === 'rejected') {
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
};
if (this.state === 'pending') {
this.onResolvedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0)
});
};
});
return promise2;
}
catch(fn){
return this.then(null,fn);
}
}
function resolvePromise(promise2, x, resolve, reject){
if(x === promise2){
return reject(new TypeError('Chaining cycle detected for promise'));
}
let called;
if (x != null && (typeof x === 'object' || typeof x === 'function')) {
try {
let then = x.then;
if (typeof then === 'function') {
then.call(x, y => {
if(called)return;
called = true;
resolvePromise(promise2, y, resolve, reject);
}, err => {
if(called)return;
called = true;
reject(err);
})
} else {
resolve(x);
}
} catch (e) {
if(called)return;
called = true;
reject(e);
}
} else {
resolve(x);
}
}
//resolve方法
Promise.resolve = function(val){
return new Promise((resolve,reject)=>{
resolve(val)
});
}
//reject方法
Promise.reject = function(val){
return new Promise((resolve,reject)=>{
reject(val)
});
}
//race方法
Promise.race = function(promises){
return new Promise((resolve,reject)=>{
for(let i=0;i<promises.length;i++){
promises[i].then(resolve,reject)
};
})
}
//all方法(获取所有的promise,都执行then,把结果放到数组,一起返回)
Promise.all = function(promises){
let arr = [];
let i = 0;
function processData(index,data){
arr[index] = data;
i++;
if(i == promises.length){
resolve(arr);
};
};
return new Promise((resolve,reject)=>{
for(let i=0;i<promises.length;i++){
promises[i].then(data=>{
processData(i,data);
},reject);
};
});
}

Promise的各种方法


Promise.prototype.catch()


catch 异常处理函数,处理前面回调中可能抛出的异常。只接收一个参数onRejected处理程序。它相当于调用Promise.prototype.then(null,onRejected),所以它也会返回一个新的Promise



  • 栗子


let p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(10)
}, 1000)
}).then(() => {
throw Error("1123")
}).catch((err) => {
console.log(err);
})
.then(() => {
console.log('异常捕获后可以继续.then');
})
复制代码

当第一个.then的异常被捕获后可以继续执行。


Promise.all()


Promise.all()创建的Promise会在这一组Promise全部解决后在解决。也就是说会等待所有的promise程序都返回结果之后执行后续的程序。返回一个新的Promise。



  • 栗子


let p1 = new Promise((resolve, reject) => {  
resolve('success1')
})

let p2 = new Promise((resolve, reject) => {
resolve('success1')
})
// let p3 = Promise.reject('failed3')
Promise.all([p1, p2]).then((result) => {
console.log(result) // ['success1', 'success2']

}).catch((error) => {
console.log(error)
})
// Promise.all([p1,p3,p2]).then((result) => {
// console.log(result)
// }).catch((error) => {
// console.log(error) // 'failed3'
//
// })
复制代码

有上述栗子得到,all的性质:



  • 如果所有都成功,则合成Promise的返回值就是所有子Promise的返回值数组。

  • 如果有一个失败,那么第一个失败的会把自己的理由作为合成Promise的失败理由。


Promise.race()


Promise.race()是一组集合中最先解决或最先拒绝的Promise,返回一个新的Promise。



  • 栗子


let p1 = new Promise((resolve, reject) => {  
setTimeout(() => {
resolve('success1')
},1000)
})

let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('failed2')
}, 1500)
})

Promise.race([p1, p2]).then((result) => {
console.log(result)
}).catch((error) => {
console.log(error) // 'success1'
})
复制代码

有上述栗子得到,race的性质:

无论如何,最先执行完成的,就执行相应后面的.then或者.catch。谁先以谁作为回调


总结


上面的Promise就总结到这里,讲的可能不太清楚,有兴趣的小伙伴可以看看链接呀,有什么理解也可以在下方评论区一起交流学习。


面试结束了,面试官人很好,聊的很开心,问题大概都能说上来一点,却总有关键部分忘了hhhhhh,结尾跟面试官聊了一下容易忘这个问题,哈哈哈哈他说我忘就是没学会,以后还是要多总结,多做项目...


面试可以让自己发现更多的知识盲点,从而促进自己学习,大家一起加油冲呀!!


作者:_清水
链接:https://juejin.cn/post/6952083081519955998

收起阅读 »

HashMap原理浅析及相关知识

一、初识Hashmap 作为集合Map的主要实现类;线程不安全的,效率高;存储null的key和value。 二、HashMap在Jdk7中实现原理 1、HashMap map = new HashMap() 实例化之后会在底层创建长度是16的一维数组Ent...
继续阅读 »

一、初识Hashmap


作为集合Map的主要实现类;线程不安全的,效率高;存储null的key和value。


image.png


二、HashMap在Jdk7中实现原理


1、HashMap map = new HashMap()


实例化之后会在底层创建长度是16的一维数组Entry[] table。


2、map.put(key1,value1)


调用Key1所在类的hashCode()计算key1哈希值,得到Entry数组中存放的位置                   ---比较存放位置

如果此位置为空,此时key1-value1添加成功 *情况1,添加成功*

此位置不为空(以为此位置存在一个或多个数据(以链表形式存在)),比较key1和已存在的数据的哈希值: --比较哈希值

如果key1的哈希值与存在数据哈希值都不相同,此时key1-value1添加成功 *情况2,添加成功*

如果key1的哈希值与某一存在数据(key2,value2)相同,继续调用key1类的equals(key2)方法 --equals比较

如果equals()返回false,此时key1-value1添加成功 *情况3,添加成功*

如果equals()返回true,此时value1替换value2 *情况4,更新原有key的值*

情况2和情况3状态下,key1-value1和原来的数据以链表方式存储。

添加过程中会涉及扩容,超出临界值(存放位置非空)时扩容。默认扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来。




三、HashMap在Jdk8之后实现原理


1、HashMap map = new HashMap()


底层没创建一个长度为16的数组,而是在首次调用put()方法时,底层创建长度为16的数组。


2、map.put(key1,value1)


final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;//首次put,创建长度为16的数组
if ((p = tab[i = (n - 1) & hash]) == null)// 需要插入数据位置为空。注:[i = (n - 1) & hash]找到当前key应插入的位置
tab[i] = newNode(hash, key, value, null); //*情况1*
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))//*情况4*
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//红黑树情况
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);//*情况2、3*
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))//*情况4*
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

3、map.entrySet()


返回一个Set集合


public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}

4、map.get(ket)


返回key对应的value值。


public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}

5、常见参数:


DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16


DEFAULT_LOAD_FACTOR:HashMap的默认加载因子:0.75


threshold:扩容的临界值,=容量*填充因子:16 * 0.75 => 12


TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树:8


MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量:64


四、涉及的基础知识


位运算符用来对二进制位进行操作,Java中提供了如下表所示的位运算符:位运算符中,除 ~ 以外,其余均为二元运算符。


操作数只能为整型和字符型数据。


C语言中六种位运算符:


<<左移


>>右移


| 按位或


& 按位与


~取反


^ 按位异或


左移符号<<:向左移动若干位,高位丢弃,低位补零,对于左移N位,就等于乘以2^n


带符号右移操作>>:向右移动若干位,低位进行丢弃,高位按照符号位进行填补,对于正数做右移操作时,高位补充0;负数进行右移时,高位补充1


不带符号的右移操作>>>:与右移操作类似,高位补零,低位丢弃,正数符合规律,负数不符合规律


键(key)经过hash函数得到的结果作为地址去存放当前的键值对(key-value)(这个是hashmap的存值方式),但是却发现该地址已经有人先来了,一山不容二虎,就会产生冲突。这个冲突就是hash冲突了。


简单来说:两个不同对象的hashCode相同,这种现象称为hash冲突。


HashMap的Put方法在第2、3情况添加前会产生哈希冲突,HashMap采用的链地址法(将所有哈希地址相同的都链接在同一个链表中 ,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况)解决哈希冲突。


五、相关面试问题


1、HashMap原理?


见上


2、HashMap初始化时阈值默认为12(加载因子为0.75),会使HashMap提前进行扩容,那为什么不在HashMap满的时候再进行扩容?


若加载因子越大,填满的元素越多,好处是,空间利用率高了,但冲突的机会加大了.链表长度会越来越长,查找效率降低。
反之,加载因子越小,填满的元素越少,好处是冲突的机会减小了,但空间浪费多了.表中的数据将过于稀疏(很多空间还没用,就开始扩容了)
冲突的机会越大,则查找的成本越高. 因此,必须在 "冲突的机会"与"空间利用率"之间寻找一种平衡与折衷.
这种平衡与折衷本质上是数据结构中有名的"时-空"矛盾的平衡与折衷.
如果机器内存足够,并且想要提高查询速度的话可以将加载因子设置小一点;相反如果机器内存紧张,并且对查询速度没有什么要求的话可以将加载因子设置大一点。不过一般我们都不用去设置它,让它取默认值0.75就好了。


3、什么是哈希冲突?如何解决?


4、并发集合


以下均为java.util.concurrent - Java并发工具包中的同步集合


4.1、ConcurrentHashMap 支持完全并发的检索和更新,所希望的可调整并发的哈希表。此类遵守与 Hashtable 相同的功能规范,并且包括对应于 Hashtable 的每个方法的方法版本。不过,尽管所有操作都是线程安全的,但检索操作不必锁定,并且不支持以某种防止所有访问的方式锁定整个表。此类可以通过程序完全与 Hashtable 进行互操作,这取决于其线程安全,而与其同步细节无关。


4.2、ConcurrentSkipListMap 是基于跳表的实现,也是支持key有序排列的一个key-value数据结构,在并发情况下表现很好,是一种空间换时间的实现,ConcurrentSkipListMap是基于一种乐观锁的方式去实现高并发。


4.3、ConCurrentSkipListSet (在JavaSE 6新增的)提供的功能类似于TreeSet,能够并发的访问有序的set。因为ConcurrentSkipListSet是基于“跳跃列表(skip list)”实现的,只要多个线程没有同时修改集合的同一个部分,那么在正常读、写集合的操作中不会出现竞争现象。


4.4、CopyOnWriteArrayList 是ArrayList 的一个线程安全的变形,其中所有可变操作(添加、设置,等等)都是通过对基础数组进行一次新的复制来实现的。这一般需要很大的开销,但是当遍历操作的数量大大超过可变操作的数量时,这种方法可能比其他替代方法更 有效。在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时,它也很有用。“快照”风格的迭代器方法在创建迭代器时使用了对数组状态的引用。此数组在迭代器的生存期内绝不会更改,因此不可能发生冲突,并且迭代器保证不会抛出 ConcurrentModificationException。自创建迭代器以后,迭代器就不会反映列表的添加、移除或者更改。不支持迭代器上更改元素的操作(移除、设置和添加)。这些方法将抛出 UnsupportedOperationException。


4.5、CopyOnWriteArraySet 线程安全的无序的集合,可以将它理解成线程安全的HashSet。有意思的是,CopyOnWriteArraySet和HashSet虽然都继承于共同的父类AbstractSet;但是,HashSet是通过“散列表(HashMap)”实现的,而CopyOnWriteArraySet则是通过“动态数组(CopyOnWriteArrayList)”实现的,并不是散列表。


4.6、ConcurrentLinkedQueue 是一个基于链接节点的、无界的、线程安全的队列。此队列按照 FIFO(先进先出)原则对元素进行排序,队列的头部 是队列中时间最长的元素。队列的尾部 是队列中时间最短的元素。新的元素插入到队列的尾部,队列检索操作从队列头部获得元素。当许多线程共享访问一个公共 collection 时,ConcurrentLinkedQueue 是一个恰当的选择,此队列不允许 null 元素。


注:ArrayList和HashMap是非并发集合,迭代时不能进行修改和删除操作

注:CopyOnWriteArrayList和CopyOnWriteArraySet,最适合于读操作通常大大超过写操作的情况


5、线程安全集合及实现原理?


5.1 早期线程安全的集合


Vector:作为Collection->List接口的古老实现类;线程安全的,效率低;底层使用Object[] elementData存储


HashTable:作为Map古老的实现类;线程安全的,效率低;不能存储null的key和value(Properties为其子类:常用来处理配置文件。key和value都是String类型)


5.2 Collections包装方法


Vector和HashTable被弃用后,它们被ArrayList和HashMap代替,但它们不是线程安全的,所以Collections工具类中提供了相应的包装方法把它们包装成线程安全的集合


List<E> synArrayList = Collections.synchronizedList(new ArrayList<E>());

Set<E> synHashSet = Collections.synchronizedSet(new HashSet<E>());

Map<K,V> synHashMap = Collections.synchronizedMap(new HashMap<K,V>());

...

5.3 java.util.concurrent包中的集合


ConcurrentHashMap和HashTable都是线程安全的集合,它们的不同主要是加锁粒度上的不同。HashTable的加锁方法是给每个方法加上synchronized关键字,这样锁住的是整个Table对象。而ConcurrentHashMap是更细粒度的加锁
在JDK1.8之前,ConcurrentHashMap加的是分段锁,也就是Segment锁,每个Segment含有整个table的一部分,这样不同分段之间的并发操作就互不影响
JDK1.8对此做了进一步的改进,它取消了Segment字段,直接在table元素上加锁,实现对每一行进行加锁,进一步减小了并发冲突的概率


CopyOnWriteArrayList和CopyOnWriteArraySet
它们是加了写锁的ArrayList和ArraySet,锁住的是整个对象,但读操作可以并发执行


除此之外还有ConcurrentSkipListMap、ConcurrentSkipListSet、ConcurrentLinkedQueue、ConcurrentLinkedDeque等,至于为什么没有ConcurrentArrayList,原因是无法设计一个通用的而且可以规避ArrayList的并发瓶颈的线程安全的集合类,只能锁住整个list,这用Collections里的包装类就能办到


6、HashMap和hashTable的区别?


HashMap:作为Map的主要实现类;线程不安全的,效率高;存储null的key和value


Hashtable:作为古老的实现类;线程安全的,效率低;不能存储null的key和value


7、hashCode的作用?如何重载hashCode方法?


hashCode的存在主要是用于查找的快捷性,如Hashtable,HashMap等,hashCode是用来在散列存储结构中确定对象的存储地址的;如果两个对象相同,就是适用于equals(Java.lang.Object) 方法,那么这两个对象的hashCode一定要相同;如果对象的equals方法被重写,那么对象的hashCode也尽量重写,并且产生hashCode使用的对象,一定要和equals方法中使用的一致,否则就会违反上面提到的第2点;两个对象的hashCode相同,并不一定表示两个对象就相同,也就是不一定适用于equals(java.lang.Object)方法,只能够说明这两个对象在散列存储结构中,如Hashtable,他们“存放在同一个篮子里”。


总结:再归纳一下就是hashCode是用于查找使用的,而equals是用于比较两个对象的是否相等的。


作者:求求了瘦10斤吧
链接:https://juejin.cn/post/7039596855012884510

收起阅读 »

Android论网络加载框架(Android-async-http,afinal,xUtils,Volley,okhttp,Retrofit)的特点和优缺点

一:HTTP,TCP,UDP,Socket简要介绍 1、TCP TCP简要介绍 TCP是面向连接的、传输可靠(保证数据正确性且保证数据顺序)、用于传输大量数据(流模式)、速度慢,建立连接需要开销较多(时间,系统资源)。 TCP三次握手 建立一个TCP连接时,需...
继续阅读 »

一:HTTP,TCP,UDP,Socket简要介绍


1、TCP


TCP简要介绍


TCP是面向连接的、传输可靠(保证数据正确性且保证数据顺序)、用于传输大量数据(流模式)、速度慢,建立连接需要开销较多(时间,系统资源)。


TCP三次握手


建立一个TCP连接时,需要客户端和服务器总共发送3个包。


  三次握手的目的是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号并交换 TCP 窗口大小信息.在 Socket 编程中,客户端执行connect()时。将触发三次握手。


首先了解一下几个标志,SYN(synchronous),同步标志,ACK (Acknowledgement),即确认标志,seq是Sequence Number(序列号)。


  第一次握手:客户端发送一个TCP的SYN标志位置1的包指明客户打算连接的服务器的端口,以及初始序号X,保存在包头的序列号(Sequence Number)字段里。


  第二次握手:服务器发回确认包(ACK)应答。即SYN标志位和ACK标志位均为1同时,将确认序号(Acknowledgement Number)设置为客户的序列号加1以,即X+1。


  第三次握手:客户端再次发送确认包(ACK) SYN标志位为0,ACK标志位为1。并且把服务器发来ACK的序号字段+1,放在确定字段中发送给对方.并且在数据段放写序列号的+1。


tcp四次挥手


TCP的连接的拆除需要发送四个包,因此称为四次挥手(four-way handshake)。


为什么连接的时候是三次握手,关闭的时候却是四次挥手?


因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来 同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,” 你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。


TCP的优缺点


优点:


可靠,稳定 TCP的可靠体现在TCP在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制,在数据传完后,还会断开连接用来节约系统资源。


缺点:


慢,效率低,占用系统资源高,易被攻击 TCP在传递数据之前,要先建连接,这会消耗时间,而且在数据传递时,确认机制、重传机制、拥塞控制机制等都会消耗大量的时间,而且要在每台设备上维护所有的传输连接,事实上,每个连接都会占用系统的CPU、内存等硬件资源。 而且,因为TCP有确认机制、三次握手机制,这些也导致TCP容易被人利用,实现DOS、DDOS、CC等攻击。


2、UDP:


面向非连接、传输不可靠、用于传输少量数据(数据包模式)、速度快。


UDP的优点: 快,比TCP稍安全 UDP没有TCP的握手、确认、窗口、重传、拥塞控制等机制,UDP是一个无状态的传输协议,所以它在传递数据时非常快。没有TCP的这些机制,UDP较TCP被攻击者利用的漏洞就要少一些。但UDP也是无法避免攻击的,比如:UDP Flood攻击…… UDP的


UDP缺点:不可靠,不稳定,因为UDP没有TCP那些可靠的机制,在数据传递时,如果网络质量不好,就会很容易丢包。


3、HTTP


(1) HTTP简要介绍


HTTP协议即超文本传送协议(HypertextTransfer Protocol ),是Web联网的基础,也是手机联网常用的协议之一,HTTP协议是建立在TCP协议之上的一种应用。


(2) HTTP特点


  HTTP连接最显著的特点是客户端发送的每次请求都需要服务器回送响应,在请求结束后,会主动释放连接,从建立连接到关闭连接的过程称为“一次连接”,因此HTTP连接是一种“短连接”



  • 在HTTP 1.0中,客户端的每次请求都要求建立一次单独的连接,在处理完本次请求后,就自动释放连接。

  • 在HTTP 1.1中则可以在一次连接中处理多个请求,并且多个请求可以重叠进行,不需要等待一个请求结束后再发送下一个请求。 


HTTP是基于客户端/服务端(C/S)的架构模型


  客户端发送一个HTTP请求到服务器的请求消息包括以下格式:请求行(request line)、请求头部(header)、空行和请求数据四个部分组成,


HTTP响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。


(3) HTTP优缺点


优点:



  • 基于应用级的接口使用方便

  • 程序员开发水平要求不高,容错性强


缺点:



  • 传输速度慢,数据包大(Http协议中包含辅助应用信息)

  • 如实时交互,服务器性能压力大。

  • 数据传输安全性差


4、Socket


(1) Socket简要介绍


网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。


建立网络通信连接至少要一对端口号(socket)。socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口;HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。


(2) Socket优缺点


优点:



  • 传输数据为字节级,传输数据可自定义,数据量小(对于手机应用讲:费用低)

  • 传输数据时间短,性能高

  • 适合于客户端和服务器端之间信息实时交互

  • 可以加密,数据安全性强


Socket缺点:



  • 需对传输的数据进行解析,转化成应用级的数据

  • 对开发人员的开发水平要求高

  • 相对于Http协议传输,增加了开发量


5、TCP HTTP UDP三者的关系:



  • TCP/IP是个协议组,可分为四个层次:网络接口层、网络层、传输层和应用层。

  • 在网络层有:IP协议、ICMP协议、ARP协议、RARP协议和BOOTP协议。

  • 在传输层中有:TCP协议与UDP协议。

  • 在应用层有:FTP、HTTP、TELNET、SMTP、DNS等协议。

  • 因此,HTTP本身就是一个协议,是从Web服务器传输超文本到本地浏览器的传送协议。


二:HttpURLConnection和httpclient


在Android开发中网络请求是最常用的操作之一, Android SDK中对HTTP(超文本传输协议)也提供了很好的支持,这里包括两种接口:



  • 标准Java接口(java.NET) —-HttpURLConnection,可以实现简单的基于URL请求、响应功能;

  • Apache接口(org.appache.http)—-HttpClient,使用起来更方面更强大。


但在android API23的SDK中Google将HttpClient移除了。Google建议使用httpURLconnection进行网络访问操作。


HttpURLconnection是基于http协议的,支持get,post,put,delete等各种请求方式,最常用的就是get和post,下面针对这两种请求方式进行讲解。


1、HttpURLConnection


在JDK的java.net包中已经提供了访问HTTP协议的基本功能的类:HttpURLConnection。


HttpURLConnection是Java的标准类,它继承自URLConnection,可用于向指定网站发送GET请求、POST请求。


2、httpclient


HttpClient 是Apache Jakarta Common 下的子项目,可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。


三:android常用网络框架


1、Android-async-http


Android-async-http简要介绍


Android-async-http 是一个强大的网络请求库,这个网络请求库是基于 Apache HttpClient 库之上的一个异步网络请求处理库,网络处理均基于 Android 的非 UI 线程,通过回调方法处理请求结果。可惜的是 Android 6.0 (api 23) SDK,不再提供 org.apache.http.* (只保留几个类)。


Android-async-http优点


优点:



  • 在匿名回调中处理请求结果

  • 在 UI 线程外进行 http 请求

  • 文件断点上传

  • 智能重试

  • 默认 gzip 压缩

  • 支持解析成 Json 格式

  • 可将 Cookies 持久化到 SharedPreference


2、afinal


afinal简要介绍


afinal是一个开源的android的orm和ioc应用开发框架,其特点是小巧灵活,代码入侵量少。在android应用开发中,通过afinal的ioc框架,诸如ui绑定,事件绑定,通过注解可以自动绑定。通过afinal的orm框架,无需任何配置信息,一行代码就可以对android的sqlite数据库进行增删改查操作。同时,afinal内嵌了finalHttp等简单易用的工具,可以轻松的对http请求进行操作。


afinal主要组件



  • FinalHttp:用于请求http数据,直接ajax方式请求,文件上传, 断点续传下载文件等

  • FinalBitmap:用于显示bitmap图片,无需考虑线程并发和oom等问题。

  • FinalActivity:完全可以通过注解方式绑定控件和事件,无需编写代码。

  • FinalDb:android中sqlite的orm框架,一行代码搞定增删改查。


afinal特点



  • 设计简单小巧灵活

  • orm零配置,但可以配置,可以通过灵活的注解配置达到更加强大的功能

  • 数据库查询支持DbModel,可以轻松的进行各种复杂的查询

  • android的ui和事件绑定完全通过注解的方式,无需编写一行代码

  • http请求支持ajax方式请求

  • 体积小(不到100KB),不依赖第三方jar包


afinal优缺点


优点
android中的orm框架,一行代码就可以进行增删改查。支持一对多,多对一等查询。


缺点
目前暂时不支持复合主键,并且对SQL语句的支持也非常有限,一些比较复杂的业务逻辑实现非常麻烦!


3、xUtils


xUtils简要介绍


xUtils是基于Afinal开发的目前功能比较完善的一个Android开源框架,最近又发布了xUtil3.0,在增加新功能的同时又提高了框架的性能。


下面来看看官方(github.com/wyouflf/xUt…)对xUtils3的介绍:



  • xUtils包含了很多实用的android工具;

  • xUtils支持超大文件(超过2G)上传,更全面的http请求协议支持(11种谓词),拥有更加灵活的ORM,更多的事件注解支持且不受混淆影响;

  • xUitls最低兼容android 2.2 (api level 8)!

  • xUtils3变化较多所以建立了新的项目不在旧版(github.com/wyouflf/xUtils)上继续维护, 相对于旧版本:

  • HTTP实现替换HttpClient为UrlConnection, 自动解析回调泛型, 更安全的断点续传策略;

  • 支持标准的Cookie策略, 区分domain, path;

  • 事件注解去除不常用的功能, 提高性能;

  • 数据库api简化提高性能, 达到和greenDao一致的性能;

  • 图片绑定支持gif(受系统兼容性影响, 部分gif文件只能静态显示), webp; 支持圆角, 圆形, 方形等裁剪, 支持自动旋转。


xUtils主要组件


目前xUtils主要有四大模块:
ViewUtils模块:



  • android中的ioc(控制倒转)框架,完全注解方式就可以进行UI,资源和事件绑定;

  • 新的事件绑定方式,使用混淆工具混淆后仍可正常工作;

  • 目前支持常用的20种事件绑定,参见ViewCommonEventListener类和包com.lidroid.xutils.view.annotation.event。


HttpUtils模块:



  • 支持同步,异步方式的请求;

  • 支持大文件上传,上传大文件不会oom;

  • 支持GET,POST,PUT,MOVE,COPY,DELETE,HEAD,OPTIONS,TRACE,CONNECT请求;

  • 下载支持301/302重定向,支持设置是否根据Content-Disposition重命名下载的文件;

  • 返回文本内容的请求(默认只启用了GET请求)支持缓存,可设置默认过期时间和针对当前请求的过期时间。


BitmapUtils模块:



  • 加载bitmap的时候无需考虑bitmap加载过程中出现的oom和android容器快速滑动时候出现的图片错位等现象;

  • 支持加载网络图片和本地图片;

  • 内存管理使用lru算法,更好的管理bitmap内存;

  • 可配置线程加载线程数量,缓存大小,缓存路径,加载显示动画等…


DbUtils模块:



  • android中的orm(对象关系映射)框架,一行代码就可以进行增删改查;

  • 支持事务,默认关闭;

  • 可通过注解自定义表名,列名,外键,唯一性约束,NOT NULL约束,CHECK约束等(需要混淆的时候请注解表名和列名);

  • 支持绑定外键,保存实体时外键关联实体自动保存或更新;

  • 自动加载外键关联实体,支持延时加载;

  • 支持链式表达查询,更直观的查询语义


4、Volley框架


Volley简要介绍


在2013年Google I/O大会上推出了一个新的网络通信框架Volley。Volley既可以访问网络取得数据,也可以加载图片,并且在性能方面也进行了大幅度的调整,它的设计目标就是非常适合去进行数据量不大,但通信频繁的网络操作,而对于大数据量的网络操作,比如说下载文件等,Volley的表现就会非常糟糕。在使用Volley前请下载Volley库并放在libs目录下并add到工程中。


Volley的主要特点



  • 扩展性强。Volley 中大多是基于接口的设计,可配置性强。

  • 一定程度符合 Http 规范,包括返回 ResponseCode(2xx、3xx、4xx、5xx)的处理,请求头的处理,缓存机制的支持等。并支持重试及优先级定义。

  • 默认 Android2.3 及以上基于 HttpURLConnection,2.3 以下基于 HttpClient 实现,这两者的区别及优劣在4.2.1 Volley中具体介绍。

  • 提供简便的图片加载工具。


Volley提供的功能



  • JSON,图像等的异步下载;

  • 网络请求的排序(scheduling)

  • 网络请求的优先级处理

  • 缓存

  • 多级别取消请求

  • 和Activity和生命周期的联动(Activity结束时同时取消所有网络请求)


Volley优缺点


优点



  • 非常适合进行数据量不大,但通信频繁的网络操作

  • 可直接在主线程调用服务端并处理返回结果

  • 可以取消请求,容易扩展,面向接口编程

  • 网络请求线程NetworkDispatcher默认开启了4个,可以优化,通过手机CPU数量

  • 通过使用标准的HTTP缓存机制保持磁盘和内存响应的一致

  • 通信更快、更稳定、更简单


缺点



  • 使用的是HttpClient的,HttpURLConnection类

  • 6.0不支持的HttpClient了,如果想支持得添加org.apache.http.legacy.jar

  • 对大文件下载Volley的表现非常糟糕

  • 只支持HTTP请求

  • 图片加载性能一般

  • 不适合进行大数据的上传和下载

  • 不能下载文件:这也是它最致命的地方


为什么使用Volley:



  • 高效的的Get/Post方式的数据请求交互

  • 网络图片的加载和缓存

  • 谷歌官方推出

  • 性能稳定和强劲


5、okhttp


okhttp简介


一个处理网络请求的开源项目,是安卓端最火热的轻量级框架,由移动支付Square公司贡献(该公司还贡献了Picasso),用于替代HttpUrlConnection和Apache HttpClient(android API23 6.0里已移除HttpClient)


okhttp优势



  • 支持HTTP2/SPDY(SPDY是Google开发的基于TCP的传输层协议,用以最小化网络延迟,提升网络速度,优化用户的网络使用体验。),可以合并多个到同一个主机的请求

  • 允许连接到同一个主机地址的所有请求,提高请求效率

  • socket自动选择最好路线,并支持自动重连,拥有自动维护的socket连接池,减少握手次数,减少了请求延迟,共享Socket,减少对服务器的请求次数

  • 基于Headers的缓存策略减少重复的网络请求。

  • 缓存响应数据来减少重复的网络请求

  • 减少了对数据流量的消耗

  • 自动处理GZip压缩

  • OkHttp使用Okio来大大简化数据的访问与存储,Okio是一个增强 java.io 和 java.nio的库。

  • OkHttp还处理了代理服务器问题和SSL握手失败问题。


okhttp流程图


\


okhttp功能



  • PUT,DELETE,POST,GET等请求

  • 基于Http的文件上传

  • 文件的上传下载

  • 上传下载的进度回调

  • 加载图片(内部会图片大小自动压缩)

  • 支持请求回调,直接返回对象、对象集合

  • 支持session的保持

  • 支持自签名网站https的访问,提供方法设置下证书就行

  • 支持取消某个请求


6、Retrofit


Retrofit简介


Retrofit与okhttp共同出自于Square公司,retrofit就是对okhttp做了一层封装。把网络请求都交给给了Okhttp,我们只需要通过简单的配置就能使用retrofit来进行网络请求了,主要作者是Android大神JakeWharton


Retrofit特性



  • 将rest API封装为java接口,我们根据业务需求来进行接口的封装,实际开发可能会封装多个不同的java接口以满足业务需求。(注意:这里会用到Retrofit的注解:比如get,post)

  • 使用Retrofit提供的封装方法将我们的生成我们接口的实现类,这个真的很赞,不用我们自己实现,通过注解Retrofit全部帮我们自动生成好了。

  • 调用我们实现类对象的接口方法。


为什么要用Retrofit




  • 在处理HTTP请求的时候,因为不同场景或者边界情况等比较难处理。你需要考虑网络状态,需要在请求失败后重试,需要处理HTTPS等问题,二这些事情让你很苦恼,而Retrofit可以将你从这些头疼的事情中解放出来。




  • 当然你也可以选择android-async-http和Volley,但为什么选择Retrofit?首先效率高,其次Retrofit强大且配置灵活,其次是和OkHttp无缝衔接。




  • 在Retrofit2之前,OkHttp是一个可选的客户端。Retrofit2中,Retrofit与OkHttp强耦合,使得更好地利用OkHttp,包括使用OkHttp解决一些棘手的问题。




Retrofit流程图


\


Retrofit优缺点


优点:



  • 可以配置不同HTTP client来实现网络请求,如okhttp、httpclient等

  • 请求的方法参数注解都可以定制

  • 支持同步、异步和RxJava

  • 超级解耦

  • 可以配置不同的反序列化工具来解析数据,如json、xml等

  • 使用非常方便灵活

  • 框架使用了很多设计模式(感兴趣的可以看看源码学习学习)


缺点:



  • 不能接触序列化实体和响应数据

  • 执行的机制太严格

  • 使用转换器比较低效

  • 只能支持简单自定义参数类型 

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

Android查看第三方库的依赖树汇总

项目的开发过程中,我们或多或少都会引入第三方库,引入的库越多,越容易产生库之间的依赖冲突。 下面就拿我遇到的问题还原一下: 之前接人容联客服系统的时候,集成完成后进入客服页面发生闪退,我们回顾一下错误信息: 我们关键看一下报错代码: java.lang.No...
继续阅读 »

项目的开发过程中,我们或多或少都会引入第三方库,引入的库越多,越容易产生库之间的依赖冲突。


下面就拿我遇到的问题还原一下:


之前接人容联客服系统的时候,集成完成后进入客服页面发生闪退,我们回顾一下错误信息:


122.jpg


我们关键看一下报错代码:


java.lang.NoSuchMethodError: No virtual method into (Landroid/widget/ImageView;)Lcom/bumptech/glide/request/target/Target; in class Lcom/a/a/i; or its super classes (declaration of 'com.a.a.i' appears in/data/app/com.sami91sami.h5-1/base.apk)
复制代码

我们可以根据报错,跳到报错的地方:


133.jpg


该报错的意思就是:没有


into(Landroid/widget/ImageView)
复制代码

的方法,代码能编译通过,说明项目中肯定是添加依赖了,那怎么还会报这个错误呢?还没添加依赖之前,项目中也是使用的Glide进行图片的加载,会不会是项目中的Glide与容联Demo中的Glide有冲突呢。


我们可以根据报错的地方into方法,点进入看源码:


144.jpg


可以看到容联Demo使用的Glide版本是3.7.0。


再来看看项目中Glide使用的版本:


155.jpg


可以看到项目中使用的Glide版本是4.5.0。


这时就想到真的很大概率是两者的Glide版本有冲突了。


果然将容联Demo中的Glide版本改成4.5.0之后,编译运行进入客服界面后,没有报错了,完美解决。


这就是我之前遇到的库冲突的问题,这个问题有错误信息可以定位到是Glide库依赖的问题,要是遇到其它错误信息没那么显著的,那是不是就头疼了呢。


当时遇到这个问题,我并没有使用查看依赖树的方式,而是直接查看了源码,因为当时我并不知道还能这么干,幸运的是很快就定位到了问题所在,所以当我们升级第三方库或者引入新的第三方库时,库与库之间依赖冲突,我们需要知道每个第三方依赖库的依赖树,知道依赖树就清楚哪里冲突啦。


下面就记录下几种查看依赖树的方式:


方案一: Gradle task工具查看


1、点击Android studio面板右上角“Gradle”,如图所示:


1639041944906-gzb.png


2、按照如图目录找到dependencise双击,会在Run控制台输出打印,如图所示:


222.png


3、打印如图所示:


333.png


方案二:使用Gradle View插件


1、快捷键Ctrl+Alt+s,打开settings,然后点击按钮Plugins


444.png


2、搜索 Gradle View,然后安装,并重启Android Studio,我这是已经安装成功后的截图


555.png


3、点击菜单栏上View -> Tool Windows -> Gradle View,然后等待一会,就可以查看了。


666.png


如图所示:


777.png


方案三:Terminal控制台查看


在windows上Android studio Terminal中使用这个命令:


gradlew :app:dependencies(“app”为module名称)
复制代码

在MacOS中使用下面的命令:


./gradlew :app:dependencies(“app”为module名称)
复制代码

这个命令会将gradle执行的各个步骤都打印出来,包括releaseUnitTestRuntimeClasspath,releaseUnitTestCompileClasspath,releaseRuntimeClasspath,releaseCompileClasspath,lintClassPath,debugUnitTestRuntimeClasspath等等。


那么,我们可以配置configuration 参数只查看其中一个的依赖树就够了。


 ./gradlew :app:dependencies --configuration compile
复制代码

在Window系统下,无需使用./开头,直接使用gradlew即可。


执行app模块下的dependencies任务;额外配置compile,编译环境下的依赖项。


888.png


通过查看依赖树,我们就能看到哪些依赖有冲突,比如某个框架的support包冲突,只要在moudle的gradle文件下找到该冲突的依赖用括号括住,在后面加:


{
exclude group:'com.android.support'
}
复制代码

这要就可以把该框架的support包移除啦。


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

Android Canvas画布解析

1.简介在开发中,我们经常需要自定义View去实现各种各样的效果,在这个过程中经常需要用到Canvas画布去绘制各种各样的图形和图案,因此,熟练地掌握Canvas的各种使用方法,就显得尤为重要。本文将简要介绍Canvas的各种用法,加深大家的理解。2.绘制各种...
继续阅读 »

1.简介

在开发中,我们经常需要自定义View去实现各种各样的效果,在这个过程中经常需要用到Canvas画布去绘制各种各样的图形和图案,因此,熟练地掌握Canvas的各种使用方法,就显得尤为重要。本文将简要介绍Canvas的各种用法,加深大家的理解。

2.绘制各种图形

Canvas提供了很多绘制方法,基于这些方法,我们可以绘制出各种各样的图形,下面我们就开始介绍这些绘制方法。

2.1 drawARGB

此方法可以用ARGB颜色绘制一个颜色背景,方法如下:

//a:颜色的alpha部分,取值0--255
//r:颜色的red部分,取值0--255
//g:颜色的green部分,取值0--255
//b:颜色的blue部分,取值0--255
public void drawARGB(int a, int r, int g, int b)

现在使用此方法绘制一个纯色背景,示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawARGB(255,150,100,100);
}

2.2 drawArc

先介绍其中的一个方法,方法如下:

//left:左边到父布局左边的距离
//top:顶边到父布局顶边的距离
//right:右边到父布局左边的距离
//bottom:底边到父布局顶边的距离
//startAngle:弧开始的角度
//sweepAngle:顺时针方向扫描的角度
//useCenter:是否使用中心
//paint:绘制弧的画笔,这个值不能为null
public void drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, @NonNull Paint paint)

此方法用来绘制弧形,如果起始角度是负值或大于等于360,起始角度取360的模。如果扫描角度大于等于360,椭圆形将会被完全地绘制,如果扫描角度是负值,扫描角度取360的模。弧的绘制是顺时针方向,0度对应着钟表的3点钟方向。useCenter为true时的示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawArc(50,50,300,300,0,300,true,paint);
}

useCenter为false时的示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawArc(50,50,300,300,0,300,false,paint);
}

drawArc的另一个重载方法如下:

//oval:用来定义弧形的形状和大小的椭圆的边界,这个值不能为null
//startAngle:弧开始的角度
//sweepAngle:顺时针方向扫描的角度
//useCenter:是否使用中心
//paint:绘制弧的画笔,这个值不能为null
public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter, @NonNull Paint paint)

此方法useCenter为true时的示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
RectF rectF = new RectF(50,50,300,300);
canvas.drawArc(rectF,0,200,true,paint);
}

此方法useCenter为false时的示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
RectF rectF = new RectF(50,50,300,300);
canvas.drawArc(rectF,0,200,false,paint);
}

2.3 drawBitmap

这个方法是用来绘制位图的,这个方法有很多重载,先看其中的一个方法:

//bitmap:要绘制的位图
//matrix:用来变换位图的矩阵
//paint:绘制位图的画笔,可能为null
public void drawBitmap(@NonNull Bitmap bitmap, @NonNull Matrix matrix, @Nullable Paint paint)

此方法绘制位图的示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.test);
Matrix matrix = new Matrix();
canvas.drawBitmap(bitmap,matrix,paint);
}

看一个drawBitmap的重载方法如下:

//bitmap:要绘制的位图
//src:要绘制的位图的子集,即绘制的是全部或者部分位图,可能为null
//dst:位图将要通过缩放和转换去适应的矩形
//paint:绘制位图的画笔,可能为null
public void drawBitmap(@NonNull Bitmap bitmap, @Nullable Rect src, @NonNull Rect dst,
@Nullable Paint paint)

此方法用来绘制位图,通过自动缩放和转换去适应目标矩形,示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.test);
Rect srcRect = new Rect(0,0,300,300);
Rect dstRect = new Rect(0,0,600,600);
canvas.drawBitmap(bitmap,srcRect,dstRect,paint);
}

再来看另外一个重载方法如下:

//bitmap:要绘制的位图
//src:要绘制的位图的子集,即绘制的是全部或者部分位图,可能为null
//dst:位图将要通过缩放和转换去适应的矩形
//paint:绘制位图的画笔,可能为null
public void drawBitmap(@NonNull Bitmap bitmap, @Nullable Rect src, @NonNull RectF dst,
@Nullable Paint paint)

此方法也是用来绘制通过自动缩放和转换去适应目标矩形的位图,示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.test);
Rect srcRect = new Rect(0,0,300,300);
RectF dstRectF = new RectF(0,0,600,600);
canvas.drawBitmap(bitmap,srcRect,dstRectF,paint);
}

再来看绘制位图的一个方法如下:

//bitmap:要绘制的位图
//left:位图左边的位置
//top:位图顶边的位置
//paint:绘制位图的画笔,可能为null
public void drawBitmap(@NonNull Bitmap bitmap, float left, float top, @Nullable Paint paint)

这个方法绘制左上角在(x,y)的位图,如果位图和画布拥有不同的密度,将会自动缩放位图,以和画布相同的密度绘制,示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.test);
canvas.drawBitmap(bitmap,50,100,paint);
}

2.4 drawCircle

此方法使用画笔paint绘制圆,如果半径小于等于0将不会绘制任何东西,基于画笔的样式,圆将会被填充或者绘制的是轮廓,方法如下:

//cx:圆心的x坐标
//cy:圆心的y坐标
//radius:圆的半径
//paint:绘制圆的画笔
public void drawCircle(float cx, float cy, float radius, @NonNull Paint paint)

此方法的示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(200,200,150,paint);
}

2.5 drawColor

此方法使用颜色填充整个画布canvas的位图,方法如下:

//color:绘制在画布上的颜色
public void drawColor(@ColorInt int color)

示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.GREEN);
}

再来看一个重载方法如下:

//color:绘制在画布上的颜色
//mode:应用到颜色上的porter-duff模式
public void drawColor(@ColorInt int color, @NonNull PorterDuff.Mode mode)

此方法使用颜色和porter-duff模式填充整个画布canvas的位图,示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.GREEN, PorterDuff.Mode.DARKEN);
}

2.6 drawLine

此方法使用画笔paint和开始及终止的x,y坐标绘制线段,由于线总是轮廓式的,画笔paint的样式将会被忽略,方法如下:

//startX:线段起始点的x坐标
//startY:线段起始点的y坐标
//stopX:线段结束点的x坐标
//stopY:线段结束点的y坐标
//paint:绘制线段的画笔
public void drawLine(float startX, float startY, float stopX, float stopY,
@NonNull Paint paint)

示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawLine(50,50,300,300,paint);
}

2.7 drawLines

此方法绘制一系列线段,每条线需要pts数组中4个连续的值。因此,绘制一条线,数组必须至少包括4个值。逻辑上和绘制下面的数组一样,先使用pts[0]、pts[1]、pts[2]、pts[3]绘制线,接着使用[4]、pts[5]、pts[6]、pts[7]绘制线,以此类推。方法如下:

//pts:要绘制的点的数组,如[x0,y0,x1,y1,x2,y2...]
//offset:绘制前在数组中要跳过的值的个数
//count:在跳过偏移量后,要处理的数组中值的个数
//paint:绘制的画笔
public void drawLines(@Size(multiple = 4) @NonNull float[] pts, int offset, int count,
@NonNull Paint paint)

此方法的示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float[] pts = {50,10,100,200,300,200,200,400};
canvas.drawLines(pts,0,8,paint);
}

再来看另一个重载方法如下:

//pts:要绘制的点的数组,如[x0,y0,x1,y1,x2,y2...]
//paint:绘制的画笔
public void drawLines(@Size(multiple = 4) @NonNull float[] pts, @NonNull Paint paint)

示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float[] pts = {50,10,100,200,300,200,200,400};
canvas.drawLines(pts,paint);
}

2.8 drawOval

此方法使用画笔paint绘制椭圆,椭圆被填充或是轮廓由画笔paint的样式决定,方法如下:

//left:左边到父布局左边的距离
//top:顶边到父布局顶边的距离
//right:右边到父布局左边的距离
//bottom:底边到父布局顶边的距离
//paint:绘制的画笔
public void drawOval(float left, float top, float right, float bottom, @NonNull Paint paint)

示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawOval(50,50,600,300,paint);
}

再来看一个重载方法:

//oval:椭圆的矩形边界,这个值不能为null
//paint:绘制的画笔,不能为null
public void drawOval(@NonNull RectF oval, @NonNull Paint paint)

示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
RectF rectF = new RectF(50,50,600,300);
canvas.drawOval(rectF,paint);
}

2.9 drawPaint

此方法使用画笔paint填充整个画布的位图,方法如下:

//paint:在画布上绘制的画笔
public void drawPaint(@NonNull Paint paint)

2.10 drawPath

此方法使用画笔paint绘制路径,路径被填充或是轮廓由画笔paint的样式决定,方法如下:

//path:被绘制的路径
//paint:绘制路径的画笔
public void drawPath(@NonNull Path path, @NonNull Paint paint)

示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Path path = new Path();
path.moveTo(50,50);
path.lineTo(200,100);
path.lineTo(200,400);
path.lineTo(150,500);
canvas.drawPath(path,paint);
}

2.11 drawPoint

此方法用来绘制一个点,方法如下:

//x:点的x坐标
//y:点的y坐标
//paint:绘制点的画笔
public void drawPoint(float x, float y, @NonNull Paint paint)

示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawPoint(100,100,paint);
}

2.12 drawPoints

此方法绘制一系列点,每个点位于被pts[]确定的坐标的中心,点的直径由画笔的笔画宽度确定,点的形状由画笔的Cap类型确定,点的形状是正方形的,除非当Cap类型是Round的时候,点的形状是圆形的,方法如下:

//pts:要绘制的点的数组[x0,y0,x1,y1,x2,y2...]
//offset:绘制前跳过的值的个数
//count:跳过偏移量之后要处理的值的个数
//paint:绘制点的画笔
public void drawPoints(@Size(multiple = 2) float[] pts, int offset, int count,
@NonNull Paint paint)

示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float[] pts = {50,50,100,200,300,200,200,400};
canvas.drawPoints(pts,0,8,paint);
}

再来看另外一个重载方法如下:

//pts:要绘制的点的数组[x0,y0,x1,y1,x2,y2...]
//paint:绘制点的画笔
public void drawPoints(@Size(multiple = 2) @NonNull float[] pts, @NonNull Paint paint)

示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float[] pts = {50,50,100,200,300,200,200,400};
canvas.drawPoints(pts,paint);
}

2.13 drawRGB

此方法使用RGB颜色填充整个画布的位图,方法如下:

//r:颜色的red部分,取值0--255
//g:颜色的green部分,取值0--255
//b:颜色的blue部分,取值0--255
public void drawRGB(int r, int g, int b)

示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRGB(200,100,100);
}

2.14 drawRect

此方法使用画笔绘制矩形,矩形被填充或者显示轮廓由画笔的样式确定,方法如下:

//left:矩形的左边
//top:矩形的顶边
//right:矩形的右边
//bottom:矩形的底边
//paint:绘制矩形的画笔
public void drawRect(float left, float top, float right, float bottom, @NonNull Paint paint)

示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(50,100,500,300,paint);
}

看一个重载方法如下:

//r:要绘制的矩形
//paint:绘制矩形的画笔
public void drawRect(@NonNull Rect r, @NonNull Paint paint)

示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Rect rect = new Rect(50,100,500,300);
canvas.drawRect(rect,paint);
}

再来看另外一个重载方法如下:

//rect:要绘制的矩形
//paint:绘制矩形的画笔
public void drawRect(@NonNull RectF rect, @NonNull Paint paint)

此方法的示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
RectF rectF = new RectF(50,100,500,300);
canvas.drawRect(rectF,paint);
}

2.15 drawRoundRect

此方法使用画笔绘制圆角矩形,矩形被填充或者显示轮廓由画笔的样式确定,方法如下:

//rect:圆角矩形的矩形边界
//rx:圆角的x半径
//ry:圆角的y半径
//paint:绘制圆角矩形的画笔
public void drawRoundRect(@NonNull RectF rect, float rx, float ry, @NonNull Paint paint)

示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
RectF rectF = new RectF(50,100,500,300);
canvas.drawRoundRect(rectF,20,20,paint);
}

2.16 drawText

此方法用来绘制文本,原点在(x,y),原点和画笔paint中的对齐设置有关,方法如下:

//text:被绘制的文本
//x:文本的原点的x坐标
//y:文本基线的y坐标
//paint:绘制文本的画笔,可以进行颜色、大小、样式等设置
public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint)

示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
String text = "Android Canvas";
canvas.drawText(text,200,300,paint);
}

再看一个重载方法如下:

//text:被绘制的文本
//start:要绘制的文本中第一个字符的索引
//end:(end-1)是要绘制的文本中最后一个字符的索引
//x:文本的原点的x坐标
//y:文本基线的y坐标
//paint:绘制文本的画笔,可以进行颜色、大小、样式等设置
public void drawText(@NonNull String text, int start, int end, float x, float y,
@NonNull Paint paint)

示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
String text = "Android Canvas";
canvas.drawText(text,3,11,200,300,paint);
}

2.17 drawTextOnPath

此方法使用画笔paint沿着路径绘制文本,画笔的对齐方式决定从何处沿着路径开始文本的绘制,方法如下:

//text:被绘制的文本
//path:文本应该遵循的路径
//hOffset:沿着路径文本开始位置偏移的距离
//hOffset:文本在路径之上或之下的偏移的距离,可以为正值或负值
//paint:绘制文本的画笔,可以进行颜色、大小、样式等设置
public void drawTextOnPath(@NonNull String text, @NonNull Path path, float hOffset,
float vOffset, @NonNull Paint paint)

示例如下:

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
String text = "Android Canvas";
Path path = new Path();
path.moveTo(50,50);
path.lineTo(200,100);
path.lineTo(400,400);
canvas.drawTextOnPath(text,path,0,0,paint);
}

3.总结

在自定义View的时候,将会经常用到Canvas,因此熟练地掌握和运用这些绘制方法就显得比较重要。使用Canvas可以绘制点、线、矩形、圆、椭圆、文本、路径、位图等各种各样的图形图案,本文详细地介绍了Canvas的各种方法,并给出了示例代码,灵活运用这些方法进行组合,就能绘制出各种各样的图案和效果。

收起阅读 »

如何优雅地在Vue页面中引入img图片

vue
我们在学习html的时候,图片标签<img>引入图片 <img src="../assets/images/avatar.png" width="100%"> 但是这样会有2个弊端:因为采用绝对路径引入,所以如果后面这张图片移动了目录,...
继续阅读 »

我们在学习html的时候,图片标签<img>引入图片


<img src="../assets/images/avatar.png" width="100%">

但是这样会有2个弊端:

  • 因为采用绝对路径引入,所以如果后面这张图片移动了目录,就需要修改代src里的路径
  • 如果这张图片在同一页面内有多个地方要使用到,就需要引入多次,而且图片移动了目录,这么多地方都要修改src路径

怎么办?使用动态路径import、require



首先讲讲这两个兄弟,在ES6之前,JS一直没有自己的模块语法,为了解决这种尴尬就有了require.js,在ES6发布之后JS又引入了import的概念

  • 使用import引入
  • import之后需要在data中注册一下,否则显示不了


    <script>
    import lf1 from '@/assets/images/lf1.png'
    import lf2 from '@/assets/images/lf2.png'
    import lf3 from '@/assets/images/lf3.png'
    import lf4 from '@/assets/images/lf4.png'
    import lf5 from '@/assets/images/lf5.png'
    import lf6 from '@/assets/images/lf6.png'
    import lf7 from '@/assets/images/lf7.png'
    import top1 from '@/assets/images/icon_top1.png'

    export default {
    name: 'Left',
    data () {
    return {
    lf1,
    lf2,
    lf3,
    lf4,
    lf5,
    lf6,
    lf7,
    top1
    }
    }
    }
    </script>
    • 使用require引入

    <script>
    import top1 from '@/assets/images/cityOfVitality/icon_top1.png'

    export default {
    name: 'Right',
    data () {
    return {
    rt1: require('@/assets/images/crt1.png'),
    rt2: require('@/assets/images/crt2.png'),
    rt3: require('@/assets/images/crt3.png'),
    rt4: require('@/assets/images/crt4.png'),
    rt5: require('@/assets/images/crt5.png'),
    rt6: require('@/assets/images/crt6.png'),
    top1
    }
    }
    }
    </script>

    作者:Jesse90s
    链接:https://juejin.cn/post/7019964864256802829

    收起阅读 »

    原来flex布局还能那么细?

    简介: flex布局(Flexible布局,弹性布局)是在小程序开发经常使用的布局方式 开启了flex布局的元素叫做flex container flex container里面的直接子元素叫做flex items(也就是开启了flex布局的盒子包裹的...
    继续阅读 »

    简介:



    • flex布局(Flexible布局,弹性布局)是在小程序开发经常使用的布局方式

    • 开启了flex布局的元素叫做flex container




    • flex container里面的直接子元素叫做flex items(也就是开启了flex布局的盒子包裹的第一层子元素)

    • 设置display的属性为flex或者inline-flex可以开启flex布局即成为flex container




    属性值设置为flex和inline-flex的区别:



    1. 如果display对应的值是flex的话,那么flex container是以block-level的形式存在的,相当于是一个块级元素

    2. 如果display的值设置为inline-flex的话,那么flex container是以inline-level的形式存在的,相当于是一个行内块元素




    1. 这两个属性值差异的影响在设置了属性值的元素上面,它们在子元素上的效果都是一样的

    2. 如果一个元素的父元素开启了flex布局;那么其子元素的display属性对自身的影响将会失效,但是对其内容的影响依旧存在的;


    举个例子:父元素设置了display: flex,即使子元素设置了display:block或者display:inline的属性,子元素还是会表现的像个行内块元素一样,这就是父元素对其的影响使其display属性对自身的影响失效了;


    但是为什么我们说其对内容的影响还在呢?假如说父子元素都设置了display: flex,那么子元素自身依然是行块级元素,并不会因为其开启了flex布局就变为块级元素,但是该子元素的内容依然会受到它flex布局的影响,各种flex特有的属性就会生效;


    总结:我们如果想让设置flex布局的盒子变成块级元素的话,那就dispaly的属性值就设置为flex;如果想让盒子变为行内块元素的话,就设置为inline-flex;父元素开启了flex布局之后,子元素的display属性对元素本身的影响就会失效,但是依旧可以影响盒子内部的元素;


    应用在flex container上的CSS属性



    1. flex-flow



    • felx-flowflex-direction || flex-wrap的缩写,这个属性很灵活,你可以只写一个属性,也可以两个都写,甚至交换前后顺序都是可以的

    • flex-flow:column wrap === flex-direction:column;flex-wrap:wrap




    • 如果只写了一个属性值的话,那么另一个属性就直接取默认值;flex-flow:row-reverse === flex-direction:row-reverse;flex-wrap:nowrap



    1. flex-direction


    flex items默认都是沿着main axis(主轴)从main start开始往main end方向排布的



    • flex-direction决定了主轴的方向,有四个取值

    • 分别为row(默认值)、row-reversecolumncolumn-reverse




    • 注意:flex-direction并不是直接改变flex items的排列顺序,他只是通过改变了主轴方向间接的改变了顺序


    1. flex-wrap


    flex-wrap能够决定flex items是在单行还是多行显示



    • nowrap(默认):单行


    本例中父盒子宽度为500px,子盒子为100px;当增加了多个子盒子并且给父盒子设置了flex-wrap:nowrap属性后,效果如下图所示:


    我们会惊奇的发现,父盒子的宽度没有变化,子盒子也确实没有换行,但是他们的宽度均缩小至能适应不换行的条件为止了,这也就是flex布局又称为弹性布局的原因


    所以,我们也可以得出一个结论:如果使用了flex布局的话,一个盒子的大小就算是将宽高写死了也是有可能发生改变的




    • wrap:多行


    换行后元素是往哪边排列跟交叉轴的方向有很大的关系,排列方向是顺着交叉轴的方向来的;


    用的还是刚刚的例子,只不过现在将属性flex-wrap的值设置为了wrap,效果如下图所示:


    子盒子的高度在能够正常换行的情况不会发生变化,但因为当前交叉轴的方向是从上往下的,那么要换行的元素就会排列在下方




    • wrap-reverse:多行(对比wrap,cross start与cross end相反),这个方法可以让交叉轴起点和终点相反,这样整体的布局就会翻转过来



    注意:这里就不是单纯的将要换行的元素向上排列,所有的元素都会受到影响,因为交叉轴的起始点和终止点已经反过来了



    1. justify-content


    Tip:下列图像灰色部分均无任何元素,其他颜色的区域为盒子内容区域


    justify-content决定了flex items在主轴上的对齐方式,总共有6个属性值:



    • flex-start(默认值):在主轴方向上与main start对齐




    • flex-end:在主轴方向上与main end对齐




    • center:在主轴方向上居中对齐




    • space-between


    特点:



    1. 与main start、main end两端对齐

    2. flex items之间的距离相等




    • space-evenly


    特点:



    1. flex items之间的距离相等

    2. flex items与main start、main end之间的距离等于flex items的距离




    • space-around


    特点:



    1. flex items之间的距离相等

    2. flex items与main start、main end之间的距离等于flex items的距离的一半




    1. align-items


    align-items决定了单行flex items在cross axis(交叉轴)上的对齐方式


    注意:主轴只要是横向的,无论flex-direction设置的是row还是row-reverse,其交叉轴都是从上指向下的;


    主轴只要是纵向的,无论flex-direction设置的是column还是column-reverse,其交叉轴都是从左指向右的;


    也就是说:主轴可能会有四种,但是交叉轴只有两种



    该属性具有如下几个属性值:



    • stretch(默认值):当flex items在交叉轴方向上的size(指width或者height,由交叉轴方向确定)为auto时,会自动拉伸至填充;但是如果flex items的size并不是auto,那么产生的效果就和设置为flex-start一样


    注意:触发条件为:父元素设置align-items的属性值为stretch,而子元素在交叉轴方向上的size设置为auto




    • flex-start:与cross start对齐




    • flex-end:与cross end对齐




    • center:居中对齐




    • baseline:与基准线对齐



    至于baseline这个属性值,平时用的并不是很多,基准线可以认为是盒子里面文字的底线,基准线对齐就是让每个盒子文字的底线对齐


    注意:align-items的默认值与justify-content的默认值不同,它并不是flex-start,而是stretch



    1. align-content



    • align-content决定了多行flex-items在主轴上的对齐方式,用法与justify-content类似,具有以下属性值

    • stretch(默认值)、flex-startflex-endcenterspace-bewteenspace-aroundspace-evenly




    • 大部分属性值看图应该就能明白,主要说一下stretch,当flex items在交叉轴方向上的size设置为auto之后,多行元素的高度之和会挤满父盒子,并且他们的高度是均分的,这和align-itemsstretch属性有点不一样,后者是每一个元素对应的size会填充父盒子,而前者则是均分



    应用在flex items上的CSS属性



    1. flex



    • flex是flex-grow flex-shrink?|| flex-basis的简写,说明flex属性值可以是一个、两个、或者是三个,剩下的为默认值

    • 默认值为flex: 0 1 auto(不放大但会缩小)

    • none: 0 0 auto(既不放大也不缩小)

    • auto:1 1 auto(放大且缩小)

    • 但是其简写方式是多种多样的,不过我们用到最多的还是flex:n;举个"栗子":如果flex是一个非负整数n,则该数字代表的是flex-grow的值,对应的flex-shrink默认为1,但是要格外注意:这里flex-basis的值并不是默认值auto,而是改成了0%;即flex:n === flex:n 1 0%;所以我们常用的flex:1 --> flex:1 1 0%;下图是flex简写的所有情况:




    1. flex-grow



    • flex-grow决定了flex-items如何扩展

    • 可以设置任何非负数字(正整数、正小数、0),默认值为0




    • 只有当flex container在主轴上有剩余的size时,该属性才会生效

    • 如果所有的flex itemsflex-grow属性值总和sum超过1,每个flex item扩展的size就为flex container剩余size * flex-grow / sum

    • 利用上一条计算公式,我们可以得出:当flex itemsflex-grow属性值总和sum不超过1时,扩展的总长度为剩余 size * sum,但是sum又小于1,所以最终flex items不可能完全填充felx container







    • 如果所有的flex itemsflex-grow属性值总和sum不超过1,每个flex item扩展的size就为flex container剩余size * flex-grow





    注意:不要认为flex item扩展的值都是按照flex-grow/sum的比例来进行分配,也并不是说看到flex-grow是小数,就认为其分配到的空间是剩余size*flex-grow,这些都是不准确的。当看到flex item使用了该属性时,首先判断的应该是sum是否大于1,再来判断通过哪种方法来计算比例



    • flex items扩展后的最终size不能超过max-width/max-height






    1. flex-basis



    • flex-basis用来设置flex items主轴方向上的base size,以后flew-growflex-shrink计算时所需要用的base size就是这个

    • auto(默认值)、content:取决于内容本身的size,这两个属性可以认为效果都是一样的,当然也可以设置具体的值和百分数(根据父盒子的比例计算)




    • 决定flex items最终base size因素的优先级为max-width/max-height/min-width/min-height > flex-basis > width/height > 内容本身的size

    • 可以理解为给flex items设置了flex-basis属性且属性值为具体的值或者百分数的话,主轴上对应的size(width/height)就不管用了



    1. flex-shrink



    • flex-shrink决定了flex items如何收缩

    • 可以设置任意非负数字(正小数、正整数、0),默认值是1




    • flex items在主轴方向上超过了flex container的size之后,flex-shrink属性才会生效

    • 注意:与flex-grow不同,计算每个flex item缩小的大小都是通过同一个公式来的,计算比例的方式也有所不同




    • 收缩比例 = flex-shrink * flex item的base size,base size就是flex item放入flex container之前的size

    • 每个flex item收缩的size为flex items超出flex container的size * 收缩比例 / 所有flex items 的收缩比例之和




    • flex items收缩后的最终size不能小于min-width/min-height

    • 总结:当flex items的flex-shrink属性值的总和小于1时,通过其计算收缩size的公式可知,其总共收缩的距离是超出的size * sum,由于sum是小于1的,那么无论如何子盒子都不会完全收缩至超过的距离,也就是说在不换行的情况下子元素一定会有超出





    不同的盒子缩小的值和其自身的flex-shrink属性有关,而且还与自己的原始宽度有关,这是跟flex-grow最大的区别




    1. order



    • order决定了flex items的排布顺序

    • 可以设置为任意整数(正整数、负整数、0),值越小就排在越前面




    • 默认值为0,当flex itemsorder一致时,则按照渲染的顺序排列






    1. align-self



    • flex items可以通过align-self覆盖flex container设置的align-items

    • 默认值为auto:默认遵从flex containeralign-items设置




    • stretchflex-startflex-endcenterbaseline,效果跟align-items一致,简单来说,就是align-items有什么属性,align-self就有哪些属性,当然auto除外


    .item:nth-child(2) {
    align-self: flex-start;
    background-color: #f8f;
    }


    疑难点解析:


    大家在看到flex-wrap那里换行的图片会不会有疑惑,为什么换行的元素不是紧挨着上一行的元素呢?而是有点像居中了的感觉



    想想多行元素在交叉轴上是上依靠哪一个属性进行排列的,当然是align-content了,那它的默认属性值是什么呢?--->stretch


    对,就是因为默认值是stretch,但是flex item又设置了高度,所以flex item不会被拉伸,但是它们会排列在要被拉伸的位置;我们可以测试一下,将flex-items交叉轴上的size设置为auto之后,stretch属性值才会表现的更加明显,平分flex-container在主轴上的高度,每个元素所在的位置就是上一张图所在的位置



    作者:Running53
    链接:https://juejin.cn/post/7033420158685151262

    收起阅读 »

    微信小程序iOS中JS的Date() 获取到的日期时间显示NaN的解决办法

    首先,js日期格式化函数(通过将日期转化为时间戳,再转化为指定格式):function formatDateTime(timeStamp) { var date = new Date(); date.setTime(timeStamp); var y = d...
    继续阅读 »

    首先,js日期格式化函数(通过将日期转化为时间戳,再转化为指定格式):

    function formatDateTime(timeStamp) { 
    var date = new Date();
    date.setTime(timeStamp);
    var y = date.getFullYear();
    var m = date.getMonth() + 1;
    var d = date.getDate();
    m = m < 10 ? ('0' + m) : m;
    d = d < 10 ? ('0' + d) : d;
    return y + '/' + m + '/' + d;
    };

    然后new Date('2018-08-12 23:00:00').getTime(); 安卓可以,苹果iOS却出现NanNan的问题

    这是因为iOS的日期格式是/不是-

    修改后:

    new Date('2018-08-12 23:00:00'.toString().replace(/\,/g, '/')

    OK。

    同理 new Date().getDay() 获取不到当前时间之前日期的星期几 也需要替换下


    原文链接:https://blog.csdn.net/gdali/article/details/88893549

    收起阅读 »

    写动画不用愁,Lottie 已经支持 Jetpack Compose 啦!

    概述 Lottie 是一款优秀的移动应用动画效果框架,支持为原生应用添加动画效果。Lottie 在不需要对代码进行重写的情况下让工程师更加方便的创建更丰富的动画效果,有了 Lottie 就不再需要使用 Gif 动画来展现效果,在移动开发领域 Lottie 已经...
    继续阅读 »

    概述


    Lottie 是一款优秀的移动应用动画效果框架,支持为原生应用添加动画效果。Lottie 在不需要对代码进行重写的情况下让工程师更加方便的创建更丰富的动画效果,有了 Lottie 就不再需要使用 Gif 动画来展现效果,在移动开发领域 Lottie 已经广为人知。 伴随着 Jetpack Compose 1.0 的正式发布,Lottie 也同样支持了 Jetpack Compose。这篇文章将指引你如何在 Jeptack Compose 中使用 Lottie 动画。这篇文章所使用的 Lottie 动画文件来自 Lottie 官方网站 ,你可以在这里找到更多免费的 Lottie 动画文件。


    添加 Lottie 依赖项


    你需要 build.gradle(app) 脚本文件中,添加依赖项目。



    implementation "com.airbnb.android:lottie-compose:4.0.0"



    配置 Lottie 资源


    你可以通过 Lottie 官方网站 或其他途径获取到你想要添加的Lottie动画对应静态 json 资源,或者你也可以使用URL方式。


    如果你使用的是静态 json 文件方式,你可以将其放入 res/raw 目录下。


    如果你使用的是URL方式,后续需要加载 lottie 时,你可以选用 URL 方式。


    创建 Lottie 动画


    首先,我们创建两个 mutableState 用于描述动画的速度与开始暂停状态。


    var isPlaying by remember {
    mutableStateOf(true)
    }
    var speed by remember {
    mutableStateOf(1f)
    }

    下一步,我们需要加载我们预先准备好的 Lottie资源。 这里我选择使用本地res/raw目录下静态资源的方式。


    val lottieComposition by rememberLottieComposition(
    spec = LottieCompositionSpec.RawRes(R.raw.lottie),
    )

    当然 Lottie 还为你提供了其他加载方式。


    sealed interface LottieCompositionSpec {
    // 加载 res/raw 目录下的静态资源
    inline class RawRes(@androidx.annotation.RawRes val resId: Int) : LottieCompositionSpec

    // 加载 URL
    inline class Url(val url: String) : LottieCompositionSpec

    // 加载手机目录下的静态资源
    inline class File(val fileName: String) : LottieCompositionSpec

    // 加载 asset 目录下的静态资源
    inline class Asset(val assetName: String) : LottieCompositionSpec

    // 直接加载 json 字符串
    inline class JsonString(val jsonString: String) : LottieCompositionSpec
    }

    再接下来,我们还需要描述 Lottie 的动画状态。


    val lottieAnimationState by animateLottieCompositionAsState (
    composition = lottieComposition, // 动画资源句柄
    iterations = LottieConstants.IterateForever, // 迭代次数
    isPlaying = isPlaying, // 动画播放状态
    speed = speed, // 动画速度状态
    restartOnPlay = false // 暂停后重新播放是否从头开始
    )

    最后,我们仅需要把动画资源句柄和动画状态提供给 LottieAnimation Composable 即可。


    LottieAnimation(
    lottieComposition,
    lottieAnimationState,
    modifier = Modifier.size(400.dp)
    )

    效果展示





    源代码


    @Preview
    @Composable
    fun LottieDemo() {
    var isPlaying by remember {
    mutableStateOf(true)
    }
    var speed by remember {
    mutableStateOf(1f)
    }

    val lottieComposition by rememberLottieComposition(
    spec = LottieCompositionSpec.RawRes(R.raw.lottie),
    )

    val lottieAnimationState by animateLottieCompositionAsState (
    composition = lottieComposition,
    iterations = LottieConstants.IterateForever,
    isPlaying = isPlaying,
    speed = speed,
    restartOnPlay = false
    )


    Box(
    modifier = Modifier.fillMaxSize(),
    contentAlignment = Alignment.Center
    ) {
    Column {
    Text(
    text = "Lottie Animation In Jetpack Compose",
    fontSize = 30.sp
    )
    Spacer(modifier = Modifier.height(30.dp))
    LottieAnimation(
    lottieComposition,
    lottieAnimationState,
    modifier = Modifier.size(400.dp)
    )

    Row(
    horizontalArrangement = Arrangement.SpaceAround,
    modifier = Modifier.fillMaxWidth(),
    verticalAlignment = Alignment.CenterVertically
    ) {
    Row(
    horizontalArrangement = Arrangement.SpaceBetween,
    verticalAlignment = Alignment.CenterVertically
    ) {
    Button(
    onClick = {
    speed = max(speed - 0.25f, 0f)
    },
    colors = ButtonDefaults.buttonColors(
    backgroundColor = Color(0xFF0F9D58)
    )
    ) {
    Text(
    text = "-",
    color = Color.White,
    fontWeight = FontWeight.Bold,
    fontSize = 20.sp,
    )
    }

    Text(
    text = "Speed ( $speed ) ",
    color = Color.Black,
    fontWeight = FontWeight.Bold,
    fontSize = 15.sp, modifier = Modifier.padding(horizontal = 10.dp)

    )
    Button(
    onClick = {
    speed += 0.25f
    },
    colors = ButtonDefaults.buttonColors(
    backgroundColor = Color(0xFF0F9D58)
    )
    ) {
    Text(
    text = "+",
    color = Color.White,
    fontWeight = FontWeight.Bold,
    fontSize = 20.sp
    )
    }
    }

    Button(
    onClick = {
    isPlaying = !isPlaying
    },
    colors = ButtonDefaults.buttonColors(
    backgroundColor = Color(0xFF0F9D58)
    )
    ) {
    Text(
    text = if (isPlaying) "Pause" else "Play",
    color = Color.White
    )
    }
    }
    }
    }
    }

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

    Flutter | 启动,渲染,setState 流程

    前言 用了这么久 Flutter 了,居然都不知道他的启动过程,真的是学之有愧啊,今天我们来分析一下 Flutter 的启动流程,以及他的渲染过程,对其做一个简单的剖析。 启动流程 Flutter 的启动入口在 lib/main.dart 里的 main() ...
    继续阅读 »

    前言


    用了这么久 Flutter 了,居然都不知道他的启动过程,真的是学之有愧啊,今天我们来分析一下 Flutter 的启动流程,以及他的渲染过程,对其做一个简单的剖析。


    启动流程


    Flutter 的启动入口在 lib/main.dart 里的 main() 函数中,他是 Dart 应用程序的起点,main 函数中最简单的实现如下:


    void main() => runApp(MyApp());

    可以看到,main 函数中只调用了 runApp() 方法,我们看看它里面都干了什么:


    void runApp(Widget app) {
    WidgetsFlutterBinding.ensureInitialized()
    ..scheduleAttachRootWidget(app)
    ..scheduleWarmUpFrame();
    }

    接收了一个 widget 参数,它是 Flutter 启动后要展示的第一个组件,而 WidgetsFlutterBinding 正是绑定 widgetFlutter 引擎的桥梁,定义如下:


    /// 基于 Widgets 框架的应用程序的具体绑定。
    class WidgetsFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding, ServicesBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {

    static WidgetsBinding ensureInitialized() {
    if (WidgetsBinding.instance == null)
    WidgetsFlutterBinding();
    return WidgetsBinding.instance!;
    }
    }

    可以看到 WidgetsFlutterBinding 继承自 BindingBase ,并且混入了很多 Binding,在介绍这些 Binding 之前我们先介绍一下 Window ,下面是 Window 的官方解释:



    The most basic interface to the host operating system's user interface.


    主机操作系统用户界面的最基本界面。



    很明显,Window 正是 Flutter Framework 连接宿主操作系统的接口,


    我们看一下 Window 类的部分定义


    @Native("Window,DOMWindow")
    class Window extends EventTarget implements WindowEventHandlers, WindowBase GlobalEventHandlers,
    _WindowTimers, WindowBase64 {

    // 当前设备的DPI,即一个逻辑像素显示多少物理像素,数字越大,显示效果就越精细保真。
    // DPI是设备屏幕的固件属性,如Nexus 6的屏幕DPI为3.5
    double get devicePixelRatio => _devicePixelRatio;

    // Flutter UI绘制区域的大小
    Size get physicalSize => _physicalSize;

    // 当前系统默认的语言Locale
    Locale get locale;

    // 当前系统字体缩放比例。
    double get textScaleFactor => _textScaleFactor;

    // 当绘制区域大小改变回调
    VoidCallback get onMetricsChanged => _onMetricsChanged;
    // Locale发生变化回调
    VoidCallback get onLocaleChanged => _onLocaleChanged;
    // 系统字体缩放变化回调
    VoidCallback get onTextScaleFactorChanged => _onTextScaleFactorChanged;
    // 绘制前回调,一般会受显示器的垂直同步信号VSync驱动,当屏幕刷新时就会被调用
    FrameCallback get onBeginFrame => _onBeginFrame;
    // 绘制回调
    VoidCallback get onDrawFrame => _onDrawFrame;
    // 点击或指针事件回调
    PointerDataPacketCallback get onPointerDataPacket => _onPointerDataPacket;
    // 调度Frame,该方法执行后,onBeginFrame和onDrawFrame将紧接着会在合适时机被调用,
    // 此方法会直接调用Flutter engine的Window_scheduleFrame方法
    void scheduleFrame() native 'Window_scheduleFrame';
    // 更新应用在GPU上的渲染,此方法会直接调用Flutter engine的Window_render方法
    void render(Scene scene) native 'Window_render';

    // 发送平台消息
    void sendPlatformMessage(String name,
    ByteData data,
    PlatformMessageResponseCallback callback) ;
    // 平台通道消息处理回调
    PlatformMessageCallback get onPlatformMessage => _onPlatformMessage;

    ... //其它属性及回调

    }

    可以看到 Window 中包含了当前设备和系统的一些信息和 Flutter Engine 的一些回调。


    现在回过头来看一下 WidgetsFlutterBinding 混入的各种 Binding。通过查看这些 Binding 的源码,我们可以发现这些 Binding 中基本都是监听并处理 Window 对象中的一些事件,然后将这些事件安装 Framework 的模型进行包装,抽象后然后进行分发。可以看到 WidgetsFlutterBinding 正是粘连 Flutter engine 与上层 Framework 的胶水。




    • GestureBinding:提供了 window.onPointerDataPacket 回调,绑定 Fragment 手势子系统,是 Framework 事件模型与底层事件的绑定入口。


      mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, HitTestTarget {
      @override
      void initInstances() {
      super.initInstances();
      _instance = this;
      window.onPointerDataPacket = _handlePointerDataPacket;
      }
      }
      复制代码



    • ServiceBinidng:提供了 window.onPlatformMessage 回调,用户绑定平台消息通道(message channel) ,主要处理原生和 Flutter 通信。


      mixin SchedulerBinding on BindingBase {
      @override
      void initInstances() {
      super.initInstances();
      _instance = this;
      if (!kReleaseMode) {
      addTimingsCallback((List<FrameTiming> timings) {
      timings.forEach(_profileFramePostEvent);
      });
      }
      }



    • SchedulerBinding:提供了 window.onBeginFramewindow.onDrawFrame 回调,监听刷新事件,绑定 Framework 绘制调度子系统。




    • PaintingBinding :绑定绘制库,主要用户处理图片缓存




    • SemanticsBidning:语义化层与 Flutter engine 的桥梁,主要是辅助功能的底层支持。




    • RendererBinding:提供了 window.onMetricsChangedwindow.onTextScaleFactorChanged 等回调。他是渲染树与 Flutter engine 的桥梁。




    • WidgetsBinding:提供了 window.onLocaleChangeonBulidScheduled 等回调。他是 Flutter widget 层与 engine 的桥梁。




    widgetsFlutterBinding.ensureInitiallized() 负责初始化一个 widgetsBinding 的全局单例,紧接着会调用 WidgetBindingattachRootwWidget 方法,该方法负责将根 Widget 添加到 RenderView 上,代码如下:


    void scheduleAttachRootWidget(Widget rootWidget) {
    Timer.run(() {
    attachRootWidget(rootWidget);
    });
    }

    void attachRootWidget(Widget rootWidget) {
    final bool isBootstrapFrame = renderViewElement == null;
    _readyToProduceFrames = true;
    _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
    container: renderView,
    debugShortDescription: '[root]',
    child: rootWidget,
    ).attachToRenderTree(buildOwner!, renderViewElement as RenderObjectToWidgetElement<RenderBox>?);
    if (isBootstrapFrame) {
    SchedulerBinding.instance!.ensureVisualUpdate();
    }
    }

    注意,代码中有 renderViewrenderViewElement 两个变量,renderView 是一个 Renderobject ,他是渲染树的根。而 renderViewElement 是 renderView 对应的 Element 对象。


    可见该方法主要完成了根 widget 到根RenderObject 再到根 Element 的整个关联过程,我们在看看 attachToRenderTree 的源码实现过程:


    RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T>? element ]) {
    if (element == null) {
    owner.lockState(() {
    element = createElement();
    assert(element != null);
    element!.assignOwner(owner);
    });
    owner.buildScope(element!, () {
    element!.mount(null, null);
    });
    } else {
    element._newWidget = this;
    element.markNeedsBuild();
    }
    return element!;
    }

    该方法负责创建根 element,即 RenderObjectToWidgetElement ,并且将 element 与 widget 进行关联,即创建出 widget 树对应的 element 树。


    如果 element 创建过了,则将根 element 中关联的 widget 设为新的,由此可以看出 element 只会创建一次,后面会进行复用。那么 BuildOwner 是什么呢?,其实他就是 widget framework 的管理类,它跟踪哪些 widget 需要重新构建。


    组件树在构建完毕后,回到 runApp 的实现中,当调完 attachRootWidget 后,最后一行会调用 WidgetsFlutterBainding 实例的 scheduleWarmUpFrame() 方法,该方法的是现在 SchedulerBinding 中,他被调用后会立即进行一次绘制,在此次绘制结束前,该方法就会锁定事件分发,也就是说在本次绘制结束完成之前 Flutter 不会响应各种事件,这可以保证在绘制过程中不会触发新的重绘。


    总结


    通过上面上面的分析我们可以知道 WidgetsFlutterBinding 就像是一个胶水,它里面会监听并处理 window 对象的事件,并且将这些事件按照 framework的模型进行包装并且分发。所以说 widgetsFlutterBinding 正是连接 Flutter engine 与上传 Framework 的胶水。


      WidgetsFlutterBinding.ensureInitialized()
    ..scheduleAttachRootWidget(app)
    ..scheduleWarmUpFrame();


    • ensureInitialized :负责初始化 WidgetsFlutterBinding ,并且监听 window 的事件进行包装分发。

    • scheduleAttachRootWidget:在该方法的后续中,会创建根 Element ,调用 mount 完成 elementRenderObject 树的创建

    • scheduleWarmUpFrame:开始绘制第一帧


    渲染官线


    Frame


    一次绘制过程,我们可以将其称为一帧(frame),我们知道 flutter 可以实现 60 fps,就是指 1 秒中可以进行60次重绘,FPS 越大,界面就会越流畅。


    这里需要说明的是 Flutter 中的 frame 并不等于屏幕的刷新帧,因为 Flutter UI 框架并不是每次屏幕刷新都会触发,这是因为,如果 UI 在一段时间不变,那么每次重新走一遍渲染流程是不必要的,因此 Flutter 在第一帧渲染结束后会采取一种主动请求 frame 的方式来实现只有当 UI 可能会改变时才会重新走渲染流程。


    1,Flutter 会在 window 上注册一个 onBeginFrame 和一个 onDrawFrame回调,在 onDrawFrame 回调中最终会调用 drawFrame


    2,当我们调用 window.scheduleFrame 方法之后,Flutter 引擎会在合适时机(可以认为是在屏幕下一次刷新之前,具体取决于 Flutter 引擎实现) 来调用 onBeginFrame 和 onDrawFrame


    在调用 window.scheduleFrame 之前会对 onBeginFrame 和 onDrawFrame 进行注册,如下所示:


    void scheduleFrame() {
    if (_hasScheduledFrame || !framesEnabled)
    return;
    assert(() {
    if (debugPrintScheduleFrameStacks)
    debugPrintStack(label: 'scheduleFrame() called. Current phase is $schedulerPhase.');
    return true;
    }());
    ensureFrameCallbacksRegistered();
    window.scheduleFrame();
    _hasScheduledFrame = true;
    }

    void ensureFrameCallbacksRegistered() {
    window.onBeginFrame ??= _handleBeginFrame;
    window.onDrawFrame ??= _handleDrawFrame;
    }

    可以看见,只有主动调用 scheduleFrame 之后,才会调用 drawFrame(该方法是注册的回调)。


    所以我们在 Flutter 中提到 frame 时,如无特别说明,则是和 drawFrame() 相互对应,而不是和屏幕的刷新相对应。


    Frame 处理流程


    当有新的 frame 到来时,开始调用 SchedulerBinding.handleDrawFrame 来处理 frame,具体过程就是执行四个任务队列:transientCallbacks,midFrameMicotasks,persistentCallbacks,postFrameCallbacks。当四个任务队列执行完毕后当前 frame 结束。


    综上,Flutter 将整个生命周期分为 5 种状态,通过 SchedulerPhase 来表示他们:


    enum SchedulerPhase {
    /// 空闲状态,并没有 frame 在处理,这种状态表示页面未发生变化,并不需要重新渲染
    /// 如果页面发生变化,需要调用 scheduleFrame 来请求 frame。
    /// 注意,空闲状态只是代表没有 frame 在处理。通常微任务,定时器回调或者用户回调事件都有可能被执行
    /// 比如监听了 tap 事件,用户点击后我们 onTap回调就是在 onTap 执行的
    idle,

    /// 执行 临时 回调任务,临时回调任务只能被执行一次,执行后会被移出临时任务队列。
    /// 典型代表就是动画回调会在该阶段执行
    transientCallbacks,

    /// 在执行临时任务是可能会产生一下新的微任务,比如在执行第一个临时任务时创建了一个 Fluture,
    /// 且这个 Future 在所有任务执行完毕前就已经 resolve
    /// 这种情况 Future 的回调将会在 [midFrameMicrotasks] 阶段执行
    midFrameMicrotasks,

    /// 执行一些持久的任务(每一个 frame 都要执行的任务),比如渲染官线(构建,布局,绘制)
    /// 就是在该任务队列执行的
    persistentCallbacks,

    /// 在当前 frame 在结束之前将会执行 postFrameCallbacks,通常进行一些清理工作和请求新的 frame
    postFrameCallbacks,
    }

    需要注意,接下来需要重点介绍的渲染管线就是在 persistentCallbacks 中执行的。


    渲染管线(rendering pipline)


    当我们页面需要发生变化时,我们需要调用 scheduleFrame() 方法去请求 frame,该方法中会注册 _handleBeginFrame_handleDrawFrame。 当 frame 到来时就会执行 _handleDrawFrame,代码如下:


    void _handleDrawFrame() {
    //判断当前 frame 是否需要推迟,这里的推迟原因是当前坑是预热帧
    if (_rescheduleAfterWarmUpFrame) {
    _rescheduleAfterWarmUpFrame = false;
    //添加一个回调,该回调会在当前帧结束后执行
    addPostFrameCallback((Duration timeStamp) {
    _hasScheduledFrame = false;
    //重新请求 frame。
    scheduleFrame();
    });
    return;
    }
    handleDrawFrame();
    }

    void handleDrawFrame() {
    assert(_schedulerPhase == SchedulerPhase.midFrameMicrotasks);
    Timeline.finishSync(); // end the "Animate" phase
    try {
    // 切换当前生命周期状态
    _schedulerPhase = SchedulerPhase.persistentCallbacks;
    // 执行持久任务的回调,
    for (final FrameCallback callback in _persistentCallbacks)
    _invokeFrameCallback(callback, _currentFrameTimeStamp!);

    // postFrame 回调
    _schedulerPhase = SchedulerPhase.postFrameCallbacks;
    final List<FrameCallback> localPostFrameCallbacks =
    List<FrameCallback>.from(_postFrameCallbacks);
    _postFrameCallbacks.clear();
    for (final FrameCallback callback in localPostFrameCallbacks)
    _invokeFrameCallback(callback, _currentFrameTimeStamp!);
    } finally {
    // 将状态改为空闲状态
    _schedulerPhase = SchedulerPhase.idle;
    Timeline.finishSync(); // end the Frame
    //....
    _currentFrameTimeStamp = null;
    }
    }

    在上面的代码中,对持久任务进行了遍历,并且进行回调,对应的是 _persistentCallbacks ,通过对调用栈的分析,发现该回调是在初始化 RendererBinding 的时候被添加到 _persistentCallbacks 中的:


    mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, SemanticsBinding, HitTestable {
    @override
    void initInstances() {
    super.initInstances();
    //添加持久任务回调......
    addPersistentFrameCallback(_handlePersistentFrameCallback);
    initMouseTracker();
    if (kIsWeb) {
    //添加 postFrame 任务回调
    addPostFrameCallback(_handleWebFirstFrame);
    }
    }
    void addPersistentFrameCallback(FrameCallback callback) {
    _persistentCallbacks.add(callback);
    }

    所以最终的回调就是 _handlePersistentFrameCallback


    void _handlePersistentFrameCallback(Duration timeStamp) {
    drawFrame();
    _scheduleMouseTrackerUpdate();
    }

    在上面代码中,调用到了 drawFrame 方法。




    通过上面的分析之后,我们知道了当 frame 到来时,会调用到 drawFrame 中,由于 drawFrame 有一个实现方法,所以首先会调用到 WidgetsBinding 的 drawFrame() 方法,如下:


    void drawFrame() {
    .....//省略无关
    try {
    if (renderViewElement != null)
    buildOwner!.buildScope(renderViewElement!); // 1.重新构建widget树
    super.drawFrame();
    buildOwner!.finalizeTree();
    }
    }

    最终的调用如下:


    void drawFrame() {
    assert(renderView != null);
    pipelineOwner.flushLayout(); // 2.更新布局
    pipelineOwner.flushCompositingBits();//3.更新“层合成”信息
    pipelineOwner.flushPaint(); // 4.重绘
    if (sendFramesToEngine) {
    renderView.compositeFrame(); // 5. 上屏,会将绘制出的bit数据发送给GPU
    ...../////
    }
    }

    可以到上面代码主要做了五件事:


    1,重新构建 widget 树(buildScope())


    2,更新布局(flushLayout())


    3,更新"层合成"信息(flushCompositingBits())


    4,重绘(flushPaint())


    5,上屏:将绘制的产物显示在屏幕上


    上面的五部我们称为 rendering pipline ,中文翻译为 “渲染流水线” 或者 “渲染管线”,而这五个步骤便是重中之重。下面我们以 setState 的更新流程为例先对整个更新流程有一个比较深的印象。


    setState 执行流


    void setState(VoidCallback fn) {
    assert(fn != null);
    //执行 callback,返回值不能是 future
    final Object? result = fn() as dynamic;
    assert(() {
    if (result is Future) {
    throw ...//
    }
    }());
    _element!.markNeedsBuild();
    }

    void markNeedsBuild() {
    ....//
    //标注该 element 需要重建
    _dirty = true;
    owner!.scheduleBuildFor(this);
    }

    void scheduleBuildFor(Element element) {
    //注释1
    if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
    _scheduledFlushDirtyElements = true;
    onBuildScheduled!();
    }
    //注释2
    _dirtyElements.add(element);
    element._inDirtyList = true;
    }

    当调用 setState 后:


    1,首先调用 markNeedsBuild 方法,将 element 的 dirty 标记为 true,表示需要重建


    2,接着调用 scheduleBuildFor ,将当前的 element 添加到 _dirtyElements 列表中(注释2)


    下面我们着重看一下 注释1的代码,


    首先判断 _scheduledFlushDirtyElements 如果为 false,该字段值初始值默认就是 false,接着判断 onBuildScheduled 不为 null,其实 onBuildScheduled 在 WidgetBinding初始化的时候就已经创建了,所以他是不会为 null 的。


    当条件成立后,就会直接执行 onBuildScheduled 回调。我们跟踪一下:


    mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, RendererBinding, SemanticsBinding {
    @override
    void initInstances() {
    super.initInstances();
    ...///
    buildOwner!.onBuildScheduled = _handleBuildScheduled
    }

    void _handleBuildScheduled() {
    ...///
    ensureVisualUpdate();
    }

    根据上面代码我们可以知道 onBuildScheduled 确实是在 WidgetsBinding 的初始化方法中进行初始化的。并且他的实现中调用了 ensureVisualUpdate 方法,我们继续跟进一下:


    void ensureVisualUpdate() {
    switch (schedulerPhase) {
    case SchedulerPhase.idle:
    case SchedulerPhase.postFrameCallbacks:
    scheduleFrame();
    return;
    case SchedulerPhase.transientCallbacks:
    case SchedulerPhase.midFrameMicrotasks:
    case SchedulerPhase.persistentCallbacks:
    return;
    }
    }

    上面代码中,判断了 schedulerPhase 的状态,如果是 idle 和 postFrameCallbacks 状态的时候,就开始调用 scheduleFrame。



    对于上面每种状态所代表的意义,在文章上面已经说过了,这里就不在赘述。值得一提的是,在每次 frame 流程完成的时候,在 finally 代码块中将状态又改为了 idle 。这也侧面说明如果你频繁的 setState 的时候,如果上次的渲染流程没有完成,则不会发起新的渲染。



    接着继续看 scheduleFrame:


    void scheduleFrame() {
    //判断流程是否已经开始了
    if (_hasScheduledFrame || !framesEnabled)
    return;
    // 注释1
    ensureFrameCallbacksRegistered();
    // 注释2
    window.scheduleFrame();
    _hasScheduledFrame = true;
    }

    注释1:注册 onBeginFrame 和 onDrawFrame ,这两个函数类型的字段在上面的 "渲染管线中已经说过了"。


    注释2:flutter framework 想 Flutter Engine 发起一个请求,接着 Flutter 引擎会在合适的时机去调用 onBeginFrame 和 onDrawFrame。这个时机可以认为是屏幕下一次刷新之前,具体取决于 Flutter 引擎实现。


    到此,setState 中最核心的就是触发了一个 请求,在下一次屏幕刷新的时候就会回调 onBeginFrame,执行完成之后才会调用 onDrawFrame 方法。




    void handleBeginFrame(Duration? rawTimeStamp) {
    ...///
    assert(schedulerPhase == SchedulerPhase.idle);
    _hasScheduledFrame = false;
    try {
    Timeline.startSync('Animate', arguments: timelineArgumentsIndicatingLandmarkEvent);
    //将生命周期改为 transientCallbacks,表示正在执行一些临时任务的回调
    _schedulerPhase = SchedulerPhase.transientCallbacks;
    final Map<int, _FrameCallbackEntry> callbacks = _transientCallbacks;
    _transientCallbacks = <int, _FrameCallbackEntry>{};
    callbacks.forEach((int id, _FrameCallbackEntry callbackEntry) {
    if (!_removedIds.contains(id))
    _invokeFrameCallback(callbackEntry.callback, _currentFrameTimeStamp!, callbackEntry.debugStack);
    });
    _removedIds.clear();
    } finally {
    _schedulerPhase = SchedulerPhase.midFrameMicrotasks;
    }
    }

    上面代码主要是执行了_transientCallbacks 的回调方法。执行完成后将生命周期改为了 midFrameMicrotasks。


    接下来就是执行 handlerDrawFrame 方法了。该方法在上面已经分析过了,已经知道它最终就会走到 drawFrame 方法中。


    # WidgetsBindign.drawFrame()
    void drawFrame() {
    .....//省略无关
    try {
    if (renderViewElement != null)
    buildOwner!.buildScope(renderViewElement!); // 1.重新构建widget树
    super.drawFrame();
    buildOwner!.finalizeTree();
    }
    }
    # RendererBinding.drawFrame()
    void drawFrame() {
    assert(renderView != null);
    pipelineOwner.flushLayout(); // 2.更新布局
    pipelineOwner.flushCompositingBits();//3.更新“层合成”信息
    pipelineOwner.flushPaint(); // 4.重绘
    if (sendFramesToEngine) {
    renderView.compositeFrame(); // 5. 上屏,会将绘制出的bit数据发送给GPU
    ...../////
    }
    }

    以上,便是 setState 调用的大概过程,实际的流程会更加复杂一点,例如在这个过程中不允许再次调用 setState,还有在 frame 中会涉及到动画的调度,以及如何进行布局更新,重绘等。通过上面的分析,我们需要对整个流程有一个比较深的印象。


    至于上面 drawFrame 中的绘制流程,我们放在下一篇文章中介绍。


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

    其他都是错的,只有这一篇正确解决:Flutter Textfield长按报错修复:NosuchMethodError: The getter ‘pasterBu

    正确解决:Flutter Textfield长按报错修复:NosuchMethodError: The getter 'pasterButtonLabel' was ca ????????? 为什么叫正确解决?? 关于这个问题,我在百度上看过很多人的答案,基本...
    继续阅读 »

    正确解决:Flutter Textfield长按报错修复:NosuchMethodError: The getter 'pasterButtonLabel' was ca ?????????


    为什么叫正确解决??
    关于这个问题,我在百度上看过很多人的答案,基本无一例外都是,说:“Cupertino缺少了对应的非英文版本的支持”。
    大家真的看过源码吗?真的是缺少Cupertino么?我是真不相信的,flutter出了这么多年,连个中文都不支持?然后我就查阅了源码:
    我发现了这个类 GlobalCupertinoLocalizations
    有木有很眼熟,他和
    GlobalMaterialLocalizations & GlobalWidgetsLocalizations 没啥区别


    class _GlobalCupertinoLocalizationsDelegate extends LocalizationsDelegate {
    const _GlobalCupertinoLocalizationsDelegate();

    @override
    bool isSupported(Locale locale) => kCupertinoSupportedLanguages.contains(locale.languageCode);

    static final Map> _loadedTranslations = >{};

    @override
    Future load(Locale locale) {
    assert(isSupported(locale));
    return _loadedTranslations.putIfAbsent(locale, () {
    util.loadDateIntlDataIfNotLoaded();

    final String localeName = intl.Intl.canonicalizedLocale(locale.toString());
    assert(
    locale.toString() == localeName,
    'Flutter does not support the non-standard locale form $locale (which '
    'might be $localeName',
    );

    late intl.DateFormat fullYearFormat;
    late intl.DateFormat dayFormat;
    late intl.DateFormat mediumDateFormat;
    // We don't want any additional decoration here. The am/pm is handled in
    // the date picker. We just want an hour number localized.
    late intl.DateFormat singleDigitHourFormat;
    late intl.DateFormat singleDigitMinuteFormat;
    late intl.DateFormat doubleDigitMinuteFormat;
    late intl.DateFormat singleDigitSecondFormat;
    late intl.NumberFormat decimalFormat;

    void loadFormats(String? locale) {
    fullYearFormat = intl.DateFormat.y(locale);
    dayFormat = intl.DateFormat.d(locale);
    mediumDateFormat = intl.DateFormat.MMMEd(locale);
    // TODO(xster): fix when https://github.com/dart-lang/intl/issues/207 is resolved.
    singleDigitHourFormat = intl.DateFormat('HH', locale);
    singleDigitMinuteFormat = intl.DateFormat.m(locale);
    doubleDigitMinuteFormat = intl.DateFormat('mm', locale);
    singleDigitSecondFormat = intl.DateFormat.s(locale);
    decimalFormat = intl.NumberFormat.decimalPattern(locale);
    }

    if (intl.DateFormat.localeExists(localeName)) {
    loadFormats(localeName);
    } else if (intl.DateFormat.localeExists(locale.languageCode)) {
    loadFormats(locale.languageCode);
    } else {
    loadFormats(null);
    }

    return SynchronousFuture(getCupertinoTranslation(
    locale,
    fullYearFormat,
    dayFormat,
    mediumDateFormat,
    singleDigitHourFormat,
    singleDigitMinuteFormat,
    doubleDigitMinuteFormat,
    singleDigitSecondFormat,
    decimalFormat,
    )!);
    });
    }

    @override
    bool shouldReload(_GlobalCupertinoLocalizationsDelegate old) => false;

    @override
    String toString() => 'GlobalCupertinoLocalizations.delegate(${kCupertinoSupportedLanguages.length} locales)';
    }

    源码中加载语言也没说不支持中文啊!!
    还有网上很多配置本地化时候都是这么写的:


              GlobalMaterialLocalizations.delegate,
    GlobalWidgetsLocalizations.delegate,

    仔细看了源码,我想说:
    这么写不香么??
    GlobalMaterialLocalizations.delegates



    /// A value for [MaterialApp.localizationsDelegates] that's typically used by
    /// internationalized apps.
    ///
    /// ## Sample code
    ///
    /// To include the localizations provided by this class and by
    /// [GlobalWidgetsLocalizations] in a [MaterialApp],
    /// use [GlobalMaterialLocalizations.delegates] as the value of
    /// [MaterialApp.localizationsDelegates], and specify the locales your
    /// app supports with [MaterialApp.supportedLocales]:
    ///
    /// ```dart
    /// new MaterialApp(
    /// localizationsDelegates: GlobalMaterialLocalizations.delegates,
    /// supportedLocales: [
    /// const Locale('en', 'US'), // English
    /// const Locale('he', 'IL'), // Hebrew
    /// ],
    /// // ...
    /// )
    /// ```
    static const List> delegates = >[
    GlobalCupertinoLocalizations.delegate,
    GlobalMaterialLocalizations.delegate,
    GlobalWidgetsLocalizations.delegate,
    ];
    }

    仅此一篇文章,我希望大家认真阅读源码,提升水平

    收起阅读 »

    字节跳动面试官:请你实现一个大文件上传和断点续传(下)

    接 字节跳动面试官:请你实现一个大文件上传和断点续传(上) 断点续传的原理在于前端/服务端需要记住已上传的切片,这样下次上传就可以跳过之前已上传的部分,有两种方案实现记忆的功能第一种是前端的解决方案,第二种是服务端,而前端方案有一个缺陷,如果换了个浏览器就失...
    继续阅读 »

    字节跳动面试官:请你实现一个大文件上传和断点续传(上)



    断点续传

    断点续传的原理在于前端/服务端需要记住已上传的切片,这样下次上传就可以跳过之前已上传的部分,有两种方案实现记忆的功能

    • 前端使用 localStorage 记录已上传的切片 hash

    • 服务端保存已上传的切片 hash,前端每次上传前向服务端获取已上传的切片

    第一种是前端的解决方案,第二种是服务端,而前端方案有一个缺陷,如果换了个浏览器就失去了记忆的效果,所以这里选取后者

    生成 hash

    无论是前端还是服务端,都必须要生成文件和切片的 hash,之前我们使用文件名 + 切片下标作为切片 hash,这样做文件名一旦修改就失去了效果,而事实上只要文件内容不变,hash 就不应该变化,所以正确的做法是根据文件内容生成 hash,所以我们修改一下 hash 的生成规则

    这里用到另一个库 spark-md5,它可以根据文件内容计算出文件的 hash 值,另外考虑到如果上传一个超大文件,读取文件内容计算 hash 是非常耗费时间的,并且会引起 UI 的阻塞,导致页面假死状态,所以我们使用 web-worker 在 worker 线程计算 hash,这样用户仍可以在主界面正常的交互

    由于实例化 web-worker 时,参数是一个 js 文件路径且不能跨域,所以我们单独创建一个 hash.js 文件放在 public 目录下,另外在 worker 中也是不允许访问 dom 的,但它提供了importScripts 函数用于导入外部脚本,通过它导入 spark-md5

    // /public/hash.js
    self.importScripts("/spark-md5.min.js"); // 导入脚本

    // 生成文件 hash
    self.onmessage = e => {
    const { fileChunkList } = e.data;
    const spark = new self.SparkMD5.ArrayBuffer();
    let percentage = 0;
    let count = 0;
    const loadNext = index => {
      const reader = new FileReader();
      reader.readAsArrayBuffer(fileChunkList[index].file);
      reader.onload = e => {
        count++;
        spark.append(e.target.result);
        if (count === fileChunkList.length) {
          self.postMessage({
            percentage: 100,
            hash: spark.end()
          });
          self.close();
        } else {
          percentage += 100 / fileChunkList.length;
          self.postMessage({
            percentage
          });
          // 递归计算下一个切片
          loadNext(count);
        }
      };
    };
    loadNext(0);
    };
    复制代码

    在 worker 线程中,接受文件切片 fileChunkList,利用 FileReader 读取每个切片的 ArrayBuffer 并不断传入 spark-md5 中,每计算完一个切片通过 postMessage 向主线程发送一个进度事件,全部完成后将最终的 hash 发送给主线程

    spark-md5 需要根据所有切片才能算出一个 hash 值,不能直接将整个文件放入计算,否则即使不同文件也会有相同的 hash,具体可以看官方文档

    spark-md5

    接着编写主线程与 worker 线程通讯的逻辑

    +      // 生成文件 hash(web-worker)
    +   calculateHash(fileChunkList) {
    +     return new Promise(resolve => {
    +       // 添加 worker 属性
    +       this.container.worker = new Worker("/hash.js");
    +       this.container.worker.postMessage({ fileChunkList });
    +       this.container.worker.onmessage = e => {
    +         const { percentage, hash } = e.data;
    +         this.hashPercentage = percentage;
    +         if (hash) {
    +           resolve(hash);
    +         }
    +       };
    +     });
      },
      async handleUpload() {
        if (!this.container.file) return;
        const fileChunkList = this.createFileChunk(this.container.file);
    +     this.container.hash = await this.calculateHash(fileChunkList);
        this.data = fileChunkList.map(({ file },index) => ({
    +       fileHash: this.container.hash,
          chunk: file,
          hash: this.container.file.name + "-" + index, // 文件名 + 数组下标
          percentage:0
        }));
        await this.uploadChunks();
      }  
    复制代码

    主线程使用 postMessage 给 worker 线程传入所有切片 fileChunkList,并监听 worker 线程发出的 postMessage 事件拿到文件 hash

    加上显示计算 hash 的进度条,看起来像这样

    img

    至此前端需要将之前用文件名作为 hash 的地方改写为 workder 返回的这个 hash

    img

    服务端则使用 hash 作为切片文件夹名,hash + 下标作为切片名,hash + 扩展名作为文件名,没有新增的逻辑

    img

    img

    文件秒传

    在实现断点续传前先简单介绍一下文件秒传

    所谓的文件秒传,即在服务端已经存在了上传的资源,所以当用户再次上传时会直接提示上传成功

    文件秒传需要依赖上一步生成的 hash,即在上传前,先计算出文件 hash,并把 hash 发送给服务端进行验证,由于 hash 的唯一性,所以一旦服务端能找到 hash 相同的文件,则直接返回上传成功的信息即可

    +    async verifyUpload(filename, fileHash) {
    +       const { data } = await this.request({
    +         url: "http://localhost:3000/verify",
    +         headers: {
    +           "content-type": "application/json"
    +         },
    +         data: JSON.stringify({
    +           filename,
    +           fileHash
    +         })
    +       });
    +       return JSON.parse(data);
    +     },
      async handleUpload() {
        if (!this.container.file) return;
        const fileChunkList = this.createFileChunk(this.container.file);
        this.container.hash = await this.calculateHash(fileChunkList);
    +     const { shouldUpload } = await this.verifyUpload(
    +       this.container.file.name,
    +       this.container.hash
    +     );
    +     if (!shouldUpload) {
    +       this.$message.success("秒传:上传成功");
    +       return;
    +   }
        this.data = fileChunkList.map(({ file }, index) => ({
          fileHash: this.container.hash,
          index,
          hash: this.container.hash + "-" + index,
          chunk: file,
          percentage: 0
        }));
        await this.uploadChunks();
      }  
    复制代码

    秒传其实就是给用户看的障眼法,实质上根本没有上传

    image-20200109143511277

    :)

    服务端的逻辑非常简单,新增一个验证接口,验证文件是否存在即可

    + const extractExt = filename =>
    + filename.slice(filename.lastIndexOf("."), filename.length); // 提取后缀名
    const UPLOAD_DIR = path.resolve(__dirname, "..", "target"); // 大文件存储目录

    const resolvePost = req =>
    new Promise(resolve => {
      let chunk = "";
      req.on("data", data => {
        chunk += data;
      });
      req.on("end", () => {
        resolve(JSON.parse(chunk));
      });
    });

    server.on("request", async (req, res) => {
    if (req.url === "/verify") {
    +   const data = await resolvePost(req);
    +   const { fileHash, filename } = data;
    +   const ext = extractExt(filename);
    +   const filePath = path.resolve(UPLOAD_DIR, `${fileHash}${ext}`);
    +   if (fse.existsSync(filePath)) {
    +     res.end(
    +       JSON.stringify({
    +         shouldUpload: false
    +       })
    +     );
    +   } else {
    +     res.end(
    +       JSON.stringify({
    +         shouldUpload: true
    +       })
    +     );
    +   }
    }
    });
    server.listen(3000, () => console.log("正在监听 3000 端口"));
    复制代码

    暂停上传

    讲完了生成 hash 和文件秒传,回到断点续传

    断点续传顾名思义即断点 + 续传,所以我们第一步先实现“断点”,也就是暂停上传

    原理是使用 XMLHttpRequest 的 abort 方法,可以取消一个 xhr 请求的发送,为此我们需要将上传每个切片的 xhr 对象保存起来,我们再改造一下 request 方法

       request({
        url,
        method = "post",
        data,
        headers = {},
        onProgress = e => e,
    +     requestList
      }) {
        return new Promise(resolve => {
          const xhr = new XMLHttpRequest();
          xhr.upload.onprogress = onProgress;
          xhr.open(method, url);
          Object.keys(headers).forEach(key =>
            xhr.setRequestHeader(key, headers[key])
          );
          xhr.send(data);
          xhr.onload = e => {
    +         // 将请求成功的 xhr 从列表中删除
    +         if (requestList) {
    +           const xhrIndex = requestList.findIndex(item => item === xhr);
    +           requestList.splice(xhrIndex, 1);
    +         }
            resolve({
              data: e.target.response
            });
          };
    +       // 暴露当前 xhr 给外部
    +       requestList?.push(xhr);
        });
      },
    复制代码

    这样在上传切片时传入 requestList 数组作为参数,request 方法就会将所有的 xhr 保存在数组中了

    img

    每当一个切片上传成功时,将对应的 xhr 从 requestList 中删除,所以 requestList 中只保存正在上传切片的 xhr

    之后新建一个暂停按钮,当点击按钮时,调用保存在 requestList 中 xhr 的 abort 方法,即取消并清空所有正在上传的切片

     handlePause() {
      this.requestList.forEach(xhr => xhr?.abort());
      this.requestList = [];
    }
    复制代码

    image-20200109143737924

    点击暂停按钮可以看到 xhr 都被取消了

    img

    恢复上传

    之前在介绍断点续传的时提到使用第二种服务端存储的方式实现续传

    由于当文件切片上传后,服务端会建立一个文件夹存储所有上传的切片,所以每次前端上传前可以调用一个接口,服务端将已上传的切片的切片名返回,前端再跳过这些已经上传切片,这样就实现了“续传”的效果

    而这个接口可以和之前秒传的验证接口合并,前端每次上传前发送一个验证的请求,返回两种结果

    • 服务端已存在该文件,不需要再次上传

    • 服务端不存在该文件或者已上传部分文件切片,通知前端进行上传,并把已上传的文件切片返回给前端

    所以我们改造一下之前文件秒传的服务端验证接口

    const extractExt = filename =>
    filename.slice(filename.lastIndexOf("."), filename.length); // 提取后缀名
    const UPLOAD_DIR = path.resolve(__dirname, "..", "target"); // 大文件存储目录

    const resolvePost = req =>
    new Promise(resolve => {
      let chunk = "";
      req.on("data", data => {
        chunk += data;
      });
      req.on("end", () => {
        resolve(JSON.parse(chunk));
      });
    });
     
    + // 返回已经上传切片名列表
    + const createUploadedList = async fileHash =>
    +   fse.existsSync(path.resolve(UPLOAD_DIR, fileHash))
    +   ? await fse.readdir(path.resolve(UPLOAD_DIR, fileHash))
    +   : [];

    server.on("request", async (req, res) => {
    if (req.url === "/verify") {
      const data = await resolvePost(req);
      const { fileHash, filename } = data;
      const ext = extractExt(filename);
      const filePath = path.resolve(UPLOAD_DIR, `${fileHash}${ext}`);
      if (fse.existsSync(filePath)) {
        res.end(
          JSON.stringify({
            shouldUpload: false
          })
        );
      } else {
        res.end(
          JSON.stringify({
            shouldUpload: true
    +         uploadedList: await createUploadedList(fileHash)
          })
        );
      }
    }
    });
    server.listen(3000, () => console.log("正在监听 3000 端口"));
    复制代码

    接着回到前端,前端有两个地方需要调用验证的接口

    • 点击上传时,检查是否需要上传和已上传的切片

    • 点击暂停后的恢复上传,返回已上传的切片

    新增恢复按钮并改造原来上传切片的逻辑



    +   async handleResume() {
    +     const { uploadedList } = await this.verifyUpload(
    +       this.container.file.name,
    +       this.container.hash
    +     );
    +     await this.uploadChunks(uploadedList);
      },
      async handleUpload() {
        if (!this.container.file) return;
        const fileChunkList = this.createFileChunk(this.container.file);
        this.container.hash = await this.calculateHash(fileChunkList);

    +     const { shouldUpload, uploadedList } = await this.verifyUpload(
          this.container.file.name,
          this.container.hash
        );
        if (!shouldUpload) {
          this.$message.success("秒传:上传成功");
          return;
        }

        this.data = fileChunkList.map(({ file }, index) => ({
          fileHash: this.container.hash,
          index,
          hash: this.container.hash + "-" + index,
          chunk: file,
          percentage: 0
        }));

    +     await this.uploadChunks(uploadedList);
      },
      // 上传切片,同时过滤已上传的切片
    +   async uploadChunks(uploadedList = []) {
        const requestList = this.data
    +       .filter(({ hash }) => !uploadedList.includes(hash))
          .map(({ chunk, hash, index }) => {
            const formData = new FormData();
            formData.append("chunk", chunk);
            formData.append("hash", hash);
            formData.append("filename", this.container.file.name);
            formData.append("fileHash", this.container.hash);
            return { formData, index };
          })
          .map(async ({ formData, index }) =>
            this.request({
              url: "http://localhost:3000",
              data: formData,
              onProgress: this.createProgressHandler(this.data[index]),
              requestList: this.requestList
            })
          );
        await Promise.all(requestList);
        // 之前上传的切片数量 + 本次上传的切片数量 = 所有切片数量时
        // 合并切片
    +     if (uploadedList.length + requestList.length === this.data.length) {
            await this.mergeRequest();
    +     }
      }
    复制代码

    image-20200109144331326

    这里给原来上传切片的函数新增 uploadedList 参数,即上图中服务端返回的切片名列表,通过 filter 过滤掉已上传的切片,并且由于新增了已上传的部分,所以之前合并接口的触发条件做了一些改动

    到这里断点续传的功能基本完成了

    进度条改进

    虽然实现了断点续传,但还需要修改一下进度条的显示规则,否则在暂停上传/接收到已上传切片时的进度条会出现偏差

    切片进度条

    由于在点击上传/恢复上传时,会调用验证接口返回已上传的切片,所以需要将已上传切片的进度变成 100%

       async handleUpload() {
        if (!this.container.file) return;
        const fileChunkList = this.createFileChunk(this.container.file);
        this.container.hash = await this.calculateHash(fileChunkList);
        const { shouldUpload, uploadedList } = await this.verifyUpload(
          this.container.file.name,
          this.container.hash
        );
        if (!shouldUpload) {
          this.$message.success("秒传:上传成功");
          return;
        }
        this.data = fileChunkList.map(({ file }, index) => ({
          fileHash: this.container.hash,
          index,
          hash: this.container.hash + "-" + index,
          chunk: file,
    +       percentage: uploadedList.includes(index) ? 100 : 0
        }));
        await this.uploadChunks(uploadedList);
      },
    复制代码

    uploadedList 会返回已上传的切片,在遍历所有切片时判断当前切片是否在已上传列表里即可

    文件进度条

    之前说到文件进度条是一个计算属性,根据所有切片的上传进度计算而来,这就遇到了一个问题

    img

    点击暂停会取消并清空切片的 xhr 请求,此时如果已经上传了一部分,就会发现文件进度条有倒退的现象

    img

    当点击恢复时,由于重新创建了 xhr 导致切片进度清零,所以总进度条就会倒退

    解决方案是创建一个“假”的进度条,这个假进度条基于文件进度条,但只会停止和增加,然后给用户展示这个假的进度条

    这里我们使用 Vue 的监听属性

      data: () => ({
    +   fakeUploadPercentage: 0
    }),
    computed: {
      uploadPercentage() {
        if (!this.container.file || !this.data.length) return 0;
        const loaded = this.data
          .map(item => item.size * item.percentage)
          .reduce((acc, cur) => acc + cur);
        return parseInt((loaded / this.container.file.size).toFixed(2));
      }
    },  
    watch: {
    +   uploadPercentage(now) {
    +     if (now > this.fakeUploadPercentage) {
    +       this.fakeUploadPercentage = now;
    +     }
      }
    },
    复制代码

    当 uploadPercentage 即真的文件进度条增加时,fakeUploadPercentage 也增加,一旦文件进度条后退,假的进度条只需停止即可

    至此一个大文件上传 + 断点续传的解决方案就完成了

    总结

    大文件上传

    • 前端上传大文件时使用 Blob.prototype.slice 将文件切片,并发上传多个切片,最后发送一个合并的请求通知服务端合并切片

    • 服务端接收切片并存储,收到合并请求后使用流将切片合并到最终文件

    • 原生 XMLHttpRequest 的 upload.onprogress 对切片上传进度的监听

    • 使用 Vue 计算属性根据每个切片的进度算出整个文件的上传进度

    断点续传

    • 使用 spark-md5 根据文件内容算出文件 hash

    • 通过 hash 可以判断服务端是否已经上传该文件,从而直接提示用户上传成功(秒传)

    • 通过 XMLHttpRequest 的 abort 方法暂停切片的上传

    • 上传前服务端返回已经上传的切片名,前端跳过这些切片的上传

    反馈的问题

    部分功能由于不方便测试,这里列出评论区收集到的一些问题,有兴趣的朋友可以提出你的想法/写个 demo 进一步交流

    • 没有做切片上传失败的处理

    • 使用 web socket 由服务端发送进度信息

    • 打开页面没有自动获取上传切片,而需要主动再次上传一次后才显示

    源代码

    源代码增加了一些按钮的状态,交互更加友好,文章表达比较晦涩的地方可以跳转到源代码查看

    file-upload

    谢谢观看 :)

    作者:yeyan1996
    来源:https://juejin.cn/post/6844904046436843527

    收起阅读 »

    字节跳动面试官:请你实现一个大文件上传和断点续传(上)

    前言事实上我在面试的时候确实被问到了这个问题,而且是一道在线 coding 的编程题,当时虽然思路正确,可惜最终也并不算完全答对本文将从零搭建前端和服务端,实现一个大文件上传和断点续传的 demo服务端:nodejs文章有误解的地方,欢迎指出,将在第一时间改正...
    继续阅读 »



    前言

    这段时间面试官都挺忙的,频频出现在博客文章标题,虽然我不是特别想蹭热度,但是实在想不到好的标题了-。-,蹭蹭就蹭蹭 :)

    事实上我在面试的时候确实被问到了这个问题,而且是一道在线 coding 的编程题,当时虽然思路正确,可惜最终也并不算完全答对

    结束后花了一段时间整理了下思路,那么究竟该如何实现一个大文件上传,以及在上传中如何实现断点续传的功能呢?

    本文将从零搭建前端和服务端,实现一个大文件上传和断点续传的 demo

    前端:vue element-ui

    服务端:nodejs

    文章有误解的地方,欢迎指出,将在第一时间改正,有更好的实现方式希望留下你的评论

    大文件上传

    整体思路

    前端

    前端大文件上传网上的大部分文章已经给出了解决方案,核心是利用 Blob.prototype.slice 方法,和数组的 slice 方法相似,调用的 slice 方法可以返回原文件的某个切片

    这样我们就可以根据预先设置好的切片最大数量将文件切分为一个个切片,然后借助 http 的可并发性,同时上传多个切片,这样从原本传一个大文件,变成了同时传多个小的文件切片,可以大大减少上传时间

    另外由于是并发,传输到服务端的顺序可能会发生变化,所以我们还需要给每个切片记录顺序

    服务端

    服务端需要负责接受这些切片,并在接收到所有切片后合并切片

    这里又引伸出两个问题

    1. 何时合并切片,即切片什么时候传输完成

    2. 如何合并切片

    第一个问题需要前端进行配合,前端在每个切片中都携带切片最大数量的信息,当服务端接受到这个数量的切片时自动合并,也可以额外发一个请求主动通知服务端进行切片的合并

    第二个问题,具体如何合并切片呢?这里可以使用 nodejs 的 读写流(readStream/writeStream),将所有切片的流传输到最终文件的流里

    talk is cheap,show me the code,接着我们用代码实现上面的思路

    前端部分

    前端使用 Vue 作为开发框架,对界面没有太大要求,原生也可以,考虑到美观使用 element-ui 作为 UI 框架

    上传控件

    首先创建选择文件的控件,监听 change 事件以及上传按钮




    复制代码

    请求逻辑

    考虑到通用性,这里没有用第三方的请求库,而是用原生 XMLHttpRequest 做一层简单的封装来发请求

    request({
        url,
        method = "post",
        data,
        headers = {},
        requestList
      }) {
        return new Promise(resolve => {
          const xhr = new XMLHttpRequest();
          xhr.open(method, url);
          Object.keys(headers).forEach(key =>
            xhr.setRequestHeader(key, headers[key])
          );
          xhr.send(data);
          xhr.onload = e => {
            resolve({
              data: e.target.response
            });
          };
        });
      }
    复制代码

    上传切片

    接着实现比较重要的上传功能,上传需要做两件事

    • 对文件进行切片

    • 将切片传输给服务端




    复制代码

    当点击上传按钮时,调用 createFileChunk 将文件切片,切片数量通过文件大小控制,这里设置 10MB,也就是说 100 MB 的文件会被分成 10 个切片

    createFileChunk 内使用 while 循环和 slice 方法将切片放入 fileChunkList 数组中返回

    在生成文件切片时,需要给每个切片一个标识作为 hash,这里暂时使用文件名 + 下标,这样后端可以知道当前切片是第几个切片,用于之后的合并切片

    随后调用 uploadChunks 上传所有的文件切片,将文件切片,切片 hash,以及文件名放入 FormData 中,再调用上一步的 request 函数返回一个 proimise,最后调用 Promise.all 并发上传所有的切片

    发送合并请求

    这里使用整体思路中提到的第二种合并切片的方式,即前端主动通知服务端进行合并,所以前端还需要额外发请求,服务端接受到这个请求时主动合并切片




    复制代码

    服务端部分

    简单使用 http 模块搭建服务端

    const http = require("http");
    const server = http.createServer();

    server.on("request", async (req, res) => {
    res.setHeader("Access-Control-Allow-Origin", "*");
    res.setHeader("Access-Control-Allow-Headers", "*");
    if (req.method === "OPTIONS") {
      res.status = 200;
      res.end();
      return;
    }
    });

    server.listen(3000, () => console.log("正在监听 3000 端口"));
    复制代码

    接受切片

    使用 multiparty 包处理前端传来的 FormData

    在 multiparty.parse 的回调中,files 参数保存了 FormData 中文件,fields 参数保存了 FormData 中非文件的字段

    const http = require("http");
    const path = require("path");
    const fse = require("fs-extra");
    const multiparty = require("multiparty");

    const server = http.createServer();
    + const UPLOAD_DIR = path.resolve(__dirname, "..", "target"); // 大文件存储目录

    server.on("request", async (req, res) => {
    res.setHeader("Access-Control-Allow-Origin", "*");
    res.setHeader("Access-Control-Allow-Headers", "*");
    if (req.method === "OPTIONS") {
      res.status = 200;
      res.end();
      return;
    }

    + const multipart = new multiparty.Form();

    + multipart.parse(req, async (err, fields, files) => {
    +   if (err) {
    +     return;
    +   }
    +   const [chunk] = files.chunk;
    +   const [hash] = fields.hash;
    +   const [filename] = fields.filename;
    +   const chunkDir = path.resolve(UPLOAD_DIR, filename);

    +   // 切片目录不存在,创建切片目录
    +   if (!fse.existsSync(chunkDir)) {
    +     await fse.mkdirs(chunkDir);
    +   }

    +     // fs-extra 专用方法,类似 fs.rename 并且跨平台
    +     // fs-extra 的 rename 方法 windows 平台会有权限问题
    +     // https://github.com/meteor/meteor/issues/7852#issuecomment-255767835
    +     await fse.move(chunk.path, `${chunkDir}/${hash}`);
    +   res.end("received file chunk");
    + });
    });

    server.listen(3000, () => console.log("正在监听 3000 端口"));
    复制代码

    image-20200110215559194

    查看 multiparty 处理后的 chunk 对象,path 是存储临时文件的路径,size 是临时文件大小,在 multiparty 文档中提到可以使用 fs.rename(由于我用的是 fs-extra,它的 rename 方法 windows 平台权限问题,所以换成了 fse.move) 移动临时文件,即移动文件切片

    在接受文件切片时,需要先创建存储切片的文件夹,由于前端在发送每个切片时额外携带了唯一值 hash,所以以 hash 作为文件名,将切片从临时路径移动切片文件夹中,最后的结果如下

    img

    合并切片

    在接收到前端发送的合并请求后,服务端将文件夹下的所有切片进行合并

    const http = require("http");
    const path = require("path");
    const fse = require("fs-extra");

    const server = http.createServer();
    const UPLOAD_DIR = path.resolve(__dirname, "..", "target"); // 大文件存储目录

    + const resolvePost = req =>
    +   new Promise(resolve => {
    +     let chunk = "";
    +     req.on("data", data => {
    +       chunk += data;
    +     });
    +     req.on("end", () => {
    +       resolve(JSON.parse(chunk));
    +     });
    +   });

    + const pipeStream = (path, writeStream) =>
    + new Promise(resolve => {
    +   const readStream = fse.createReadStream(path);
    +   readStream.on("end", () => {
    +     fse.unlinkSync(path);
    +     resolve();
    +   });
    +   readStream.pipe(writeStream);
    + });

    // 合并切片
    + const mergeFileChunk = async (filePath, filename, size) => {
    + const chunkDir = path.resolve(UPLOAD_DIR, filename);
    + const chunkPaths = await fse.readdir(chunkDir);
    + // 根据切片下标进行排序
    + // 否则直接读取目录的获得的顺序可能会错乱
    + chunkPaths.sort((a, b) => a.split("-")[1] - b.split("-")[1]);
    + await Promise.all(
    +   chunkPaths.map((chunkPath, index) =>
    +     pipeStream(
    +       path.resolve(chunkDir, chunkPath),
    +       // 指定位置创建可写流
    +       fse.createWriteStream(filePath, {
    +         start: index * size,
    +         end: (index + 1) * size
    +       })
    +     )
    +   )
    + );
    + fse.rmdirSync(chunkDir); // 合并后删除保存切片的目录
    +};

    server.on("request", async (req, res) => {
    res.setHeader("Access-Control-Allow-Origin", "*");
    res.setHeader("Access-Control-Allow-Headers", "*");
    if (req.method === "OPTIONS") {
      res.status = 200;
      res.end();
      return;
    }

    +   if (req.url === "/merge") {
    +     const data = await resolvePost(req);
    +     const { filename,size } = data;
    +     const filePath = path.resolve(UPLOAD_DIR, `${filename}`);
    +     await mergeFileChunk(filePath, filename);
    +     res.end(
    +       JSON.stringify({
    +         code: 0,
    +         message: "file merged success"
    +       })
    +     );
    +   }

    });

    server.listen(3000, () => console.log("正在监听 3000 端口"));
    复制代码

    由于前端在发送合并请求时会携带文件名,服务端根据文件名可以找到上一步创建的切片文件夹

    接着使用 fs.createWriteStream 创建一个可写流,可写流文件名就是切片文件夹名 + 后缀名组合而成

    随后遍历整个切片文件夹,将切片通过 fs.createReadStream 创建可读流,传输合并到目标文件中

    值得注意的是每次可读流都会传输到可写流的指定位置,这是通过 createWriteStream 的第二个参数 start/end 控制的,目的是能够并发合并多个可读流到可写流中,这样即使流的顺序不同也能传输到正确的位置,所以这里还需要让前端在请求的时候多提供一个 size 参数

       async mergeRequest() {
        await this.request({
          url: "http://localhost:3000/merge",
          headers: {
            "content-type": "application/json"
          },
          data: JSON.stringify({
    +         size: SIZE,
            filename: this.container.file.name
          })
        });
      },
    复制代码

    img

    其实也可以等上一个切片合并完后再合并下个切片,这样就不需要指定位置,但传输速度会降低,所以使用了并发合并的手段,接着只要保证每次合并完成后删除这个切片,等所有切片都合并完毕后最后删除切片文件夹即可

    img

    至此一个简单的大文件上传就完成了,接下来我们再此基础上扩展一些额外的功能

    显示上传进度条

    上传进度分两种,一个是每个切片的上传进度,另一个是整个文件的上传进度,而整个文件的上传进度是基于每个切片上传进度计算而来,所以我们先实现切片的上传进度

    切片进度条

    XMLHttpRequest 原生支持上传进度的监听,只需要监听 upload.onprogress 即可,我们在原来的 request 基础上传入 onProgress 参数,给 XMLHttpRequest 注册监听事件

     // xhr
      request({
        url,
        method = "post",
        data,
        headers = {},
    +     onProgress = e => e,
        requestList
      }) {
        return new Promise(resolve => {
          const xhr = new XMLHttpRequest();
    +       xhr.upload.onprogress = onProgress;
          xhr.open(method, url);
          Object.keys(headers).forEach(key =>
            xhr.setRequestHeader(key, headers[key])
          );
          xhr.send(data);
          xhr.onload = e => {
            resolve({
              data: e.target.response
            });
          };
        });
      }
    复制代码

    由于每个切片都需要触发独立的监听事件,所以还需要一个工厂函数,根据传入的切片返回不同的监听函数

    在原先的前端上传逻辑中新增监听函数部分

        // 上传切片,同时过滤已上传的切片
      async uploadChunks(uploadedList = []) {
        const requestList = this.data
    +       .map(({ chunk,hash,index }) => {
            const formData = new FormData();
            formData.append("chunk", chunk);
            formData.append("hash", hash);
            formData.append("filename", this.container.file.name);
    +         return { formData,index };
          })
    +       .map(async ({ formData,index }) =>
            this.request({
              url: "http://localhost:3000",
              data: formData,
    +           onProgress: this.createProgressHandler(this.data[index]),
            })
          );
        await Promise.all(requestList);
          // 合并切片
        await this.mergeRequest();
      },
      async handleUpload() {
        if (!this.container.file) return;
        const fileChunkList = this.createFileChunk(this.container.file);
        this.data = fileChunkList.map(({ file },index) => ({
          chunk: file,
    +       index,
          hash: this.container.file.name + "-" + index
    +       percentage:0
        }));
        await this.uploadChunks();
      }    
    +   createProgressHandler(item) {
    +     return e => {
    +       item.percentage = parseInt(String((e.loaded / e.total) * 100));
    +     };
    +   }
    复制代码

    每个切片在上传时都会通过监听函数更新 data 数组对应元素的 percentage 属性,之后把将 data 数组放到视图中展示即可

    文件进度条

    将每个切片已上传的部分累加,除以整个文件的大小,就能得出当前文件的上传进度,所以这里使用 Vue 计算属性

      computed: {
          uploadPercentage() {
            if (!this.container.file || !this.data.length) return 0;
            const loaded = this.data
              .map(item => item.size * item.percentage)
              .reduce((acc, cur) => acc + cur);
            return parseInt((loaded / this.container.file.size).toFixed(2));
          }
    }
    复制代码

    最终视图如下

    img

    字节跳动面试官:请你实现一个大文件上传和断点续传(下)

    作者:yeyan1996
    来源:https://juejin.cn/post/6844904046436843527

    收起阅读 »

    腾讯三面:40亿个QQ号码如何去重?

    大家好,我是道哥。今天,我们来聊一道常见的考题,也出现在腾讯面试的三面环节,非常有意思。具体的题目如下:文件中有40亿个QQ号码,请设计算法对QQ号码去重,相同的QQ号码仅保留一个,内存限制1G. 这个题目的意思应该很清楚了,比较直白。为了便于大家理解,我来画...
    继续阅读 »

    大家好,我是道哥。

    今天,我们来聊一道常见的考题,也出现在腾讯面试的三面环节,非常有意思。具体的题目如下:

    文件中有40亿个QQ号码,请设计算法对QQ号码去重,相同的QQ号码仅保留一个,内存限制1G.

    这个题目的意思应该很清楚了,比较直白。为了便于大家理解,我来画个动图玩玩,希望大家喜欢。

    能否做对这道题目,很大程度上就决定了能否拿下腾讯的offer,有一定的技巧性,一起来看下吧。

    在原题中,实际有40亿个QQ号码,为了方便起见,在图解和叙述时,仅以4个QQ为例来说明。

    方法一:排序

    很自然地,最简单的方式是对所有的QQ号码进行排序,重复的QQ号码必然相邻,保留第一个,去掉后面重复的就行。

    原始的QQ号为:

    排序后的QQ号为:

    去重就简单了:

    可是,面试官要问你,去重一定要排序吗?显然,排序的时间复杂度太高了,无法通过腾讯面试。

    方法二:hashmap

    既然直接排序的时间复杂度太高,那就用hashmap吧,具体思路是把QQ号码记录到hashmap中:

    mapFlag[123] = true
    mapFlag[567] = true
    mapFlag[123] = true
    mapFlag[890] = true

    由于hashmap的去重性质,可知实际自动变成了:

    mapFlag[123] = true
    mapFlag[567] = true
    mapFlag[890] = true

    很显然,只有123,567,890存在,所以这也就是去重后的结果。

    可是,面试官又要问你了:实际要存40亿QQ号码,1G的内存够分配这么多空间吗?显然不行,无法通过腾讯面试。

    方法三:文件切割

    显然,这是海量数据问题。看过很多面经的求职者,自然想到文件切割的方式,避免内存过大。

    可是,绞尽脑汁思考,要么使用文件间的归并排序,要么使用桶排序,反正最终是能排序的。

    既然排序好了,那就能实现去重了,貌似就万事大吉了。我只能坦白地说,高兴得有点早哦。

    接着,面试官又要问你:这么多的文件操作,效率自然不高啊。显然,无法通过腾讯面试。

    方法四:bitmap

    来看绝招!我们可以对hashmap进行优化,采用bitmap这种数据结构,可以顺利地同时解决时间问题和空间问题。

    在很多实际项目中,bitmap经常用到。我看了不少组件的源码,发现很多地方都有bitmap实现,bitmap图解如下:

    这是一个unsigned char类型,可以看到,共有8位,取值范围是[0, 255],如上这个unsigned char的值是255,它能标识0~7这些数字都存在。

    同理,如下这个unsigned char类型的值是254,它对应的含义是:1~7这些数字存在,而数字0不存在:

    由此可见,一个unsigned char类型的数据,可以标识0~7这8个整数的存在与否。以此类推:

    • 一个unsigned int类型数据可以标识0~31这32个整数的存在与否。

    • 两个unsigned int类型数据可以标识0~63这64个整数的存在与否。

    显然,可以推导出来:512MB大小足够标识所有QQ号码的存在与否,请注意:QQ号码的理论最大值为2^32 - 1,大概是43亿左右。

    接下来的问题就很简单了:用512MB的unsigned int数组来记录文件中QQ号码的存在与否,形成一个bitmap,比如:

    bitmapFlag[123] = 1
    bitmapFlag[567] = 1
    bitmapFlag[123] = 1
    bitmapFlag[890] = 1

    实际上就是:

    bitmapFlag[123] = 1
    bitmapFlag[567] = 1
    bitmapFlag[890] = 1

    然后从小到大遍历所有正整数(4字节),当bitmapFlag值为1时,就表明该数是存在的。 而且,从上面的过程可以看到,自动实现了去重。显然,这种方式 可以通过腾讯的面试 。

    扩展练习一

    文件中有40亿个互不相同的QQ号码,请设计算法对QQ号码进行排序,内存限制1G.

    很显然,直接用bitmap, 标记这40亿个QQ号码的存在性,然后从小到大遍历正整数,当bitmapFlag的值为1时,就输出该值,输出后的正整数序列就是排序后的结果。

    请注意,这里必须限制40亿个QQ号码互不相同。通过bitmap记录,客观上就自动完成了排序功能。

    扩展练习二

    文件中有40亿个互不相同的QQ号码,求这些QQ号码的中位数,内存限制1G.

    我知道,一些刷题经验丰富的人,最开始想到的肯定是用堆或者文件切割,这明显是犯了本本主义错误。直接用bitmap排序,当场搞定中位数。

    扩展练习三

    文件中有40亿个互不相同的QQ号码,求这些QQ号码的top-K,内存限制1G.

    我知道,很多人背诵过top-K问题,信心满满,想到用小顶堆或者文件切割,这明显又是犯了本本主义错误。直接用bitmap排序,当场搞定top-K问题。

    扩展练习四

    文件中有80亿个QQ号码,试判断其中是否存在相同的QQ号码,内存限制1G.

    我知道,一些吸取了经验教训的人肯定说,直接bitmap啊。然而,又一次错了。根据容斥原理可知:

    因为QQ号码的个数是43亿左右(理论值2^32 - 1),所以80亿个QQ号码必然存在相同的QQ号码。

    海量数据的问题,要具体问题具体分析,不要眉毛胡子一把抓。有些人完全不刷题,肯定不行。有些人刷题后不加思考,不会变通,也是不行的。好了,先说这么多。我们也会一步一个脚印,争取每篇文章讲清讲透一件事,也希望大家阅读后有所收获,心情愉快。

    作者:爱码有道
    来源:https://mp.weixin.qq.com/s/YlLYDzncB6tqbffrg__13w

    收起阅读 »

    看完这篇文章保你面试稳操胜券——React篇

    ✨欢迎各位小伙伴:\textcolor{blue}{欢迎各位小伙伴:}欢迎各位小伙伴: ✨ 进大厂收藏这一系列就够了,全方位搜集总结,为大家归纳出这篇面试宝典,面试途中祝你一臂之力!,共分为四个系列 ✨包含Vue40道经典面试题\textcolor{g...
    继续阅读 »



    ✨欢迎各位小伙伴:\textcolor{blue}{欢迎各位小伙伴:}欢迎各位小伙伴:
    ✨ 进大厂收藏这一系列就够了,全方位搜集总结,为大家归纳出这篇面试宝典,面试途中祝你一臂之力!,共分为四个系列
    ✨包含Vue40道经典面试题\textcolor{green}{包含Vue40道经典面试题}包含Vue40道经典面试题
    ✨包含react12道高并发面试题\textcolor{green}{包含react12道高并发面试题}包含react12道高并发面试题
    ✨包含微信小程序34道必问面试题\textcolor{green}{包含微信小程序34道必问面试题}包含微信小程序34道必问面试题
    ✨包含javaScript80道扩展面试题\textcolor{green}{包含javaScript80道扩展面试题}包含javaScript80道扩展面试题
    ✨包含APP10道装逼面试题\textcolor{green}{包含APP10道装逼面试题}包含APP10道装逼面试题
    ✨包含HTML/CSS30道基础面试题\textcolor{green}{包含HTML/CSS30道基础面试题}包含HTML/CSS30道基础面试题
    ✨还包含Git、前端优化、ES6、Axios面试题\textcolor{green}{还包含Git、前端优化、ES6、Axios面试题}还包含Git、前端优化、ES6、Axios面试题
    ✨接下来让我们饱享这顿美味吧。一起来学习吧!!!\textcolor{pink}{接下来让我们饱享这顿美味吧。一起来学习吧!!!}接下来让我们饱享这顿美味吧。一起来学习吧!!!
    ✨本篇为《看完这篇文章保你面试稳操胜券》第五篇(react、app、git)\textcolor{pink}{本篇为《看完这篇文章保你面试稳操胜券》第五篇(react、app、git)}本篇为《看完这篇文章保你面试稳操胜券》第五篇(react、app、git)

    react

    React 中 keys 的作用是什么?

    Keys是React用于追踪哪些列表中元素被修改、被添加或者被移除的辅助标识 在开发过程中,我们需要保证某个元素的 key 在其同级元素中具有唯一性。 在 React Diff 算法中React 会借助元素的 Key 值来判断该元素是新近创建的还是被移动而来的元素, 从而减少不必要的元素重渲染。此外,React 还需要借助 Key 值来判断元素与本地状态的关联关系, 因此我们绝不可忽视转换函数中 Key 的重要性

    传入 setState 函数的第二个参数的作用是什么?

    该函数会在 setState 函数调用完成并且组件开始重渲染的时候被调用,我们可以用该函数来监听渲染是否完成

    React 中 refs 的作用是什么

    Refs 是 React 提供给我们的安全访问 DOM元素或者某个组件实例的句柄 可以为元素添加ref属性然后在回调函数中接受该元素在 DOM 树中的句柄,该值会作为回调函数的第一个参数返回

    在生命周期中的哪一步你应该发起 AJAX 请求

    我们应当将AJAX 请求放到 componentDidMount 函数中执行,主要原因有下

    React 下一代调和算法 Fiber 会通过开始或停止渲染的方式优化应用性能,其会影响到 componentWillMount 的触发次数。对于 componentWillMount 这个生命周期函数的调用次数会变得不确定,React 可能会多次频繁调用 componentWillMount。如果我们将 AJAX 请求放到 componentWillMount 函数中,那么显而易见其会被触发多次,自然也就不是好的选择。 如果我们将AJAX 请求放置在生命周期的其他函数中,我们并不能保证请求仅在组件挂载完毕后才会要求响应。如果我们的数据请求在组件挂载之前就完成,并且调用了setState函数将数据添加到组件状态中,对于未挂载的组件则会报错。而在 componentDidMount 函数中进行 AJAX 请求则能有效避免这个问题

    shouldComponentUpdate 的作用

    shouldComponentUpdate 允许我们手动地判断是否要进行组件更新,根据组件的应用场景设置函数的合理返回值能够帮我们避免不必要的更新

    如何告诉 React 它应该编译生产环境版

    通常情况下我们会使用 Webpack 的 DefinePlugin 方法来将 NODE_ENV 变量值设置为 production。 编译版本中 React会忽略 propType 验证以及其他的告警信息,同时还会降低代码库的大小, React 使用了 Uglify 插件来移除生产环境下不必要的注释等信息

    概述下 React 中的事件处理逻辑

    为了解决跨浏览器兼容性问题,React 会将浏览器原生事件(Browser Native Event)封装为合成事件(SyntheticEvent)传入设置的事件处理器中。 这里的合成事件提供了与原生事件相同的接口,不过它们屏蔽了底层浏览器的细节差异,保证了行为的一致性。 另外有意思的是,React 并没有直接将事件附着到子元素上,而是以单一事件监听器的方式将所有的事件发送到顶层进行处理。 这样 React 在更新 DOM 的时候就不需要考虑如何去处理附着在 DOM 上的事件监听器,最终达到优化性能的目的

    createElement 与 cloneElement 的区别是什么

    createElement 函数是 JSX 编译之后使用的创建 React Element 的函数,而 cloneElement 则是用于复制某个元素并传入新的 Props

    redux中间件

    中间件提供第三方插件的模式,自定义拦截 action -> reducer 的过程。变为 action -> middlewares -> reducer。 这种机制可以让我们改变数据流,实现如异步action ,action 过滤,日志输出,异常报告等功能 redux-logger:提供日志输出 redux-thunk:处理异步操作 redux-promise:处理异步操作,actionCreator的返回值是promise

    react组件的划分业务组件技术组件?

    根据组件的职责通常把组件分为UI组件和容器组件。 UI 组件负责 UI 的呈现,容器组件负责管理数据和逻辑。 两者通过React-Redux 提供connect方法联系起来

    react旧版生命周期函数

    初始化阶段

    getDefaultProps:获取实例的默认属性 getInitialState:获取每个实例的初始化状态 componentWillMount:组件即将被装载、渲染到页面上 render:组件在这里生成虚拟的DOM节点 componentDidMount:组件真正在被装载之后 运行中状态

    componentWillReceiveProps:组件将要接收到属性的时候调用 shouldComponentUpdate:组件接受到新属性或者新状态的时候(可以返回false,接收数据后不更新,阻止render调用,后面的函数不会被继续执行了) componentWillUpdate:组件即将更新不能修改属性和状态 render:组件重新描绘 componentDidUpdate:组件已经更新 销毁阶段

    componentWillUnmount:组件即将销毁

    新版生命周期

    在新版本中,React 官方对生命周期有了新的 变动建议:

    使用getDerivedStateFromProps替换componentWillMount; 使用getSnapshotBeforeUpdate替换componentWillUpdate; 避免使用componentWillReceiveProps; 其实该变动的原因,正是由于上述提到的 Fiber。首先,从上面我们知道 React 可以分成 reconciliation 与 commit两个阶段,对应的生命周期如下:

    reconciliation

    componentWillMount componentWillReceiveProps shouldComponentUpdate componentWillUpdate commit

    componentDidMount componentDidUpdate componentWillUnmount 在 Fiber 中,reconciliation 阶段进行了任务分割,涉及到 暂停 和 重启,因此可能会导致 reconciliation 中的生命周期函数在一次更新渲染循环中被 多次调用 的情况,产生一些意外错误

    Git相关面试题

    git代码冲突处理

    先将本地修改存储起来 git stash 暂存了本地修改之后,就可以pull了。 git pull 还原暂存的内容 git stash pop stash@{0}

    避免重复的合并冲突

    正如每个开发人员都知道的那样,修复合并冲突相当繁琐,但重复解决完全相同的冲突(例如,在长时间运行的功能分支中)更让人心烦。解决方案是:

    git config --global rerere.enabled true 或者你可以通过手动创建目录在每个项目的基础上启用.git/rr-cache。

    使用其他设备从GitHub中导出远程分支项目,无法成功。

    其原因在于本地中根本没有其分支。解决命令如下: git fetch -- 获取所有分支的更新 git branch -a -- 查看本地和远程分支列表,remotes开头的均为远程分支 -- 导出其远程分支,并通过-b设定本地分支跟踪远程分支 git checkout remotes/branch_name -b branch_name

    APP相关面试题

    你平常会看日志吗, 一般会出现哪些异常(Exception)?

    这个主要是面试官考察你会不会看日志,是不是看得懂java里面抛出的异常,Exception

    一般面试中java Exception(runtimeException )是必会被问到的问题 app崩溃的常见原因应该也是这些了。常见的异常列出四五种,是基本要求。

    常见的几种如下:

    NullPointerException - 空指针引用异常 ClassCastException - 类型强制转换异常。 IllegalArgumentException - 传递非法参数异常。 ArithmeticException - 算术运算异常 ArrayStoreException - 向数组中存放与声明类型不兼容对象异常 IndexOutOfBoundsException - 下标越界异常 NegativeArraySizeException - 创建一个大小为负数的数组错误异常 NumberFormatException - 数字格式异常 SecurityException - 安全异常 UnsupportedOperationException - 不支持的操作异常

    app的日志如何抓取?

    app本身的日志,可以用logcat抓取,参考这篇:http://www.cnblogs.com/yoyoketang/…

    adb logcat | find “com.sankuai.meituan” >d:\hello.txt

    也可以用ddms抓取,手机连上电脑,打开ddms工具,或者在Android Studio开发工具中,打开DDMS

    app对于不稳定偶然出现anr和crash时候你是怎么处理的?

    app偶然出现anr和crash是比较头疼的问题,由于偶然出现无法复现步骤,这也是一个测试人员必备的技能,需要抓日志。查看日志主要有3个方法:

    方法一:app开发保存错误日志到本地 一般app开发在debug版本,出现anr和crash的时候会自动把日志保存到本地实际的sd卡上,去对应的app目录取出来就可以了

    方法二:实时抓取 当出现偶然的crash时候,这时候可以把手机拉到你们app开发那,手机连上他的开发代码的环境,有ddms会抓日志,这时候出现crash就会记录下来日志。 尽量重复操作让bug复现就可以了

    也可以自己开着logcat,保存日志到电脑本地,参考这篇:http://www.cnblogs.com/yoyoketang/…

    adb logcat | find “com.sankuai.meituan” >d:\hello.txt

    方法三:第三方sdk统计工具

    一般接入了第三方统计sdk,比如友盟统计,在友盟的后台会抓到报错的日志

    App出现crash原因有哪些?

    为什么App会出现崩溃呢?百度了一下,查到和App崩溃相关的几个因素:内存管理错误,程序逻辑错误,设备兼容,网络因素等,如下: 1.内存管理错误:可能是可用内存过低,app所需的内存超过设备的限制,app跑不起来导致App crash。 或是内存泄露,程序运行的时间越长,所占用的内存越大,最终用尽全部内存,导致整个系统崩溃。 亦或非授权的内存位置的使用也可能会导致App crash。 2.程序逻辑错误:数组越界、堆栈溢出、并发操作、逻辑错误。 e.g. app新添加一个未经测试的新功能,调用了一个已释放的指针,运行的时候就会crash。 3.设备兼容:由于设备多样性,app在不同的设备上可能会有不同的表现。 4.网络因素:可能是网速欠佳,无法达到app所需的快速响应时间,导致app crash。或者是不同网络的切换也可能会影响app的稳定性。

    app出现ANR,是什么原因导致的?

    那么导致ANR的根本原因是什么呢?简单的总结有以下两点:

    1.主线程执行了耗时操作,比如数据库操作或网络编程 2.其他进程(就是其他程序)占用CPU导致本进程得不到CPU时间片,比如其他进程的频繁读写操作可能会导致这个问题。

    细分的话,导致ANR的原因有如下几点: 1.耗时的网络访问 2.大量的数据读写 3.数据库操作 4.硬件操作(比如camera) 5.调用thread的join()方法、sleep()方法、wait()方法或者等待线程锁的时候 6.service binder的数量达到上限 7.system server中发生WatchDog ANR 8.service忙导致超时无响应 9.其他线程持有锁,导致主线程等待超时 10.其它线程终止或崩溃导致主线程一直等待。

    android和ios测试区别?

    App测试中ios和Android有哪些区别呢? 1.Android长按home键呼出应用列表和切换应用,然后右滑则终止应用; 2.多分辨率测试,Android端20多种,ios较少; 3.手机操作系统,Android较多,ios较少且不能降级,只能单向升级;新的ios系统中的资源库不能完全兼容低版本中的ios系统中的应用,低版本ios系统中的应用调用了新的资源库,会直接导致闪退(Crash); 4.操作习惯:Android,Back键是否被重写,测试点击Back键后的反馈是否正确;应用数据从内存移动到SD卡后能否正常运行等; 5.push测试:Android:点击home键,程序后台运行时,此时接收到push,点击后唤醒应用,此时是否可以正确跳转;ios,点击home键关闭程序和屏幕锁屏的情况(红点的显示); 6.安装卸载测试:Android的下载和安装的平台和工具和渠道比较多,ios主要有app store,iTunes和testflight下载; 7.升级测试:可以被升级的必要条件:新旧版本具有相同的签名;新旧版本具有相同的包名;有一个标示符区分新旧版本(如版本号), 对于Android若有内置的应用需检查升级之后内置文件是否匹配(如内置的输入法)

    另外:对于测试还需要注意一下几点: 1.并发(中断)测试:闹铃弹出框提示,另一个应用的启动、视频音频的播放,来电、用户正在输入等,语音、录音等的播放时强制其他正在播放的要暂停; 2.数据来源的测试:输入,选择、复制、语音输入,安装不同输入法输入等; 3.push(推送)测试:在开关机、待机状态下执行推送,消息先死及其推送跳转的正确性; 应用在开发、未打开状态、应用启动且在后台运行的情况下是push显示和跳转否正确; 推送消息阅读前后数字的变化是否正确; 多条推送的合集的显示和跳转是否正确;

    4.分享跳转:分享后的文案是否正确;分享后跳转是否正确,显示的消息来源是否正确;

    5.触屏测试:同时触摸不同的位置或者同时进行不同操作,查看客户端的处理情况,是否会crash等

    app测试和web测试有什么区别?

    WEB测试和App测试从流程上来说,没有区别。 都需要经历测试计划方案,用例设计,测试执行,缺陷管理,测试报告等相关活动。 从技术上来说,WEB测试和APP测试其测试类型也基本相似,都需要进行功能测试、性能测试、安全性测试、GUI测试等测试类型。

    他们的主要区别在于具体测试的细节和方法有区别,比如:性能测试,在WEB测试只需要测试响应时间这个要素,在App测试中还需要考虑流量测试和耗电量测试。

    兼容性测试:在WEB端是兼容浏览器,在App端兼容的是手机设备。而且相对应的兼容性测试工具也不相同,WEB因为是测试兼容浏览器,所以需要使用不同的浏览器进行兼容性测试(常见的是兼容IE6,IE8,chrome,firefox)如果是手机端,那么就需要兼容不同品牌,不同分辨率,不同android版本甚至不同操作系统的兼容。(常见的兼容方式是兼容市场占用率前N位的手机即可),有时候也可以使用到兼容性测试工具,但WEB兼容性工具多用IETester等工具,而App兼容性测试会使用Testin这样的商业工具也可以做测试。

    安装测试:WEB测试基本上没有客户端层面的安装测试,但是App测试是存在客户端层面的安装测试,那么就具备相关的测试点。

    还有,App测试基于手机设备,还有一些手机设备的专项测试。如交叉事件测试,操作类型测试,网络测试(弱网测试,网络切换)

    交叉事件测试:就是在操作某个软件的时候,来电话、来短信,电量不足提示等外部事件。

    操作类型测试:如横屏测试,手势测试

    网络测试:包含弱网和网络切换测试。需要测试弱网所造成的用户体验,重点要考虑回退和刷新是否会造成二次提交。弱网络的模拟,据说可以用360wifi实现设置。

    从系统架构的层面,WEB测试只要更新了服务器端,客户端就会同步会更新。而且客户端是可以保证每一个用户的客户端完全一致的。但是APP端是不能够保证完全一致的,除非用户更新客户端。如果是APP下修改了服务器端,意味着客户端用户所使用的核心版本都需要进行回归测试一遍。

    还有升级测试:升级测试的提醒机制,升级取消是否会影响原有功能的使用,升级后用户数据是否被清除了。

    Android四大组件

    Android四大基本组件:Activity、BroadcastReceiver广播接收器、ContentProvider内容提供者、Service服务。

    Activity:

    应用程序中,一个Activity就相当于手机屏幕,它是一种可以包含用户界面的组件,主要用于和用户进行交互。一个应用程序可以包含许多活动,比如事件的点击,一般都会触发一个新的Activity。

    BroadcastReceiver广播接收器:

    应用可以使用它对外部事件进行过滤只对感兴趣的外部事件(如当电话呼入时,或者数据网络可用时)进行接收并做出响应。广播接收器没有用户界面。然而,它们可以启动一个activity或serice 来响应它们收到的信息,或者用NotificationManager来通知用户。通知可以用很多种方式来吸引用户的注意力──闪动背灯、震动、播放声音等。一般来说是在状态栏上放一个持久的图标,用户可以打开它并获取消息。

    ContentProvider内容提供者:

    内容提供者主要用于在不同应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访问数据的安全性。只有需要在多个应用程序间共享数据时才需要内容提供者。例如:通讯录数据被多个应用程序使用,且必须存储在一个内容提供者中。它的好处:统一数据访问方式。

    Service服务:

    是Android中实现程序后台运行的解决方案,它非常适合去执行那些不需要和用户交互而且还要长期运行的任务(一边打电话,后台挂着QQ)。服务的运行不依赖于任何用户界面,即使程序被切换到后台,或者用户打开了另一个应用程序,服务扔然能够保持正常运行,不过服务并不是运行在一个独立的进程当中,而是依赖于创建服务时所在的应用程序进程。当某个应用程序进程被杀掉后,所有依赖于该进程的服务也会停止运行(正在听音乐,然后把音乐程序退出)。

    Activity生命周期?

    周期即活动从开始到结束所经历的各种状态。生命周期即活动从开始到结束所经历的各个状态。从一个状态到另一个状态的转变,从无到有再到无,这样一个过程中所经历的状态就叫做生命周期。

    Activity本质上有四种状态:

    1.运行(Active/Running):Activity处于活动状态,此时Activity处于栈顶,是可见状态,可以与用户进行交互

    2.暂停(Paused):当Activity失去焦点时,或被一个新的非全面屏的Activity,或被一个透明的Activity放置在栈顶时,Activity就转化为Paused状态。此刻并不会被销毁,只是失去了与用户交互的能力,其所有的状态信息及其成员变量都还在,只有在系统内存紧张的情况下,才有可能被系统回收掉

    3.停止(Stopped):当Activity被系统完全覆盖时,被覆盖的Activity就会进入Stopped状态,此时已不在可见,但是资源还是没有被收回

    4.系统回收(Killed):当Activity被系统回收掉,Activity就处于Killed状态

    如果一个活动在处于停止或者暂停的状态下,系统内存缺乏时会将其结束(finish)或者杀死(kill)。这种非正常情况下,系统在杀死或者结束之前会调用onSaveInstance()方法来保存信息,同时,当Activity被移动到前台时,重新启动该Activity并调用onRestoreInstance()方法加载保留的信息,以保持原有的状态。

    在上面的四中常有的状态之间,还有着其他的生命周期来作为不同状态之间的过度,用于在不同的状态之间进行转换,生命周期的具体说明见下。

    什么是activity

    什么是activity,这个前两年出去面试APP测试岗位,估计问的最多了,特别是一些大厂,先问你是不是做过APP测试,那好,你说说什么是activity? 如果没看过android的开发原理,估计这个很难回答,要是第一个问题就被难住了,面试的信心也会失去一半了,士气大减。

    Activity是Android的四大组件之一,也是平时我们用到最多的一个组件,可以用来显示View。 官方的说法是Activity一个应用程序的组件,它提供一个屏幕来与用户交互,以便做一些诸如打电话、发邮件和看地图之类的事情,原话如下: An Activity is an application component that provides a screen with which users can interact in order to do something, such as dial the phone, take a photo, send an email, or view a map.

    Activity是一个Android的应用组件,它提供屏幕进行交互。每个Activity都会获得一个用于绘制其用户界面的窗口,窗口可以充满哦屏幕也可以小于屏幕并浮动在其他窗口之上。 一个应用通常是由多个彼此松散联系的Activity组成,一般会指定应用中的某个Activity为主活动,也就是说首次启动应用时给用户呈现的Activity。将Activity设为主活动的方法 当然Activity之间可以进行互相跳转,以便执行不同的操作。每当新Activity启动时,旧的Activity便会停止,但是系统会在堆栈也就是返回栈中保留该Activity。 当新Activity启动时,系统也会将其推送到返回栈上,并取得用在这里插入图片描述 户的操作焦点。当用户完成当前Activity并按返回按钮是,系统就会从堆栈将其弹出销毁,然后回复前一Activity 当一个Activity因某个新Activity启动而停止时,系统会通过该Activity的生命周期回调方法通知其这一状态的变化。 Activity因状态变化每个变化可能有若干种,每一种回调都会提供执行与该状态相应的特定操作的机会

    语音通话功能

    WebRTC实时通讯的核心 WebRTC 建立连接步骤 1.为连接的两端创建一个 RTCPeerConnection 对象,并且给 RTCPeerConnection 对象添加本地流。

    2.获取本地媒体描述信息(SDP),并与对端进行交换。

    3.获取网络信息(Candidate,IP 地址和端口),并与远端进行交换。

    装逼神器

    一般通过面试的短短一个小时时间,面试官需要对你的技术底子进行磨盘,如果你看完下面这些材料,相信你一定能够让他心里直呼牛逼(下面所有链接文章均是小编自己总结的)

    关于scoped样式穿透问题

    blog.csdn.net/JHXL_/artic…

    Vue2和Vue3的区别

    blog.csdn.net/JHXL_/artic…

    项目中的登录流程

    blog.csdn.net/JHXL_/artic…

    构造函数、原型、继承

    blog.csdn.net/JHXL_/artic…

    项目中遇到的难点

    写在最后

    ✨原创不易,还希望各位大佬支持一下\textcolor{blue}{原创不易,还希望各位大佬支持一下}原创不易,还希望各位大佬支持一下
    👍 点赞,你的认可是我创作的动力!\textcolor{green}{点赞,你的认可是我创作的动力!}点赞,你的认可是我创作的动力!
    ⭐️ 收藏,你的青睐是我努力的方向!\textcolor{green}{收藏,你的青睐是我努力的方向!}收藏,你的青睐是我努力的方向!
    ✏️ 评论,你的意见是我进步的财富!\textcolor{green}{评论,你的意见是我进步的财富!}评论,你的意见是我进步的财富!

    作者:几何心凉
    来源:https://juejin.cn/post/7039640038509903909

    收起阅读 »

    撸一个 webpack 插件,希望对大家有所帮助

    最近,陆陆续续搞 了一个 UniUsingComponentsWebpackPlugin 插件(下面介绍),这是自己第三个开源项目,希望大家一起来维护,一起 star 呀,其它两个:vue-okr-tree基于 Vue 2的组织架构树组件地址:github....
    继续阅读 »

    最近,陆陆续续搞 了一个 UniUsingComponentsWebpackPlugin 插件(下面介绍),这是自己第三个开源项目,希望大家一起来维护,一起 star 呀,其它两个:

    • vue-okr-tree

      基于 Vue 2的组织架构树组件

      地址:github.com/qq449245884…

    • ztjy-cli

      团队的一个简易模板初始化脚手架

      地址:github.com/qq449245884…

    • UniUsingComponentsWebpackPlugin

      地址:github.com/qq449245884…

      配合UniApp,用于集成小程序原生组件

      • 配置第三方库后可以自动引入其下的原生组件,而无需手动配置

      • 生产构建时可以自动剔除没有使用到的原生组件

    背景

    第一个痛点

    用 uniapp开发小程序的小伙伴应该知道,我们在 uniapp 中要使用第三方 UI 库(vant-weappiView-weapp)的时候 ,想要在全局中使用,需要在 src/pages.json 中的 usingComponents 添加对应的组件声明,如:

    // src/pages.json
    "usingComponents": {
       "van-button": "/wxcomponents/@vant/weapp/button/index",
    }

    但在开发过程中,我们不太清楚需要哪些组件,所以我们可能会全部声明一遍(PS:这在做公共库的时候更常见),所以我们得一个个的写,做为程序员,我们绝不允许使用这种笨方法。这是第一个痛点

    第二个痛点

    使用第三方组件,除了在 src/pages.json 还需要在对应的生产目录下建立 wxcomponents,并将第三方的库拷贝至该文件下,这个是 uniapp 自定义的,详细就见:uniapp.dcloud.io/frame?id=%e…

    这是第二个痛点

    第三个痛点

    第二痛点,我们将整个UI库拷贝至 wxcomponents,但最终发布的时候,我们不太可能全都用到了里面的全局组件,所以就将不必要的组件也发布上去,增加代码的体积。

    有的小伙伴就会想到,那你将第三方的库拷贝至 wxcomponents时候,可以只拷使用到的就行啦。是这理没错,但组件里面可能还会使用到其它组件,我们还得一个个去看,然后一个个引入,这又回到了第一个痛点了

    有了这三个痛点,必须得有个插件来做这些傻事,处理这三个痛点。于是就有 UniUsingComponentsWebpackPlugin 插件,这个webpack 插件主要解决下面几个问题:

    • 配置第三方库后可以自动引入其下的原生组件,而无需手动配置

    • 生产构建时可以自动剔除没有使用到的原生组件

    webpack 插件

    webpack 的插件体系是一种基于 Tapable 实现的强耦合架构,它在特定时机触发钩子时会附带上足够的上下文信息,插件定义的钩子回调中,能也只能与这些上下文背后的数据结构、接口交互产生 side effect,进而影响到编译状态和后续流程。

    从形态上看,插件通常是一个带有 apply函数的类:

    class SomePlugin {
       apply(compiler) {
      }
    }

    Webpack 会在启动后按照注册的顺序逐次调用插件对象的 apply 函数,同时传入编译器对象 compiler ,插件开发者可以以此为起点触达到 webpack 内部定义的任意钩子,例如:

    class SomePlugin {
       apply(compiler) {
           compiler.hooks.thisCompilation.tap('SomePlugin', (compilation) => {
          })
      }
    }

    注意观察核心语句 compiler.hooks.thisCompilation.tap,其中 thisCompilation 为 tapable 仓库提供的钩子对象;tap 为订阅函数,用于注册回调。

    Webpack 的插件体系基于tapable 提供的各类钩子展开,所以有必要先熟悉一下 tapable 提供的钩子类型及各自的特点。

    到这里,就不做继续介绍了,关于插件的更多 详情可以去官网了解。

    这里推荐 Tecvan 大佬写的 《Webpack 插件架构深度讲解》mp.weixin.qq.com/s/tXkGx6Ckt…

    实现思路

    UniUsingComponentsWebpackPlugin 插件主要用到了三个 compiler 钩子。

    第一个钩子是 environment:

    compiler.hooks.environment.tap(
        'UniUsingComponentsWebpackPlugin',
        async () => {
          // todo someing
        }
      );

    这个钩子主要用来自动引入其下的原生组件,这样就无需手动配置。解决第一个痛点

    第二个钩子 thisCompilation,这个钩子可以获得 compilation,能对最终打包的产物进行操作:

    compiler.hooks.thisCompilation.tap(
        'UniUsingComponentsWebpackPlugin',
        (compilation) => {
          // 添加资源 hooks
          compilation.hooks.additionalAssets.tapAsync(
            'UniUsingComponentsWebpackPlugin',
            async (cb) => {
              await this.copyUsingComponents(compiler, compilation);
              cb();
            }
          );
        }
      );

    所以这个勾子用来将 node_modules 下的第三库拷贝到我们生产 dist 目录里面的 wxcomponents解决第二个痛点

    ps:这里也可直接用现有的 copy-webpack-plugin 插件来实现。

    第三个钩子 done,表示 compilation 执行完成:

        if (process.env.NODE_ENV === 'production') {
        compiler.hooks.done.tapAsync(
          'UniUsingComponentsWebpackPlugin',
          (stats, callback) => {
            this.deleteNoUseComponents();
            callback();
          }
        );
      }

    执行完成后,表示我们已经生成 dist 目录了,可以读取文件内容,分析,获取哪些组件被使用了,然后删除没有使用到组件对应的文件。这样就可以解决我们第三个痛点了

    PS:这里我判断只有在生产环境下才会 剔除,开发环境没有,也没太必要。

    使用

    安装

    npm install uni-using-components-webpack-plugin --save-dev

    然后将插件添加到 WebPack Config 中。例如:

    const UniUsingComponentsWebpackPlugin = require("uni-using-components-webpack-plugin");

    module.exports = {
     plugins: [
    new UniUsingComponentsWebpackPlugin({
      patterns: [
      {
      prefix: 'van',
      module: '@vant/weapp',
      },
      {
      prefix: 'i',
      module: 'iview-weapp',
      },
      ],
      })
    ],
    };

    注意:uni-using-components-webpack-plugin 只适用在 UniApp 开发的小程序。

    参数

    NameTypeDescription
    patterns{Array}为插件指定相关

    Patterns

    moduleprefix
    模块名组件前缀

    module 是指 package.json 里面的 name,如使用是 Vant 对应的 module@vant/weapp,如果使用是 iview,刚对应的 moduleiview-weapp,具体可看它们各自的 package.json

    prefix 是指组件的前缀,如 Vant 使用是 van 开头的前缀,iview 使用是 i 开头的前缀,具体可看它们各自的官方文档。

    PS: 这里得吐曹一下 vant,叫别人使用 van 的前缀,然后自己组件里面声明子组件时,却没有使用 van 前缀,如 picker 组件,它里面的 JSON 文件是这么写的:

    {
    "component": true,
    "usingComponents": {
    "picker-column": "../picker-column/index",
    "loading": "../loading/index"
    }
    }

    picker-columnloading 都没有带 van 前缀,因为这个问题,在做 自动剔除 功能中,我是根据 前缀来判断使用哪些组件的,由于这里的 loadingpicker-column 没有加前缀,所以就被会删除,导致最终的 picker 用不了。为了解决这个问题,增加了不少工作量。

    希望 Vant 官方后面的版本能优化一下。

    总结

    本文通用自定义 Webpack 插件来实现日常一些技术优化需求。主要为大家介绍了 Webpack 插件的基本组成和简单架构,通过三个痛点,引出了 uni-using-components-webpack-plugin 插件,并介绍了使用方式,实现思路。

    最后,关于 Webpack 插件开发,还有更多知识可以学习,建议多看看官方文档《Writing a Plugin》进行学习。

    代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

    作者:前端小智
    来源:https://juejin.cn/post/7039855875967696904

    收起阅读 »

    膜拜!用最少的代码却实现了最牛逼的滚动动画!

    今天老鱼带领大家学习如何使用最少的代码创建令人叹为观止的滚动动画~ 在聊ScrollTrigger插件之前我们先简单了解下GSAP。 GreenSock 动画平台 (GSAP) 可为 JavaScript 可以操作的任何内容(CSS 属性、SVG、Reac...
    继续阅读 »

    今天老鱼带领大家学习如何使用最少的代码创建令人叹为观止的滚动动画~



    在聊ScrollTrigger插件之前我们先简单了解下GSAP



    GreenSock 动画平台 (GSAP) 可为 JavaScript 可以操作的任何内容(CSS 属性、SVG、React、画布、通用对象等)动画化,并解决不同浏览器上存在的兼容问题,而且比 jQuery快 20 倍。大约1000万个网站和许多主要品牌都在使用GSAP。



    接下来老鱼带领大家一起学习ScrollTrigger插件的使用。


    插件简介


    ScrollTrigger是基于GSAP实现的一款高性能页面滚动触发HTML元素动画的插件。


    通过ScrollTrigger使用最少的代码创建令人叹为观止的滚动动画。我们需要知道ScrollTrigger是基于GSAP实现的插件,ScrollTrigger是处理滚动事件的,而真正处理动画是GSAP,二者组合使用才能实现滚动动画~


    插件特点



    • 将任何动画链接到特定元素,以便它仅在视图中显示该元素时才执行该动画。

    • 可以在进入/离开定义的区域或将其直接链接到滚动栏时在动画上执行操作(播放、暂停、恢复、重新启动、反转、完成、重置)。

    • 延迟动画和滚动条之间的同步。

    • 根据速度捕捉动画中的进度值。

    • 嵌入滚动直接触发到任何 GSAP 动画(包括时间线)或创建独立实例,并利用丰富的回调系统做任何您想做的事。

    • 高级固定功能可以在某些滚动位置之间锁定一个元素。

    • 灵活定义滚动位置。

    • 支持垂直或水平滚动。

    • 丰富的回调系统。

    • 当窗口调整大小时,自动重新计算位置。

    • 在开发过程中启用视觉标记,以准确查看开始/结束/触发点的位置。

    • 在滚动记录器处于活动状态时,如将active类添加到触发元素中:toggleClass: "active"

    • 使用 matchMedia() 标准媒体查询为各种屏幕尺寸创建不同的设置。

    • 自定义滚动触发器容器,可以定义一个 div 而不一定是浏览器视口。

    • 高度优化以实现最大性能。

    • 插件大约只有6.5kb大小。


    安装/引用


    CDN


    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.8.0/gsap.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.8.0/ScrollTrigger.min.js"></script>

    ES Modules


    import { gsap } from "gsap";
    import { ScrollTrigger } from "gsap/ScrollTrigger";

    gsap.registerPlugin(ScrollTrigger);

    UMD/CommonJS


    import { gsap } from "gsap/dist/gsap";
    import { ScrollTrigger } from "gsap/dist/ScrollTrigger";

    gsap.registerPlugin(ScrollTrigger);


    简单示例


    gsap.to(".box", {
    scrollTrigger: ".box", // start the animation when ".box" enters the viewport (once)
    x: 500
    });

    高级示例


    let tl = gsap.timeline({
      // 添加到整个时间线
      scrollTrigger: {
        trigger: ".container",
        pin: true,   // 在执行时固定触发器元素
        start: "top top", // 当触发器的顶部碰到视口的顶部时
        end: "+=500", // 在滚动 500 px后结束
        scrub: 1, // 触发器1秒后跟上滚动条
        snap: {
          snapTo: "labels", // 捕捉时间线中最近的标签
          duration: {min: 0.2, max: 3}, // 捕捉动画应至少为 0.2 秒,但不超过 3 秒(由速度决定)
          delay: 0.2, // 从上次滚动事件开始等待 0.2 秒,然后再进行捕捉
          ease: "power1.inOut" // 捕捉动画的过度时间(默认为“power3”)
        }
      }
    });

    // 向时间线添加动画和标签
    tl.addLabel("start")
    .from(".box p", {scale: 0.3, rotation:45, autoAlpha: 0})
    .addLabel("color")
    .from(".box", {backgroundColor: "#28a92b"})
    .addLabel("spin")
    .to(".box", {rotation: 360})
    .addLabel("end");

    自定义示例


    ScrollTrigger.create({
    trigger: "#id",
    start: "top top",
    endTrigger: "#otherID",
    end: "bottom 50%+=100px",
    onToggle: self => console.log("toggled, isActive:", self.isActive),
    onUpdate: self => {
      console.log("progress:", self.progress.toFixed(3), "direction:", self.direction, "velocity", self.getVelocity());
    }
    });

    接下来,我们一起来看使用ScrollTrigger可以实现怎样的效果吧。


    利用ScrollTrigger可以实现很多炫酷的效果,还有更多示例及源代码,快去公众号后台回复aaa滚动获取学习吧!也欢迎同学们和老鱼讨论哦~


    作者:大前端实验室
    链接:https://juejin.cn/post/7038378577028448293

    收起阅读 »

    领导:小伙子,咱们这个页面出来太慢了!赶紧给我优化一下。

    性能优化 这样一个词应该已经是老生常谈了,不仅在面试中面试官会以此和你掰头,而且在工作中领导也会因为网页加载速度慢来敲打你学(打)习(工),那么前端性能优化,如果判断到底需不需要做,如果需要做又怎么去做或者说怎么去找到优化的切入点? 接下来让我们一起来探索前端...
    继续阅读 »

    性能优化


    这样一个词应该已经是老生常谈了,不仅在面试中面试官会以此和你掰头,而且在工作中领导也会因为网页加载速度慢来敲打你学(打)习(工),那么前端性能优化,如果判断到底需不需要做,如果需要做又怎么去做或者说怎么去找到优化的切入点?


    接下来让我们一起来探索前端性能优化(emo~


    如何量化网站是否需要做性能优化?


    首先现在工具碎片化的时代,各种工具满天飞,如何找到一个方便又能直击痛点的工具,是重中之重的首要任务。



    下面使用的就是Chrome自带的插件工具进行分析



    可以使用chrome自带的lightHouse工具进行分析。得出的分数会列举出三个档次。然后再根据提出不同建议进行优化。


    例如:打开掘金的页面,然后点开开发者工具中的Lighthouse插件


    1.png


    我们可以看到几项指标:



    • First Contentful Paint 首屏加载时间(FCP)

    • Time to interactive 可互动的时间(TTI) 衡量一个页面多长时间才能完全交互

    • Speed Index 内容明显填充的速度(SI) 分数越低越好

    • Total Blocking Time 总阻塞时间(TBT) 主线程运行超过50ms的任务叫做Long Task,Total Blocking Time (TBT) 是 Long Tasks(所有超过 50ms 的任务)阻塞主线程并影响页面可用性的时间量,比如异步任务过长就会导致阻塞主线程渲染,这时就需要处理这部分任务

    • Largest Contentful Paint 最大视觉元素加载的时间(LCP) 对于SEO来说最重要的指标,用户如果打开页面很久都不能看清楚完整页面,那么SEO就会很低。(对于Google来说)

    • Cumulative Layout Shift 累计布局偏移(CLS) 衡量页面点击某些内容位置发生偏移后对页面对影响 eg:当图片宽高不确定时会时该指标更高,还比如异步或者dom动态加载到现有内容上的情况也会造成CLS升高


    以上的6个指标就能很好的量化我们网页的性能。得出类似以下结论,并采取措施。



    下面的图片是分析自己的项目得出的图表



    2.png


    3.png



    • 比如打包体积 (webpack优化,tree-sharking和按需加载插件,以及css合并)

    • 图片加载大小优化(使用可压缩图片,搭配上懒加载和预加载)

    • http1.0替换为http2.0后可使用二进制标头和多路复用。(某些图片使用cdn请求时使用了http1.0)

    • 图片没有加上width和heigth(或者说没有父容器限制),当页面重绘重排时容易造成页面排版混乱的情况

    • 避免巨大的网络负载,比如图片的同时请求和减少同时请求的数量

    • 静态资源缓存

    • 减少未使用的 JavaScript 并推迟加载脚本(defer和async)



    千遍万遍,不如自己行动一遍。dev your project!然后再对比服用,效果更好哦!



    如何做性能优化


    Vue-cli已经做了的优化:



    • 使用cache-loader默认为Vue/Babel/TypeScript编译开启,文件会缓存在node_modules/.cache里

    • 图片小于4k的会转为base64储存在js文件中

    • 生产环境会将css提取成单独的文件

    • 提取公共代码

    • 代码压缩

    • 给所有的js文件和css文件加上preload


    我们需要做的优化:(下面做出的优化都是根据分析工具得出后,对应自己的项目进行细化而来)

    1. 首先代码层面:

      1. 多图片的页面需要做图片懒加载+预加载+cdn请求以及压缩。后期会推出一篇关于图片优化的文章...
      2. 组件按需加载
      3. 对于迫不得已的dom操作,尽量一次性操作。避免多次操作dom造成页面重绘重排
      4. 公共组件的提取
      5. ajax的请求尽量能够减少多个,如果ajax请求比较慢,但是又必须得请求。那么可以考虑使用 Web Worker
    2. 打包项目。

      1. 使用webpack插件 例如 tree-sharking进行剔除无关的依赖加载。使用terser进行代码压缩,给执行时间长的loader加 cache-loader,可以使得下次打包就会使用 node_modules/.cache 里的
      2. 静态资源使用缓存或者cdn加载,部分动态文件设置缓存过期时间

    作者:Tzyito
    链接:https://juejin.cn/post/7008422231403397134

    收起阅读 »

    知道这个,再也不用写一堆el-table-column了

    前言 最近在写一个练手项目,接触到了一个问题,就是el-table中的项太多了,我写了一堆el-table-column,导致代码太长了,看起来特别费劲,后来发现了一个让人眼前一亮的方法,瞬间挽救了我的眼睛。 下面就来分享一下! 进入正题 上面就是table...
    继续阅读 »

    前言


    最近在写一个练手项目,接触到了一个问题,就是el-table中的项太多了,我写了一堆el-table-column,导致代码太长了,看起来特别费劲,后来发现了一个让人眼前一亮的方法,瞬间挽救了我的眼睛。


    下面就来分享一下!


    进入正题


    image.png
    上面就是table中的全部项,去除第一个复选框,最后一个操作的插槽,一共七项,也就是说el-table-column一共要写9对。这简直不能忍!


    image.png



    这个图只作举一个例子用,跟上面不产生对应关系。



    其中就有5个el-form-item,就这么一大堆。


    所以,我当时就想,可不可以用v-for去渲染el-table-column这个标签呢?保留复选框和最后的操作插槽,我们只需要渲染中间的那几项就行。


    经过我的实验,确实是可以实现的。



    这么写之后就开始质疑之前的我为什么没有这个想法? 要不就能少写一堆💩啦



    实现代码如下(标签部分):


    
                v-for="item in columns"
    :key="item.prop"
    :prop="item.prop"
    :label="item.label"
    :formatter="item.formatter"
    :width="item.width">



    思路是这样,把标签需要显示的定义在一个数组中,遍历数组来达到我们想要的效果,formatter是我们完成提交的数据和页面显示数据的一个转换所用到的。具体写法在下面js部分有写。


    定义数组的写法是vue3 composition api的写法,这个思路的话,用Vue2的写法也能实现的,重要的毕竟是思想(啊,我之前还是想不到这种思路)。



    再吐槽一下下,这种写法每写一个函数或者变量就要return回去,也挺麻烦的感觉,hhhhh



    实现代码如下(JS部分):


    const columns = reactive([
    {
    label:'用户ID',
    prop:'userId'
    },
    {
    label:'用户名',
    prop:'userName'
    },
    {
    label:'用户邮箱',
    prop:'userEmail'
    },
    {
    label:'用户角色',
    prop:'role',
    formatter(row,column,value){
    return {
    0:"管理员",
    1:"普通用户"
    }[value]
    }
    },
    {
    label:'用户状态',
    prop:'state',
    formatter(row,column,value){
    return {
    1:"在职",
    2:"离职",
    3:"试用期"
    }[value]
    }
    },
    {
    label:'注册时间',
    prop:'createTime'
    },
    {
    label:'最后登陆时间',
    prop:'lastLoginTime'
    }
    ])

    作者:Ned
    链接:https://juejin.cn/post/7025921628684943396

    收起阅读 »

    浏览器为什么能唤起App的页面

    疑问的开端 大家有没有想过一个问题:在浏览器里打开某个网页,网页上有一个按钮点击可以唤起App。 这样的效果是怎么实现的呢?浏览器是一个app;为什么一个app可以调起其他app的页面? 说到跨app的页面调用,大家是不是能够想到一个机制:Activity的...
    继续阅读 »

    疑问的开端


    大家有没有想过一个问题:在浏览器里打开某个网页,网页上有一个按钮点击可以唤起App。


    image.png


    这样的效果是怎么实现的呢?浏览器是一个app;为什么一个app可以调起其他app的页面?


    说到跨app的页面调用,大家是不是能够想到一个机制:Activity的隐式调用?


    一、隐式启动原理


    当我们有需要调起其他app的页面时,使用的API就是隐式调用。


    比如我们有一个app声明了这样的Activity:


    <activity android:name=".OtherActivity"
    android:screenOrientation="portrait">
    <intent-filter>
    <action android:name="mdove"/>
    <category android:name="android.intent.category.DEFAULT"/>
    </intent-filter>
    </activity>

    其他App想启动上边这个Activity如下的调用就好:


    val intent = Intent()
    intent.action = "mdove"
    startActivity(intent)

    我们没有主动声明Activity的class,那么系统是怎么为我们找到对应的Activity的呢?其实这里和正常的Activity启动流程是一样的,无非是if / else的实现不同而已。


    接下来咱们就回顾一下Activity的启动流程,为了避免陷入细节,这里只展开和大家相对“耳熟能详”的类和调用栈,以串流程为主。


    1.1、跨进程


    首先我们必须明确一点:无论是隐式启动还是显示启动;无论是启动App内Activity还是启动App外的Activity都是跨进程的。比如我们上述的例子,一个App想要启动另一个App的页面。



    注意没有root的手机,是看不到系统孵化出来的进程的。也就是我们常见的为什么有些代码打不上断点。



    image.png


    追过startActivity()的同学,应该很熟悉下边这个调用流程,跟进几个方法之后就发现进到了一个叫做ActivityTread的类里边。



    ActivityTread这个类有什么特点?有main函数,就是我们的主线程。



    很快我们能看到一个比较常见类的调用:Instrumentation


    // Activity.java
    public void startActivityForResult(@RequiresPermission Intent intent, int requestCode, @Nullable Bundle options) {
    mInstrumentation.execStartActivity(this, mMainThread.getApplicationThread(), mToken, this, intent, requestCode, options);
    // 省略
    }

    注意mInstrumentation#execStartActivity()有一个标黄的入参,它是ActivityThread中的内部类ApplicationThread



    ApplicationThread这个类有什么特点,它实现了IApplicationThread.Stub,也就是aidl的“跨进程调用的客户端回调”。



    此外mInstrumentation#execStartActivity()中又会看到一个大名鼎鼎的调用:


    public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options) {
    // 省略...
    ActivityManager.getService()
    .startActivity(whoThread, who.getBasePackageName(), intent,
    intent.resolveTypeIfNeeded(who.getContentResolver()),
    token, target != null ? target.mEmbeddedID : null,
    requestCode, 0, null, options);
    return null;
    }

    我们点击去getService()会看到一个标红的IActivityManager的类。



    它并不是一个.java文件,而是aidl文件。



    所以ActivityManager.``getService``()本质返回的是“进程的服务端”接口实例,也就是:


    1.2、ActivityManagerService



    public class ActivityManagerService extends IActivityManager.Stub



    所以执行到这就转到了系统进程(system_process进程)。省略一下代码细节,看一下调用栈:


    image.png


    从过上述debug截图,看一看到此时已经拿到了我们的目标Activitiy的相关信息。


    这里简化一些获取目标类的源码,直接引入结论:


    1.3、PackageManagerService


    这里类相当于解析手机内的所有apk,将其信息构造到内存之中,比如下图这样:



    image.png



    小tips:手机目录中/data/system/packages.xml,可以看到所有apk的path、进程名、权限等信息。



    1.4、启动新进程


    打开目标Activity的前提是:目标Activity的进程启动了。所以第一次想要打开目标Activity,就意味着要启动进程。


    启动进程的代码就在启动Activity的方法中:


    resumeTopActivityInnerLocked->startProcessLocked


    image.png


    这里便引入了另一个另一个大名鼎鼎的类:ZygoteInit。这里简单来说会通过ZygoteInit来进行App进程启动的。


    1.5、ApplicationThread


    进程启动后,继续回到目标Activity的启动流程。这里依旧是一系列的system_process进行的转来转去,然后IApplicationThread进入目标进程。



    注意看,在这里再次通过IApplicationThread回调到ActivityThread


    class H extends Handler {
    // 省略
    public void handleMessage(Message msg) {
    switch (msg.what) {
    case EXECUTE_TRANSACTION:
    final ClientTransaction transaction = (ClientTransaction) msg.obj;
    mTransactionExecutor.execute(transaction);
    // 省略
    break;
    case RELAUNCH_ACTIVITY:
    handleRelaunchActivityLocally((IBinder) msg.obj);
    break;
    }
    // 省略...
    }
    }

    // 执行Callback
    public void execute(ClientTransaction transaction) {
    final IBinder token = transaction.getActivityToken();
    executeCallbacks(transaction);
    }

    这里所谓的CallBack的实现是LaunchActivityItem#execute(),对应的实现:


    public void execute(ClientTransactionHandler client, IBinder token,
    PendingTransactionActions pendingActions) {
    ActivityClientRecord r = new ActivityClientRecord(token, mIntent, mIdent, mInfo,
    mOverrideConfig, mCompatInfo, mReferrer, mVoiceInteractor, mState, mPersistentState,
    mPendingResults, mPendingNewIntents, mIsForward,
    mProfilerInfo, client);
    client.handleLaunchActivity(r, pendingActions, null);
    }

    此时就转到了ActivityThread#handleLaunchActivity(),也就转到了咱们日常的生命周期里边,调用栈如下:



    上述截图的调用链中暗含了Activity实例化的过程(反射):


    public @NonNull Activity instantiateActivity(@NonNull ClassLoader cl, @NonNull String className, @Nullable Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {

    return (Activity) cl.loadClass(className).newInstance();

    }
    复制代码

    二、浏览器启动原理


    Helo站内的回流页就是一个标准的,浏览器唤起另一个App的实例。


    2.1、交互流程


    html标签有一个属性href,比如:<a href="...">


    我们常见的一种用法:<a href="``https://www.baidu.com``">。也就是点击之后跳转到百度。


    因为这个是前端的标签,依托于浏览器及其内核的实现,跳转到一个网页似乎很“顺其自然”(不然叫什么浏览器)。


    当然这里和android交互的流程基本一致:用隐式调用的方式,声明需要启动的Activity;然后<a href="">传入对应的协议(scheme)即可。比如:


    前端页面:


    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    </head>
    <body>
    <a href="mdove1://haha"> 启动OtherActivity </a>
    </body>

    android声明:


    <activity
    android:name=".OtherActivity"
    android:screenOrientation="portrait">
    <intent-filter>
    <data
    android:host="haha"
    android:scheme="mdove1" />
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.BROWSABLE" />
    <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
    </activity>

    2.2、推理实现


    浏览器能够加载scheme,可以理解为是浏览器内核做了封装。那么想要让android也能支持对scheme的解析,难道是由浏览器内核做处理吗?


    很明显不可能,做了一套移动端的操作系统,然后让浏览器过来实现,是不是有点杀人诛心。


    所以大概率能猜测出来,应该是手机中的浏览器app做的处理。我们就基于这个猜想去看一看浏览器.apk的实现。


    2.3、浏览器实现


    基于上边说的/data/system/packages.xml文件,我们可以pull出来浏览器的.apk。



    然后jadx反编译一下Browser.apk中WebView相关的源码:




    我们可以发现对href的处理来自于隐式跳转,所以一切就和上边的流程串了起来。


    作者:咸鱼正翻身
    链接:https://juejin.cn/post/7033751175551942692

    收起阅读 »

    实现穿梭栈帧的魔法--协程

    1. 协程-穿梭栈帧的魔法 协程的特性是代码中调用一次,实际会执行2次,第一次如果不满足条件先return一个状态,当满足条件的时候线程池会回调该方法执行第二次,而且还具有单例特性(指该函数第一次和第二次执行期间共享同一个上下文),妥妥的操作栈帧的魔法师。 2...
    继续阅读 »

    1. 协程-穿梭栈帧的魔法


    协程的特性是代码中调用一次,实际会执行2次,第一次如果不满足条件先return一个状态,当满足条件的时候线程池会回调该方法执行第二次,而且还具有单例特性(指该函数第一次和第二次执行期间共享同一个上下文),妥妥的操作栈帧的魔法师。


    2. 如何实现协程


    前提:本文仅探讨kotlin协程实现


    其实在反编译suspend函数反编译后就能知道协程的实现原理(以下)


    github.com/yujinyan/ko…


    //协程代码
    //suspend fun foo() :Any{
    // delay(3000L)
    // val value =getCurrentTime()
    // Log.e("TAG", "result is $value")
    //}
    //等价代码
    @suspend fun foo() {
    foo(object : Continuation<Any> {
    override fun resumeWith(result: Result<Any>) {
    val value = result.getOrThrow()
    Log.e("TAG", "result is $value")
    }
    })
    }

    @suspend fun foo(continuation: Continuation<Any>): Any {
    class FooContinuation : Continuation<Any> {
    var label: Int = 0

    override fun resumeWith(result: Result<Any>) {
    val outcome = invokeSuspend()
    if (outcome === COROUTINE_SUSPENDED) return
    continuation.resume(result.getOrThrow())
    }

    fun invokeSuspend(): Any {
    return foo(this)
    }
    }

    val cont = (continuation as? FooContinuation) ?: FooContinuation()
    return when (cont.label) {
    0 -> {
    cont.label++
    //异步延时任务
    AppExecutors.newInstance().otherIO.execute {
    Thread.sleep(3000L)
    val value = getCurrentTime()
    cont.resume(value)
    }
    COROUTINE_SUSPENDED
    }
    1 -> 1 // return 1
    else -> error("shouldn't happen")
    }
    }

    核心就是函数内匿名内部类的巧用,真的很妙



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

    构建Java IO框架体系

    IO框架 Java IO的学习是一件非常艰巨的任务。 它的挑战是来自于要覆盖所有的可能性。不仅存在各种I/O源端还有想要和他通信的接收端(文件/控制台/网络链接),而且还需要以不同的方式与他们进行通信(顺序/随机存取/缓冲/二进制/字符/行/字 等等)这...
    继续阅读 »

    IO框架


    	Java IO的学习是一件非常艰巨的任务。

    它的挑战是来自于要覆盖所有的可能性。不仅存在各种I/O源端还有想要和他通信的接收端(文件/控制台/网络链接),而且还需要以不同的方式与他们进行通信(顺序/随机存取/缓冲/二进制/字符/行/字 等等)这些情况综合起来就给我们带来了大量的学习任务,大量的类需要学习。


    我们要学会所有的这些java 的IO是很难的,因为我们没有构建一个关于IO的体系,要构建这个体系又需要深入理解IO库的演进过程,所以,我们如果缺乏历史的眼光,很快我们会对什么时候应该使用IO中的哪些类,以及什么时候不该使用它们而困惑。


    所以,在开发者的眼中,IO很乱,很多类,很多方法,很迷茫。


    IO简介


    数据流是一组有序,有起点和终点的字节的数据序列。包括输入流和输出流。


    流序列中的数据既可以是未经加工的原始二进制数据,也可以是经一定编码处理后符合某种格式规定的特定数据。因此Java中的流分为两种: **1) 字节流:**数据流中最小的数据单元是字节 **2) 字符流:**数据流中最小的数据单元是字符, Java中的字符是Unicode编码,一个字符占用两个字节。


    Java.io包中最重要的就是5个类和一个接口。5个类指的是File、OutputStream、InputStream、Writer、Reader;一个接口指的是Serializable。掌握了这些就掌握了Java I/O的精髓了。


    Java I/O主要包括如下3层次:


    1. 流式部分——最主要的部分。如:OutputStream、InputStream、Writer、Reader等

    2. 非流式部分——如:File类、RandomAccessFile类和FileDescriptor等类

    3. 其他——文件读取部分的与安全相关的类,如:SerializablePermission类,以及与本地操作系统相关的文件系统的类,如:FileSystem类和Win32FileSystem类和WinNTFileSystem类。


    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uFYWs0jZ-1638951173815)(F:\001_优秀课题\29_Java IO\IO图谱.png)]


    IO详细介绍


    在Android 平台,从应用的角度出发,我们最需要关注和研究的就是 字节流(Stream)字符流(Reader/Writer)和 File/ RandomAccessFile。当我们需要的时候再深入研究也未尝不是一件好事。关于字符和字节,例如文本文件,XML这些都是用字符流来读取和写入。而如RAR,EXE文件,图片等非文本,则用字节流来读取和写入。面对如此复杂的类关系,有一个点是我们必须要首先掌握的,那就是设计模式中的修饰模式,学会并理解修饰模式是搞懂流必备的前提条件哦。


    字节流的学习


    在具体的学习流之前,我们必须要学的一个设计模式是装饰模式。因为从流的整个发展历史,出现的各种类之间的关系看,都是沿用了修饰模式,都是一个类的功能可以用来修饰其他类,然后组合成为一个比较复杂的流。比如说:


         	DataOutputStream out = new DataOutputStream(
    new BufferedOutputStream(
    new FileOutputStream(file)));

    从上面的代码块中大家不难看出这些类的关系:为了向文件中写入数据,首先需要创建一个FileOutputStream,然后为了提升访问的效率,所以将它发送给具备缓存功能的BufferedOutput-Stream,而为了实现与机器类型无关的java基本类型数据的输出,所以,我们将缓存的流传递给了DataOutputStream。从上面的关系,我们可以看到,其根本目的都是为outputSteam添加额外的功能。而这种额外功能的添加就是采用了装饰模式来构建的代码。因此,学习流,必须要学好装饰模式。


    下面的图是一个关于字节流的图谱,这张图谱比较全面的概况了我们字节流中间的各个类以及他们之间的关系。


    输入输出流.jpg


    字节流的学习过程


    为什么要按照一个学习路线来呢?原因是他们的功能决定的。


    OutputStream -> FileOutputStream/FilterOutputStream ->DataOutputStream->bufferedOutputStream


    相应的学习InputStream方法就好了。


    从学习的角度来,我们应该先掌握FilterOutputStream, 以及FileOutputStream,这两个类是基本的类,从继承关系可以不难发现他们都是对 abstract 类 OutputStream的拓展,是它的子类。然而,伴随着 对 Stream流的功能的拓展,所以就出现了 DataOutputStream,(将java中的基础数据类型写入数据字节输出流中、保存在存储介质中、然后可以用DataOutputStream从存储介质中读取到程序中还原成java基础类型)。这里多提一句、DataOutputStream、FilterOutputStream三个类的关系的这种设计既使用了装饰器模式 避免了类的爆炸式增长。


    为了提升Stream的执行效率,所以出现了bufferedOutputStream。bufferedOutputStream就是将本地添加了一个缓存的数组。在使用bufferedOutputStream之前每次从磁盘读入数据的时候都是需要访问多少byte数据就向磁盘中读多少个byte的数据,而出现bufferedOutputSteam之后,策略就改了,会先读取整个缓存空间相应大小的数据,这样就是从磁盘读取了一块比较大的数据,然后缓存起来,从而减少了对磁盘的访问的次数以达到提升性能的目的。


    另外一方面,我们知道了outputStream(输出流)的发展历史后,我们便可以知道如何使用outpuSteam了,同样的方法,我们可以运用到inputStream中来,这样对称的解释就出现到了inputStream相关的中来了,于是,我们对整个字节流就有了全方位的理解,所以这样子我们就不会感觉到流的复杂了。这个时候对于其他的一些字节流的使用(byteArrayOutputStream/PipeOutputStream/ObjectOutputStream)的学习就自需要在使用的时候看看API即可。


    字符流的学习


    下图则是一个关于字符流的图谱,这张图谱比较全面的概况了我们字符流中间的各个类以及他们之间的关系。


    字符输入输出流.jpg


    字符流的学习和字节流的学习是一样的,它和字节流有着同样的发展过程,只是,字节流面向的是我们未知或者即使知道了他们的编码格式也意义不大的文件(png,exe, zip)的时候是采用字节,而面对一些我们知道文件构造我们就能够搞懂它的意义的文件(json,xml)等文件的时候我们还是需要以字符的形式来读取,所以就出现了字符流。reader 和 Stream最大的区别我认为是它包含了一个readline()接口,这个接口标明了,一行数据的意义,这也是可以理解的,因为自有字符才具备行的概念,相反字节流中的行也就是一个字节符号。


    字符流的学习历程:


    Writer- >FilterWriter->BufferedWriter->OutputStreamWriter->FileWriter->其他


    同时类比着学习Reader相关的类。


    FilterWriter/FilterReader

    字符过滤输出流、与FilterOutputStream功能一样、只是简单重写了父类的方法、目的是为所有装饰类提供标准和基本的方法、要求子类必须实现核心方法、和拥有自己的特色。这里FilterWriter没有子类、可能其意义只是提供一个接口、留着以后的扩展。。。本身是一个抽象类。


    BufferedWriter/BufferedReader

    BufferedWriter是 Writer类的一个子类。他的功能是为传入的底层字符输出流提供缓存功能、同样当使用底层字符输出流向目的地中写入字符或者字符数组时、每写入一次就要打开一次到目的地的连接、这样频繁的访问不断效率底下、也有可能会对存储介质造成一定的破坏、比如当我们向磁盘中不断的写入字节时、夸张一点、将一个非常大单位是G的字节数据写入到磁盘的指定文件中的、没写入一个字节就要打开一次到这个磁盘的通道、这个结果无疑是恐怖的、而当我们使用BufferedWriter将底层字符输出流、比如FileReader包装一下之后、我们可以在程序中先将要写入到文件中的字符写入到BufferedWriter的内置缓存空间中、然后当达到一定数量时、一次性写入FileReader流中、此时、FileReader就可以打开一次通道、将这个数据块写入到文件中、这样做虽然不可能达到一次访问就将所有数据写入磁盘中的效果、但也大大提高了效率和减少了磁盘的访问量!


    OutputStreamWriter/InputStreamReader

    输入字符转换流、是输入字节流转向输入字符流的桥梁、用于将输入字节流转换成输入字符流、通过指定的或者默认的编码将从底层读取的字节转换成字符返回到程序中、与OutputStreamWriter一样、本质也是使用其内部的一个类来完成所有工作:StreamDecoder、使用默认或者指定的编码将字节转换成字符;OutputStreamWriter/ InputStreamReader只是对StreamDecoder进行了封装、isr内部所有方法核心都是调用StreamDecoder来完成的、InputStreamReader只是对StreamDecoder进行了封装、使得我们可以直接使用读取方法、而不用关心内部实现。


    	OutputStreamWriter、InputStreamReader分别为InputStream、OutputStream的低级输入输出流提供将字节转换成字符的桥梁、他们只是外边的一个门面、真正的核心:

    OutputStreamWriter中的StreamEncoder:


             1、使用指定的或者默认的编码集将字符转码为字节        

    2、调用StreamEncoder自身实现的写入方法将转码后的字节写入到底层字节输出流中。

    InputStreamReader中的StreamDecoder:


            1、使用指定的或者默认的编码集将字节解码为字符         

    2、调用StreamDecoder自身实现的读取方法将解码后的字符读取到程序中。

    在理解这两个流的时候要注意:java——io中只有将字节转换成字符的类、没有将字符转换成字节的类、原因很简单——字符流的存在本来就像对字节流进行了装饰、加工处理以便更方便的去使用、在使用这两个流的时候要注意:由于这两个流要频繁的对读取或者写入的字节或者字符进行转码、解码和与底层流的源和目的地进行交互、所以使用的时候要使用BufferedWriter、BufferedReader进行包装、以达到最高效率、和保护存储介质。


    FileReader/FileWriter

    FileReader和FileWriter 继承于InputStreamReader/OutputStreamWriter。


    从源码可以发现FileWriter 文件字符输出流、主要用于将字符写入到指定的打开的文件中、其本质是通过传入的文件名、文件、或者文件描述符来创建FileOutputStream、然后使用OutputStreamWriter使用默认编码将FileOutputStream转换成Writer(这个Writer就是FileWriter)。如果使用这个类的话、最好使用BufferedWriter包装一下、高端大气上档次、低调奢华有内涵!


    FileReader 文件字符输入流、用于将文件内容以字符形式读取出来、一般用于读取字符形式的文件内容、也可以读取字节形式、但是因为FileReader内部也是通过传入的参数构造InputStreamReader、并且只能使用默认编码、所以我们无法控制编码问题、这样的话就很容易造成乱码。所以读取字节形式的文件还是使用字节流来操作的好、同样在使用此流的时候用BufferedReader包装一下、就算冲着BufferedReader的readLine()方法去的也要使用这个包装类、不说他还能提高效率、保护存储介质。


    字节流与字符流的关系


    那么字节输入流和字符输入流之间的关系是怎样的呢?请看下图


    字节与字符输入流.jpg


    同样的字节与字符输出流字节的关系也如下图所示


    字节与字符输出流.jpg


    字节流与字符流的区别


    字节流和字符流使用是非常相似的,那么除了操作代码的不同之外,还有哪些不同呢?


      字节流在操作的时候本身是不会用到缓冲区(内存)的,是与文件本身直接操作的,而字符流在操作的时候是使用到缓冲区的字节流在操作文件时,即使不关闭资源(close方法),文件也能输出,但是如果字符流不使用close方法的话,则不会输出任何内容,说明字符流用的是缓冲区,并且可以使用flush方法强制进行刷新缓冲区,这时才能在不close的情况下输出内容


      那开发中究竟用字节流好还是用字符流好呢?

      在所有的硬盘上保存文件或进行传输的时候都是以字节的方法进行的,包括图片也是按字节完成,而字符是只有在内存中才会形成的,所以使用字节的操作是最多的。


      如果要java程序实现一个拷贝功能,应该选用字节流进行操作(可能拷贝的是图片),并且采用边读边写的方式(节省内存)。


    字节流与字符流的转换


    虽然Java支持字节流和字符流,但有时需要在字节流和字符流两者之间转换。InputStreamReader和OutputStreamWriter,这两个为类是字节流和字符流之间相互转换的类。


      InputSreamReader用于将一个字节流中的字节解码成字符:

      有两个构造方法: 


       InputStreamReader(InputStream in);

      功能:用默认字符集创建一个InputStreamReader对象


       InputStreamReader(InputStream in,String CharsetName);

      功能:接收已指定字符集名的字符串,并用该字符创建对象


      OutputStream用于将写入的字符编码成字节后写入一个字节流。

      同样有两个构造方法


      OutputStreamWriter(OutputStream out);

      功能:用默认字符集创建一个OutputStreamWriter对象;   


      OutputStreamWriter(OutputStream out,String  CharSetName);

      功能:接收已指定字符集名的字符串,并用该字符集创建OutputStreamWrite对象


    为了避免频繁的转换字节流和字符流,对以上两个类进行了封装。


      BufferedWriter类封装了OutputStreamWriter类;


      BufferedReader类封装了InputStreamReader类;


      封装格式


      BufferedWriter out=new BufferedWriter(new OutputStreamWriter(System.out));
    BufferedReader in= new BufferedReader(new InputStreamReader(System.in);

      利用下面的语句,可以从控制台读取一行字符串:


      BufferedReader in=new BufferedReader(new InputStreamReader(System.in));
    String line=in.readLine();


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

    kotlin 与java 互操作

    简介 大多数情况下,你不需要关注这个问题。但是,如果你的代码中包含了部分Java代码,了解这些可能帮你解决一些棘手的问题,同时让你设计的Api更加可靠 互操作性与可空性 Java世界里所有对象都可能是null,当一个kotlin函数返回string类型值,你不...
    继续阅读 »

    简介


    大多数情况下,你不需要关注这个问题。但是,如果你的代码中包含了部分Java代码,了解这些可能帮你解决一些棘手的问题,同时让你设计的Api更加可靠


    互操作性与可空性


    Java世界里所有对象都可能是null,当一个kotlin函数返回string类型值,你不能想当然地认为它的返回值就能符合kotlin关于空值的规定


    kotlin


    fun main() {
    val my = MyClass()
    val value = my.getCanNullValue()
    println(value?.capitalize())
    }

    java


    public class MyClass {
    public String value;

    public String getCanNullValue(){
    return value;
    }
    }

    类型映射


    代码运行时,所有的映射类型都会重新映射回对应的java类型


    fun main() {
    val my = MyClass()
    my.value = "a123"
    val value = my.getCanNullValue()
    println(value.javaClass)
    }

    结果为:class java.lang.String


    属性访问


    不需要调用相关setter方法,你可以使用赋值语法来设置一个java字段值了


    val my = MyClass()
    my.value = "a123"

    @JvmName


    这个注解可以改变字节码中生成的类名或方法名称,如果作用在顶级作用域(文件中),则会改变生成对应Java类的名称。如果作用在方法上,则会改变生成对应Java方法的名称。


    kotlin


    @file:JvmName("FooKt")
    @JvmName("foo1")
    fun foo() {
    println("Hello, Jvm...")
    }

    java


    // 相当于下面的Java代码
    public final class FooKt {
    public static final void foo1() {
    String var0 = "Hello, Jvm...";
    System.out.println(var0);
    }
    }

    第一个注解@file:JvmName("FooKt")的作用是使生成的类名变为FooKt,第二个注解的作用是使生成的方法名称变为foo1


    @JvmField


    Kotlin编译器默认会将类中声明的成员变量编译成私有变量,Java语言要访问该变量必须通过其生成的getter方法。而使用上面的注解可以向Java暴露该变量,即使其访问变为公开(修饰符变为public)。


    Kotlin


    class JavaToKotlin {
    @JvmField
    val info = "Hello"
    }

    @JvmOverloads


    由于Kotlin语言支持方法参数默认值,而实现类似功能Java需要使用方法重载来实现,这个注解就是为解决这个问题而生的,添加这个注解会自动生成重载方法


    Kotlin


    @JvmOverloads
    fun prinltInfo(name: String, age: Int = 1) {
    println("$name $age")
    }

    java


     public static void main(String[] args) {
    MyKotlin.prinltInfo("arrom");
    MyKotlin.prinltInfo("arrom", 20);
    }

    @JvmStatic


    @JvmStatic注解的作用类似于@JvmField,可以直接调用伴生对象里的函数


    class JavaToKotlin {
    @JvmField
    val info = "Hello"

    companion object {
    @JvmField
    val max: Int = 200

    @JvmStatic
    fun loadConfig(): String {
    return "loading config"
    }
    }
    }

    @Throws


    由于Kotlin语言不支持CE(Checked Exception),所谓CE,即方法可能抛出的异常是已知的。Java语言通过throws关键字在方法上声明CE。为了兼容这种写法,Kotlin语言新增了@Throws注解,该注解的接收一个可变参数,参数类型是多个异常的KClass实例。Kotlin编译器通过读取注解参数,在生成的字节码中自动添加CE声明。


    Kotlin


    @Throws(IllegalArgumentException::class)
    fun div(x: Int, y: Int): Float {
    return x.toFloat() / y
    }

    Java


    // 生成的代码相当于下面这段Java代码
    public static final float div(int x, int y) throws IllegalArgumentException {
    return (float)x / (float)y;
    }

    添加了@Throws(IllegalArgumentException::class)注解后,在生成的方法签名上自动添加了可能抛出的异常声明(throws IllegalArgumentException),即CE。


    @Synchronized


    用于产生同步方法。Kotlin语言不支持synchronized关键字,处理类似Java语言的并发问题,Kotlin语言建议使用同步方法进行处理


    Kotlin


    @Synchronized
    fun start() {
    println("Start do something...")
    }

    java


    // 生成的代码相当于下面这段Java代码
    public static final synchronized void start() {
    String var0 = "Start do something...";
    System.out.println(var0);
    }

    函数类型操作


    Java中没有函数类型,所以,在Java里,kotlin函数类型使用FunctionN这样的名字的接口来表示,N代表入参的个数,一共有24个这样的接口,从Function0到Function23,每个接口都包含一个invoke函数,调用匿名函数需要调用invoke


    kotlin:


    val funcp:(String) -> String = {
    it.capitalize()
    }

    java:


    Function1 funcp = ArromKt.getFuncp();
    funcp.invoke("arrom");

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