注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

又到年会抽奖的时候,这是你要的抽奖程序

原标题:公司年会用了我的抽奖程序,然后我中奖了…… 这是我去年写的代码和文章,眼看又到年底抽奖季了,翻出来洗洗还能再用背景临近年末,又到了各大公司举办年会的时候了。对于年会,大家最关心的应该就是抽奖了吧?虽然中奖概率通常不高,但总归是个机会,期待一下也是好...
继续阅读 »

原标题:公司年会用了我的抽奖程序,然后我中奖了……

这是我去年写的代码和文章,眼看又到年底抽奖季了,翻出来洗洗还能再用

背景

临近年末,又到了各大公司举办年会的时候了。对于年会,大家最关心的应该就是抽奖了吧?虽然中奖概率通常不高,但总归是个机会,期待一下也是好的。

最近,我们部门举办了年会,也有抽奖环节。临近年会的前几天,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
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    LiveData学习记

    LiveData 使用 var liveData: MutableLiveData<String>? = null //初始化 liveData = MutableLiveData() // 设置 observe liveData?.observe...
    继续阅读 »

    LiveData 使用


    var liveData: MutableLiveData<String>? = null
    //初始化
    liveData = MutableLiveData()
    // 设置 observe
    liveData?.observe(this, {
    Log.e("Main2", "2界面接收数据 = $it")
    Toast.makeText(this, "2界面接收数据 = $it", Toast.LENGTH_LONG).show()
    })
    // 发送值
    liveData?.value = "2界面发送数据 $indexValue"

    LiveData 是针对同一个界面数据相互传递, 配合 MVVM 使用


    如果想跨界面使用 比如 Activity1 想传值 给 Activity2 可以把LiveData 下沉(二次封装)


    package com.one_hour.test_livedata
    import androidx.lifecycle.MutableLiveData
    object LiveDataBusBeta{
    //创建一个Map 管理 LiveData
    private val liveDataMap: MutableMap<String, MutableLiveData<Any>> = HashMap()
    // 设置一个 key
    fun <T> getLiveData(key: String) : MutableLiveData<T>? {
    if (!liveDataMap.containsKey(key)) {
    liveDataMap.put(key, MutableLiveData<Any>())
    }
    return liveDataMap[key] as MutableLiveData<T>
    }
    fun removeMapLiveData(key : String) {
    liveDataMap.remove(key)
    }
    }

    像这样下沉后会出现 Bug, 如场景:当界面Activity1 向未创建的Activity2 发送消息时,会在Activity2 创建时 出现从界面1传过来的数据,这是我们不需要的。(现象出现叫 消息粘性)


    什么是粘性事件

    即发射的事件如果早于注册,那么注册之后依然可以接收到的事件称为粘性事件


    消息粘性 咋个形成的 ?
    先创建 new MutableLiveData -> setValue -> observe(绑定监听)


    LiveData 绑定(observe)源码

        @MainThread
    public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
    assertMainThread("observe");
    if (owner.getLifecycle().getCurrentState() == DESTROYED) {
    // ignore
    return;
    }
    LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
    ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
    if (existing != null && !existing.isAttachedTo(owner)) {
    throw new IllegalArgumentException("Cannot add the same observer"
    + " with different lifecycles");
    }
    if (existing != null) {
    return;
    }
    owner.getLifecycle().addObserver(wrapper);
    }


    1.


    owner.getLifecycle() 获取的是 Lifecycle 监听Activity 生命周期变化的流程
    androidx.appcompat.app.AppCompatActivity (继承)-> androidx.fragment.app.FragmentActivity (继承)-> androidx.activity.ComponentActivity (继承)->androidx.core.app.ComponentActivity( 实现 LifecycleOwner) -> 现在 实例化 private LifecycleRegistry mLifecycleRegistry = new LifecycleRegistry(this);


    androidx.core.app.ComponentActivity( 实现 LifecycleOwner)


    @CallSuper
    @Override
    protected void onSaveInstanceState(@NonNull Bundle outState) {
    //添加一个 mLifecycleRegistry 状态管理
    mLifecycleRegistry.markState(Lifecycle.State.CREATED);
    super.onSaveInstanceState(outState);
    }

    androidx.activity.ComponentActivity( 实现 LifecycleOwner)


        @CallSuper
    @Override
    protected void onSaveInstanceState(@NonNull Bundle outState) {
    Lifecycle lifecycle = getLifecycle();
    //设置 lifecycle 当前状态
    if (lifecycle instanceof LifecycleRegistry) {
    ((LifecycleRegistry) lifecycle).setCurrentState(Lifecycle.State.CREATED);
    }
    super.onSaveInstanceState(outState);
    mSavedStateRegistryController.performSave(outState);
    }

    androidx.fragment.app.FragmentActivity


    final LifecycleRegistry mFragmentLifecycleRegistry = new LifecycleRegistry(this);
    //开始绑定什么周期 调用 handleLifecycleEvent 绑定状态
    mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.xxx);

    2.


    ** owner.getLifecycle().addObserver(wrapper); 中 addObserver 调用了 androidx.lifecycle.LifecycleRegistry的 addObserver,而LifecycleRegistry是在FragmentActivity类中实例化获取**


        @Override
    public void addObserver(@NonNull LifecycleObserver observer) {
    State initialState = mState == DESTROYED ? DESTROYED : INITIALIZED;
    ObserverWithState statefulObserver = new ObserverWithState(observer, initialState);
    ObserverWithState previous = mObserverMap.putIfAbsent(observer, statefulObserver);

    if (previous != null) {
    return;
    }
    LifecycleOwner lifecycleOwner = mLifecycleOwner.get();
    if (lifecycleOwner == null) {
    // it is null we should be destroyed. Fallback quickly
    return;
    }

    boolean isReentrance = mAddingObserverCounter != 0 || mHandlingEvent;
    State targetState = calculateTargetState(observer);
    mAddingObserverCounter++;
    while ((statefulObserver.mState.compareTo(targetState) < 0
    && mObserverMap.contains(observer))) {
    pushParentState(statefulObserver.mState);
    statefulObserver.dispatchEvent(lifecycleOwner, upEvent(statefulObserver.mState));
    popParentState();
    // mState / subling may have been changed recalculate
    targetState = calculateTargetState(observer);
    }
    、、、、、省略代码
    }

    statefulObserver.dispatchEvent(lifecycleOwner, upEvent(statefulObserver.mState)); 在循环中一直调用


        static class ObserverWithState {
    State mState;
    LifecycleEventObserver mLifecycleObserver;

    ObserverWithState(LifecycleObserver observer, State initialState) {
    mLifecycleObserver = Lifecycling.lifecycleEventObserver(observer);
    mState = initialState;
    }

    void dispatchEvent(LifecycleOwner owner, Event event) {
    State newState = getStateAfter(event);
    mState = min(mState, newState);
    mLifecycleObserver.onStateChanged(owner, event);
    mState = newState;
    }
    }

    LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer); 监听状态改变 并且
    在ObserverWithState 中调用了 mLifecycleObserver.onStateChanged(owner, event); -》mLifecycleObserver 指的就是 LifecycleBoundObserver


    class LifecycleBoundObserver extends ObserverWrapper implements GenericLifecycleObserver


            @Override
    public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
    if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
    removeObserver(mObserver);
    return;
    }
    // 这里是如果状态 是可见的 那么就发送消息
    // 就调用 class LifecycleBoundObserver extends ObserverWrapper 父类 ObserverWrapper 的方法
    //shouldBeActive() 获取 mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED); 状态 是否可见
    activeStateChanged(shouldBeActive());
    }

    class LifecycleBoundObserver extends ObserverWrapper 父类 ObserverWrapper 的方法 并分发 dispatchingValue 值


            void activeStateChanged(boolean newActive) {
    if (newActive == mActive) {
    return;
    }
    // immediately set active state, so we'd never dispatch anything to inactive
    // owner
    mActive = newActive;
    boolean wasInactive = LiveData.this.mActiveCount == 0;
    LiveData.this.mActiveCount += mActive ? 1 : -1;
    if (wasInactive && mActive) {
    onActive();
    }
    if (LiveData.this.mActiveCount == 0 && !mActive) {
    onInactive();
    }
    if (mActive) {
    // 调用 dispatchingValue 回到 abstract class LiveData<T> 类里面的 dispatchingValue 方法
    dispatchingValue(this);
    }
    }
    }

    dispatchingValue 都调用了相同的函数 considerNotify


        void dispatchingValue(@Nullable ObserverWrapper initiator) {
    if (mDispatchingValue) {
    mDispatchInvalidated = true;
    return;
    }
    mDispatchingValue = true;
    do {
    mDispatchInvalidated = false;
    if (initiator != null) {
    considerNotify(initiator);
    initiator = null;
    } else {
    for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> iterator =
    mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
    considerNotify(iterator.next().getValue());
    if (mDispatchInvalidated) {
    break;
    }
    }
    }
    } while (mDispatchInvalidated);
    mDispatchingValue = false;
    }

    considerNotify 中 observer.mObserver.onChanged 回调数据


        private void considerNotify(ObserverWrapper observer) {
    if (!observer.mActive) {
    return;
    }
    // Check latest state b4 dispatch. Maybe it changed state but we didn't get the event yet.
    //
    // we still first check observer.active to keep it as the entrance for events. So even if
    // the observer moved to an active state, if we've not received that event, we better not
    // notify for a more predictable notification order.
    if (!observer.shouldBeActive()) {
    observer.activeStateChanged(false);
    return;
    }
    if (observer.mLastVersion >= mVersion) {
    return;
    }
    observer.mLastVersion = mVersion;
    //noinspection unchecked
    observer.mObserver.onChanged((T) mData);
    }

    解决粘性代码


    • 方法1


    import androidx.lifecycle.LifecycleOwner
    import androidx.lifecycle.MutableLiveData
    import androidx.lifecycle.Observer


    class BaseLiveData<T> : MutableLiveData<T>() {
    private var isSticky: Boolean = false
    private var mStickyData: T? = null
    private var mVersion = 0

    override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
    if (isSticky) {
    super.observe(owner, observer)
    } else {
    super.observe(owner, CustomObserver<T>(this, observer))
    }

    }

    /**
    * 发送非粘性数据
    */
    override fun setValue(value: T) {
    mVersion++
    isSticky = false
    super.setValue(value)
    }

    override fun postValue(value: T) {
    mVersion++
    isSticky = false
    super.postValue(value)
    }

    /**
    * 发送粘性数据
    */
    fun setStickyData(data: T?) {
    mStickyData = data
    isSticky = true
    setValue(data!!)
    }

    fun postStickyData(mStickyData: T?) {
    this.mStickyData = mStickyData
    isSticky = true
    super.postValue(mStickyData!!)
    }

    inner class CustomObserver<T>(val mLiveData: BaseLiveData<T>, var mObserver: Observer<in T>?,
    var isSticky: Boolean = false) : Observer<T> {

    private var mLastVersion = mLiveData.mVersion

    override fun onChanged(t: T) {
    if (mLastVersion >= mLiveData.mVersion) {
    if (isSticky && mLiveData.mStickyData != null) {
    mObserver?.onChanged(mLiveData.mStickyData)
    }
    return
    }
    mLastVersion = mLiveData.mVersion
    mObserver?.onChanged(t)

    }

    }
    }


    • 方法2


    利用反射 修改 observer.mLastVersion 值
    observer.mLastVersion 的 获取值的调用链 :
    observer.mLastVersion -》considerNotify (iterator.next().getValue()) -> mObservers (SafeIterableMap<Observer<? super T>, ObserverWrapper> mObservers = new SafeIterableMap<>()) -> ObserverWrapper(int mLastVersion = START_VERSION;) (子类LifecycleBoundObserver, 但是只有父类 ObserverWrapper 才有 mLastVersion, 所以获取父类的 mLastVersion 进行修改)


    import androidx.lifecycle.LifecycleOwner
    import androidx.lifecycle.LiveData
    import androidx.lifecycle.MutableLiveData
    import androidx.lifecycle.Observer
    import java.lang.reflect.Field
    import java.lang.reflect.Method


    class BaseUnStickyLiveData<T> : MutableLiveData<T>() {

    private var isSticky = false

    override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
    super.observe(owner, observer)
    if (!isSticky) {
    hookClass(observer)
    }
    }

    override fun setValue(value: T) {
    isSticky = false
    super.setValue(value)
    }

    override fun postValue(value: T) {
    isSticky = false
    super.postValue(value)
    }

    fun setStickyValue(value: T) {
    isSticky = true
    super.setValue(value)
    }

    fun setStickyPostValue(value: T) {
    isSticky = true
    super.postValue(value)
    }

    private fun hookClass(observer: Observer<in T>) {
    val liveDataClass = LiveData::class.java
    try {
    //获取field private SafeIterableMap<Observer<T>, ObserverWrapper> mObservers
    val mObservers: Field = liveDataClass.getDeclaredField("mObservers")
    mObservers.setAccessible(true)

    //获取SafeIterableMap集合mObservers
    val observers: Any = mObservers.get(this)

    //获取SafeIterableMap的get(Object obj)方法
    val observersClass: Class<*> = observers.javaClass
    val methodGet: Method = observersClass.getDeclaredMethod("get", Any::class.java)
    methodGet.setAccessible(true)

    //获取到observer在集合中对应的ObserverWrapper对象
    val objectWrapperEntry: Any = methodGet.invoke(observers, observer)
    var objectWrapper: Any? = null
    if (objectWrapperEntry is Map.Entry<*, *>) {
    objectWrapper = objectWrapperEntry.value
    }
    if (objectWrapper == null) {
    //throw NullPointerException("ObserverWrapper can not be null")
    return
    }

    // 获取ListData的mVersion
    val mVersion: Field = liveDataClass.getDeclaredField("mVersion")
    mVersion.setAccessible(true)
    val mVersionValue: Any = mVersion.get(this)

    //获取ObserverWrapper的Class对象 LifecycleBoundObserver extends ObserverWrapper
    val wrapperClass: Class<*> = objectWrapper.javaClass.superclass

    //获取ObserverWrapper的field mLastVersion
    val mLastVersion: Field = wrapperClass.getDeclaredField("mLastVersion")
    mLastVersion.setAccessible(true)

    //把当前ListData的mVersion赋值给 ObserverWrapper的field mLastVersion
    mLastVersion.set(objectWrapper, mVersionValue)
    } catch (e: Exception) {
    if (BuildConfig.DEBUG) {
    throw RuntimeException(e)
    } else {
    e.printStackTrace()
    }
    }
    }
    }

    配合二次 封装的 LiveDataBusBeta 使用


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

    看一遍就理解:动态规划详解

    前言 我们刷leetcode的时候,经常会遇到动态规划类型题目。动态规划问题非常非常经典,也很有技巧性,一般大厂都非常喜欢问。今天跟大家一起来学习动态规划的套路,文章如果有不正确的地方,欢迎大家指出哈,感谢感谢~ 什么是动态规划? 动态规划的核心思想 一个例...
    继续阅读 »

    前言


    我们刷leetcode的时候,经常会遇到动态规划类型题目。动态规划问题非常非常经典,也很有技巧性,一般大厂都非常喜欢问。今天跟大家一起来学习动态规划的套路,文章如果有不正确的地方,欢迎大家指出哈,感谢感谢~



    • 什么是动态规划?

    • 动态规划的核心思想

    • 一个例子走进动态规划

    • 动态规划的解题套路

    • leetcode案例分析



    公众号:捡田螺的小男孩


    什么是动态规划?


    动态规划(英语:Dynamic programming,简称 DP),是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题。



    dynamic programming is a method for solving a complex problem by breaking it down into a collection of simpler subproblems.



    以上定义来自维基百科,看定义感觉还是有点抽象。简单来说,动态规划其实就是,给定一个问题,我们把它拆成一个个子问题,直到子问题可以直接解决。然后呢,把子问题答案保存起来,以减少重复计算。再根据子问题答案反推,得出原问题解的一种方法。



    一般这些子问题很相似,可以通过函数关系式递推出来。然后呢,动态规划就致力于解决每个子问题一次,减少重复计算,比如斐波那契数列就可以看做入门级的经典动态规划问题。



    动态规划核心思想


    动态规划最核心的思想,就在于拆分子问题,记住过往,减少重复计算


    动态规划在于记住过往


    我们来看下,网上比较流行的一个例子:




    • A : "1+1+1+1+1+1+1+1 =?"

    • A : "上面等式的值是多少"

    • B : 计算 "8"

    • A : 在上面等式的左边写上 "1+" 呢?

    • A : "此时等式的值为多少"

    • B : 很快得出答案 "9"

    • A : "你怎么这么快就知道答案了"

    • A : "只要在8的基础上加1就行了"

    • A : "所以你不用重新计算,因为你记住了第一个等式的值为8!动态规划算法也可以说是 '记住求过的解来节省时间'"



    一个例子带你走进动态规划 -- 青蛙跳阶问题


    暴力递归



    leetcode原题:一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 10 级的台阶总共有多少种跳法。



    有些小伙伴第一次见这个题的时候,可能会有点蒙圈,不知道怎么解决。其实可以试想:




    • 要想跳到第10级台阶,要么是先跳到第9级,然后再跳1级台阶上去;要么是先跳到第8级,然后一次迈2级台阶上去。

    • 同理,要想跳到第9级台阶,要么是先跳到第8级,然后再跳1级台阶上去;要么是先跳到第7级,然后一次迈2级台阶上去。

    • 要想跳到第8级台阶,要么是先跳到第7级,然后再跳1级台阶上去;要么是先跳到第6级,然后一次迈2级台阶上去。



    假设跳到第n级台阶的跳数我们定义为f(n),很显然就可以得出以下公式:


    f(10) = f(9)+f(8)
    f (9) = f(8) + f(7)
    f (8) = f(7) + f(6)
    ...
    f(3) = f(2) + f(1)

    即通用公式为: f(n) = f(n-1) + f(n-2)

    那f(2) 或者 f(1) 等于多少呢?



    • 当只有2级台阶时,有两种跳法,第一种是直接跳两级,第二种是先跳一级,然后再跳一级。即f(2) = 2;

    • 当只有1级台阶时,只有一种跳法,即f(1)= 1;


    因此可以用递归去解决这个问题:


    class Solution {
    public int numWays(int n) {
    if(n == 1){
    return 1;
    }
    if(n == 2){
    return 2;
    }
    return numWays(n-1) + numWays(n-2);
    }
    }

    去leetcode提交一下,发现有问题,超出时间限制了



    为什么超时了呢?递归耗时在哪里呢?先画出递归树看看:




    • 要计算原问题 f(10),就需要先计算出子问题 f(9) 和 f(8)

    • 然后要计算 f(9),又要先算出子问题 f(8) 和 f(7),以此类推。

    • 一直到 f(2) 和 f(1),递归树才终止。


    我们先来看看这个递归的时间复杂度吧:


    递归时间复杂度 = 解决一个子问题时间*子问题个数


    • 一个子问题时间 = f(n-1)+f(n-2),也就是一个加法的操作,所以复杂度是 O(1);

    • 问题个数 = 递归树节点的总数,递归树的总节点 = 2^n-1,所以是复杂度O(2^n)。


    因此,青蛙跳阶,递归解法的时间复杂度 = O(1) * O(2^n) = O(2^n),就是指数级别的,爆炸增长的,如果n比较大的话,超时很正常的了。


    回过头来,你仔细观察这颗递归树,你会发现存在大量重复计算,比如f(8)被计算了两次,f(7)被重复计算了3次...所以这个递归算法低效的原因,就是存在大量的重复计算


    既然存在大量重复计算,那么我们可以先把计算好的答案存下来,即造一个备忘录,等到下次需要的话,先去备忘录查一下,如果有,就直接取就好了,备忘录没有才开始计算,那就可以省去重新重复计算的耗时啦!这就是带备忘录的解法。


    带备忘录的递归解法(自顶向下)


    一般使用一个数组或者一个哈希map充当这个备忘录



    • 第一步,f(10)= f(9) + f(8),f(9) 和f(8)都需要计算出来,然后再加到备忘录中,如下:




    • 第二步, f(9) = f(8)+ f(7),f(8)= f(7)+ f(6), 因为 f(8) 已经在备忘录中啦,所以可以省掉,f(7),f(6)都需要计算出来,加到备忘录中~



    第三步, f(8) = f(7)+ f(6),发现f(8),f(7),f(6)全部都在备忘录上了,所以都可以剪掉。



    所以呢,用了备忘录递归算法,递归树变成光秃秃的树干咯,如下:



    备忘录的递归算法,子问题个数=树节点数=n,解决一个子问题还是O(1),所以带备忘录的递归算法的时间复杂度是O(n)。接下来呢,我们用带备忘录的递归算法去撸代码,解决这个青蛙跳阶问题的超时问题咯~,代码如下:


    public class Solution {
    //使用哈希map,充当备忘录的作用
    Map<Integer, Integer> tempMap = new HashMap();
    public int numWays(int n) {
    // n = 0 也算1种
    if (n == 0) {
    return 1;
    }
    if (n <= 2) {
    return n;
    }
    //先判断有没计算过,即看看备忘录有没有
    if (tempMap.containsKey(n)) {
    //备忘录有,即计算过,直接返回
    return tempMap.get(n);
    } else {
    // 备忘录没有,即没有计算过,执行递归计算,并且把结果保存到备忘录map中,对1000000007取余(这个是leetcode题目规定的)
    tempMap.put(n, (numWays(n - 1) + numWays(n - 2)) % 1000000007);
    return tempMap.get(n);
    }
    }
    }

    去leetcode提交一下,如图,稳了:



    其实,还可以用动态规划解决这道题。


    自底向上的动态规划


    动态规划跟带备忘录的递归解法基本思想是一致的,都是减少重复计算,时间复杂度也都是差不多。但是呢:



    • 带备忘录的递归,是从f(10)往f(1)方向延伸求解的,所以也称为自顶向下的解法。

    • 动态规划从较小问题的解,由交叠性质,逐步决策出较大问题的解,它是从f(1)往f(10)方向,往上推求解,所以称为自底向上的解法。


    动态规划有几个典型特征,最优子结构、状态转移方程、边界、重叠子问题。在青蛙跳阶问题中:



    • f(n-1)和f(n-2) 称为 f(n) 的最优子结构

    • f(n)= f(n-1)+f(n-2)就称为状态转移方程

    • f(1) = 1, f(2) = 2 就是边界啦

    • 比如f(10)= f(9)+f(8),f(9) = f(8) + f(7) ,f(8)就是重叠子问题。


    我们来看下自底向上的解法,从f(1)往f(10)方向,想想是不是直接一个for循环就可以解决啦,如下:



    带备忘录的递归解法,空间复杂度是O(n),但是呢,仔细观察上图,可以发现,f(n)只依赖前面两个数,所以只需要两个变量a和b来存储,就可以满足需求了,因此空间复杂度是O(1)就可以啦



    动态规划实现代码如下:


    public class Solution {
    public int numWays(int n) {
    if (n<= 1) {
    return 1;
    }
    if (n == 2) {
    return 2;
    }
    int a = 1;
    int b = 2;
    int temp = 0;
    for (int i = 3; i <= n; i++) {
    temp = (a + b)% 1000000007;
    a = b;
    b = temp;
    }
    return temp;
    }
    }

    动态规划的解题套路


    什么样的问题可以考虑使用动态规划解决呢?



    如果一个问题,可以把所有可能的答案穷举出来,并且穷举出来后,发现存在重叠子问题,就可以考虑使用动态规划。



    比如一些求最值的场景,如最长递增子序列、最小编辑距离、背包问题、凑零钱问题等等,都是动态规划的经典应用场景。


    动态规划的解题思路


    动态规划的核心思想就是拆分子问题,记住过往,减少重复计算。 并且动态规划一般都是自底向上的,因此到这里,基于青蛙跳阶问题,我总结了一下我做动态规划的思路:



    • 穷举分析

    • 确定边界

    • 找出规律,确定最优子结构

    • 写出状态转移方程


    1. 穷举分析



    • 当台阶数是1的时候,有一种跳法,f(1) =1

    • 当只有2级台阶时,有两种跳法,第一种是直接跳两级,第二种是先跳一级,然后再跳一级。即f(2) = 2;

    • 当台阶是3级时,想跳到第3级台阶,要么是先跳到第2级,然后再跳1级台阶上去,要么是先跳到第 1级,然后一次迈 2 级台阶上去。所以f(3) = f(2) + f(1) =3

    • 当台阶是4级时,想跳到第3级台阶,要么是先跳到第3级,然后再跳1级台阶上去,要么是先跳到第 2级,然后一次迈 2 级台阶上去。所以f(4) = f(3) + f(2) =5

    • 当台阶是5级时......


    自底向上的动态规划


    2. 确定边界


    通过穷举分析,我们发现,当台阶数是1的时候或者2的时候,可以明确知道青蛙跳法。f(1) =1,f(2) = 2,当台阶n>=3时,已经呈现出规律f(3) = f(2) + f(1) =3,因此f(1) =1,f(2) = 2就是青蛙跳阶的边界。


    3. 找规律,确定最优子结构


    n>=3时,已经呈现出规律 f(n) = f(n-1) + f(n-2) ,因此,f(n-1)和f(n-2) 称为 f(n) 的最优子结构。什么是最优子结构?有这么一个解释:



    一道动态规划问题,其实就是一个递推问题。假设当前决策结果是f(n),则最优子结构就是要让 f(n-k) 最优,最优子结构性质就是能让转移到n的状态是最优的,并且与后面的决策没有关系,即让后面的决策安心地使用前面的局部最优解的一种性质



    4, 写出状态转移方程


    通过前面3步,穷举分析,确定边界,最优子结构,我们就可以得出状态转移方程啦:



    5. 代码实现


    我们实现代码的时候,一般注意从底往上遍历哈,然后关注下边界情况,空间复杂度,也就差不多啦。动态规划有个框架的,大家实现的时候,可以考虑适当参考一下:


    dp[0][0][...] = 边界值
    for(状态1 :所有状态1的值){
    for(状态2 :所有状态2的值){
    for(...){
    //状态转移方程
    dp[状态1][状态2][...] = 求最值
    }
    }
    }

    leetcode案例分析


    我们一起来分析一道经典leetcode题目吧



    给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。



    示例 1:


    输入:nums = [10,9,2,5,3,7,101,18]
    输出:4
    解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

    示例 2:


    输入:nums = [0,1,0,3,2,3]
    输出:4

    我们按照以上动态规划的解题思路,



    • 穷举分析

    • 确定边界

    • 找规律,确定最优子结构

    • 状态转移方程


    1.穷举分析


    因为动态规划,核心思想包括拆分子问题,记住过往,减少重复计算。 所以我们在思考原问题:数组num[i]的最长递增子序列长度时,可以思考下相关子问题,比如原问题是否跟子问题num[i-1]的最长递增子序列长度有关呢?


    自顶向上的穷举

    这里观察规律,显然是有关系的,我们还是遵循动态规划自底向上的原则,基于示例1的数据,从数组只有一个元素开始分析。



    • 当nums只有一个元素10时,最长递增子序列是[10],长度是1.

    • 当nums需要加入一个元素9时,最长递增子序列是[10]或者[9],长度是1。

    • 当nums再加入一个元素2时,最长递增子序列是[10]或者[9]或者[2],长度是1。

    • 当nums再加入一个元素5时,最长递增子序列是[2,5],长度是2。

    • 当nums再加入一个元素3时,最长递增子序列是[2,5]或者[2,3],长度是2。

    • 当nums再加入一个元素7时,,最长递增子序列是[2,5,7]或者[2,3,7],长度是3。

    • 当nums再加入一个元素101时,最长递增子序列是[2,5,7,101]或者[2,3,7,101],长度是4。

    • 当nums再加入一个元素18时,最长递增子序列是[2,5,7,101]或者[2,3,7,101]或者[2,5,7,18]或者[2,3,7,18],长度是4。

    • 当nums再加入一个元素7时,最长递增子序列是[2,5,7,101]或者[2,3,7,101]或者[2,5,7,18]或者[2,3,7,18],长度是4.


    分析找规律,拆分子问题

    通过上面分析,我们可以发现一个规律


    如果新加入一个元素nums[i], 最长递增子序列要么是以nums[i]结尾的递增子序列,要么就是nums[i-1]的最长递增子序列。看到这个,是不是很开心,nums[i]的最长递增子序列已经跟子问题 nums[i-1]的最长递增子序列有关联了。


    原问题数组nums[i]的最长递增子序列 = 子问题数组nums[i-1]的最长递增子序列/nums[i]结尾的最长递增子序列

    是不是感觉成功了一半呢?但是如何把nums[i]结尾的递增子序列也转化为对应的子问题呢?要是nums[i]结尾的递增子序列也跟nums[i-1]的最长递增子序列有关就好了。又或者nums[i]结尾的最长递增子序列,跟前面子问题num[j](0=<j<i)结尾的最长递增子序列有关就好了,带着这个想法,我们又回头看看穷举的过程:



    nums[i]的最长递增子序列,不就是从以数组num[i]每个元素结尾的最长子序列集合,取元素最多(也就是长度最长)那个嘛,所以原问题,我们转化成求出以数组nums每个元素结尾的最长子序列集合,再取最大值嘛。哈哈,想到这,我们就可以用dp[i]表示以num[i]这个数结尾的最长递增子序列的长度啦,然后再来看看其中的规律:



    其实,nums[i]结尾的自增子序列,只要找到比nums[i]小的子序列,加上nums[i] 就可以啦。显然,可能形成多种新的子序列,我们选最长那个,就是dp[i]的值啦




    • nums[3]=5,以5结尾的最长子序列就是[2,5],因为从数组下标0到3遍历,只找到了子序列[2]5小,所以就是[2]+[5]啦,即dp[4]=2

    • nums[4]=3,以3结尾的最长子序列就是[2,3],因为从数组下标0到4遍历,只找到了子序列[2]3小,所以就是[2]+[3]啦,即dp[4]=2

    • nums[5]=7,以7结尾的最长子序列就是[2,5,7][2,3,7],因为从数组下标0到5遍历,找到2,5和3都比7小,所以就有[2,7],[5,7],[3,7],[2,5,7]和[2,3,7]这些子序列,最长子序列就是[2,5,7]和[2,3,7],它俩不就是以5结尾和3结尾的最长递增子序列+[7]来的嘛!所以,dp[5]=3 =dp[3]+1=dp[4]+1



    很显然有这个规律:一个以nums[i]结尾的数组nums



    • 如果存在j属于区间[0,i-1],并且num[i]>num[j]的话,则有,dp(i) =max(dp(j))+1,


    最简单的边界情况


    当nums数组只有一个元素时,最长递增子序列的长度dp(1)=1,当nums数组有两个元素时,dp(2) =2或者1,
    因此边界就是dp(1)=1。


    确定最优子结构


    从穷举分析,我们可以得出,以下的最优结构:


    dp(i) =max(dp(j))+1,存在j属于区间[0,i-1],并且num[i]>num[j]。

    max(dp(j)) 就是最优子结构。


    状态转移方程


    通过前面分析,我们就可以得出状态转移方程啦:



    所以数组num[i]的最长递增子序列就是:


    最长递增子序列 =max(dp[i])

    代码实现


    class Solution {
    public int lengthOfLIS(int[] nums) {
    if (nums.length == 0) {
    return 0;
    }
    int[] dp = new int[nums.length];
    //初始化就是边界情况
    dp[0] = 1;
    int maxans = 1;
    //自底向上遍历
    for (int i = 1; i < nums.length; i++) {
    dp[i] = 1;
    //从下标0到i遍历
    for (int j = 0; j < i; j++) {
    //找到前面比nums[i]小的数nums[j],即有dp[i]= dp[j]+1
    if (nums[j] < nums[i]) {
    //因为会有多个小于nums[i]的数,也就是会存在多种组合了嘛,我们就取最大放到dp[i]
    dp[i] = Math.max(dp[i], dp[j] + 1);
    }
    }
    //求出dp[i]后,dp最大那个就是nums的最长递增子序列啦
    maxans = Math.max(maxans, dp[i]);
    }
    return maxans;
    }
    }

    参考与感谢



    • leetcode官网

    • 《labuladong算法小抄》

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

    如何进一步提高flutter内存表现

    前言 性能稳定性是App的生命,Flutter带了很多创新与机遇,然而团队在享受Flutter带来的收益同时也迎接了很多新事物带来的挑战。 本文就内存优化过程中一些实践经验跟大家做一个分享。 Flutter 上线之后 闲鱼使用一套混合栈管理的方案将Flutte...
    继续阅读 »

    前言


    性能稳定性是App的生命,Flutter带了很多创新与机遇,然而团队在享受Flutter带来的收益同时也迎接了很多新事物带来的挑战。


    本文就内存优化过程中一些实践经验跟大家做一个分享。


    Flutter 上线之后


    闲鱼使用一套混合栈管理的方案将Flutter嵌入到现有的App中。在产品体验上我们取得了优于Native的体验。主要得益于Flutter的在跨平台渲染方面的优势,部分原因则是因为我们用Dart语言重新实现的页面抛弃了很多历史的包袱轻装上阵。


    上线之后各方面技术指标,都达到甚至超出了部分预期。而我们最为担心的一些稳定性指标,比如crash也在稳定的范围之内。但是在一段时间后我们发现由于内存过高而被系统杀死的abort率数据有比较明显的异常。性能稳定性问题是非常关键的,于是我们火速开展了问题排查。


    问题定位与排查


    显然问题出在了过大的内存消耗上。内存消耗在App中构成比较复杂,如何在复杂的业务中去定位到罪魁祸首呢?稍加观察,我们确定Flutter问题相对比价明显。工欲善其事必先利其器,需要更好地定位内存的问题,善用已经的工具是非常有帮助的。好在我们在Native层和Dart层都有足够多的性能分析工具进行使用。


    工具分析


    这里简单介绍我们如何使用的工具去观察手机数据以便于分析问题。需要注意的是,本文的重点不是工具的使用方法介绍,所以只是简单列举部分使用到的常见工具。


    Xcode Instruments


    Instruments是iOS内存排查的利器,可以比较便捷地观察实时内存使用情况,自然不必多说。


    Xcode MemGraph + VMMap


    XCode 8之后推出的MEMGraph是Xcode的内存调试利器,可以看到实时的可视化的内存。更为方便的是,你可以将MemGraph导出,配合命令行工具更好的得到结构化的信息。


    Dart Observatory


    这是Dart语言官方的调试工具,里面也包含了类似于Xcode的Instruments的工具。在Debug模式下Dart VM启动以后会在特定的端口接受调试请求。官方文档


    观察结果


    在整个过程中我进行了大量的观察,这里分享一部分典型的数据表现。


    通过Xcode Instruments排查的话,我们观察到CG Raster Data这个数据有些高。这个Raster Data呢其实是图片光栅化的时候的内存消耗。


    我们将App内存异常的场景的MemGraph导出来,对其执行VMMap指令得出的结果:


    vmmap --summary Runner[40957].memgraph

    vmmap Runner[40957].memgraph | grep 'IOKit'

    vmmap Summary


    vmmap address


    我们主要关注resident和dirty的内存。发现IOKit占用了大量的内存。


    结合Xcode Raster Data还有IOKit的大量内存消耗,我们开始怀疑问题是图内存泄漏导致的。经过进一步通过Dart Observatory观察Dart Image对象的内存情况。

    Dart image instance

    观察结果显示,在内存较高的场景下在Dart层的确同时存在了较多Image(如图中270)的对象。现在基本可以确定内存问题跟Dart层的图片有很大的关系。


    这个结果,我估计很多人都已经想到了,App有明显的内存问题很有可能就是跟多媒体资源有关系。通过工具得出的准确数据线索,我们得到一个大致的方向去深入研究。


    诡异的Dart图片数量爆炸


    图片对象泄漏?


    前面我们用工具观察到Dart层的Image对象数量过多直接导致了非常大的内存压力,我们起初怀疑存在图片的内存泄漏。但是我们在经过进一步确认以后发现图片其实并没有真正的泄漏。


    Dart语言采用垃圾回收机制(Garbage Collection 下面开始简称GC)来管理分配的内存,VM层面的垃圾回收应该大多数情况下是可信的。但是从实际观察来看,图片数量的爆炸造成的较大的内存峰值直观感觉上GC来得有些不及时。在Debug模式下我们使用Dart Observatory手动触发GC,最终这些图片对象在没有引用的情况下最终还是会被回收。


    至此,我们基本可以确认,图片对象不存在泄漏。那是什么导致了GC的反应迟钝呢,难道是Dart语言本身的问题吗?


    Garbage Collection 不及时?


    为此我需要了解一下Dart内存管理机制垃圾回收的实现,关于详细的内存问题我团队的 @匠修 同学已经发过一篇相关文章可以参考:内存文章


    我这里不详细讨论Dart垃圾回收实现细节,只聊一聊Flutter与Dart相关的一些内容。


    关于Flutter我需要首先明确几个概念:




    1. Framework(Dart)(跟iOS平台连接的库Flutter.framework要区别开)特指由Dart编写的Flutter相关代码。




    2. Dart VM执行Dart代码的Dart语言相关库,它是以C实现的Dart SDk形式提供的。对外主要暴露了C接口Dart Api。里面主要包含了Dart的编译器,运行时等等。




    3. FLutter Engine C++实现的Flutter驱动引擎。他主要负责跨平台的绘制实现,包含Skia渲染引擎的接入;Dart语言的集成;以及跟Native层的适配和Embeder相关的一些代码。简单理解,iOS平台上面Flutter.framework, Android平台上的Flutter.jar便是引擎代码构建后的产物。




    在Dart代码里面对于GC是没有感知的。


    对于Dart SDK也就是Dart语言我们可以做的很有限,因为Dart语言本身是一种标准,如果Dart真的有问题我们需要和Dart维护团队协作推进问题的解决。Dart语言设计的时候初衷也是希望GC对于使用者是透明的,我们不应该依赖GC实现的具体算法和策略。不过我们还是需要通过Dart SDK的源码去理解GC的大致情况。


    既然我们前面已经确认并非内存泄漏,所以我们在对GC延迟的问题的调查主要放在Flutter Engine以及Dart CG入口上。


    Flutter与Dart Garbage Collection


    既然感觉GC不及时,先撇开消耗,我们至少可以尝试多触发几次GC来减轻内存峰值压力。但是我在仔细查阅dart_api.h(/src/third_party/dart/runtime/include/dart_api.h )接口文件后,但是并没有找到显式提供触发GC的接口。


    但是找到了如下这个方法Dart_NotifyIdle


    /**
    * Notifies the VM that the embedder expects to be idle until |deadline|. The VM
    * may use this time to perform garbage collection or other tasks to avoid
    * delays during execution of Dart code in the future.
    *
    * |deadline| is measured in microseconds against the system's monotonic time.
    * This clock can be accessed via Dart_TimelineGetMicros().
    *
    * Requires there to be a current isolate.
    */

    DART_EXPORT void Dart_NotifyIdle(int64_t deadline);

    这个接口意思是我们可以在空闲的时候显式地通知Dart,你接下来可以利用这些时间(dealine之前)去做GC。注意,这里的GC不保证会马上执行,可以理解我们请求Dart去做GC,具体做不做还是取决于Dart本身的策略。


    另外,我还找到一个方法叫做Dart_NotifyLowMemory:


    /**
    * Notifies the VM that the system is running low on memory.
    *
    * Does not require a current isolate. Only valid after calling Dart_Initialize.
    */

    DART_EXPORT void Dart_NotifyLowMemory();

    不过这个Dart_NotifyLowMemory方法其实跟GC没有太大关系,它其实是在低内存的情况下把多余的isolate去终止掉。你可以简单理解,把一些不是必须的线程给清理掉。


    在研究Flutter Engine代码后你会发现,Flutter Engine其实就是通过Dart_NotifyIdle去跟Dart层进行GC方面的协作的。我们可以在Flutter Engine源码animator.cc看到以下代码:


      
    //Animator负责刷新和通知帧的绘制
    if (!frame_scheduled_) {
    // We don't have another frame pending, so we're waiting on user input
    // or I/O. Allow the Dart VM 100 ms.
    delegate_.OnAnimatorNotifyIdle(*this, dart_frame_deadline_ + 100000);
    }


    //delegate 最终会调用到这里
    bool RuntimeController::NotifyIdle(int64_t deadline) {
    if (!root_isolate_) {
    return false;
    }

    tonic::DartState::Scope scope(root_isolate_.get());
    //Dart api接口
    Dart_NotifyIdle(deadline);
    return true;
    }

    这里的逻辑比较直观:如果当前没有帧渲染的任务时候就通过NotifyIdle告诉Dart层可以进行GC操作了。注意,这里并不是说只有在这种情况下Dart才回去做GC,Flutter只是通过这种方式尽可能利用空闲去做GC,配合Dart以更合理的时间去做GC。


    看到这里,我们有足够的理由去尝试一下这个接口,于是我们在一些内存压力比较大的场景进行了手动请求GC的操作。线上的Abort虽然有明显好转,但是内存峰值并没有因此得到改善。我们需要进一步找到根本原因。


    图片数量爆炸的真相


    为了确定图片大量囤积释放不及时的问题,我们需要跟踪Flutter图片从初始化到销毁的整个流程。


    我们从Dart层开始去追寻Image对象的生命周期,我们可以看到Flutter里面所以的图片都是经过ImageProvider来获取的,ImageProvider在获取图片的时候会调用一个Resolve接口,而这个接口会首先查询ImageCache去读取图片,如果不存在缓存就new Image的实例出来。


    关键代码:


      ImageStream resolve(ImageConfiguration configuration) {
    assert(configuration != null);
    final ImageStream stream = new ImageStream();
    T obtainedKey;
    obtainKey(configuration).then((T key) {
    obtainedKey = key;
    stream.setCompleter(PaintingBinding.instance.imageCache.putIfAbsent(key, () => load(key)));
    }).catchError(
    (dynamic exception, StackTrace stack) async {
    FlutterError.reportError(new FlutterErrorDetails(
    exception: exception,
    stack: stack,
    library: 'services library',
    context: 'while resolving an image',
    silent: true, // could be a network error or whatnot
    informationCollector: (StringBuffer information) {
    information.writeln('Image provider: $this');
    information.writeln('Image configuration: $configuration');
    if (obtainedKey != null)
    information.writeln('Image key: $obtainedKey');
    }
    ));
    return null;
    }
    );
    return stream;
    }

    大致的逻辑



    1. Resolve 请求获取图片.

    2. 查询是否存在于ImageCache.Yes->3 NO->4

    3. 返回已经存在的图片对象

    4. 生成新的Image对象并开始加载
      看起来没有特别复杂的逻辑,不过这里我要提一下Flutter ImageCache的实现。


    Flutter ImageCache


    Flutter ImageCache最初的版本其实非常简单,用Map实现的基于LRU算法缓存。这个算法和实现没有什么问题,但是要注意的是ImageCache缓存的是ImageStream对象,也就是缓存的是一个异步加载的图片的对象。而且缓存没有对占用内存总量做限制,而是采用默认最大限制1000个对象(Flutter在0.5.6 beta中加入了对内存大小限制的逻辑)。缓存异步加载对象的一个问题是,在图片加载解码完成之前,无法知道到底将要消耗多少内存,至少在Flutter这个Cache实现中没有处理这个问题。具体的实现感兴趣的朋友可以阅读ImageCache.dart源码。


    其实Flutter本身提供了定制化Cache的能力,所以优化ImageCache的第一步就是要根据机型的物理内存去做缓存大小的适配,设置ImageCache的合理限制。关于ImageCache的问题,可以参考官方文档和这个issue,我这里不展开去聊了。


    Flutter Image生命周期


    回到我们的Image对象跟踪,很明显,在缓存没有命中的情况下会有新的Image产生。继续深入代码会发现Image对象是由这段代码产生的:



    Future instantiateImageCodec(Uint8List list) {
    return _futurize(
    (_Callback callback) => _instantiateImageCodec(list, callback, null)
    );
    }

    String _instantiateImageCodec(Uint8List list, _Callback callback, _ImageInfo imageInfo)
    native 'instantiateImageCodec';

    这里有个native关键字,这是Dart调用C代码的能力,我们查看具体的源码可以发现这个最终初始化的是一个C++的codec对象。具体的代码在Flutter Engine codec.cc。它大致的过程就是先在IO线程中启动了一个解码任务,在IO完成之后再把最终的图片对象发回UI线程。关于Flutter线程的详细介绍,我在另外一篇文章中已经有介绍,这里附上链接给有兴趣的朋友。深入理解Flutter Engine线程模型。经过来这些代码和线程分析,我们得到大致的流程图:


    图片爆炸流程图


    也就是说,解码任务在IO线程进行,IO任务队列里面都是C++ lambda表达式,持有了实际的解码对象,也就持有了内存资源。当IO线程任务过多的时候,会有很多IO任务在等待执行,这些内存资源也被闭包所持有而等待释放。这就是为什么直观上会有内存释放不及时而造成内存峰值的问题。这也解释了为什么之前拿到的vmmap虚拟内存数据里面IOKit是大头。


    这样我们找到了关键的线索,在缓存不命中的情况下,大量初始化Image对象,导致IO线程任务繁重,而IO又持有大量的图片解码所用的内存资源。带这个推论,我在Flutter Engine的Task Runner加入了任务数量和C++ image对象的监控代码,证实了的确存在IO任务线程过载的情况,峰值在极端情况下瞬时达到了100+IO操作。


    IO Runner监控


    到这里问题似乎越来越明了了,但是为什么会有这么IO任务触发呢?上述逻辑虽然可能会有IO线程过载的情况下占用大量内存的情况。上层要求生成新的图片对象,这种请求是没有错误的,设计就是如此。就好比主线程阻塞大量的任务,必然会导致界面卡顿,但者却不是主线程本身的问题。我们需要从源头找到导致新对象创建暴涨真正导致IO线程过载的原因。


    大量请求的根源


    在前面的线索之下,我们继续寻找问题的根源。我们在实际App操作的过程当中发现,页面Push的越多,图片生成的速度越来越快。也就是说页面越多请求越快,看起来没有什么大问题。但是可见的图片其实总是在一定数量范围之内的,不应该随着页面增多而加快对象创建的频率。我们下意识的开始怀疑是否存在不可见的Image Widget也在不断请求图片的情况。最终导致了Cache无法命中而大量生成新的图片的场景。


    我开始调查每个页面的图片加载请求,我们知道Flutter里面万物皆Widget,页面都是是Widget,由Navigator管理。我在Widget的生命周期方法(详细见Flutter官方文档)中加入监控代码,如我所料,在Navigator栈底下不可见的页面也还在不停的Resolve Image,直接导致了image对象暴涨而导致IO线程过载,导致了内存峰值。


    看起来,我们终于找到了根本原因。解决方案并不难。在页面不可见的时候没必要发出多余的图片加载请求,峰值也就随之降下来了。再经过一番代码优化和测试以后问题得到了根本上的解决。优化上线以后,我们看到了数据发生了质的好转。
    有朋友可能想问,为什么不可见的Widget也会被调用到相关的生命周期方法。这里我推荐阅读Flutter官方文档关于Widget相关的介绍,篇幅有限我这里不展开介绍了。widgets


    至此,我们已经解决了一个较为严重的内存问题。内存优化情况复杂,可以点也比较多,接下来我继续简要分享在其它一些方面的优化方案。


    截图缓存优化


    文件缓存+预加载策略


    我们是采用嵌入式Flutter并使用一套混合栈模式管理Native和Flutter页面相互跳转的逻辑。由于FlutterView在App中是单例形式存在的,我们为了更好的用户体验,在页面切换的过程中使用的截图的方式来进行过渡。


    大家都知道,图片是非常占用内存的对象,我们如何在不降低用户体验的同时获得最小的内存消耗呢?假如我们每push一个页面都保存一张截图,那么内存是以线性复杂度增长的,这显然不够好。


    内存和空间在大多数情况下是一个互相转换的关系,优化很多时候其实是找一个合理的折中点。
    最终我采用了预加载+缓存的策略,在页面最多只在内存中同时存在两个截图,其它的存文件,在需要的时候提前进行预加载。
    简要流程图:


    简要流程图


    这样的话就做到了不影响用户体验的前提下,将空间复杂度从O(n)降低到了O(1)。
    这个优化进一步节省了不必要的内存开销。


    截图额外的优化



    • 针对当前设备的内存情况,自适应调整截图的分辨率,争取最小的内存消耗。

    • 在极端的内存情况下,把所有截图都从内存中移除存(存文件可恢复),采用PlaceHolder的形式。极端情况下避免被杀,保证可用性的体验降级策略。


    页面兜底策略


    对于电商类App存在一个普遍的问题,用户会不断的push页面到栈里面,我们不能阻止用户这种行为。我们当然可以把老页面干掉,每次回退的时候重新加载,但是这种用户体验跟Web页一样,是用户不可接受的。我们要维持页面的状态以保证用户体验。这必然会导致内存的线性增长,最终肯定难免要被杀。我们优化的目的是提高用户能够push的极限页面数量。


    对于Flutter页面优化,除了在优化每一个页面消耗的内存之外,我们做了降级兜底策略去保证App的可用性:在极端情况下将老页面进行销毁,在需要的时候重新创建。这的确降低了用户体验,在极端情况下,降级体验还是比Crash要好一些。



    FlutterViewController 单例析构


    另外我想讨论的一个话题是关于FlutterViewController的。目前Flutter的设计是按照单例模式去运行的,这对于完全用Flutterc重新开发的App没有太大的问题。但是对于混合型App,多出来的常驻内存确实是一个问题。


    实际上,Flutter Engine底层实现是考虑到了析构这个问题,有相关的接口。但是在Embeder这一层(具体FlutterViewController Message Channels这一层),在实现过程中存在一些循环引用,导致在Native层就算没有引用FlutterViewController的时候也无法释放.


    FlutterViewController引用图


    我在经过一段时间的尝试后,算是把循环引用解除了。这些循环引用主要集中在FlutterChannel这一块。在解除之后我顺利的释放了FlutterViewController,可以明显看到常驻内存得到了释放。但是我发现释放FlutterViewController的时候会导致一部分Skia Image对象泄漏,因为Skia Objects必须在它创建的线程进行释放(详情请参考skia_gpu_object.cc源码),线程同步的问题。关于这个问题我在GitHub上面有一个issue大家可以参考。FlutterViewController释放issue


    目前,这个优化我们已经反馈给Flutter团队,期待他们官方支持。希望大家可以一起探索研究。


    进一步探讨


    除此之外,Flutter内存方面其实还有比较多方面可以去研究。我这里列举几个目前观察到的问题。




    1. 我在内存分析的时候发现Flutter底层使用的boring ssl库有可以确定的内存泄漏。虽然这个泄漏比较缓慢,但是对于App长期运行还是有影响的。我在GitHub上面提了个issue跟进,目前已有相关的人员进行跟进。SSL leak issue




    2. 关于图片渲染,目前Flutter还是有优化空间的,特别是图片的按需剪裁。大多数情况下是没有不要将整一个bitmap解压到内存中的,我们可以针对显示的区域大小和屏幕的分辨率对图片进行合理的缩放以取得最好的性能消耗。




    3. 在分析Flutter内存的MemGraph的时候,我发现Skia引擎当中对于TextLayout消耗了大量的内存.目前我没有找到具体的原因,可能存在优化的空间。




    结语


    在这篇文章里,我简要的聊了一下目前团队在Flutter应用内存方面做出的尝试和探索。短短一篇文章无法包含所有内容,只能推出了几个典型的案例来作分析,希望可以跟大家一起探讨研究。欢迎感兴趣的朋友一起研究,如有更好的想法方案,我非常乐意看到你的分享。


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

    Flutter动画实现粒子漂浮效果

    要问2019年最火的移动端框架,肯定非Google的Flutter莫属。 本着学习的态度,基本的Dart语法(个人感觉语法风格接近Java+JS)过完之后,开始撸代码练手。 效果图 (这里为了方便录制gif,动画设置的较快;如果将动画的Duration设...
    继续阅读 »

    要问2019年最火的移动端框架,肯定非Google的Flutter莫属。

    image

    本着学习的态度,基本的Dart语法(个人感觉语法风格接近Java+JS)过完之后,开始撸代码练手。




    效果图


    image


    (这里为了方便录制gif,动画设置的较快;如果将动画的Duration设置成20s,看起来就是浮动的效果了)
    粒子碰撞的效果参考了张风捷特列 大佬的Flutter动画之粒子精讲


    1. Flutter的动画原理



    在任何系统的UI框架中,动画实现的原理都是相同的,即:在一段时间内,快速地多次改变UI外观;由于人眼会产生视觉暂留,所以最终看到的就是一个“连续”的动画,这和电影的原理是一样的。我们将UI的一次改变称为一个动画帧,对应一次屏幕刷新,而决定动画流畅度的一个重要指标就是帧率FPS(Frame Per Second),即每秒的动画帧数。



    简而言之,就是逐帧绘制,只要屏幕刷新的足够快,我们就会觉得这是个连续的动画。
    设想一个小球从屏幕顶端移动到底端的动画,为了完成这个动画,我们需要哪些数据呢?



    • 小球的运动轨迹,即起始点s、终点e和中间任意一点p

    • 动画持续时长t


    只有这两个参数够吗?明显是不够的,因为小球按照给定的轨迹运动,可能是匀速、先快后慢、先慢后快、甚至是一会儿快一会慢的交替地运动,只要在时间t内完成,都是可能的。所以我们应该再指定一个参数c来控制动画的速度。


    1.1 vsync探究


    废话不多说,我们看看Flutter中是动画部分的代码:


    AnimationController controller = AnimationController(
    vsync: this,
    duration: Duration(seconds: 2),
    )..addListener(() {
    //_renderBezier();
    print(controllerG.value);
    print('这是第${++count}次回调');
    });
    复制代码

    简要分析一下,AnimationController,顾名思义,控制器,用来控制动画的播放。传入的参数中,duration我们知道是前面提到过的动画持续时长t,那这个vsync是啥参数呢?打过游戏的同学可能对这个单词有印象,vsync 就是 垂直同步 。那什么是垂直同步呢?



    垂直同步又称场同步(Vertical Hold),从CRT显示器的显示原理来看,单个像素组成了水平扫描线,水平扫描线在垂直方向的堆积形成了完整的画面。显示器的刷新率受显卡DAC控制,显卡DAC完成一帧的扫描后就会产生一个垂直同步信号。我们平时所说的打开垂直同步指的是将该信号送入显卡3D图形处理部分,从而让显卡在生成3D图形时受垂直同步信号的制约。



    简而言之就是,显卡在完成渲染后,将画面数据送往显存中,而显示器从显存中一行一行从上到下取出画面数据,进行显示。但是屏幕的刷新率和显卡渲染数据的速度很多时候是不匹配的,试想一下,显示器刚扫描显示完屏幕上半部分的画面,正准备从显存取下面的画面数据时,显卡送来了下一帧的图像数据覆盖了原来的显存,这个时候显示器取出的下面部分的图像就和上面的不匹配,造成画面撕裂。


    为了避免这种情况,我们引入垂直同步信号,只有在显示器完整的扫描显示完一帧的画面后,显卡收到垂直同步信号才能刷新显存。
    可是这个物理信号跟我们flutter动画有啥关系呢?vsync对应的参数是this,我们继续分析一下this对应的下面的类。


    class _RunBallState extends State<RunBall> with TickerProviderStateMixin 
    复制代码

    with关键字是使用该类的方法而不继承该类,Mixin是类似于Java的接口,区别在于Mixin中的方法不是抽象方法而是已经实现了的方法。



    这个TickerProviderStateMixin到底是干啥的呢???经过哥们儿Google的帮助,在网上找到了



    关于动画的驱动,在此简单的说一下,Ticker是被SchedulerBinding所驱动。SchedulerBinding则是监听着Window.onBeginFrame回调。
    Window.onBeginFrame的作用是什么呢,是告诉应用该提供一个scene了,它是被硬件的VSync信号所驱动的。



    于是我们终于发现了,绕了一圈,归根到底还是真正的硬件产生的垂直同步信号在驱动着Flutter的动画的进行。


    ..addListener(() {
    //_renderBezier();
    print(controllerG.value);
    print('这是第${++count}次回调');
    });

    复制代码

    注意到之前的代码中存在一个动画控制器的监听器,动画在执行时间内,函数回调controller.value会生成一个从0到1的double类型的数值。我们在控制台打印出结果如下:

    image


    image


    经过观察,两次试验,在2s的动画执行时间内,该回调函数分别被执行了50次,53次,并不是一个固定值。也就是说硬件(模拟器)的屏幕刷新率大概维持在(25~26.5帧/s)。


    结论:硬件决定动画刷新率


    1.2 动画动起来


    搞懂了动画的原理之后,我们接下来就是逐帧的绘制了。关于Flutter的自定义View,跟android原生比较像。


    image


    继承CustomPainter类,重写paint和shouldRepaint方法,具体实现可以看代码.


    class Ball {
    double aX;
    double aY;
    double vX;
    double vY;
    double x;
    double y;
    double r;
    Color color;}

    复制代码

    小球Ball具有圆心坐标、半径、颜色、速度、加速度等属性,通过数学表达式计算速度和加速度的关系,就可以实现匀加速的效果。


    //运动学公式,看起来少了个时间t;实际上这些函数在动画过程中逐帧回调,把每帧刷新周期当成单位时间,相当于t=1
    _ball.x += _ball.vX;//位移=速度*时间
    _ball.y += _ball.vY;
    _ball.vX += _ball.aX;//速度=加速度*时间
    _ball.vY += _ball.aY;

    复制代码

    控制器使得函数不断回调,在回调函数函数里改变小球的相关参数,并且调用setState()函数,使得UI重新绘制;小球的轨迹坐标不断地变化,逐帧绘制的小球看起来就在运动了。你甚至可以在添加效果使得小球在撞到边界时变色或者半径变小(参考文章开头的粒子碰撞效果图)。


    2. 小球随机浮动的思考


    问题来了,我想要一个漂浮的效果呢?最好是随机的轨迹,就像气泡在空中飘乎不定,于是引起了我的思考;匀速然后方向随机?感觉不够优雅,于是去网上搜了一下,发现了思路!



    首先随机生成一条贝塞尔曲线作为轨迹,等小球运动到终点,再生成新的贝塞尔曲线轨迹



    生成二阶贝塞尔曲线的公式如下:


    //二次贝塞尔曲线轨迹坐标,根据参数t返回坐标;起始点p0,控制点p1,终点p2
    Offset quadBezierTrack(double t, Offset p0, Offset p1, Offset p2) {
    var bx = (1 - t) * (1 - t) * p0.dx + 2 * t * (1 - t) * p1.dx + t * t * p2.dx;
    var by = (1 - t) * (1 - t) * p0.dy + 2 * t * (1 - t) * p1.dy + t * t * p2.dy;

    return Offset(bx, by);
    }
    复制代码

    很巧的是,这里需要传入一个0~1之间double类型的参数t,恰好前面我们提过,animationController会在给定的时间内,生成一个0~1的value;这太巧了。


    起始点的坐标不用说,接下来就剩解决控制点p1和p2,当然是随机生成这两点,但是如果同时有多个小球呢?比如5个小球同时进行漂浮,每个小球都对应一组三个坐标的信息,给小球Ball添加三个坐标的属性?不,这个时候,我们可以巧妙地利用带种子参数的随机数。



    我们知道随机数在生成的时候,如果种子相同的话,每次生成的随机数也是相同的。



    每个小球对象在创建的时候自增地赋予一个整形的id,作为随机种子;比如5个小球,我们起始的id为:2,4,6,8,10;


        Offset p0 = ball.p0;//起点坐标
    Offset p1 = _randPosition(ball.id);
    Offset p2 = _randPosition(ball.id + 1);
    复制代码

    rand(2),rand(2+1)为第一个小球的p1和p2坐标;当所有小球到达终点时,此时原来的终点p2为新一轮贝塞尔曲线的起点;此时相应的id也应增加,为了防止重复,id应增加小球数量5 *2,即第二轮运动开始时,5个小球的id为:12,14,16,18,20。
    这样就保证了每轮贝塞尔曲线运动的时候,对于每个小球而言,p0,p1,p2是确定的;新一轮的运动所需要的随机的三个坐标点,只需要改变id的值就好了。


    Path quadBezierPath(Offset p0, Offset p1, Offset p2) {
    Path path = new Path();
    path.moveTo(p0.dx, p0.dy);
    path.quadraticBezierTo(p1.dx, p1.dy, p2.dx, p2.dy);
    return path;
    }
    复制代码

    这个时候,我们还可以利用Flutter自带的api画出二次贝塞尔曲线的轨迹,看看小球的运动是否落在轨迹上。


    image


    2.1 一些细节


    animation = CurvedAnimation(parent: controllerG, curve: Curves.bounceOut);
    复制代码

    这里的Curve就是前面提到的,控制动画过程的参数,flutter自带了挺多效果,我最喜欢这个bounceOut(弹出效果)

    image


     animation.addStatusListener((status) {
    switch (status) {
    case AnimationStatus.dismissed:
    // TODO: Handle this case.
    break;
    case AnimationStatus.forward:
    // TODO: Handle this case.
    break;
    case AnimationStatus.reverse:
    // TODO: Handle this case.
    break;
    case AnimationStatus.completed:
    // TODO: Handle this case.
    controllerG.reset();
    controllerG.forward();
    break;
    }
    });

    复制代码

    监听动画过程的状态,当一轮动画结束时,status状态为AnimationStatus.completed;此时,我们将控制器reset重置,再forward重新启动,此时就会开始新一轮的动画效果;如果我们选的是reverse,则动画会反向播放。




    GestureDetector(
    child: Container(
    width: double.infinity,
    height: 200,
    child: CustomPaint(
    painter: FloatBallView(_ballsF, _areaF),
    ),
    ),
    onTap: () {
    controllerG.forward();
    },
    onDoubleTap: () {
    controllerG.stop();
    },
    ),
    复制代码

    为了方便控制,我还加了个手势监听器,单击控制动画运行,双击暂停动画。


    3 完结


    水平有限,文中如有错误还请各位指出,我是梦龙Dragon


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

    浅探Google V8引擎

    探析它之前,我们先抛出以下几个疑问:为什么需要 V8 引擎呢?V8 引擎到底是个啥?它可以做些什么呢?了解它能有什么收获呢?接下来就针对以上几个问题进行详细描述。由来我们都知道,JS 是一种解释型语言,支持动态类型(明显不同于 Java 等这类静态语言就是在命...
    继续阅读 »

    探析它之前,我们先抛出以下几个疑问:

    • 为什么需要 V8 引擎呢?

    • V8 引擎到底是个啥?

    • 它可以做些什么呢?

    • 了解它能有什么收获呢?

    接下来就针对以上几个问题进行详细描述。

    由来

    我们都知道,JS 是一种解释型语言,支持动态类型(明显不同于 Java 等这类静态语言就是在命名一个变量时不需要声明变量的类型)、弱类型、基于原型的语言,内置支持类型。而一般 JS 都是在前端执行(直接影响界面),需要能够快速响应用户,那么就要求语言本身可以被快速地解析和执行,JS 引擎就为此而问世。

    这里提到了解释型语言和静态语言(编译型语言),先简单介绍一下二者:

    • 解释型语言(JS)

      • 每次运行时需要解释器按句依次解释源代码执行,将它们翻译成机器认识的机器代码再执行

    • 编译型语言(Java)

      • 运行时可经过编译器翻译成可执行文件,再由机器运行该可执行文件即可

    从上面的描述中可以看到 JS 运行时每次都要根据源文件进行解释然后执行,而编译型的只需要编译一次,下次可直接运行其可执行文件,但是这样就会导致跨平台的兼容性很差,因此各有优劣。

    而众多 JS 引擎(V8、JavaScriptCore、SpiderMonkey、Chakra等)中 V8 是最为出色的,加上它也是应用于当前最流行的谷歌浏览器,所以我们非常有必要去认识和了解一下,这样对于开发者也就更清楚 JS 在浏览器中到底是如何运行的了。

    认识

    定义

    • 使用 C++ 开发

    • 谷歌开源

    • 编译成原生机器码(支持IA-32, x86-64, ARM, or MIPS CPUs)

    • 使用了如内联缓存(inline caching)等方法来提高性能

    • 运行速度快,可媲美二进制程序

    • 支持众多操作系统,如 windows、linux、android 等

    • 支持其他硬件架构,如 IA32,X64,ARM 等

    • 具有很好的可移植和跨平台特性

    运行

    先来一张官方流程图:

    img

    准备

    JS 文件加载(不归 V8 管):可能来自于网络请求、本地的cache或者是也可以是来自service worker,这是 V8 运行的前提(有源文件才有要解释执行的)。 3种加载方式 & V8的优化

    • Cold load: 首次加载脚本文件时,没有任何数据缓存

    • Warm load:V8 分析到如果使用了相同的脚本文件,会将编译后的代码与脚本文件一起缓存到磁盘缓存中

    • Hot load: 当第三次加载相同的脚本文件时,V8 可以从磁盘缓存中载入脚本,并且还能拿到上次加载时编译后的代码,这样可以避免完全从头开始解析和编译脚本

    而在 V8 6.6 版本的时候进一步改进代码缓存策略,简单讲就是从缓存代码依赖编译过程的模式,改变成两个过程解耦,并增加了可缓存的代码量,从而提升了解析和编译的时间,大大提升了性能,具体细节见V8 6.6 进一步改进缓存性能

    分析

    此过程是将上面环节得到的 JS 代码转换为 AST(抽象语法树)。

    词法分析

    从左往右逐个字符地扫描源代码,通过分析,产生一个不同的标记,这里的标记称为 token,代表着源代码的最小单位,通俗讲就是将一段代码拆分成最小的不可再拆分的单元,这个过程称为词法标记,该过程的产物供下面的语法分析环节使用。

    这里罗列一下词法分析器常用的 token 标记种类:

    • 常数(整数、小数、字符、字符串等)

    • 操作符(算术操作符、比较操作符、逻辑操作符)

    • 分隔符(逗号、分号、括号等)

    • 保留字

    • 标识符(变量名、函数名、类名等)

    TOKEN-TYPE TOKEN-VALUE\
    -----------------------------------------------\
    T_IF                 if\
    T_WHILE              while\
    T_ASSIGN             =\
    T_GREATTHAN          >\
    T_GREATEQUAL         >=\
    T_IDENTIFIER name    / numTickets / ...\
    T_INTEGERCONSTANT    100 / 1 / 12 / ....\
    T_STRINGCONSTANT     "This is a string" / "hello" / ...

    上面提到会逐个从左至右扫描代码然后分析,那么很明显就会想到两种方案,扫描完再分析(非流式处理)和边扫描边分析(流式处理),简单画一下他们的时序图就能发现流式处理效率要高得多,同时分析完也会释放分析过程中占用的内存,也能大大提高内存使用效率,可见该优化的细节处理。

    语法分析

    语法分析是指根据某种给定的形式文法对由单词序列构成的输入文本(例如上个阶段的词法分析产物-tokens stream),进行分析并确定其语法结构的过程,最后产出其 AST(抽象语法树)。

    V8 会将语法分析的过程分为两个阶段来执行:

    • Pre-parser

      • 跳过还未使用的代码

      • 不会生成对应的 AST,会产生不带有变量的引用和声明的 scopes 信息

      • 解析速度会是 Full-parser 的 2 倍

      • 根据 JS 的语法规则仅抛出一些特定的错误信息

    • Full-parser

      • 解析那些使用的代码

      • 生成对应的 AST

      • 产生具体的 scopes 信息,带有变量引用和声明等信息

      • 抛出所有的 JS 语法错误

    为什么要做两次解析?

    如果仅有一次,那只能是 Full-parser,但这样的话,大量未使用的代码会消耗非常多的解析时间,结合实例来看下:通过 Coverage 录制的方式可以分析页面哪些代码没有用到,如下图可以看到最高有 75% 的没有被执行。

    img

    但是预解析并不是万能的,得失是并存的,很明显的一个场景:该文件中的代码全都执行了,那其实就是没必要的,当然这种情况其实还是占比远不如上面的例子,所以这里其实也是一种权衡,需要照顾大多数来达到综合性能的提升。

    下面给出一个示例:

    function add(x, y) {
       if (typeof x === "number") {
           return x + y;
      } else {
           return x + 'tadm';
      }
    }

    复制上面的代码到 web1web2 可以很直观的看到他们的 tokens 和 AST 结构(也可自行写一些代码体验)。

    img

    • tokens

    [
      {
           "type": "Keyword",
           "value": "function"
      },
      {
           "type": "Identifier",
           "value": "add"
      },
      {
           "type": "Punctuator",
           "value": "("
      },
      {
           "type": "Identifier",
           "value": "x"
      },
      {
           "type": "Punctuator",
           "value": ","
      },
      {
           "type": "Identifier",
           "value": "y"
      },
      {
           "type": "Punctuator",
           "value": ")"
      },
      {
           "type": "Punctuator",
           "value": "{"
      },
      {
           "type": "Keyword",
           "value": "if"
      },
      {
           "type": "Punctuator",
           "value": "("
      },
      {
           "type": "Keyword",
           "value": "typeof"
      },
      {
           "type": "Identifier",
           "value": "x"
      },
      {
           "type": "Punctuator",
           "value": "==="
      },
      {
           "type": "String",
           "value": "\"number\""
      },
      {
           "type": "Punctuator",
           "value": ")"
      },
      {
           "type": "Punctuator",
           "value": "{"
      },
      {
           "type": "Keyword",
           "value": "return"
      },
      {
           "type": "Identifier",
           "value": "x"
      },
      {
           "type": "Punctuator",
           "value": "+"
      },
      {
           "type": "Identifier",
           "value": "y"
      },
      {
           "type": "Punctuator",
           "value": ";"
      },
      {
           "type": "Punctuator",
           "value": "}"
      },
      {
           "type": "Keyword",
           "value": "else"
      },
      {
           "type": "Punctuator",
           "value": "{"
      },
      {
           "type": "Keyword",
           "value": "return"
      },
      {
           "type": "Identifier",
           "value": "x"
      },
      {
           "type": "Punctuator",
           "value": "+"
      },
      {
           "type": "String",
           "value": "'tadm'"
      },
      {
           "type": "Punctuator",
           "value": ";"
      },
      {
           "type": "Punctuator",
           "value": "}"
      },
      {
           "type": "Punctuator",
           "value": "}"
      }
    ]
    • AST

    {
     "type": "Program",
     "body": [
      {
         "type": "FunctionDeclaration",
         "id": {
           "type": "Identifier",
           "name": "add"
        },
         "params": [
          {
             "type": "Identifier",
             "name": "x"
          },
          {
             "type": "Identifier",
             "name": "y"
          }
        ],
         "body": {
           "type": "BlockStatement",
           "body": [
            {
               "type": "IfStatement",
               "test": {
                 "type": "BinaryExpression",
                 "operator": "===",
                 "left": {
                   "type": "UnaryExpression",
                   "operator": "typeof",
                   "argument": {
                     "type": "Identifier",
                     "name": "x"
                  },
                   "prefix": true
                },
                 "right": {
                   "type": "Literal",
                   "value": "number",
                   "raw": "\"number\""
                }
              },
               "consequent": {
                 "type": "BlockStatement",
                 "body": [
                  {
                     "type": "ReturnStatement",
                     "argument": {
                       "type": "BinaryExpression",
                       "operator": "+",
                       "left": {
                         "type": "Identifier",
                         "name": "x"
                      },
                       "right": {
                         "type": "Identifier",
                         "name": "y"
                      }
                    }
                  }
                ]
              },
               "alternate": {
                 "type": "BlockStatement",
                 "body": [
                  {
                     "type": "ReturnStatement",
                     "argument": {
                       "type": "BinaryExpression",
                       "operator": "+",
                       "left": {
                         "type": "Identifier",
                         "name": "x"
                      },
                       "right": {
                         "type": "Literal",
                         "value": "tadm",
                         "raw": "'tadm'"
                      }
                    }
                  }
                ]
              }
            }
          ]
        },
         "generator": false,
         "expression": false,
         "async": false
      }
    ],
     "sourceType": "script"
    }

    解释

    该阶段就是将上面产生的 AST 转换成字节码。

    这里增加字节码(中间产物)的好处是,并不是将 AST 直接翻译成机器码,因为对应的 cpu 系统会不一致,翻译成机器码时要结合每种 cpu 底层的指令集,这样实现起来代码复杂度会非常高;还有个就是内存占用的问题,因为机器码会存储在内存中,而退出进程后又会存储在磁盘上,加上转换后的机器码多出来很多信息,会比源文件大很多,导致了严重的内存占用问题。

    V8 在执行字节码的过程中,使用到了通用寄存器累加寄存器,函数参数和局部变量保存在通用寄存器里面,累加器中保存中间计算结果,在执行指令的过程中,如果直接由 cpu 从内存中读取数据的话,比较影响程序执行的性能,使用寄存器存储中间数据的设计,可以大大提升 cpu 执行的速度。

    编译

    这个过程主要是 V8 的 TurboFan编译器 将字节码翻译成机器码的过程。

    字节码配合解释器和编译器这一技术设计,可以称为JIT(即时编译技术),Java 虚拟机也是类似的技术,解释器在解释执行字节码时,会收集代码信息,标记一些热点代码(就是一段代码被重复执行多次),TurboFan 会将热点代码直接编译成机器码,缓存起来,下次调用直接运行对应的二进制的机器码,加快执行速度。

    在 TurboFan 将字节码编译成机器码的过程中,还进行了简化处理:常量合并、强制折减、代数重新组合。

    比如:3 + 4 --> 7,x + 1 + 2 --> x + 3 ......

    执行

    到这里我们就开始执行上一阶段产出的机器码。

    而在 JS 的执行过程中,经常遇到的就是对象属性的访问。作为一种动态的语言,一个简单的属性访问可能包含着复杂的语义,比如Object.xxx的形式,可能是属性的直接访问,也可能去调用的对象的Getter方法,还有可能是要通过原型链往上层对象中查找。这种不确定性而且动态判断的情况,会浪费很多查找时间,所以 V8 会把第一次分析的结果放在缓存中,当再次访问相同的属性时,会优先从缓存中去取,调用 GetProperty(Object, "xxx", feedback_cache) 的方法获取缓存,如果有缓存结果,就会跳过查找过程,又大大提升了运行性能。

    除了上面针对读取对象属性的结果缓存的优化,V8 还引入了 Object Shapes(隐藏类)的概念,这里面会记录一些对象的基本信息(比如对象拥有的所有属性、每个属性对于这个对象的偏移量等),这样我们去访问属性时就可以直接通过属性名和偏移量直接定位到他的内存地址,读取即可,大大提升访问效率。

    既然 V8 提出了隐藏类(两个形状相同的对象会去复用同一个隐藏类,何为形状相同的对象?两个对象满足有相同个数的相同属性名称和相同的属性顺序),那么我们开发者也可以很好的去利用它:

    • 尽量创建形状相同的对象

    • 创建完对象后尽量不要再去操作属性,即不增加或者删除属性,也就不会破环对象的形状

    完成

    到此 V8 已经完成了一份 JS 代码的读取、分析、解释、编译、执行。

    总结

    以上就是从 JS 代码下载到最终在 V8 引擎执行的过程分析,可以发现 V8 其实有很多实现的技术点,有着很巧妙的设计思想,比如流式处理、缓存中间产物、垃圾回收等,这里面又会涉及到很多细节,很值得继续深入研究。

    作者:Tadm
    来源:https://juejin.cn/post/7032278688192430117

    收起阅读 »

    手写清除console的loader

    前言删除console方式介绍通过编辑器查找所有console,或者eslint编译时的报错提示定位语句,然后清除,就是有点费手,不够优雅 因此下面需要介绍几种优雅的清除方式该插件可用于压缩我们的js代码,同时可以通过配置去掉console语句,安装后配置在...
    继续阅读 »




    前言

    作为一个前端,对于console.log的调试可谓是相当熟悉,话不多说就是好用!帮助我们解决了很多bug^_^
    但是!有因必有果(虽然不知道为什么说这句但是很顺口),如果把console发到生产环境也是很头疼的,尤其是如果打印的信息很私密的话,可能要凉凉TT

    删除console方式介绍

    对于在生产环境必须要清除的console语句,如果手动一个个删除,听上去就很辛苦,因此这篇文章本着看到了就要学,学到了就要用的精神我打算介绍一下手写loader的方式清除代码中的console语句,在此之前也介绍一下其他可以清除console语句的方式吧哈哈

    1. 方式一:暴力清除

    通过编辑器查找所有console,或者eslint编译时的报错提示定位语句,然后清除,就是有点费手,不够优雅
    因此下面需要介绍几种优雅的清除方式

    2. 方式二 :uglifyjs-webpack-plugin

    该插件可用于压缩我们的js代码,同时可以通过配置去掉console语句,安装后配置在webpack的optimization下,即可使用,需要注意的是:此配置只在production环境下生效

    安装
    npm i uglifyjs-webpack-plugin

    其中drop_console和pure_funcs的区别是:

    • drop_console的配置值为boolean,也就是说如果为true,那么代码中所有带console前缀的调试方式都会被清除,包括console.log,console.warn等

    • pure_funcs的配置值是一个数组,也就是可以配置清除那些带console前缀的语句,截图中配的是['console.log'],因此生产环境上只会清除console.log,如果代码中包含其他带console的前缀,如console.warn则保留

    但是需要注意的是,该方法只对ES5语法有效,如果你的代码中涉及ES6就会报错

    3. 方式三:terser-webpack-plugin

    webpack v5 开箱即带有最新版本的 terser-webpack-plugin。如果你使用的是 webpack v5 或更高版本,同时希望自定义配置,那么仍需要安装 terser-webpack-plugin。如果使用 webpack v4,则必须安装 terser-webpack-plugin v4 的版本。

    安装
    npm i terser-webpack-plugin@4

    terser-webpack-plugin对于清楚console的配置可谓是跟uglifyjs-webpack-plugin一点没差,但是他们最大的差别就是TerserWebpackPlugin支持ES6的语法

    4. 方式四:手写loader删除console

    终于进入了主题了,朋友们

    1. 什么是loader

    众所周知,webpack只能理解js,json等文件,那么除了js,json之外的文件就需要通过loader去顺利加载,因此loader在其中担任的就是翻译工作。loader可以看作一个node模块,实际上就是一个函数,但他不能是一个箭头函数,因为它需要继承webpack的this,可以在loader中使用webpack的方法。

    • 单一原则,一个loader只做一件事

    • 调用方式,loader是从右向左调用,遵循链式调用

    • 统一原则,输入输出都是字符串或者二进制数据

    根据第三点,下面的代码就会报错,因为输出的是数字而不是字符串或二进制数据

    module.exports = function(source) {
      return 111
    }

    1. 新建清除console语句的loader

    首先新建一个dropConsole.js文件

    // source:表示当前要处理的内容
    const reg = /(console.log\()(.*)(\))/g;
    module.exports = function(source) {
      // 通过正则表达式将当前处理内容中的console替换为空字符串
      source = source.replace(reg, "")
      // 再把处理好的内容return出去,坚守输入输出都是字符串的原则,并可达到链式调用的目的供下一个loader处理
      return source
    }
    1. 在webpack的配置文件中引入

    module: {
      rules:[
          {
              test: /\.js/,
              use: [
                  {
                  loader: path.resolve(__dirname, "./dropConsole.js"),
                  options: {
                    name: "前端"
                  }
                  }
              ]
          },
        {
      ]
    }

    在webpack的配置中,loader的导入需要绝对路径,否则导入失效,如果想要像第三方loader一样引入,就需要配置resolveLoader 中的modules属性,告诉webpack,当node_modules中找不到时,去别的目录下找

    module: {
      rules:[
          {
              test: /\.js/,
              use: [
                  {
                  loader: 'dropConsole',
                  options: {
                    name: "前端"
                  }
                  }
              ]
          },
        {
      ]
    }
    resolveLoader:{
      modules:["./node_modules","./build"] //此时我的loader写在build目录下
    },

    正常运行后,调试台将不会打印console信息

    1. 最后介绍几种在loader中常用的webpack api

    • this.query:返回webpack的参数即options的对象

    • this.callback:同步模式,可以把自定义处理好的数据传递给webpack

    const reg = /(console.log\()(.*)(\))/g;
    module.exports = function(source) {
      source = source.replace(reg, "");
      this.callback(null,source);
      // return的作用是让webpack知道loader返回的结果应该在this.callback当中,而不是return中
      return    
    }
    • this.async():异步模式,可以大致的认为是this.callback的异步版本,因为最终返回的也是this.callback

    const  path = require('path')
    const util = require('util')
    const babel = require('@babel/core')


    const transform = util.promisify(babel.transform)

    module.exports = function(source,map,meta) {
    var callback = this.async();

    transform(source).then(({code,map})=> {
        callback(null, code,map)
    }).catch(err=> {
        callback(err)
    })
    };

    最后的最后,webpack博大精深,值得我们好好学习,深入研究!

    作者:我也想一夜暴富
    来源:https://juejin.cn/post/7038413043084034062

    收起阅读 »

    uniapp热更新

    热更新主要是针对app上线之后页面出现bug,修改之后又得打包,上线,每次用户都得在应用市场去下载很影响用户体验,如果用户不愿意更新,一直提示都不愿意更新,这个bug就会一直存在。 可能你一不小心写错了代码,整个团队的努力都会付之东流,苦不苦,冤不冤,想想都苦...
    继续阅读 »



    为什么要热更新

    热更新主要是针对app上线之后页面出现bug,修改之后又得打包,上线,每次用户都得在应用市场去下载很影响用户体验,如果用户不愿意更新,一直提示都不愿意更新,这个bug就会一直存在。 可能你一不小心写错了代码,整个团队的努力都会付之东流,苦不苦,冤不冤,想想都苦,所以这个时候热更新就显得很重要了。

    首先你需要在manifest.json 中修改版本号

    如果之前是1.0.0那么修改之后比如是1.0.1或者1.1.0这样

    然后你需要在HBuilderX中打一个wgt包

    在顶部>发行>原生App-制作移动App资源升级包

    包的位置会在控制台里面输出

    你需要和后端约定一下接口,传递参数

    然后你就可以在app.vue的onLaunch里面编写热更新的代码了,如果你有其他需求,你可以在其他页面的onLoad里面编写。

    // #ifdef APP-PLUS  //APP上面才会执行
    plus.runtime.getProperty(plus.runtime.appid,
    function(widgetInfo) {
    uni.request({
    url: '请求url写你自己的',
    method: "POST",
    data: {
    version: widgetInfo.version,
    //app版本号
    name: widgetInfo.name //app名称
    },
    success: (result) = >{
    console.log(result) //请求成功的数据
    var data = result.data.data
    if (data.update && data.wgtUrl) {
    var uploadTask = uni.downloadFile({ //下载
    url: data.wgtUrl,
    //后端传的wgt文件
    success: (downloadResult) = >{ //下载成功执行
    if (downloadResult.statusCode === 200) {
    plus.runtime.install(downloadResult.tempFilePath, {
    force: flase
    },
    function() {
    plus.runtime.restart();
    },
    function(e) {});
    }
    },
    }) uploadTask.onProgressUpdate((res) = >{
    // 测试条件,取消上传任务。
    if (res.progress == 100) { //res.progress 上传进度
    uploadTask.abort();
    }
    });
    }
    }
    });
    });
    // #endif

    不支持的情况

    • SDK 部分有调整,比如新增了 Maps 模块等,不可通过此方式升级,必须通过整包的方式升级。

    • 原生插件的增改,同样不能使用此方式。
      对于老的非自定义组件编译模式,这种模式已经被淘汰下线。但以防万一也需要说明下,老的非自定义组件编译模式,如果之前工程没有 nvue 文件,但更新中新增了 nvue 文件,不能使用此方式。因为非自定义组件编译模式如果没有nvue文件是不会打包weex引擎进去的,原生引擎无法动态添加。自定义组件模式默认就含着weex引擎,不管工程下有没有nvue文件。

    注意事项

    • 条件编译,仅在 App 平台执行此升级逻辑。

    • appid 以及版本信息等,在 HBuilderX 真机运行开发期间,均为 HBuilder 这个应用的信息,因此需要打包自定义基座或正式包测试升级功能。

    • plus.runtime.version 或者 uni.getSystemInfo() 读取到的是 apk/ipa 包的版本号,而非 manifest.json 资源中的版本信息,所以这里用 plus.runtime.getProperty() 来获取相关信息。

    • 安装 wgt 资源包成功后,必须执行 plus.runtime.restart(),否则新的内容并不会生效。

    • 如果App的原生引擎不升级,只升级wgt包时需要注意测试wgt资源和原生基座的兼容性。平台默认会对不匹配的版本进行提醒,如果自测没问题,可以在manifest中配置忽略提示,详见ask.dcloud.net.cn/article/356…

    • http://www.example.com 是一个仅用做示例说明的地址,实际应用中应该是真实的 IP 或有效域名,请勿直接复制粘贴使用。

    关于热更新是否影响应用上架

    应用市场为了防止开发者不经市场审核许可,给用户提供违法内容,对热更新大多持排斥态度。

    但实际上热更新使用非常普遍,不管是原生开发中还是跨平台开发。

    Apple曾经禁止过jspatch,但没有打击其他的热更新方案,包括cordovar、react native、DCloud。封杀jspatch其实是因为jspatch有严重安全漏洞,可以被黑客利用,造成三方黑客可篡改其他App的数据。

    使用热更新需要注意:

    • 上架审核期间不要弹出热更新提示

    • 热更新内容使用https下载,避免被三方网络劫持

    • 不要更新违法内容、不要通过热更新破坏应用市场的利益,比如iOS的虚拟支付要老老实实给Apple分钱

    如果你的应用没有犯这些错误,应用市场是不会管的。

    作者:是一个秃头
    来源:https://juejin.cn/post/7039273141901721608

    收起阅读 »

    GC回收机制与分代回收策略

    GC回收机制一、前言垃圾回收:Garbage Collection,简写 GC。JVM 中的垃圾回收器会自动回收无用的对象。但是 GC 自动回收的代价是:当这种自动化机制出错,我们就需要深入理解 GC 回收机制,甚至需要对这些 自动化 的技术实施必要的监控与...
    继续阅读 »



    GC回收机制

    一、前言

    垃圾回收Garbage Collection,简写 GCJVM 中的垃圾回收器会自动回收无用的对象。

    但是 GC 自动回收的代价是:当这种自动化机制出错,我们就需要深入理解 GC 回收机制,甚至需要对这些 自动化 的技术实施必要的监控与调节。

    在虚拟机中,程序计数器、虚拟机栈、本地方法栈 3 个区域随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出而执行着出栈和入栈操作。所以这几个区域不需要考虑回收的问题。

    而在 堆和方法区 中,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样。这部分的只有在程序运行期间才会知道需要创建哪些对象,这部分的内存的创建和回收是动态的,也是垃圾回收器重点关注的地方。

    二、什么是垃圾

    垃圾 就是 内存中已经没有用的对象。既然是 垃圾回收,就必须知道哪些对象是垃圾。

    Java 虚拟机中使用了一种叫做 可达性分析 的算法 来决定对象是否可以被回收

    GCRoot示意图

    上图中 A、B、C、D、E 与 GCRoot 直接或间接产生引用链,所以 GC 扫描到这些对象时,并不会执行回收操作;J、K、M虽然之间有引用链,但是并没有与 GCRoot 存在引用链,所以当 GC 扫描到他们时会将他们回收。

    注意的是,上图中所有的对象,包括 GCRoot,都是内存中的引用。

    作为 GCRoot 的几种对象
    1. Java虚拟机栈(局部变量表)中的引用的对象;

    2. 方法区中静态引用指向的对象;

    3. 仍处于存活状态中的线程对象;

    4. Native方法中 JNI 引用的对象;

    三、什么时候回收

    不同的虚拟机实现有着不同的 GC 实现机制,但一般情况下都会存在下面两种情况:

    1. Allocation Failure:在堆内存分配中,如果因为可用剩余空间不足导致对象内存分配失败,这时系统会触发一次 GC。

    2. System.gc():在应用层,Java开发工程师可以主动调用此API来请求一次 GC。

    四、验证GCRoot的几种情况

    在验证之前,先了解Java命令时的参数。

    -Xms:初始分配 JVM 运行时的内存大小,如果不指定则默认为物理内存的 1/64

    举个小例子

    // 表示从物理内存中分配出 200M 空间给 JVM 内存
    java -Xms200m HelloWorld
    1.验证虚拟机栈(栈帧中的局部变量)中引用的对象作为 GCRoot
    // 验证代码
    public class GCRootLocalVariable {

      private int _10MB = 10 * 1024 * 1024;
      private byte[] memory = new byte[8 * _10MB];

      public static void main(String[] args) {
          System.out.println("开始时:");
          printMemory();
          method();
          System.gc();
          System.out.println("第二次GC完成");
          printMemory();
      }

      public static void method() {
          GCRootLocalVariable gc = new GCRootLocalVariable();
          System.gc();
          System.out.println("第一次GC完成");
          printMemory();
      }

      // 打印出当前JVM剩余空间和总的空间大小
      public static void printMemory() {
          long freeMemory = Runtime.getRuntime().freeMemory();
          long totalMemory = Runtime.getRuntime().totalMemory();
          System.out.println("剩余空间:" + freeMemory / 1024 / 1024 + "M");
          System.out.println("总共空间:" + totalMemory / 1024 / 1024 + "M");
      }
    }
    // 打印日志:
    开始时:
    剩余空间:119M
    总共空间:123M
    第一次GC完成
    剩余空间:40M
    总共空间:123M
    第二次GC完成
    剩余空间:120M
    总共空间:123M

    从上述代码中可以看到:

    第一次打印内存信息,分别为 119M 和 123M;

    第二次打印内存信息,分别为 40M 和 123M;剩余空间小了 80M,是因为在 method() 方法中创建了局部变量 gc(位于栈帧中的局部变量),并且这个 gc 对象会被作为 GCRoot。虽然创建的对象未被使用并且调用了 System.gc(),但是因为该方法未结束,所以创建的对象不能被回收。

    第三次打印内存信息,分别为 120M 和 123M;method() 方法已经结束,创建的对象 gc 也随方法消失,不再有引用类型指向该 80M 对象。

    【值得注意的是】

    private int _10MB = 10 * 1024 * 1024;
    private byte[] memory = new byte[8 * _10MB];

    上面 2 行代码是必须的,如果去掉,那么 3 次打印结果将会一致,idea 也会出现Instantiation of utility class 警告信息,说这个类只存在静态方法,没必要创建这个对象。

    这也就是说为什么创建 GCRootLocalVariable() 会需要 80M 的大小,是因为 GCRootLocalVariable 在创建时就会为其内部变量 memory 确定 80M 的大小。

    2.验证方法区中的静态变量引用的对象作为 GCRoot
    public class GCRootStaticVariable {
      private static int _10M = 10 * 1024 * 1024;
      private byte[] memory;
      private static GCRootStaticVariable staticVariable;

      public GCRootStaticVariable(int size) {
          memory = new byte[size];
      }

      public static void main(String[] args) {
          System.out.println("程序开始:");
          printMemory();
          GCRootStaticVariable g = new GCRootStaticVariable(2 * _10M);
          g.staticVariable = new GCRootStaticVariable(4 * _10M);
          // 将g设置为null,调用GC时可以回收此对象内存
          g = null;
          System.gc();
          System.out.println("GC完成");
          printMemory();
      }

      // 打印JVM剩余空间和总空间
      private static void printMemory() {
          long freeMemory = Runtime.getRuntime().freeMemory();
          long totalMemory = Runtime.getRuntime().totalMemory();
          System.out.println("剩余空间" + freeMemory/1024/1024 + "M");
          System.out.println("总共空间" + totalMemory/1024/1024 + "M");
      }
    }

    打印结果:
    程序开始:
    剩余空间119M
    总共空间123M
    GC完成
    剩余空间81M
    总共空间123M

    通过上述打印结果可知:

    1. 程序刚开始时打印结果为 119M;

    2. 当创建 g 对象时分配 20M 内存,又为静态变量 staticVariable 分配 40M 内存;

    3. 当调用 gc 回收时,非静态变量 memory 分配的 20M 内存被回收;

    4. 但是作为 GCRoot 的静态变量 staticVariable 不会被回收,所以最终打印结果少了 40M 内存。

    3.验证活跃线程作为GCRoot
    public class GCRootThread {

      private int _10M = 10 * 1024 * 1024;
      private byte[] memory = new byte[8 * _10M];

      public static void main(String[] args) throws InterruptedException {
          System.out.println("程序开始:");
          printMemory();
          AsyncTask asyncTask = new AsyncTask(new GCRootThread());
          Thread thread = new Thread(asyncTask);
          thread.start();
          System.gc();
          System.out.println("main方法执行完成,执行gc");
          printMemory();
          thread.join();
          asyncTask = null;
          System.gc();
          System.out.println("线程代码执行完成,执行gc");
          printMemory();
      }

      private static void printMemory() {
          long freeMemory = Runtime.getRuntime().freeMemory();
          long totalMemory = Runtime.getRuntime().totalMemory();
          System.out.println("剩余内存:" + freeMemory / 1024 / 1024 + "M");
          System.out.println("总共内存:" + totalMemory / 1024 / 1024 + "M");
      }

      private static class AsyncTask implements Runnable {

          private GCRootThread gcRootThread;

          public AsyncTask(GCRootThread gcRootThread) {
              this.gcRootThread = gcRootThread;
          }

          @Override
          public void run() {
              try {
                  Thread.sleep(500);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
      }
    }
    打印结果:
    程序开始:
    剩余内存:119M
    总共内存:123M
    main方法执行完成,执行gc
    剩余内存:41M
    总共内存:123M
    线程代码执行完成,执行gc
    剩余内存:120M
    总共内存:123M

    通过上述打印结果可知:

    1. 程序刚开始时可用内存为 119M;

    2. 第一次调用 gc 时,线程并没有执行结束,并且它作为 GCRoot ,所以它所引用的 80M 内存不会被 GC 回收掉;

    3. thread.join() 保证线程结束后再调用后续代码,所以当第二次调用 GC 时,线程已经执行完毕并被置为 null;

    4. 这时线程已经销毁,所以该线程所引用的 80M 内存被 GC 回收掉。

    4.测试成员变量是否可作为GCRoot
    public class GCRootClassVariable {
      private static int _10M = 10 * 1024 * 1024;
      private byte[] memory;
      private GCRootClassVariable gcRootClassVariable;

      public GCRootClassVariable(int size) {
          memory = new byte[size];
      }

      public static void main(String[] args) {
          System.out.println("程序开始:");
          printMemory();
          GCRootClassVariable g = new GCRootClassVariable(2 * _10M);
          g.gcRootClassVariable = new GCRootClassVariable(4 * _10M);
          g = null;
          System.gc();
          System.out.println("GC完成");
          printMemory();
      }

      private static void printMemory() {
          long freeMemory = Runtime.getRuntime().freeMemory();
          long totalMemory = Runtime.getRuntime().totalMemory();
          System.out.println("剩余内存:" + freeMemory / 1024 / 1024 + "M");
          System.out.println("总共内存:" + totalMemory / 1024 / 1024 + "M");
      }
    }
    打印结果:
    程序开始:
    剩余内存:119M
    总共内存:123M
    GC完成
    剩余内存:121M
    总共内存:123M

    上述打印结果可知:

    1. 第一次打印结果与第二次打印结果一致:全局变量 gcRootClassVariable 随着 g=null 后被销毁。

    2. 所以全局变量并不能作为 GCRoot。

    五、如何回收垃圾(常见的几种垃圾回收算法)

    1.标记清除算法(Mark and Sweep GC)

    从 “GCRoots” 集合开始,将内存整个遍历一次,保留所有可以被 GCRoots 直接或间接引用到的对象,而剩下的对象都当做垃圾对待并回收。

    上述整个过程分为两步:

    1. Mark标记阶段:找到内存中的所有 GC Root 对象,只要是和 GC Root 对象直接或间接相连则标记为灰色(存活对象),否则标记为黑色(垃圾对象)。

    2. Sweep清楚阶段:当遍历完所有的 GC Root 之后,则将标记为垃圾的对象直接清楚。

    标记清除算法示意图

    标记清除算法优缺点

    【优点】

    实现简单,不需要将对象进行移动。

    【缺点】

    需要中断进程内其他组件的执行,并且可能产生内存碎片,提高了垃圾回收的频率。

    2.复制算法(Copying)
    1. 将现有的内存空间分为两块,每次只使用其中一块;

    2. 在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中;

    3. 之后清除正在使用的内存块中的所有对象;

    4. 交换两个内存的角色,完成垃圾回收(目前使用A,B是空闲,算法完成后A为空闲,设置B为使用状态)。

    复制算法复制前示意图

    复制算法复制后示意图

    复制算法优缺点

    【优点】

    按顺序分配内存即可;实现简单、运行高效,不用考虑内存碎片问题。

    【缺点】

    可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。

    3.标记压缩算法(Mark-Compact)
    1. 需要先从根节点开始对所有可达对象做一次标记;

    2. 之后并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端;

    3. 最后清理边界外所有的空间。

    所有,标记压缩也分为两步完成:

    1. Mark标记阶段:找到内存中的所有 GC Root 对象,只要和 GC Root 对象直接或间接相连则标记为灰色(存活对象),否则标记为黑色(垃圾对象)

    2. Compact压缩阶段:将剩余存活对象按顺序压缩到内存的某一端

    标记压缩算法示意图

    标记压缩算法优缺点

    【优点】

    避免了碎片产生,又不需要两块相同的内存空间,性价比较高。

    【缺点】

    所谓压缩操作,仍需要进行局部对象移动,一定程度上还是降低了效率。

    分代回收策略

    Java 虚拟机根据对象存活的周期不同,把堆内存划分为 新生代老年代,这就是 JVM 的内存分代策略。

    注意:在 HotSpot 中除了 新生代老年代,还有 永久代

    分代回收的中心思想:对于新创建的对象会在新生代中分配内存,此区域的对象生命周期一般较短,如果经过多次回收仍然存活下来,则将它们转移到老年代中。

    一、年轻代

    新生成的对象优先存放在新生代中,存活率很低

    新生代中,常规应用进行一次垃圾收集一般可以回收 70% ~ 95% 的空间,回收效率很高。所以一般采用的 GC 回收算法是 复制算法

    新生代也可细分3部分:Eden、Survivor0(简称 S0)、Survivor1(简称 S1),这 3 部分按照 8:1:1 的比例来划分新生代。

    新生代老年代示意图

    新生成的对象会存放在 Eden 区。

    新生代老年代示意图

    当 Eden 区满时,会触发垃圾回收,回收掉垃圾之后,将剩下存活的对象存放到 S0 区。当下一次 Eden 区满时,再次触发垃圾回收,这时会将 Eden 区 和 S0 区存活的对象全部复制到 S1 区,并清空 Eden 区和 S0 区。

    新生代老年代示意图

    上述步骤重复 15 次之后,依然存活下来的对象存放到 老年区

    二、老年代

    一个对象如果在新生代存活了足够长的时间而没有被清理掉,则会被复制到老年代。

    老年代的内存大小一般比新生代大,能存放更多的对象。如果对象比较大(比如长字符串或大数组),并且新生代的剩余空间不足,则这个大对象会直接被分配到老年代上。

    可以使用 -XX:PretenureSizeThreshold 来控制直接升入老年代的对象大小。

    因为老年代对象的生命周期较长,不需要过多的复制操作,所以一般采用标记压缩的回收算法。

    【注意的是】

    有这么一种情况,老年代中的对象会引用新生代中的对象,这时如果要执行新生代的 GC,则可能要查询整个老年代引用新生代的情况,这种效率是极低的。所以老年代中维护了一个 512byte 的 table,所有老年代对象引用新生代对象的引用都记录在这里。这样新生代 GC 时只需要查询这个表即可。

    三、GC log分析

    为了让上层应用开发人员更加方便调试 Java 程序,JVM 提供了相应的 GC 日志,在 GC 执行垃圾回收事件中,会有各种相应的 log 被打印出来。

    新生代和老年代打印的日志是有区别的:

    【新生代GC:轻GC】这一区域的 GC 叫做 Minor GC。因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度比较快。

    【老年代GC:重GC】发生在这一区域的 GC 叫做 Major GC 或者 Full GC,当出现 Major GC,经常会伴随至少一次 Minor GC

    Major GCFull GC 在有些虚拟机中还是有区别的:前者是仅回收老年代中的垃圾对象,后者是回收整个堆中的垃圾对象。

    常用的 GC 命令参数
    命令参数功能描述
    -verbose:gc显示 GC 的操作内容
    -Xms20M初始化堆大小为 20M
    -Xmx20M设置堆最大分配内存 20M
    -Xmn10M设置新生代的内存大小为 10M
    -XX:+PrintGCDetails打印GC的详细log日志
    -XX:SurvivorRatio=8新生代中 Eden 区域与 Survivor 区域的大小比值为 8:1:1

    添加 VM Options 参数:分配堆内存 20M,10M给新生代,10M给老年代

    // VM args: -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
    public class MinorGCTest {

      private static final int _1M = 1024 * 1024;

      public static void main(String[] args) {
          byte[] a, b, c, d;
          a = new byte[2 * _1M];
          b = new byte[2 * _1M];
          c = new byte[2 * _1M];
          d = new byte[_1M];
      }
    }
    打印结果:(这里测试是第二次修改后的运行效果)
    [GC (Allocation Failure) [PSYoungGen: 7820K->840K(9216K)] 7820K->6992K(19456K), 0.0072302 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
    [Full GC (Ergonomics) [PSYoungGen: 840K->0K(9216K)] [ParOldGen: 6152K->6759K(10240K)] 6992K->6759K(19456K), [Metaspace: 3198K->3198K(1056768K)], 0.0087734 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
    Heap
    PSYoungGen     total 9216K, used 1190K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
    eden space 8192K, 14% used [0x00000000ff600000,0x00000000ff7298d8,0x00000000ffe00000)
    from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
    to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
    ParOldGen       total 10240K, used 6759K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
    object space 10240K, 66% used [0x00000000fec00000,0x00000000ff299cd0,0x00000000ff600000)
    Metaspace       used 3205K, capacity 4496K, committed 4864K, reserved 1056768K
    class space   used 351K, capacity 388K, committed 512K, reserved 1048576K

    上述字段意思代表如下:

    字段代表含义
    PSYoungGen新生代
    eden新生代中的 Eden 区
    from新生代中的 S0 区
    to新生代中的 S1 区
    ParOldGen老年代
    1. 第一次运行效果后,因为 Eden 区 8M,S0 和 S1 各 1M。所以 a、b、c、d 共有 7M 空间都会在 Eden 区。

    2. 修改 d = new byte[2 * _1M],再次运行;

    3. JVM 会将 a/b/c 存放到 Eden 区,Eden 占有 6M 空间,无法再分配 2M 空间给 d;

    4. 因此会执行一次轻 GC,并尝试将 a/b/c 复制到 S1 区;

    5. 但是因为 S1 区只有 1M 空间,所以没办法存储 a/b/c 三者任一对象。

    6. 这种情况下,JVM 将 a/b/c 转移到老年代,将 d 保存在 Eden 区。

    【最终结果】

    Eden区 占用 2M 空间(d),老年代占用 6M 空间(a,b,c)

    四、引用

    通过 GC Roots 的引用可达性来判断对象是否存活,JVM 中的引入关系有以下四种:

    引用英文名GC回收机制使用示例
    强引用Strong Reference如果一个对象具有强引用,那么垃圾回收期绝不会回收它Object obj = new Object();
    软引用Soft Reference在内存实在不足时,会对软引用进行回收SoftReference softObj = new SoftReference();
    弱引用Weak Reference第一次GC回收时,如果垃圾回收器遍历到此弱引用,则将其回收WeakReference weakObj = new WeakReference();
    虚引用Phantom Reference一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获取一个对象的实例不会使用
    软引用的用法
    public class SoftReferenceNormal {

      static class SoftObject {
          byte[] data = new byte[120 * 1024 * 1024]; // 120M
      }

      public static void main(String[] args) {
          SoftReference<SoftObject> softObj = new SoftReference<>(new SoftObject());
          System.out.println("第一次GC前,软引用:" + softObj.get());
          System.gc();
          System.out.println("第一次GC后,软引用:" + softObj.get());
          SoftObject obj = new SoftObject();
          System.out.println("分配100M强引用,软引用:" + softObj.get());
      }
    }

    添加 VM Option 参数:-Xmx200M 给堆内存分配最大200M内存

    第一次 GC 前,软引用:SoftReferenceNormal$SoftObject@1b6d3586
    第一次 GC 后,软引用:SoftReferenceNormal$SoftObject@1b6d3586
    分配 100M 强引用,软引用:null
    1. 添加参数后位堆内存分配最大 200M 空间,分配给 softObj 对象 120M。

    2. 第一次 GC 后,因为剩余内存任然够,所以软引用并没有被回收。

    3. 当分配 100M 强引用后,堆内存空间不够,会触发GC回收,回收掉软引用。

    软引用隐藏的问题

    【注意】

    被软引用对象关联的对象会自动被垃圾回收器回收,但是软引用对象本身也是一个对象,这些创建的软引用并不会自动被垃圾回收器回收掉。

    public class SoftReferenceTest {

      static class SoftObject {
          byte[] data = new byte[1024]; // 占用1k空间
      }

      private static final int _100K = 100 * 1024;
      // 静态集合保存软引用,会导致这些软引用对象本身无法被垃圾回收器回收
      private static Set<SoftReference<SoftObject>> cache = new HashSet<>(_100K);

      public static void main(String[] args) {
          for (int i = 0; i < _100K; i++) {
              SoftObject obj = new SoftObject();
              cache.add(new SoftReference(obj));
              if (i * 10000 == 0) {
                  System.out.println("cache size is " + cache.size());
              }
          }
          System.out.println("END");
      }
    }

    添加 VM Option 参数:-Xms4m -Xmx4m -Xmn2m

    // 打印结果:
    cache size is 1
    Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
    at SoftReferenceTest$SoftObject.<init>(SoftReferenceTest.java:8)
    at SoftReferenceTest.main(SoftReferenceTest.java:17)

    程序崩溃,崩溃的原因并不是堆内存溢出,而是超出了 GC 开销限制。

    这里错误的原因是:JVM 不停的回收软引用中的对象,回收次数过快,回收内存较小,占用资源过高了。

    【解决方案】注册一个引用队列,将这个对象从 Set 中移除掉。

    public class SoftReferenceTest {

      static class SoftObject {
          byte[] data = new byte[1024]; // 占用1k空间
      }

      private static final int _100K = 100 * 1024;
      // 静态集合保存软引用,会导致这些软引用对象本身无法被垃圾回收器回收
      private static Set<SoftReference<SoftObject>> cache = new HashSet<>(_100K);
      // 解决方案:注册一个引用队列,将要移除的对象从中删除
      private static ReferenceQueue<SoftObject> queue = new ReferenceQueue<>();
      // 记录清空次数
      private static int removeReferenceIndex = 0;

      public static void main(String[] args) {
          for (int i = 0; i < _100K; i++) {
              SoftObject obj = new SoftObject();
              cache.add(new SoftReference(obj, queue));
              // 清除掉软引用
              removeSoft();
              if (i * 10000 == 0) {
                  System.out.println("cache size is " + cache.size());
              }
          }
          System.out.println("END removeReferenceIndex: " + removeReferenceIndex);
      }

      private static void removeSoft() {
          Reference<? extends SoftObject> poll = queue.poll();
          while (poll != null) {
              if (cache.remove(poll)) {
                  removeReferenceIndex++;
              }
              poll = queue.poll();
          }
      }
    }
    // 打印结果:
    cache size is 1
    END removeReferenceIndex: 101745

    作者:沅兮
    来源:https://juejin.cn/post/7037330678731505672


    收起阅读 »

    swift 键盘收起

    iOS
    直接调用就能收起键盘,无需调用其他方法        UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), t...
    继续阅读 »







    直接调用就能收起键盘,无需调用其他方法    

        UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)

    收起阅读 »

    iOS 底层原理探索 之 结构体内存对齐

    iOS
    写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之 路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。 目录如下:iOS 底层原理探索之 alloc以上内容的总结专栏iOS 底层原理探索 之 阶段总结准备Objective-C...
    继续阅读 »


    写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
    路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。

    目录如下:

    1. iOS 底层原理探索之 alloc

    以上内容的总结专栏


    准备

    Objective-C ,通常写作ObjC或OC,是扩充C的面向对象编程语言。它主要适用于Mac OS X 和 GNUstep者两个使用OpenStep标准的系统,而在NeXTSTEP和OpenStep中它更是基本语言。

    GCC和Clang含Objective-C的编译器,Objective-C可以在GCC以及Clang运作的系统上编译。

    我们平时开发用的Objective-C语言,编译后最终会转化成C/C++语言。

    为什么要研究结构体的内存对齐呢? 因为作为一名iOS开发人员,随着对于底层的不断深入探究,我们都知道,所有的对象在底层中都是一个结构体。那么结构体的内存空间又会被系统分配多少空间,这个问题,值得我们一探究竟。

    首先,从大神Cooci那里盗取了一张各数据类型占用的空间大小图片,作为今天探究结构体内存对齐原理的依据。

    image.png

    当我们创建一个对象的时候,我们并不需要过多的在意属性的顺序,因为系统会帮我们做优化处理。但是,在创建结构体的时候,就需要我们去分析了,因为这个时候系统并不会帮助我们做优化。

    接下来,我们看下面两个结构体:

    struct Struct1 {    
    double a;
    char b;
    int c;
    short d;
    char e;
    }struct1;

    struct Struct2 {
    double a;
    int b;
    char c;
    short d;
    char e;
    }struct2;


    两个结构体拥有的数据类型是相同的,按照图片中double 是8字节, char 是1字节, int 是4字节,short 是2字节, 那么 两个结构体应该是占 16字节的内存空间,也就是分配16字节空间即可,然而,我们看下面的结果:

        printf("%lu--%lu", sizeof(struct1), sizeof(struct2));
    ------------
    24--16

    那么,这就是有问题的了,两个拥有相同数据类型的结构体,被系统分配到的内存空间是不一样的,这是为什么呢?今天的重点就是这里,结构体的

    内存对齐原则:

    1:数据成员对⻬规则:结构(struct)(或联合(union))的数据成员,第
    一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置
    要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是
    数组,结构体等)的整数倍开始(比如int为4字节,则要从4的整数倍地址
    开始存储。

    2:结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从
    其内部最大元素大小的整数倍地址开始存储。(struct a里存有struct b,
    b里有char,int ,double等元素,那b应该从8的整数倍开始存储)

    3:收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员
    的整数倍,不足的要补⻬。

    那么,我们按照以上内存对齐原则再来分析下 struct1 和 struct2 :


    struct Struct1 { /// 18 --> 24
    double a; //8 [0 1 2 3 4 5 6 7]
    char b; //1 [8 ]
    int c; //4 [9 [12 13 14 15]
    short d; //2 [16 17]
    char e; //1 [18]
    }struct1;


    struct Struct2 { /// 16 --> 16
    double a; //8 [0 1 2 3 4 5 6 7]
    int b; //4 [8 9 10 11]
    char c; //1 [ 12 ]
    short d; //2 [14 15]
    char e; // 1 [16]
    }struct2;


    接着,我们看下下面的结构体

    struct Struct3 {    
    double a;
    int b;
    char c;
    short d;
    int e;
    struct Struct1 str;
    }struct3;


    打印输出结果为 48 ,分析如下:

        double a;           //8 [0 1 2 3 4 5 6 7]
    int b; //4 [8 9 10 11]
    char c; //1 [12]
    short d; //2 [ 14 15 ]
    int e; //4 [ 16 17 18 19]
    struct Struct1 str; //24 [24 ... 47]

    所以,struct3 大小为48。


    猜想:内存对齐的收尾工作中的内部最大成员指的是什么的大小呢?

    接下来我们来一一验证一下

    struct LGStruct4 {          /// 40 --> 48 
    double a; //8 [0 1 2 3 4 5 6 7]
    int b; //4 [8 9 10 11]
    char c; //1 [12]
    short d; //2 [14 15]
    int e; //4 [16 17 18 19]
    struct Struct2 str; //16 [24 ... 39]
    }struct4;

    按照我对于内存对齐原则中收尾工作的理解, 最终的大小 应该是 Struct2 的 大小 16 的整数倍 也就是 48 才对。然而, 结果却是:

        NSLog(@"%lu", sizeof(struct4));
    --------
    SMObjcBuild[8076:213800] 40

    对,是40你没有看错,这样的话,很显然,我理解的就是错误的, 结构体内部最大成员应该指的是这里的 double,那么我们接下来验证一下: 1、

    struct Struct2 {    ///16
    double a; //8 [0 1 2 3 4 5 6 7]
    int b; //4 [8 9 10 11]
    char c; //1 [ 12 ]
    short d; //2 [14 15]
    }struct2;

    struct LGStruct4 { /// 24

    short d; //2 [0 1]

    struct Struct2 str; // 16 [8 ... 23]

    }struct4;

    结果是 :24


    因为,结构体内部最大成员是 double也就是8;并不是按照 LGStruct4中的str长度为16的整数倍来计算,所以最后的结果是24。

    总结

    结构体内部最大成员指的是结构体内部的数据类型,所以,结构体内包含结构体的时候,并不是按照内部的结构体长度的整数倍来计算的哦。


    收起阅读 »

    iOS 底层原理探索 之 alloc

    iOS
    iOS 底层原理探索 之 alloc写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之 路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。 内容的总结专栏iOS 底层原理探索 之 阶段总结序作为一名iOS开发人员,在平时开发工...
    继续阅读 »

    iOS 底层原理探索 之 alloc

    写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
    路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。


    内容的总结专栏


    作为一名iOS开发人员,在平时开发工作中,所有的对象我们使用最多的是alloc来创建。那么alloc底层做了哪些操作呢?接下来我会一步一步探究alloc方法的底层实现。

    初探

    我们先来看下面的代码

        SMPerson *p1 = [SMPerson alloc];
    SMPerson *p2 = [p1 init];
    SMPerson *p3 = [p1 init];

    NSLog(@"%@-%p-%p", p1, p1, &p1);
    NSLog(@"%@-%p-%p", p2, p2, &p2);
    NSLog(@"%@-%p-%p", p3, p3, &p3);

    打印内容:

        <SMPerson: 0x600000710400>-0x600000710400-0x7ffee6f15088
    <SMPerson: 0x600000710400>-0x600000710400-0x7ffee6f15080
    <SMPerson: 0x600000710400>-0x600000710400-0x7ffee6f15078

    可见,在 SMPerson 使用 alloc 方法从系统中申请开辟内存空间后 init方法并没有对内存空间做任何的处理,地址指针的创建来自于 alloc方法。如下所示:

    地址.001.jpeg

    注:细心的你一定注意到了,p1、p2、p3都是相差了8个字节。 这是因为,指针占内存空间大小为8字节,p1、p2、p3 都是从栈内存空间上申请的,且栈内存空间是连续的。同时,他们都指向了同一个内存地址。

    那么, alloc 是如何开辟内存空间的呢?

    首先,第一反应是,我们要Jump to Definition,

    2241622899100_.pic_hd.jpg

    结果,Xcode中并不能直接跳转后显示其底层实现,所以 并不是我们想要的。

    2251622899278_.pic_hd.jpg

    WX20210605-214250@2x.png

    中探

    接下来,我们通过三种方法来一探究竟:

    方法1

    既然不可以直接跳转到API文档来查看alloc的内部实现,那么我们还可以通过下 符号断点 来探寻 其实现原理。

    WX20210605-212725@2x.png

    接下来我们就来到此处

    WX20210605-213213@2x.png

    一个名为 libobjc.A.dylib 的库,至此,我们就应该要去找苹果开源的库,以寻找我们想要的答案。

    点击查看苹果开源源码汇总

    方法2

    我们也可以直接在alloc那一行打一个断点,代码运行到此处后,按住control键 点击 step into, 接下来,就来到里这里

    WX20210605-214413@2x.png 我们可以看到一个 objc_alloc 的函数方法到调用,此时,我们再下一个符号断点,同样的,我们还是找到了 libobjc.A.dylib 这个库。

    WX20210605-215027@2x.png

    方法3

    此外,我们还是可以通过汇编来调试和查找相应的实现内容,断点依然是在alloc那一行。

    Debug > Debug Workflow > Always Show Disassembly

    WX20210605-215336@2x.png

    找到 callq 方法调用那一行, WX20210605-215715@2x.png

    接着, step into 进去, 我们找到了 objc_alloc 的调用, 之后的操作和 方法2的后续步骤一样,最终,可以找到 libobjc.A.dylib 这个库。 WX20210605-215732@2x.png

    深探

    下载源码 objc4-818.2

    接下来对源码进行分析,

    alloc方法会调用到此处

    WX20210605-231454@2x.png

    接着是 调用 _objc_rootAlloc

    WX20210605-231517@2x.png

    之后调用 到 callAlloc

    WX20210605-231545@2x.png

    跟着断点会来到 _objc_rootAllocWithZone

    WX20210605-231647@2x.png

    之后是 _class_createInstanceFromZone

    此方法是重点

    WX20210605-231758@2x.png

    _class_createInstanceFromZone 方法中,该方法就是一个类初始化所走的流程,重点的地方有三处

    第一处是:
        // 计算出开辟内存空间大小
    size = cls->instanceSize(extraBytes);

    内部实现如下: WX20210605-231838@2x.png 其中在计算内存空间大小时,会调用 cache.fastInstanceSize(extraBytes) 方法,

    最终会调用 align16(size + extra - FAST_CACHE_ALLOC_DELTA16) 方法。 align16 的实现如下:

    static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
    }

    可见, 系统会进行 16字节 的对齐操作,也就是说,一个对象所占用的内存大小至少是16字节。

    在这里 我们举个例子: size_t x = 8; 那么 align16操作后的大小计算过程如下:

        (8 + 15) & ~15;

    0000 0000 0000 1000 8
    0000 0000 0000 1111 15

    = 0000 0000 0001 0111 23
    1111 1111 1111 0000 ~15

    = 0000 0000 0001 0000 16


    第二处是:
        ///向系统申请开辟内存空间,返回地址指针;
    obj = (id)calloc(1, size);

    第三处是:
        /// 将类和指针做绑定
    obj->initInstanceIsa(cls, hasCxxDtor);

    总结:

    所以,最后我们总结一下, alloc的底层调用流程如下:

    alloc流程.001.jpeg

    就是这样一个流程,系统就帮我们创建出来一个类对象。

    补充

    image.png

    • lldb 如何打印实力对象中成员为 double 类型的数值: e -f f -- <值>
    收起阅读 »

    String还有长度限制?是多少?

    前言 话说Java中String是有长度限制的,听到这里很多人不禁要问,String还有长度限制?是的有,而且在JVM编译中还有规范,而且有的家人们在面试的时候也遇到了。 String 首先要知道String的长度限制我们就需要知道String是怎么存储字符串...
    继续阅读 »

    前言


    话说Java中String是有长度限制的,听到这里很多人不禁要问,String还有长度限制?是的有,而且在JVM编译中还有规范,而且有的家人们在面试的时候也遇到了。


    String


    首先要知道String的长度限制我们就需要知道String是怎么存储字符串的,String其实是使用的一个char类型的数组来存储字符串中的字符的。



    那么String既然是数组存储那数组会有长度的限制吗?是的有限制,但是是在有先提条件下的,我们看看String中返回length的方法。



    由此我们看到返回值类型是int类型,Java中定义数组是可以给数组指定长度的,当然不指定的话默认会根据数组元素来指定:


    int[] arr1 = new int[10]; // 定义一个长度为10的数组
    int[] arr2 = {1,2,3,4,5}; // 那么此时数组的长度为5
    复制代码

    整数在java中是有限制的,我们通过源码来看看int类型对应的包装类Integer可以看到,其长度最大限制为2^31 -1,那么说明了数组的长度是0~2^31-1,那么计算一下就是(2^31-1 = 2147483647 = 4GB)



    看到这我们尝试通过编码来验证一下上述观点。



    以上是我通过定义字面量的形式构造的10万个字符的字符串,编译之后虚拟机提示报错,说我们的字符串长度过长,不是说好了可以存21亿个吗?为什么才10万个就报错了呢?


    其实这里涉及到了JVM编译规范的限制了,其实JVM在编译时,如果我们将字符串定义成了字面量的形式,编译时JVM是会将其存放在常量池中,这时候JVM对这个常量池存储String类型做出了限制,接下来我们先看下手册是如何说的。



    常量池中,每个 cp_info 项的格式必须相同,它们都以一个表示 cp_info 类型的单字节 “tag”项开头。后面 info[]项的内容 由tag 的类型所决定。



    我们可以看到 String类型的表示是 CONSTANT_String ,我们来看下CONSTANT_String具体是如何定义的。



    这里定义的 u2 string_index 表示的是常量池的有效索引,其类型是CONSTANT_Utf8_info 结构体表示的,这里我们需要注意的是其中定义的length我们看下面这张图。



    在class文件中u2表示的是无符号数占2个字节单位,我们知道1个字节占8位,2个字节就是16位 ,那么2个字节能表示的范围就是2^16- 1 = 65535 。范中class文件格式对u1、u2的定义的解释做了一下摘要:


    #这里对java虚拟机规摘要部分


    ##1、class文件中文件内容类型解释


    定义一组私有数据类型来表示 Class 文件的内容,它们包括 u1,u2 和 u4,分别代 表了 1、2 和 4 个字节的无符号数。


    每个 Class 文件都是由 8 字节为单位的字节流组成,所有的 16 位、32 位和 64 位长度的数 据将被构造成 2 个、4 个和 8 个 8 字节单位来表示。


    ##2、程序异常处理的有效范围解释


    start_pc 和 end_pc 两项的值表明了异常处理器在 code[]数组中的有效范围。


    start_pc 必须是对当前 code[]数组中某一指令的操作码的有效索引,end_pc 要 么是对当前 code[]数组中某一指令的操作码的有效索引,要么等于 code_length 的值,即当前 code[]数组的长度。start_pc 的值必须比 end_pc 小。


    当程序计数器在范围[start_pc, end_pc)内时,异常处理器就将生效。即设 x 为 异常句柄的有效范围内的值,x 满足:start_pc ≤ x < end_pc


    实际上,end_pc 值本身不属于异常处理器的有效范围这点属于 Java 虚拟机历史上 的一个设计缺陷:如果 Java 虚拟机中的一个方法的 code 属性的长度刚好是 65535 个字节,并且以一个 1 个字节长度的指令结束,那么这条指令将不能被异常处理器 所处理。


    不过编译器可以通过限制任何方法、实例初始化方法或类初始化方法的code[]数组最大长度为 65534,这样可以间接弥补这个 BUG。



    注意:这里对个人认为比较重要的点做了标记,首先第一个加粗说白了就是说数组有效范围就是【0-65565】但是第二个加粗的地方又解释了,因为虚拟机还需要1个字节的指令作为结束,所以其实真正的有效范围是【0-65564】,这里要注意这里的范围仅限编译时期,如果你是运行时拼接的字符串是可以超出这个范围的。



    接下来我们通过一个小实验来测试一下我们构建一个长度为65534的字符串,看看是否就能编译通过。0期阶段汇总


    首先通过一个for循环构建65534长度的字符串,在控制台打印后,我们通过自己度娘的一个在线字符统计工具计算了一下确实是65534个字符,如下:




    然后我们将字符复制后以定义字面量的形式赋值给字符串,可以看到我们选择这些字符右下角显示的确实是65534,于是乎运行了一波,果然成功了。




    #看到这里我们来总结一下:


    ##字符串有长度限制吗?是多少?


    首先字符串的内容是由一个字符数组 char[] 来存储的,由于数组的长度及索引是整数,且String类中返回字符串长度的方法length() 的返回值也是int ,所以通过查看java源码中的类Integer我们可以看到Integer的最大范围是2^31 -1,由于数组是从0开始的,所以数组的最大长度可以使【0~2^31】通过计算是大概4GB。


    但是通过翻阅java虚拟机手册对class文件格式的定义以及常量池中对String类型的结构体定义我们可以知道对于索引定义了u2,就是无符号占2个字节,2个字节可以表示的最大范围是2^16 -1 = 65535。


    其实是65535,但是由于JVM需要1个字节表示结束指令,所以这个范围就为65534了。超出这个范围在编译时期是会报错的,但是运行时拼接或者赋值的话范围是在整形的最大范围。


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

    ASM字节码插桩

    ASM字节码插桩 一、什么是插桩 QQ空间曾经发布的热修复解决方案中利用Javaassist库实现向类的构造函数中插入一段代码解决CLASS_ISPREVERIFIED 问题。包括了Instant Run的实现以及参照Instant Run实现的热修复美团Ro...
    继续阅读 »

    ASM字节码插桩


    一、什么是插桩


    QQ空间曾经发布的热修复解决方案中利用Javaassist库实现向类的构造函数中插入一段代码解决CLASS_ISPREVERIFIED 问题。包括了Instant Run的实现以及参照Instant Run实现的热修复美团Robus等都利用到了插桩技术。


    插桩就是将一段代码插入或者替换原本的代码。字节码插桩顾名思义就是在我们编写的源码编译成字节码(Class)后,在Android下生成dex之前修改Class文件,修改或者增强原有代码逻辑的操作。


    插桩前.png


    插桩后.png


    我们需要查看方法执行耗时,如果每一个方法都需要自己手动去加入这些内容,当不需要时也需要一个个删去相应的代码。1个、两个方法还好,如果有10个、20个得多麻烦!所以可以利用注解来标记需要插桩的方法,结合编译后操作字节码来帮助我们自动插入,当不需要时关掉插桩即可。这种AOP思想让我们只需要关注插桩代码本身。


    二、字节码操作框架


    上面我们提到QQ空间使用了Javaassist来进行字节码插桩,除了Javaassist之外还有一个应用更为广泛的ASM框架同样也是字节码操作框架,Instant Run包括AspectJ就是借助ASM来实现各自的功能。


    我们非常熟悉的JSON格式数据是基于文本的,我们只需要知道它的规则就能够轻松的生成、修改JSON数据。同样的Class字节码也有其自己的规则(格式)。操作JSON可以借助GSON来非常方便的生成、修改JSON数据。而字节码Class,同样可以借助Javassist/ASM来实现对其修改。



    字节码操作框架的作用在于生成或者修改Class文件,因此在Android中字节码框架本身是不需要打包进入APK的,只有其生成/修改之后的Class才需要打包进入APK中。它的工作时机在上图Android打包流程中的生成Class之后,打包dex之前。


    三、ASM的使用


    由于ASM具有相对于Javassist更好的性能以及更高的灵活行,我们这篇文章以使用ASM为主。在真正利用到Android中之前,我们可以先在Java程序中完成对字节码的修改测试。


    3.1、在AS中引入ASM


    ASM可以直接从jcenter()仓库中引入,所以我们可以进入:bintray.com/进行搜索



    点击图中标注的工件进入,可以看到最新的正式版本为:7.1。



    因此,我们可以在AS中加入:


    引入ASM.png


    同时,需要注意的是:我们使用testImplementation引入,这表示我们只能在Java的单元测试中使用这个框架,对我们Android中的依赖关系没有任何影响。



    AS中使用gradle的Android工程会自动创建Java单元测试与Android单元测试。测试代码分别在test与androidTest。



    3.2、准备待插桩Class


    test/java下面创建一个Java类:


    public class InjectTest {

    public static void main(String[] args) {

    }
    }
    </pre>

    由于我们操作的是字节码插桩,所以可以进入test/java下面使用javac对这个类进行编译生成对应的class文件。


    javac InjectTest.java

    3.3、执行插桩


    因为main方法中没有任何输出代码,我们输入命令:java InjectTest执行这个Class不会有任何输出。那么我们接下来利用ASM,向main方法中插入一开始图中的记录函数执行时间的日志输出。


    在单元测试中写入测试方法


    <pre spellcheck="false" lang="java" cid="n37" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"> /**
    * 1、准备待分析的class
    */
    FileInputStream fis = new FileInputStream
    ("xxxxx/test/java/InjectTest.class");

    /**
    * 2、执行分析与插桩
    */
    //class字节码的读取与分析引擎
    ClassReader cr = new ClassReader(fis);
    // 写出器 COMPUTE_FRAMES 自动计算所有的内容,后续操作更简单
    ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
    //分析,处理结果写入cw EXPAND_FRAMES:栈图以扩展格式进行访问
    cr.accept(new ClassAdapterVisitor(cw), ClassReader.EXPAND_FRAMES);


    /**
    * 3、获得结果并输出
    */
    byte[] newClassBytes = cw.toByteArray();
    File file = new File("xxx/test/java2/");
    file.mkdirs();

    FileOutputStream fos = new FileOutputStream
    ("xxx/test/java2/InjectTest.class");
    fos.write(newClassBytes);

    fos.close();</pre>

    关于ASM框架本身的设计,我们这里先不讨论。上面的代码会获取上一步生成的class,然后由ASM执行完插桩之后,将结果输出到test/java2目录下。其中关键点就在于第2步中,如何进行插桩。


    把class数据交给ClassReader,然后进行分析,类似于XML解析,分析结果会以事件驱动的形式告知给accept的第一个参数ClassAdapterVisitor


    <pre spellcheck="false" lang="java" cid="n41" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public class ClassAdapterVisitor extends ClassVisitor {

    public ClassAdapterVisitor(ClassVisitor cv) {
    super(Opcodes.ASM7, cv);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature,
    String[] exceptions) {
    System.out.println("方法:" + name + " 签名:" + desc);

    MethodVisitor mv = super.visitMethod(access, name, desc, signature,
    exceptions);
    return new MethodAdapterVisitor(api,mv, access, name, desc);
    }
    }</pre>

    分析结果通过ClassAdapterVisitor获得,一个类中会存在方法、注解、属性等,因此ClassReader会将调用ClassAdapterVisitor中对应的visitMethodvisitAnnotationvisitField这些visitXX方法。


    我们的目的是进行函数插桩,因此重写visitMethod方法,在这个方法中我们返回一个MethodVisitor方法分析器对象。一个方法的参数、注解以及方法体需要在MethodVisitor中进行分析与处理。


    <pre spellcheck="false" lang="java" cid="n45" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">package com.enjoy.asminject.example;

    import com.enjoy.asminject.ASMTest;

    import org.objectweb.asm.AnnotationVisitor;
    import org.objectweb.asm.MethodVisitor;
    import org.objectweb.asm.Type;
    import org.objectweb.asm.commons.AdviceAdapter;
    import org.objectweb.asm.commons.Method;

    /**
    * AdviceAdapter: 子类
    * 对methodVisitor进行了扩展, 能让我们更加轻松的进行方法分析
    */
    public class MethodAdapterVisitor extends AdviceAdapter {

    private boolean inject;

    protected MethodAdapterVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
    super(api, methodVisitor, access, name, descriptor);
    }


    /**
    * 分析方法上面的注解
    * 在这里干嘛???
    * <p>
    * 判断当前这个方法是不是使用了injecttime,如果使用了,我们就需要对这个方法插桩
    * 没使用,就不管了。
    *
    * @param desc
    * @param visible
    * @return
    */
    @Override
    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
    if (Type.getDescriptor(ASMTest.class).equals(desc)) {
    System.out.println(desc);
    inject = true;
    }
    return super.visitAnnotation(desc, visible);
    }

    private int start;

    @Override
    protected void onMethodEnter() {
    super.onMethodEnter();
    if (inject) {
    //执行完了怎么办? 记录到本地变量中
    invokeStatic(Type.getType("Ljava/lang/System;"),
    new Method("currentTimeMillis", "()J"));

    start = newLocal(Type.LONG_TYPE); //创建本地 LONG类型变量
    //记录 方法执行结果给创建的本地变量
    storeLocal(start);
    }
    }

    @Override
    protected void onMethodExit(int opcode) {
    super.onMethodExit(opcode);
    if (inject){
    invokeStatic(Type.getType("Ljava/lang/System;"),
    new Method("currentTimeMillis", "()J"));
    int end = newLocal(Type.LONG_TYPE);
    storeLocal(end);

    getStatic(Type.getType("Ljava/lang/System;"),"out",Type.getType("Ljava/io" +
    "/PrintStream;"));

    //分配内存 并dup压入栈顶让下面的INVOKESPECIAL 知道执行谁的构造方法创建StringBuilder
    newInstance(Type.getType("Ljava/lang/StringBuilder;"));
    dup();
    invokeConstructor(Type.getType("Ljava/lang/StringBuilder;"),new Method("<init>","()V"));


    visitLdcInsn("execute:");
    invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("append","(Ljava/lang/String;)Ljava/lang/StringBuilder;"));

    //减法
    loadLocal(end);
    loadLocal(start);
    math(SUB,Type.LONG_TYPE);


    invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("append","(J)Ljava/lang/StringBuilder;"));
    invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("toString","()Ljava/lang/String;"));
    invokeVirtual(Type.getType("Ljava/io/PrintStream;"),new Method("println","(Ljava/lang/String;)V"));

    }
    }
    }</pre>

    MethodAdapterVisitor继承自AdviceAdapter,其实就是MethodVisitor 的子类,AdviceAdapter封装了指令插入方法,更为直观与简单。


    上述代码中onMethodEnter进入一个方法时候回调,因此在这个方法中插入指令就是在整个方法最开始加入一些代码。我们需要在这个方法中插入long s = System.currentTimeMillis();。在onMethodExit中即方法最后插入输出代码。


    <pre spellcheck="false" lang="java" cid="n48" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">@Override
    protected void onMethodEnter() {
    super.onMethodEnter();
    if (inject) {
    //执行完了怎么办? 记录到本地变量中
    invokeStatic(Type.getType("Ljava/lang/System;"),
    new Method("currentTimeMillis", "()J"));

    start = newLocal(Type.LONG_TYPE); //创建本地 LONG类型变量
    //记录 方法执行结果给创建的本地变量
    storeLocal(start);
    }
    }</pre>

    这里面的代码怎么写?其实就是long s = System.currentTimeMillis();这句代码的相对的指令。我们可以先写一份代码


    <pre spellcheck="false" lang="java" cid="n50" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">void test(){
    //插入的代码
    long s = System.currentTimeMillis();
    /**
    * 方法实现代码....
    */
    //插入的代码
    long e = System.currentTimeMillis();
    System.out.println("execute:"+(e-s)+" ms.");
    }</pre>

    然后使用javac编译成Class再使用javap -c查看字节码指令。也可以借助插件来查看,就不需要我们手动执行各种命令。


    插件安装.png


    安装完成之后,可以在需要插桩的类源码中点击右键:


    查看字节码.png


    点击ASM Bytecode Viewer之后会弹出


    字节码.png


    所以第20行代码:long s = System.currentTimeMillis();会包含两个指令:INVOKESTATICLSTORE


    再回到onMethodEnter方法中


    <pre spellcheck="false" lang="java" cid="n59" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">@Override
    protected void onMethodEnter() {
    super.onMethodEnter();
    if (inject) {
    //invokeStatic指令,调用静态方法
    invokeStatic(Type.getType("Ljava/lang/System;"),
    new Method("currentTimeMillis", "()J"));
    //创建本地 LONG类型变量
    start = newLocal(Type.LONG_TYPE);
    //store指令 将方法执行结果从操作数栈存储到局部变量
    storeLocal(start);
    }
    }</pre>

    而`onMethodExit`也同样根据指令去编写代码即可。最终执行完插桩之后,我们就可以获得修改后的class数据。

    四、Android中的实现


    在Android中实现,我们需要考虑的第一个问题是如何获得所有的Class文件来判断是否需要插桩。Transform就是干这件事情的。


    相关视频


    Android项目实战 微信Matrix卡顿监控方案,函数自动埋点实践


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

    Flutter 单例的实现

    和谐学习!不急不躁!!我是你们的老朋友小青龙~ 前言 回顾iOS,单例的写法如下: static JXWaitingView *shared; +(JXWaitingView*)sharedInstance{ static dispatch_once_t...
    继续阅读 »

    和谐学习!不急不躁!!我是你们的老朋友小青龙~


    前言


    回顾iOS,单例的写法如下:


    static JXWaitingView *shared;

    +(JXWaitingView*)sharedInstance{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    shared=[[JXWaitingView alloc]initWithTitle:nil];
    });
    return shared;
    }

    其目的是通过dispatch_once来控制【初始化方法】只会执行一次,然后用static修饰的对象来接收并返回它。所以核心是只会执行一次初始化


    创建单例


    创建单例的案例


    class Student {
    String? name;
    int? age;
    //构造方法
    Student({this.name, this.age});

    // 单例方法
    static Student? _dioInstance;
    static Student instanceSingleStudent() {
    if (_dioInstance == null) {
    _dioInstance = Student();
    }
    return _dioInstance!;
    }
    }

    测试单例效果


    测试一


    import 'package:flutter_async_programming/Student.dart';

    void main() {
    Student studentA = Student.instanceSingleStudent();
    studentA.name = "张三";
    Student studentB = Student.instanceSingleStudent();
    print('studentA姓名是${studentA.name}');
    print('studentB姓名是${studentB.name}');
    }

    运行效果


    image.png


    测试二


    import 'package:flutter_async_programming/Student.dart';

    void main() {
    Student studentA = Student.instanceSingleStudent();
    studentA.name = "张三";
    Student studentB = Student.instanceSingleStudent();
    studentB.name = "李四";
    print('studentA姓名是${studentA.name}');
    print('studentB姓名是${studentB.name}');
    }

    运行效果


    image.png


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

    拒绝编译等待 - 动态研发模式 ARK

    iOS
    背景 pod install 时间长:编译优化绝大部分任务放在了 CocoaPods 上,CocoaPods 承担了更多工作,执行时间因此变长。编译时间长:虽然现阶段绝大部分工程已经从源码编译转型成二进制编译,但编译耗时依旧在十分钟左右,且现有工程基础上已无更...
    继续阅读 »



    背景

    iOS 业界研发模式多为 CocoaPods + Xcode + Git 的多仓组件化开发模型。为追求极致的研发体验、提升研发效率,对该研发模式进行了大量优化,但目前遇到了以下瓶颈,亟需突破:

    • pod install 时间长:编译优化绝大部分任务放在了 CocoaPods 上,CocoaPods 承担了更多工作,执行时间因此变长。

    • 编译时间长:虽然现阶段绝大部分工程已经从源码编译转型成二进制编译,但编译耗时依旧在十分钟左右,且现有工程基础上已无更好优化手段。

    • 超大型工程通病:Xcode Index 慢、爆内存、甚至卡死,链接时间长。

    如何处理这些问题?

    究其本质,产生这些问题的原因在于工程规模庞大。据此我们停下了对传统模式各节点的优化工作,以"缩小工程规模"为切入点,探索新型研发模式——动态研发模式 ARK。

    ARK[1] 是全链路覆盖的动态研发模式,旨在保证工程体验的前提下缩小工程规模:通过基线构建的方式,提供线下研发所需物料;同时通过实时的动态库转化技术,保证本地研发仅需下载和编译开发仓库。

    Show Case

    动态研发模式本地研发流程图如下。接下来就以抖音产品为例,阐述如何使用 ARK 做一次本地开发。
    演示基于字节跳动本地研发工具 MBox[2]

    1. 仓库下载

    ARK 研发模式下,本地研发不再拉取主仓代码,取而代之的是 ARK 仓库。ARK 仓库含有与主仓对应的所有配置,一次适配接入后期不需要持续维护。

    相较传统 APP 仓库动辄几个 GB 的大小,ARK 仓库贯彻了缩减代码规模这一概念。仓库仅有应用配置信息,不包含任何组件代码。ARK 仓库大小仅 2 MB,在 1 s 内可以完成仓库下载 。

    在 MBox 中的使用仅需几步点击操作。首先选择要开发的产品,然后勾选 ark 模式,选择开发分支,最后点击 Create 便可以数秒完成仓库下载。

    1. 开发组件

    CocoaPods 下进行组件开发一般是将组件仓库下载到本地,修改 Podfile 对应组件 A 为本地引用 pod A, :path =>'./A' ,之后进行本地开发。而在 MBox 和 ARK 的研发流程中,仅需选择要开发的组件点击 Add 便可进行本地开发。

    动态研发模式 ARK 通过解析 Podfile.lock 支持了 Checkout From Commit 功能,该功能根据宿主的组件依赖信息自动拉取相应的组件版本到本地,带来便捷性的同时也保证了编译成功率。

    1. pod install

    传统研发模式下 pod install 必须要经历 解析 Podfile 依赖、下载依赖、创建 Pods.xcodeproj 工程、集成 workspace 四个步骤,其中依赖解析和下载依赖两个步骤尤为耗时。

    ARK 研发模式下 Podfile 中没有组件,因此依赖解析、下载依赖这两个环节耗时几乎为零。其次由于工程中仅需开发组件步骤中添加的组件,在创建 Pods 工程、集成工程这两个环节中代码规模的降低,对提升集成速度的效果非常显著。

    没有依赖信息,编译、链接阶段显然不能成功。ARK 解决方案通过自研 cocoapods-ark 及配套工具链来保证编译、链接、运行的成功,其原理后续会在系列文章中介绍。

    1. 开发组件编译&调试

    和传统模式一样通过 Xcode 打开工程的 xcworkspace ,即可正常开发、调试完整的应用。

    工程中仅保留开发组件,但是依然有变量、函数、头文件跳转能力;参与 Index、编译的规模变小,Xcode 几乎不存在 loading 状态,大型工程也可以秒开;编译速度大幅提升。在整个动态研发流程中,通过工具链将组件从静态库转化成动态库,链接时间明显缩短。

    1. 查看全源码

    ARK 工程下默认只有开发组件的源码,查看全源码是开发中的刚需。动态研发流程提供了 pod doc 异步命令实现该能力,此命令可以在开发时执行,命令执行完成后重启工程即可通过 Document Target 查看工程中其他组件源码。

    pod doc 优点:

    • 支持异步和同步,执行过程中不影响本地开发。

    • 执行指令时跳过依赖解析环节,从服务端获取依赖信息,下载源码。

    • 通过 xcodegen 异步生成 Document 工程,大幅降低 pod install 时间。

    • 仅复用 pod installer 中的资源下载、缓存模块。

    • 支持仓库统一鉴权,自动跳过无权限组件仓库。

    收益

    体验上: 与传统模式开发流程一致,零成本切换动态研发模式。

    工具上: 站在巨人的肩膀上,CocoaPods 工具链相关优化在 ARK 同样生效。

    时间上: 传统研发模式中,历经各项优化后虽然能将全链路开发时间控制在 20 分钟左右,但这样的研发体验依旧不够友好。开发非常容易在这个时间间隔内被其他事情打断,良好的研发体验应该是连贯的。结合本地开发体验我们认为,一次连贯的开发体验应该将工程集成时间控制在分钟级,当前研发模式成功做到了这一点,将全链路开发时间控制在 5 分钟以内。

    成功率: 成功率一直是研发效率中容易被忽视的一个指标。据不完全数据统计,集团内应用的本地全量编译成功率不足五成。一半的同学会在首次编译后执行重试。显然,对于工程新手来说就是噩梦,这意味着很长时间将在这一环节中浪费。而 ARK 从平台基线到本地工具链贯彻 Sandbox 的理念,整体上提高编译成功率。

    写在最后

    ARK 当前已经在字节跳动内部多业务落地使用。从初期技术方案的探索到实际落地应用,遇到了很多技术难点,也对研发模式有了新的思考。

    相关技术文章将陆续分享,敬请期待。

    扩展阅读

    [1] ARK: https://github.com/kuperxu/KwaiTechnologyCommunication/blob/master/5.WWDC-ARK.pdf
    [2] MBox: https://mp.weixin.qq.com/s/5_IlQPWnCug_f3SDrnImCw

    作者:字节跳动终端技术——徐纪光
    来源:https://blog.csdn.net/YZcoder/article/details/121374743


    收起阅读 »

    手把手带你,优化一个滚动时流畅的TableView

    iOS
    手把手带你,优化一个滚动时流畅的TableView这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战我的专栏iOS 底层原理探索iOS 底层原理探索 之 阶段总结意识到我的问题平时使用手机的时间不算少,每天阅读新闻的时候会感觉到新闻类的app优化的还是...
    继续阅读 »

    手把手带你,优化一个滚动时流畅的TableView

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


    我的专栏

    1. iOS 底层原理探索
    2. iOS 底层原理探索 之 阶段总结

    意识到我的问题

    平时使用手机的时间不算少,每天阅读新闻的时候会感觉到新闻类的app优化的还是很好的,TableView的Cell滚动的时候不会去加载显示图片内容,当一次滑动结束之后,Cell上的新闻图片便开始逐个的加载显示出来,所以整个滑动的过程是很流畅的。这中体验也是相当nice的。

    我最开始的做法

    开发中TableView的使用是非常值频繁的,当TableViewCell上需要加载图片的时候,是一件比较头疼的事。因为,用户一边滑动TableView,TableView需要一边从网络获取图片。之前的操作都是放在 cellForRowAtIndexPath 中来处理,这就导致用户在滑动TableView的时候,会特别的卡(尤其是滑动特别快时),而且,手机的CPU使用率也会飙的非常的高。对于用户来说,这显然是一个十分糟糕的体验。

    糟糕的图片显示 代码

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    ImageTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
    cell.index = indexPath;

    NSMutableDictionary *info = [self.dataSource objectAtIndex:cell.index.row];

    NSString *url = [info objectForKey: @"img" ];
    NSData *iData = [NSData dataWithContentsOfURL:[NSURL URLWithString: url ]];
    cell.img.image = [UIImage imageWithData:iData];
    cell.typeL.text = [NSString stringWithFormat:@"%ld-%ld", cell.index.section, cell.index.row];

    return cell;
    }

    糟糕的手机CPU飙升率

    未命名.gif

    糟糕的用户滑动体验

    未命名1.gif

    不只是用户,对于开发这来讲,这也是不可以接受的体验。

    平时接触并使用的app也非常的多,发现他们多处理方式就是,当用户滑动列表的时候,不再加载图片,等用户的滑动结束之后,会开始逐一的加载图片。这是非常好的优化思路,减轻了CPU的负担,也不会基本不会让用户感觉到页面滚动时候的卡顿。这也就是最开始我描述的我看新闻app的使用体验。

    收到这个思路的启发,我们开始着手将上面糟糕的体验作一下优化吧。

    总结思路开启优化之路

    那么,带着这个优化思路,我开始了对于这个TableView 的优化。

    • 首先,我们只加载当前用户可以看到的cell上的图片。
    • 其次,我们一次只加载一张图片。

    要完成以上两点,图片的加载显示就不能在cellForRowAtIndexPath中完成,我们要定义并实现一个图片的加载显示方法,以便在合适的时机,调用刷新内容显示。

    loadSeeImage 加载图片的优化

    #pragma mark load Images
    - (void)loadSeeImage {

    //记录本次加载的几张图片
    NSInteger loadC = 0;

    // 用户可以看见的cells
    NSArray *cells = [self.imageTableView visibleCells];

    // 调度组
    dispatch_group_t group = dispatch_group_create();

    for (int i = 0; i < cells.count; i++) {

    ImageTableViewCell *cell = [cells objectAtIndex:i];

    NSMutableDictionary *info = [self.dataSource objectAtIndex:cell.index.row];
    NSString *url = [info objectForKey: @"img" ];

    NSString *data = [info objectForKey:@"data"];

    if ([data isKindOfClass:[NSData class]]) {


    }else {

    // 添加调度则到我们的串行队列中去
    dispatch_group_async(group, self.loadQueue, ^{

    NSData *iData = [NSData dataWithContentsOfURL:[NSURL URLWithString: url ]];
    NSLog(@" load image %ld-%ld ", cell.index.section, cell.index.row);
    if (iData) {
    // 缓存
    [info setValue:@"1" forKey:@"isload"];
    [info setValue:iData forKey:@"data"];
    }
    NSString *isload = [info objectForKey:@"isload"];

    if ([isload isEqualToString:@"0"]) {

    dispatch_async(dispatch_get_main_queue(), ^{

    cell.img.image = [UIImage imageNamed:@""];
    }); }else {

    if (iData) {

    dispatch_async(dispatch_get_main_queue(), ^{
    //显示加载后的图片
    cell.img.image = [UIImage imageWithData:iData];
    });
    }
    }

    });

    if (i == cells.count - 1) {

    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    // 全部加载完毕的通知
    NSLog(@"load finished");
    });
    }

    loadC += 1;
    }
    }

    NSLog(@"本次加载了 %ld 张图片", loadC);
    }

    其次就是 loadSeeImage 调用时机的处理,我们要做到用户在滑动列表之后加载,就是在下面两处加载:

    - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView   {  

    [self loadSeeImage];
    }

    - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {

    if (scrollView.isDragging || scrollView.isDecelerating || scrollView.isTracking) {
    return;
    }
    [self loadSeeImage];
    }

    当然,首次进入页面,列表数据加载完毕后,我们也要加载一次图片的哦。 好的下面看下优化后的结果:

    优化xcode.gif

    优化phone.gif

    CPU占用率比之前最高的时候降低了一半多,app在滑动的时候也没有明显卡顿的地方。 完美。

    收起阅读 »

    面向 JavaScript 开发人员的 5 大物联网库

    最近元宇宙的概念火遍互联网,自 Facebook 改名 Meta 以来,国内外越来越多的企业纷纷加入到布局元宇宙的行列。元宇宙之所以强势进入人们视野,与其底层技术的进步有着莫大的关系,包括AR/VR、云计算、物联网、5G、人工智能、数字孪生等等。其中,5G作为...
    继续阅读 »


    最近元宇宙的概念火遍互联网,自 Facebook 改名 Meta 以来,国内外越来越多的企业纷纷加入到布局元宇宙的行列。元宇宙之所以强势进入人们视野,与其底层技术的进步有着莫大的关系,包括AR/VR、云计算、物联网、5G、人工智能、数字孪生等等。其中,5G作为重要的连接基础,是元宇宙场景得以实现的关键。元宇宙将汇集游戏引擎、AR可穿戴设备、VR、现实世界数据集和不断发展的物联网。

    物联网(英语:InternetofThings,简称 IoT)是一种计算设备、机器、数码机器之间相互联系的系统,它拥有一种统一的统一识别代码(UID),并且能够在网络上传送数据,不需要人与人、或人与设备之间的交互。

    作为一个前端工程师(JavaScript工程师),似乎觉得这一切有点模式,其实不然,现代 JavaScript 的可以使用的场景越来越多,包括物联网,在本文中,将介绍可以在 JavaScript 代码中用于连接设备的 5 个脚本库。

    1. Cylon.js

    官方网站: https://cylonjs.com/

    Cylon.js 是用于机器人、物理计算和物联网 (IoT) 的流行 JavaScript 框架之一。不仅仅是一个“物联网”库,它还是一个完整的机器人框架,支持超过 43 个不同的平台,这是与机器连接的 43 种不同的地方或方式,目前支持的机器人和物理计算系统及软件平台有Arduino、Beaglebone Black、BLE、Disispark、Intel Galileo and Edison、Intel IoT Analytics、OpenCV、Octobl、Raspberry Pi、Salesforce等。

    可以使用 Cylon.js 连接到关键字并侦听它或 Arduino 板发送的事件,或者提供一个 HTTP API 接口并通过那里获取数据(它们也支持 socket.ioMQTT)。想通过 JavaScript 控制无人机吗?这并非不可以,首先需要安装:

    npm install cylon cylon-firmata cylon-gpio cylon-i2c

    然后运行一个这样的小脚本, 参考文章:

    npm install cylon cylon-ardrone

    然后运行脚本:

    const Cylon = require("cylon");

    Cylon.robot({
      connections: {
          ardrone: { adaptor: "ardrone", port: "192.168.1.1" },
      },

      devices: {
          drone: { driver: "ardrone" },
      },

      work: function (my) {
          my.drone.takeoff();
          after((10).seconds(), function () {
              my.drone.land();
          });
          after((15).seconds(), function () {
              my.drone.stop();
          });
      },
    }).start();

    如果有设备可以试试。 Cylon.js 的工作方式是允许其他人通过插件的方式提供连接器,这意味着这个库提供的功能没有限制。最重要的是,文档本身非常详细,写得很好,完整的代码示例。

    2. IoT.js

    官方网站: https://iotjs.net/

    IoT.js 是一个用 JavaScript 编写的物联网 (IoT) 框架。它旨在基于网络技术在物联网世界中提供一个可互操作的服务平台。

    如果希望在一个连接的设备中执行一些物联网(而不是在一个强大的、充满资源的服务器中的接收端),那么可能需要针对该环境进行优化。这个 IoT 框架运行在 JerryScript 引擎之上, JerryScript 引擎是一个针对小型设备优化的 JavaScript 运行时。这意味着,虽然无法使用最先进的 JS 的全部功能,但确实可以使用:

    • 完全支持 ECMAScript 5.1 语法。

    • 低内存消耗优化

    • 能够将 JS 代码预编译为字节码

    但是,兼容平台的数量没有 Cylon.js 多,而 IoT.js 只兼容:

    关于他们的文档,这应该是衡量一个库有多好的标准之一。他们有一些基本的例子和入门指南。但可能就是这样了。考虑到 IoT.js 是一个底层的硬件接口,现在看起来它希望开发人员已经有使用其他产品的经验,而不是针对JS开发人员寻求进入物联网。

    3. Johnny-Five

    官方网站: http://johnny-five.io/

    Johnny Five 是流行的 JavaScript 机器人和物联网平台之一。由 Bocoup 于 2012 年开发的 Johnny Five 一个开源的、基于 Firmata 协议的物联网和机器人编程框架,是 JavaScript 开发人员可用的最古老的机器人和物联网平台之一,从那时起,它的功能和兼容性都在不断增长。

    Johnny Five 支持 Arduino(所有型号)、Electric Imp、Beagle Bone、Intel Galileo & Edison、Raspberry Pi 等。该平台可轻松与流行的应用程序库(如 Express.js 和 Socket.io)以及物联网框架(如 Octoblu)结合使用。

    他们的文档非常详细,充满了关于硬件连接的示例和图表,这是一个很好的学习资源。

    4. NodeRed

    官方网站: https://nodered.org/

    NodeRed 是建立在 Node.js 之上,是一个基于流的编程工具,最初由 IBM 的新兴技术服务团队开发,现在是 JS 基金会的一部分。该平台允许在部署之前从浏览器以图形方式设置数据流和工作流。在理想的情况下,不需要编写任何代码,也许设置一些平台凭据。 NodeRed 还充当和其他人共享他们创建的流程的中心化平台,这是防止每次都重新创建轮子的好方法,即使没有真正编写代码。

    5. Zetta

    官方网站: https://www.zettajs.org/

    ZettaJS 是一个基于 Node.js 构建的开源平台,用于创建跨地理分布式计算机和云运行的物联网服务器。是另一种通过 JavaScript 与远程设备交互的方式。这里的主要区别在于 ZettaJS 的目标是将每个设备都变成一个 API,这是将 IoT 泛化为一个通用概念的好方法。如今,设备及其接口的数量正在爆炸增长,但没有对其进行规范控制。 ZettaJS 正试图在这方面进行改进,通过非常直观的编码方式,可以简单地为设备安装驱动程序,并在其中启用公共接口,并通过代码与它们交互。

    6. 总结

    通过上面介绍,JavaScript 不仅限于浏览器,甚至不限于基于 API 的后端开发,还可以随心所欲地从设备中提取数据或从设备中提取数据,并使用几乎完全相同的语言来控制它。

    作者:天行无忌
    来源:https://blog.51cto.com/devpoint/4762760

    收起阅读 »

    给团队做个分享,用30张图带你快速了解TypeScript

    正文30张脑图常见的基本类型我们知道TS是JS的超集,那我们先从几种JS中常见的数据类型说起,当然这些类型在TS中都有相应的,如下:特殊类型除了一些在JS中常见的类型,也还有一些TS所特有的类型类型断言和类型守卫如何在运行时需要保证和检测来自其他地方的数据也符...
    继续阅读 »

    正文

    30张脑图

    常见的基本类型

    我们知道TSJS的超集,那我们先从几种JS中常见的数据类型说起,当然这些类型在TS中都有相应的,如下:

    1常见的基本类型.png

    特殊类型

    除了一些在JS中常见的类型,也还有一些TS所特有的类型

    2特殊类型.png

    类型断言和类型守卫

    如何在运行时需要保证和检测来自其他地方的数据也符合我们的要求,这就需要用到断言,而断言需要类型守卫

    3类型断言.png

    接口

    接口本身只是一种规范,里头定义了一些必须有的属性或者方法,接口可以用于规范functionclass或者constructor,只是规则有点区别

    4TS中的接口.png

    类和修饰符

    JS一样,类class出现的目的,其实就是把一些相关的东西放在一起,方便管理

    TS主要也是通过class关键字来定义一个类,并且它还提供了3个修饰符

    5类和修饰符.png

    类的继承和抽象类

    TS中的继承ES6中的类的继承极其相识,子类可以通过extends关键字继承一个类

    但是它还有抽象类的概念,而且抽象类作为基类,不能new

    6.0类的继承和抽象类.png

    泛型

    将泛型理解为宽泛的类型,它通常用于类和函数

    但不管是用于类还是用于函数,核心思想都是:把类型当一种特殊的参数传入进去

    7泛型.png

    类型推断

    TS中是有类型推论的,即在有些没有明确指出类型的地方,类型推论会帮助提供类型

    8类型推断.png

    函数类型

    为了让我们更容易使用,TS为函数添加了类型等

    9函数.png

    数字枚举和字符串枚举

    枚举的好处是,我们可以定义一些带名字的常量,而且可以清晰地表达意图或创建一组有区别的用例

    TS支持数字的和基于字符串的枚举

    10枚举.png

    类型兼容性

    TS里的类型兼容性是基于结构子类型的 11类型兼容性.png

    联合类型和交叉类型

    补充两个TS的类型:联合类型和交叉类型

    12联合类型和交叉类型.png

    for..of和for..in

    TS也支持for..offor..in,但你知道他们两个主要的区别吗

    13forin和forof.png

    模块

    TS的模块化沿用了JS模块的概念,模块是在自身的作用域中执行,在一个模块里的变量,函数,类等等在模块外部是不可见的,除非你明确地使用export形式之一导出它们

    14模块.png

    命名空间的使用

    使用命名空间的方式,其实非常简单,格式如下: namespace X {}

    15命名空间的使用.png

    解决单个命名空间过大的问题

    16解决单个命名空间过大的问题.png

    简化命名空间

    要简化命名空间,核心就是给常用的对象起一个短的名字

    TS中使用import为指定的符号创建一个别名,格式大概是:import q = x.y.z

    17简化命名空间.png

    规避2个TS中命名空间和模块的陷阱

    18陷阱.png

    模块解析流程

    模块解析是指编译器在查找导入模块内容时所遵循的流程

    流程大致如下:

    image.png

    相对和非相对模块导入

    相对和非相对模块导入主要有以下两点不同

    image.png

    Classic模块解析策略

    TS的模块解析策略,其中的一种就叫Classic

    21Classic模块解析策略.png

    Node.js模块解析过程

    为什么要说Node.js模块解析过程,其实是为了讲TS的另一种模块解析策略做铺垫---Node模块解析策略。

    因为Node模块解析策略就是一种试图在运行时模仿Node.js模块解析的策略

    22Node.js的模块解析过程.png

    Node模块解析策略

    Node模块解析策略模仿Node.js运行时的解析策略来在编译阶段定位模块定义文件的模块解析的策略,但是跟Node.js会有点区别

    23Node模块解析策略.png

    声明合并之接口合并

    声明合并指的就是编译器会针对同名的声明合并为一个声明

    声明合并包括接口合并,接口的合并需要区分接口里面的成员有函数成员和非函数成员,两者有差异

    24接口合并.png

    合并命名空间

    命名空间的合并需要分两种情况:一是同名的命名空间之间的合并,二是命名空间和其他类型的合并

    25合并命名空间.png

    JSX模式

    TS具有三种JSX模式:preservereactreact-native

    26JSX.png

    三斜线指令

    三斜线指令其实上面有讲过,像/// <reference>

    它的格式就是三条斜线后面跟一个标签

    27三斜线指令.png


    作者:LBJ
    链接:https://juejin.cn/post/7036266588227502093

    收起阅读 »

    我去!爬虫遇到字体反爬,哭了

    今天准备爬取某某点评店铺信息时,遇到了『字体』反爬。比如这样的: 还有这样的: 可以看到这些字体已经被加密(反爬) 竟然遇到这种情况,那辰哥就带大家如何去解决这类反爬(字体反爬类) 01 网页分析在开始分析反爬之前,先简单的介绍一下背景(爬取的网页) 辰...
    继续阅读 »

    今天准备爬取某某点评店铺信息时,遇到了『字体』反爬。比如这样的:


    img

    还有这样的:


    img

    可以看到这些字体已经被加密反爬


    竟然遇到这种情况,那辰哥就带大家如何去解决这类反爬(字体反爬类


    01 网页分析

    在开始分析反爬之前,先简单的介绍一下背景(爬取的网页)


    img

    辰哥爬取的某某点评的店铺信息。一开始查看网页源码是这样的


    img

    这种什么也看不到,咱们换另一种方式:通过程序直接把整个网页源代码保存下来


    img

    获取到的网页源码如下:


    img

    比如这里看到评论数(4位数)都有对应着一个编号(相同的数字编号相同),应该是对应着网站的字体库


    下一步,我们需要找到这个网站的字体库。


    02 获取字体库

    这里的字体库建议在目标网站里面去获取,因为不同的网站的字体库是不一样,导致解码还原的字体也会不一样。


    1、抓包获取字体库


    img

    在浏览器network里面可以看到一共有三种字体库。(三种字体库各有不同的妙用,后面会有解释


    img

    把字体库链接复制在浏览器里面打开,就可以把字体库下载到本地。


    2、查看字体库


    这里使用FontCreator的工具查看字体库。


    下载地址:


    https://www.high-logic.com/font-editor/fontcreator/download

    这里需要注册,邮箱验证才能下载,不过辰哥已经下载了,可以在公众号回复:FC,获取安装包。


    安装之后,把刚刚下载的字体库在FontCreator中打开


    img

    可以看到字体的内容以及对应的编号


    比如数字7对应F399数字8对应F572 ,咱们在原网页和源码对比,是否如此???


    img

    可以看到,真是一模一样对应着解码就可以还原字体。


    3、为什么会有三个字体库


    img

    在查看加密字体的CSS样式时,方式有css内容是这样的


    img

    字体库1:d35c3812.woff 对应解码class为 shopNum


    字体库2:084c9fff.woff 对应解码class为 reviewTag和address


    字体库3:73f5e6f3.woff 对应解码class为 tagName


    也就是说,字体所属的不同class标签,对应的解密字体库是不一样的,辰哥这里不得不说一句:太鸡贼了


    img

    咱们这里获取的评论数,clas为shopNum,需要用到字体库d35c3812.woff


    03 代码实现解密

    1、加载字体库


    既然我们已经知道了字体反爬的原理,那么我们就可以开始编程实现解密还原。


    加载字体库的Python库包是:fontTools ,安装命令如下:


    pip install fontTools

    img

    将字体库的内容对应关系保存为xml格式


    img

    code和name是一一对应关系


    img

    img

    可以看到网页源码中的编号后四位对应着字体库的编号。


    因此我们可以建立应该字体对应集合


    img

    建立好映射关系好,到网页源码中去进行替换


    img

    img

    这样我们就成功的将字体反爬处理完毕。后面提取内容大家基本都没问题。


    2、完整代码


    img

    输出结果:


    img

    可以看到加密的数字全部都还原了。


    04 小结

    辰哥在本文中主要讲解了如此处理字体反爬问题,并以某某点评为例去实战演示分析。辰哥在文中处理的数字类型,大家可以尝试去试试中文如何解决。


    作者:Python研究者
    来源:https://juejin.cn/post/6970933428145356831

    收起阅读 »

    js实现放大镜

    借助宽高等比例放大的两张图片,结合js中鼠标偏移量、元素偏移量、元素自身宽高等属性完成;左侧遮罩移动Xpx,右侧大图移动X*倍数px;其余部分就是用小学数学算一下就OK了。JS // 获取小图和遮罩、大图、大盒子    var small ...
    继续阅读 »



    先看效果图

    实现原理

    借助宽高等比例放大的两张图片,结合js中鼠标偏移量、元素偏移量、元素自身宽高等属性完成;左侧遮罩移动Xpx,右侧大图移动X*倍数px;其余部分就是用小学数学算一下就OK了。

    HTML和CSS

     <div class="wrap">
       
       <div id="small">
         <img src="img/1.jpg" alt="" >
         <div id="mark">div>
       div>
       
       <div id="big">
         <img src="img/2.jpg" alt="" id="bigimg">
       div>
     div>
    * {
        margin: 0;
        padding: 0;
      }
      .wrap {
        width: 1500px;
        margin: 100px auto;
      }

      #small {
        width: 432px;
        height: 768px;
        float: left;
        position: relative;
      }

      #big {
        /* background-color: seagreen; */
        width: 768px;
        height: 768px;
        float: left;
        /* 超出取景框的部分隐藏 */
        overflow: hidden;
        margin-left: 20px;
        position: relative;
        display: none;
      }

      #bigimg {
        /* width: 864px; */
        position: absolute;
        left: 0;
        top: 0;
      }

      #mark {
        width: 220px;
        height: 220px;
        background-color: #fff;
        opacity: .5;
        position: absolute;
        left: 0;
        top: 0;
        /* 鼠标箭头样式 */
        cursor: move;
        display: none;
      }

    JS

     // 获取小图和遮罩、大图、大盒子
       var small = document.getElementById("small")
       var mark = document.getElementById("mark")
       var big = document.getElementById("big")
       var bigimg = document.getElementById("bigimg")
       // 在小图区域内获取鼠标移动事件;遮罩跟随鼠标移动
       small.onmousemove = function (e) {
         // 得到遮罩相对于小图的偏移量(鼠标所在坐标-小图相对于body的偏移-遮罩本身宽度或高度的一半)
         var s_left = e.pageX - mark.offsetWidth / 2 - small.offsetLeft
         var s_top = e.pageY - mark.offsetHeight / 2 - small.offsetTop
         // 遮罩仅可以在小图内移动,所以需要计算遮罩偏移量的临界值(相对于小图的值)
         var max_left = small.offsetWidth - mark.offsetWidth;
         var max_top = small.offsetHeight - mark.offsetHeight;
         // 遮罩移动右侧大图也跟随移动(遮罩每移动1px,图片需要向相反对的方向移动n倍的距离)
         var n = big.offsetWidth / mark.offsetWidth
         // 遮罩跟随鼠标移动前判断:遮罩相对于小图的偏移量不能超出范围,超出范围要重新赋值(临界值在上边已经计算完成:max_left和max_top)
         // 判断水平边界
         if (s_left < 0) {
           s_left = 0
        } else if (s_left > max_left) {
           s_left = max_left
        }
         //判断垂直边界
         if (s_top < 0) {
           s_top = 0
        } else if (s_top > max_top) {
           s_top = max_top
        }
         // 给遮罩left和top赋值(动态的?因为e.pageX和e.pageY为变化的量),动起来!
         mark.style.left = s_left + "px";
         mark.style.top = s_top + "px";
         // 计算大图移动的距离
         var levelx = -n * s_left;
         var verticaly = -n * s_top;
         // 让图片动起来
         bigimg.style.left = levelx + "px";
         bigimg.style.top = verticaly + "px";
      }
       // 鼠标移入小图内才会显示遮罩和跟随移动样式,移出小图后消失
       small.onmouseenter = function () {
         mark.style.display = "block"
         big.style.display= "block"
      }
       small.onmouseleave = function () {
         mark.style.display = "none"
         big.style.display= "none"
      }

    总结

    • 鼠标焦点一旦动起来,它的偏移量就是动态的;父元素和子元素加上定位后,通过动态改变某个元素的lefttop值来实现“动”的效果。

    • 大图/小图=放大镜(遮罩)/取景框

    • 两张图片一定要等比例缩放

    作者:Onion韩
    来源:https://juejin.cn/post/7030963292818374670

    收起阅读 »

    从谷歌一行代码学到的姿势

    网上很流行的一行代码,据说是谷歌工程师写的,它的作用是给页面所有元素增加一个随机颜色的外边框。[].forEach.call($$("*"),function(a){a.style.outline="1px solid #"+(~~(Math.random()...
    继续阅读 »

    网上很流行的一行代码,据说是谷歌工程师写的,它的作用是给页面所有元素增加一个随机颜色的外边框

    [].forEach.call($$("*"),function(a){a.style.outline="1px solid #"+(~~(Math.random()*(1<<24))).toString(16)})

    运行效果如下图:

    这个代码虽然只有一行,但是包含的知识点不少,网上有很多解析。我也说下自己的理解,然后最后推荐在实务中使用TreeWalker对象进行遍历。

    我的理解其中主要包含如下4个知识点:

    1. [].forEach.call
    2. $$("*")
    3. a.style.outline
    4. (~~(Math.random()*(1<<24))).toString(16)

    1 [].forEach.call

    1.1 [].forEach

    forEach是数组遍历的一个方法,接收一个函数参数用来处理每一个遍历的元素,常规的使用姿势是:

    let arr = [3, 5, 8];
    arr.forEach((item) => {
    console.log(item);
    })
    // 控制台输出:
    // 3
    // 5
    // 8

    那么下面的写法:

    [].forEach

    只是为了得到 forEach 这个方法,这个方法是定义都在Array.prototype上的方法,[] 表示空数组,可以访问到数组原型对象上的方法。

    得到 forEach 这个方法后,就可以通过 call 发起调用。

    1.2 call

    call函数用来调用一个函数,和普通调用不同,call调用可以修改函数内this指向。

    常规调用函数的姿势:

    let object1 = {
    id: 1,
    printId() {
    console.log(this.id)
    }
    }
    object1.printId();
    // 控制台输出:
    // 1

    因为是正常调用,方法内的this指向object1对象,所以上例输出1。

    使用call调用printId方法,并传入另外一个对象object2:

    let object2 = {
    id: 2
    }
    object1.printId.call(object2);
    // 控制台输出:
    // 2

    这里使用call调用object1.printId函数,传入了object2对象,那么printId函数内的this就是指向object2这个对象,所以结果输出2。

    1.3 综合分析

    综合来看:

    [].forEach.call( $$("*"), function(a){} )

    这行代码的意思就是遍历如下对象:

    $$("*") 

    然后用如下方法处理每个元素:

    function(a){}

    其中,a就是遍历的的每一个元素。

    那么

    $$("*") 

    指什么呢?我们接着往后看。

    2 $$("*")

    这个写法用来获取页面所有元素,相当于

    document.querySelectorAll('*')

    只是

    $$("*") 

    只能在浏览器开发控制台内使用,这个是浏览器开发控制台提供出来的预定义API,至于为什么,大家可以参考底部的参考文章。

    3 a.style.outline

    设置元素边框,估计很多人都知道,但是设置外边框就比较少人了解了,外边框的效果和边框类似,唯一不同的点是外边框盒子模型的算式,仅仅做装饰使用。

    <style type="text/css">
    #swiper {
    width: 100px;
    height: 100px;
    outline: 10px solid;
    }
    style>

    <div id="swiper">div>

    运行效果:

    div元素实际的宽高还是100 * 100,如果把outline改成border,那么div元素的实际宽高就是120 * 120,因为要加上border的宽度。

    外边框设置的最大作用就是:

    可以设置元素边框效果,但是不影响页面布局。

    4 (~~(Math.random()*(1<<24))).toString(16)

    这个代码从结果是得到一个16进制的颜色值,但是为什么能得到呢?

    16进制的颜色值:81f262

    4.1 Math.random()

    这个容易理解,就是随机 [0, 1) 的小数。

    4.2 1<<24

    这个表示1左移24位,二进制表示如下所示:

    1 0000 0000 0000 0000 0000 0000  

    十进制就是表示:

    2^24

    那么

    Math.random() * (1<<24)

    就会得到如下范围的一个随机浮点数:

    [0, 2^24) 

    4.3 两次按位取反

    因为Math.random()得到是一个小数,所以两次按位取反就是为了过滤掉小数部分,最后得到整数。

    所以

    (~~(Math.random()*(1<<24)))

    就会得到如下范围的一个随机整数:

    [0, 2^24) 

    4.4 转成字符串toString(16)

    最后就是把上面得到的数字转成16进制,我们知道toString()是用来把相关的对象转成字符串的,它可以接收一个进制参数,转成不同的进制,默认是转成10进制。

    对象.toString(2); // 转成2进制
    对象.toString(8); // 转成8进制
    对象.toString(10); // 转成10进制
    对象.toString(16); // 转成16进制

    上面的得到的随机整数用二进制表示就是:

    0000 0000 0000 0000 0000 0000  

    1111 1111 1111 1111 1111 1111

    那么2进制转成16进制,是不是就是每4位转一个?

    最终是不是就得到一个6个长度的16进制数了?

    这个字符串加上#是不是就是16进制的颜色值了?

    形如:

    #ac83ce
    #b74384
    等等...

    实务应用

    虽然上面的代码简短,并且知识含量也很高,但是在实务中如果要遍历元素,我并不建议使用这样的方式。

    主要原因是两个:

    1. $$("*") 只在开发控制台可以用,正常项目代码中不能用。
    2. 选中所有元素再遍历,性能低。

    如果实务中要遍历元素,建议是用 TreeWalker。querySelectorAll是一次性获取所有元素然后遍历,TreeWalker是迭代器的方式,性能上 TreeWalker 更优,另外 TreeWalker 还支持各种过滤。

    参考如下示例:

    // 实例化 TreeWalker 对象
    let walker = document.createTreeWalker(
    document.documentElement,
    NodeFilter.SHOW_ELEMENT
    );
    // 遍历
    let node = walker.nextNode();
    while (node !== null) {
    node.style.outline = "1px solid #" + (~~(Math.random() * (1 << 24))).toString(16);
    node = walker.nextNode();
    }

    虽然代码更多,当时性能更好,并且支持各种过滤等,功能也更加强大。

    如果大家有学到新姿势,麻烦帮忙点个赞,谢谢。欢迎大家留言讨论。

    参考资料

    JavaScript中的$$(*)代表什么和$选择器的由来:ourjs.com/detail/54ab…

    querySelectorAll vs NodeIterator vs TreeWalker:stackoverflow.com/questions/6…

    作者:晴空闲云
    来源:https://juejin.cn/post/7034777643014684703

    收起阅读 »

    现在实现倒计时都这么卷了吗?

    但是在校准时间的过程中,为了快速追赶落后的时间,时间跳动太快了,导致体验不太好,体感上感觉这时间不准呀,因此我再在那基础上再优化了一版 为求实现一版超准确!超平稳!性能极好!体验极佳的倒计时 旧版的功能实现代码 const totalDuration = 10...
    继续阅读 »

    但是在校准时间的过程中,为了快速追赶落后的时间,时间跳动太快了,导致体验不太好,体感上感觉这时间不准呀,因此我再在那基础上再优化了一版


    为求实现一版超准确!超平稳!性能极好!体验极佳的倒计时


    旧版的功能实现代码


    const totalDuration = 10 * 1000;
    let requestRef = null;
    let startTime;
    let prevEndTime;
    let prevTime;
    let currentCount = totalDuration;
    let endTime;
    let timeDifferance = 0; // 每1s倒计时偏差值,单位ms
    let interval = 1000;
    let nextTime = interval;

    setInterval(() => {
    let n = 0;
    while (n++ < 1000000000);
    }, 0);

    const animate = (timestamp) => {
    if (prevTime !== undefined) {
    const deltaTime = timestamp - prevTime;
    if (deltaTime >= nextTime) {
    prevTime = timestamp;
    prevEndTime = endTime;
    endTime = new Date().getTime();
    currentCount = currentCount - 1000;
    console.log("currentCount: ", currentCount / 1000);
    timeDifferance = endTime - startTime - (totalDuration - currentCount);
    console.log(timeDifferance);
    nextTime = interval - timeDifferance;
    // 慢太多了,就立刻执行下一个循环
    if (nextTime < 0) {
    nextTime = 0;
    }
    console.log(`执行下一次渲染的时间是:${nextTime}ms`);
    if (currentCount <= 0) {
    currentCount = 0;
    cancelAnimationFrame(requestRef);
    console.log(`累计偏差值: ${endTime - startTime - totalDuration}ms`);
    return;
    }
    }
    } else {
    startTime = new Date().getTime();
    prevTime = timestamp;
    endTime = new Date().getTime();
    }
    requestRef = requestAnimationFrame(animate);
    };

    requestRef = requestAnimationFrame(animate);


    然后有个细小的问题在于这段代码


    // 慢太多了,就立刻执行下一个循环
    if (nextTime < 0) {
    nextTime = 0;
    }

    问题在于,假如遇到线程阻塞的情况,出现了倒计时落后情况严重,假设3s,我这里设置下一个循环是0s,然后现在倒计时当前15s,就会看到快速倒计时到12s,产品同学说你这倒计时还怎么加速了呀


    这倒计时加速像极了职业生涯结束在加速倒计时一样,瑟瑟发抖的我立刻赶紧修复一下


    其实很简单,就是把这个临近值0设置接近每次循环的时间数即可,那么其实是看不出来每次是有在稍微加速/减速的,这里每次循环的时间数是1s,那么我们可以将上面这段代码修改下,把以前立刻就追赶描述的操作,放缓一下追赶的脚步,以此优化用户体验


    例如以前追赶2s3s~4s内立刻追赶上,那么波动是很明显的,但是如果把2s的落后秒数,平躺到接下来要倒计时的1min里,每次大概追赶30ms,那是看不出来滴


    // 慢到一定临界点,比正常循环的时间数稍微慢点,再执行下一个循环
    if (nextTime < 900) {
    nextTime = 900;
    }

    这里我设置落后太多时,每秒追赶100ms,假如落后2s20s后就能追赶回来啦,而且看不出明显波动,时间又是被校验准确的,得到了产品同学的好评!


    虽然修改很小,但是也是反复思考得到的~如果对时间要求比较严格,而且倒计时时间范围比较小,来不及把差距平摊到这么大的时间段,可建议让后端同学定时推送最新的倒计时给前端来校验时间准确性,这就万无一失啦


    结语


    以上是我使用requestAnimationFrame实现倒计时功能反复雕琢的心得,希望能对大家有帮助~如果能获得一个小小的赞作为鼓励会十分感激!!


    作者:一只凤梨
    链接:https://juejin.cn/post/7026735190634414087

    收起阅读 »

    中高级前端不一定了解的setTimeout | 网易实践小总结

    setTimeout的创建和执行 我们知道setTimeout是一个延时器,它会在规定的时间后延迟执行回调函数,这篇文章就来说说setTimeout它是怎么执行的。 首先我们知道消息队列是用来存储宏任务的,并且主线程会按照顺序取出队列里的任务依次执行,所以为了...
    继续阅读 »

    setTimeout的创建和执行


    我们知道setTimeout是一个延时器,它会在规定的时间后延迟执行回调函数,这篇文章就来说说setTimeout它是怎么执行的。


    首先我们知道消息队列是用来存储宏任务的,并且主线程会按照顺序取出队列里的任务依次执行,所以为了保证setTimeout能够在规定的时间内执行,setTimeout创建的任务不会被添加到消息队列里,与此同时,浏览器还维护了另外一个队列叫做延迟消息队列,该队列就是用来存放延迟任务,setTimeout创建的任务会被存放于此,同时它会被记住创建时间,延迟执行时间。


    然后我们看下具体例子:


    setTimeout(function showName() { console.log('showName') }, 1000)
    setTimeout(function showName() { console.log('showName1') }, 1000)
    console.log('martincai')

    以上例子执行是这样:



    • 1.从消息队列中取出宏任务进行执行(首次任务直接执行)

    • 2.执行setTimeout,此时会创建一个延迟任务,延迟任务的回调函数是showName,发起时间是当前的时间,延迟时间是第二个参数1000ms,然后该延迟任务会被推入到延迟任务队列

    • 3.执行console.log('martincai')代码

    • 4.从延迟队列里去筛选所有已过期任务(当前时间 >= 发起时间 + 延迟时间),然后依次执行


    所以我们可以看到showName和showName1同时执行,原因是两个延迟任务都已经过期


    循环源码:


    void MainTherad(){
    for(;;){
    // 执行消息队列中的任务
    Task task = task_queue.takeTask();
    ProcessTask(task);

    // 执行延迟队列中的任务
    ProcessDelayTask()

    if(!keep_running) // 如果设置了退出标志,那么直接退出线程循环
    break;
    }
    }

    删除延迟任务


    clearTimeout是windows下提供的原生方法,用于删除特定的setTimeout的延迟任务,我们在定义setTimeout的时候返回值就是当前任务的唯一id值,那么clearTimeout就会拿着id在延迟消息队列里查找对应的任务,将其踢出队列即可


    setTimeout的几个注意点:



    1. setTimeout会受到消息队列里的宏任务的执行时间影响,上面我们可以看到延迟消息队列的任务会在消息队列的弹出的当前任务执行完之后再执行,所以当前任务的执行时间会阻碍到setTimeout的延迟任务的执行时间


      function showName() {
    setTimeout(function show() {
    console.log('show')
    }, 0)
    for (let i = 0; i <= 5000; i++) {}
    }
    showName()

    这里去执行一遍可以发现setTimeout并不是在0ms左右执行,中间会有明显的延迟,因为setTimeout在执行的时候首先会将任务放入到延迟消息队列里,等到showName执行完之后,才会去延迟队列里去查找已过期的任务,这里setTimeout任务会被showName耽误



    1. setTimeout嵌套下会有4ms的延迟


    Chrome会把嵌套5层以上的setTimeout后当作阻塞方法,在第6次调用setTimeout的时候会自动将延时器更改为至少4ms的延迟时间



    1. 未激活的页面的setTimeout更改为至少1000ms


    当前tab页面不在active状态的时候,setTimeout的延迟至少会被更改1000ms,这样做是为了减少性能消耗和电量消耗



    1. 延迟时间有最大值


    目前Chrome、Firefox等主流浏览器都是用32bit去存储延时时间,所以最大值是2的31次方 - 1


      setTimeout(() => {
    console.log(1)
    }, 2 ** 31)

    以上代码会立即执行


    作者:我在曾经眺望彼岸
    链接:https://juejin.cn/post/7032091028609990692

    收起阅读 »

    Android 图形处理 —— Matrix 原理剖析

    Matrix 简介 Android 图形库中的 android.graphics.Matrix 是一个 3×3 的 float 矩阵,其主要作用是坐标变换 它的结构大概是这样的 其中每个位置的数值作用和其名称所代表的的含义是一一对应的 MSCALE_X、M...
    继续阅读 »

    Matrix 简介


    Android 图形库中的 android.graphics.Matrix 是一个 3×3 的 float 矩阵,其主要作用是坐标变换


    它的结构大概是这样的


    matrix


    其中每个位置的数值作用和其名称所代表的的含义是一一对应的



    • MSCALE_X、MSCALE_Y:控制缩放

    • MTRANS_X、MTRANS_Y:控制平移

    • MSKEW_X、MSKEW_X:控制错切

    • MSCALE_X、MSCALE_Y、MSKEW_X、MSKEW_X:控制旋转

    • MPERSP_0、MPERSP_1、MPERSP_2:控制透视


    matrix_1


    在 Android 中,我们直接实例化一个 Matrix,内部的矩阵长这样:


    matrix_3


    是一个左上到右下为 1,其余为 0 的矩阵,也叫单位矩阵,一般数学上表示为 I


    Matrix 坐标变换原理


    前面说到 Matirx 主要的作用就是处理坐标的变换,而坐标的基本变换有:平移、缩放、旋转和错切



    这里所说的基本变换,也称仿射变换 ,透视不属于仿射变化,关于透视相关的内容不在本文的范围内



    当矩阵的最后一行是 0,0,1 代表该矩阵是仿射矩阵,下文中所有的矩阵默认都是仿射矩阵


    线性代数中的矩阵乘法


    在正式介绍 Matrix 是如何控制坐标变换的原理之前,我们先简单复习一下线性代数中的矩阵乘法,详细的讲解可参见维基百科或者翻翻大学的《线性代数》,这里只做最简单的介绍




    • 两个矩阵相乘,前提是第一个矩阵的列数等于第二个矩阵的行数




    • 若 A 为 m × n 的矩阵,B 为 n × p 的矩阵,则他们的乘积 AB 会是一个 m × p 的矩阵,表达可以写为





    • 由定义计算,AB 中任意一点(a,b)的值为 A 中第 a 行的数和 B 中第 b 列的数的乘积的和







    了解矩阵乘法的基本方法之后,我们还需要记住几个性质,对后续的分析有用



    • 满足结合律,即 A(BC)=(AB)C

    • 满足分配律,即 A(B + C) = AB + AC (A + B)C = AC + BC

    • 不满足交换律,即 AB != BA

    • 单位矩阵 I 与任意矩阵相乘,等于矩阵本身,即 IA = ABI = B


    缩放(Scale)


    我们先想想,让我们实现把一个点 (x0, y0) 的 x 轴和 y 轴分别缩放 k1 和 k2 倍,我们会怎么做,很简单


    val x = k1 * x0
    val y = k2 * y0

    那如果用矩阵怎么实现呢,前面我们讲到 Matrix 中 MSCALE_XMSCALE_Y 是用来控制缩放的,我们在这里填分别设置为 k1 和 k2,看起来是这样的


    image-20211109103257621

    而点 (x0, y0) 用矩阵表示是这样的


    image-20211109103824496

    有些人会疑问,最后一行这里不是还有一个 1 吗,这是使用了齐次坐标系的缘故,在数学中我们的点和向量都是这样表示的 (x, y),两者看起来一样,计算机无法区分,为了让计算机也可以区分它们,增加了一个标志位,即


    (x, y, 1) -> 点
    (x, y, 0) -> 向量

    现在 Matrix 和点都可以用矩阵表示了,接下来我们看看怎么通过这两个矩阵得到一个缩放之后的点 (x, y). 前面我们已经介绍过矩阵的乘法,让我们看看把上面两个矩阵相乘会得到什么结果


    image-20211109104922576

    可以看到,矩阵相乘得到了一个(k1x0, k2y0,1)的矩阵,上面说过,计算机中,这个矩阵就代表点 (k1x0, k2y0), 而这个点刚好就是我们要的缩放之后的点


    以上所有过程用代码来实现,看起来就是像下面这样


    val xy = FloatArray(x0, y0)
    Matrix().apply {
    setScale(k1, k2)
    mapPoints(xy)
    }

    平移(Translate)


    平移和缩放也是类似的,实现平移,我们一般可写为


    val x = x0 + deltaX
    val y = y0 + deltaY

    而用矩阵来实现则是


    val xy = FloatArray(x0, y0)
    Matrix().apply {
    setTranslate(k1, k2)
    mapPoints(xy)
    }

    换成数学表示


    translate


    根据矩阵乘法


    x = 1 × x0 + 0 × y0 + deltaX × 1 = x0 + deltaX
    y = 0 × x0 + 1 × y0 + deltaY × 1 = y0 + deltaY

    可得和一开始的实现也是效果一致的


    错切(Skew)


    错切相对于平移和缩放,可能大部分人对这个名词比较陌生,直接看三张图大家可能会比较直观


    水平错切


    x = x0 + ky0
    y = y0

    矩阵表示



    水平错切


    垂直错切


    x = x0
    y = kx0 + y0

    矩阵表示




    复合错切


    x = x0 + k1y0
    y = k2x0 + y0

    矩阵表示




    旋转(Rotate)


    旋转相对以上三种变化又有一点复杂,这里涉及一些三角函数的计算,忘记的可以去维基百科 先复习下



    image-20211108215739508

    同样我们先自己实现一下旋转,假设一个点 A(x0, y0), 距离原点的距离为 r,与水平夹角为 α,现绕原点顺时针旋转 θ 度,旋转之后的点为 B(x, y)



    用矩阵表示




    Matrix 复合操作原理


    前面介绍了四种基本变换,如果我们需要同时应用上多种变化,比如先绕原点顺时针旋转 90° 再 x 轴平移 100,y 轴平移 100, 最后 x、y 轴缩放0.5 倍,那么就需要用到复合操作


    还是先用自己的实现来实现一下


    x = ((x0 · cosθ - y0 · sinθ) + 100) · 0.5
    y = ((y0 · cosθ + x0 · sinθ) + 100) · 0.5

    矩阵表示


    image-20211206155715836


    按照前面的方式逐个推导,最终也能得到和上述一样的结果


    到此,我们可以对 Matrix 做出一个基本的认识:Matrix 基于矩阵计算的原理,解决了计算机中坐标映射和变化的问题


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