注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

算法题每日一练:位运算

一、前言 学习目标: 掌握 原码 反码 补码 基本运算以及转换 熟练应用 与 或 非 同或 异或 应用 对于 移位运算 在题目中熟练应用,后面会出位运算的题目 二、概述 计算机最主要的功能是处理数值、文字、声音、图形图像等信息。 在计算机内部,各种信息都必...
继续阅读 »

一、前言


学习目标:



  • 掌握 原码 反码 补码 基本运算以及转换

  • 熟练应用 与 或 非 同或 异或 应用

  • 对于 移位运算 在题目中熟练应用,后面会出位运算的题目


二、概述


计算机最主要的功能是处理数值、文字、声音、图形图像等信息。


在计算机内部,各种信息都必须经过数字化编码后才能被传送、存储和处理,所有的数据以二进制的形式存储在设备中,即 0、1 这两种状态。


计算机对二进制数据进行的运算(+、-、*、/)都是叫位运算,例如下面的计算:


int a=74;
int b=58;
int c=a+b;

a 74 : 1 0 0 1 0 1 0
b 58 : 1 1 1 0 1 0
c 132 : 1 0 0 0 0 1 0 0

十进制数字转换成底层的二进制数字之后,二进制逐位相加,满2进1。


三、原码 反码 补码


1.原码


在计算机的运算中,计算机只能做加法,减法、乘除都没法计算。原码是最简单的机器数表示法,用最高位表示符号位,其他位存放该数的二进制的绝对值。


2.png


首位的0表示正数、1表示负数。


特点



  • 表示直观易懂,正负数区分清晰

  • 加减法运算复杂,要先判断符号是正号还是负号、相同还是相反


2.反码


正数的反码还是等于原码,负数的反码就是它的原码除符号位外,按位取反。


1.png


特点



  • 反码的表示范围与原码的表示范围相同

  • 反码表示在计算机中往往作为数码变换的中间环节


3.补码


正数的补码等于它的原码,负数的补码等于反码+1


3.png


特点:



  • 在计算机运算时,都是以补码的方式运算的,下面的位运算也是补码形式计算


四、基本运算


1.与


符号:&


运算规则:两个二进制位都为1时才为1,否则为0


示例:1001&1111=1001


2.或


符号:|


运算规则:两个二进制位都为0时才为0,否则为1


示例:1001&1100=1101


3.非


符号:~


运算规则:0变成1,1变成0


示例:~1001 = 0110


4.同或


符号:~


运算规则:数字相同时为1,相反为0


示例:1001~1100=1010


5.异或


符号:^


运算规则:两个二进制位相反为1,相同为0


示例:1001^0111=1110


五、移位运算


1.左移


符号:<<


运算规则:符号位不变,低位补0


示例


a<<b 代表十进制数字a向左移动b个进位
/* 左移:
* 左移1位,相当于原数值 * 2
* 左移2位,相当于原数值 * 4
* 左移n位,相当于原数值 * 2^n
*/
计算 10 << 1
10的补码:0000 1010
-----------------------
结果补码:0001 0100 ==> 正数,即 10*2=20

计算 10 << 2
10的补码:0000 1010
-----------------------
结果补码:0010 1000 ==> 正数,即 10*2^2=40

计算 10 << 3
10的补码:0000 1010
-----------------------
结果补码:0101 0000 ==> 正数,即 10*2^3=80

计算 12 << 1
12的补码:0000 1100
-----------------------
结果补码:0001 1000 ==> 正数,即 12*2=24

2.右移


符号:>>


运算规则:低位溢出,符号位不变,并用符号位补溢出的高位


示例


a>>b 代表十进制数字a向右移动b个进位
/* 右移:
* 右移1位,相当于原数值 / 2
* 右移2位,相当于原数值 / 4
* 右移3位,相当于原数值 / 2^n
* 结果没有小数(向下取整)
*/
计算 80 >> 1
80的补码:0101 0000
-----------------------
结果补码:0010 1000 ==> 正数,即 80/2=40

计算 80 >> 2
80的补码:0101 0000
-----------------------
结果补码:0001 01000 ==> 正数,即 80/2^2=20

计算 80 >> 3
80的补码:0101 0000
-----------------------
结果补码:0000 1010 ==> 正数,即 80/2^3=10

计算 24 >> 1
12的补码:0001 1000
-----------------------
结果补码:0000 1100 ==> 正数,即 24/2=12

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

?Flutter 那些花里胡哨的底部菜单? 进来绝不后悔

前言 前段时间,学习到了Flutter动画,正愁不知道写个项目练习巩固,突然有一天产品在群里发了一个链接【ios中的动画标签】(下面有例图),我心里直呼"好家伙",要是产品都要求做成这样,产品经理和程序员又又又又又又得打起来! 还好只是让我们参考,刚好可以拿来...
继续阅读 »

前言


前段时间,学习到了Flutter动画,正愁不知道写个项目练习巩固,突然有一天产品在群里发了一个链接【ios中的动画标签】(下面有例图),我心里直呼"好家伙",要是产品都要求做成这样,产品经理和程序员又又又又又又得打起来! 还好只是让我们参考,刚好可以拿来练习。


GitHub地址:github.com/longer96/fl…


t01.png


我们每天都会看到底部导航菜单,它们在应用程序内引导用户,允许他们在不同的tag之间快速切换。但是谁说切换标签就应该很无聊?
让我们一起探索标签栏中有趣的动画。虽然你在应用程序中可能不会使用到,但看看它的实现可能会给你提供一些灵感、设计参考。


如果恰好能给你带来一点点帮助,那是再好不过啦~ 路过的帅逼帮忙点个 star


先上几张花里胡哨的底部菜单 参考图


s01.gif


s04.gif


s03.gif


s02.gif


效果分析


咳咳,有的动效确实挺难的,需要设计师的鼎力支持,我只好选软的柿子捏


p00.png


首先我们观察,它是由文字和指示器组成的。点击之后指示器切换,文字缩放。



  • 每个tag 均分了屏幕宽度

  • 点击之后,指示器从之前的tag中部位置拉长到选中tag的中部位置

  • 指示器到达选中tag之后,长度立马向选中tag位置收缩


稍微复杂一点的是指示器的动画,看上去有3个变量:左边距、右边距、指示器宽度。
但变量越多,越不方便控制,细心想一下 我们发现其实只需要控制: 左、右边距就可以了,指示器宽度设置成自适应(或者只控制左边距和指示器宽度)


实现效果


p11.gif


其实很多类似底部菜单都可以如法炮制,指示器位于tag后面,根据不同的条件调整位置和尺寸。


d00.gif


d01.gif


d02.gif


实现一款底部菜单


常见的还有另一种展开类似的菜单,比如这样
x00.gif


咱们还是先简单分析一下



  • 由一个按钮、多个tag按钮组成

  • 点击之后,tag呈扇状展开或收缩


看上去只有2步,还是很简单的嘛


第一步:我们用帧布局叠放按钮和tag


Stack(
children: [
// tag菜单

// 菜单/关闭 按钮
]
)

第二步:管理好tag的位置
简单介绍一下Flow,Flutter中Flow是一个对子组件尺寸以及位置调整非常高效的控件。



Flow用转换矩阵在对子组件进行位置调整的时候进行了优化:在Flow定位过后,如果子组件的尺寸或者位置发生了变化,在FlowDelegate中的paintChildren()方法中调用context.paintChild 进行重绘,而context.paintChild在重绘时使用了转换矩阵,并没有实际调整组件位置。



使用起来也很简单,只需要实现FlowDelegate的paintChildren()方法,就可以自定义布局策略。所以我们需要计算好每一个tag的轨迹位置。


经过你的细心观察,你发现tag的轨迹呈半圆状展开,对 没错 就是需要翻出三角函数


sjhs.jpg


f00.png


经过你的又一次细心观察,你发现有5个tag,半圆实际可以放7个,但是为了有更好的显示效果,可以将需要展示的tag放在中间位置(过滤掉第一个和最后一个)


所以我们可以列出简单的计算


final total = context.childCount + 1;

for (int i = 0; i < childCount; i++) {
x = cos(pi * (total - i - 1) / total) * Radius;
y = sin(pi * (total - i - 1) / total) * Radius;
}

你发现太规整的圆其实并不是那么好看,优化一下



  • 将x轴半径设置为 父级约束宽度的一半

  • 将Y轴半径设置为 父级约束高度

  • 给动画加上曲线,让tag有类似回弹效果

  • 注意y轴得转换为负数,因为我们的坐标点位于下方


a003.gif


微调一下,好啦 恭喜你!
3句代码,让产品经理给你点了18杯茶


b001.png


class FlowAnimatedCircle extends FlowDelegate {
final Animation<double> animation;

/// icon 尺寸
final double iconSize = 48.0;

/// 菜单左右边距
final paddingHorizontal = 8.0;

FlowAnimatedCircle(this.animation) : super(repaint: animation);

@override
void paintChildren(FlowPaintingContext context) {
// 进度等于0,也就是收起来的时候不绘制
final progress = animation.value;
if (progress == 0) return;

final xRadius = context.size.width / 2 - paddingHorizontal;
final yRadius = context.size.height - iconSize;

// 开始(0,0)在父组件的中心
double x = 0;
double y = 0;

final total = context.childCount + 1;

for (int i = 0; i < context.childCount; i++) {
x = progress * cos(pi * (total - i - 1) / total) * xRadius;
y = progress * sin(pi * (total - i - 1) / total) * yRadius;

// 使用Matrix定位每个子组件
context.paintChild(
i,
transform: Matrix4.translationValues(
x, -y + (context.size.height / 2) - (iconSize / 2), 0),
);
}
}

@override
bool shouldRepaint(FlowAnimatedCircle oldDelegate) => false;
}

只要理解到了上面的实现,下面这3种也能很轻松完成


b000.png


b002.gif


b003.gif


最后


收集、参考实现了几个底部导航,当然可能很多地方需要优化,大家不要喷我哦



  • 有很棒的底部菜单希望推荐

  • 需要使用的,建议大家clone下来,直接引入,具体需求(如未读消息)自己添加

  • 欢迎Fork & pr贡献您的代码,大家共同学习❤

  • Android 体验下载 d.cc53.cn/sn6c

  • Web在线体验 footer.eeaarr.cn

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

用 Markdown 做 PPT,就是这么简单!

相信绝大多数朋友做 PPT(幻灯片 / Slides / Deck 等各种称呼了)都是用的 PowerPoint 或者 KeyNote 吧?功能是比较强大,但你有没有遇到过这样的痛点: 各种标题、段落的格式不统一,比如字体大小、行间距等等各个页面不太一样,然...
继续阅读 »

相信绝大多数朋友做 PPT(幻灯片 / Slides / Deck 等各种称呼了)都是用的 PowerPoint 或者 KeyNote 吧?功能是比较强大,但你有没有遇到过这样的痛点:



  • 各种标题、段落的格式不统一,比如字体大小、行间距等等各个页面不太一样,然后得用格式刷来挨个刷一下。

  • 想给 PPT 做版本控制,然后就保存了各种复制版本,比如“一版”、“二版”、“终版”、“最终版”、“最终不改版”、“最终稳定不改版”等等,想必大家都见过类似这样的场景吧。

  • 想插入代码,但是插入之后发现格式全乱了或者高亮全没了,然后不得不截图插入进去。

  • 想插入个公式,然后发现 PPT、Keynote 对 Latex 兼容不太好或者配置稍微麻烦,就只能自己重新敲一遍或者贴截图。

  • 想插入一个酷炫的交互组件,比如嵌入一个微博的网页页面实时访问、插入一个可以交互的组件、插入一个音乐播放器组件,原生的 PPT 功能几乎都不支持,这全得依赖于 PowerPoint 或者 KeyNote 来支持才行。


如果你遇到这些痛点,那请你一定要看下去。如果你没有遇到,那也请你看下去吧(拜托。


好,说回正题,我列举了那么多痛点,那这些痛点咋解决呢?


能!甚至解决方案更加轻量级,那就是用 Markdown 来做 PPT!


你试过用 Markdown 写 PPT 吗?没有吧,试试吧,试过之后你就发现上面的功能简直易如反掌。


具体怎么实现呢?


接下来,就有请今天的主角登场了!它就是 Slidev。


什么是 Slidev?


简而言之,Slidev 就是可以让我们用 Markdown 写 PPT 的工具库,基于 Node.js、Vue.js 开发。


利用它我们可以简单地把 Markdown 转化成 PPT,而且它可以支持各种好看的主题、代码高亮、公式、流程图、自定义的网页交互组件,还可以方便地导出成 pdf 或者直接部署成一个网页使用。


官方主页:sli.dev/


GitHub:github.com/slidevjs/sl…


安装和启动


下面我们就来了解下它的基本使用啦。


首先我们需要先安装好 Node.js,推荐 14.x 及以上版本,安装方法见 setup.scrape.center/nodejs


接着,我们就可以使用 npm 这个命令了。


然后我们可以初始化一个仓库,运行命令如下:


npm init slidev@latest

这个命令就是初始化一个 Slidev 的仓库,运行之后它会让我们输入和选择一些选项,如图所示:



比如上图就是先输入项目文件夹的名称,比如这里我取名叫做 slidevtest。


总之一些选项完成之后,Slidev 会在本地 3000 端口上启动,如图所示:



接着,我们就可以打开浏览器 http://localhost:3000 来查看一个 HelloWorld 版本的 PPT 了,如图所示:



我们可以点击空格进行翻页,第二页展示了一张常规的 PPT 的样式,包括标题、正文、列表等,如图所示:



那这一页的 Markdown 是什么样的呢?其实就是非常常规的 Markdown 文章的写法,内容如下:


# What is Slidev?

Slidev is a slides maker and presenter designed for developers, consist of the following features

- 📝 **Text-based** - focus on the content with Markdown, and then style them later
- 🎨 **Themable** - theme can be shared and used with npm packages
- 🧑‍💻 **Developer Friendly** - code highlighting, live coding with autocompletion
- 🤹 **Interactive** - embedding Vue components to enhance your expressions
- 🎥 **Recording** - built-in recording and camera view
- 📤 **Portable** - export into PDF, PNGs, or even a hostable SPA
- 🛠 **Hackable** - anything possible on a webpage

<br>
<br>

Read more about [Why Slidev?](https://sli.dev/guide/why)

是不是?我们只需要用同样格式的 Markdown 语法就可以轻松将其转化为 PPT 了。


快捷键操作


再下一页介绍了各种快捷键的操作,这个就很常规了,比如点击空格、上下左右键来进行页面切换,如图所示:



更多快捷键的操作可以看这里的说明:sli.dev/guide/navig…,一些简单的快捷键列举如下:



  • f:切换全屏

  • right / space:下一动画或幻灯片

  • left:上一动画或幻灯片

  • up:上一张幻灯片

  • down:下一张幻灯片

  • o:切换幻灯片总览

  • d:切换暗黑模式

  • g:显示“前往...”


代码高亮


接下来就是代码环节了,因为 Markdown 对代码编写非常友好,所以展示自然也不是问题了,比如代码高亮、代码对齐等都是常规操作,如图所示:



那左边的代码定义就直接这么写就行了:


# Code

Use code snippets and get the highlighting directly![^1]

```ts {all|2|1-6|9|all}
interface User {
id: number
firstName: string
lastName: string
role: string
}

function updateUser(id: number, update: User) {
const user = getUser(id)
const newUser = {...user, ...update}
saveUser(id, newUser)
}
```

由于是 Markdown,所以我们可以指定是什么语言,比如 TypeScript、Python 等等。


网页组件


接下来就是非常酷炫的环节了,我们还可以自定义一些网页组件,然后展示出来。


比如我们看下面的一张图。左边就呈现了一个数字计数器,点击左侧数字就会减 1,点击右侧数字就会加 1;另外图的右侧还嵌入了一个组件,这里显示了一个推特的消息,通过一个卡片的形式呈现了出来,不仅仅可以看内容,甚至我们还可以点击下方的喜欢、回复、复制等按钮来进行一些交互。


这些功能在网页里面并不稀奇,但是如果能做到 PPT 里面,那感觉就挺酷的。



那这一页怎么做到的呢?这个其实是引入了一些基于 Vue.js 的组件,本节对应的 Markdown 代码如下:


# Components

<div grid="~ cols-2 gap-4">
<div>

You can use Vue components directly inside your slides.

We have provided a few built-in components like `<Tweet/>` and `<Youtube/>` that you can use directly. And adding your custom components is also super easy.

```html
<Counter :count="10" />
```

<!-- ./components/Counter.vue -->
<Counter :count="10" m="t-4" />

Check out [the guides](https://sli.dev/builtin/components.html) for more.

</div>
<div>

```html
<Tweet id="1390115482657726468" />
```

<Tweet id="1390115482657726468" scale="0.65" />

</div>
</div>

这里我们可以看到,这里引入了 Counter、Tweet 组件,而这个 Counter 就是 Vue.js 的组件,代码如下:


<script setup lang="ts">
import { ref } from 'vue'

const props = defineProps({
count: {
default: 0,
},
})

const counter = ref(props.count)
</script>

<template>
<div flex="~" w="min" border="~ gray-400 opacity-50 rounded-md">
<button
border="r gray-400 opacity-50"
p="2"
font="mono"
outline="!none"
hover:bg="gray-400 opacity-20"
@click="counter -= 1"
>
-
</button>
<span m="auto" p="2">{{ counter }}</span>
<button
border="l gray-400 opacity-50"
p="2"
font="mono"
outline="!none"
hover:bg="gray-400 opacity-20"
@click="counter += 1"
>
+
</button>
</div>
</template>

这就是一个标准的基于 Vue.js 3.x 的组件,都是标准的 Vue.js 语法,所以如果我们要添加想要的组件,直接自己写就行了,什么都能实现,只要网页能支持的,统统都能写!


主题定义


当然,一些主题定制也是非常方便的,我们可以在 Markdown 文件直接更改一些配置就好了,比如就把 theme 换个名字,整个主题样式就变了,看如下的对比图:



上面就是一些内置主题,当然我们也可以去官方文档查看一些别人已经写好的主题,见:sli.dev/themes/gall…


另外我们自己写主题也是可以的,所有的主题样式都可以通过 CSS 等配置好,想要什么就可以有什么,见:sli.dev/themes/writ…


公式和图表


接下来就是一个非常强大实用的功能,公式和图表,支持 Latex、流程图,如图所示:




比如上面的 Latex 的源代码就是这样的:


Inline $\sqrt{3x-1}+(1+x)^2$

Block
$$
\begin{array}{c}

\nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} &
= \frac{4\pi}{c}\vec{\mathbf{j}} \nabla \cdot \vec{\mathbf{E}} & = 4 \pi \rho \\

\nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} & = \vec{\mathbf{0}} \\

\nabla \cdot \vec{\mathbf{B}} & = 0

\end{array}
$$

其语法也是和 Latex 一样的。


其背后是怎么实现的呢?其实是因为 Slidev 默认集成了 Katex 这个库,见:katex.org/,有了 Katex 的加持,所有公式的显示都不是事。


页面分隔


有的朋友就好奇了,既然是用 Markdown 写 PPT,那么每一页之间是怎么分割的呢?


其实很简单,最常规的,用三条横线分割就好了,比如:


---
layout: cover
---

# 第 1 页

This is the cover page.

---

# 第 2 页

The second page

当然,除了使用三横线,我们还可以使用更丰富的定义模式,可以给每一页制定一些具体信息,就是使用两层三横线。


比如这样:


---
theme: seriph
layout: cover
background: 'https://source.unsplash.com/1600x900/?nature,water'
---

上面这样的配置可以替代三横线,是另一种可以用作页面分隔的写法,借助这种写法我们可以定义更多页面的具体信息。


备注


当然我们肯定也想给 PPT 添加备注,这个也非常简单,通过注释的形式写到 Markdown 源文件就好了:


---
layout: cover
---

# 第 1 页

This is the cover page.

<!-- 这是一条备注 -->

这里可以看到其实就是用了注释的特定语法。


演讲者头像


当然还有很多酷炫的功能,比如说,我们在讲 PPT 的时候,可能想同时自己也出镜,Slidev 也可以支持。


因为开的是网页,而网页又有捕捉摄像头的功能,所以最终效果可以是这样子:



是的没错!右下角就是演讲者的个人头像,它被嵌入到了 PPT 中!是不是非常酷!


演讲录制


当然,Slidev 还支持演讲录制功能,因为它背后集成了 WebRTC 和 RecordRTC 的 API,一些录制配置如下所示:



所以,演讲过程的录制完全不是问题。


具体的操作可以查看:sli.dev/guide/recor…


部署


当然用 Slidev 写的 PPT 还可以支持部署,因为这毕竟就是一个网页。


而且部署非常简单和轻量级,因为这就是一些纯静态的 HTML、JavaScript 文件,我们可以轻松把它部署到 GitHub Pages、Netlify 等站点上。


试想这么一个场景:别人在演讲之前还在各种拷贝 PPT,而你打开了一个浏览器直接输入了一个网址,PPT 就出来了,众人惊叹,就问你装不装逼?


具体的部署操作可以查看:sli.dev/guide/hosti…


让我们看几个别人已经部署好的 PPT,直接网页打开就行了:



就是这么简单方便。


版本控制


什么?你想实现版本控制,那再简单不过了。


Markdown 嘛,配合下专业版本管理工具 Git,版本控制再也不是难题。


总结


以上就是对 Slidev 的简单介绍,确实不得不说有些功能真的非常实用,而且我本身特别喜欢 Markdown 和网页开发,所以这个简直对我来说太方便了。


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

Flutter 保证数据操作原子性

Flutter 保证数据操作原子性 Flutter 是单线程架构,按道理理说,Flutter 不会出现 Java 的多线程相关的问题。 但在我使用 Flutter 过程中,却发现 Flutter 依然会存在数据操作原子性的问题。 其实 Flutter 中存在...
继续阅读 »

Flutter 保证数据操作原子性


Flutter 是单线程架构,按道理理说,Flutter 不会出现 Java 的多线程相关的问题。


但在我使用 Flutter 过程中,却发现 Flutter 依然会存在数据操作原子性的问题。



其实 Flutter 中存在多线程的(Isolate 隔离池),只是 Flutter 中的多线程更像 Java 中的多进程,因为 Flutter 中线程不能像 Java 一样,可以两个线程去操作同一个对象。


我们一般将计算任务放在 Flutter 单独的线程中,例如一大段 Json 数据的解析,可以将解析计算放在单独的线程中,然后将解析完后的 Map<String, dynamic> 返回到主线程来用。



Flutter单例模式


在 Java 中,我们一般喜欢用单例模式来理解 Java 多线程问题。这里我们也以单例来举例,我们先来一个正常的:


class FlutterSingleton {
static FlutterSingleton? _instance;

/// 将构造方法声明成私有的
FlutterSingleton._();

static FlutterSingleton getInstance() {
if (_instance == null) {
_instance = FlutterSingleton._();
}
return _instance!;
}
}

由于 Flutter 是单线程架构的, 所以上述代码是没有问题的。


问题示例


但是, 和 Java 不同的是, Flutter 中存在异步方法。


做 App 开发肯定会涉及到数据持久化,Android 开发应该都熟悉 SharedPreferences,Flutter 中也存在 SharedPreferences 库,我们就以此来举例。同样实现单例模式,只是这次无可避免的需要使用 Flutter 中的异步:


class SPSingleton {
static SPSingleton? _instance;

String? data;

/// 将构造方法声明成私有的
SPSingleton._fromMap(Map<String, dynamic> map) : data = map['data'];

static Future<SPSingleton> _fromSharedPreferences() async {
// 模拟从 SharedPreferences 中读取数据, 并以此来初始化当前对象
Map<String, String> map = {'data': 'mockData'};
await Future.delayed(Duration(milliseconds: 10));
return SPSingleton._fromMap(map);
}

static Future<SPSingleton> getInstance() async {
if (_instance == null) {
_instance = await SPSingleton._fromSharedPreferences();
}
return _instance!;
}
}

void main() async {
SPSingleton.getInstance().then((value) {
print('instance1.hashcode = ${value.hashCode}');
});
SPSingleton.getInstance().then((value) {
print('instance2.hashcode = ${value.hashCode}');
});
}

运行上面的代码,打印日志如下:


instance1.hashcode = 428834223
instance2.hashcode = 324692380

可以发现,我们两次调用 SPSingleton.getInstance() 方法,分别创建了两个对象,说明上面的单例模式实现有问题。


我们来分析一下 getInstance() 方法:


static Future<SPSingleton> getInstance() async {
if (_instance == null) { // 1
_instance = await SPSingleton._fromSharedPreferences(); //2
}
return _instance!;
}

当第一次调用 getInstance() 方法时,代码在运行到 1 处时,发现 _instance 为 null, 就会进入 if 语句里面执行 2 处, 并因为 await 关键字挂起, 并交出代码的执行权, 直到被 await 的 Future 执行完毕,最后将创建的 SPSingleton 对象赋值给 _instance 并返回。


当第二次调用 getInstance() 方法时,代码在运行到 1 处时,可能会发现 _instance 还是为 null (因为 await SPSingleton._fromSharedPreferences() 需要 10ms 才能返回结果), 然后和第一次调用 getInstance() 方法类似, 创建新的 SPSingleton 对象赋值给 _instance。


最后导致两次调用 getInstance() 方法, 分别创建了两个对象。


解决办法


问题原因知道了,那么该怎样解决这个问题呢?


究其本质,就是 getInstance() 方法的执行不具有原子性,即:在一次 getInstance() 方法执行结束前,不能执行下一次 getInstance() 方法。


幸运的是, 我们可以借助 Completer 来将异步操作原子化,下面是借助 Completer 改造后的代码:


import 'dart:async';

class SPSingleton {
static SPSingleton? _instance;
static Completer<bool>? _monitor;

String? data;

/// 将构造方法声明成私有的
SPSingleton._fromMap(Map<String, dynamic> map) : data = map['data'];

static Future<SPSingleton> _fromSharedPreferences() async {
// 模拟从 SharedPreferences 中读取数据, 并以此来初始化当前对象
Map<String, String> map = {'data': 'mockData'};
await Future.delayed(Duration(milliseconds: 10));
return SPSingleton._fromMap(map);
}

static Future<SPSingleton> getInstance() async {
if (_instance == null) {
if (_monitor == null) {
_monitor = Completer<bool>();
_instance = await SPSingleton._fromSharedPreferences();
_monitor!.complete(true);
} else {
// Flutter 的 Future 支持被多次 await
await _monitor!.future;
_monitor = null;
}
}
return _instance!;
}
}

void main() async {
SPSingleton.getInstance().then((value) {
print('instance1.hashcode = ${value.hashCode}');
});
SPSingleton.getInstance().then((value) {
print('instance2.hashcode = ${value.hashCode}');
});
}

我们再次分析一下 getInstance() 方法:


static Future<SPSingleton> getInstance() async {
if (_instance == null) { // 1
if (_monitor == null) { // 2
_monitor = Completer<bool>(); // 3
_instance = await SPSingleton._fromSharedPreferences(); // 4
_monitor!.complete(true); // 5
} else {
// Flutter 的 Future 支持被多次 await
await _monitor!.future; //6
_monitor = null;
}
}
return _instance!; // 7
}

当第一次调用 getInstance() 方法时, 1 处和 2 处都会判定为 true, 然后进入执行到 3 处创建一个的 Completer 对象, 然后在 4 的 await 处挂起, 并交出代码的执行权, 直到被 await 的 Future 执行完毕。


此时第二次调用的 getInstance() 方法开始执行,1 处同样会判定为 true, 但是到 2 处时会判定为 false, 从而进入到 else, 并因为 6 处的 await 挂起, 并交出代码的执行权;


此时, 第一次调用 getInstance() 时的 4 处执行完毕, 并执行到 5, 并通过 Completer 通知第二次调用的 getInstance() 方法可以等待获取代码执行权了。


最后,两次调用 getInstance() 方法都会返回同一个 SPSingleton 对象,以下是打印日志:


instance1.hashcode = 786567983
instance2.hashcode = 786567983


由于 Flutter 的 Future 是支持多次 await 的, 所以即便是连续 n 次调用 getInstance() 方法, 从第 2 到 n 次调用会 await 同一个 Completer.future, 最后也能返回同一个对象。



Flutter任务队列


虽然我们经常拿单例模式来解释说明 Java 多线程问题,可这并不代表着 Java 只有在单例模式时才有多线程问题。


同样的,也并不代表着 Flutter 只有在单例模式下才有原子操作问题。


问题示例


我们同样以数据持久化来举例,只是这次我们以数据库操作来举例。


我们在操作数据库时,经常会有这样的需求:如果数据库表中存在这条数据,就更新这条数据,否则就插入这条数据。


为了实现这样的需求,我们可能会先从数据库表中查询数据,查询到了就更新,没查询到就插入,代码如下:


class Item {
int id;
String data;
Item({
required this.id,
required this.data,
});
}

class DBTest {
DBTest._();
static DBTest instance = DBTest._();
bool _existsData = false;
Future<void> insert(String data) async {
// 模拟数据库插入操作,10毫秒过后,数据库中才有数据
await Future.delayed(Duration(milliseconds: 10));
_existsData = true;
print('执行了插入');
}

Future<void> update(String data) async {
// 模拟数据库更新操作
await Future.delayed(Duration(milliseconds: 10));
print('执行了更新');
}

Future<Item?> selected(int id) async {
// 模拟数据库查询操作
await Future.delayed(Duration(milliseconds: 10));
if (_existsData) {
// 数据库中有数据才返回
return Item(id: 1, data: 'mockData');
} else {
// 数据库没有数据时,返回null
return null;
}
}

/// 先从数据库表中查询数据,查询到了就更新,没查询到就插入
Future<void> insertOrUpdate(int id, String data) async {
Item? item = await selected(id);
if (item == null) {
await insert(data);
} else {
await update(data);
}
}
}

void main() async {
DBTest.instance.insertOrUpdate(1, 'data');
DBTest.instance.insertOrUpdate(1, 'data');
}

我们期望的输出日志为:


执行了插入
执行了更新

但不幸的是, 输出的日志为:


执行了插入
执行了插入

原因也是异步方法操作数据, 不是原子操作, 导致逻辑异常。


也许我们也可以效仿单例模式的实现,利用 Completer 将 insertOrUpdate() 方法原子化。


但对于数据库操作是不合适的,因为我们可能还有其它需求,比如说:调用插入数据的方法,然后立即从数据库中查询这条数据,发现找不到。


如果强行使用 Completer,那么到最后,可能这个类中会出现一大堆的 Completer ,代码难以维护。


解决办法


其实我们想要的效果是,当有异步方法在操作数据库时,别的操作数据的异步方法应该阻塞住,也就是同一时间只能有一个方法来操作数据库。我们其实可以使用任务队列来实现数据库操作的需求。


我这里利用 Completer 实现了一个任务队列:


import 'dart:async';
import 'dart:collection';

/// TaskQueue 不支持 submit await submit, 以下代码就存在问题
///
/// TaskQueue taskQueue = TaskQueue();
/// Future<void> task1(String arg)async{
/// await Future.delayed(Duration(milliseconds: 100));
/// }
/// Future<void> task2(String arg)async{
/// 在这里submit时, 任务会被添加到队尾, 且当前方法任务不会结束
/// 添加到队尾的任务必须等到当前方法任务执行完毕后, 才能继续执行
/// 而队尾的任务必须等当前任务执行完毕后, 才能执行
/// 这就导致相互等待, 使任务无法进行下去
/// 解决办法是, 移除当前的 await, 让当前任务结束
/// await taskQueue.submit(task1, arg);
/// }
///
/// taskQueue.submit(task2, arg);
///
/// 总结:
/// 被 submit 的方法的内部如果调用 submit 方法, 此方法不能 await, 否则任务队列会被阻塞住
///
/// 如何避免此操作, 可以借鉴以下思想:
/// 以数据库操作举例, 有个save方法的逻辑是插入或者更新(先查询数据库select,再进行下一步操作);
/// sava方法内部submit,并且select也submit, 就容易出现submit await submit的情况
///
/// 我们可以这样操作,假设当前类为 DBHelper:
/// 将数据库的增,删,查,改操作封装成私有的 async 方法, 且私有方法不能使用submit
/// DBHelper的公有方法, 可以调用自己的私有 async 方法, 但不能调用自己的公有方法, 公有方法可以使用submit
/// 这样就不会存在submit await submit的情况了
class TaskQueue {
/// 提交任务
Future<O> submit<A, O>(Function fun, A? arg) async {
if (!_isEnable) {
throw Exception('current TaskQueue is recycled.');
}
Completer<O> result = new Completer<O>();

if (!_isStartLoop) {
_isStartLoop = true;
_startLoop();
}

_queue.addLast(_Runnable<A, O>(
fun: fun,
arg: arg,
completer: result,
));
if (!(_emptyMonitor?.isCompleted ?? true)) {
_emptyMonitor?.complete();
}

return result.future;
}

/// 回收 TaskQueue
void recycle() {
_isEnable = false;
if (!(_emptyMonitor?.isCompleted ?? true)) {
_emptyMonitor?.complete();
}
_queue.clear();
}

Queue<_Runnable> _queue = Queue<_Runnable>();
Completer? _emptyMonitor;
bool _isStartLoop = false;
bool _isEnable = true;

Future<void> _startLoop() async {
while (_isEnable) {
if (_queue.isEmpty) {
_emptyMonitor = new Completer();
await _emptyMonitor!.future;
_emptyMonitor = null;
}

if (!_isEnable) {
// 当前TaskQueue不可用时, 跳出循环
return;
}

_Runnable runnable = _queue.removeFirst();
try {
dynamic result = await runnable.fun(runnable.arg);
runnable.completer.complete(result);
} catch (e) {
runnable.completer.completeError(e);
}
}
}
}

class _Runnable<A, O> {
final Completer<O> completer;
final Function fun;
final A? arg;

_Runnable({
required this.completer,
required this.fun,
this.arg,
});
}


由于 Flutter 中的 future 不支持暂停操作, 一旦开始执行, 就只能等待执行完。


所以这里的任务队列实现是基于方法的延迟调用来实现的。



TaskQueue 的用法示例如下:


void main() async {
Future<void> test1(String data) async {
await Future.delayed(Duration(milliseconds: 20));
print('执行了test1');
}

Future<String> test2(Map<String, dynamic> args) async {
await Future.delayed(Duration(milliseconds: 10));
print('执行了test2');
return 'mockResult';
}

TaskQueue taskQueue = TaskQueue();
taskQueue.submit(test1, '1');
taskQueue.submit(test2, {
'data1': 1,
'data2': '2',
}).then((value) {
print('test2返回结果:${value}');
});

await Future.delayed(Duration(milliseconds: 200));
taskQueue.recycle();
}
/*
执行输出结果如下:

执行了test1
执行了test2
test2返回结果:mockResult
*/


值得注意的是: 这里的 TaskQueue 不支持 submit await submit, 原因及示例代码已在注释中说明,这里不再赘述。



为了避免出现 submit await submit 的情况,我代码注释中也做出了建议(假设当前类为 DBHelper):




  • 将数据库的增、删、查、改操作封装成私有的异步方法, 且私有异步方法不能使用 submit;




  • DBHelper 的公有方法, 可以调用自己的私有异步方法, 但不能调用自己的公有异步方法, 公有异步方法可以使用 submit;




这样就不会出现 submit await submit 的情况了。


于是,上述的数据库操作示例代码就变成了以下的样子:


class Item {
int id;
String data;
Item({
required this.id,
required this.data,
});
}

class DBTest {
DBTest._();
static DBTest instance = DBTest._();
TaskQueue _taskQueue = TaskQueue();
bool _existsData = false;
Future<void> _insert(String data) async {
// 模拟数据库插入操作,10毫秒过后,数据库才有数据
await Future.delayed(Duration(milliseconds: 10));
_existsData = true;
print('执行了插入');
}

Future<void> insert(String data) async {
await _taskQueue.submit(_insert, data);
}

Future<void> _update(String data) async {
// 模拟数据库更新操作
await Future.delayed(Duration(milliseconds: 10));
print('执行了更新');
}

Future<void> update(String data) async {
await _taskQueue.submit(_update, data);
}

Future<Item?> _selected(int id) async {
// 模拟数据库查询操作
await Future.delayed(Duration(milliseconds: 10));
if (_existsData) {
// 数据库中有数据才返回
return Item(id: 1, data: 'mockData');
} else {
// 数据库没有数据时,返回null
return null;
}
}

Future<Item?> selected(int id) async {
return await _taskQueue.submit(_selected, id);
}

/// 先从数据库表中查询数据,查询到了就更新,没查询到就插入
Future<void> _insertOrUpdate(Map<String, dynamic> args) async {
int id = args['id'];
String data = args['data'];
Item? item = await _selected(id);
if (item == null) {
await _insert(data);
} else {
await _update(data);
}
}

Future<Item?> insertOrUpdate(int id, String data) async {
return await _taskQueue.submit(_insertOrUpdate, {
'id': id,
'data': data,
});
}
}

void main() async {
DBTest.instance.insertOrUpdate(1, 'data');
DBTest.instance.insertOrUpdate(1, 'data');
}

输出日志也变成了我们期望的样子:


执行了插入
执行了更新

总结




  • Flutter 异步方法修改数据时, 一定要注意数据操作的原子性, 不能因为 Flutter 是单线程架构,就忽略多个异步方法竞争导致数据异常的问题。




  • Flutter 保证数据操作的原子性,也有可行办法,当逻辑比较简单时,可直接使用 Completer,当逻辑比较复杂时,可以考虑使用任务队列。




另外,本文中的任务队列实现有很大的缺陷,不支持 submit await submit,否则整个任务队列会被阻塞住。


如果诸位有其它的任务队列实现方式,或者保证数据操作原子性的方法,欢迎留言。


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

Mybatis的where标签,竟然还有这么多不知道的!

背景 在上篇文章,我们系统地学习了where 1=1 相关的知识点,大家可以回看《不要再用where 1=1了!有更好的写法!》这篇文章。文章中涉及到了Mybatis的替代方案,有好学的朋友在评论区有朋友问了基于Mybatis写法的问题。 于是,就有了这篇文章...
继续阅读 »

背景


在上篇文章,我们系统地学习了where 1=1 相关的知识点,大家可以回看《不要再用where 1=1了!有更好的写法!》这篇文章。文章中涉及到了Mybatis的替代方案,有好学的朋友在评论区有朋友问了基于Mybatis写法的问题。


于是,就有了这篇文章。本篇文章会将Mybatis中where标签的基本使用形式、小技巧以及容易踩到的坑进行总结梳理,方便大家更好地实践运用d


原始的手动拼接


在不使用Mybatis的where标签时,我们通常是根据查询条件进行手动拼接,也就是用到了上面提到的where 1=1的方式,示例如下:


  <select id="selectSelective" resultType="com.secbro.entity.User">
  select * from t_user
  where 1=1
  <if test="username != null and username != ''">
    and username = #{username}
  </if>
  <if test="idNo != null and idNo != ''">
    and id_no = #{idNo}
  </if>
</select>

这种方式主要就是为了避免语句拼接错误,出现类似如下的错误SQL:


select * from t_user where and username = 'Tom' and id = '1001';
select * from t_user where and id = '1001';

当添加上1=1时,SQL语句便是正确的了:


select * from t_user where 1=1 and username = 'Tom' and id = '1001';
select * from t_user where 1=1 and id = '1001';

这个我们之前已经提到过,多少对MySQL数据库的有一定的压力。因为1=1条件的优化过滤是需要MySQL做的。如果能够将这部分放到应用程序来做,就减少了MySQL的压力。毕竟,应用程序是可以轻易地横向扩展的。


Mybatis where标签的使用


为了能达到MySQL性能的调优,我们可以基于Mybatis的where标签来进行实现。where标签是顶层的遍历标签,需要配合if标签使用,单独使用无意义。通常有下面两种实现形式。


方式一:


  <select id="selectSelective" resultType="com.secbro.entity.User">
  select * from t_user
  <where>
    <if test="username != null and username != ''">
      username = #{username}
    </if>
    <if test="idNo != null and idNo != ''">
      and id_no = #{idNo}
    </if>
  </where>
</select>

方式二:


  <select id="selectSelective" resultType="com.secbro.entity.User">
  select * from t_user
  <where>
    <if test="username != null and username != ''">
      and username = #{username}
    </if>
    <if test="idNo != null and idNo != ''">
      and id_no = #{idNo}
    </if>
  </where>
</select>

仔细观察会发现,这两种方式的区别在于第一if条件中的SQL语句是否有and


这里就涉及到where标签的两个特性:



  • 第一,只有if标签有内容的情况下才会插入where子句;

  • 第二,若子句的开通为 “AND” 或 “OR”,where标签会将它替换去除;


所以说,上面的两种写法都是可以了,Mybatis的where标签会替我们做一些事情。


但需要注意的是:where标签只会 智能的去除(忽略)首个满足条件语句的前缀。所以建议在使用where标签时,每个语句都最好写上 and 前缀或者 or 前缀,否则像以下写法就会出现问题:


  <select id="selectSelective" resultType="com.secbro.entity.User">
  select * from t_user
  <where>
    <if test="username != null and username != ''">
      username = #{username}
    </if>
    <if test="idNo != null and idNo != ''">
      id_no = #{idNo}
    </if>
  </where>
</select>

生成的SQL语句如下:


select * from t_user      WHERE username = ?  id_no = ?

很显然,语法是错误的。


因此,在使用where标签时,建议将所有条件都添加上and或or


进阶:自定义trim标签


上面使用where标签可以达到拼接条件语句时,自动去掉首个条件的and或or,那么如果是其他自定义的关键字是否也能去掉呢?


此时,where标签就无能为力了,该trim标签上场了,它也可以实现where标签的功能。


  <select id="selectSelective" resultType="com.secbro.entity.User">
  select * from t_user
  <trim prefix="where" prefixOverrides="and | or ">
    <if test="username != null and username != ''">
      and username = #{username}
    </if>
    <if test="idNo != null and idNo != ''">
      and id_no = #{idNo}
    </if>
  </trim>
</select>

将上面基于where标签的写改写为trim标签,发现执行效果完全一样。而且trim标签具有了更加灵活的自定义性。


where语句的坑


另外,在使用where语句或其他语句时一定要注意一个地方,那就是:注释的使用。


先来看例子:


  <select id="selectSelective" resultType="com.secbro.entity.User">
  select * from t_user
  <where>
    <if test="username != null and username != ''">
      and username = #{username}
    </if>
    <if test="idNo != null and idNo != ''">
      /* and id_no = #{idNo}*/
      and id_no = #{idNo}
    </if>
  </where>
</select>

上述SQL语句中添加了 /**/的注释,生成的SQL语句为:


select * from t_user WHERE username = ? /* and id_no = ?*/ and id_no = ? 

执行时,直接报错。


还有一个示例:


  <select id="selectSelective" resultType="com.secbro.entity.User">
  select * from t_user
  <where>
    <if test="username != null and username != ''">
      -- and username = #{username}
      and username = #{username}
    </if>
    <if test="idNo != null and idNo != ''">
      and id_no = #{idNo}
    </if>
  </where>
</select>

生成的SQL语句为:


select * from t_user WHERE -- and username = ? and username = ? and id_no = ? 

同样会导致报错。


这是因为我们使用 XML 方式配置 SQL 时,如果在 where 标签之后添加了注释,那么当有子元素满足条件时,除了 < !-- --> 注释会被 where 忽略解析以外,其它注释例如 // 或 /**/ 或 -- 等都会被 where 当成首个子句元素处理,导致后续真正的首个 AND 子句元素或 OR 子句元素没能被成功替换掉前缀,从而引起语法错误。


同时,个人在实践中也经常发现因为在XML中使用注释不当导致SQL语法错误或执行出错误的结果。强烈建议,非必要,不要在XML中注释掉SQL,可以通过版本管理工具来追溯历史记录和修改。


小结


本文基于Mybatis中where标签的使用,展开讲了它的使用方式、特性以及拓展到trim标签的替代作用,同时,也提到了在使用时可能会出现的坑。内容虽然简单,但如果能够很好地实践、避免踩坑也是能力的体现。


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

算法题每日一练:汉明距离

一、问题描述 两个整数之间的 汉明距离 指的是这两个数字对应二进制位不同的位置的数目。 给你两个整数 x 和 y,计算并返回它们之间的汉明距离。 题目链接:汉明距离。 二、题目要求 样例 1 输入: x = 1...
继续阅读 »

一、问题描述


两个整数之间的 汉明距离 指的是这两个数字对应二进制位不同的位置的数目。


给你两个整数 x 和 y,计算并返回它们之间的汉明距离。


题目链接:汉明距离


二、题目要求


样例 1


输入: x = 1, y = 4
输出: 2
解释:
1 (0 0 0 1)
4 (0 1 0 0)
↑ ↑
上面的箭头指出了对应二进制位不同的位置。

样例 2


输入: x = 3, y = 1
输出: 1

考察


1.位运算简单题型
2.建议用时5~20min

三、问题分析


本题是位运算的第3题,没了解过位运算相关知识点可以看这一篇文章,讲解比较详细:


算法题每日一练---第45天:位运算


什么是汉明距离,简单来讲就是将两个10进制数字转换成2进制数字之后,统计不同位置上面1的个数,那如何将这题逐渐向位运算靠拢呢?


不同位置,啥叫不同位置,不就是如果相同位置都是1或0,那么就不计数。只有当相同位置一个为1,另一个为0时才开始计数,这不就是位运算的异或计算吗?


7.png


四、编码实现


class Solution {
public:
int hammingDistance(int x, int y) {
int i,ans=0;//初始化数据
for(i=0;i<32;i++)//32位循环判断
{
if((x^y)&1<<i)//与计算并且开始查询1的个数
{
ans++;//计数器++
}
}
return ans;//输出结果
}
};

五、测试结果


1.png


2.png


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

细说Android apk四代签名:APK v1、APK v2、APK v3、APK v4

简介 大部分开发者对apk签名还停留在APK v2,对APK v3和APK v4了解很少,而且网上大部分文章讲解的含糊不清,所以根据官网文档重新整理一份。 apk签名从APK v1到APK v2改动很大,是颠覆性的,而APK v3只是对APK v2的一次升级,...
继续阅读 »

简介


大部分开发者对apk签名还停留在APK v2,对APK v3和APK v4了解很少,而且网上大部分文章讲解的含糊不清,所以根据官网文档重新整理一份。


apk签名从APK v1到APK v2改动很大,是颠覆性的,而APK v3只是对APK v2的一次升级,APK v4则是一个补充。


本篇文章主要参考Android各版本改动:
developer.android.google.cn/about/versi…


APK v1


就是jar签名,apk最初的签名方式,大家都很熟悉了,签名完之后是META-INF 目录下的三个文件:MANIFEST.MF、CERT.SF、CERT.RSA。


MANIFEST.MF

MANIFEST.MF中是apk种每个文件名称和摘要SHA1(或者 SHA256),如果是目录则只有名称


CERT.SF

CERT.SF则是对MANIFEST.MF的摘要,包括三个部分:



  • SHA1-Digest-Manifest-Main-Attributes:对 MANIFEST.MF 头部的块做 SHA1(或者SHA256)后再用 Base64 编码

  • SHA1-Digest-Manifest:对整个 MANIFEST.MF 文件做 SHA1(或者 SHA256)后再用 Base64 编码

  • SHA1-Digest:对 MANIFEST.MF 的各个条目做 SHA1(或者 SHA256)后再用 Base64 编码


CERT.RSA

CERT.RSA是将CERT.SF通过私钥签名,然后将签名以及包含公钥信息的数字证书一同写入 CERT.RSA 中保存


通过这三层校验来确保apk中的每个文件都不被改动。


APK v2


官方说明:source.android.google.cn/security/ap…


APK 签名方案 v2 是在 Android 7.0 (Nougat) 中引入的。为了使 APK 可在 Android 6.0 (Marshmallow) 及更低版本的设备上安装,应先使用 JAR 签名功能对 APK 进行签名,然后再使用 v2 方案对其进行签名。


APK v1的缺点就是META-INF目录下的文件并不在校验范围内,所以之前多渠道打包等都是通过在这个目录下添加文件来实现的。


APK 签名方案 v2 是一种全文件签名方案,该方案能够发现对 APK 的受保护部分进行的所有更改,从而有助于加快验证速度并增强完整性保证。


使用 APK 签名方案 v2 进行签名时,会在 APK 文件中插入一个 APK 签名分块,该分块位于“ZIP 中央目录”部分之前并紧邻该部分。在“APK 签名分块”内,v2 签名和签名者身份信息会存储在 APK 签名方案 v2 分块中。


image.png


通俗点说就是签名信息不再以文件的形式存储,而是将其转成二进制数据直接写在apk文件中,这样就避免了APK v1的META-INF目录的问题。


在 Android 7.0 及更高版本中,可以根据 APK 签名方案 v2+ 或 JAR 签名(v1 方案)验证 APK。更低版本的平台会忽略 v2 签名,仅验证 v1 签名。


image.png


APK v3


官方说明:source.android.google.cn/security/ap…


APK 签名方案 v3 是在 Android 9 中引入的。


Android 9 支持 APK 密钥轮替,这使应用能够在 APK 更新过程中更改其签名密钥。为了实现轮替,APK 必须指示新旧签名密钥之间的信任级别。为了支持密钥轮替,我们将 APK 签名方案从 v2 更新为 v3,以允许使用新旧密钥。v3 在 APK 签名分块中添加了有关受支持的 SDK 版本和 proof-of-rotation 结构的信息。


简单来说APK v3就是为了Andorid9的APK 密钥轮替功能而出现的,就是在v2的基础上增加两个数据块来存储APK 密钥轮替所需要的一些信息,所以可以看成是v2的升级。具体结构见官网说明即可。


APK 密钥轮替功能可以参考:developer.android.google.cn/about/versi…



具有密钥轮转的 APK 签名方案


Android 9 新增了对 APK Signature Scheme v3 的支持。该架构提供的选择可以在其签名块中为每个签名证书加入一条轮转证据记录。利用此功能,应用可以通过将 APK 文件过去的签名证书链接到现在签署应用时使用的证书,从而使用新签名证书来签署应用。


developer.android.google.cn/about/versi…



注:运行 Android 8.1(API 级别 27)或更低版本的设备不支持更改签名证书。如果应用的 minSdkVersion 为 27 或更低,除了新签名之外,可使用旧签名证书来签署应用。


详细了解如何使用 apksigner 轮转密钥参考:developer.android.google.cn/studio/comm…


在 Android 9 及更高版本中,可以根据 APK 签名方案 v3、v2 或 v1 验证 APK。较旧的平台会忽略 v3 签名而尝试验证 v2 签名,然后尝试验证 v1 签名。


image.png


APK v4


官方说明:source.android.google.cn/security/ap…


APK 签名方案 v4 是在 Android 11 中引入的。


Android 11 通过 APK 签名方案 v4 支持与流式传输兼容的签名方案。v4 签名基于根据 APK 的所有字节计算得出的 Merkle 哈希树。它完全遵循 fs-verity 哈希树的结构(例如,对salt进行零填充,以及对最后一个分块进行零填充。)Android 11 将签名存储在单独的 .apk.idsig 文件中。v4 签名需要 v2 或 v3 签名作为补充。


APK v4同样是为了新功能而出现的,这个新功能就是ADB 增量 APK 安装,可以参考Android11 功能和 API 概览:
developer.android.google.cn/about/versi…



ADB 增量 APK 安装


在设备上安装大型(2GB 以上)APK 可能需要很长的时间,即使应用只是稍作更改也是如此。ADB(Android 调试桥)增量 APK 安装可以安装足够的 APK 以启动应用,同时在后台流式传输剩余数据,从而加速这一过程。如果设备支持该功能,并且您安装了最新的 SDK 平台工具,adb install 将自动使用此功能。如果不支持,系统会自动使用默认安装方法。


developer.android.google.cn/about/versi…




运行以下 adb 命令以使用该功能。如果设备不支持增量安装,该命令将会失败并输出详细的解释。


adb install --incremental


在运行 ADB 增量 APK 安装之前,您必须先为 APK 签名并创建一个 APK 签名方案 v4 文件。必须将 v4 签名文件放在 APK 旁边,才能使此功能正常运行。


developer.android.google.cn/about/versi…



因为需要流式传输,所以需要将文件分块,对每一块进行签名以便校验,使用的方式就是Merkle 哈希树(http://www.kernel.org/doc/html/la… v4就是做这部分功能的。所以APK v4与APK v2或APK v3可以算是并行的,所以APK v4签名后还需要 v2 或 v3 签名作为补充。


运行 adb install --incremental 命令时,adb 会要求 .apk.idsig 文件存在于 .apk 旁边(所以APK v4的签名文件.apk.idsig并不会打包进apk文件中


默认情况下,它还会使用 .idsig 文件尝试进行增量安装;如果此文件缺失或无效,该命令会回退到常规安装。


image.png


总结


综上,可以看到APK v4是面向ADB即开发调试的,而如果我们没有签名变动的需求也可以不考虑APK v3,所以目前国内大部分还停留在APK v2。


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

用了这么久Java,我竟不知道Java是值传递

泪目,想不到我用了这么久的Java编程语言,竟然使用的是值传递。本篇文章我们将带大家搞清楚Java值传递的特性。前言是不是有人会这样认为Java在传递参数时,参数如果是普通类型,那就是值传递,如果是对象,那就是引用传递。如果是这样认为那就大错特错了。下面我们一...
继续阅读 »

泪目,想不到我用了这么久的Java编程语言,竟然使用的是值传递。本篇文章我们将带大家搞清楚Java值传递的特性。

前言

是不是有人会这样认为Java在传递参数时,参数如果是普通类型,那就是值传递,如果是对象,那就是引用传递。

如果是这样认为那就大错特错了。

下面我们一起看一下Java的值传递特性。

Java 真的是值传递

检验是值传递最好的方法就是交换方法,我们在交换方法中将两个对象的引用互换,再看一下原来的值会不会被影响。

我们可以看如下例子:

  • 首先创建两个用户一个张三,一个李四。
  • 然后我们调用身份互换方法swap
  • 在身份互换方法中确认两人是否身份互换成功(可以看到结果是成功的)
  • 如果是引用传递的话我们创建的两个用户已经完成了身份互换
  • 但实际结果是两个用户
package com.zhj.interview;

public class Test15 {

   public static void main(String[] args) {
       User ZhangSan = new User("张三");
       User LiSi = new User("李四");
       System.out.println("开始交换---------------------------------");
       swap(ZhangSan, LiSi);
       System.out.println("交换结束---------------------------------");
       System.out.println("交换后ZhangSan的名字:" + ZhangSan.name);
       System.out.println("交换后LiSi的名字:" + LiSi.name);
  }

   private static void swap(User ZhangSan, User LiSi){
       User user;
       user = ZhangSan;
       ZhangSan = LiSi;
       LiSi = user;
       System.out.println("交换后ZhangSan的名字:" + ZhangSan.name);
       System.out.println("交换后LiSi的名字:" + LiSi.name);
  }
}
class User{
   String name;
   User(String name) {
       this.name = name;
  }
}

运行结果:

开始交换---------------------------------
交换后ZhangSan的名字:李四
交换后LiSi的名字:张三
交换结束---------------------------------
交换后ZhangSan的名字:张三
交换后LiSi的名字:李四

如下图所示,值传递的意思是将引用进行值传递,希望下图能对大家理解上有所帮助。

image.png

造成错觉的原因

造成我们认为Java在传递参数时,参数如果是普通类型,那就是值传递,如果是对象,那就是引用传递的原因就是,我们将传入的对象修改后,开始创建的对象内容也会改变。

最好的理解方式就是:

我们传入的参数只是钥匙,值传递就是配两把钥匙给方法,引用传递就是把自己的钥匙给方法;

如果把自己的钥匙给方法,方法内交换了钥匙之后,我们自己的钥匙也就被掉包了(引用传递);

如果是额外配两把钥匙给对方,方法内交换的钥匙是新配的钥匙,不会影响我们自己的钥匙(值传递);

需要注意的是,无论是使用自己的钥匙,还是后配的钥匙,打开房门,修改房屋内结构,这个变化是不受影响钥匙影响的,因为改变的不是钥匙。

在JVM中对象引用与对象信息的体现,如下图所示。

image.png


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

收起阅读 »

神奇的二进制

一. 前言 太极生两仪,两仪生四相,四相生八卦,八卦定吉凶 中国的八卦与西方的二进制其实原理上有极其相似的一面. 但是演化和推算方面, 八卦更胜一筹. 阴: 0, 阳: 1; 老阴: 00, ...
继续阅读 »

一. 前言


太极生两仪,两仪生四相,四相生八卦,八卦定吉凶


中国的八卦与西方的二进制其实原理上有极其相似的一面. 但是演化和推算方面, 八卦更胜一筹.


阴: 0, 阳: 1;


老阴: 00, 少阳: 10, 少阴: 01, 老阳: 11;


坤: 000, 艮: 100, 坎: 010,巽 :110, ,震: 001, 离:101, 兑: 011, 乾: 111;


image.png


二.正文.


计算机就是一个二进制的逻辑机器


计算机三大件:主板,CPU,内存; 而最主要的就是cpu, cpu的主要组成就是可以理解为晶体管, 晶体管也就相当于是个开关, 即打电报中的._. 这种二进制的简单也造就了计算机快的特性.


**计算机只会二进制, 那怎样才能让计算机有用呢? **


那就是约定大于配置


先是字符集



  • ASCII字符集

  • ISO 8859-1字符集

  • GB2312字符集

  • GBK字符集

  • utf8字符集


数字, 以int为例子:



















十进制二进制
2013140000 0000 0000 0011 0001 0010 0110 0010
-2013141000 0000 0000 0011 0001 0010 0110 0010

二进制在源码的巧妙使用(我们以java示例)


很多源码中使用了二进制来区分各个状态, 而且状态可以组合的形式.
主要使用的是&|的计算来实现的.


0&0 = 0;           0|0 = 0;
0&1 = 0; 0|1 = 1;
1&0 = 0; 1|0 = 1;
1&1 = 1; 1|1 = 1;

例如:


Streams
public int characteristics() {
// 既包含所有的特性
return Spliterator.ORDERED | Spliterator.SIZED | Spliterator.SUBSIZED |
Spliterator.IMMUTABLE | Spliterator.NONNULL |
Spliterator.DISTINCT | Spliterator.SORTED;
}

public int characteristics() {
if (beforeSplit) {
// 两个共同都有的特性: aSpliterator.characteristics() & bSpliterator.characteristics()
return aSpliterator.characteristics() & bSpliterator.characteristics()
& ~(Spliterator.DISTINCT | Spliterator.SORTED
| (unsized ? Spliterator.SIZED | Spliterator.SUBSIZED : 0));
}
else {
return bSpliterator.characteristics();
}
}

public interface Spliterator<T> {
public static final int ORDERED = 0x00000010;

public static final int DISTINCT = 0x00000001;

public static final int SORTED = 0x00000004;

public static final int SIZED = 0x00000040;

public static final int NONNULL = 0x00000100;

public static final int IMMUTABLE = 0x00000400;

public static final int CONCURRENT = 0x00001000;

public static final int SUBSIZED = 0x00004000;
}

二进制在一些记录中的头信息巧妙使用


mysql中 COMPACT行格式 中一条记录.
image.png


JVM中一个对象的二进制的巧妙使用
image.png


二进制解决一些有趣的问题



有1000桶酒,其中1桶有毒。而一旦吃了,毒性会在1周后发作。现在我们用小老鼠做实验,要在1周后找出那桶毒酒,问最少需要多少老鼠。



这个问题其实大家都见到过. 解题思路也巧妙地使用了二进制来完成.




  • 把10只老鼠依次排队为:0 - 9,关在笼子里




  • 把1000瓶药依次编号,并换算成二进制,如 8 = 1000,根据二进制中出现1的位数对相应位置小鼠喝药。 因为我们选择的是10只


    小鼠,2^10 = 1024 > 1000,能够保证所有的编号的酒都能被喂小鼠而不会遗漏被转为二进制的1的位.




  • 长时间的等待,等待小鼠的死亡




  • 把死亡小鼠的依次记录,然后10位的二进制中, 死亡老鼠编号对应10位中的二进制数为1, 没死的老鼠编号对应10为中的二进制数为0, 即知毒药的编号.


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

二叉搜索树怎么这么难做呢?

首先我们需要去了解一下, 二叉搜索树的性质:对于 BST的每一个节点 node,左子树节点的值都比 node的值要小,右子树的值都要比node的值大。对于BST的每一个节点node, 它的左侧和右侧都是 BST...
继续阅读 »

首先我们需要去了解一下, 二叉搜索树的性质:

  1. 对于 BST的每一个节点 node,左子树节点的值都比 node的值要小,右子树的值都要比node的值大。
  2. 对于BST的每一个节点node, 它的左侧和右侧都是 BST

这里需要说明一下的是,从刷算法的角度来讲,还有一个重要的性质: BST的中序遍历的结果是有序的(升序)

那么我们开始吧, 最近一直拖更,很不好意思.

230. 二叉搜索树中第K个小的元素

image.png

思路梳理 如果这么理解, 产生一个升序的序列(数组),那么我们可以根据第k个小的元素 1,2,3,4,5这里第1个小的元素在哪里?那不就是1嘛.刚好借助于BST中序遍历的结果。是不是就很巧了呢。

代码实现

/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @param {number} k
* @return {number}
*/
// BST 的两个性质 左边的比右边的大
// 中序遍历的结果是 升序的

var kthSmallest = function(root, k) {
// 返回结果 记录结果
let res = 0;
// 记录升序之后的排名
let count = 0;
function traverse(root, k) {
// base case
if (root === null) return

traverse(root.left, k)
//中序
count++;
if (count === k) {
res = root.val;
return res;
}
traverse(root.right, k)
}

//定义函数
traverse(root, k)
return res;
}

538. 把二叉搜索树转换为累加树

image.png

image.png

思路梳理

其实这道题需要需要一个反过来的想法, BST的中序遍历的结果是升序的, 如果我们稍微作为一下修改呢?

function traverse(root) {

traverse(root.right)
// 中序遍历的结果是不是就成了 逆序(降序)的方式排列呢
// 这里做累加的结果
traverse(root.left)
}
traverse(root)

通过逆向的思考方式, 我从 8开始,也就是右子树开始依次去右中左的方式去遍历和累加和,是不是会更好一点呢,你可以思考一下,仔细去看一下那颗实例树:

代码实现

/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {TreeNode}
*/

var convertBST = function(root) {
// 升序变为降序
// sum 记录累加和
let sum = 0;

function traverse(root) {
// base case
if (root === null) return

traverse(root.right)
// 维护累加值
sum += root.val;
root.val = sum;
traverse(root.left)
}
traverse(root)
return root;
}

1038. 从搜索树到更大的和树

image.png

image.png

思路梳理

这道题和上题完全一样的思路和写法这里就不做赘述了

代码实现

/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {TreeNode}
*/
var bstToGst = function(root) {
// 升序变降序
// 记录累加和
let sum = 0;

function traverse(root) {
if (root === null) return;

traverse(root.right)
// 累加和
sum += root.val;
root.val = sum;
traverse(root.left)
}
traverse(root)
return root;
}

654. 最大二叉树

image.png

image.png

image.png

思路梳理

其实对于构建一颗二叉树最关键的是把root也就是把根节点找出来就好了。 那么我们就需要去遍历去找出数组中最大的值maxVal,把根节点root找出来了。 那么就可以把 maxVal左边和右边的数组进行递归,作为root的左右子树

// 伪代码
var constructMaximumBinaryTree = function([3,2,1,6,0,5]) {
// 找出数组中的最大值
let root = TreeNode(6)

let root.left = constructMaximumBinaryTree([3,2,1])
let root.right = constructMaximumBinaryTree([0,5])
return root;
}

这里我们需要的注意的是如何去构建一个递归函数: 参数是如何要求的? 因为需要 分离左子树和右子树 我们需要不断的确认子树的开始和结束的位置

function build (nums, start, end) {
// base case
if (left > right) return;

let maxValue = -1, index = -1; // index 是最大值的索引值是重要的分离条件
// find
for (let i = start; i < end; i++) {
if (nums[i] > maxValue) {
maxValue = nums[i];
index = i;
}
}
// 此时去构建树 root;
let root = new TreeNode(maxValue);

root.left = build(nums, start, index - 1)
root.right = build(nums, index + 1, end)
// 别忘了返回
return root;
}

这里其实我们已经写出来的本题的核心代码了,需要我们自己耐心的组合一下就好了啊

代码实现

/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {number[]} nums
* @return {TreeNode}
*/
var constructMaximumBinaryTree = function(nums) {
let root = build(nums, 0, nums.length-1)
return root;
}

function build(nums, start, end) {
// base case
if (left > right) return;

let maxVal = -1, index = -1;
for (let i = start; i < end; i++) {
if (nums[i] > maxVal) {
maxVal = nums[i]
index = i;
}
}

// 制作树
let root = new TreeNode(maxVal)

root.left = build(nums, start, index - 1)
root.right = build(nums, index + 1, end)
return root;
}

98. 验证二叉搜索树

image.png

思路分析

BST类似的代码逻辑就是利用 左小右大的特性

function BST(root, target) {
if (root.val === target) {
// 找到目标做点什么呢
}
if (root.val < target) {
// 右 比 root 大
BST(root.right, target)
}
if (root.val > target) {
// 左 比 root 小
BST(root.left, target)
}
}

那么对于我们去验证一棵树是不是合法的是需要注意一些事情的:

  1. 对于每一个节点 root代码值都需要去检查它的左右孩子节点是否都是符合左小右大的原则
  2. 从 BST的定义出发的话, root的整个左子树都要小于 root.val, 整个右子树都要大于 root.val

但是就会产生一个问题,就是对于某个节点,它只能管得了自己的左右子节点,如何去把约束关系传递给左右子树呢?

left.val < root.val < right.val

是不是可以借助于这种关系去约束他们呢?

主函数的定义

function isValidBST(root, null, null)

对于左子树来说 每一个左子树的 val 都需要满足于 min.val < val < root.val

isValidBST(root.left, min, root) 

那么同样的对于 每一个右子树的 val 都需要满足于 root.val < val < max.val

isValidBST(root.right, root, max)

代码实现

/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @return {boolean}
*/
var isValidBST = function(root) {
return isValid(root, null, null)
}

function isValid(root, min, max) {
// 怎么就合法了呢? 找完还一直没报false 不就是满足条件的吗
if (root === null) return true;
// 比最小的还小 不合法
if (min !== null && root.val <= min.val) return false;
// 比最大的还大。不合法
if (max !== null && root.val >= max.val) return false;

return isvalid(root.left, min, root) && isValid(root.right, root, max)
}

700. 二叉搜索树中的搜索

image.png

思路分析

其实,你看像不像我们上面提到的二叉树的思维模版呢? 题目要求是 找到对应的val的节点, 并返回以该节点为根的子树 其实就是可以理解为 返回当前节点就好了 会带有以 该节点为根的一颗子树的。

代码实现

/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @param {number} val
* @return {TreeNode}
*/

var searchBST = function(root, val) {
// base case
if (root === null) return null;

if (root.val === val) {
return root;
}

if (root.val < val) {
searchBST(root.right)
}
if (root.val > val) {
searchBST(root.left)
}

}

701. 二叉搜索树中的插入操作

image.png

image.png

思路分析

这里 需要注意的一点就是你你怎么样才能插入呢?如果是 target === val说明不是空位置, 那就插不进去啊, 所以我们要插入的位置肯定是一个空位置, 你如果认真分析过 这个过程 你对 base case 有没有一个全新的认识呢? 就是这里

代码实现

/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @param {number} val
* @return {TreeNode}
*/
var insertIntoBST = function(root, val) {
// base case
if (root === null) // 插入
return new TreeNode(val)

// if (root === val) 不需要了
if (root.val < val) {
insertIntoBST(root.right)
}
if (root.val > val) {
insertIntoBST(root.left)
}
}

450. 删除二叉搜索树中的节点

image.png

思路分析

先上一个基本的模版:

var deleteNode = function(root, key) {
// 基本的摹本
if (root === null) return null;

if (root.val === key) {
// 删除操作
}

if (root.val < key) {
deleteNode(root.right, val)
}
if (root.val > key) {
deleteNode(root.left, val)
}
}

当 root.val === key的时候 需要我们去执行一个删除的逻辑了

case1: 没有子孩子

if (root.left === null && root.right) reture null;

case2: 只有一个非空节点的情况,那么需要这个非空的节点接替自己的位置

if (root.left === null) return root.right;
if (root.right === null) return root.left;

case3: 如果有两个节点就麻烦了,我们就需要把 左子树中最大的或者是右子树中最小的元素来接替自己, 我们采用第二种方式

if (root.left !== null && root.right !== null) {
// 找到右子树中的最小的节点
let minNode = getMin(root.right)
// 把当前的值 替换为最小的值
root.val = minNode;
// 我们还需要把右子树中最小的值 删除掉
root.right = deleteNode(root.right, minNode.val)
}

获取右子树中最小的元素

// 其实这里的最小的就是 左子树
function getMin(node) {
while (node.left !== null) 继续找下面的左子树
node = node.left;
return node; // 没有发现和前端的原型链好像啊
}

代码实现

/**
* Definition for a binary tree node.
* function TreeNode(val, left, right) {
* this.val = (val===undefined ? 0 : val)
* this.left = (left===undefined ? null : left)
* this.right = (right===undefined ? null : right)
* }
*/
/**
* @param {TreeNode} root
* @param {number} key
* @return {TreeNode}
*/
var deleteNode = function(root, key) {
// 基本的摹本
if (root === null) return null;

if (root.val === key) {
// 删除操作
// case1, 2
if (root.left === null) return root.right;
if (root.right === null) return root.left;
// case 3
let minNode = getMin(root.right);
root.val = minNode.val; // 一加
root.right = deleteNode(root.right, minNode); // 一减
}

if (root.val < key) {
deleteNode(root.right, val)
}
if (root.val > key) {
deleteNode(root.left, val)
}

function getMin(node) {
while(node.left !== null) node = node.left;
return node;
}
}


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

收起阅读 »

学不好Lambda,能学好Kotlin吗

嗯,当然 不能 进入正题,Kotlin中,高阶函数的身影无处不在,听上去高端大气上档次的高阶函数,简化一点讲,其实就是Lambda + 函数。 如果,Lambda学不好,就会导致高阶函数学不好,就会导致协程等等一系列的Kotlin核心学不好,Kotlin自然就...
继续阅读 »

嗯,当然


不能


进入正题,Kotlin中,高阶函数的身影无处不在,听上去高端大气上档次的高阶函数,简化一点讲,其实就是Lambda + 函数。


如果,Lambda学不好,就会导致高阶函数学不好,就会导致协程等等一系列的Kotlin核心学不好,Kotlin自然就一知半解了。所以,下面,一起来学习吧。


开始一个稍微复杂一点的实现


需求如下:传入一个参数,打印该参数,并且返回该参数


分析


乍看需求,这还不简单,一个print加一个return不就完事了,但是如果用Lambda,该怎么写呢?


val myPrint = { str: String ->
print("str is $str")
str
}


  • 这里划第一个重点,Lambda的最后一行作为返回值输出。此时,如果直接打印myPrint,是可以直接输出的


fun main() {
println(myPrint("this is kotlin"))
}

image.png


结果和预想一致。如果对这种函数的写法结构有什么疑惑的,可以查看juejin.cn/post/701173…


String.()


一脸懵逼,这是啥玩意?(此处应有表情 尼克杨问号脸)


先写个例子看看


val testStr : String.() -> Unit = {
print(this)
}


  • 官方一点解释,在.和()之间没有函数名,所以这是给String增加了一个匿名的扩展函数,这个函数的功能是打印String。在括号内,也就是Lambda体中,会持有String本身,也就是this。怎么调用呢?如下:


fun main() {
"hello kotlin".testStr()
}
// 执行结果:hello kotlin


  • 此外这里还有一个重点:扩展函数是可以全局调用的

  • 扩展函数有啥用?举个例子,如果对Glide提供的方法不满意,可以直接扩展一个Glide.xxx函数供自己调用,在xxx函数内部,可以取到this,也就是Glide本身。

  • 有兴趣可以看一下Compose的源码,原来扩展函数还可以这么用


终极形态


先看代码


val addInt : Int.(Int) -> String = {
"两数相加的结果是${this + it}"
}

用已有的知识分析一下:



  • Int.():匿名的扩展函数

  • this:当前的Int,也就是调用这个扩展函数的对象

  • "两数相加的结果是${this + it}" : Lambda的最后一行,也就返回值


如何调用


一般有如下两种调用方式:


fun main() {
println(addInt(1,2))
println(1.addInt(2))
}


  • 第二种更加符合规范,之所以可以有第一种写法,是因为this会默认作为第一个参数

  • 此处可以记住一个知识点,扩展了某一个函数,扩展函数内部的this就是被扩展的函数本身


Kotlin函数返回值那些事


在Kotlin函数中,如果不指定函数的返回值类型,则默认为Unit


fun output() {println("helle kotlin")}


  • 上述函数的返回值为Unit类型


当函数体中出现return的时候,则需要手动为函数指定类型


fun output2() : Int {
return 0
}


  • 返回Int类型的0,需要手动指定函数的返回值类型,否则报错


如果是以下的函数,那么返回值为?


fun output3() = {}


  • 此处的返回值为() -> Unit,可省略,写全了,就是如下的样子:


fun output3() : () -> Unit = {}


  • 此处函数返回函数,已经是高阶函数的范畴了


如果函数接着套一个函数呢,比如


fun output4() = run { println("hello kotlin") }


  • 虽说run是一个函数,但是此处的返回值就不是() -> Unit

  • 此处的返回就是run的返回值,但是run是什么?


@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}


  • run的作用就是执行内部的函数,在这里就是println方法。

  • run的返回自是R,也就是泛型,具体一点就是println的返回值,这里println的返回值是Unit,所以可以得出上面的output4的返回值就是Unit。

  • 这里如果不是很懂的话,可以看一个简单一点的例子


fun output5() = run {true}


  • 此处,函数的返回值就是true的类型,Boolen


函数中套一个函数怎么传参呢


刚刚的例子中,知道了怎么写一个函数中套函数,那么其中嵌套得函数怎么传参呢


fun output6() = {a: Int ->  println("this is $a")}


  • a为参数,函数是println,所以output6的返回值类型为(Int) -> Unit

  • 如果需要调用的话,需要这么写:


output6()(1)

最后一个重点:在写Lambda的时候,记住换行


几种函数写法的区别


fun a()


常见的函数


val a = {}


a是一个变量,只不过是一个接受了匿名函数的变量,可以执行这个函数,和第一种现象一致。


这里的a还可以赋值给另一个变量 val a2 = a,但是函数本身不能直接赋给一个变量,可以使用::,让函数本身变成函数的引用


--end---


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

已开源!Flutter 流畅度优化组件 Keframe

列表流畅度优化这是一个通用的流畅度优化方案,通过分帧渲染优化由构建导致的卡顿,例如页面切换或者复杂列表快速滚动的场景。代码中 example 运行在 VIVO X23(骁龙 660),在相同的滚动操作下优化前后 200 帧采集数据指标对比(录屏在文章最后):优...
继续阅读 »

列表流畅度优化

这是一个通用的流畅度优化方案,通过分帧渲染优化由构建导致的卡顿,例如页面切换或者复杂列表快速滚动的场景。

代码中 example 运行在 VIVO X23(骁龙 660),在相同的滚动操作下优化前后 200 帧采集数据指标对比(录屏在文章最后):

优化前优化后
优化前优化后

监控工具来自:fps_monitor,指标详细信息:页面流畅度不再是谜!调试神器开箱即用,Flutter FPS检测工具

  • 流畅:一帧耗时低于 18ms
  • 良好:一帧耗时在 18ms-33ms 之间
  • 轻微卡顿:一帧耗时在 33ms-67ms 之间
  • 卡顿:一帧耗时大于 66.7ms

采用分帧优化后,卡顿次数从 平均 33.3 帧出现了一帧,降低到 200 帧中仅出现了一帧,峰值也从 188ms 降低到 90ms。卡顿现象大幅减轻,流畅帧占比显著提升,整体表现更流畅。下方是详细数据。

优化前优化后
平均多少帧出现一帧卡顿33.3200
平均多少帧出现一帧轻微卡顿8.666.7
最大耗时188.0ms90.0ms
平均耗时27.0ms19.4ms
流畅帧占比40%64.5%

页面切换流畅度提升

在打开一个页面或者 Tab 切换时,系统会渲染整个页面并结合动画完成页面切换。对于复杂页面,同样会出现卡顿掉帧。借助分帧组件,将页面的构建逐帧拆解,通过 DevTools 中的性能工具查看。切换过程的峰值由 112.5ms 降低到 30.2 ms,整体切换过程更加流畅。

image.pngimage.png

实际优化合集:Keframe 流畅度优化实践合集

如何使用?

项目依赖:

在 pubspec.yaml 中添加 keframe 依赖

dependencies:
keframe: version

组件仅区分非空安全与空安全版本

非空安全使用: 1.0.2

空安全版本使用: 2.0.2

github 地址:github.com/LianjiaTech…

pub 查看:pub.dev/packages/ke…

Dont forget star ~

快速上手:

如下图所示

image.png

假如现在页面由 A、B、C、D 四部分组成,每部分耗时 10ms,在页面时构建为 40ms。使用分帧组件 FrameSeparateWidget 嵌套每一个部分。页面构建时会在第一帧渲染简单的占位,在后续四帧内分别渲染 A、B、C、D。

对于列表,在每一个 item 中嵌套 FrameSeparateWidget,并将 ListView 嵌套在 SizeCacheWidget 内即可。

image.png


构造函数说明

FrameSeparateWidget :分帧组件,将嵌套的 widget 单独一帧渲染

类型参数名是否必填含义
Keykey
intindex分帧组件 id,使用 SizeCacheWidget 的场景必传,SizeCacheWidget 中维护了 index 对应的 Size 信息
Widgetchild实际需要渲染的 widget
WidgetplaceHolder占位 widget,尽量设置简单的占位,不传默认是 Container()

SizeCacheWidget:缓存子节点中,分帧组件嵌套的实际 widget 的尺寸信息

类型参数名是否必填含义
Keykey
Widgetchild子节点中如果包含分帧组件,则缓存实际的 widget 尺寸
intestimateCount预估屏幕上子节点的数量,提高快速滚动时的响应速度

方案设计与分析:

卡顿的本质,就是 单帧的绘制时间过长。基于此自然衍生出两种思路解决:

1、减少一帧的绘制耗时,因为导致耗时过长的原因有很多,比如不合理的刷新,或者绘制时间过长,都有可能,需要具体问题具体分析,后面我会分享一些我的优化经验。

2、在不对耗时优化下,将一帧的任务拆分到多帧内,保证每一帧都不超时。这也是本组件的设计思路,分帧渲染。

如下图所示:

image.png

原理并不复杂,问题在于如何在 Flutter 中实践这一机制。

因为涉及到帧与系统的调度,自然联想到看 SchedulerBinding 中有无现成的 API。

发现了 scheduleTask 方法,这是系统提供的一个执行任务的方法,但这个方法存在两个问题:

  • 1、其中的渲染任务是优先级进行堆排序,而堆排序是不稳定排序,这会导致任务的执行顺序并非 FIFO。从效果上来看,就是列表不会按照顺序渲染,而是会出现跳动渲染的情况

  • 2、这个方法本身存在调度问题,我已经提交 issue 与 pr,不过一直卡在单元测试上,如果感兴趣可以以在这里交流谈论。

fix: Tasks scheduled through 'SchedulerBinding.instance.scheduleTask'… #82781

最终,参考这个设计结合 endOfFrame 方法的使用,完成了分帧队列。整个渲染流程变为下图所示:

image.png

对于列表构建场景来说,假设屏幕上能显示五个 item。首先在第一帧的时候,列表会渲染 5 个占位的 Widget,同时添加 5 个高优先级任务到队列中,这里的任务可以是简单的将占位 Widget 和实际 item进行替换,也可通过渐变等动画提升体验。在后续的五帧中占位 Widget 依次被替换成实际的列表 item。

在 ListView流畅度翻倍!!Flutter卡顿分析和通用优化方案 这篇文章中有更加详细的分析。


一些展示效果(Example 说明请查看 Github

卡顿的页面往往都是由多个复杂 widget 同时渲染导致。通过为复杂的 widget 嵌套分帧组件 FrameSeparateWidget。渲染时,分帧组件会在第一帧同时渲染多个 palceHolder,之后连续的多帧内依次渲染复杂子项,以此提升页面流畅度。

例如 example 中的优化前示例:

ListView.builder(
itemCount: childCount,
itemBuilder: (c, i) => CellWidget(
color: i % 2 == 0 ? Colors.red : Colors.blue,
index: i,
),
)

其中 CellWidget 高度为 60,内部嵌套了三个 TextField 的组件(整体构建耗时在 9ms 左右)。

优化仅需为每一个 item 嵌套分帧组件,并为其设置 placeHolder(placeHolder 尽量简单,样式与实际 item 接近即可)。

在列表情况下,给 ListView 嵌套 SizeCacheWidget,同时建议将预加载范围 cacheExtent 设置大一点,例如 500(该属性默认为 250),提升慢速滑动时候的体验。

Screenrecording_20210611_194905.gif (占位与实际列表项不一致时,首次渲染抖动,二次渲染正常)

此外,也可以给 item 嵌套透明度/位移等动画,优化视觉上的效果。

效果如下图:

Screenrecording_20210315_133310.gifScreenrecording_20210315_133848.gif

分帧的成本

当然分帧方案也非十全十美,在我看来主要有两点成本:

1、额外的构建开销:整个构建过程的构建消耗由「n * widget消耗 」变成了「n *( widget + 占位)消耗 + 系统调度 n 帧消耗」。可以看出,额外的开销主要由占位的复杂度决定。如果占位只是简单的 Container,测试后发现整体构建耗时大概提升在 15 % 左右。这种额外开销对于当下的移动设备而言,成本几乎可以不计。

2、视觉上的变化:如同上面的演示,组件会将 item 分帧渲染,页面在视觉上出现占位变成实际 widget 的过程。但其实由于列表存在缓存区域(建议将缓存区调大),在高端机或正常滑动情况下用户并无感知。而在中低端设备上快速滑动能感觉到切换的过程,但比严重顿挫要好。


优化前后对比演示

注:gif 帧率只有20

优化前优化后
优化前优化后

最后:一点点思考

列表优化篇到此告一段落,在整个开源实践过程中,有两点感触较深:

「点」与「面」的关系

我们在思考技术方案的时候可以由「点」到「面」,站在一个较高视野去想问题的本质。

而在执行的时候则需要由「面」到「点」的进行逐级拆分,抓住问题的关键节点,并且拟定进度计划,逐步破解。

很多时候,这种向上和向下的逻辑思维才是我们的核心竞争力

以不变应万变

对于未知的东西,我们往往会过度的将它想复杂。在一开始分析列表构建原理的时候,我也苦于无从下手,走了很多弯路。但其实对于 Flutter 这套 「UI」 框架而言,核心仍然在于三棵树的构建机制

在这套体系内,抓住不变的东西,无论是生命周期、路由等等问题都可以从里面找到答案。我之前也有过总结:Flutter 核心渲染机制 与 Flutter路由设计与源码解析 。


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

收起阅读 »

GitHub 限制俄罗斯使用代码,「开源无国界」是伪命题吗?

东欧世界的战火终究还是烧到了开源世界。2022 年 3 月 2 日,Github 官方发文称,会遵守美国政府的相关规定,限制俄罗斯通过 Github 获得军事技术能力。除了 GitHub,更多的开源社区也加入了这场运动: Node.js 官网在其首页加入了声援...
继续阅读 »

东欧世界的战火终究还是烧到了开源世界。

2022 年 3 月 2 日,Github 官方发文称,会遵守美国政府的相关规定,限制俄罗斯通过 Github 获得军事技术能力。除了 GitHub,更多的开源社区也加入了这场运动: Node.js 官网在其首页加入了声援乌克兰的标语;知名前端框架 React 也在官网中加入了声援乌克兰的横幅;俄罗斯「国民操作系统」Sailfish OS 的制造商 Jolla 公司正试图切断与俄罗斯的联系…… 美国当地时间的昨天,React 在 Github 的代码仓库涌入了来自全世界的政见不合的开发者, 彼此通过提交 issue 的方式发表激烈意见,直到官方入场才得以平息。 在「科技无国界」、「艺术无国界」、「体育无国界」被大家认为不存在的今天,「开源无国界」也成为伪命题了吗?

pic_60edcf2e.png

1 开源软件开发者有国界

公元 1 世纪,哲学家普鲁塔克提出一个问题:如果忒修斯船上的木头被逐渐替换,直到所有的木头都不是原来的木头,那这艘船还是原来的那艘船吗? 今天的开源圈,类似的忒修斯悖论依然存在。 开源软件的代码量和复杂度上已远超当年,一个开源项目可能会使用或集成多种开源组件,同一个开源项目可能也会有成千上万的开发者参与进来,贡献他们的智慧。 当一个开源项目中的代码被逐渐替换,甚至所有的代码都不是原来的代码,那这个项目的所有者还是最初的作者吗? 就目前的共识来看,这个问题是肯定的。代码原作者对代码拥有所有权,可以自由决定谁可以使用自己的代码。这些天,就有开发者发表声明,禁止俄罗斯境内的程序员使用其开源的代码。 也就是说,假设今天有一个俄罗斯程序员,参与到了某个开源项目的建设中,甚至成为了其中的主要贡献者。但如果项目的原作者,认为项目被俄罗斯政府运用在了军事领域,决定禁止俄罗斯境内的个人或组织使用这些开源代码,这位程序员就只能看着自己的努力付之东流了。 所以,开源开发者是有国界的。

2 开源平台和社区有国界

除了开源作者拥有限制他人使用开源代码的权利,在开源托管平台眼中,开发者同样会因为其所处的国家而享有不同的待遇。 2019 年,全球最大开源代码托管平台 GitHub 出于美国贸易管制法律要求,对伊朗、克里米亚的开发者用户进行了限制,甚至是封禁账号。 还是在这一年,全球第二大开源代码托管平台 GitLab 宣布了一个「封锁令」,禁止给中国和俄罗斯公民提供 offer,不久后,GitLab 风险与全球合规总监对这种歧视性和报复性的行为不满而辞职。 开源代码可以在许可证的范围内自由传播,但保管开源代码的公司,却不得不以实体的方式,遵守所在地的法律法规。即便国家政策不以黑纸白字的方式严格约束,在政治正确、舆论环境等多方因素影响下,代码托管平台同样难以保持中立。 这次 GitHub 发布公告后,一种声音再次被提起,我们要建设一个属于国内开发者自己的代码托管平台,要摆脱对对国外开源社区的依赖。 所以,开源社区也是有国界的的。

3 开源有国界,开源精神无国界

当大家反驳各种「科学/艺术/体育无国界」时,说的最多的就是「科学家/艺术家/运动员有国界」。不可否认,程序员之间也同样有国界,这也是为什么大家在 Github 的 React 代码仓库争论的原因。 当人们带着对同一件事情的不同看法,抱着想要说服对方的目的,怀着累积已久的情绪,来到同一个空间,结局必然是惨烈的。这些年国内外各大社交平台的分化,已经无数次证明了这一点。 但之所以开源社区能保持一份相对的平和与冷静,和大家来到这里的目的,以及交流的方式是有密不可分的关系的。 开源最初很简单,一个人创造了一个东西,拿出来分享给大家,大家通过自由使用这个东西,为世界创造价值的同时,收获快乐和回报。带着这个美好的初衷,开源走过了几十年岁月,发展成为数字世界的基石,并还在不断壮大中。 人性总有善的一面,也有恶的一面。但在开源大家庭里,大家收获善意并用善意回报,在这个过程中慢慢学会同理、尊重、分享等美好品质。哪怕彼此因为出身和经历不同,会有各种各样的摩擦,但最终能带着共同的愿景,放下偏见,互相成长。 也许你会因为同情支持某一方,也许他会出于同理而支持另一方,但究其本质,都是出于善。 科技有国界,开源也许也有国界。 如果真有什么东西是无国界的,那就是人与人之间的善意。

作者:李磊
来源:https://mp.weixin.qq.com/s/Atc2lrpGddKmueymDJzuBA

收起阅读 »

后端一次给你10万条数据,如何优雅展示,到底考察我什么?

前言 大家好,我是林三心,用最通俗的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心,今天跟大家来唠唠嗑,如果后端真的返回给前端10万条数据,咱们前端要怎么优雅地展示出来呢?(哈哈假设后端真的能传10万条数据到前端) 前置工作 先把前置工作给做好,后...
继续阅读 »

前言


大家好,我是林三心,用最通俗的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心,今天跟大家来唠唠嗑,如果后端真的返回给前端10万条数据,咱们前端要怎么优雅地展示出来呢?(哈哈假设后端真的能传10万条数据到前端)


image.png


前置工作


先把前置工作给做好,后面才能进行测试


后端搭建


新建一个server.js文件,简单起个服务,并返回给前端10w条数据,并通过nodemon server.js开启服务



没有安装nodemon的同学可以先全局安装npm i nodemon -g



// server.js

const http = require('http')
const port = 8000;

http.createServer(function (req, res) {
// 开启Cors
res.writeHead(200, {
//设置允许跨域的域名,也可设置*允许所有域名
'Access-Control-Allow-Origin': '*',
//跨域允许的请求方法,也可设置*允许所有方法
"Access-Control-Allow-Methods": "DELETE,PUT,POST,GET,OPTIONS",
//允许的header类型
'Access-Control-Allow-Headers': 'Content-Type'
})
let list = []
let num = 0

// 生成10万条数据的list
for (let i = 0; i < 100000; i++) {
num++
list.push({
src: 'https://p3-passport.byteacctimg.com/img/user-avatar/d71c38d1682c543b33f8d716b3b734ca~300x300.image',
text: `我是${num}号嘉宾林三心`,
tid: num
})
}
res.end(JSON.stringify(list));
}).listen(port, function () {
console.log('server is listening on port ' + port);
})
复制代码

前端页面


先新建一个index.html


// index.html

// 样式
<style>
* {
padding: 0;
margin: 0;
}
#container {
height: 100vh;
overflow: auto;
}
.sunshine {
display: flex;
padding: 10px;
}
img {
width: 150px;
height: 150px;
}
</style>

// html部分
<body>
<div id="container">
</div>
<script src="./index.js"></script>
</body>
复制代码

然后新建一个index.js文件,封装一个AJAX函数,用来请求这10w条数据


// index.js

// 请求函数
const getList = () => {
return new Promise((resolve, reject) => {
//步骤一:创建异步对象
var ajax = new XMLHttpRequest();
//步骤二:设置请求的url参数,参数一是请求的类型,参数二是请求的url,可以带参数
ajax.open('get', 'http://127.0.0.1:8000');
//步骤三:发送请求
ajax.send();
//步骤四:注册事件 onreadystatechange 状态改变就会调用
ajax.onreadystatechange = function () {
if (ajax.readyState == 4 && ajax.status == 200) {
//步骤五 如果能够进到这个判断 说明 数据 完美的回来了,并且请求的页面是存在的
resolve(JSON.parse(ajax.responseText))
}
}
})
}

// 获取container对象
const container = document.getElementById('container')
复制代码

直接渲染


最直接的方式就是直接渲染出来,但是这样的做法肯定是不可取的,因为一次性渲染出10w个节点,是非常耗时间的,咱们可以来看一下耗时,差不多要消耗12秒,非常消耗时间


截屏2021-11-18 下午10.07.45.png


const renderList = async () => {
console.time('列表时间')
const list = await getList()
list.forEach(item => {
const div = document.createElement('div')
div.className = 'sunshine'
div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
container.appendChild(div)
})
console.timeEnd('列表时间')
}
renderList()
复制代码

setTimeout分页渲染


这个方法就是,把10w按照每页数量limit分成总共Math.ceil(total / limit)页,然后利用setTimeout,每次渲染1页数据,这样的话,渲染出首页数据的时间大大缩减了


截屏2021-11-18 下午10.14.46.png


const renderList = async () => {
console.time('列表时间')
const list = await getList()
console.log(list)
const total = list.length
const page = 0
const limit = 200
const totalPage = Math.ceil(total / limit)

const render = (page) => {
if (page >= totalPage) return
setTimeout(() => {
for (let i = page * limit; i < page * limit + limit; i++) {
const item = list[i]
const div = document.createElement('div')
div.className = 'sunshine'
div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
container.appendChild(div)
}
render(page + 1)
}, 0)
}
render(page)
console.timeEnd('列表时间')
}
复制代码

requestAnimationFrame


使用requestAnimationFrame代替setTimeout,减少了重排的次数,极大提高了性能,建议大家在渲染方面多使用requestAnimationFrame


const renderList = async () => {
console.time('列表时间')
const list = await getList()
console.log(list)
const total = list.length
const page = 0
const limit = 200
const totalPage = Math.ceil(total / limit)

const render = (page) => {
if (page >= totalPage) return
// 使用requestAnimationFrame代替setTimeout
requestAnimationFrame(() => {
for (let i = page * limit; i < page * limit + limit; i++) {
const item = list[i]
const div = document.createElement('div')
div.className = 'sunshine'
div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
container.appendChild(div)
}
render(page + 1)
})
}
render(page)
console.timeEnd('列表时间')
}
复制代码

文档碎片 + requestAnimationFrame


文档碎片的好处



  • 1、之前都是每次创建一个div标签就appendChild一次,但是有了文档碎片可以先把1页的div标签先放进文档碎片中,然后一次性appendChildcontainer中,这样减少了appendChild的次数,极大提高了性能

  • 2、页面只会渲染文档碎片包裹着的元素,而不会渲染文档碎片


const renderList = async () => {
console.time('列表时间')
const list = await getList()
console.log(list)
const total = list.length
const page = 0
const limit = 200
const totalPage = Math.ceil(total / limit)

const render = (page) => {
if (page >= totalPage) return
requestAnimationFrame(() => {
// 创建一个文档碎片
const fragment = document.createDocumentFragment()
for (let i = page * limit; i < page * limit + limit; i++) {
const item = list[i]
const div = document.createElement('div')
div.className = 'sunshine'
div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
// 先塞进文档碎片
fragment.appendChild(div)
}
// 一次性appendChild
container.appendChild(fragment)
render(page + 1)
})
}
render(page)
console.timeEnd('列表时间')
}
复制代码

懒加载


为了比较通俗的讲解,咱们启动一个vue前端项目,后端服务还是开着


其实实现原理很简单,咱们通过一张图来展示,就是在列表尾部放一个空节点blank,然后先渲染第1页数据,向上滚动,等到blank出现在视图中,就说明到底了,这时候再加载第二页,往后以此类推。


至于怎么判断blank出现在视图上,可以使用getBoundingClientRect方法获取top属性



IntersectionObserver 性能更好,但是我这里就拿getBoundingClientRect来举例



截屏2021-11-18 下午10.41.01.png


<script setup lang="ts">
import { onMounted, ref, computed } from 'vue'
const getList = () => {
// 跟上面一样的代码
}

const container = ref<HTMLElement>() // container节点
const blank = ref<HTMLElement>() // blank节点
const list = ref<any>([]) // 列表
const page = ref(1) // 当前页数
const limit = 200 // 一页展示
// 最大页数
const maxPage = computed(() => Math.ceil(list.value.length / limit))
// 真实展示的列表
const showList = computed(() => list.value.slice(0, page.value * limit))
const handleScroll = () => {
// 当前页数与最大页数的比较
if (page.value > maxPage.value) return
const clientHeight = container.value?.clientHeight
const blankTop = blank.value?.getBoundingClientRect().top
if (clientHeight === blankTop) {
// blank出现在视图,则当前页数加1
page.value++
}
}

onMounted(async () => {
const res = await getList()
list.value = res
})
</script>

<template>
<div id="container" @scroll="handleScroll" ref="container">
<div class="sunshine" v-for="(item) in showList" :key="item.tid">
<img :src="item.src" />
<span>{{ item.text }}</span>
</div>
<div ref="blank"></div>
</div>
</template>
复制代码


作者:Sunshine_Lin
来源  :https://juejin.cn/post/7031923575044964389 收起阅读 »

不要把政治带进开源项目

今天是前端开源圈值得记住的一天,起因是前端占有率第一的框架React官网挂上了一个横幅,大概意思就是支持乌克兰,为乌克兰提供人道主义援助 把自己立于道德的制高点,不带脏字,外加阴阳怪气的输出全场,相比起来我们满屏的小可爱就显得段位low了一些 作为开发者,你...
继续阅读 »

今天是前端开源圈值得记住的一天,起因是前端占有率第一的框架React官网挂上了一个横幅,大概意思就是支持乌克兰,为乌克兰提供人道主义援助
把自己立于道德的制高点,不带脏字,外加阴阳怪气的输出全场,相比起来我们满屏的小可爱就显得段位low了一些

作为开发者,你个人在微博和twitter上支持俄罗斯还是支持乌克兰,都是个人看法,无可厚非,但是React团队直接把政治带进了开源项目,并且美国轰炸叙利亚等过的时候你保持沉默,如此双标的操作让众多中国开发者冲进react的issue,发表着各种愤怒的言论
github.com/facebook/re…


image.png


首先React这个操作让我也很不喜欢,这一点我很支持Vue作者的做法,在twitter上反对战争,但是拒绝Vue这个开源项目上带上政治色彩,比React团队不知道高到哪里去了,同时去React骂人的开发者我也感觉很low,骂人并不能解决问题


image.pngimage.png
image.png
相比于中国开发者满屏的小可爱,我们看下高手是怎么做的,在github的feedback下面使用种族主义的政治正确打脸提议封禁俄罗斯项目的人,大概意思就是
github.com/github/feed…
image.png


如果你真的认为因领导人的所作所为而惩罚全体人民会带来和平,那么我建议你尝试阅读一些历史:看看《凡尔赛条约》是多么成功。
如果你认为切断与一个国家所有人民的沟通会带来和平,并改善该国的政治,那么看看朝鲜,判断一下他们的孤立政策有多健康。
现在有战争,人们在受苦--请不要用这个作为你自己判断仇恨的借口。


如果GitHub应该做什么,那应该是通过向乌克兰人民表示支持--让任何访问GitHub的人看到这场战争有多糟糕。
而不是相反:将所有俄罗斯人与世界其他地区的意见隔离开来。
image.png


把自己立于道德的制高点,不带脏字,外加阴阳怪气的输出全场,相比起来我们满屏的小可爱就显得段位low了一些


但是现在github已经开始封禁这些评论的账号,也挺扯淡的
技术无国界都是扯淡,你TM有本事封禁俄罗斯发明的元素周期表,还有无线电啥的,我个人反对把整治带进开源项目,否则我只能表示 略略略
image.png





作者:花果山大圣
链接:https://juejin.cn/post/7070896218078969870
收起阅读 »

如何用charts_flutter创建Flutter图表

应用程序中的图表提供了数据的图形显示或图画表示,跨越了行业和应用程序。像Mint这样的移动应用程序使用饼状图来监测消费习惯,像Strava这样的健身应用程序使用线状图和条状图来分析步幅、心率和海拔高度。在构建Flutter应用程序时,开发者可以使用由谷歌维护的...
继续阅读 »

应用程序中的图表提供了数据的图形显示或图画表示,跨越了行业和应用程序。像Mint这样的移动应用程序使用饼状图来监测消费习惯,像Strava这样的健身应用程序使用线状图和条状图来分析步幅、心率和海拔高度。

在构建Flutter应用程序时,开发者可以使用由谷歌维护的官方charts_flutter 库来创建这些类型的图表。

在本教程中,我们将学习如何使用charts_flutter 创建一些最常见的图表--线形图、饼图和条形图。

我们将用这些图表来显示一个虚构的Flutter图表开发者社区五年来的增长情况。虽然本教程中的数据是捏造的,但本教程可以很容易地利用真实数据。

前提条件

要学习本教程,您必须具备以下条件。

创建并设置一个Flutter项目charts_flutter

要创建一个新的Flutter项目,运行以下命令。

flutter create projectName

接下来,在您的代码编辑器中打开这个新项目。如上所述,我们将使用[chart_flutter](https://pub.dev/packages/charts_flutter) ,Flutter的官方库

要将chart_flutter 导入您的项目,请打开pubspec.yaml 文件并将其添加到依赖项下。

dependencies:
flutter:
sdk: flutter

charts_flutter: ^0.11.0

构建应用程序的脚手架

现在我们有了新的Flutter应用程序所附带的基本代码:一个记录按钮被按下多少次的计数器。

由于我们的条形图应用程序中不需要这个,继续删除在main.dart 页面中发现的代码。删除所有的内容,除了下面的内容。

import ‘package:flutter/material.dart’;
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
//TODO: implement build
Return MaterialApp(
);
}
}

现在,在我们的构建部件中返回MaterialApp 类,以便我们可以使用Material Design。

创建一个主页

要为我们的应用程序创建一个主页,请导航到lib 文件夹,并创建一个名为home.dart 的新页面。

import 'package:flutter/material.dart';

class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ,
),
);
}
}

通过import 'package:flutter/material.dart' ,我们就可以导入Material Design。

然后,HomePage 类扩展了statelessWidget ,因为在这个页面上没有状态变化。

BuildContext widget里面,我们返回Scaffold 类,给我们一个基本的Material Design布局结构。我们的条形图将放在子参数的位置,我们将把它放在屏幕主体的中心。

所有这些现在都成为我们应用程序的支架。

随着主页的完成,我们可以在我们的main.dart 文件中指定HomePage ,因为main.dart 将我们应用程序中的所有功能集中在一起。

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: HomePage(), //This is where we specify our homepage
);
}
}

有了这段代码,main.dart 就知道每当应用加载时首先显示哪一页。

请注意,将debugShowCheckedModeBanner 设置为false ,就可以从我们的应用程序中删除调试标记。

创建一个Flutter图表应用程序

系列和模型

在我们创建图表应用之前,让我们熟悉一下Flutter图表常用的两个术语:系列和模型。

系列是一组(或系列)的信息,我们可以用它来绘制我们的图表。一个模型是我们的信息的格式,它规定了使用该模型的每个数据项必须具有的属性。

创建一个条形图

为柱状图数据创建一个模型

首先,我们将创建一个柱状图,以显示在过去五年中新增的虚构Flutter图表开发者的数量。换句话说,我们要跟踪虚构的Flutter图表社区的增长情况。

我们的模型,定义了我们的数据格式,包括我们要看的年份,那一年加入Flutter图表社区的开发者数量,以及相应条形图的颜色。

lib 文件夹中,创建一个名为developer_series.dart 的文件。下面,实现我们模型的代码。

import 'package:charts_flutter/flutter.dart' as charts;
import 'package:flutter/foundation.dart';

class DeveloperSeries {
final int year;
final int developers;
final charts.Color barColor;

DeveloperSeries(
{
@required this.year,
@required this.developers,
@required this.barColor
}
);
}

我们将模型命名为DeveloperSeries ,并指定了每个系列项目必须具备的属性(year,developers, 和barColor )。

为了防止在创建一个类的对象时,该类的参数为空,我们使用了@required 注释,如上面的代码块中所示。

要使用@required 关键字,我们必须导入foundation.dart 包。

为柱状图创建数据

现在我们有了一个条形图数据的模型,让我们继续实际创建一些数据。在主页上,通过添加以下内容为柱状图生成数据。

 import 'package:flutter/material.dart';
import 'package:charts_flutter/flutter.dart' as charts;
import 'package:;lib/developer_series.dart';

class HomePage extends StatelessWidget {
final List<DeveloperSeries> data = [

DeveloperSeries(
year: "2017",
developers: 40000,
barColor: charts.ColorUtil.fromDartColor(Colors.green),
),
DeveloperSeries(
year: "2018",
developers: 5000,
barColor: charts.ColorUtil.fromDartColor(Colors.green),
),
DeveloperSeries(
year: "2019",
developers: 40000,
barColor: charts.ColorUtil.fromDartColor(Colors.green),
),
DeveloperSeries(
year: "2020",
developers: 35000,
barColor: charts.ColorUtil.fromDartColor(Colors.green),
),
DeveloperSeries(
year: "2021",
developers: 45000,
barColor: charts.ColorUtil.fromDartColor(Colors.green),
),
];

@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ,
),
);
}
}

这是一个名为data 的简单列表。列表中的每一个项目都是按照DeveloperSeries 的模型制作的,也就是说每一个项目都有一个年份(year)、开发者的数量(developers)和条形图的颜色(barColor)的属性。

请注意,上面的数据是真实的,所以请随意操作这些数字和颜色。

构建柱状图

我们已经成功地为我们的柱状图创建了数据。现在,让我们来创建条形图本身。为了使我们的项目有条理,我们将把柱状图的代码放在一个单独的文件中。

lib ,创建一个developer_chart.dart 文件。

import 'package:flutter/material.dart';
import 'package:charts_flutter/flutter.dart' as charts;
import 'package:lib/developer_series.dart';

class DeveloperChart extends StatelessWidget {
final List<DeveloperSeries> data;

DeveloperChart({@required this.data});
@override
Widget build(BuildContext context) {
List<charts.Series<DeveloperSeries, String>> series = [
charts.Series(
id: "developers",
data: data,
domainFn: (DeveloperSeries series, _) => series.year,
measureFn: (DeveloperSeries series, _) => series.developers,
colorFn: (DeveloperSeries series, _) => series.barColor
)
];

Return charts.Barchart(series, animate: true);
}

}

通过final List<DeveloperSeries> data ,我们定义了一个名为data 的列表,它是我们之前创建的DeveloperSeries 模型形式的数据项的List 。

列表中的每一个数据项都带有相应的年份、开发人员的数量和条形颜色。

类中的DeveloperChart 构造函数确保在使用条形图类的任何地方,它所需要的数据总是被提供;这是用@required 关键字完成的。

实际的柱状图是在我们的构建部件中创建的。如你所知,所有的柱状图都有几组数据相互对照(在我们的例子中,过去五年和Flutter图表社区获得的开发者数量)。

这些数据组在一起,被称为系列。系列告诉我们Flutter要把哪一组数据放在我们条形图的水平面,哪一组放在垂直面。

然后,我们先前创建的数据列表插入到我们的系列中,并由Flutter适当地使用。

通过List<charts.Series<DeveloperSeries, String>> series ,我们创建了一个名为series 的列表。这个列表的类型为charts.Series ;charts 将Flutter导入我们的项目,Series 函数为Flutter中的柱状图创建系列。

我们刚刚创建的系列是以我们的DeveloperSeries 模型为模型。

我们将在系列中指定的参数包括:id,data,domainFn,measureFn, 和colorFN 。

  • id 标识了图表
  • data 指向要在柱状图上绘制的项目列表
  • domainFn 指向柱状图水平方向上的数值。
  • measureFn 指向垂直方向上的数值的数量
  • colorFN 指的是条形图的颜色

通过domainFn,measureFn, 和colorFN 函数,我们创建了以Subscriber 系列为参数的函数,创建了它的实例,然后使用这些实例来访问它的不同属性。

developer_chart.dart 文件中的下划线标志着第二个参数是不需要的。

在将我们的系列指向它所需要的所有数据后,我们再使用Flutter的BarChart 函数来创建我们的柱状图。

我们还可以通过简单地将animate 设置为true 来添加一个动画,以获得视觉上的吸引力,这将使图表呈现出一个漂亮的动画。

将柱状图添加到主页上

现在,我们可以将新创建的柱状图添加到我们的主页上并显示。

import 'package:flutter/material.dart';
import 'package:charts_flutter/flutter.dart' as charts;
import 'package:lib/developer_series.dart';
import 'package:lib/developer_chart.dart';

class HomePage extends StatelessWidget {
// ...

@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: DeveloperChart(
data: data,
)
),
);
}
}

在这里,我们只需在我们的页面主体内调用DeveloperChart 类,并将其指向我们要使用的数据。

为了确保我们的图表能够很好地在屏幕上显示,我们将把它放在一个Card ,在它周围包裹一个容器,并给它设置一个高度和一些填充。



class DeveloperChart extends StatelessWidget {
final List<DeveloperSeries> data;

DeveloperChart({@required this.data});
@override
Widget build(BuildContext context) {
List<charts.Series<DeveloperSeries, String>> series = [
charts.Series(
id: "developers",
data: data,
domainFn: (DeveloperSeries series, _) => series.year,
measureFn: (DeveloperSeries series, _) => series.developers,
colorFn: (DeveloperSeries series, _) => series.barColor
)
];

return Container(
height: 300,
padding: EdgeInsets.all(25),
child: Card(
child: Padding(
padding: const EdgeInsets.all(9.0),
child: Column(
children: <Widget>[
Text(
"Yearly Growth in the Flutter Community",
style: Theme.of(context).textTheme.body2,
),
Expanded(
child: charts.BarChart(series, animate: true),
)
],
),
),
),
);
}

}

通过使用expanded widget,我们将我们的柱状图很好地扩展到Card 。上面的Text widget给了我们的柱状图一个标题,让人们知道它是关于什么的。

而且,通过Theme.of(context).textTheme.body2 ,我们将Material Design默认的正文样式应用于我们的标题。

通过padding: const EdgeInsets.all(9.0) ,我们给容纳我们的条形图的卡片在所有边上加了9px的填充。最后,我们将Card 包裹在一个容器中,并给这个容器一个300px的高度和25px的边距。

现在,我们的条形图应该能很好地呈现在我们的屏幕上。

Flutter Bar Chart, Showing Growth Of The Flutter Chart Community Over Five Years With Five Green Bars, With Dates Ranging From 2017 To 2021

创建饼状图

我们也可以使用charts_flutter 包来创建饼图。事实上,我们上面遵循的程序和我们编写的代码可以创建饼图。

要将我们创建的条形图改为饼图,只需将charts.BarChart(series, animate: true) 改为child:( charts.PieChart(series, animate: true) 。

然后我们就可以在饼图上添加标签。

Expanded(
child: charts.PieChart(series,
defaultRenderer: charts.ArcRendererConfig(
arcRendererDecorators: [
charts.ArcLabelDecorator(
labelPosition: charts.ArcLabelPosition.inside)
])
animate: true),
)

ArcRendererConfig 函数可以配置饼图的外观,我们可以使用ArcLabelDecorator 函数为饼图添加标签。

labelPosition 指定将标签放在哪里,是放在里面还是外面;在这种情况下,我们指定标签应该放在外面。

Flutter Pie Chart Shows Flutter Chart Community Growth Over Five Years In Green Chart With Dates Ranging From 2017 To 2021

创建折线图

我们可以用创建其他两个图表的同样方法来创建一个折线图。我们只需对我们的数据配置做一个小小的调整。

在我们的系列列表中,List<charts.Series<DeveloperSeries, String>> 变成List<charts.Series<DeveloperSeries, num>> ,因为折线图只对数字起作用。

List<charts.Series<DeveloperSeries, num>> series = [
charts.Series(
id: "developers",
data: data,
domainFn: (DeveloperSeries series, _) => series.year,
measureFn: (DeveloperSeries series, _) => series.developers,
colorFn: (DeveloperSeries series, _) => series.barColor
)
];

现在我们可以把charts.PieChart 改为charts.Linechart ,从而得到我们的折线图。默认情况下,折线图是从原点零开始的。但是我们所关注的年份是从2016年到2021年。因此,这里是如何使我们的折线图跨越这个范围的。

Expanded(
child: charts.LineChart(series,
domainAxis: const charts.NumericAxisSpec(
tickProviderSpec:
charts.BasicNumericTickProviderSpec(zeroBound: false),
viewport: charts.NumericExtents(2016.0, 2022.0),
),
animate: true),
)

NumericAxisSpec 函数为图表中的轴设置规格。通过BasicNumericTickProviderSpec 函数,我们将zeroBound 设置为false ,这样我们的图表就不会从原点零开始。

最后,通过NumericExtents 函数,我们设置了我们希望我们的坐标轴所跨越的范围。

Flutter Line Chart With Community Growth Over The Years 2017 To 2021 Indicated By Green Line

总结

本教程的目的是向Flutter开发者展示如何在其应用程序中实现不同的图表。使用谷歌创建的强大的charts_flutter 包,我们能够实现一个柱状图、一个饼状图和一个线形图。


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

收起阅读 »

爬了下知乎神回复,笑死人了~

都说知乎出人才,爬虫爬了下知乎上的回答,整理了80条超级搞笑的神回复,已经笑趴😂1Q: 你随身携带或佩戴最久的那件东西是什么?对你有什么特殊的意义?A: 眼镜,因为瞎2Q: 有哪些东西你以为很贵,但其实很便宜?A: 大学刚毕业的我。3Q: 如何看待「当你买 i...
继续阅读 »
都说知乎出人才,爬虫爬了下知乎上的回答,整理了80条超级搞笑的神回复,已经笑趴😂


1

Q: 你随身携带或佩戴最久的那件东西是什么?对你有什么特殊的意义?

A: 眼镜,因为瞎


2

Q: 有哪些东西你以为很贵,但其实很便宜?

A: 大学刚毕业的我。


3

Q: 如何看待「当你买 iPhone 4 的时候,他买了冰箱」这段话?

A: 这暗示了,在你连iPhone都买不起的时候,他就买了房子。

世界真是不公平呀!


4

Q: 哪些因素会阻止未来粮食产量的增加?

A: 崔永元,,,


5

Q: 为什么程序员不应该会修电脑?

A: 范冰冰需要会修电视机吗?


6

Q: 有哪些主角颜值低、穷、能力弱,配角颜值高、富、能力强的影视或游戏作品?

A: 人生


7

Q: 室友要花 9.6 万元参加一个操盘手培训值得吗?

A: 非常值得! 
一次被骗9万是很少见的体验


8

Q: 深夜食堂在中国的可行性如何?

A: 在我天朝,他们被称为“午夜大排档”


9

Q: 有哪些品牌名字起得失败得让人崩溃?为什么?

A: 海伦凯勒太阳眼镜。


10

Q: 中国程序员是否偏爱「冲锋衣+牛仔裤+运动鞋」的衣着?如果是,为何会形成这样的潮流?

A: 穿那么好看给程序看吗?


11

Q: 你读过的书中,有哪些让你觉得惊艳的开头?

A: abandon


12

Q: 有哪些动漫角色穿着暴露无比,却没有给人「卖肉」的感觉?

A: 海尔兄弟


13

Q: 为什么每次圣斗士出招前都要大喊一下招式?

A: 函数要先声明,才能调用。


14

Q: 体育史上有哪些后果严重的『冤案』?

A: 如果说02年韩国一路杀到四强都不算的话那找不到别的了!


15

Q: 知乎上极力推崇读书的人为什么不把上知乎的时间都用来读书?

A: 独学而无友,则孤陋而寡闻


16

Q: 有哪些时候你发现赚钱如此容易,过后你还会那么觉得么?

A: 刷知乎的时候


17

Q: 董明珠为什么选择自己为格力代言?

A: 估计他们认为老干妈好卖,是因为老干妈把自己印在瓶子上面?


18

Q: 哪些盈利模式令你拍案叫绝?

A: 卖生男秘方,不准全额退钱。


19

Q: 售价上万的马桶,商家却为什么强调节水效果?

A: 因為節水不是為了省錢。


20

Q: 中国的部分学生宿舍是不是反人类的?

A: 在北京四环内650能住一年的地方
除了大学宿舍,哪怕树洞你再找个给我看看啊。


21

Q: 上厕所忘了拉拉链,出来被女同事看到并且笑了,如何优雅的圆场?

A: 国之利器,本不可以示人,诸位一窥门径,何其幸哉。


22

Q: 祈求代码不出 bug 该拜哪个神仙?

A: 拜雍正,专治八阿哥。


23

Q: 颜值真的有那么重要吗?

A: 同样是互联网巨头 李彦宏是老公 而马云只能当爸爸


24

Q: 为什么人常会在黑夜里,变得矫情万分?

A: 要渲染的图像少了,CPU就有空闲来思考人生了。


25

Q: 你第一次跳槽是什么原因?后悔吗?

A: 我上班就是为了钱,他非得和我谈理想,可我的理想是不上班…


26

Q: 员工辞职最主要的原因是什么?

A: 钱少事多离家远,位低权轻责任重。


27

Q: 水平极高的出租车司机能够有多神奇的驾技?

A: 在停车的一瞬间再多跳一块钱。


28

Q: 面试的时候被人问到为什么你没有去清华大学,你该怎么回答?

A: “去了,但保安没让进。


29

Q: 哪一个瞬间你觉得自己应该离职了?

A: 当然是身体原因了:
胃不好,饼太大,吃不下。
腰不好,锅太沉,背不动。


30

Q: 接了阿里 offer 后毁约会被拉黑吗?

A: 得打个电话,让对方拥抱变化。


31

Q: 有哪些事情人们在二次元可以接受,而在三次元则不可接受?

A: 没鼻子。


32

Q: 《新世纪福音战士》 TV 版第 25、26 集为什么大量运用意识流手法?它在试图表达什么?

A:
“我们没钱了。


33

Q: 如何评价《火影忍者》的大结局?

A:
辛苦了,我们知道你编不下去了。


34

Q: 你曾经被哪些书名骗了?

A: 血泪史啊,有本书叫《制服诱惑》!你妹的是动词啊!


35

Q: 你是否曾经被一本书所改变与(或)感动?甚至被改变人生观?

A: 《五年高考 三年模拟》


36

Q: 如何评价 bilibili 永远不对正版新番播放视频贴片广告的声明?

A: 其实我说吧,只要广告也可以发弹幕,就算看两分钟也无所谓……


37

Q: 男生在【日常打扮】中如何才能穿出二次元的感觉,同时显得得体又不怪异?

A: 穿女装就好了。


38

Q: 如何看待「爱狗人士」欲强行带走玉林当地活狗?

A: 作为爱钱人士,我一直不敢随便抱走别人的钱。看了这新闻,我非常羡慕爱狗人士能随便抱走别人的狗。


39

Q: 为何图书馆不能穿拖鞋?

A: 以防翻书舔手指的和看书抠脚丫的打起来。


40

Q: 去厦门鼓浪屿旅游,不能错过什么?

A: 回厦门的船。


41

Q: 现代有哪些不如古代的技术?

A: 蹴鞠


42

Q: 在杭州和喜欢的姑娘在一起有哪些好玩的地方?

A: 南山路橘子水晶酒店、九里云松酒店、湖滨凯悦酒店、西湖四季酒店、灵隐安曼法云酒店。


43

Q: 原始人为什么会画壁画?

A: 说明「装修」这种冲动是基因里的本能。


44

Q: 有哪些地方让你觉得「一定要跟最喜欢的人来这里」?

A: 民政局


45

Q: 有没有一部电影让你在深夜中痛哭?

A: 《肉蒲团》。当时我12岁,半夜偷看被我爸发现,被揍哭。


46

Q: 无神论的各位一般从哪里获得精神力量?

A: deadline


47

Q: IT 界有哪些有意思的短笑话?

A: winrarsetup.rar


48

Q: 为什么科技水平提高了,人却没有更轻松?

A:
因为科技只管吃穿住行,不管贪嗔痴妒。


49

Q: IT 工程师被叫「码农」时是否会不舒服?

A: 我们好歹还是人,产品和设计已经是狗了……


50

Q: 外国也有地域歧视吗?

A: 在上海,一老外给我说,他打心眼里瞧不起北京老外。


51

Q: 什么原因让你一直单身?

A: 我还没找到自己,如何去找另一半?


52

Q: 如果恋爱不牵手,不接吻,不上床,就不是恋爱,爱一个人的表现真的要这些身体接触吗?

A: 当然了,不然你觉得为啥“爱情”和“受精”长那么像。


53

Q: 你会怎么写三行情书?

A: 我们化学了
我们物理了
我们生物了


54

Q: 男朋友别人面前很正经,在我面前很二怎么办?

A: 真爱


55

Q: 你一直暗恋的人突然反过来向你表白是一种什么样子的体验?

A: 还想继续睡,然后努力想怎样才能把梦续上去。


56

Q: 怎么看待女朋友的蓝颜?

A: 蓝颜蓝颜,加点黄色就绿了


57

Q: 你有哪些「异性暗示跟你交往,你却木讷地错过了」的经验?

A: 大一时候一女生坐我自行车,我义正言辞地说:“手放老实点。


58

Q: 拒绝了我的人是以什么心态偶尔来我空间的?

A: 触屏手机就这点不好


59

Q: 女朋友有什么用处?

A: 让你四处躁动的心、鸡鸡和不知道怎么花的钱有个温暖着落。


60

Q: 当我有话不直说的时候,为什么男友总是不明白我的心思?

A: 题主,你猜猜我的回答是什么?

难道要我直接回答你才能明白?


61

Q: 平时开玩笑开习惯了,结果跟女生表白人家以为我是开玩笑呢,怎么办?

A: 她才没误会呢
只是她比较善良


62

Q: 怎样拒绝女生的告白?

A: 对不起,我是个好人。


63

Q: 女朋友痛经,男方该怎么做会显得贴心?

A: 跟老婆说:“来,掐老公小鸡鸡,老公陪你一起疼!


64

Q: 你会在意你的恋人有异性闺蜜吗?

A: 女人永远不会明白男人为什么怀疑她跟别的男人的友谊,因为男人太了解男人了!


65

Q: 如何成为“交际花”?

A: 善解人异
善解人疑
善解人意
善解人衣


66

Q: 怎么反驳“胸小不要说话”?

A: “怎么,怕我揭你短?


67

Q: 如何优雅地拒绝他人的表白?

A: 我知道了,你先回去等消息吧~


68

Q: 在哪里可更高几率遇见高质量男生?

A: 两会的时候可以去逛逛,每个地方牛逼的大叔都在那


69

Q: 为什么那么多人说自己寂寞、孤单、想找个男/女朋友,却还是单身?

A: 因为不仅自己丑,
还嫌别人长得丑。


70

Q: 做备胎是什么感觉?

A: 每一句话都是密码


71

Q: 滚床单来不及卸妆怎么办?

A: 别啊,我要的就是阿凡达


72

Q: 男生坚持每天给女生说晚安,持续一年会怎样?

A: ie天天都问我要不要把它设定为默认浏览器,已经好几年了


73

Q: 前女友分手时给我发「掌上珊瑚怜不得,应教移作上阳花」应该怎么回答?

A: 海誓山盟皆笑话,愿卿怜惜手中瓜。


74

Q: 女生送青蛙玩具给男生,代表什么意思?

A: 我送你個青蛙,你好歹還我點蝌蚪吧


75

Q: 女生的长相重要吗?

A: 重要,一般姑娘撒个娇就能解决的问题我都得靠武力。


76

Q: 你为什么还单身?

A: 因为单身比较专注,单身使人进步~


77

Q: 为什么绝大部分女生拒绝男生表白后都希望做朋友,这是什么心态?

A: 跟你客气一下啊,要不还能说什么,“我们不合适,我们还是做仇人吧”


78

Q: 如何用歌词表白?

A: 总想对你表白,我的心情是多么豪迈。


79

Q: 表白被拒绝时你在想什么?

A: 不愧是我看上的妹子,眼光果然不错~


80

Q: 有个女生帮了我的忙,我想感谢她但不希望她男友误会,我该怎么做?

A: 你可以给她送个锦旗。

作者丨shenzhongqiang
来源丨Python与数据分析(ID:PythonML)

收起阅读 »

Java并发-ThreadLocal

Java并发-ThreadLocal ThreadLocal简介: 多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。 而Threa...
继续阅读 »

Java并发-ThreadLocal


ThreadLocal简介:


多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。


而ThreadLocal是除了加锁这种同步方式之外的另一种保证一种规避多线程访问出现线程不安全的方法,线程并发的安全问题在于多个线程同时操作同一个变量的时候,不加以额外限制会存在线程争着写入导致数据写入不符合预期,如果我们在创建一个变量后,每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。


ThreadLocal的典型适用场景:


典型场景1:


每一个线程需要有一个独享的对象(通常是工具类,典型比如SimpleDateFormat,Random)。


以代码为例,通过SimpleDateFormat实现时间戳转换为格式化时间串的功能:


public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.execute(new Runnable() {
@Override
public void run() {
String result = toDate(1000L + finalI);
}
});
}
threadPool.shutdown();
}

public static String toDate(long seconds){
Date currentDate = new Date(seconds *1000);
SimpleDateFormat formator = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
return formator.format(currentDate);
}

上面的代码其实是没有线程安全问题的,但是存在的不足是我们调用了1000次,创建了1000次SimpleDateFormat对象,为了结局这个问题,我们可以把SimpleDateFormat对象从toDate中抽离出来,成为一个全局的变量:


static SimpleDateFormat formator = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

public static String toDate(long seconds){
Date currentDate = new Date(seconds *1000);
return formator.format(currentDate);
}
// output:
1970-01-01 08:33:06
1970-01-01 08:33:07
1970-01-01 08:32:47
1970-01-01 08:32:47
1970-01-01 08:33:10
1970-01-01 08:33:11

很容易发现,全局唯一的formator对象,因为没有加锁,是有线程安全问题的,那么我们可以通过加锁修复:


public static String toDate(long seconds){
Date currentDate = new Date(seconds *1000);
synchronized (formator){
return formator.format(currentDate);
}
}

虽然修复了线程安全问题,但是随之而来的,synchronized关键字导致各个线程需要频繁的申请锁资源,等待锁资源释放,释放锁资源,这并不划算,而利用ThreadLocal这个工具类,可以很方便的解决问题:


ThreadLocal改造如下:


class FormatorThreadLocalGetter {
public static ThreadLocal<SimpleDateFormat> formator = new ThreadLocal<>() {
@Override
protected SimpleDateFormat initialValue() {
return new SSimpleDateFormat("yyyy-MM-dd hh:mm:ss");
}
};

// or use Lambda
public static ThreadLocal<SimpleDateFormat> formator2 = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
}

public class ThreadLocalTest {
// 这里我们使用COW容器记录下每一个SimpleDateFormator的hashcode
static CopyOnWriteArraySet<String> hashSet = new CopyOnWriteArraySet<String>();

public static void main(String[] args) throws InterruptedException {
ExecutorService threadPool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
System.out.println(toDate(1000L + finalI));
}
});
}
threadPool.shutdown();
Thread.sleep(5000);
// 延迟5s,确保所有的输出都执行完毕,然后看看我们创建了多少个formator对象。
System.out.println(hashSet.size());
hashSet.forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});
}

public static String toDate(long seconds) {
Date currentDate = new Date(seconds * 1000);
SimpleDateFormat formator = FormatorThreadLocalGetter.formator.get();
// 将当前线程的formator的hashcode记录下来,看看最终有多少个hashCode
hashSet.add(String.valueOf(formator.hashCode()));
// 通过ThreadLocal去get一个formator。
return FormatorThreadLocalGetter.formator.get().format(currentDate);
}
}

// 这里我们需要override一下hashCode函数,因为默认的hashCode生成规则是
// 调用构造函数入参pattern这个String对象的hashCode,因为所有的formator
// 的pattern都一样,不重写一下会发现hashCode都一样。
class SSimpleDateFormat extends SimpleDateFormat {
private int hashCode = 0;
SSimpleDateFormat(String pattern) {
super(pattern);
}

@Override
public int hashCode() {
if (hashCode > 0) {
return hashCode;
}
hashCode = UUID.randomUUID().hashCode();
return hashCode;
}
}
// output:
1970-01-01 08:33:15
1970-01-01 08:33:10
1970-01-01 08:33:18
23 // 一千次任务总共创建了23个formator对象
-674481611
-424833271
-2124230669
411606156
-1600493931
900910308
540382160
-1054803206
...

因为线程池执行1000次任务并不是只创建了10个线程,其中仍然包括线程的销毁和新建,因此通常而言是不止10个formator对象被创建,符合预期。


典型场景2:


每个线程内需要有保存自己线程内部全局变量的地方,可以让不同的方法直接使用,避免参数传递麻烦,同时规避线程不安全行为。


典型场景2其实对于客户端来说比较少见,但是可以作为ThreadLocal的另外用法的演示,在使用场景1中,我们用到的是在ThreadLocal对象构造的时候主动去初始化我们希望通过它去保存的线程独有对象。


下面的场景是用来演示主动给ThreadLocal赋值:


举个例子如下图所示,每一个请求都是在一个thread中被处理的,然后通过层层Handler去传递和处理user信息。


image-20220223005746413.png


这些信息在同一个线程内都是相同的,但是不同的线程使用的业务内容user是不同的,这个时候我们不能简单通过一个全局的变量去存储,因为这个全局变量是线程间都可见的,为此,我们可以声明一个map结构,去保存每一个线程所独有的user信息,key是这个线程,value是我们要保存的内容,为了线程安全,我们可以采取两种方式:



  1. 给map的操作加锁(synchronized等)。

  2. 借助线程安全的map数据结构来实现这个map,比如ConcurrentHashMap。


但是无论加锁还是CHM去实现,都免不得会面临线程同步互斥的压力,而这个场景下,ThreadLocal就是一个非常好的解决方式,无需同步互斥机制,在不影响性能的前提下,user参数也不需要通过函数入参的方式去层层传递,就可以达到保存当前线程(request)对应的用户信息的目的。


简单实现如下:


class UserContextHolder{
public static ThreadLocal<User> holder = new ThreadLocal<>();
}

class Handler1{
public void handle(){
User user = new User();
user.name = "UserInfo" + user.hashCode();
// handler1通过set方法给当前线程的ThreadLoca<User>赋值
UserContextHolder.holder.set(user);
}
}

class Handler2{
public void handle(){
// handler2通过get方法获取到当前线程对应的user信息。
System.out.println("UserInfo:" + UserContextHolder.holder.get());
}
}

通过上面的例子,我们可以总结出ThreadLocal几个好处:



  1. 线程安全的存储方式,因为每一个线程都会有自己独有的一份数据,不存在并发安全性。

  2. 不需要加锁,执行效率肯定是比加锁同步的方式更高的。

  3. 可以更高效利用内存,节省内存开销,见场景1,几遍有1000个任务,相比于原始的创建1000个SimpleDateFormator对象或者加锁,显然ThreadLocal是更好的方案。

  4. 从场景2,我们也能看出,在某些场景下,可以简化我们传参的繁琐流程,降低代码的耦合程度。


ThreadLocal原理分析:


理解ThreadLocal需要先知道Thread,ThreadLocal,ThreadLocalMap三者之间的关系,如下图所示:


image-20220223012629697.png


每一个Thread对象内部都有一个ThreadLocalMap变量,这个Map是一个散列表的数据结构,而Map的Entry的key是ThreadLocal对象,Value就是ThreadLocal对象要保存的Value对象。


ThreadLocalMap本身是一个数组结构的散列表,并非传统定义的Map结构,ThreadLocalMap在遇到hash冲突的时候,采用的是线性探测法去解决冲突 ,数组存放的Entry是ThreadLocal和Value对象。


static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

**尤其需要注意,ThreadLocal工作机制的核心是线程持有的ThreadLocalMap这个数据结构,而不是ThreadLocal自身。**有点绕,可以看下文的分析。


核心API解析:


initialValue:


该方法会返回当前线程对应的数据的初始值,并且这是一个延迟初始化的方法,不会在ThreadLocal对象构造的时候调用,而是在线程调用ThreadLocal#get方法的时候调用到。


get:


得到这个线程对应的Value值,如果首次调用,会通过initialize这个方法初始化。


set:


为这个线程设置一个新的Value。


remove:


删除这个线程设置的Value,后续再get,就会再次initialValue。


源码如下:


public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// JDK源码中上面两行代码其实等价于:
// ThreadLocalMap map = t.threadLocals
if (map != null) {
// 获取到当前线程的ThreadLocalMap中存放的Entry
// Entry的key其实就是this(ThreadLocal本身)
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
// 返回ThreadLocal存放的对象本身。
return result;
}
}
// 如果上面没有找到,那么就会初始化
return setInitialValue();
}

setInitialValue实现如下:


private T setInitialValue() {
// 调用initialValue初始化ThreadLocal要保存的对象。
T value = initialValue();
Thread t = Thread.currentThread();
// 拿到当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
// Thread#ThreadLocalMap的初始值是null
if (map != null) {
// 如果map有了,set存
map.set(this, value);
} else {
// 否则就给当前Thread的threadLocals(即ThreadLocalMap)赋值并存入
// 上面创建的Value。
createMap(t, value);
}
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
// 将Value返回,作为get的返回值。
return value;
}

再来看下set操作实现,其实就是做一件事,如果线程已经有了ThreadLocalMap,那么就直接存Value,如果线程没有ThreadLocalMap,就创建Map并且存Value。


    public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}

在这里我们也可以看到如果是通过initialValue将Value的初始值写入,那么就会调用setInitialValue,如果是通过set写入初始值,那么不会调用到setInitialValue。


同时,需要注意,initialValue通常是只会调用一次的,同一个线程重复get并不会触发多次init操作,但是如果通过remove这个API,主动移除Value,后续再get,还是会触发到initialValue这个方法的。


public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}

如果我们不主动重写initialValue这个方法,默认是返回null的,一般使用匿名内部类的方法来重写initialValue方法,这样方便在后续的使用中,可以直接使用,但是要注意,initialValue除非主动remove,否则是只会调用一次的,即仍然需要做空值确认。


ThreadLocal内存泄露:


ThreadLocal被讨论的最多的就是它可能导致内存泄露的问题。


我们看下ThreadLocalMap#Entry的定义:


static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

ThreadLocalMap对Entry的引用是强引用,Entry对ThreadLocal的引用是弱引用,但是对Value的引用是强引用,这就可能会导致内存泄露。


正常情况下,当线程终止的时候,会将threadLocals置空,这看起来没有问题。


but:


如果线程不能终止,或者线程的存活时间比较久,那么Value对象将始终得不到回收,而如果Value对象再持有其它对象,比如Android当中的Activity,就会导致Activity的内存泄露,(Activity被销毁了,但是因为Value绑定的Thread还在运行状态,将导致Activity对象无法被GC回收)。


这个时候引用链就变成了如下:


Thread->ThreadLocalMap->Entry(key是null,Value不为空)->Value->Activity。


当然JDK其实已经考虑了这个问题,ThreadLocalMap在set,remove,rehash等方法中,都会主动扫描key为null的Entry,然后把对应的Value设置为null,这样原来Value对应的对象就可以被回收。


以resize为例:


private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;

for (Entry e : oldTab) {
if (e != null) {
ThreadLocal<?> k = e.get();
// 遍历到key为null的时候就将value也设置为null。
if (k == null) {
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}

setThreshold(newLen);
size = count;
table = newTab;
}

但是但是,还是有问题:


如果Thread一直在运行,但是其所持有的ThreadLocalMap又没被用到了,此事上面那些set,remove,rehash方法都不会被调用,那还是存在内存泄露的问题......


按照阿里Java规范,ThreadLocal的最佳实践需要在ThreadLocal用完之后,主动去remove,回到典型场景2的代码,我们需要在Handler2的末尾,执行ThreadLoca.remove操作,或者在Handler链路过程中,如果逻辑无法运行到Handler2末尾,相应的异常处也需要处理remove。


装箱拆箱的NPE问题:


如果使用ThreadLocal去保存基本数据类型,需要注意空指针异常,因为ThreadLocal保存的只能是封箱之后的Object类型,在做拆箱操作的时候需要兼容空指针,如下代码所示:


public class ThreadLocalNPE {
static ThreadLocal<Integer> intHolder = new ThreadLocal<>();
static int getV(){
return intHolder.get();
}
public static void main(String[] args) {
getV();// 抛异常
}
}

原因是我们在get之前没有主动set去赋值,getV中intHolder.get先拿到一个Integer的null值,null值转换为基本数据类型,当然报错,将getV的返回值修改为Integer即可。


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

撸一下ThreadPoolExecutor核心思路

ThreadPoolExecutor中知识点很多,本文只是从7个构造参数入手,看看其运转的核心思路。重点不是扣代码,是体会设计思想哈! 欢迎纠错和沟通。 ThreadPoolExecutor 以下是构造ThreadPoolExecutor的7大参数。 publ...
继续阅读 »

ThreadPoolExecutor中知识点很多,本文只是从7个构造参数入手,看看其运转的核心思路。重点不是扣代码,是体会设计思想哈!
欢迎纠错和沟通。


ThreadPoolExecutor


以下是构造ThreadPoolExecutor的7大参数。


public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {

corePoolSize 和 maximumPoolSize 以及 workQueue(BlockingQueue)共同决定了线程池中线程的数量。
下面几条是我总结的观点:



  1. 核心线程数小于等corePoolSize。

  2. 普通线程在workQueue满了后才会创建。

  3. 普通线程在任务结束后存活时长为:keepAliveTime*unit

  4. 任务总数如果超过了workQueue的容量+普通线程数,会触发 RejectedExecutionHandler

  5. 最好自定义ThreadFactory来创建线程,方便标识线程名等。

  6. ThreadPoolExecutor提供了hook的方法beforeExecute()afterExecute()

  7. shutdown()不会立即停止线程池中的未完成的任务,shutdownNow()会。


这里有个设计上的亮点:它使用了一个AtomicInteger类型的ctl来同时记录线程池状态和线程池中线程数。ctl中高3位记录状态,低29位代表线程数量。
这种方法值得学习,除了节省变量,也减少了线程池状态和当前线程数量同步问题。


@ReachabilitySensitive
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;

// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;

// Packing and unpacking ctl
private static int runStateOf(int c) { return c & ~CAPACITY; }
private static int workerCountOf(int c) { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }

核心代码区域execute():


public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();

int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
//如果当前worker数量小于corePoolSize则创建新的核心Worker
if (addWorker(command, true))
return;
c = ctl.get();
}
//线程池正在运行且可以正常添加任务(即workQueue还没有满),此时等待任务执行即可。注意此处已经将新的Runnable存储到了workQueue里了。
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
//二次确认,此时线程池不属于运行状态,且刚添加进去的任务还没有执行,则reject
reject(command);
else if (workerCountOf(recheck) == 0)
//线程池showdown中,后续应该什么都不会做直接return
addWorker(null, false);
}
//workerQueue中满了,创建非核心worker,如果不成功则reject
else if (!addWorker(command, false))
reject(command);
}

看下addWorker()到底干了啥


private boolean addWorker(Runnable firstTask, boolean core) {

retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
//如果当前线程池停止了,或者 当前task为空且workQueue中没有任务了。这些情况可以直接退出该方法。
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;

for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}

boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());

if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}

上面代码,除了线程池状态校验以及保证代码的异步安全,核心就是:


 w = new Worker(firstTask);
final Thread t = w.thread;
t.start();

创建一个Worker并启动其中的线程


Worker


Worker是什么?Worker是ThreadPoolExecutor的内部类,ThreadPoolExecutor中把excute()传递进来的Runnable认为是一个Work,那么执行Work的就是Worker了。


private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{

/** Thread this worker is running in. Null if factory fails. */
final Thread thread;
/** Initial task to run. Possibly null. */
Runnable firstTask;
/** Per-thread task counter */
volatile long completedTasks;

/**
* Creates with given first task and thread from ThreadFactory.
* @param firstTask the first task (null if none)
*/
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}

public void run() {
runWorker(this);
}

}

Worker的构造函数中持有了传入的Task,并通过ThreadFactory创建了个新的线程。


note: 新创建的线程持有了Worker对象自己,而Worker本身又实现了Runnable接口。所以当该线程启动时,run()就会被执行。


看下Worker的run()方法具体干了什么?


首先里面有个while循环,主要是获取task的,如果task一直能获取到,则就能执行到while内部。



  1. 当前task非空,即核心线程创建的时候,自带了一个task。会触发task.run()方法,并在它前后分别又beforeExecute(wt, task)afterExecute(task, thrown)hook点。

  2. 当前task==null,就需要到getTask()中获取。


final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}

getTask()就跟ThreadPoolExecutor构造函数中的BlockQueue关联上了。当前线程如果执行完第一个Task后,应该怎么办呢?如果还不满足结束条件(比如说存活时间超过keepAlive*unit),则会向worksQueue(BlockQueue)中所要任务,然后执行。


private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?

for (;;) {
int c = ctl.get();
int rs = runStateOf(c);

// Check if queue empty only if necessary.
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}

int wc = workerCountOf(c);

// Are workers subject to culling?
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}

try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}

上面前段代码就是判断当前线程池是否还能继续存在,如果能存在并且未超时,那么就从workQueue中取task()。Runnable r = timed ?workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS):workQueue.take();



/**
* Retrieves and removes the head of this queue, waiting if necessary
* until an element becomes available.
*
* @return the head of this queue
* @throws InterruptedException if interrupted while waiting
*/
E take() throws InterruptedException;

是个阻塞的行为。所以TheadPoolExecutor中的workQueue参数传入哪种类型的BlockQueue直接影响了未执行任务的执行顺序。 不过需要注意的是即使使用了优先级队列,高优先级的任务也不一定比低优先级任务先执行,因为任务是由线程发起的,workQueue没法影响线程的执行顺序。


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

为什么雷军、马斯克等大佬,都在早晨5:59分起床?

本文聊一聊,关于时间管理、自控力方面的话题,先来思考一个问题:为什么很多牛人,都有早起的习惯?大佬之所以成为大佬的原因,不仅仅源于他们的天赋、资源与运气,更重要的是他们异于常人的勤奋与毅力。 雷军想必大家都非常熟悉了,他是小米公司的创始人、董事长兼首席执行官,...
继续阅读 »

本文聊一聊,关于时间管理、自控力方面的话题,先来思考一个问题:为什么很多牛人,都有早起的习惯?

大佬之所以成为大佬的原因,不仅仅源于他们的天赋、资源与运气,更重要的是他们异于常人的勤奋与毅力。

雷军想必大家都非常熟悉了,他是小米公司的创始人、董事长兼首席执行官,也是金山软件公司董事长,更是多次登上福布斯榜的超级富豪,外界更是美称其为“雷布斯”。

在大学期间,雷军就颇有名气,他成立了自己的个人工作室,并开发了一系列杀毒与加密软件,还出版了自己的两本书。他所创办的小米公司,不过是和朋友聚会煮小米粥戏谈而生,而现如今小米已成为世界500强企业,全球第三大智能手机供应商,公司科技估值已过450亿美元。

“天下武功,唯快不破。互联网竞争的利器就是快。”

面对互联网一波又一波的竞争,雷军到底有多快,有多拼呢?

据小米公司高级副总裁祁燕内部透露,雷军每天都聚精会神专注公司事务,六点之前起床,七点来到公司,常常从早上忙到三四点才开始吃午饭,而午饭也是十分钟内快速解决,晚饭就更晚,都到十一二点,接近凌晨,一股王霸之气油然而生,在这里B哥忍不住就想评价一下:老雷这么拼简直是劳模啊。

钢铁侠马斯克也是B哥的偶像。据2021年的数据显示,马斯克目前拥有的财产已达2700亿美元,是全地球最富之人了。似乎大佬们都喜欢辍学创业,马斯克也是其中一个例子。

不过据说,马斯克当时是因为投了简历没人理会,才选择和弟弟金巴尔创业的。兄弟俩在美国加州的简陋小屋子里创办了Zip2,为客户提供在线的城市导航与指南信息,最后这家初创公司被康柏以3.7亿美元收购。

也就是这一年,马斯克创办了在线银行X.com。2000年的时候,X.com与Confinity合并为PayPal,移动支付时代就此开启。

一般来说,财富自由也就意味着可以开心玩耍了,但马斯克不这样想,或者说,比起安逸享乐,他更喜爱高难度挑战——发射火箭,实现他自童年时代就有的火星殖民梦想。

他说,“如果你回到几百年前,今天我们认为理所当然的事情就像魔术一样,能够远距离与人交谈,传输图像,飞行,访问大量数据。这些都是几百年前被认为是魔法的东西。

在这一生中,我们都是创造者。如果你努力并忠于你的梦想,你可以在宇宙中留下一个印记。”

马斯克创办了SpaceX,并担任首席执行官与首席技术官,还就真把火箭发射上天并成功回收了。

同时马斯克创立了太阳能服务公司SolarCity,也就是现在的特斯拉子公司特斯拉能源;创办了非盈利公司OpenAI,用于研究和推动友善人工智能;创办了神经科技公司Neuralink,专注于开发脑机接口......

这辉煌的一切皆来源于他的勤奋。马斯克曾向美国新闻台CBS等数家媒体透露过自己的工时,大约一周在85到120个小时之间,这个工作量约是加州基本工时(40小时)的二至三倍。

忙坏了的马斯克,通常在凌晨一点时「当机」就寝,早上七点起床后继续奋斗。难怪大家都叫他钢铁侠,这也不是没有道理的。

斯坦福大学凯里·麦格尼格尔教授在《自控力》里写道:

人类的天性不仅包括了想即时满足的自我,也包括了目标远大的自我。自控力最强的人,

不是从与自我的较量中获得自控,而是学会如何接受相互冲突的自我,并将这些自我融为一体。

李嘉诚不论几点睡觉,一定在清晨5点59分闹铃响后起床。从早年创业至今,李嘉诚一直保持着两个习惯:一是睡觉之前,一定要看书,非专业书籍,他会抓重点看,如果跟公司的专业有关,就算再难看,他也会把它看完。二是早上6点准时起床。

想象一下这样的画面:在星子寥落的清晨,你还在幸福窝在小被窝里的时候,一群企业大亨却起的比谁都早,勤勤恳恳如老黄牛一般忙于工作......

人比你起点更高并不可怕,可怕的是比你起点高的人比你更努力更勤奋。人跟人之间的发展差距并不取决于天赋,天赋卓绝的人大有人在,它也并不取决于资源,起于微末但却成功的人甚至比背景显赫的人多的多,拿张一鸣的话来说,真正决定人跟人之间差距的是,是否你能延迟满足感。

“常言道:以大多数人努力程度之低根本轮不到拼天赋。我的版本:以大多数人满足感延迟程度之低,根本轮不到拼天赋。什么是努力?早出晚归,常年不休的人有很多,差别极大,区别好像不在努力。”

“延迟满足感”是什么呢?举个例子,给一群背景条件相同的小孩每人送一个糖果,实验人员告诉小孩,等一下他要出门,回来的时候如果糖果还在,那么就奖励双倍的糖果。

说完,实验人员就出去了,在这个等待的过程中,有些小孩忍不住把糖果吃了,而没有吃的小孩等实验人员回来后如愿获得了两倍的糖果奖励。没有吃掉糖果的小孩有足够的耐心,从而获得更多糖果。在今后的发展中,往往这种延迟满足感的忍耐和毅力,会让他获得更高的社会成就。

一个早起勤奋,谨慎诚实的人抱怨命运不好。良好的品格,优良的习惯,坚强的意志,是不会被假设所谓的命运击败的。学会延迟自己的满足感,你会获得更大的成就。成大事之人不一定对别人狠,但一定对自己狠。学会培养自己延迟满足的习惯,就要学会对自己狠。

一项关于“意志力”的研究成果表明,在早晨,人们更容易完成那些需要个人自律才能做到的事情,由此可见学会掌握自己早晨的时间对于培养自己良好习惯的重要性。

无论是早睡早起人,还是晚睡晚起的夜猫族,早晨只会在起床后才开始,不管几点,都会遇上起床后的黄金一小时,这段时间要「为自己做最重要的事」,为接下来一整天做好准备。


每天晚上,给自己第二天的学习和工作列一个To do list,早晨起来,可以按照自己的To do list一项一项完成计划,洗漱、晨练、阅读、学习、处理业务与杂务......

在此B哥给大家介绍一位非常有经验的时间管理大哥,前苏联的柳比歇夫。他一生发布了70余部学术著作,一共写了一万二千五百张打字稿的论文和专著。

在26岁时,这位老大哥独创了一种“时间统计法”,记录每个事件的花销时间,通过统计和分析,进行月小结和年终总结,以此来计划与改进工作,提高对时间的利用效率。

这位大师一直坚持了56年,直到逝世。他有一本书专门讲他的时间管理法,此书叫做《奇特的一生》,各位有兴趣可以去看一看。

习惯这个东西,具有水滴石穿的力量。一件微不足道的日常小事,如果你坚持去做,就能胜过那些艰难的大事。


从明天起,可以尝试着早起2个小时,并把这2个小时用于自我投资。每天2个小时,每周5天就是10个小时,每月就是40个小时,算一下,1年就能有480个小时了,这480个小时,你可以选择投资给自己,也可以决定扔进水里。

一年之计在于春,一日之计在于晨;一家之计在于和,一生之计在于勤。你怎样对待早晨,就是怎样对待你自己的人生

作者丨B哥
来源丨BAT技术(ID:BAT_ARCH)

收起阅读 »

算法. 无重复字符的最长子串

一、题目 难度中等 给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。 示例 1: 输入: s = "abcabcbb"输出: 3解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。 示例 2: 输入: s = "bbbbb"输出:...
继续阅读 »

一、题目


难度中等


给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。


示例 1:


输入: s = "abcabcbb"输出: 3解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。


示例 2:


输入: s = "bbbbb"输出: 1解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。


示例 3:


输入: s = "pwwkew"输出: 3解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。 请注意,你的答案必须是 子串 的长度,"pwke" 是一个*子序列,*不是子串。


示例 4:


输入: s = ""输出: 0


提示:


• 0 <= s.length <= 5 * 104


• s 由英文字母、数字、符号和空格组成


二、我的解答


第一次解答:


我的思路跟官方的差不多,把光标从第一个开始,寻找最长


只不过我是每次删除第一个就全部重新加,而不是他那个窗口移动的概念


public static int lengthOfLongestSubstring(String s) {
int selection = 0;
int maxSize=0;
while (selection < s.length()) {
HashMap map = new HashMap<>();
List mapList = new ArrayList<>();
for (int i = selection; i < s.length(); i++) {
char val = s.charAt(i);
if (!map.containsKey(val)) {
map.put(val, val);
mapList.add(val);
} else {
break;
}
}
maxSize=Math.max(mapList.size(),maxSize);
selection++;
if (mapList.size() >= (s.length() - selection)) {
break;
}
}
return maxSize;
}

通过:




三、系统解答


方法一:滑动窗口


思路及算法


我们先用一个例子考虑如何在较优的时间复杂度内通过本题。


我们不妨以示例一中的字符串abcabcbb 为例,找出从每一个字符开始的,不包含重复字符的最长子串,那么其中最长的那个字符串即为答案。对于示例一中的字符串,我们列举出这些结果,其中括号中表示选中的字符以及最长的字符串:


以(a)bcabcbb 开始的最长字符串为(abc)abcbb;


以a(b)cabcbb 开始的最长字符串为a(bca)bcbb;


以 ab(c)abcbb 开始的最长字符串为ab(cab)cbb;


以 abc(a)bcbb 开始的最长字符串为 abc(abc)bb;


以 abca(b)cbb 开始的最长字符串为abca(bc)bb;


以abcab(c)bb 开始的最长字符串为abcab(cb)b;


以abcabc(b)b 开始的最长字符串为abcabc(b)b;


以abcabcb(b) 开始的最长字符串为 abcabcb(b)。


发现了什么?如果我们依次递增地枚举子串的起始位置,那么子串的结束位置也是递增的!这里的原因在于,假设我们选择字符串中的第 k 个字符作为起始位置,并且得到了不包含重复字符的最长子串的结束位置为 r_k 。那么当我们选择第 k+1 个字符作为起始位置时,首先从 k+1到 r_k的字符显然是不重复的,并且由于少了原本的第 k 个字符,我们可以尝试继续增大 r_k,直到右侧出现了重复字符为止。


这样一来,我们就可以使用「滑动窗口」来解决这个问题了:


我们使用两个指针表示字符串中的某个子串(或窗口)的左右边界,其中左指针代表着上文中「枚举子串的起始位置」,而右指针即为上文中的 r_k;


在每一步的操作中,我们会将左指针向右移动一格,表示 我们开始枚举下一个字符作为起始位置,然后我们可以不断地向右移动右指针,但需要保证这两个指针对应的子串中没有重复的字符。在移动结束后,这个子串就对应着 以左指针开始的,不包含重复字符的最长子串。我们记录下这个子串的长度;


在枚举结束后,我们找到的最长的子串的长度即为答案。


判断重复字符


在上面的流程中,我们还需要使用一种数据结构来判断是否有重复的字符,常用的数据结构为哈希集合(即 C++ 中的 std::unordered_set,Java 中的 HashSet,Python 中的 set, JavaScript 中的 Set)。在左指针向右移动的时候,我们从哈希集合中移除一个字符,在右指针向右移动的时候,我们往哈希集合中添加一个字符。


至此,我们就完美解决了本题。


注释:思路和我一样,代码比我写的简单太多了吧


class Solution {    public int lengthOfLongestSubstring(String s) {
// 哈希集合,记录每个字符是否出现过
Set<Character> occ = new HashSet<Character>();
int n = s.length();
// 右指针,初始值为 -1,相当于我们在字符串的左边界的左侧,还没有开始移动
int rk = -1, ans = 0;
for (int i = 0; i < n; ++i) {
if (i != 0) {
// 左指针向右移动一格,移除一个字符
occ.remove(s.charAt(i - 1));
}
//判断rk+1<n是为了确定右指针是否已经走到了最后
//右指针走到最后的情况,说明后续都可以走到最后,只需用rk-i+1确定
while (rk + 1 < n && !occ.contains(s.charAt(rk + 1))) {
// 不断地移动右指针
occ.add(s.charAt(rk + 1));
++rk;
}
// 第 i 到 rk 个字符是一个极长的无重复字符子串
ans = Math.max(ans, rk - i + 1);
}
return ans;
}
}

复杂度分析


时间复杂度O(N),其中 N 是字符串的长度。左指针和右指针分别会遍历整个字符串一次。


空间复杂度:O(∣Σ∣),其中Σ 表示字符集(即字符串中可以出现的字符),∣Σ∣ 表示字符集的大小。在本题中没有明确说明字符集,因此可以默认为所有 ASCII 码在 [0,128) 内的字符,即 ∣Σ∣=128。我们需要用到哈希集合来存储出现过的字符,而字符最多有 ∣Σ∣ 个,因此空间复杂度为 O(∣Σ∣)。


这个网友的很精彩


class Solution {    public int lengthOfLongestSubstring(String s) {
// 记录字符上一次出现的位置
int[] last = new int[128];
for(int i = 0; i < 128; i++) {
last[i] = -1;
}
int n = s.length();

int res = 0;
int start = 0; // 窗口开始位置
for(int i = 0; i < n; i++) {
int index = s.charAt(i);
start = Math.max(start, last[index] + 1);
res = Math.max(res, i - start + 1);
last[index] = i;
}

return res;
}
}

答案有个缺点,左指针并不需要依次递增,即多了很多无谓的循环。 发现有重复字符时,可以直接把左指针移动到第一个重复字符的下一个位置即可。


每次左指针右移一位,移除set的一个字符,这一步会导致很多无用的循环。while循环发现的重复字符不一定就是Set最早添加那个,还要好多次循环才能到达,这些都是无效循环,不如直接用map记下每个字符的索引,直接进行跳转


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

Flutter 文字环绕

文字环绕 需求 最近接到一个需求,类似于文字环绕,标题最多两行,超出省略,标题后面可以添加标签。效果如下: 富文本不能控制省略和折行,Flutter 提供了 TextPainter可以实现。 分析 标签有文字和颜色两个属性,个数不定: class Tag {...
继续阅读 »

文字环绕


需求


最近接到一个需求,类似于文字环绕,标题最多两行,超出省略,标题后面可以添加标签。效果如下:


Simulator Screen Shot - iPhone 13 - 2022-02-24 at 16.49.54.png


富文本不能控制省略和折行,Flutter 提供了 TextPainter可以实现。


分析


标签有文字和颜色两个属性,个数不定:


class Tag {

/// 标签文本
final String label;
/// 标签背景颜色
final Color color;

Tag({required this.label, required this.color});
}

标题最大行数可变,可能明天产品要最多显示三行;


文本样式可变;


先创建出来对应的Widget


class TagTitle extends StatefulWidget {
const TagTitle(
this.text, {
Key? key,
required this.tags,
this.maxLines = 2,
this.style = const TextStyle(color: Colors.black, fontSize: 16),
}) : super(key: key);

final String text;
final int maxLines;
final TextStyle style;
final List<Tag> tags;
}

实现


标题文字和标签文字有两种显示情况:



  1. 超出最大行数;

  2. 未超出最大行数;


先假设第一种情况,因为标签前后有间距,所以每个标签前后补一个空格,再把标题和文字拼接创建对应的TextSpan


    tagTexts = widget.tags.fold<String>(
' ', (previousValue, element) => '$previousValue${element.label} ');

_allSp = TextSpan(
text: '${widget.text}$tagTexts',
style: widget.style,
);


要绘制标题、省略号、标签、都需要TextSpan,所以一并创建出来,当然还有最重要的TextPainter


// 标签
final tagsSp = TextSpan(
text: tagTexts,
style: widget.style,
);

// 省略号
final ellipsizeTextSp = TextSpan(
text: ellipsizeText,
style: widget.style,
);

// 标题
final textSp = TextSpan(
text: widget.text,
style: widget.style,
);

final textPainter = TextPainter(
text: tagsSp,
textDirection: TextDirection.ltr,
maxLines: widget.maxLines,
)..layout(
minWidth: constraints.minWidth,
maxWidth: constraints.maxWidth,
);

拿到标签、省略号、标题的尺寸:


final tagsSize = textPainter.size;

textPainter.text = ellipsizeTextSp;
textPainter.layout(
minWidth: constraints.minWidth,
maxWidth: constraints.maxWidth,
);

final ellipsizeSize = textPainter.size;

textPainter.text = textSp;
textPainter.layout(
minWidth: constraints.minWidth,
maxWidth: constraints.maxWidth,
);
final textSize = textPainter.size;

算出标题超出最大长度的位置:


textPainter.text = _allSp;
textPainter.layout(
minWidth: constraints.minWidth,
maxWidth: constraints.maxWidth,
);

final pos = textPainter.getPositionForOffset(Offset(
textSize.width - tagsSize.width - ellipsizeSize.width,
textSize.height,
));

final endIndex = textPainter.getOffsetBefore(pos.offset);

如果超出的话,文字显示区域的宽度减去标签宽度减去省略号宽度,剩下的位置就是标题最大宽度偏移量,根据偏移量得到此位置的文字位置下标。


textPainter.didExceedMaxLines返回的是是否超出最大长度,也就是一开始分析的两种情况的哪一种,如果超出,就根据上面计算出来的下标截取标题文字,添加省略号,然后添加上标签;否则,直接显示标题文本和标签:


TextSpan textSpan;

if (textPainter.didExceedMaxLines) {
textSpan = TextSpan(
style: widget.style,
text: widget.text.substring(0, endIndex) + ellipsizeText,
children: _getWidgetSpan(),
);
} else {
textSpan = TextSpan(
style: widget.style,
text: widget.text,
children: _getWidgetSpan(),
);
}
return RichText(
text: textSpan,
overflow: TextOverflow.ellipsis,
maxLines: widget.maxLines,
);

标签因为带有背景,所以可以用WidgetSpan加上标签背景,这里使用CustomPaint实现:


List<WidgetSpan> _getWidgetSpan() {
return widget.tags
.map((e) => WidgetSpan(
child: CustomPaint(
painter: BgPainter(e.color),
child: Text(
' ' + e.label + ' ',
style: widget.style,
),
),
))
.toList();
}

这个BgPainter就一个功能,绘制背景色:


class BgPainter extends CustomPainter {
final Paint _painter;

final Color color;

BgPainter(this.color) : _painter = Paint()..color = color;

@override
void paint(Canvas canvas, Size size) {
canvas.drawRect(Rect.fromLTWH(2, 0, size.width - 2, size.height), _painter);
}

@override
bool shouldRepaint(covariant CustomPainter oldDelegate) =>
oldDelegate != this;
}

使用:


TagTitle(
'据法新社报道,法国总统府爱丽舍宫发布消息称,法国',
tags: [
Tag(label: '热门', color: Colors.red),
Tag(label: '国际', color: Colors.blue),
],
),
const Divider(
color: Color(0xFF167F67),
),
TagTitle(
'据法新社报道,法国总统府爱丽舍宫发布消息称,法国总统马克龙提议俄罗斯总统普京和美国总统sadaasdadadada',
tags: [
Tag(label: '热门', color: Colors.red),
Tag(label: '国际', color: Colors.blue),
],
),
const Divider(
color: Color(0xFF167F67),
),
TagTitle(
'据法新社报道,法国总统府爱丽舍宫发布',
tags: [
Tag(label: '热门', color: Colors.red),
Tag(label: '国际', color: Colors.blue),
],
),
const Divider(
color: Color(0xFF167F67),
),
TagTitle(
'据法新社报道,法国总统府爱丽舍宫发布消息称,法国总统马克龙提议俄罗',
tags: [
Tag(label: '热门', color: Colors.red),
Tag(label: '国际', color: Colors.blue),
],
),

附上完整代码:


main.dart


import 'package:custom/review/tag_title.dart';
import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}

class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);

final String title;

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TagTitle(
'据法新社报道,法国总统府爱丽舍宫发布消息称,法国',
tags: [
Tag(label: '热门', color: Colors.red),
Tag(label: '国际', color: Colors.blue),
],
),
const Divider(
color: Color(0xFF167F67),
),
TagTitle(
'据法新社报道,法国总统府爱丽舍宫发布消息称,法国总统马克龙提议俄罗斯总统普京和美国总统sadaasdadadada',
tags: [
Tag(label: '热门', color: Colors.red),
Tag(label: '国际', color: Colors.blue),
],
),
const Divider(
color: Color(0xFF167F67),
),
TagTitle(
'据法新社报道,法国总统府爱丽舍宫发布',
tags: [
Tag(label: '热门', color: Colors.red),
Tag(label: '国际', color: Colors.blue),
],
),
const Divider(
color: Color(0xFF167F67),
),
TagTitle(
'据法新社报道,法国总统府爱丽舍宫发布消息称,法国总统马克龙提议俄罗',
tags: [
Tag(label: '热门', color: Colors.red),
Tag(label: '国际', color: Colors.blue),
],
),
],
),
);
}
}

Tag_title.dart:


//@dart=2.12
import 'package:flutter/material.dart';

import 'bg_painter.dart';

class TagTitle extends StatefulWidget {
const TagTitle(
this.text, {
Key? key,
required this.tags,
this.maxLines = 2,
this.style = const TextStyle(color: Colors.black, fontSize: 16),
}) : super(key: key);

final String text;
final int maxLines;
final TextStyle style;
final List<Tag> tags;

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

class TagTitleState extends State<TagTitle> {
late final String tagTexts;
late final TextSpan _allSp;
final String ellipsizeText = '...';

@override
void initState() {
super.initState();
tagTexts = widget.tags.fold<String>(
' ', (previousValue, element) => '$previousValue${element.label} ');
_allSp = TextSpan(
text: '${widget.text}$tagTexts',
style: widget.style,
);
}

List<WidgetSpan> _getWidgetSpan() {
return widget.tags
.map((e) => WidgetSpan(
child: CustomPaint(
painter: BgPainter(e.color),
child: Text(
' ' + e.label + ' ',
style: widget.style,
),
),
))
.toList();
}

@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
assert(constraints.hasBoundedWidth);
// 标签
final tagsSp = TextSpan(
text: tagTexts,
style: widget.style,
);

// 省略号
final ellipsizeTextSp = TextSpan(
text: ellipsizeText,
style: widget.style,
);

// 标题
final textSp = TextSpan(
text: widget.text,
style: widget.style,
);

final textPainter = TextPainter(
text: tagsSp,
textDirection: TextDirection.ltr,
maxLines: widget.maxLines,
)..layout(
minWidth: constraints.minWidth,
maxWidth: constraints.maxWidth,
);
final tagsSize = textPainter.size;

textPainter.text = ellipsizeTextSp;
textPainter.layout(
minWidth: constraints.minWidth,
maxWidth: constraints.maxWidth,
);

final ellipsizeSize = textPainter.size;

textPainter.text = textSp;
textPainter.layout(
minWidth: constraints.minWidth,
maxWidth: constraints.maxWidth,
);
final textSize = textPainter.size;

textPainter.text = _allSp;
textPainter.layout(
minWidth: constraints.minWidth,
maxWidth: constraints.maxWidth,
);

final pos = textPainter.getPositionForOffset(Offset(
textSize.width - tagsSize.width - ellipsizeSize.width,
textSize.height,
));

final endIndex = textPainter.getOffsetBefore(pos.offset);

TextSpan textSpan;

if (textPainter.didExceedMaxLines) {
textSpan = TextSpan(
style: widget.style,
text: widget.text.substring(0, endIndex) + ellipsizeText,
children: _getWidgetSpan(),
);
} else {
textSpan = TextSpan(
style: widget.style,
text: widget.text,
children: _getWidgetSpan(),
);
}
return RichText(
text: textSpan,
overflow: TextOverflow.ellipsis,
maxLines: widget.maxLines,
);
},
);
}
}
class Tag {

/// 标签文本
final String label;
/// 标签背景颜色
final Color color;

Tag({required this.label, required this.color});
}

bg_painter.dart:


//@dart=2.12
import 'dart:ui' as ui;

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

class BgPainter extends CustomPainter {
final Paint _painter;

final Color color;

BgPainter(this.color) : _painter = Paint()..color = color;

@override
void paint(Canvas canvas, Size size) {
canvas.drawRect(Rect.fromLTWH(2, 0, size.width - 2, size.height), _painter);
}

@override
bool shouldRepaint(covariant CustomPainter oldDelegate) =>
oldDelegate != this;
}

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

俄罗斯自己拔网线了,启用本国互联网,咋回事?

今天,有这么一条新闻:俄罗斯准备跟全球互联网断开,启用本国互联网 Runet 。差评君简单刷了一下评论,发现网友们除了对俄罗斯的本国互联网感到好奇外,还不少人又回忆起西方国家在互联网上的独特优势,担忧了起来。被讨论到最多的一点是:根服务器主要被设置在美国,所以...
继续阅读 »

今天,有这么一条新闻:俄罗斯准备跟全球互联网断开,启用本国互联网 Runet 。


差评君简单刷了一下评论,发现网友们除了对俄罗斯的本国互联网感到好奇外,还不少人又回忆起西方国家在互联网上的独特优势,担忧了起来。

被讨论到最多的一点是:根服务器主要被设置在美国,所以美国能让任何一个国家网络 “ 瘫痪 ” 。

今天有可能威胁到俄罗斯,明天说不好就会拿来针对咱们。

甚至还有这样的言论: “ 美国一断网,咱们现在引以为傲的数字化可能会瞬间瘫痪 ” ↓↓


今天咱们就来聊聊这个事儿吧。

1美国真能通过根服务器,让某个国家的互联网 “ 瘫痪 ” 么?

2俄罗斯这个本国互联网 Runet ,到底是个啥?

要知道根服务器对网络有多大影响力,首先咱们要知道我们现在是怎么上网的。

就好像你要去拜访朋友家,就要先问清楚,他家住哪个小区几栋几单元一样。

我们的电脑想要访问某个网站,就要知道网站 IP 地址,网站的 IP 地址一般是一串数字,比如 B 站对应的 IP 地址是120.92.78.97 。


但上网又不是记忆考试,谁闲着没事要记一大堆无规律的数字串啊。

为了方便记忆,人们决定给这些地址一一备注,比如 bilibili.com 对应的是120.92.78.97 ,我们只要记住哔哩哔哩就好了, bilibili.com 就是 B 站的域名。

每一个 IP 地址对应一个域名,这些对应值会形成一个个列表,保存在DNS 服务器中。

我们想要访问某个网站,电脑要先去拜访 DNS 服务器,找到网站域名对应的 IP 地址来访问网站,这个过程就叫域名解析。


当然,全世界有这么多人上网,仅仅是一台服务器肯定支撑不了庞大的解析请求。

所以人们用了一个层级管理的架构来进行域名管理,层层往上,最后在所有服务器顶端的服务器就叫根服务器。

具体怎么查询呢?

理论上来说,比如你要访问 http://www.baidu.com ,就先去问 ICANN ( 这哥们是域名最高管理机构 )的根域名列表。

它会告诉你,我哪有空管这么细的事,你去问. com 域名的托管商,托管商是 Verisign 。

然后你再去找对应的顶级域名托管商,对应的托管机构会告诉你 baidu.com 服务器在哪。


但实际上,根域名列表很少变化,大多数 DNS 服务商都会提供它的缓存。

所以平时大家会就近访问。

先看一圈浏览器里有没有对应的缓存,再检查一圈操作系统里面有没有,在问了一圈都没有缓存以后,再层层往上查询,查到为止。

只有到处都没有缓存的时候,才会去找根。

了解完根服务器是什么,有什么用以后,你会发现,这玩意不会让网站本身 “ 瘫痪 ” ,只是会影响到大家对网站的访问。

就像你弄丢了朋友家的地址信息,这不会让你朋友家消失或出问题,只是你找不到路,去不了了。


而且嗷。。。

虽然根服务器确实就那么几个,但是跟他们保持同步缓存的 DNS 服务器有一大堆。

截至2019年,全球范围内已经部署1164个根服务器镜像节点,我国有12个根服务器镜像节点。

美国真发动了所谓的根服务器攻击,修改了手中的根服务器中的信息,咱们可以选择不同步信息。。。

有这些镜像服务器,国内的网站,甚至部分国外的网站,不需要美国的根服务器就可以进行互相访问。


所以,如果有人告诉你美国动动手指就能瘫痪咱们的网络,请回复他:too young !

实际上,如果配置合理,每个国家都可以实现在不访问根服务器的情况下,让国内互联网正常运行。

俄罗斯这次的 Runet ,就是这么一个互联网基础设施。

通过对国内流量重新路由,利用自己控制的域名系统、服务器构建网络,当遭遇 “ 根服务器攻击 ” ,又或俄罗斯自己想切断网络,防御境外的网络攻击时,确保国内网络不受影响。

其实早在2019年俄罗斯就已经成功测试了 “ 断网 ” ~


与其纠结 “ 根服务器攻击 ” ,也许威胁更大的是欧美企业对俄罗斯地区的服务限制和业务撤退。

汽车制造商通用汽车和沃尔沃已经暂停对俄罗斯的汽车出口。

世界上最大的两家航运公司地中海航运( MSC )和马士基航运暂停了往返俄罗斯的集装箱运输。

苹果近日也发声明称要暂停在俄罗斯销售苹果产品并且限制苹果支付功能。

根据2020年的数据, 29% 的俄罗斯人报告使用 Google Pay ,而20% 使用 Apple Pay ,感觉多少他们的支付会受到一些影响。

这几天,制裁还蔓延到了俄罗斯的猫身上。猫科动物国际联合会在官方社交媒体账号呼吁,停止进口饲养于俄罗斯境内的猫等。。。


阿迪达斯也宣布暂停跟俄罗斯足协的合作,大家都上赶着要制裁俄罗斯。

像 “ 根服务器攻击 ” 、 “ 断网 ” ,早就过时啦。。。

真遇上事儿,人也许不觉得拔网线这事儿有威胁,只觉得外围那一圈黑客吵闹。

来源:差评 https://www.163.com/dy/article/H1HM3T7D051196HN.html

收起阅读 »

【直播公开课】Discord模式等十大泛娱乐场景全解析

3月10日(周四)19:00诚邀您观看环信第58期线上直播公开课《Discord模式等十大场景全解析,带你玩"赚"泛娱乐行业》。本期公开课将带你了解泛娱乐行业的现状和创新,环信在泛娱乐领域的一些新场景玩法,目前市场最火的Discord模式解读,以及环信超级社区...
继续阅读 »

3月10日(周四)19:00诚邀您观看环信第58期线上直播公开课《Discord模式等十大场景全解析,带你玩"赚"泛娱乐行业》。本期公开课将带你了解泛娱乐行业的现状和创新,环信在泛娱乐领域的一些新场景玩法,目前市场最火的Discord模式解读,以及环信超级社区Demo抢先看。锁定环信公开课直播间,同时还抽取环信定制周边礼品!


疫情肆虐两年有余,很多传统的线下娱乐方式示微,这也促使海内外泛娱乐产业出现井喷。泛娱乐行业打破了产业的壁垒,让跨界变得容易,让现代互联网技术与传统产业得到了结合。为视频技术、通讯技术、AR\VR\AI带来新的增长点并推动革新,为疫情下的经济提供了新的增长点,促进了就业,带动了虚拟经济、实体经济的协同发展。

本次课程介绍了环信在泛娱乐领域的一些新玩法,包括discord模式、兴趣社交、游戏、K歌、语音电台等场景,现场还将直接演示环信超级社区Demo,与大家共同探讨行业发展的前景和机会,邀请您来一次不一样的思想碰撞。



直播课报名:

课程主题:Discord模式等十大场景全解析,带你玩"赚"泛娱乐行业
直播时间:3月10日(周四) 19:00-20:00
报名地址:
https://mudu.tv/live/watch/general?id=lrx9y‍val

讲师介绍:


王林
环信解决方案总监
云通讯领域从业十年,对社交泛娱乐行业技术信息化有深刻的洞察和思考,为众多世界500强企业客户提供产品和解决方案等服务。

你能获得什么:

了解泛娱乐行业的现状
泛娱乐领域有哪些玩法以及创新尝试
海外泛娱乐市场浅析
泛娱乐领域所需的主要组成单元
Discord产品模式解读
类Discord环信超级社区DEMO抢先看

技术交流群

识别⬇️二维码进群参与抽奖^-^






收起阅读 »

官方推荐Flow,LiveData:那我走?

记得在之前掘金上看到Google开发者的账号发了一篇《从 LiveData 迁移到 Kotlin 数据流》的文章。在之前接触ViewModel和LiveDta的时候就有在思考,ViewModel和Repository之间交互,通过什么来实现。后来翻了一下资料,...
继续阅读 »

记得在之前掘金上看到Google开发者的账号发了一篇《从 LiveData 迁移到 Kotlin 数据流》的文章。在之前接触ViewModel和LiveDta的时候就有在思考,ViewModel和Repository之间交互,通过什么来实现。后来翻了一下资料,发现官方推荐在ViewModel和Repository通过Flow来作为桥梁进行交互。


491616319700_.jpg


为了响应官方号召,我又一顿了解Flow。但在了解了Flow之后,当时心中就有大大的疑惑,Flow能够实现LiveData的功能,并且比LiveData功能更加强大,为什么不使用Flow来作为View和ViewModel之间的桥梁。


而现在官方确实推荐将Flow作为方案来替代LiveData。LiveData一脸懵逼:那我走?



image.png


LiveData


LiveData在2017年推出以来,作为Jetpack大家族的元老级人物,为安卓的MVVM架构作出了非凡的贡献,毕竟在当时的背景环境,大家都深陷RxJava的支配。而LiveData作为观察者模式的框架,能够以更平滑的学习曲线来实现变量的订阅,比起RxJava那一套更加轻量级,而且作为Google的亲儿子,在生命周期的管理上也有更出色的表现。



image.png


LiveData的缺点:


而LiveData它的缺点其实也非常明显,LiveData肩负着为UI提供数据订阅的能力,所以他的数据订阅只能在主线程,可能会有小伙伴说可以在子线程通过postValue去发布数据啊。但是其实这个postValue是有坑的,被坑过的小伙伴都应该知道短时间通过多次postValue,中间可能会存在数据的丢失。


而且在复杂的场景LiveData支持的能力确实有一些尴尬。


总结一下LiveDta有几个缺点:




  • 在异步线程修改数据可能存在数据丢失的问题




  • 在复杂的场景,LiveData的能力有一些捉襟见肘




LiveData你别走


但我们也不应该踩一捧一,确实LiveData整体上有更低的学习成本,在一些简单的场景LiveData已经完全能够满足我们的需要。


而且官方也说过并不会废弃LiveData,原因是:



  • 用 Java 写 Android 的人还需要它,因为Flow是协程的东西,所以如果你是用 Java 的,是没有办法使用Flow的,所以LiveData还是有意义的。

  • LiveData 的使用比较简单,而且功能上对于简单场景也是足够的,而 RxJava 和 Flow 这种东西学起来就没 LiveData 那么直观。


Flow


Flow是Google官方提供的一个类似于RxJava的响应式编程模型。它是基于Kotlin协程的。
它相对于Rxjava具有以下特点:



  • 具有更友好的API,学习成本较低

  • 跟Kotlin协程、LiveData结合更紧密,Flow能够转换成LiveData,在ViewModel中直接使用

  • 结合协程的作用域,当协程被取消时,Flow也会被取消,避免内存泄漏


我们知道Flow的特点之一就是冷流。那么什么是冷流呢?




  • 冷流:当数据被订阅的时候,发布者才开始执行发射数据流的代码。并且当有多个订阅者的时候,每一个订阅者何发布者都是一对一的关系,每个订阅者都会收到发布者完整的数据。




  • 热流:无论有没有订阅者订阅,事件始终都会发生。当热流有多个订阅者时,发布者跟订阅者是一对多的关系,热流可以与多个订阅者共享信息。




StateFlow


因为Flow是冷流,这与LiveData的特点完全不一样,因此Flow提供了StateFlow来实现热流。


StateFlowSharedFlow 的一个比较特殊的变种,而 SharedFlow 又是 Kotlin 数据流当中比较特殊的一种类型。StateFlow 与 LiveData 是最接近的,因为:



  • 它始终是有值的。

  • 它的值是唯一的。

  • 它允许被多个观察者共用 (因此是共享的数据流)。

  • 它永远只会把最新的值重现给订阅者,这与活跃观察者的数量是无关的。


官方推荐当暴露 UI 的状态给视图时,应该使用 StateFlow。这是一种安全和高效的观察者,专门用于容纳 UI 状态。


StateFlow使用


StateFlow替换掉LiveData是简单的。我们来看看StateFlow的构造函数:


/**
* Creates a [MutableStateFlow] with the given initial [value].
*/
@Suppress("FunctionName")
public fun <T> MutableStateFlow(value: T): MutableStateFlow<T> = StateFlowImpl(value ?: NULL)

我们在ViewModel上游中不断的发送值,View层通过collect函数去获取到上游发送的数据。


StateFlow只有在值发生改变时才会返回,如果发生更新但值没有变化时,StateFlow不会回调collect函数,但LiveData会进行回调。


stateIn


StateIn 能够将普通的流转换为StateFlow,但转换之后还需要一些配置工作.


image.png



  • scope 共享开始时所在的协程作用域范围

  • started 控制共享的开始和结束的策略

    • Lazily: 当首个订阅者出现时开始,在 scope 指定的作用域被结束时终止。

    • Eagerly: 立即开始,而在 scope 指定的作用域被结束时终止。

    • WhileSubscribed能够指定当前不有订阅者后,多少时间取消上游数据和能够指定多少时间后,缓存中的数据被丢失,回复称initialValue的值。



  • initialValue 初始值


WhileSubscribed


WhileSubscribed 策略会在没有收集器的情况下取消上游数据流。通过 stateIn 运算符创建的 StateFlow 会把数据暴露给视图 (View),同时也会观察来自其他层级或者是上游应用的数据流。让这些流持续活跃可能会引起不必要的资源浪费,例如一直通过从数据库连接、硬件传感器中读取数据等等。当您的应用转而在后台运行时,您应当保持克制并中止这些协程


@Suppress("FunctionName")
public fun WhileSubscribed(
stopTimeoutMillis: Long = 0,
replayExpirationMillis: Long = Long.MAX_VALUE
): SharingStarted =
StartedWhileSubscribed(stopTimeoutMillis, replayExpirationMillis)

WhileSubscribed


WhileSubscribed支持传入stopTimeoutMillisreplayExpirationMillis参数。


其中stopTimeoutMillis支持设置超时停止的效果,单位为ms。当最后一个订阅者不再订阅上游时,StateFlow会停止上游数据的发送。


这样就可以提供APP的性能,当没有订阅者时或者应用被切到后台后会等待stopTimeoutMillis设置的时间后上游会停止发送数据,并且会缓存停止前的缓存数据。


replayExpirationMillis


如果当上游如果停止发送太久,这时候StateFlow中缓存的数据是比较陈旧的数据,当这时候有订阅者时,我们不希望给订阅者陈旧的数据。我们可以设置replayExpirationMillis参数,当停止共享携程超过设置的replayExpirationMillis时间后,StateFlow中会将缓存重置为默认值。


在视图中观察数据


ViewModel中的StateFlow需要结合生命周期知道他们已经不在需要感知到何时不再需要被监听。我们在View视图层提供了若干个协程构建器。



  • Activity.lifecycleScope.launch : 启动协程,并且在本 Activity 销毁时结束协程。

  • Fragment.lifecycleScope.launch : 启动协程,并且在本 Fragment 销毁时结束协程。

  • Fragment.viewLifecycleOwner.lifecycleScope.launch : 启动协程,并且在本 Fragment 中的视图生命周期结束时取消协程。

  • launchWhenX :启动协程,它会在 lifecycleOwner 进入 X 状态之前一直等待,又在离开 X 状态时挂起协程。


image.png


通过上面官方的这个图,我们可以看出当APP进入后台时,如果APP还在后台收集数据更新可能引发应用崩溃和资源的浪费。


repeatOnLifecycle


因此google官方提供了新的API接口repeatOnLifecycle能够在某个特定的状态满足时启动协程,并且在生命周期所有者退出该状态时停止协程。


image.png


当视图处于 STARTED 状态时会开始收集流,并且在 RESUMED 状态时保持收集,最终在视图进入 STOPPED 状态时结束收集过程。


image.png


使用repeatOnLifecycle和StateFlow能够帮助我们应用根据应用生命周期优化性能和设备资源。


通过repeatOnLifecycleStateFlow能够帮助我们更好管理数据流。最后以官方的一句话结束本文。



当然,如果您并不需要使用到 Kotlin 数据流的强大功能,就用 LiveData 好了 :)


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

安全私密的聊天系统可免费使用可转让可定制

iOS
安全私密的聊天系统可免费使用可转让可定制超级稳定的聊天通信系统可在线免费使用 可聊天、红包、转账、超级大群超过20台服务器承载均衡保证超级大群永不丢包,保证不卡顿 用过才知好注册超级方便,国内国外均可注册使用,支持充值提现,支持多人语音视频想下载只需appst...
继续阅读 »


安全私密的聊天系统可免费使用可转让可定制

超级稳定的聊天通信系统可在线免费使用 可聊天、红包、转账、超级大群
超过20台服务器承载均衡保证超级大群永不丢包,保证不卡顿 用过才知好
注册超级方便,国内国外均可注册使用,支持充值提现,支持多人语音视频

想下载只需appstore搜索“兔八” 免费下载使用
安卓版本请前往以下地址下载:tuba2007.com

对于系统源码和定制的问题:

系统支持android、ios、pc(c/s)、webpc、h5等版本

功能具备 系统稳定 团队在通信行业从事十二年

针对大的集团客户高并发做了针对性优化和处理

前端在秒内频繁收发消息上进行了极致优化 可以做到百万级并发不
卡顿不丢包 可合同约束此条款

在功能上除了具备微信常备功能(单聊、群聊、充值提现、h5外链扩展、各红包类型、单以及多人
语音视频、红包转账、位置焚毁、收藏转发、语音翻译、多端同步等等)外,最主要可对超级大群

进行扩展 可保证在达到2000大群的情况下频繁红包等消息类型的调取收发不卡顿 底层架构支持
并能支撑超级大群频繁多开和多处理

在扩展上做了处理和升级

可快速扩展新的消息类型以及在系统扩展第三方应用 可实现与第三方系统的用户同步与数据互通

支持源码合作以及开发定制合作

支持Saas

支持私有化服务器快速t1独立部署

支持一键快速销毁服务器所有存储非存储数据
功能不一一叙述、
需要体验以及合作的加

V:youhuisam 球球:383189941

收起阅读 »

Android自定义View第五弹(可滑动的星星评价)

距离上一篇自定义view已经过去了一年多了,这次主要给大家介绍的是可滑动的星星评价,虽然Google官方也提供了 RatingBar 但是没办法满足我的需要只能自己定义一个了,废话不多说先上图: 这个选中以及默认的心型都是UI提供的图片,上代码: 1.自定...
继续阅读 »

距离上一篇自定义view已经过去了一年多了,这次主要给大家介绍的是可滑动的星星评价,虽然Google官方也提供了 RatingBar 但是没办法满足我的需要只能自己定义一个了,废话不多说先上图:
在这里插入图片描述
这个选中以及默认的心型都是UI提供的图片,上代码:


1.自定义view的代码


import android.content.Context
import android.graphics.*
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import cn.neoclub.uki.R
import kotlin.math.abs
import kotlin.math.ceil
import kotlin.math.roundToInt

/**
* Author: Mr.Dong
* Date: 2022/2/15 4:31 下午
* Description: 点击心心评价
*/
class HeartRatingBar : View {
private var starDistance = 0 //星星间距
private var starCount = 5 //星星个数
private var starSize = 0 //星星高度大小,星星一般正方形,宽度等于高度
private var starMark = 0 //评分星星
private var starFillBitmap: Bitmap? = null //亮星星
private var starEmptyDrawable : Drawable? = null//暗星星
private var onStarChangeListener : OnStarChangeListener? = null//监听星星变化接口

private var paint : Paint? = null//绘制星星画笔
//是否显示整数的星星
private var integerMark = false
//初始化可以被定义为滑动的距离(超过这个距离就是滑动,否则就是点击事件)
private var scaledTouchSlop:Int=0

constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
init(context, attrs)
}

constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
init(context, attrs)
}

/**
* 初始化UI组件
*
* @param context
* @param attrs
*/
private fun init(context: Context, attrs: AttributeSet?) {
//获取滑动的有效距离
scaledTouchSlop=ViewConfiguration.get(context).scaledTouchSlop
isClickable = true
//获取各种属性的值
val mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.HeartRatingBar)
starDistance = mTypedArray.getDimension(R.styleable.HeartRatingBar_starDistance, 0f).toInt()
starSize = mTypedArray.getDimension(R.styleable.HeartRatingBar_starSize, 20f).toInt()
starCount = mTypedArray.getInteger(R.styleable.HeartRatingBar_starCount, 5)
starEmptyDrawable = mTypedArray.getDrawable(R.styleable.HeartRatingBar_starEmpty)
starFillBitmap = drawableToBitmap(mTypedArray.getDrawable(R.styleable.HeartRatingBar_starFill))
mTypedArray.recycle()
paint = Paint()
//设置抗锯齿
paint?.isAntiAlias = true
//设置渲染器
paint?.shader = BitmapShader(starFillBitmap!!, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
}

/**
* 设置是否需要整数评分
* @param integerMark
*/
fun setIntegerMark(integerMark: Boolean) {
this.integerMark = integerMark
}

/**
* 设置显示的星星的分数
*
* @param mark
*/
private fun setStarMark(mark: Int) {
starMark = if (integerMark) {
//ceil函数 去除小数点后面的 返回 double 类型,返回值大于或等于给定的参数 例Math.ceil(100.675) = 101.0
ceil(mark.toDouble()).toInt()
} else {
(mark * 10).toFloat().roundToInt() * 1 / 10
}
if (onStarChangeListener != null) {
onStarChangeListener?.onStarChange(starMark) //调用监听接口
}
invalidate()
}

/**
* 获取显示星星的数目
*
* @return starMark
*/
fun getStarMark(): Int {
return starMark
}

/**
* 定义星星点击的监听接口
*/
interface OnStarChangeListener {
fun onStarChange(mark: Int)
}

/**
* 设置监听
* @param onStarChangeListener
*/
fun setOnStarChangeListener(onStarChangeListener: OnStarChangeListener?) {
this.onStarChangeListener = onStarChangeListener
}

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
//设置view的宽度和高度 继承view必须重写此方法
setMeasuredDimension(starSize * starCount + starDistance * (starCount - 1), starSize)
}

override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (starFillBitmap == null || starEmptyDrawable == null) {
return
}
//绘制空的星星
for (i in 0 until starCount) {
//设置starEmptyDrawable绘制的长方形区域,当调用draw()方法后就可以直接绘制
starEmptyDrawable?.setBounds(
(starDistance + starSize) * i,
0,
(starDistance + starSize) * i + starSize,
starSize
)
starEmptyDrawable?.draw(canvas)
}
if (starMark > 1) {
//绘制了第一个star
canvas.drawRect(0f, 0f, starSize.toFloat(), starSize.toFloat(), paint!!)
if (starMark - starMark == 0) { //第一步必走这里
//绘制亮星星
for (i in 1 until starMark) {
//每次位移start的宽度+间距
canvas.translate((starDistance + starSize).toFloat(), 0f)
canvas.drawRect(0f, 0f, starSize.toFloat(), starSize.toFloat(), paint!!)
}
} else { //非整形的star绘制走这里
for (i in 1 until starMark - 1) {
canvas.translate((starDistance + starSize).toFloat(), 0f)
canvas.drawRect(0f, 0f, starSize.toFloat(), starSize.toFloat(), paint!!)
}
canvas.translate((starDistance + starSize).toFloat(), 0f)
canvas.drawRect(
0f,
0f,
starSize * (((starMark - starMark) * 10).toFloat().roundToInt() * 1.0f / 10),
starSize.toFloat(),
paint!!
)
}
} else {
//startMark=0 啥都没绘制
canvas.drawRect(0f, 0f, (starSize * starMark).toFloat(), starSize.toFloat(), paint!!)
}
}

//记录一下上次down的x的位置
private var downX:Int=0

override fun onTouchEvent(event: MotionEvent): Boolean {
var x = event.x.toInt()
if (x < 0) x = 0
if (x > measuredWidth) x = measuredWidth
when (event.action) {
MotionEvent.ACTION_DOWN -> {
downX=x
//对于除数不能为0的限制
if(starCount==0||(measuredWidth * 1 / starCount)==0){
return false
}
val count=x * 1 / (measuredWidth * 1 / starCount)
setStarMark(count+1)
}
MotionEvent.ACTION_MOVE -> {
//当滑动距离的绝对值小于官方定义的有效滑动距离则不走move当做down处理
if(abs(event.x-downX)<scaledTouchSlop){
return false
}
if(starCount==0||(measuredWidth * 1 / starCount)==0){
return false
}
setStarMark(x * 1 / (measuredWidth * 1 / starCount))
}
MotionEvent.ACTION_UP -> {}
}
invalidate()
return super.onTouchEvent(event)
}

/**
* drawable转bitmap
*
* @param drawable
* @return
*/
private fun drawableToBitmap(drawable: Drawable?): Bitmap? {
if (drawable == null) return null
val bitmap = Bitmap.createBitmap(starSize, starSize, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, starSize, starSize)
drawable.draw(canvas)
return bitmap
}
}

2.自定义View的使用


    <cn.neoclub.uki.message.widget.HeartRatingBar
android:id="@+id/rb_rating_bar"
android:layout_width="match_parent"
android:layout_height="40dp"
app:starCount="5"
app:starDistance="7dp"
app:starEmpty="@drawable/icon_heart_rating_default"
app:starFill="@drawable/icon_heart_rating_select"
app:starSize="40dp" />

3.attrs.xml文件中的属性


 <declare-styleable name="HeartRatingBar">
<attr name="starDistance" format="dimension"/>
<attr name="starSize" format="dimension"/>
<attr name="starCount" format="integer"/>
<attr name="starEmpty" format="reference"/>
<attr name="starFill" format="reference"/>
</declare-styleable>

4.送你两张图,怕你运行不起来


1.icon_heart_rating_select.png


在这里插入图片描述


2.icon_heart_rating_default.png


在这里插入图片描述


是不是感觉这边少了个icon,对了就是少一张😄(其实是有图的)


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

Android监听截屏

Android系统没有提供默认的截屏事件监听方式,需要开发者自己想办法实现。查看了网上推荐的实现方式,主要是通过内容观察者(ContentObserver)监听媒体数据库的变化,根据内容名称(路径)中是否包含关键字,判断是否为截屏事件。 关键字: pr...
继续阅读 »

Android系统没有提供默认的截屏事件监听方式,需要开发者自己想办法实现。查看了网上推荐的实现方式,主要是通过内容观察者(ContentObserver)监听媒体数据库的变化,根据内容名称(路径)中是否包含关键字,判断是否为截屏事件。
关键字:


    private static final String[] KEYWORDS = {
"screenshot", "screen_shot", "screen-shot", "screen shot",
"screencapture", "screen_capture", "screen-capture", "screen capture",
"screencap", "screen_cap", "screen-cap", "screen cap", "snap", "截屏"
};

第一步:对ContentResolver添加内、外存储变化监听;


mInternalObserver = new MediaContentObserver(MediaStore.Images.Media.INTERNAL_CONTENT_URI, MainHandler.get());
mExternalObserver = new MediaContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, MainHandler.get());
mResolver = AppContext.get().getContentResolver();
// 添加监听
mResolver.registerContentObserver(
MediaStore.Images.Media.INTERNAL_CONTENT_URI,
false,
mInternalObserver
);
mResolver.registerContentObserver(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
false,
mExternalObserver
);

第二步:当内容观察者(ContentObserver)监听到变化时,会调用onChange方法,此时,我们使用ContentResolver去查询最新的一条数据;
需要注意的是,查询外部存储一定要有读取存储权限(Manifest.permission.READ_EXTERNAL_STORAGE),否则会在查询的时候报错;
第三步:判断查到到数据是否为截图文件;在这里有一个很难处理到问题,在华为荣耀手机上,截图预览图生成的同时就会通知存储内容变化,而小米则是在截图预览图消失后通知变化;
解决方案:



  1. 判断当前文件路径是否与上次有效路径相同,相同执行步骤2,不相同则执行步骤3;

  2. 当前路径与上次路径相同,取消回调请求,重新延迟发送回调请求;

  3. 当前路径与上次路径不同,判断内容的生成时间(MediaStore.Images.ImageColumns.DATE_TAKEN)和添加时间(MediaStore.Images.ImageColumns.DATE_ADDED)是否相同,相同执行步骤4,不相同则执行步骤5;

  4. 内容的生成时间和添加时间相同,认为此时为生成长截图,立刻取消回调请求,执行空回调(用于取消弹窗等操作);

  5. 内容的生成时间和添加时间不同,检查是否含有关键字,若判定为截图,更新上次有效路径,取消回调请求,重新延迟发送回调请求;


 // 获取各列的索引
int dataIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);
int dateTakenIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_TAKEN);
int dateAddIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_ADDED);
// 获取行数据
final String data = cursor.getString(dataIndex);
long dateTaken = cursor.getLong(dateTakenIndex);
long dateAdded = cursor.getLong(dateAddIndex);
if (data.length() > 0) {
if (TextUtils.equals(lastData, data)) {
MainHandler.get().removeCallbacks(shotCallBack);
MainHandler.get().postDelayed(shotCallBack, 500);
} else if (dateTaken == 0 || dateTaken == dateAdded * 1000) {
MainHandler.get().removeCallbacks(shotCallBack);
if (listener != null) {
listener.onShot(null);
}
} else if (checkScreenShot(data)) {
MainHandler.get().removeCallbacks(shotCallBack);
lastData = data;
MainHandler.get().postDelayed(shotCallBack, 500);
}
}

完整代码:(其中AppContext为全局Application单例,MainHandler为全局主线程Handler单例)


public class ScreenShotHelper {
private static final String[] KEYWORDS = {
"screenshot", "screen_shot", "screen-shot", "screen shot",
"screencapture", "screen_capture", "screen-capture", "screen capture",
"screencap", "screen_cap", "screen-cap", "screen cap", "snap", "截屏"
};

/**
* 读取媒体数据库时需要读取的列
*/
private static final String[] MEDIA_PROJECTIONS = {
MediaStore.Images.ImageColumns.DATA,
MediaStore.Images.ImageColumns.DATE_TAKEN,
MediaStore.Images.ImageColumns.DATE_ADDED,
};
/**
* 内部存储器内容观察者
*/
private ContentObserver mInternalObserver;
/**
* 外部存储器内容观察者
*/
private ContentObserver mExternalObserver;
private ContentResolver mResolver;
private OnScreenShotListener listener;
private String lastData;
private Runnable shotCallBack = new Runnable() {
@Override
public void run() {
if (listener != null) {
final String path = lastData;
if (path != null && path.length() > 0) {
listener.onShot(path);
}
}
}
};

private ScreenShotHelper() {
// 初始化
mInternalObserver = new MediaContentObserver(MediaStore.Images.Media.INTERNAL_CONTENT_URI, null);
mExternalObserver = new MediaContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null);

mResolver = AppContext.get().getContentResolver();
// 添加监听
mResolver.registerContentObserver(
MediaStore.Images.Media.INTERNAL_CONTENT_URI,
false,
mInternalObserver
);
mResolver.registerContentObserver(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
false,
mExternalObserver
);
}

private static class Instance {
static ScreenShotHelper mInstance = new ScreenShotHelper();
}

public static ScreenShotHelper get() {
return Instance.mInstance;
}

public void setScreenShotListener(OnScreenShotListener listener) {
this.listener = listener;
}

public void removeScreenShotListener(OnScreenShotListener listener) {
if (this.listener == listener) {
synchronized (ScreenShotHelper.class) {
if (this.listener == listener) {
this.listener = null;
}
}
}
}

public void stopListener() {
mResolver.unregisterContentObserver(mInternalObserver);
mResolver.unregisterContentObserver(mExternalObserver);
}

private void handleMediaContentChange(Uri contentUri) {
Cursor cursor = null;
try {
// 数据改变时查询数据库中最后加入的一条数据
cursor = mResolver.query(
contentUri,
MEDIA_PROJECTIONS,
null,
null,
MediaStore.Images.ImageColumns.DATE_ADDED + " desc limit 1"
);
if (cursor == null) {
return;
}
if (!cursor.moveToFirst()) {
return;
}
// 获取各列的索引
int dataIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);
int dateTakenIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_TAKEN);
int dateAddIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_ADDED);
// 获取行数据
final String data = cursor.getString(dataIndex);
long dateTaken = cursor.getLong(dateTakenIndex);
long dateAdded = cursor.getLong(dateAddIndex);
if (data.length() > 0) {
if (TextUtils.equals(lastData, data)) {
//更改资源文件名也会触发,并且传递过来的是之前的截屏文件,所以只对分钟以内的有效
if (System.currentTimeMillis() - dateTaken < 3 * 3600) {
MainHandler.get().removeCallbacks(shotCallBack);
MainHandler.get().postDelayed(shotCallBack, 500);
}
} else if (dateTaken == 0 || dateTaken == dateAdded * 1000) {
MainHandler.get().removeCallbacks(shotCallBack);
if (listener != null) {
listener.onShot(null);
}
} else if (checkScreenShot(data)) {
MainHandler.get().removeCallbacks(shotCallBack);
lastData = data;
MainHandler.get().postDelayed(shotCallBack, 500);
}
}
} catch (Exception e) {
//
} finally {
if (cursor != null && !cursor.isClosed()) {
cursor.close();
}
}
}

/**
* 根据包含关键字判断是否是截屏
*/
private boolean checkScreenShot(String data) {
if (data == null || data.length() < 2) {
return false;
}
data = data.toLowerCase();
for (String keyWork : KEYWORDS) {
if (data.contains(keyWork)) {
return true;
}
}
return false;
}

private class MediaContentObserver extends ContentObserver {
private Uri mContentUri;

MediaContentObserver(Uri contentUri, Handler handler) {
super(handler);
mContentUri = contentUri;
}

@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
if (listener != null) {
handleMediaContentChange(mContentUri);
}
}
}

public interface OnScreenShotListener {
void onShot(@Nullable String data);
}

}

总结: 

1.必须要有读取内存的权限; 

2.内容生成时间为毫秒,内容添加时间为秒,比较时需要注意换算; 

3.当内容生成时间等于内容添加时间时,应当取消之前的截屏监听操作(尤其是会遮挡页面视图的部分);


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

Flutter 多端统一配置

本文介绍Flutter的全局变量统一配置的一种实现方法。3.2 多端统一配置为了方便对项目进行维护,我们需要将配置文件抽象出来进行统一管理。3.2.1 需求建立配置文件,统一常用配置信息,可多端共享。3.2.2 实现1 创建test项目创建项目:flutter...
继续阅读 »
本文介绍Flutter的全局变量统一配置的一种实现方法。

3.2 多端统一配置

为了方便对项目进行维护,我们需要将配置文件抽象出来进行统一管理。

3.2.1 需求

建立配置文件,统一常用配置信息,可多端共享。

3.2.2 实现

1 创建test项目

创建项目:flutter create test

进入项目:cd test

2 assets目录

创建文件夹:mkdir assets

3 创建配置文件

创建全局共享配置文件:touch assets/app.properties

4 编辑配置文件

app.properties中定义所需参数

serverHost=http://127.0.0.1:8080/
version=0.1.1


5 配置assets权限

打开pubspec.yaml,配置app.properties权限

flutter:
...
assets:
    - app.properties


6 创建dart配置文件

创建配置文件:touch lib/config.dart,并写入如下内容:

import 'package:flutter/services.dart';

class Config {
 factory Config() => _instance;
 static Config _instance;
 Config._internal();
 String serverHost = "";
 String version = "";

 Future init() async {
   Map<String, String> properties = Map();
   String value = await rootBundle.loadString("assets/app.properties");
   List<String> list = value.split("\n");
   list?.forEach((element) {
     if (element != null && element.contains("=")) {
       String key = element.substring(0, element.indexOf("="));
       String value = element.substring(element.indexOf("=") + 1);
       properties[key] = value;
    }
  });
   parserProperties(properties);
   return Future.value();
}

 void parserProperties(Map<String, String> properties) {
   serverHost = properties['serverHost'] ?? "";
   version = properties['version'] ?? "";
}
}

以后代码中需要用到全局变量通过Config调用即可。

收起阅读 »

跟我学企业级flutter项目:如何用dio封装一套企业级可扩展高效的网络层

前言跟我学flutter系列:跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制企业...
继续阅读 »

前言

跟我学flutter系列:
跟我学flutter:我们来举个例子通俗易懂讲解dart 中的 mixin
跟我学flutter:我们来举个例子通俗易懂讲解异步(一)ioslate
跟我学flutter:我们来举个例子通俗易懂讲解异步(二)ioslate循环机制
企业级篇目:
跟我学企业级flutter项目:用bloc手把手教你搭建用户认证系统
跟我学企业级flutter项目:dio网络框架增加公共请求参数&header
跟我学企业级flutter项目:如何用dio封装一套企业级可扩展高效的网络层
跟我学企业级flutter项目:如何封装一套易用,可扩展的Hybrid混合开发webview
跟我学企业级flutter项目:手把手教你制作一款低耦合空页面widget

网上有很多,比如说“Flutter Dio 亲妈级别封装教程”这篇文章,该文章上有几点问题:



  1. 重试机制代码错误

  2. token存取耦合很高

  3. 网络请求只能针对单一地址进行访问

  4. 网络请求缓存机制也不是很完美。


一旦依照这样的封装去做,那么项目后期的扩展性和易用性会有一定的阻碍,那么如何做到token存取无耦合,而且还能让app多种网络地址一同请求,还可以做到针对不同请求不同超时时长处理,网络缓存还加入可自动清理的lru算法呢?那么今天这篇文章为你揭晓企业级flutter dio网络层封装。


搭建前夕准备


三方库:


dio_cache_interceptor lru缓存库
dio 网络库
retrofit 网络生成库
connectivity_plus 网络情况判断


技能:


单例模式
享元模式
迭代


文章:


持久化:跟我学企业级flutter项目:dio网络框架增加公共请求参数&header


准备好如上技能,我们来封装一套优秀的网络层


一、准备好几个基本拦截器


1、超时拦截器


import 'dart:collection';

import 'package:dio/dio.dart';
import 'package:flutter_base_lib/src/app/contants.dart';
import 'package:flutter_base_lib/src/tools/net/cache_object.dart';

class TimeInterceptor extends Interceptor {

@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
Map extra = options.extra;
bool connect = extra.containsKey(SysConfig.connectTimeout);
bool receive = extra.containsKey(SysConfig.receiveTimeOut);
if(connect||receive){
if(connect){
int connectTimeout = options.extra[SysConfig.connectTimeout];
options.connectTimeout = connectTimeout;
}
if(receive){
int receiveTimeOut = options.extra[SysConfig.receiveTimeOut];
options.receiveTimeout = receiveTimeOut;
}
}
super.onRequest(options, handler);

}

}

作用:单独针对个别接口进行超时时长设定,如(下载,长链接接口)


2、缓存拦截器


dio_cache_interceptor 这个库中有lru算法缓存拦截库,可直接集成


3、持久化拦截器


跟我学企业级flutter项目:dio网络框架增加公共请求参数&header 本篇文章介绍了如何持久化


4、重试拦截器



import 'dart:async';
import 'dart:io';

import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:dio/dio.dart';
import 'package:flutter_base_lib/src/app/application.dart';
import 'package:flutter_base_lib/src/app/contants.dart';
import 'package:flutter_ulog/flutter_ulog.dart';

import '../dio_utli.dart';

/// 重试拦截器
class RetryOnConnectionChangeInterceptor extends Interceptor {
Dio? dio;

RequestInterceptorHandler? mHandler;
// RetryOnConnectionChangeInterceptor(){
//
// }

@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
mHandler = handler;
super.onRequest(options, handler);
}


@override
Future onError(DioError err, ErrorInterceptorHandler handler) async{
if (dio!=null&&Application.config.httpConfig.retry&&await _shouldRetry(err)) {
return await retryLoop(err,handler,1);
}
return super.onError(err, handler);
}

Future retryLoop(DioError err, ErrorInterceptorHandler handler,int retry) async {
try {
ULog.d("${err.requestOptions.uri.toString()} retry : ${retry}",tag: "${SysConfig.libNetTag}Retry");
await retryHttp(err,handler);
} on DioError catch (err) {
if(await _shouldRetry(err)&&retry _shouldRetry(DioError err) async{
return err.error != null && err.error is SocketException && await isConnected();
}

Future isConnected() async {
var connectivityResult = await (Connectivity().checkConnectivity());
return connectivityResult != ConnectivityResult.none;
}
}

该重试拦截器与其他文章封装不同,主要是用重试次数来管理重试机制。


5、日志拦截器



import 'dart:convert';

import 'package:dio/dio.dart';
import 'package:flutter_base_lib/src/app/contants.dart';
import 'package:flutter_ulog/flutter_ulog.dart';
typedef void LibLogPrint(String message);
class LibLogInterceptor extends Interceptor {
LibLogInterceptor({
this.request = true,
this.requestHeader = true,
this.requestBody = false,
this.responseHeader = true,
this.responseBody = false,
this.error = true
});

/// Print request [Options]
bool request;

/// Print request header [Options.headers]
bool requestHeader;

/// Print request data [Options.data]
bool requestBody;

/// Print [Response.data]
bool responseBody;

/// Print [Response.headers]
bool responseHeader;

/// Print error message
bool error;

@override
void onRequest(
RequestOptions options, RequestInterceptorHandler handler
) async
{
var builder = StringBuffer('*** Request *** \n');
builder.write(_printKV('uri', options.uri));
//options.headers;

if (request) {
builder.write(_printKV('method', options.method));
builder.write(_printKV('responseType', options.responseType.toString()));
builder.write(_printKV('followRedirects', options.followRedirects));
builder.write(_printKV('connectTimeout', options.connectTimeout));
builder.write(_printKV('sendTimeout', options.sendTimeout));
builder.write(_printKV('receiveTimeout', options.receiveTimeout));
builder.write(_printKV(
'receiveDataWhenStatusError', options.receiveDataWhenStatusError));
builder.write(_printKV('extra', options.extra));
}
if (requestHeader) {
builder.write('headers:\n');
options.headers.forEach((key, v) => builder.write(_printKV(' $key', v)));
}
if (requestBody) {
var res = options.data;
builder.write('data:\n');
builder.write(_message(res));
// try{
// ULog.json(res.toString(),tag: "${SysConfig.libNetTag}RequestJson");
// } on Exception catch (e) {
// ULog.d(res,tag: "${SysConfig.libNetTag}RequestJson");
// }
}
ULog.d(builder.toString(),tag: "${SysConfig.libNetTag}Request");
handler.next(options);
}

// Handles any object that is causing JsonEncoder() problems
Object toEncodableFallback(dynamic object) {
return object.toString();
}

String _message(dynamic res) {
if (res is Map || res is Iterable) {
var encoder = JsonEncoder.withIndent(' ', toEncodableFallback);
return encoder.convert(res);
} else {
return res.toString();
}
}

@override
void onResponse(Response response, ResponseInterceptorHandler handler) async
{
var builder = StringBuffer('*** Response *** \n');
_printResponse(response,builder,(message){
ULog.d(message,tag: "${SysConfig.libNetTag}Response");
});
handler.next(response);
}

@override
void onError(DioError err, ErrorInterceptorHandler handler) async
{
if (error) {
var builder = StringBuffer('*** DioError *** \n');
builder.write('uri: ${err.requestOptions.uri}\n');
builder.write('$err');
if (err.response != null) {
_printResponse(err.response!,builder,(message){
ULog.e(message,tag: "${SysConfig.libNetTag}Error");
});
}else{
ULog.e(builder.toString(),tag: "${SysConfig.libNetTag}Error");
}
}

handler.next(err);
}

void _printResponse(Response response,StringBuffer builder,LibLogPrint pr) {
builder.write(_printKV('uri', response.requestOptions.uri));
if (responseHeader) {
builder.write(_printKV('statusCode', response.statusCode));
if (response.isRedirect == true) {
builder.write(_printKV('redirect', response.realUri));
}

builder.write('headers:\n');
response.headers.forEach((key, v) => builder.write(_printKV(' $key', v.join('\r\n\t'))));
}
if (responseBody) {
var res = response.toString();
builder.write('Response Text:\r\n');
var resJ = res.trim();
if (resJ.startsWith("{")) {
Map decode = JsonCodec().decode(resJ);
builder.write(_message(decode));
}else if (resJ.startsWith("[")) {
List decode = JsonCodec().decode(resJ);
builder.write(_message(decode));
}else {
builder.write(res);
}

// try{
// ULog.json(res,tag: "${SysConfig.libNetTag}ResponseJson");
// } on Exception catch (e) {
// ULog.d(res,tag: "${SysConfig.libNetTag}ResponseJson");
// }
}
pr(builder.toString());

}

String _printKV(String key, Object? v) {
return '$key: $v \n';
}

}

在这里插入图片描述
主要是日志拦截打印


6、错误拦截器



import 'dart:io';

import 'package:dio/dio.dart';
import 'package:flutter_base_lib/src/app/contants.dart';
import 'package:flutter_base_lib/src/exception/lib_network_exception.dart';
import 'package:flutter_base_lib/src/tools/net/dio_utli.dart';
import 'package:flutter_ulog/flutter_ulog.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter_base_lib/src/lib_localizations.dart';

/// 错误处理拦截器
class ErrorInterceptor extends Interceptor {
// 是否有网
Future isConnected() async {
var connectivityResult = await (Connectivity().checkConnectivity());
return connectivityResult != ConnectivityResult.none;
}
@override
Future onError(DioError err, ErrorInterceptorHandler handler) async {
if (err.type == DioErrorType.other) {
bool isConnectNetWork = await isConnected();
if (!isConnectNetWork && err.error is SocketException) {
err.error = SocketException(LibLocalizations.getLibString().libNetWorkNoConnect!);
}else if (err.error is SocketException){
err.error = SocketException(LibLocalizations.getLibString().libNetWorkError!);
}
}
err.error = LibNetWorkException.create(err);
ULog.d('DioError : ${err.error.toString()}',tag: "${SysConfig.libNetTag}Interceptor");
super.onError(err, handler);
}

}

与其他人封装不同,服务器请求异常code,我将其抛到业务层自主处理。常规异常则走库文案。



import 'dart:io';

import 'package:dio/dio.dart';
import 'package:flutter_base_lib/src/lib_localizations.dart';

class LibNetWorkException implements Exception{

final String _message;
final int _code;

int get code{
return _code;
}

String get message{
return _message;
}

LibNetWorkException( this._code,this._message);

@override
String toString() {
return "$_code : $_message";
}



factory LibNetWorkException.create(DioError error) {
switch (error.type) {
case DioErrorType.cancel:{
return LibNetWorkException(-1, LibLocalizations.getLibString().libNetRequestCancel!);
}
case DioErrorType.connectTimeout:{
return LibNetWorkException(-1, LibLocalizations.getLibString().libNetFailCheck!);
}
case DioErrorType.sendTimeout:{
return LibNetWorkException(-1, LibLocalizations.getLibString().libNetTimeOutCheck!);
}
case DioErrorType.receiveTimeout:{
return LibNetWorkException(-1, LibLocalizations.getLibString().libNetResponseTimeOut!);
}
case DioErrorType.response:{
try{
return LibNetWorkException(error.response!.statusCode!,"HTTP ${error.response!.statusCode!}:${LibLocalizations.getLibString().libNetServerError!}");
} on Exception catch (_) {
return LibNetWorkException(-1, error.error.message);
}
}
default:
{
return LibNetWorkException(-1, error.error.message);
}
}
}
}

二、工具类封装


1、主要类




import 'dart:io';

import 'package:dio/adapter.dart';
import 'package:dio/dio.dart';
import 'package:flutter_base_lib/src/tools/net/interceptor/error_interceptor.dart';
import 'package:flutter_base_lib/src/tools/net/interceptor/lib_log_interceptor.dart';

import '../../../flutter_base_lib.dart';
import 'interceptor/presistent_interceptor.dart';
import 'interceptor/retry_on_connection_change_interceptor.dart';
import 'interceptor/time_interceptor.dart';

class DioUtil{

final
String _baseUrl;
final HttpConfig _config;
final List
_interceptors;

late Dio _dio;

Dio
get dio{
return _dio;
}
DioUtil._internal(
this._baseUrl, this._config, this._interceptors){
BaseOptions options =
new BaseOptions(
baseUrl: _baseUrl,
connectTimeout: _config.connectTimeout,
receiveTimeout: _config.receiveTimeOut,
);
_dio =
new Dio(options);
var retry = new Dio(options);
_interceptors.forEach((element) {
if(element is RetryOnConnectionChangeInterceptor){
element.dio = retry;
}
else{
if(!(element is ErrorInterceptor)){
retry.interceptors.add(element);
}
}
_dio.interceptors.add(element);
});
proxy(_dio);
proxy(retry);
}

void proxy(Dio dio){
if (SpSotre.instance.getBool(SysConfig.PROXY_ENABLE)??false) {
String? porxy = SpSotre.instance.getString(SysConfig.PROXY_IP_PROT)??null;
if(porxy!=null){
(dio.httpClientAdapter
as DefaultHttpClientAdapter).onHttpClientCreate =
(client) {
client.findProxy = (uri) {
return "PROXY $porxy";
};
//代理工具会提供一个抓包的自签名证书,会通不过证书校验,所以我们禁用证书校验
client.badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
};
}
}
}

static late Map
_dioUtils = Map();

static DioUtil instance(String baseUrl,{HttpConfig? config, List
? interceptors,List? applyInterceptors}){
if(!_dioUtils.containsKey(baseUrl)){
List
list = [PresistentInterceptor(),TimeInterceptor(),RetryOnConnectionChangeInterceptor(),LibLogInterceptor(requestBody: Application.config.debugState,responseBody: Application.config.debugState),ErrorInterceptor()];
// List
list = [ErrorInterceptor(),PresistentInterceptor()];
var inter = interceptors??list;
if(applyInterceptors!=null){
inter.addAll(applyInterceptors);
}
_dioUtils[baseUrl] = DioUtil._internal(baseUrl,config??Application.config.httpConfig,inter);
}
return _dioUtils[baseUrl]!;
}

// CancelToken _cancelToken = new CancelToken();


}


工具类封装,主要运用享元模式,可以支持多种url进行访问,不同的url有不同的配置。(灵活可用)


2、辅助类:




class HttpConfig{
final int _connectTimeout ;
final int _receiveTimeOut ;
final bool _retry;
final int _retryCount;

get connectTimeout{
return _connectTimeout;
}

get receiveTimeOut{
return _receiveTimeOut;
}

get retry{
return _retry;
}
get retryCount{
return _retryCount;
}

HttpConfig(HttpConfigBuilder builder): _connectTimeout = builder._connectTimeout,_receiveTimeOut = builder._receiveTimeOut,_retry = builder._retry,_retryCount = builder._retryCount;
}

class HttpConfigBuilder {
int _connectTimeout = 10000;//连接超时时间
int _receiveTimeOut = 30000;//接收超时时间
bool _retry = false;
int _retryCount = 3;

// var maxRetry = 1 重试次数

HttpConfigBuilder setConnectTimeout(int connectTimeout){
_connectTimeout = connectTimeout;
return this;
}

HttpConfigBuilder setReceiveTimeOut(int receiveTimeOut){
_receiveTimeOut = receiveTimeOut;
return this;
}

HttpConfigBuilder setRetry(bool retry){
_retry = retry;
return this;
}

HttpConfigBuilder setRetryCount(int retryCount){
_retryCount = _retryCount;
return this;
}

HttpConfig build() => HttpConfig(this);
}


三、使用


import 'package:flutter_app_me/data/model/api_result.dart';
import 'package:flutter_app_me/data/model/user.dart';
import 'package:flutter_app_me/data/model/user_infos.dart';
import 'package:flutter_base_lib/flutter_base_lib.dart';
import 'package:retrofit/retrofit.dart';
import 'package:dio/dio.dart';

import 'api_methods.dart';

part 'api_service.g.dart';

@RestApi()
abstract class RestClient {
factory RestClient(Dio dio, {String baseUrl}) = _RestClient;

@GET(ApiMethods.userinfoJson)
Future> userinfoJson();

// "test123332","123456"
@POST(ApiMethods.login)
@Extra({SysConfig.connectTimeout:100000})
Future> userLogin(@Queries() User user);
}

网络请求配置




class BusinessErrorException implements Exception {
final int _errorCode;
final String? _errorMsg;

BusinessErrorException(
this._errorCode, this._errorMsg);

int
get errorCode {
return _errorCode;
}

String?
get errorMsg => _errorMsg;
}


class TokenTimeOutException implements Exception {
final String? _errorMsg;
TokenTimeOutException(
this._errorMsg);
String?
get errorMsg => _errorMsg;

}

class RequestCodeErrorException implements Exception {
final String? _errorMsg;
final int _errorCode;
RequestCodeErrorException(
this._errorCode, this._errorMsg);

int
get errorCode {
return _errorCode;
}

String?
get errorMsg => _errorMsg;
}

业务基本异常


import 'package:business_package_auth/business_package_auth.dart';
import 'package:flutter_base_lib/flutter_base_lib.dart';
import 'package:flutter_base_ui/flutter_base_ui.dart';
import 'package:dio/dio.dart';
import 'package:wisdomwork_lib/src/model/api_result.dart';

const int httpSuccessCode = 0;
const int httpErrorCode = 1;
const int httpTokenExt = 10001;

extension SuccessExt on Success {
Success appSuccess() {
var data = this.data;
if (data is ApiResult) {
if (data.code != httpSuccessCode) {
switch (data.code){
case httpTokenExt:
TipToast.instance.tip(data.msg ?? LibLocalizations.getLibString().libBussinessTokenTimeOut!,tipType: TipType.warning);
BlocProvider.of(LibRouteNavigatorObserver.instance.navigator!.context).add(LogOut());
throw TokenTimeOutException(data.msg);
case httpErrorCode:
TipToast.instance.tip(data.msg ?? LibLocalizations.getLibString().libBussinessRequestCodeError!,tipType: TipType.error);
throw RequestCodeErrorException(data.code!,data.msg);
default:
throw BusinessErrorException(data.code!, data.msg);
}

}
}
return this;
}
}

extension ErrorExt on Error {
void appError() {
var exception = this.exception;
if (exception is LibNetWorkException) {
TipToast.instance.tip(exception.message, tipType: TipType.error);
}
}
}


typedef ResultF = Future> Function();

mixin RemoteBase {

Future>> remoteDataResult(ResultF resultF) async {
try {
var data = await resultF.call();
return Success(data).appSuccess();
} on DioError catch (err, stack) {
var e = err.error;
ULog.e(e.toString(), error: e, stackTrace: stack);
return Error(e)..appError();
} on Exception catch (e, stack) {
ULog.e(e.toString(), error: e, stackTrace: stack);
return Error(e)..appError();
}
}

}

业务基本异常处理方式


import 'package:flutter_base_lib/flutter_base_lib.dart';
import 'package:flutter_base_ui/flutter_base_ui.dart';
import 'package:wisdomwork/data/services/api_service.dart';
import 'package:wisdomwork_lib/wisdomwork_lib.dart';

mixin WisdomworkRemoteBase{
var rest = RestClient(DioUtil.instance(
AppEnvironment.envConfig![AppConfig.apiName]!,
applyInterceptors: [UiNetInterceptor()]).dio);
}

业务请求接口,实现

     final data = await AppResponsitory.instance.login(state.phoneText, state.codeText);
if (userResult != null) {
if (userResult is Success) {
if (userResult.data!.data!= null) {
onGetUser(userResult.data!.data!, context);
}
} else if(userResult is Error){
var exception = (userResult as Error).exception;
if(exception is BusinessErrorException){
Fluttertoast.showToast(msg: exception.errorMsg.toString());
}
}
}

业务请求与异常处理

收起阅读 »

Android如何优雅地解决重复Drawable资源

1. 前言 最近鸿洋大神和路遥大佬分别在他们的公众号上发布了关于解决Shape/Selector冗余的方案。这篇文章在上周末就已经写好了。虽然类似的解决方案特别多,实现思路也都差不多。但我仍然要安利一下我的这个解决方案。原因有以下几点。 很纯粹,就是用代...
继续阅读 »

1. 前言



最近鸿洋大神和路遥大佬分别在他们的公众号上发布了关于解决Shape/Selector冗余的方案。这篇文章在上周末就已经写好了。虽然类似的解决方案特别多,实现思路也都差不多。但我仍然要安利一下我的这个解决方案。原因有以下几点。




  1. 很纯粹,就是用代码的方式实现了xml实现的Drawable,不用重写自定义View或者Hook系统的基础组件。




  2. 最大程度的复刻xml所拥有的能力,甚至连单位dp还是px的api都提供好了。




  3. 使用Builder模式将方法和参数都约束起来,使用起来很方便,不用去众多的api中寻找方法。结合Kotlin的语法,一个字“香”。




  4. 内部实现了缓存策略,以及根据Hash判重策略,这也是目前市面上的其他解决方案所没有的。




当然美中不足的是,目前所有的xml替换都是需要手工去完成,如果在编译期能够通过gradle插件自动转换,那就完美了。如果您有相关的经验,可以尝试一起把这个库做得更好。



2. Android为什么用xml生成Drawable


xml是Android系统中生成Drawable的首选方案,所以很多同学都习惯了使用xml生成GradientDrawable和SelectorDrawable,它们确实很方便。但是随之而来的问题,我相信很多同学都是深有体会的,哪怕是GradientDrawable中一个圆角大小的改动,或者一个颜色值的改动,都需要在原来的xml文件基础上拷贝一份新的xml文件,这样导致项目中的drawable文件越来越多。甚至一些编码规范没做好的团队,明明完全一样效果的drawable在项目中也有可能出现多份。


针对这种情况,有没有必要处理呢?大部分的xml文件也就1 2kb,占用空间很小,对包体积大小影响也没那么大。虽然说Android系统Drawable缓存是以文件名为维度的,但是它的回收策略做的挺棒的,冗余的xml对内存占用有影响,但没那么大。


那就任由文件数量膨胀吗?我觉得答案是见仁见智的,不处理也可以,无非就是写起来臃肿点呗,至少不用花时间去想一套解决方案。当然我们也可以精益求精,使用代码生成Drawable方案,实现与xml完全一样的效果,同时又能避免冗余的xml文件出现。


意外的收获👉 在项目使用svg作为图片时,发现在Android5.0 和Android6.0手机上,xml定义的selector图片显示不正常。究其原因是因为Android7.0以下不支持svg格式的fillType,导致selector渲染出来的图片有问题。想了很多方法都无法解决,最终通过代码生成selector的方案解决了。



在开始写通过代码生成Drawable之前,首先思考一个问题?为什么Android系统会首选xml生成Drawable方案呢?


通过分析xml渲染Drawable原理,我觉得系统兼容可能是使用xml的一个重要原因。以GradientDrawable的setPadding方法为例,该方法在Android Q版本引入。如果我们在xml文件引入padding,在Android Q以下版本也不会出问题。如果是代码中使用就需要做版本判断


<padding android:top="10dp" android:bottom="10dp" android:left="10dp" android:right="10dp"></padding>
复制代码


闲话少叙,先看看最终的效果,下图左边是通过xml生成GradientDrawable,右边是通过代码生成GradientDrawable效果。



3. xml实现和代码实现


看下具体代码实现



  1. GradientDrawable xml实现



2. GradientDrawable代码实现



  1. StateListDrawable xml实现



4. StateListDrawable 代码实现


addState(StatePressed)方法表示android:state_pressed="true"


minusState(StatePressed)方法表示android:state_pressed="false"


当然也可以添加多个状态



4. 特性以及源码


该库有以下特性:



  1. xml能实现的,它全部能实现

  2. 使用Builder模式,更容易构建Drawable

  3. 支持所有的android:state_xxx

  4. GradientDrawable,只要所有构建的参数内容一样(顺序可以打乱),内存中只会保留一份

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

Flutter快速开发——列表分页加载封装

在 App 中,列表数据加载是一个很常见的功能,几乎大多数 App 中都存在列表数据的展示,而对于大数据量的列表展示,为提高用户体验、减少服务器压力等,一般采用分页加载列表数据,首次只加载一页数据,当用户向下滑动列表到底部时再触发加载下一页数据。为方便开发过程...
继续阅读 »

在 App 中,列表数据加载是一个很常见的功能,几乎大多数 App 中都存在列表数据的展示,而对于大数据量的列表展示,为提高用户体验、减少服务器压力等,一般采用分页加载列表数据,首次只加载一页数据,当用户向下滑动列表到底部时再触发加载下一页数据。

为方便开发过程中快速实现列表分页的功能,对列表分页加载统一封装是必不可少的,这样在开发过程中只需关注实际的业务逻辑而不用在分页数据加载的处理上花费过多时间,从而节省开发工作量、提高开发效率。

0x00 效果

首先来看一下经过封装后的列表分页加载的效果:

paging.gif

封装后的使用示例代码:

State:

class ArticleListsState  extends PagingState<Article>{
}

Controller:

class ArticleListsController extends PagingController<Article, ArticleListsState> {
final ArticleListsState state = ArticleListsState();
/// 用于接口请求
final ApiService apiService = Get.find();


@override
ArticleListsState getState() => ArticleListsState();

@override
Future<PagingData<Article>?> loadData(PagingParams pagingParams) async{
/// 请求接口数据
PagingData<Article>? articleList = await apiService.getArticleList(pagingParams);
return articleList;
}
}

View:

class ArticleListsPage extends StatelessWidget {
final controller = Get.find<ArticleListsController>();
final state = Get.find<ArticleListsController>().state;

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("文章列表")),
body: buildRefreshListWidget<Article,ArticleListsController>(itemBuilder: (item, index){
return _buildItem(item);
}),
);
}

/// item 布局
Widget _buildItem(Article item) {
return Card(...);
}
}

0x01 实现

上面展示了通过封装后的列表分页加载实现的文章列表效果并附上了关键示例代码,通过示例代码可以看出,在使用封装后的列表分页加载功能时只需要关注数据请求本身和界面布局展示,而无需关注分页的具体细节,使列表分页加载的实现变得更简单。下面将通过代码介绍具体如何实现列表分页加载的封装。

整体介绍

在看具体实现之前,先带大家从整体结构、最终实现的功能、使用到的三方库上做一个整体介绍。

整体结构

整个列表封装分为三层,StateControllerView

  • State: 用于存放界面状态数据,一个复杂的界面可能存在很多的状态数据,为了便于对状态数据的维护将其统一放到 State 里,对于有列表分页加载的页面,其列表数据也统一封装到 State 里。
  • Controller: 页面业务逻辑处理。
  • View: 界面 UI 元素,即 Widget 。

实现功能

封装后的列表分页加载实现功能主要如下:

  • 列表数据显示
  • 下拉刷新
  • 上拉加载
  • 自动判断是否还有更多数据
  • 自动处理分页逻辑
  • 列表 item 点击事件封装

使用到的第三方库

列表分页加载封装中 GetX 主要使用到了依赖管理和状态管理,当然 GetX 除了依赖管理还有很多其他功能,因本篇文章主要介绍列表分页的封装,不会过多介绍 GetX,关于 GetX 更多使用及介绍可参考以下文章:

具体实现

前面介绍了对于列表分页加载的封装整体分为三层:StateControllerView,而封装的主要工作就是对这三层的封装,实现 PagingState 、PagingController 的基类以及 buildRefreshListWidget 函数的封装。

PagingState

PagingState 用于封装保存分页状态数据及列表数据,不涉及实际业务逻辑处理,源码如下:

class PagingState<T>{

/// 分页的页数
int pageIndex = 1;

///是否还有更多数据
bool hasMore = true;

/// 用于列表刷新的id
Object refreshId = Object();

/// 列表数据
List<T> data = <T>[];
}

PagingState 有一个泛型 T 为列表 data 的 item 类型 ,即列表数据 item 的数据实体类型。refreshId 刷新列表界面的 id,用于后面 Controller 刷新指定 Widget 使用,属于 GetX 状态管理的功能,具体可详阅 GetX 相关文章。其他变量的作用在注释里描述得很详细,这里就不作赘述了。

PagingController

PagingController 封装分页的逻辑处理,源码如下:

abstract class PagingController<M,S extends PagingState<M>> extends GetxController{

/// PagingState
late S pagingState;
/// 刷新控件的 Controller
RefreshController refreshController = RefreshController();

@override
void onInit() {
super.onInit();
/// 保存 State
pagingState = getState();
}

@override
void onReady() {
super.onReady();
/// 进入页面刷新数据
refreshData();
}


/// 刷新数据
void refreshData() async{
initPaging();
await _loadData();
/// 刷新完成
refreshController.refreshCompleted();
}

///初始化分页数据
void initPaging() {
pagingState.pageIndex = 1;
pagingState.hasMore = true;
pagingState.data.clear();
}

/// 数据加载
Future<List<M>?> _loadData() async {
PagingParams pagingParams = PagingParams.create(pageIndex: pagingState.pageIndex);
PagingData<M>? pagingData = await loadData(pagingParams);
List<M>? list = pagingData?.data;

/// 数据不为空,则将数据添加到 data 中
/// 并且分页页数 pageIndex + 1
if (list != null && list.isNotEmpty) {
pagingState.data.addAll(list);
pagingState.pageIndex += 1;
}

/// 判断是否有更多数据
pagingState.hasMore = pagingState.data.length < (pagingData?.total ?? 0);

/// 更新界面
update([pagingState.refreshId]);
return list;
}


/// 加载更多
void loadMoreData() async{
await _loadData();
/// 加载完成
refreshController.loadComplete();
}

/// 最终加载数据的方法
Future<PagingData<M>?> loadData(PagingParams pagingParams);

/// 获取 State
S getState();

}

PagingController 继承自 GetxController ,有两个泛型 MS ,分别为列表 item 的数据实体类型和 PageState 的类型。

成员变量 pagingState 类型为泛型 S 即 PagingState 类型,在 onInit 中通过抽象方法 getState 获取,getState 方法在子类中实现,返回 PagingState 类型对象。

refreshController 为 pull_to_refresh 库中控制刷新控件 SmartRefresher 的 Controller ,用于控制刷新/加载完成。

refreshData 、loadMoreData 方法顾名思义是下拉刷新和上拉加载更多,在对应事件中调用,其内部实现调用 _loadData 加载数据,加载完成后调用 refreshController 的刷新完成或加载完成, refreshData 中加载数据之前还调用了初始化分页数据的 initPaging 方法,用于重置分页参数和数据。

_loadData 是数据加载的核心代码,首先创建 PagingParams 对象,即分页请求数据参数实体,创建时传入了分页的页数,值为 PagingState 中维护的分页页数 pageIndexPagingParams 实体源码如下:

class PagingParams {

int current = 1;
Map<String, dynamic>? extra = {};
Map<String, dynamic> model = {};
String? order = 'descending';
int size = 10;
String? sort = "id";

factory PagingParams.create({required int pageIndex}){
var params = PagingParams();
params.current = pageIndex;
return params;
}
}

字段包含当前页数、每页数据条数、排序字段、排序方式以及扩展业务参数等。此类可根据后台接口分页请求协议文档进行创建。

分页参数创建好后,调用抽象方法 loadData 传入创建好的参数,返回 PagingData 数据,即分页数据实体,源码如下:

class PagingData<T> {

int? current;
int? pages;
List<T>? data;
int? size;
int? total;

PagingData();

factory PagingData.fromJson(Map<String, dynamic> json) => $PagingDataFromJson<T>(json);

Map<String, dynamic> toJson() => $PagingDataToJson(this);

@override
String toString() {
return jsonEncode(this);
}
}

该实体包含列表的真实数据 data ,以及分页相关参数,比如当前页、总页数、总条数等,可根据后台分页接口返回的实际数据进行调整。其中 fromJson 、toJson 是用于 json 数据解析和转换用。

关于 json 数据解析可参考前面写的 : Flutter应用框架搭建(三)Json数据解析

数据加载完成后,判断数据是否为空,不为空则将数据添加到 data 集合中,并且分页的页数加 1。然后判断是否还有更多数据,此处是根据 data 中的数据条数与分页返回的总条数进行比较判断的,可能不同团队的分页接口实现规则不同,可根据实际情况进行调整,比如使用页数进行判断等。

方法最后调用了 Controller 的 update 方法刷新界面数据。

流程如下:

paging2.png

View

View 层对 ListView 和 pull_to_refresh 的 SmartRefresher 进行封装,满足列表数据展示和下拉刷新/上拉加载更多功能。其封装主要为 Widget 参数配置的封装,涉及业务逻辑代码不多,故未将其封装为 Widget 控件,而是封装成方法进行调用, 共三个方法:

  • buildListView: ListView 控件封装
  • buildRefreshWidget: 下拉刷新/上拉加载更多控件封装
  • buildRefreshListWidget: 带分页加载的 ListView 控件封装

其中前面两个是单独分别对 ListView 和 SmartRefresher 的封装,第三个则是前两者的结合。

buildListView:

Widget buildListView<T>(
{required Widget Function(T item, int index) itemBuilder,
required List<T> data,
Widget Function(T item, int index)? separatorBuilder,
Function(T item, int index)? onItemClick,
ScrollPhysics? physics,
bool shrinkWrap = false,
Axis scrollDirection = Axis.vertical}) {
return ListView.separated(
shrinkWrap: shrinkWrap,
physics: physics,
padding: EdgeInsets.zero,
scrollDirection: scrollDirection,
itemBuilder: (ctx, index) => GestureDetector(
child: itemBuilder.call(data[index], index),
onTap: () => onItemClick?.call(data[index], index),
),
separatorBuilder: (ctx, index) =>
separatorBuilder?.call(data[index], index) ?? Container(),
itemCount: data.length);
}

代码不多,主要是对 ListView 的常用参数包装了一遍,并添加了泛型 T 即列表数据 item 的类型。其次对 itemCount 和 itemBuilder 做了特殊处理, itemCount 赋值为 data.length 列表数据的长度;ListView 的 itemBuilder 调用了传入的 itemBuilder 方法,后者参数与 ListView 的参数有区别,传入的是 item 数据和下标 index, 且使用 GestureDetector 包裹封装了 item 点击事件调用onItemClick

buildRefreshWidget:

Widget buildRefreshWidget({
required Widget Function() builder,
VoidCallback? onRefresh,
VoidCallback? onLoad,
required RefreshController refreshController,
bool enablePullUp = true,
bool enablePullDown = true
}) {
return SmartRefresher(
enablePullUp: enablePullUp,
enablePullDown: enablePullDown,
controller: refreshController,
onRefresh: onRefresh,
onLoading: onLoad,
header: const ClassicHeader(idleText: "下拉刷新",
releaseText: "松开刷新",
completeText: "刷新完成",
refreshingText: "加载中......",),
footer: const ClassicFooter(idleText: "上拉加载更多",
canLoadingText: "松开加载更多",
loadingText: "加载中......",),
child: builder(),
);
}

对 SmartRefresher 参数进行封装,添加了 header 和 footer 的统一处理,这里可以根据项目实际需求进行封装,可以使用其他下拉刷新/上拉加载的风格或者自定义实现效果,关于 SmartRefresher 的使用请参考官网 : flutter_pulltorefresh

buildRefreshListWidget:

Widget buildRefreshListWidget<T, C extends PagingController<T, PagingState<T>>>(
{
required Widget Function(T item, int index) itemBuilder,
bool enablePullUp = true,
bool enablePullDown = true,
String? tag,
Widget Function(T item, int index)? separatorBuilder,
Function(T item, int index)? onItemClick,
ScrollPhysics? physics,
bool shrinkWrap = false,
Axis scrollDirection = Axis.vertical
}) {
C controller = Get.find(tag: tag);
return GetBuilder<C>(builder: (controller) {
return buildRefreshWidget(
builder: () =>
buildListView<T>(
data: controller.pagingState.data,
separatorBuilder: separatorBuilder,
itemBuilder: itemBuilder,
onItemClick: onItemClick,
physics: physics,
shrinkWrap: shrinkWrap,
scrollDirection: scrollDirection
),
refreshController: controller.refreshController,
onRefresh: controller.refreshData,
onLoad: controller.loadMoreData,
enablePullDown: enablePullDown,
enablePullUp: enablePullUp && controller.pagingState.hasMore,
);
}, tag: tag, id: controller.pagingState.refreshId,);
}

buildRefreshListWidget 是对前面两者的再次封装,参数也基本上是前面两者的结合,buildRefreshWidget 的 builder 传入的是 buildListView 。

为了将下拉刷新、上拉加载更多的操作进行统一封装,这里引入了 PagingController 的泛型 C 并通过 GetX 的依赖管理获取到当前的 PagingController 实例 controller:

  • buildListView 的 data 传入 PagingState 的 data 即分页数据,即 controller.pagingState.data
  • refreshController 传入 PagingController 中创建的 refreshController 对象,即 controller.refreshController
  • onRefresh / onRefresh 调用 PagingController 的 refreshData / loadMoreData 方法
  • enablePullUp 使用方法传入的 enablePullUp 和 PagingState 的 hasMore(是否有更多数据) 共同判断

列表数据加载完成后将自动刷新界面,这里使用了 GetBuilder 包裹 buildRefreshWidget,并添加 tag 和 id 参数,其中 tag 是 GetX 依赖注入的 tag ,用于区分注入的实例, id 则为刷新的 id,可通过 id 刷新指定控件,这里传入的就是 PagingState 里定义的 refreshId ,即刷新指定列表。

整体 View 结构如下:

paging3.png

0x02 总结

经过上诉的封装后就能快速实现文章开头展示的列表分页加载效果,通过简单的代码就能实现完整的列表分页加载功能,让开发者关注业务本身,从而节省开发工作量、提高开发效率和质量。最后附上一张整体的结构关系图:

paging4.png

源码:flutter_app_core


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

收起阅读 »

Binder机制和AIDL的理解

Android 进程间通信 为什么要去理解Android的进程间通信机制 对于Android开发工程师来说,如果不去理解进程间通信机制也可以使用系统提供的API完成应用开发,但如果想要达到更高的层级,那么就不能简单只会调用API。无论是工作中遇到一些疑难问题,...
继续阅读 »

Android 进程间通信


为什么要去理解Android的进程间通信机制


对于Android开发工程师来说,如果不去理解进程间通信机制也可以使用系统提供的API完成应用开发,但如果想要达到更高的层级,那么就不能简单只会调用API。无论是工作中遇到一些疑难问题,还是想要学习源码的一些功能实现,或者是想要提升APP的性能等,这些工作都需要我们去看系统的源码,而系统的源码中进程间通信无处不在,如果不理解进程间通信机制,那么很难看懂系统源码,而且容易迷失在大量的代码中。


Android 进程间通信机制


为什么使用Binder作为Android进程间通信机制


Android Bander设计与实现 - 设计篇 这篇文章写得很好了。主要是为了弥补Linux中其他进程间通信方式得性能和安全性不足。当然Binder机制也并非是谷歌为了Android原创技术,Binder机制源于OpenBinder,OpenBinder要早于Android系统出现。所以如果想要立即Android得进程间通信,主要就是理解Binder机制。


Binder进程间通信基本框架



在Android中,2个应用或者进程之间的通信都需要经过Binder代理,二者不能直接通信,同样APP在使用系统服务时也需要跨进程通信,比如我们最常用的ActivityManagerService(AMS)也是一个系统服务进程,此外APP使用WIFI 、定位、媒体服务等都是系统进程,APP想要使用这些系统服务的功能一定要通过Binder进行通信。


Binder到底是什么


我们一直在说利用Binder机制进行进程间通信,那么Binder具体是什么?是一个Java类,还是一个底层驱动?通常我们说Binder机制是Android系统不同层Binder相关代码组成的一套跨进程通信功能。Binder机制相关代码从最底层的驱动层到最顶层的应用层都有,所以要读懂Binder机制,就需要我们耐心的逐层进行分析。



Binder机制代码结构


如何理解AIDL


我们从上图没有看到任何AIDL相关的信息,也就是说Binder机制是与AIDL无关的,那么我们日常中如果要跨进程都要写一个AIDL类然后由AS生成一些Java类,我们使用这些类实现进程间通信,这么做的目的其实是由AS帮我们生成一些模板代码,减少我们的工作和出错概率,其实不用AIDL我们也可以实现Binder通信,并且可以更好的理解Binder机制。下面我写一个Demo进程,这个Demo中有AIDL文件并生成相关代码,但我们不用,只是用来作为对比,我们用最少的代码实现Binder通信,并通过对比我们写的代码和AIDL生成的代码来更好的理解AIDL生成的代码的作用。代码Github


不使用ADIL,手动实现进程间通信



项目结构


代码中client为客户端,server为服务端




客户端进程发送一个字符串给服务端,服务端进程接收到将字符显示到界面上。项目中没有用到AIDL为我们生成Binder通信类,而是用最简单的方式实现Binder通信,因而我们可以看清Binder通信最关键的地方。首先我们要知道,实现了IBinder接口的类的对象是可以跨进程传递的。


服务端

1.服务端RemoteService继承Service

2.创建一个继承Binder的类ServerBinder,并覆写onTransact方法,用于处理Client的调用,Binder实现了IBinder接口

3.服务端覆写Service的onBind方法,返回一个ServerBinder对象(这个ServerBinder对象是最终传递给Client端)


public class RemoteService extends Service {
public static final int TRANSAVTION_showMessage = IBinder.FIRST_CALL_TRANSACTION;

@Nullable
@Override
public IBinder onBind(Intent intent) {
return new ServerBinder();
}

static class ServerBinder extends Binder {
public ServerBinder() {
}

@Override
protected boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags) throws RemoteException {

switch (code) {
case TRANSAVTION_showMessage:
String message = data.readString();
Log.d("ServerBinder", "showMessage " + message);
if (ServerMainActivity.tvShowMessage != null) {//显示收到数据逻辑
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
ServerMainActivity.tvShowMessage.setText(message);
}
});
}
if (reply != null) {
reply.writeNoException();
}
return true;
}
return super.onTransact(code, data, reply, flags);
}


}
}

客户端

1.客户端创建一个ServiceConnection对象,用于与服务端建立连接,并获取到服务端的IBinder对象

2.客户端通过bindService与服务端的RemoteService建立连接


public class ClientMainActivity extends AppCompatActivity {
private Button mSendString;
private EditText mStingEditText;
public static final int TRANSAVTION_showMessage = IBinder.FIRST_CALL_TRANSACTION;
private IBinder mServer;//服务端的Binder对象
private boolean isConnection = false;

private ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {

isConnection = true;
mServer = service;//建立连接成功,保存服务端进程的IBinder对象
Log.d("Client"," onServiceConnected success");
}

@Override
public void onServiceDisconnected(ComponentName name) {
isConnection = false;
}
};

//与服务端进程中RemoteService建立连接
private void attemptToBindService() {
Intent intent = new Intent();
intent.setClassName("com.binder.server", "com.binder.server.RemoteService");
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mSendString = findViewById(R.id.btn_send_string);
mStingEditText = findViewById(R.id.et_string);
mSendString.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (!isConnection) {
attemptToBindService();
return;
}
//发送数据给服务端进程
Parcel data = Parcel.obtain();
Parcel replay = Parcel.obtain();
if (mServer != null) {
try {
data.writeString(mStingEditText.getText().toString());
Log.d("Client"," mServer.transact call");
//发送数据到服务端进程
mServer.transact(TRANSAVTION_showMessage, data, replay, 0);
replay.readException();
} catch (RemoteException e) {
e.printStackTrace();
} finally {
replay.recycle();
data.recycle();
}
}


}
});
}

@Override
protected void onStart() {
super.onStart();
if (!isConnection) {
attemptToBindService();
}
}

从上面的代码来看Binder的跨进程通信核心就是客户端获取到服务端的IBinder对象,然后调用这个对象的transact方法发送数据,实现进程间通信。


使用ADIL生成相关类,进行进程间通信



加入AIDL文件


interface IShowMessageAidlInterface {
/**
* Demonstrates some basic types that you can use as parameters
* and return values in AIDL.
*/
void showMessage(String msg);
}

修改Client端代码

public class ClientMainActivityUseAidl extends AppCompatActivity {
private Button mSendString;
private EditText mStingEditText;
private IShowMessageAidlInterface mServer;//服务端的Binder对象代理
private boolean isConnection = false;

private ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
isConnection = true;
//调用IShowMessageAidlInterface.Stub.asInterface静态方法,将service转换为一接口
mServer = IShowMessageAidlInterface.Stub.asInterface(service);
Log.d("Client"," onServiceConnected success");
}

@Override
public void onServiceDisconnected(ComponentName name) {
isConnection = false;
}
};
private void attemptToBindService() {
Intent intent = new Intent();
intent.setClassName("com.binder.server", "com.binder.server.RemoteServiceUseAidl");
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mSendString = findViewById(R.id.btn_send_string);
mStingEditText = findViewById(R.id.et_string);
mSendString.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (!isConnection) {
attemptToBindService();
return;
}
try {
//直接调用接口的showMessage方法
mServer.showMessage(mStingEditText.getText().toString());
} catch (RemoteException e) {
e.printStackTrace();
}
}
});
}

@Override
protected void onStart() {
super.onStart();
if (!isConnection) {
attemptToBindService();
}
}

1.客户端利用 IShowMessageAidlInterface生成类中的Stub内部类将接受到的IBinder对象转换为一个接口

2.在发送数据时,直接调用IShowMessageAidlInterface接口的showMessage方法


asInterface方法

   public static com.binder.server.IShowMessageAidlInterface asInterface(android.os.IBinder obj)
{
if ((obj==null)) {
return null;
}
//查询obj对象是否是本地接口,也就是是不是在同一个进程
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin!=null)&&(iin instanceof com.binder.server.IShowMessageAidlInterface))) {
如果是同一个进程直接返回
return ((com.binder.server.IShowMessageAidlInterface)iin);
}
//如果是不同进程,则将IBinder对象利用Proxy封装一层
return new com.binder.server.IShowMessageAidlInterface.Stub.Proxy(obj);
}

Proxy类

 private static class Proxy implements com.binder.server.IShowMessageAidlInterface
{
private android.os.IBinder mRemote;
Proxy(android.os.IBinder remote)
{
mRemote = remote;
}
@Override public android.os.IBinder asBinder()
{
return mRemote;
}
public java.lang.String getInterfaceDescriptor()
{
return DESCRIPTOR;
}
/**
* Demonstrates some basic types that you can use as parameters
* and return values in AIDL.
*/
//代理对象做的工作是把AIDL接口中定义的方法中的数据进行封装,方便进行跨进程传输
@Override public void showMessage(java.lang.String msg) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeString(msg);
boolean _status = mRemote.transact(Stub.TRANSACTION_showMessage, _data, _reply, 0);
if (!_status && getDefaultImpl() != null) {
getDefaultImpl().showMessage(msg);
return;
}
_reply.readException();
}
finally {
_reply.recycle();
_data.recycle();
}
}
public static com.binder.server.IShowMessageAidlInterface sDefaultImpl;
}

所以我们可以知道,客户端用到了AIDL文件生成Stub类中的asInterface方法,把接收到的远程IBinder转换为IShowMessageAidlInterface接口,而这个接口的具体实现其实是Proxy类,代理类对方法传入数据进行封装,然后发送给mRemote 服务端。


修改服务端代码


public class RemoteServiceUseAidl extends Service {
@Nullable
@Override
public IBinder onBind(Intent intent) {
return new IShowMessageAidlInterface.Stub() {
@Override
public void showMessage(String msg) throws RemoteException {
if (ServerMainActivity.tvShowMessage != null) {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
ServerMainActivity.tvShowMessage.setText(msg);
}
});
}
}
};
}
}

服务端的 onBind方法返回AIDL生成的Stub类的对象,Stub是个抽象类,其中待实现的方法为AIDL中定义的showMessage方法。


 public static abstract class Stub extends android.os.Binder implements com.binder.server.IShowMessageAidlInterface
{
private static final java.lang.String DESCRIPTOR = "com.binder.server.IShowMessageAidlInterface";
static final int TRANSACTION_showMessage = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
/** Construct the stub at attach it to the interface. */
public Stub()
{
this.attachInterface(this, DESCRIPTOR);
}
@Override public android.os.IBinder asBinder()
{
return this;
}
@Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
{
java.lang.String descriptor = DESCRIPTOR;
switch (code)
{
case INTERFACE_TRANSACTION:
{
reply.writeString(descriptor);
return true;
}
case TRANSACTION_showMessage:
{
data.enforceInterface(descriptor);
java.lang.String _arg0;
_arg0 = data.readString();
this.showMessage(_arg0);
reply.writeNoException();
return true;
}
default:
{
return super.onTransact(code, data, reply, flags);
}
}
}

}

可以看到Sub抽象类中继承自Binder,也就是客端最终拿到的是这个Stub IBinder对象,客户端调用tansact方法最终会调用到Stub类的onTransact进行处理,Stub的onTransact方法根据code确定客端户调用了哪个方法,然后对接收到的data数据进行读取解析,将处理好的数据交给IShowMessageAidlInterface中对应的方法。


总结:

1.AIDL生成的类中Stub的静态方法asInterface和Proxy类是给客户端用于发送数据的

2.Stub抽象类是由服务端实现,接收处理客户端数据的


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

深入探索Flutter性能优化

目录 一、检测手段 1、Flutter Inspector 2、性能图层 3、Raster 线程问题 4、UI 线程问题定位 5、检查多视图叠加的视图渲染开关 checkerboardOffscreenLayers 6、检查缓存的图像开关 checkerb...
继续阅读 »

目录



  • 一、检测手段

    • 1、Flutter Inspector

    • 2、性能图层

    • 3、Raster 线程问题

    • 4、UI 线程问题定位

    • 5、检查多视图叠加的视图渲染开关 checkerboardOffscreenLayers

    • 6、检查缓存的图像开关 checkerboardRasterCacheImages



  • 二、关键优化指标

    • 1、页面异常率

    • 2、页面帧率

    • 3、页面加载时长



  • 三、布局加载优化

    • 1、常规优化

    • 2、深入优化



  • 四、启动速度优化

    • 1、引擎预加载

    • 2、Dart VM 预热



  • 五、内存优化

    • 1、const 实例化

    • 2、识别出消耗多余内存的图片

    • 3、针对 ListView item 中有 image 的情况来优化内存



  • 六、包体积优化

    • 1、图片优化

    • 2、移除冗余的二三库

    • 3、启用代码缩减和资源缩减

    • 4、构建单 ABI 架构的包



  • 七、总结


前言


Flutter 作为目前最火爆的移动端跨平台框架,能够帮助开发者通过一套代码库高效地构建多平台的精美应用,并支持移动、Web、桌面和嵌入式平台。对于 Android 来说,Flutter 能够创作媲美原生的高性能应用,但是,在较为复杂的 App 中,使用 Flutter 开发也很难避免产生各种各样的性能问题。在这篇文章中,我将和你一起全方位地深入探索 Flutter 性能优化的疆域。


一、检测手段


准备


以 profile 模式启动应用,如果是混合 Flutter 应用,在 flutter/packages/flutter_tools/gradle/flutter.gradle 的 buildModeFor 方法中将 debug 模式改为 profile即可。


为什么要在分析模式下来调试应用性能?


分析模式在发布模式的基础之上,为分析工具提供了少量必要的应用追踪信息。


那,为什么要在发布模式的基础上来调试应用性能?


与调试代码可以在调试模式下检测 Bug 不同,性能问题需要在发布模式下使用真机进行检测。这是因为,相比发布模式而言,调试模式增加了很多额外的检查(比如断言),这些检查可能会耗费很多资源,而更重要的是,调试模式使用 JIT 模式运行应用,代码执行效率较低。这就使得调试模式运行的应用,无法真实反映出它的性能问题


而另一方面,模拟器使用的指令集为 x86,而真机使用的指令集是 ARM。这两种方式的二进制代码执行行为完全不同,因此,模拟器与真机的性能差异较大,例如,针对一些 x86 指令集擅长的操作,模拟器会比真机快,而另一些操作则会比真机慢。这也同时意味着,你无法使用模拟器来评估真机才能出现的性能问题。


1、Flutter Inspector


Flutter Inspector有很多功能,但你应该把注意力花在更有用的功能学习上,例如:“Select Widget Mode” 和 “Repaint Rainbow”


Select Widget Mode


点击 “Select Widget Mode” 图标,可以在手机上查看当前页面的布局框架与容器类型。



作用


快速查看陌生页面的布局实现方式


Repaint Rainbow


点击 “Repaint Rainbow” 图标,它会 为所有 RenderBox 绘制一层外框,并在它们重绘时会改变颜色



作用


帮你找到 App 中频繁重绘导致性能消耗过大的部分


例如:一个小动画可能会导致整个页面重绘,这个时候使用 RepaintBoundary Widget 包裹它,可以将重绘范围缩小至本身所占用的区域,这样就可以减少绘制消耗。


使用场景


例如 页面的进度条动画刷新时会导致整个布局频繁重绘


缺点


使用 RepaintBoundary Widget 会创建额外的绘制画布,这将会增加一定的内存消耗


2、性能图层


性能图层会在当前应用的最上层,以 Flutter 引擎自绘的方式展示 Raster 与 UI 线程的执行图表,而其中每一张图表都代表当前线程最近 300 帧的表现,如果 UI 产生了卡顿(跳帧),这些图表可以帮助你分析并找到原因。


蓝色垂直的线条表示已执行的正常帧,绿色的线条代表的是当前帧,如果其中有一帧处理时间过长,就会导致界面卡顿,图表中就会展示出一个红色竖条


如果红色竖条出现在 GPU 线程图表,意味着渲染的图形太复杂,导致无法快速渲染;而如果是出现在了 UI 线程图表,则表示 Dart 代码消耗了大量资源,需要优化代码的执行时间。如下图所示:



3、Raster 线程问题定位


它定位的是 渲染引擎底层渲染的异常


解决方案是 把需要静态缓存的图像加入到 RepaintBoundary。而 RepaintBoundary 可以确定 Widget 树的重绘边界,如果图像足够复杂,Flutter 引擎会自动将其缓存,避免重复刷新。当然,因为缓存资源有限,如果引擎认为图像不够复杂,也可能会忽略 RepaintBoundary。


4、UI 线程问题定位


问题场景


在视图构建时,在 build 方法中使用了一些复杂的运算,或是在主 Isolate 中进行了同步的 I/O 操作。


使用 Performance 进行检测


点击 Android Studio 底部工具栏中的 “Open DevTools” 按钮,然后在打开的 Dart DevTools 网页中将顶部的 tab 切换到 Performance。


与性能图层能够自动记录应用执行的情况不同,使用 Performance 来分析代码执行轨迹,你需要手动点击 “Record” 按钮去主动触发,在完成信息的抽样采集后,点击 “Stop” 按钮结束录制。这时,你就可以得到在这期间应用的执行情况了。


使用 Performance 记录应用的执行情况,即 CPU 帧图,又被称为火焰图。火焰图是基于记录代码执行结果所产生的图片,用来展示 CPU 的调用栈,表示的是 CPU 的繁忙程度


其中:



  • y 轴:表示调用栈,其每一层都是一个函数。调用栈越深,火焰就越高,底部就是正在执行的函数,上方都是它的父函数

  • x 轴:表示单位时间,一个函数在 x 轴占据的宽度越宽,就表示它被采样到的次数越多,即执行时间越长


所以,我们要 检测 CPU 耗时问题,皆可以查看火焰图底部的哪个函数占据的宽度最大。只要有 “平顶”,就表示该函数可能存在性能问题。如下图所示:



一般的耗时问题,我们通常可以 使用 Isolate(或 compute)将这些耗时的操作挪到并发主 Isolate 之外去完成



dart 的单线程执行异步任务是怎么实现的?



网络调用的执行是由操作系统提供的另外的底层线程做的,而在 event queue 里只会放一个网络调用的最终执行结果(成功或失败)和响应执行结果的处理回调。


5、使用 checkerboardOffscreenLayers 检查多视图叠加的视图渲染


只要在 MaterialApp 的初始化方法中,将 checkerboardOffscreenLayers 开关设置为 true,分析工具就会自动帮你检测多视图叠加的情况。


这时,使用了 saveLayer 的 Widget 会自动显示为棋盘格式,并随着页面刷新而闪烁。


而 saveLayer 一般会通过一些功能性 Widget,在涉及需要剪切或半透明蒙层的场景中间接地使用。


6、使用 checkerboardRasterCacheImages 检查缓存的图像


它也是用来检测在界面重绘时频繁闪烁的图像(即没有静态缓存)。解决方案是把需要静态缓存的图像加入到 RepaintBoundary。


二、关键优化指标


1、页面异常率


页面异常率,即 页面渲染过程中出现异常的概率。


它度量的是页面维度下功能不可用的情况,其统计公式为:



页面异常率 = 异常发生次数 / 整体页面 PV 数。



统计异常发生次数


利用 Zone 与 FlutterError 这两个方法,然后在异常拦截的方法中,去累计异常的发生次数。


统计整体页面 PV 数


继承自 NavigatorObserver 的观察者,并在其 didPush 方法中,去累加页面的打开次数。


2、页面帧率


Flutter 在全局 Window 对象上提供了帧回调机制。我们可以在 Window 对象上注册 onReportTimings 方法,将最近绘制帧耗费的时间(即 FrameTiming),以回调的形式告诉我们。


有了每一帧的绘制时间后,我们就可以计算 FPS 了。


为了让 FPS 的计算更加平滑,我们需要保留最近 25 个 FrameTiming 用于求和计算。


由于帧的渲染是依靠 VSync 信号驱动的,如果帧绘制的时间没有超过 16.67 ms,我们也需要把它当成 16.67 ms 来算,因为绘制完成的帧必须要等到下一次 VSync 信号来了之后才能渲染。而如果帧绘制时间超过了 16.67 ms,则会占用后续 VSync 的信号周期,从而打乱后续的绘制次序,产生卡顿现象。


那么,页面帧率的统计公式就是:



FPS = 60 * 实际渲染的帧数 / 本来应该在这个时间内渲染完成的帧数。



首先,定义一个容量为 25 的列表,用于存储最近的帧绘制耗时 FrameTiming。


然后,在 FPS 的计算函数中,你再将列表中每帧绘制时间与 VSync 周期 frameInterval 进行比较,得出本来应该绘制的帧数。


最后,两者相除就得到了 FPS 指标。


3、页面加载时长



页面加载时长 = 页面可见的时间 - 页面创建的时间(包括网络加载时长)



统计页面可见的时间


WidgetsBinding 提供了单次 Frame 回调的 addPostFrameCallback 方法,它会在当前 Frame 绘制完成之后进行回调,并且只会回调一次。一旦监听到 Frame 绘制完成回调后,我们就可以确认页面已经被渲染出来了,因此我们可以借助这个方法去获取页面的渲染完成时间 endTime。


统计页面创建的时间


获取页面创建的时间比较容易,我们只需要在页面的初始化函数 initState() 里记录页面的创建时间 startTime。


最后,再将这两个时间做减法,你就能得到页面的加载时长。


需要注意的是,正常的页面加载时长一般都不应该超过2秒。如果超过了,则意味着有严重的性能问题。


三、布局加载优化



Flutter 为什么要使用声明书 UI 的编写方式?



为了减轻开发人员的负担,无需编写如何在不同的 UI 状态之间进行切换的代码,Flutter 使用了声明式的 UI 编写方式,而不是 Android 和 iOS 中的命令式编写方式。


这样的话,当用户界面发生变化时,Flutter 不会修改旧的 Widget 实例,而是会构造新的 Widget 实例


Fluuter 框架使用 RenderObjects 管理传统 UI 对象的职责(比如维护布局的状态)。 RenderObjects 在帧之间保持不变, Flutter 的轻量级 Widget 通知框架在状态之间修改 RenderObjects, 而 Flutter Framework 则负责处理其余部分。


1、常规优化


常规优化即针对 build() 进行优化,build() 方法中的性能问题一般有两种:耗时操作和 Widget 堆叠


1)、在 build() 方法中执行了耗时操作


我们应该尽量避免在 build() 中执行耗时操作,因为 build() 会被频繁地调用,尤其是当 Widget 重建的时候。


此外,我们不要在代码中进行阻塞式操作,可以将文件读取、数据库操作、网络请求等通过 Future 来转换成异步方式来完成。


最后,对于 CPU 计算频繁的操作,例如图片压缩,可以使用 isolate 来充分利用多核心 CPU。


isolate 作为 Flutter 中的多线程实现方式,之所以被称之为 isolate(隔离),是因为每一个 isolate 都有一份单独的内存


Flutter 会运行一个事件循环,它会从事件队列中取得最旧的事件,处理它,然后再返回下一个事件进行处理,依此类推,直到事件队列清空为止。每当动作中断时,线程就会等待下一个事件


实质上,不仅仅是 isolate,所有的高级 API 都能够应用于异步编程,例如 Futures、Streams、async 和 await,它们全部都是构建在这个简单的事件循环之上。


而,async 和 await 实际上只是使用 futures 和 streams 的替代语法,它将代码编写形式从异步变为同步,主要用来帮助你编写更清晰、简洁的代码。


此外,async 和 await 也能使用 try on catch finally 来进行异常处理,这能够帮助你处理一些数据解析方面的异常。


2)、build() 方法中堆砌了大量的 Widget


这将会导致三个问题:



  • 1、代码可读性差:画界面时需要一个 Widget 嵌套一个 Widget,但如果 Widget 嵌套太深,就会导致代码的可读性变差,也不利于后期的维护和扩展。

  • 2、复用难:由于所有的代码都在一个 build(),会导致无法将公共的 UI 代码复用到其它的页面或模块。

  • 3、影响性能:我们在 State 上调用 setState() 时,所有 build() 中的 Widget 都将被重建,因此 build() 中返回的 Widget 树越大,那么需要重建的 Widget 就越多,也就会对性能越不利。


所以,你需要 控制 build 方法耗时,将 Widget 拆小,避免直接返回一个巨大的 Widget,这样 Widget 会享有更细粒度的重建和复用。


3)、使用 Widget 而不是函数


如果一个函数可以做同样的事情,Flutter 就不会有 StatelessWidget ,使用 StatelessWidget 的最大好处在于:能尽量避免不必要的重建。总的来说,它的优势有:



  • 1)、允许性能优化:const 构造函数,更细粒度的重建等等。

  • 2)、确保在两个不同的布局之间切换时,能够正确地处理资源(因为函数可能重用某些先前的状态)。

  • 3)、确保热重载正常工作,使用函数可能会破坏热重载。

  • 4)、在 flutter 自带的 Widget 显示工具中能看到 Widget 的状态和参数。

  • 5)、发生错误时,有更清晰的提示:此时,Flutter 框架将为你提供当前构建的 Widget 名称,更容易排查问题。

  • 6)、可以定义 key 和方便使用 context 的 API。


4)、尽可能地使用 const


如果某一个实例已经用 const 定义好了,那么其它地方再次使用 const 定义时,则会直接从常量池里取,这样便能够节省 RAM。


5)、尽可能地使用 const 构造器


当构建你自己的 Widget 或者使用 Flutter 的 Widget 时,这将会帮助 Flutter 仅仅去 rebuild 那些应当被更新的 Widget。


因此,你应该尽量多用 const 组件,这样即使父组件更新了,子组件也不会重新进行 rebuild 操作。特别是针对一些长期不修改的组件,例如通用报错组件和通用 loading 组件等。


6)、使用 nil 去替代 Container() 和 SizedBox()


首先,你需要明白 nil 仅仅是一个基础的 Widget 元素 ,它的构建成本几乎没有。


在某些情况下,如果你不想显示任何内容,且不能返回 null 的时候,你可能会返回类似 const SizedBox/Container 的 Widget,但是 SizedBox 会创建 RenderObject,而渲染树中的 RenderObject 会带来多余的生命周期控制和额外的计算消耗,即便你没有给 SizedBox 指定任何的参数。


下面,是我平时使用 nil 的一套方式:


// BEST
text != null ? Text(text) : nil
or
if (text != null) Text(text)
text != null ? Text(text) : const Container()/SizedBox()
复制代码

7)、列表优化


在构建大型网格或列表的时候,我们要尽量避免使用 ListView(children: [],) 或 GridView(children: [],)。


因为,在这种场景下,不管列表内容是否可见,会导致列表中所有的数据都会被一次性绘制出来,这种用法类似于 Android 的 ScrollView。


如果我们列表数据比较大的时候,建议使用 ListView 和 GridView 的 builder 方法,它们只会绘制可见的列表内容,类似于 Android 的 RecyclerView。


其实,本质上,就是对列表采用了懒加载而不是直接一次性创建所有的子 Widget,这样视图的初始化时间就减少了


8)、针对于长列表,记得在 ListView 中使用 itemExtent。


有时候当我们有一个很长的列表,想要用滚动条来大跳时,使用 itemExtent 就很重要了,它会帮助 Flutter 去计算 ListView 的滚动位置而不是计算每一个 Widget 的高度,与此同时,它能够使滚动动画有更好的性能


9)、减少可折叠 ListView 的构建时间


针对于可折叠的 ListView,未展开状态时,设置其 itemCount 为 0,这样 item 只会在展开状态下才进行构建,以减少页面第一次的打开构建时间


10)、尽量不要为 Widget 设置半透明效果


考虑用图片的形式代替,这样被遮挡的部分 Widget 区域就不需要绘制了。


除此之外,还有网络请求预加载优化、抽取文本 Theme 等常规的优化方式就不赘述了。


2、深入优化


1)、优化光栅线程


所有的 Flutter 应用至少都会运行在两个并行的线程上:UI 线程和 Raster 线程


**UI 线程是你构建 Widgets 和运行应用逻辑的地方。
**
Raster 线程是 Flutter 用来栅格化你的应用的。它从 UI 线程获取指令并将它们转换为可以发送到图形卡的内容。


在光栅线程中,会获取图片的字节,调整图像的大小,应用透明度、混合模式、模糊等等,直到产生最后的图形像素。然后,光栅线程会将其发送到图形卡,继而发送到屏幕上显示。


使用 Flutter DevTools-Performance 进行检测,步骤如下:



  • 1、在 Performance Overlay 中,查看光栅线程和 UI 线程哪个负载过重。

  • 2、在 Timeline Events 中,找到那些耗费时间最长的事件,例如常见的 SkCanvas::Flush,它负责解决所有待处理的 GPU 操作。

  • 3、找到对应的代码区域,通过删除 Widgets 或方法的方式来看对性能的影响。


2)、用 key 加速 Flutter 的性能优化光栅线程


一个 element 是由 Widget 内部创建的,它的主要目的是,知道对应的 Widget 在 Widget 树中所处的位置。但是元素的创建是非常昂贵的,通过 Keys(ValueKeys 和 GlobalKeys),我们可以去重复使用它们。



GlobalKey 与 ValueKey 的区别?



GlobalKey 是全局使用的 key,在跨小部件的场景时,你就可以使用它去刷新其它小部件。但,它是很昂贵的,如果你不需要访问 BuildContext、Element 和 State,应该尽量使用 LocalKey。


而 ValueKey 和 ObjectKey、UniqueKey 一样都归属于局部使用的 LocalKey,无法跨容器使用,ValueKey 比较的是 Widget 的值,而 ObjectKey 比较的是对象的 key,UniqueKey 则每次都会生成一个不同的值。


元素的生命周期



  • Mount:挂载,当元素第一次被添加到树上的时候调用。

  • Active:当需要激活之前失活的元素时被调用。

  • Update:用新数据去更新 RenderObject。

  • Deactive:当元素从 Widget 树中被移除或移动时被调用。如果一个元素在同一帧期间被移动了且它有 GlobalKey,那么它仍然能够被激活。

  • UnMount:卸载,如果一个元素在一帧期间没有被激活,它将会被卸载,并且再也不会被复用。


优化方式


**为了去改善性能,你需要去尽可能让 Widget 使用 Activie 和 Update 操作,并且尽量避免让 Widget触发 UnMount 和 Mount。**而使用 GlobayKeys 和 ValueKey 则能做到这一点:


/// 1、给 MaterialApp 指定 GlobalKeys
MaterialApp(key: global, home: child,);
/// 2、通过把 ValueKey 分配到正在被卸载的根 Widget,你就能够
/// 减少 Widget 的平均构建时间。
Widget build(BuildContext context) {
return Column(
children: [
value
? const SizedBox(key: ValueKey('SizedBox'))
: const Placeholder(key: ValueKey('Placeholder')),
GestureDetector(
key: ValueKey('GestureDetector'),
onTap: () {
setState(() {
value = !value;
});
},
child: Container(
width: 100,
height: 100,
color: Colors.red,
),
),
!value
? const SizedBox(key: ValueKey('SizedBox'))
: const Placeholder(key: ValueKey('Placeholder')),
],
);
}


如何知道哪些 Widget 会被 Update,哪些 Widget会被 UnMount?



只有 build 直接 return 的那个根 Widget 会自动更新,其它都有可能被 UnMount,因此都需要给其分配 ValueKey。



为什么没有给 Container 分配 ValueKey?



因为 Container 是 GestureDetector 的一个子 Widget,所以当给 GestureDetector 使用 ValueKey 去实现复用更新时,Container 也能被自动更新。


优化效果


优化前:



优化后:



可以看到,平均构建时间 由 5.5ms 减少到 1.6ms,优化效果还是很明显的。


优势


大幅度减少 Widget的平均构建时间。


缺点



  • 过多使用 ValueKey 会让你的代码变得更冗余。

  • 如果你的根 Widget 是 MaterialApp 时,则需要使用 GlobalKey,但当你去重复使用 GlobalKey 时可能会导致一些错误,所以一定要避免滥用 Key。


注意📢:在大部分场景下,Flutter 的性能都是足够的,不需要这么细致的优化,只有当产生了视觉上的问题,例如卡顿时才需要去分析优化。


四、启动速度优化


1、Flutter 引擎预加载


使用它可以达到页面秒开的一个效果,具体实现为:


在 HIFlutterCacheManager 类中定义一个 preLoad 方法,使用 Looper.myQueue().addIdleHandler 添加一个 idelHandler,当 CPU 空闲时会回调 queueIdle 方法,在这个方法里,你就可以去初始化 FlutterEngine,并把它缓存到集合中。


预加载完成之后,你就可以通过 HIFlutterCacheManager 类的 getCachedFlutterEngine 方法从集合中获取到缓存好的引擎。


2、Dart VM 预热


对于 Native + Flutter 的混合场景,如果不想使用引擎预加载的方式,那么要提升 Flutter 的启动速度也可以通 过Dart VM 预热来完成,这种方式会提升一定的 Flutter 引擎加载速度,但整体对启动速度的提升没有预加载引擎提升的那么多。



无论是引擎预加载还是 Dart VM 预热都是有一定的内存成本的,如果 App 内存压力不大,并且预判用户接下来会访问 Flutter 业务,那么使用这个优化就能带来很好的价值;反之,则可能造成资源浪费,意义不大。


五、内存优化


1、const 实例化


优势


**const 对象只会创建一个编译时的常量值。在代码被加载进 Dart Vm 时,在编译时会存储在一个特殊的查询表里,由于 flutter 采用了 AoT 编译,const + values 的方式会提供一些小的性能优势。**例如:const Color() 仅仅只分配一次内存给当前实例。


应用场景


Color()、GlobayKey() 等等。


2、识别出消耗多余内存的图片


Flutter Inspector:点击 “Invert Oversized Images”,它会识别出那些解码大小超过展示大小的图片,并且系统会将其倒置,这些你就能更容易在 App 页面中找到它。



针对这些图片,你可以指定 cacheWidth 和 cacheHeight 为展示大小,这样可以让 flutter 引擎以指定大小解析图片,减少内存消耗。


3、针对 ListView item 中有 image 的情况来优化内存


ListView 不能够杀死那些在屏幕可视范围之外的那些 item,如果 item 使用了高分辨率的图片,那么它将会消耗非常多的内存。


换言之,ListView 在默认情况下会在整个滑动/不滑动的过程中让子 Widget 保持活动状态,这一点是通过 AutomaticKeepAlive 来保证,在默认情况下,每个子 Widget 都会被这个 Widget 包裹,以使被包裹的子 Widget 保持活跃。


其次,如果用户向后滚动,则不会再次重新绘制子 Widget,这一点是通过 RepaintBoundaries 来保证,在默认情况下,每个子 Widget 都会被这个 Widget 包裹,它会让被包裹的子 Widget 仅仅绘制一次,以此获得更高的性能。


但,这样的问题在于,如果加载大量的图片,则会消耗大量的内存,最终可能使 App 崩溃。


解决方案


通过将这两个选项置为 false 来禁用它们,这样不可见的子元素就会被自动处理和 GC。


ListView.builder(
...
addAutomaticKeepAlives: false (true by default)
addRepaintBoundaries: false (true by default)
);

由于重新绘制子元素和管理状态等操作会占用更多的 CPU 和 GPU 资源,但是它能够解决你 App 的内存问题,并且会得到一个高性能的视图列表。


六、包体积优化


1、图片优化


对图片压缩或使用在线的网络图片。


2、移除冗余的二三库


随着业务的增加,项目中会引入越来越多的二三方库,其中有不少是功能重复的,甚至是已经不再使用的。移除不再使用的和将相同功能的库进行合并可以进一步减少包体积。


3、启用代码缩减和资源缩减


打开 minifyEnabled 和 shrinkResources,构建出来的 release 包会减少 10% 左右的大小,甚至更多。


4、构建单 ABI 架构的包


目前手机市场上,x86 / x86_64/armeabi/mips / mips6 的占有量很少,arm64-v8a 作为最新一代架构,是目前的主流,而 armeabi-v7a 只存在少部分的老旧手机中。


所以,为了进一步优化包大小,你可以构建出单一架构的安装包,在 Flutter 中可以通过以下方式来构建出单一架构的安装包


cd 
flutter build apk --split-per-abi

如果想进一步压缩包体积可将 so 进行动态下发,将 so 放在远端进行动态加载,不仅能进一步减少包体积也可以实现代码的热修复和动态加载。


七、总结


在本篇文章中,我主要从以下 六个方面 讲解了 Flutter 性能优化相关的知识:


1)、检测手段:Flutter Inspector、性能图层、Raster 和 UI 线程问题的定位
使用 checkerboardOffscreenLayers 检查多视图叠加的视图渲染 、使用 checkerboardRasterCacheImages 检查缓存的图像。
2)、关键优化指标:包括页面异常率、页面帧率、页面加载时长。
3)、布局加载优化:十大常规优化、优化光栅线程、用 key 加速 Flutter 的性能。
4)、启动速度优化:引擎预加载和 Dart VM 预热。
5)、内存优化:const 实例化、识别出消耗多余内存的图片、针对 ListView item 中有 image 的情况来优化内存。
6)、包体积优化:图片优化、移除冗余的二三库、启用代码缩减和资源缩减、构建单 ABI 架构的包。


在近一年实践 Flutter 的过程中,越发发现一个人真正应该具备的核心能力应该是你的思考能力。


思考能力,包括 结构化思考/系统性思考/迁移思考/层级思考/逆向思考/多元思考 等,使用这些思考能力分析问题时能快速地把握住问题的本质,在本质上做功夫,才是王道,才是真的 yyds。


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

重谈Handler的内存泄漏

Handler 的内存泄漏问题 在多线程操作中,handler会使用的非常多,但是每次使用handler你有没有考虑内存泄漏的问题。 如果你使用handler进行操作时,你会发现出现以下提示 This Handler class should be stati...
继续阅读 »

Handler 的内存泄漏问题


在多线程操作中,handler会使用的非常多,但是每次使用handler你有没有考虑内存泄漏的问题。


如果你使用handler进行操作时,你会发现出现以下提示
This Handler class should be static or leaks might occur (anonymous android.os.Handler)这样的提示。翻译:
由于此Handler被声明为内部类,因此可能会阻止外部类被垃圾回收。 如果Handler使用Looper或MessageQueue作为主线程以外的线程,则没有问题。 如果Handler正在使用主线程的Looper或MessageQueue,则需要修复Handler声明,如下所示:将Handler声明为静态类; 在外部类中,实例化外部类的WeakReference,并在实例化Handler时将此对象传递给Handler; 使用WeakReference对象对外部类的成员进行所有引用。



警告原因:handler没有设置为静态类,声明内部类可能会阻止被GC回收,从而导致内存泄漏



那么为什么会造成内存泄漏呢。
首先来说下什么是内存泄漏
内存泄漏(Memory Leak):指的是程序已经动态分配的堆内存由于某种原因程序未释放或者无法释放,造成系统资源浪费,会造成程序运行缓慢甚至系统崩溃等严重后果。
问题代码:


public class MainActivity extends AppCompatActivity {
private Handler mHandler = new Handler();
private TextView mTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = (TextView) findViewById(R.id.tv);
//模拟内存泄漏
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
mTextView.setText(&quot;yiyi&quot;);
}
}, 1000);
}

内存泄漏原因


从上面问题代码,可以看出这里通过内部类方式创建handler,而在java中,非静态内部类会持有外部类的引用,这里的postDelayed是一个延迟处理消息,将一个handler装入到message中,将消息放进消息队列messageQueueLooper进行取消息进行处理。如果此时activity要退出了,想要调用**destroy**销毁,但是此时Looper正在处理消息,**Looper**的生命周期明显比activity长,这将使得activity无法被**GC**回收,最终造成内存泄漏。并且此时handler还持有activity的引用,也是造成内存泄漏的一个原因(不是根本原因)。



但是我觉得真正handler造成内存泄漏的根本原因是生命周期比activity长,比如TextView也是内部类创建的,那么它怎么没有造成内存泄漏,它也持有外部类Activity的引用,根本原因是它的生命周期比Activity短,Activity销毁时候,它可以被GC回收



总结


当handler有没有处理的消息或者正在处理消息,此时Handler的生命周期明显比Activity长,GC持有Activity与handler两者的引用,导致Activity无法被GC回收,造成内存泄漏。而handler是不是内部类,并不是造成内存泄漏的根本原因。


解决方案


静态内部类+弱引用



将Handler的子类设置成 静态内部类,并且可加上 使用WeakReference弱引用持有Activity实例



原因:弱引用的对象拥有短暂的生命周期。而垃圾回收器不管内存是否充足都会回收弱引用对象。


public class HandlerActivity extends AppCompatActivity  {
private static class MyHandler extends Handler {
private final WeakReference&lt;HandlerActivity&gt; mActivity;
public MyHandler(HandlerActivity activity) {
mActivity = new WeakReference&lt;HandlerActivity&gt;(activity);
}

@Override
public void handleMessage(Message msg) {
HandlerActivity activity = mActivity.get();
if (activity != null) {
}
}

private final MyHandler mHandler = new MyHandler(this);
private static final Runnable mRunnable = new Runnable() {
@Override
public void run() { }
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mHandler.postDelayed(mRunnable, 1000 * 60 * 1);
finish();
}
}
复制代码

Activity生命周期结束时,清空消息队列
只需在Activity的onDestroy()方法中调用mHandler.removeCallbacksAndMessages(null);就行了。


@Override
protected void onDestroy() {
super.onDestroy();
if(handler!=null){
handler.removeCallbacksAndMessages(null);
handler = null;
}
}

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

前端到底用nginx来做啥

这篇文章是收集我在工作中经常会用到的nginx相关知识点,本文并不是基础知识的讲解更多的是一些方案中的简单实现。location的匹配规则= 表示精确匹配。只有请求的url路径与后面的字符串完全相等时,才会命中。^~ 表示如果该符号后面的字符是最佳匹配,采用该...
继续阅读 »

这篇文章是收集我在工作中经常会用到的nginx相关知识点,本文并不是基础知识的讲解更多的是一些方案中的简单实现。

location的匹配规则

  1. = 表示精确匹配。只有请求的url路径与后面的字符串完全相等时,才会命中。

  2. ^~ 表示如果该符号后面的字符是最佳匹配,采用该规则,不再进行后续的查找。

  3. ~ 表示该规则是使用正则定义的,区分大小写。

  4. ~* 表示该规则是使用正则定义的,不区分大小写。

注意的是,nginx的匹配优先顺序按照上面的顺序进行优先匹配,而且注意的是一旦某一个匹配命中直接退出,不再进行往下的匹配

剩下的普通匹配会按照最长匹配长度优先级来匹配,就是谁匹配的越多就用谁。

server {
   server_name website.com;
   location /document {
       return 701;
  }
   location ~* ^/docume.*$ {
       return 702;
  }
   location ~* ^/document$ {
       return 703;
  }

}
curl -I website.com:8080/document 702
# 匹配702 因为正则的优先级更高,而且正则是一旦匹配到就直接退出 所以不会再匹配703

server {
   server_name website.com;
   location ~* ^/docume.*$ {
       return 701;
  }

   location ^~ /doc {
       return 702;
  }
   location ~* ^/document$ {
       return 703;
  }
}
curl http://website.com/document
HTTP/1.1 702
# 匹配702 因为 ^~精确匹配的优先级比正则高 也是匹配到之后支持退出

server {
   server_name website.com;
   location /doc {
       return 702;
  }
   location /docu {
       return 701;
  }
}
# 701 前缀匹配匹配是按照最长匹配,跟顺序无关

history模式、跨域、缓存、反向代理

# html设置history模式
location / {
   index index.html index.htm;
   proxy_set_header Host $host;
   # history模式最重要就是这里
   try_files $uri $uri/ /index.html;
   # index.html文件不可以设置强缓存 设置协商缓存即可
   add_header Cache-Control 'no-cache, must-revalidate, proxy-revalidate, max-age=0';
}

# 接口反向代理
location ^~ /api/ {
   # 跨域处理 设置头部域名
   add_header Access-Control-Allow-Origin *;
   # 跨域处理 设置头部方法
   add_header Access-Control-Allow-Methods 'GET,POST,DELETE,OPTIONS,HEAD';
   # 改写路径
   rewrite ^/api/(.*)$ /$1 break;
   # 反向代理
   proxy_pass http://static_env;
   proxy_set_header Host $http_host;
}

location ~* \.(?:css(\.map)?|js(\.map)?|gif|svg|jfif|ico|cur|heic|webp|tiff?|mp3|m4a|aac|ogg|midi?|wav|mp4|mov|webm|mpe?g|avi|ogv|flv|wmv)$ {
   # 静态资源设置七天强缓存
   expires 7d;
   access_log off;
}

以目录去区分多个history单文件

因为不可能每一个项目开启一个域名,仅仅指向通过增加路径来划分多个网站,比如:

  1. http://www.taobao.com/tmall/login访问天猫的登录页面

  2. http://www.taobao.com/alipay/login访问支付宝的登录页面

server {
   listen 80;
   server_name taobao.com;
   index index.html index.htm;
   # 通过正则来匹配捕获 [tmall|alipay]中间的这个路径
   location ~ ^/([^\/]+)/(.*)$ {
       try_files $uri $uri/ /$1/dist/index.html =404;
  }
}

负载均衡

基于upstream做负载均衡,中间会涉及一些相关的策略比如ip_hashweight

upstream backserver{ 
   # 哈希算法,自动定位到该服务器 保证唯一ip定位到同一部机器 用于解决session登录态的问题
   ip_hash;
   server 127.0.0.1:9090 down; (down 表示单前的server暂时不参与负载)
   server 127.0.0.1:8080 weight=2; (weight 默认为1.weight越大,负载的权重就越大)
   server 127.0.0.1:6060;
   server 127.0.0.1:7070 backup; (其它所有的非backup机器down或者忙的时候,请求backup机器)
}

灰度部署

如何根据headers头部来进行灰度,下面的例子是用cookie来设置

如何获取头部值在nginx中可以通过$http_xxx来获取变量

upstream stable {
   server xxx max_fails=1 fail_timeout=60;
   server xxx max_fails=1 fail_timeout=60;
}
upstream canara {
  server xxx max_fails=1 fail_timeout=60;
}

server {
   listen 80;
   server_name xxx;
   # 设置默认
   set $group "stable";

   # 根据cookie头部设置接入的服务
   if ($http_cookie ~* "tts_version_id=canara"){
       set $group canara;
  }
   if ($http_cookie ~* "tts_version_id=stable"){
       set $group stable;
  }
   location / {
       proxy_pass http://$group;
       proxy_set_header   Host             $host;
       proxy_set_header   X-Real-IP       $remote_addr;
       proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
       index  index.html index.htm;
  }
}

优雅降级

常用于ssr的node服务挂了返回500错误码然后降级到csr的cos桶或者nginx中

优雅降级主要用error_page参数来进行降级指向备用地址。

upstream ssr {
   server xxx max_fails=1 fail_timeout=60;
   server xxx max_fails=1 fail_timeout=60;
}
upstream csr {
   server xxx max_fails=1 fail_timeout=60;
   server xxx max_fails=1 fail_timeout=60;
}

location ^~ /ssr/ {
   proxy_pass http://ssr;
   # 开启自定义错误捕获 如果这里不设置为on的话 会走向nginx处理的默认错误页面
   proxy_intercept_errors on;
   # 捕获500系列错误 如果500错误的话降级为下面的csr渲染
   error_page 500 501 502 503 504 = @csr_location

   # error_page 500 501 502 503 504 = 200 @csr_location
   # 注意这上面的区别 等号前面没有200 表示 最终返回的状态码已 @csr_location为准 加了200的话表示不管@csr_location返回啥都返回200状态码
}

location @csr_location {
   # 这时候地址还是带着/ssr/的要去除
   rewrite ^/ssr/(.*)$ /$1 break;
   proxy_pass http://csr;
   rewrite_log on;
}

webp根据浏览器自动降级为png

这套方案不像常见的由nginx把png转为webp的方案,而是先经由图床系统(node服务)上传两份图片:

  1. 一份是原图png

  2. 一份是png压缩为webp的图片(使用的是imagemin-webp)

然后通过nginx检测头部是否支持webp来返回webp图片,不支持的话就返回原图即可。这其中还做了错误拦截,如果cos桶丢失webp图片及时浏览器支持webp也要降级为png

http {
 include       /etc/nginx/mime.types;
 default_type application/octet-stream;

 # 设置日志格式
 log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
 '$status $body_bytes_sent "$http_referer" '
 '"$http_user_agent" "$http_x_forwarded_for"'
 '"$proxy_host" "$upstream_addr"';

 access_log /var/log/nginx/access.log main;

 sendfile       on;
 keepalive_timeout 65;

 # 开启gzip
 gzip on;
 gzip_vary on;
 gzip_proxied any;
 gzip_comp_level 6;
 gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;

 # 负载均衡 这里可以是多个cos桶地址即可
 upstream static_env {
   server xxx;
   server xxx;
}

 # map 设置变量映射 第一个变量指的是要通过映射的key值 Accpet 第二个值的是变量别名
 map $http_accept $webp_suffix {
   # 默认为 空字符串
   default   "";
   # 正则匹配如果Accep含有webp字段 设置为.webp值
   "~*webp"  ".webp";
}
 server {

   listen 8888;
   absolute_redirect off;    #取消绝对路径的重定向
   #网站主页路径。此路径仅供参考,具体请您按照实际目录操作。
   root /usr/share/nginx/html;

   location / {
     index index.html index.htm;
     proxy_set_header Host $host;
     try_files $uri $uri/ /index.html;
     add_header Cache-Control 'no-cache, max-age=0';
  }

   # favicon.ico
   location = /favicon.ico {
     log_not_found off;
     access_log off;
  }

   # robots.txt
   location = /robots.txt {
     log_not_found off;
     access_log off;
  }

   #
   location ~* \.(png|jpe?g)$ {
     # Pass WebP support header to backend
     # 如果header头部中支持webp
     if ($webp_suffix ~* webp) {
       # 先尝试找是否有webp格式图片
       rewrite ^/(.*)\.(png|jpe?g)$ /$1.webp break;
       # 找不到的话 这里捕获404错误 返回原始错误 注意这里的=号 代表最终返回的是@static_img的状态吗
       error_page 404 = @static_img;
    }
     proxy_intercept_errors on;
     add_header Vary Accept;
     proxy_pass http://static_env;
     proxy_set_header Host $http_host;
     expires 7d;
     access_log off;
  }

   location @static_img {
     #set $complete $schema $server_addr $request_uri;
     rewrite ^/.+$ $request_uri break;
     proxy_pass http://static_env;
     proxy_set_header Host $http_host;
     expires 7d;
  }


   # assets, media
   location ~* \.(?:css(\.map)?|js(\.map)?|gif|svg|jfif|ico|cur|heic|webp|tiff?|mp3|m4a|aac|ogg|midi?|wav|mp4|mov|webm|mpe?g|avi|ogv|flv|wmv)$ {
     proxy_pass http://static_env;
     proxy_set_header Host $http_host;
     expires 7d;
     access_log off;
  }


   error_page   500 502 503 504 /50x.html;
   location = /50x.html {
     root   /usr/share/nginx/html;
  }
}
}


作者:一米八的萝卜
来源:https://juejin.cn/post/7064378702779891749

收起阅读 »

专业前端怎么使用console

学习前端开发时,几乎最先学习的就是console.log()。毕竟多数人的第一行代码都是:console.log('Hello World');console对象提供了对于浏览器调试控制台的访问,可以从任何全局对象中访问到console对象。灵活运用conso...
继续阅读 »

学习前端开发时,几乎最先学习的就是console.log()

毕竟多数人的第一行代码都是:console.log('Hello World');

console对象提供了对于浏览器调试控制台的访问,可以从任何全局对象中访问到console对象。

灵活运用console对象所提供的方法,可以让开发变得更简单。

最常见的控制台方法:

console.log()– 打印内容的通用方法。
console.info()– 打印资讯类说明信息。
console.debug()– 在控制台打印一条 "debug" 级别的消息。
console.warn()– 打印一个警告信息。
console.error()– 打印一条错误信息。
复制代码


console.log()写css


console.log() 使用参数


console.clear();

用于清除控制台信息。


console.count(label);

输出count()被调用的次数,可以使用一个参数label。演示如下:

var user = "";

function greet() {
console.count(user);
return "hi " + user;
}

user = "bob";
greet();
user = "alice";
greet();
greet();
console.count("alice");
复制代码

输出


console.dir()

使用console.dir()可以打印对象的属性,在控制台中逐级查看对象的详细信息。


console.memory

console.memory是一个属性,而不是方法,使用memory属性用来检查内存信息。


console.time() 和 console.timeEnd()

  • console.time()– 使用输入参数的名称启动计时器。在给定页面上最多可以同时运行 10,000 个计时器。

  • console.timeEnd()– 停止指定的计时器并记录自启动以来经过的时间(以毫秒为单位)。


console.assert()

如果断言为假,将错误信息写入控制台,如果为真,无显示。


console.trace();

console.trace()方法将堆栈跟踪输出到控制台。


console.table();

console中还可以打印表格



打印Html元素


console.group() 和 console.groupEnd()

在控制台上创建一个新的分组,随后输出到控制台上的内容都会被添加到一个锁进,表示该内容属于当前分组,知道调用console.groupEnd()之后,当前分组结束。



作者:正经程序员
来源:https://juejin.cn/post/7065856171436933156

收起阅读 »

10个常见的前端手写功能,你全都会吗?

万丈高楼平地起,地基打的牢,才能永远立于不败之地。今天给大家带来的是10个常见的 JavaScript 手写功能,重要的地方已添加注释。有的是借鉴别人的,有的是自己写的,如有不正确的地方,欢迎多多指正。1、防抖function debounce(fn, del...
继续阅读 »

万丈高楼平地起,地基打的牢,才能永远立于不败之地。今天给大家带来的是10个常见的 JavaScript 手写功能,重要的地方已添加注释。有的是借鉴别人的,有的是自己写的,如有不正确的地方,欢迎多多指正。

1、防抖

function debounce(fn, delay) {
 let timer
 return function (...args) {
   if (timer) {
     clearTimeout(timer)
  }
   timer = setTimeout(() => {
     fn.apply(this, args)
  }, delay)
}
}

// 测试
function task() {
 console.log('run task')
}
const debounceTask = debounce(task, 1000)
window.addEventListener('scroll', debounceTask)
复制代码

2、节流

function throttle(fn, delay) {
 let last = 0 // 上次触发时间
 return (...args) => {
   const now = Date.now()
   if (now - last > delay) {
     last = now
     fn.apply(this, args)
  }
}
}

// 测试
function task() {
 console.log('run task')
}
const throttleTask = throttle(task, 1000)
window.addEventListener('scroll', throttleTask)
复制代码

3、深拷贝

function deepClone(obj, cache = new WeakMap()) {
 if (obj === null || typeof obj !== 'object') return obj
 if (obj instanceof Date) return new Date(obj)
 if (obj instanceof RegExp) return new RegExp(obj)
 
 if (cache.get(obj)) return cache.get(obj) // 如果出现循环引用,则返回缓存的对象,防止递归进入死循环
 let cloneObj = new obj.constructor() // 使用对象所属的构造函数创建一个新对象
 cache.set(obj, cloneObj) // 缓存对象,用于循环引用的情况

 for (let key in obj) {
   if (obj.hasOwnProperty(key)) {
     cloneObj[key] = deepClone(obj[key], cache) // 递归拷贝
  }
}
 return cloneObj
}

// 测试
const obj = { name: 'Jack', address: { x: 100, y: 200 } }
obj.a = obj // 循环引用
const newObj = deepClone(obj)
console.log(newObj.address === obj.address) // false
复制代码

4、手写 Promise

class MyPromise {
 constructor(executor) {
   this.status = 'pending' // 初始状态为等待
   this.value = null // 成功的值
   this.reason = null // 失败的原因
   this.onFulfilledCallbacks = [] // 成功的回调函数存放的数组
   this.onRejectedCallbacks = [] // 失败的回调函数存放的数组
   let resolve = value => {
     if (this.status === 'pending') {
       this.status = 'fulfilled'
       this.value = value;
       this.onFulfilledCallbacks.forEach(fn => fn()) // 调用成功的回调函数
    }
  }
   let reject = reason => {
     if (this.status === 'pending') {
       this.status = 'rejected'
       this.reason = reason
       this.onRejectedCallbacks.forEach(fn => fn()) // 调用失败的回调函数
    }
  };
   try {
     executor(resolve, reject)
  } catch (err) {
     reject(err)
  }
}
 then(onFulfilled, onRejected) {
   // onFulfilled如果不是函数,则修改为函数,直接返回value
   onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
   // onRejected如果不是函数,则修改为函数,直接抛出错误
   onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err }
   return new MyPromise((resolve, reject) => {
     if (this.status === 'fulfilled') {
       setTimeout(() => {
         try {
           let x = onFulfilled(this.value);
           x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
        } catch (err) {
           reject(err)
        }
      })
    }
     if (this.status === 'rejected') {
       setTimeout(() => {
         try {
           let x = onRejected(this.reason)
           x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
        } catch (err) {
           reject(err)
        }
      })
    }
     if (this.status === 'pending') {
       this.onFulfilledCallbacks.push(() => { // 将成功的回调函数放入成功数组
         setTimeout(() => {
           let x = onFulfilled(this.value)
           x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
        })
      })
       this.onRejectedCallbacks.push(() => { // 将失败的回调函数放入失败数组
         setTimeout(() => {
           let x = onRejected(this.reason)
           x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
        })
      })
    }
  })
}
}

// 测试
function p1() {
 return new MyPromise((resolve, reject) => {
   setTimeout(resolve, 1000, 1)
})
}
function p2() {
 return new MyPromise((resolve, reject) => {
   setTimeout(resolve, 1000, 2)
})
}
p1().then(res => {
 console.log(res) // 1
 return p2()
}).then(ret => {
 console.log(ret) // 2
})
复制代码

5、异步控制并发数

function limitRequest(urls = [], limit = 3) {
 return new Promise((resolve, reject) => {
   const len = urls.length
   let count = 0

   // 同时启动limit个任务
   while (limit > 0) {
     start()
     limit -= 1
  }

   function start() {
     const url = urls.shift() // 从数组中拿取第一个任务
     if (url) {
       axios.post(url).then(res => {
         // todo
      }).catch(err => {
         // todo
      }).finally(() => {
         if (count == len - 1) {
           // 最后一个任务完成
           resolve()
        } else {
           // 完成之后,启动下一个任务
           count++
           start()
        }
      })
    }
  }

})
}

// 测试
limitRequest(['http://xxa', 'http://xxb', 'http://xxc', 'http://xxd', 'http://xxe'])
复制代码

6、继承

ES5继承(寄生组合继承)

function Parent(name) {
 this.name = name
}
Parent.prototype.eat = function () {
 console.log(this.name + ' is eating')
}

function Child(name, age) {
 Parent.call(this, name)
 this.age = age
}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.contructor = Child

// 测试
let xm = new Child('xiaoming', 12)
console.log(xm.name) // xiaoming
console.log(xm.age) // 12
xm.eat() // xiaoming is eating
复制代码

ES6继承

class Parent {
 constructor(name) {
   this.name = name
}
 eat() {
   console.log(this.name + ' is eating')
}
}

class Child extends Parent {
 constructor(name, age) {
   super(name)
   this.age = age
}
}

// 测试
let xm = new Child('xiaoming', 12)
console.log(xm.name) // xiaoming
console.log(xm.age) // 12
xm.eat() // xiaoming is eating
复制代码

7、数组排序

sort 排序

// 对数字进行排序,简写
const arr = [3, 2, 4, 1, 5]
arr.sort((a, b) => a - b)
console.log(arr) // [1, 2, 3, 4, 5]

// 对字母进行排序,简写
const arr = ['b', 'c', 'a', 'e', 'd']
arr.sort()
console.log(arr) // ['a', 'b', 'c', 'd', 'e']
复制代码

冒泡排序

function bubbleSort(arr) {
let len = arr.length
for (let i = 0; i < len - 1; i++) {
// 从第一个元素开始,比较相邻的两个元素,前者大就交换位置
for (let j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
let num = arr[j]
arr[j] = arr[j + 1]
arr[j + 1] = num
}
}
// 每次遍历结束,都能找到一个最大值,放在数组最后
}
return arr
}

//测试
console.log(bubbleSort([2, 3, 1, 5, 4])) // [1, 2, 3, 4, 5]
复制代码

8、数组去重

Set 去重

const newArr = [...new Set(arr)]
// 或
const newArr = Array.from(new Set(arr))
复制代码

indexOf 去重

function resetArr(arr) {
 let res = []
 arr.forEach(item => {
   if (res.indexOf(item) === -1) {
     res.push(item)
  }
})
 return res
}

// 测试
const arr = [1, 1, 2, 3, 3]
console.log(resetArr(arr)) // [1, 2, 3]
复制代码

9、获取 url 参数

URLSearchParams 方法

// 创建一个URLSearchParams实例
const urlSearchParams = new URLSearchParams(window.location.search);
// 把键值对列表转换为一个对象
const params = Object.fromEntries(urlSearchParams.entries());
复制代码

split 方法

function getParams(url) {
 const res = {}
 if (url.includes('?')) {
   const str = url.split('?')[1]
   const arr = str.split('&')
   arr.forEach(item => {
     const key = item.split('=')[0]
     const val = item.split('=')[1]
     res[key] = decodeURIComponent(val) // 解码
  })
}
 return res
}

// 测试
const user = getParams('http://www.baidu.com?user=%E9%98%BF%E9%A3%9E&age=16')
console.log(user) // { user: '阿飞', age: '16' }
复制代码

10、事件总线 | 发布订阅模式

class EventEmitter {
 constructor() {
   this.cache = {}
}

 on(name, fn) {
   if (this.cache[name]) {
     this.cache[name].push(fn)
  } else {
     this.cache[name] = [fn]
  }
}

 off(name, fn) {
   const tasks = this.cache[name]
   if (tasks) {
     const index = tasks.findIndex((f) => f === fn || f.callback === fn)
     if (index >= 0) {
       tasks.splice(index, 1)
    }
  }
}

 emit(name, once = false) {
   if (this.cache[name]) {
     // 创建副本,如果回调函数内继续注册相同事件,会造成死循环
     const tasks = this.cache[name].slice()
     for (let fn of tasks) {
       fn();
    }
     if (once) {
       delete this.cache[name]
    }
  }
}
}

// 测试
const eventBus = new EventEmitter()
const task1 = () => { console.log('task1'); }
const task2 = () => { console.log('task2'); }

eventBus.on('task', task1)
eventBus.on('task', task2)
eventBus.off('task', task1)
setTimeout(() => {
 eventBus.emit('task') // task2
}, 1000)
复制代码

以上就是工作或求职中最常见的手写功能,你是不是全都掌握了呢,欢迎在评论区交流。如果文章对你有所帮助,


作者:前端阿飞
来源:https://juejin.cn/post/7031322059414175774

收起阅读 »

IDEA 不为人知的 5 个骚技巧!真香!

工欲善其事,必先利其器,磊哥最近发现了几个特别棒的 IDEA“骚”技巧,已经迫不及待的想要分享给你了,快上车...1.快速补全行末分号使用快捷键 Shfit + Ctrl + Enter 轻松实现。2.自带的 HTTP 请求工具IDEA 自带了 HTTP 的测...
继续阅读 »


工欲善其事,必先利其器,磊哥最近发现了几个特别棒的 IDEA“骚”技巧,已经迫不及待的想要分享给你了,快上车...

1.快速补全行末分号


使用快捷键 Shfit + Ctrl + Enter 轻松实现。

2.自带的 HTTP 请求工具

IDEA 自带了 HTTP 的测试工具,这个功能隐藏的有点深。

这下可以卸载掉 Postman 了(我信你个鬼,你个糟老头...),如下图所示:


使用快捷键 Shift + Ctrl + A,然后搜索 “rest client”,输入回车打开 HTTP 请求测试页面。

3.粘贴板历史记录

俗话说的好,程序员都是面向 CV 编程(Ctrl+C 复制、Ctrl+V 粘贴),那怎么能不知道这个神奇的功能呢?

只需要使用快捷键 Shitf + Ctrl + V 就打开粘贴板的历史记录了,话说这个快捷键磊哥最熟了呢,如下图所示:


4.神奇的 Language Injection

我们将 String 转换为 JSON 格式非常的麻烦,需要各种转义,而 IDEA 为我们提供了 Language Injection,可以轻松的将字符串转换为 JSON,如下图所示:


PS:妈妈再也不用担心我转换字符串了。

Language Injection 也可以支持正则表达式,甚至支持简单的正则表达式的测试能力:


5.秒查字节码

这是一个超牛的功能,磊哥最近才发现的。

从此可以告别传统的 javac 生成字节码,再用 javap -c xxx 查看字节码的方式了,IDEA 支持直接查看字节码,只能说相见恨晚,如下图所示:


最后

你还知道哪些更“骚”的技巧吗?欢迎评论区留言补充。

参考 & 鸣谢

http://www.jianshu.com/p/364b94a66…

作者:Java中文社群
来源:https://juejin.cn/post/6846687591199145998

收起阅读 »

中国邮政竟然开咖啡店了?

近日,一家特别的咖啡馆在厦门国贸大厦开业。这家咖啡馆由厦门国贸邮政支局改造而成,门牌左边写着“中国邮政”,右边写着“邮局咖啡”,被视为中国邮政进军咖啡领域的标志。“天涯海角都能送达”的国民“慢递”公司中国邮政,竟然跨界开起了咖啡馆,该消息一经传出就引来众多关注...
继续阅读 »

近日,一家特别的咖啡馆在厦门国贸大厦开业。

这家咖啡馆由厦门国贸邮政支局改造而成,门牌左边写着“中国邮政”,右边写着“邮局咖啡”,被视为中国邮政进军咖啡领域的标志。

pic_630dec45.png

“天涯海角都能送达”的国民“慢递”公司中国邮政,竟然跨界开起了咖啡馆,该消息一经传出就引来众多关注。据报道,邮局咖啡还计划将业务版图拓展至“北上广深”等更多城市。

通过天眼查APP获悉,该公司全称为“上海中域咖烨管理咨询有限公司”,于2021年9月才新成立,法人代表为张天盛。

pic_989db436.png

公司总部位于上海陆家嘴,经营业务涵盖企业管理咨询、信息咨询服务、食品销售等。是一家关于咖啡品牌运营和咖啡连锁经营的管理咨询公司。

在天眼查APP上也可以看到,该公司在2021年9月、10月还申请注册邮局咖啡相关商标,包含“邮局咖啡”“COFFEE POSTE”等商标,涉国际分类食品等,当前商标状态为“商标无效”。2021年11月22日,该公司厦门分公司成立。

pic_18d92fa6.png

据介绍,中国邮政目前在全国拥有邮政快递营业网点32万个,按照14亿人计算,意味着每4500人就有一个服务网点,网点密度居世界之首。

2021年,中国邮政支持的网购零售额已经超过8万亿元,农村网点覆盖3万多个乡镇。依托无人能及网点规模和物流能力,加上高比例的自有物业优势,一旦邮局咖啡运营模式成熟,中国邮政可以快速地将邮局咖啡复制到全国各地。

对于这样的咖啡馆,你愿意体验吗?

来源丨胡萝卜周
https://mp.weixin.qq.com/s/MfLMNq4rnedgOGDxKG3s9g

收起阅读 »

Google 大佬们为什么要开发 Go 这门新语言?

Go
大家平时都是在用 Go 语言,那以往已经有了 C、C++、Java、PHP。Google 的大佬们为什么还要再开发一门新的语言呢?难不成是造轮子,其他语言不香吗?背景Go 编程语言构思于 2007 年底,构思的目的是:为了解决在 Google 开发软件基础设施...
继续阅读 »

大家平时都是在用 Go 语言,那以往已经有了 C、C++、Java、PHP。Google 的大佬们为什么还要再开发一门新的语言呢?

难不成是造轮子,其他语言不香吗?

背景

Go 编程语言构思于 2007 年底,构思的目的是:为了解决在 Google 开发软件基础设施时遇到的一些问题。


图上三位是 Go 语言最初的设计者,功力都非常的深厚,按序从左起分别是:

  • Robert Griesemer:参与过 Google V8 JavaScript 引擎和 Java HotSpot 虚拟机的研发。

  • Rob Pike:Unix 操作系统早期开发者之一,UTF-8 创始人之一,Go 语言吉祥物设计者是 Rob Pike 的媳妇。

  • Ken Thompson:图灵奖得主,Unix 操作系统早期开发者之一,UTF-8 创始人之一,C 语言(前身 B 语言)的设计者。

遇到的问题

曾经在早期的采访中,Google 大佬们反馈感觉 "编程" 太麻烦了,他们很不喜欢 C++,对于现在工作所用的语言和环境感觉比较沮丧,充满着许多不怎么好用的特性。

具体遭遇到的问题。如下:

  • 软件复杂:多核处理器、网络系统、大规模计算集群和网络编程模型所带来的问题只能暂时绕开,没法正面解决。

  • 软件规模:软件规模也发生了变化,今天的服务器程序由数千万行代码组成,由数百甚至数千名程序员进行工作,而且每天都在更新(据闻 Go 就是在等编译的 45 分钟中想出来的)。

  • 编译耗时:在大型编译集群中,构建时间也延长到了几分钟,甚至几小时。

设计目的

为了实现上述目标,在既有语言上改造的话,需要解决许多根本性的问题,因此需要一种新的语言。

这门新语言需要符合以下需求:

  • 目的:设计和开发 Go 是为了使在这种环境下能够提高工作效率

  • 设计:在 Go 的设计上,除了比较知名的方面:如内置并发和垃圾收集。还考虑到:严格的依赖性管理,随着系统的发展,软件架构的适应性,以及跨越组件之间边界的健壮性。

这门新语言就是现在的 Go。

Go 在 Google

Go 是 Google 设计的一种编程语言,用于帮助解决谷歌的问题,而 Google 的问题很大。

Google 整体的应用软件很庞大,硬件也很庞大,有数百万行的软件,服务器主要是 C++ 语言,其他部分则是大量的 Java 和 Python。

数以千计的工程师在代码上工作,在一个由所有软件组成的单一树的 "头 " 上工作,所以每天都会对该树的所有层次进行重大改变。

一个大型的定制设计的分布式构建系统使得这种规模的开发是可行的,但它仍然很大。

当然,所有这些软件都在几十亿台机器上运行,这些机器被视为数量不多的独立、联网的计算集群。


简而言之,Google 的开发规模很大,速度可能是缓慢的,而且往往是笨拙的。但它是有效的。

Go 项目的目标是:消除 Google 软件开发的缓慢和笨拙,从而使这个过程更富有成效和可扩展。这门语言是由编写、阅读、调试和维护大型软件系统的人设计的,也是为他们设计的

因此 Go 的目的不是为了研究编程语言的设计,而是为了改善其设计者及其同事的工作环境。

Go 更多的是关于软件工程而不是编程语言研究。或者换个说法,它是为软件工程服务的语言设计。

痛点

当 Go 发布时,有些人声称它缺少被认为是现代语言的必要条件的特定功能或方法。在缺乏这些设施的情况下,Go怎么可能有价值?

我们的答案是:Go 所拥有的特性可以解决那些使大规模软件开发变得困难的问题。

这些问题包括:

  • 构建速度缓慢。

  • 不受控制的依赖关系。

  • 每个程序员使用不同的语言子集。

  • 对程序的理解不透彻(代码可读性差,文档不全等)。

  • 工作的重复性。

  • 更新的成本。

  • 版本偏移(version skew)。

  • 编写自动工具的难度。

  • 跨语言的构建。

纯粹一门语言的单个功能并不能解决这些问题,我们需要对软件工程有一个更大的看法。因此在 Go 的设计中,我们试图把重点放在这些问题的解决方案上。

总结

软件工程指导了 Go 的设计。

与大多数通用编程语言相比,Go 的设计是为了解决我们在构建大型服务器软件时接触到的一系列软件工程问题。这可能会使 Go 听起来相当沉闷和工业化。

但事实上,整个设计过程中对清晰、简单和可组合性的关注反而导致了一种高效、有趣的语言,许多程序员发现它的表现力和力量。

为此产生的 Go 特性包括:

  • 清晰的依赖关系。

  • 清晰的语法。

  • 清晰的语义。

  • 相对于继承的组合。

  • 编程模型提供的简单性(垃圾收集、并发)。

  • 简单的工具(Go工具、gofmt、godoc、gofix)。

这就是为什么要开发 Go 的由来,以及为什么会产生如此的设计和特性的原因。

你学会了吗?:)

若有任何疑问欢迎评论区反馈和交流,最好的关系是互相成就,各位的点赞就是煎鱼创作的最大动力,感谢支持。

文章持续更新,可以微信搜【脑子进煎鱼了】阅读,本文 GitHub github.com/eddycjy/blo… 已收录,学习 Go 语言可以看 Go 学习地图和路线,欢迎 Star 催更。

参考


作者:煎鱼eddycjy
来源:https://juejin.cn/post/7054028466060001288

收起阅读 »

17 张程序员壁纸(使用频率很高)

喜欢哪一个?欢迎评论区投票!1、三思后再写代码!!!2、从世界上搜索喜欢你的人!!!3、代码没写完,哪里有脸睡觉!!!4、程序员的 Home 键!!!5、编程是一门艺术!!!6、云 ~~~~ 雨!!!7、程序人生!!!8、只有极客才懂!!!9、黑客的世界!!!...
继续阅读 »
喜欢哪一个?欢迎评论区投票!

1、三思后再写代码!!!

pic_4498ed76.png

2、从世界上搜索喜欢你的人!!!

pic_873047c5.png

3、代码没写完,哪里有脸睡觉!!!

pic_7f78edf2.png

4、程序员的 Home 键!!!

pic_6ed79fcd.png

5、编程是一门艺术!!!

pic_f6431596.png

6、云 ~~~~ 雨!!!

pic_f563f6a9.png

7、程序人生!!!

pic_2b05ebc4.png

8、只有极客才懂!!!

pic_685bd2a2.png

9、黑客的世界!!!

pic_4e0cd921.png

10、黑~~~人!!!

pic_0dfdbdcd.png

11、PHP 专属!!!

pic_ada48829.png

12、程序 ~ 代码!!!

pic_5605272b.png

13、我就是一个极客!!!

pic_3d0526f8.png

14、CODE!!!

pic_78af3b72.png

15、源代码!!!

pic_1de85de0.png

16、CODE PARTICLE!!!

pic_4f88f6bf.png

17、一个While 引发的人生故事!!!

pic_195f5f45.png

来源:https://mp.weixin.qq.com/s/4b4GSnBoEcqE9Zn5GcVZHQ

收起阅读 »

写了个自动批改小孩作业的代码(下)

接:写了个自动批改小孩作业的代码(上)2.4 切割图像上帝说要有光,就有了光。于是,当光投过来时,物体的背后就有了影。我们就知道了,有影的地方就有东西,没影的地方是空白。这就是投影。这个简单的道理放在图像切割上也很实用。我们把文字的像素做个投影,这样我们就知道...
继续阅读 »

接:写了个自动批改小孩作业的代码(上)

2.4 切割图像

上帝说要有光,就有了光。

于是,当光投过来时,物体的背后就有了影。

我们就知道了,有影的地方就有东西,没影的地方是空白。


这就是投影。

这个简单的道理放在图像切割上也很实用。

我们把文字的像素做个投影,这样我们就知道某个区间有没有文字,并且知道这个区间文字是否集中。

下面是示意图:


2.4.1 投影大法

最有效的方法,往往都是用循环实现的。

要计算投影,就得一个像素一个像素地数,查看有几个像素,然后记录下这一行有N个像素点。如此循环。


首先导入包:

import numpy as np
import cv2
from PIL import Image, ImageDraw, ImageFont
import PIL
import matplotlib.pyplot as plt
import os
import shutil
from numpy.core.records import array
from numpy.core.shape_base import block
import time

比如说要看垂直方向的投影,代码如下:

# 整幅图片的Y轴投影,传入图片数组,图片经过二值化并反色
def img_y_shadow(img_b):
  ### 计算投影 ###
  (h,w)=img_b.shape
  # 初始化一个跟图像高一样长度的数组,用于记录每一行的黑点个数
  a=[0 for z in range(0,h)]
  # 遍历每一列,记录下这一列包含多少有效像素点
  for i in range(0,h):          
      for j in range(0,w):      
          if img_b[i,j]==255:    
              a[i]+=1  
  return a

最终得到是这样的结构:[0, 79, 67, 50, 50, 50, 109, 137, 145, 136, 125, 117, 123, 124, 134, 71, 62, 68, 104, 102, 83, 14, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, ……38, 44, 56, 106, 97, 83, 0, 0, 0, 0, 0, 0, 0]表示第几行总共有多少个像素点,第1行是0,表示是空白的白纸,第2行有79个像素点。

如果我们想要从视觉呈现出来怎么处理呢?那可以把它立起来拉直画出来。


# 展示图片
def img_show_array(a):
  plt.imshow(a)
  plt.show()
   
# 展示投影图, 输入参数arr是图片的二维数组,direction是x,y轴
def show_shadow(arr, direction = 'x'):

  a_max = max(arr)
  if direction == 'x': # x轴方向的投影
      a_shadow = np.zeros((a_max, len(arr)), dtype=int)
      for i in range(0,len(arr)):
          if arr[i] == 0:
              continue
          for j in range(0, arr[i]):
              a_shadow[j][i] = 255
  elif direction == 'y': # y轴方向的投影
      a_shadow = np.zeros((len(arr),a_max), dtype=int)
      for i in range(0,len(arr)):
          if arr[i] == 0:
              continue
          for j in range(0, arr[i]):
              a_shadow[i][j] = 255

  img_show_array(a_shadow)

我们来试验一下效果:

我们将上面的原图片命名为question.jpg放到代码同级目录。

# 读入图片
img_path = 'question.jpg'
img=cv2.imread(img_path,0)
thresh = 200
# 二值化并且反色
ret,img_b=cv2.threshold(img,thresh,255,cv2.THRESH_BINARY_INV)

二值化并反色后的变化如下所示:


上面的操作很有作用,通过二值化,过滤掉杂色,通过反色将黑白对调,原来白纸区域都是255,现在黑色都是0,更利于计算。

计算投影并展示的代码:

img_y_shadow_a = img_y_shadow(img_b)
show_shadow(img_y_shadow_a, 'y') # 如果要显示投影

下面的图是上面图在Y轴上的投影


从视觉上看,基本上能区分出来哪一行是哪一行。

2.4.2 根据投影找区域

最有效的方法,往往还得用循环来实现。

上面投影那张图,你如何计算哪里到哪里是一行,虽然肉眼可见,但是计算机需要规则和算法。

# 图片获取文字块,传入投影列表,返回标记的数组区域坐标[[左,上,右,下]]
def img2rows(a,w,h):
   
  ### 根据投影切分图块 ###
  inLine = False # 是否已经开始切分
  start = 0 # 某次切分的起始索引
  mark_boxs = []
  for i in range(0,len(a)):        
      if inLine == False and a[i] > 10:
          inLine = True
          start = i
      # 记录这次选中的区域[左,上,右,下],上下就是图片,左右是start到当前
      elif i-start >5 and a[i] < 10 and inLine:
          inLine = False
          if i-start > 10:
              top = max(start-1, 0)
              bottom = min(h, i+1)
              box = [0, top, w, bottom]
              mark_boxs.append(box)
               
  return mark_boxs

通过投影,计算哪些区域在一定范围内是连续的,如果连续了很长时间,我们就认为是同一区域,如果断开了很长一段时间,我们就认为是另一个区域。


通过这项操作,我们就可以获得Y轴上某一行的上下两个边界点的坐标,再结合图片宽度,其实我们也就知道了一行图片的四个顶点的坐标了mark_boxs存下的是[坐,上,右,下]。


如果调用如下代码:

(img_h,img_w)=img.shape
row_mark_boxs = img2rows(img_y_shadow_a,img_w,img_h)
print(row_mark_boxs)

我们获取到的是所有识别出来每行图片的坐标,格式是这样的:[[0, 26, 596, 52], [0, 76, 596, 103], [0, 130, 596, 155], [0, 178, 596, 207], [0, 233, 596, 259], [0, 282, 596, 311], [0, 335, 596, 363], [0, 390, 596, 415]]

2.4.3 根据区域切图片

最有效的方法,最终也得用循环来实现。这也是计算机体现它强大的地方。

# 裁剪图片,img 图片数组, mark_boxs 区域标记
def cut_img(img, mark_boxs):

img_items = [] # 存放裁剪好的图片
for i in range(0,len(mark_boxs)):
img_org = img.copy()
box = mark_boxs[i]
# 裁剪图片
img_item = img_org[box[1]:box[3], box[0]:box[2]]
img_items.append(img_item)
return img_items

这一步骤是拿着方框,从大图上用小刀划下小图,核心代码是img_org[box[1]:box[3], box[0]:box[2]]图片裁剪,参数是数组的[上:下,左:右],获取的数据还是二维的数组。

如果保存下来:

# 保存图片
def save_imgs(dir_name, imgs):

  if os.path.exists(dir_name):
      shutil.rmtree(dir_name)
  if not os.path.exists(dir_name):    
      os.makedirs(dir_name)

  img_paths = []
  for i in range(0,len(imgs)):
      file_path = dir_name+'/part_'+str(i)+'.jpg'
      cv2.imwrite(file_path,imgs[i])
      img_paths.append(file_path)
   
  return img_paths

# 切图并保存
row_imgs = cut_img(img, row_mark_boxs)
imgs = save_imgs('rows', row_imgs) # 如果要保存切图
print(imgs)

图片是下面这样的:


2.4.4 循环可去油腻

还是循环。横着行我们掌握了,那么针对每一行图片,我们竖着切成三块是不是也会了,一个道理。


需要注意的是,横竖是稍微有区别的,下面是上图的x轴投影。


横着的时候,字与字之间本来就是有空隙的,然后块与块也有空隙,这个空隙的度需要掌握好,以便更好地区分出来是字的间距还是算式块的间距。

幸好,有种方法叫膨胀。

膨胀对人来说不积极,但是对于技术来说,不管是膨胀(dilate),还是腐蚀(erode),只要能达到目的,都是好的。

kernel=np.ones((3,3),np.uint8)  # 膨胀核大小
row_img_b=cv2.dilate(img_b,kernel,iterations=6) # 图像膨胀6次

膨胀之后再投影,就很好地区分出了块。


根据投影裁剪之后如下图所示:


同理,不膨胀可截取单个字符。


这样,这是一块区域的字符。

一行的,一页的,通过循环,都可以截取出来。

有了图片,就可以识别了。有了位置,就可以判断识别结果的关系了。

下面提供一些代码,这些代码不全,有些函数你可能找不到,但是思路可以参考,详细的代码可以去我的github去看。

def divImg(img_path, save_file = False):

  img_o=cv2.imread(img_path,1)
  # 读入图片
  img=cv2.imread(img_path,0)
  (img_h,img_w)=img.shape
  thresh = 200
  # 二值化整个图,用于分行
  ret,img_b=cv2.threshold(img,thresh,255,cv2.THRESH_BINARY_INV)

  # 计算投影,并截取整个图片的行
  img_y_shadow_a = img_y_shadow(img_b)
  row_mark_boxs = img2rows(img_y_shadow_a,img_w,img_h)
  # 切行的图片,切的是原图
  row_imgs = cut_img(img, row_mark_boxs)
  all_mark_boxs = []
  all_char_imgs = []
  # ===============从行切块======================
  for i in range(0,len(row_imgs)):
      row_img = row_imgs[i]
      (row_img_h,row_img_w)=row_img.shape
      # 二值化一行的图,用于切块
      ret,row_img_b=cv2.threshold(row_img,thresh,255,cv2.THRESH_BINARY_INV)
      kernel=np.ones((3,3),np.uint8)
      #图像膨胀6次
      row_img_b_d=cv2.dilate(row_img_b,kernel,iterations=6)
      img_x_shadow_a = img_x_shadow(row_img_b_d)
      block_mark_boxs = row2blocks(img_x_shadow_a, row_img_w, row_img_h)
      row_char_boxs = []
      row_char_imgs = []
      # 切块的图,切的是原图
      block_imgs = cut_img(row_img, block_mark_boxs)
      if save_file:
          b_imgs = save_imgs('cuts/row_'+str(i), block_imgs) # 如果要保存切图
          print(b_imgs)
      # =============从块切字====================
      for j in range(0,len(block_imgs)):
          block_img = block_imgs[j]
          (block_img_h,block_img_w)=block_img.shape
          # 二值化块,因为要切字符图片了
          ret,block_img_b=cv2.threshold(block_img,thresh,255,cv2.THRESH_BINARY_INV)
          block_img_x_shadow_a = img_x_shadow(block_img_b)
          row_top = row_mark_boxs[i][1]
          block_left = block_mark_boxs[j][0]
          char_mark_boxs,abs_char_mark_boxs = block2chars(block_img_x_shadow_a, block_img_w, block_img_h,row_top,block_left)
          row_char_boxs.append(abs_char_mark_boxs)
          # 切的是二值化的图
          char_imgs = cut_img(block_img_b, char_mark_boxs, True)
          row_char_imgs.append(char_imgs)
          if save_file:
              c_imgs = save_imgs('cuts/row_'+str(i)+'/blocks_'+str(j), char_imgs) # 如果要保存切图
              print(c_imgs)
      all_mark_boxs.append(row_char_boxs)
      all_char_imgs.append(row_char_imgs)


  return all_mark_boxs,all_char_imgs,img_o

最后返回的值是3个,all_mark_boxs是标记的字符位置的坐标集合。[左,上,右,下]是指某个字符在一张大图里的坐标,打印一下是这样的:

[[[[19, 26, 34, 53], [36, 26, 53, 53], [54, 26, 65, 53], [66, 26, 82, 53], [84, 26, 101, 53], [102, 26, 120, 53], [120, 26, 139, 53]], [[213, 26, 229, 53], [231, 26, 248, 53], [249, 26, 268, 53], [268, 26, 285, 53]], [[408, 26, 426, 53], [427, 26, 437, 53], [438, 26, 456, 53], [456, 26, 474, 53], [475, 26, 492, 53]]], [[[20, 76, 36, 102], [38, 76, 48, 102], [50, 76, 66, 102], [67, 76, 85, 102], [85, 76, 104, 102]], [[214, 76, 233, 102], [233, 76, 250, 102], [252, 76, 268, 102], [270, 76, 287, 102]], [[411, 76, 426, 102], [428, 76, 445, 102], [446, 76, 457, 102], [458, 76, 474, 102], [476, 76, 493, 102], [495, 76, 511, 102]]]]

它是有结构的。它的结构是:


all_char_imgs这个返回值,里面是上面坐标结构对应位置的图片。img_o就是原图了。

2.5 识别

循环,循环,还是TM循环!

对于识别,2.3 预测数据已经讲过了,那次是对于2张独立图片的识别,现在我们要对整张大图切分后的小图集合进行识别,这就又用到了循环。

翠花,上代码!

all_mark_boxs,all_char_imgs,img_o = divImg(path,save)
model = cnn.create_model()
model.load_weights('checkpoint/char_checkpoint')
class_name = np.load('class_name.npy')

# 遍历行
for i in range(0,len(all_char_imgs)):
  row_imgs = all_char_imgs[i]
  # 遍历块
  for j in range(0,len(row_imgs)):
      block_imgs = row_imgs[j]
      block_imgs = np.array(block_imgs)
      results = cnn.predict(model, block_imgs, class_name)
      print('recognize result:',results)

上面代码做的就是以块为单位,传递给神经网络进行预测,然后返回识别结果。

针对这张图,我们来进行裁剪和识别。


看底部的最后一行

recognize result: ['1', '0', '12', '2', '10']
recognize result: ['8', '12', '6', '10']
recognize result: ['1', '0', '12', '7', '10']

结果是索引,不是真实的字符,我们根据字典10: '=', 11: '+', 12: '-', 13: '×', 14: '÷'转换过来之后结果是:

recognize result: ['1', '0', '-', '2', '=']
recognize result: ['8', '-', '6', '=']
recognize result: ['1', '0', '-', '7', '=']

和图片是对应的:


2.6 计算并反馈

循环……

我们获取到了10-2=、8-6=2,也获取到了他们在原图的位置坐标[左,上,右,下],那么怎么把结果反馈到原图上呢?

往往到这里就剩最后一步了。

再来温习一遍需求:作对了,能打对号;做错了,能打叉号;没做的,能补上答案。

实现分两步走:计算(是作对做错还是没错)和反馈(把预期结果写到原图上)。

2.6.1 计算 python有个函数很强大,就是eval函数,能计算字符串算式,比如直接计算eval("5+3-2")。

所以,一切都靠它了。

# 计算数值并返回结果  参数chars:['8', '-', '6', '=']
def calculation(chars):
  cstr = ''.join(chars)
  result = ''
  if("=" in cstr): # 有等号
      str_arr = cstr.split('=')
      c_str = str_arr[0]
      r_str = str_arr[1]
      c_str = c_str.replace("×","*")
      c_str = c_str.replace("÷","/")
      try:
          c_r = int(eval(c_str))
      except Exception as e:
          print("Exception",e)

      if r_str == "":
          result = c_r
      else:
          if str(c_r) == str(r_str):
              result = "√"
          else:
              result = "×"

  return result

执行之后获得的结果是:

recognize result: ['8', '×', '4', '=']
calculate result: 32
recognize result: ['2', '-', '1', '=', '1']
calculate result: √
recognize result: ['1', '0', '-', '5', '=']
calculate result: 5

2.6.2 反馈

有了结果之后,把结果写到图片上,这是最后一步,也是最简单的一步。

但是实现起来,居然很繁琐。

得找坐标吧,得计算结果呈现的位置吧,我们还想标记不同的颜色,比如对了是绿色,错了是红色,补齐答案是灰色。

下面代码是在一个图img上,把文本内容text画到(left,top)位置,以特定颜色和大小。

# 绘制文本
def cv2ImgAddText(img, text, left, top, textColor=(255, 0, 0), textSize=20):
  if (isinstance(img, np.ndarray)): # 判断是否OpenCV图片类型
      img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
  # 创建一个可以在给定图像上绘图的对象
  draw = ImageDraw.Draw(img)
  # 字体的格式
  fontStyle = ImageFont.truetype("fonts/fangzheng_shusong.ttf", textSize, encoding="utf-8")
  # 绘制文本
  draw.text((left, top), text, textColor, font=fontStyle)
  # 转换回OpenCV格式
  return cv2.cvtColor(np.asarray(img), cv2.COLOR_RGB2BGR)

结合着切图的信息、计算的信息,下面代码提供思路参考:

# 获取切图标注,切图图片,原图图图片
all_mark_boxs,all_char_imgs,img_o = divImg(path,save)
# 恢复模型,用于图片识别
model = cnn.create_model()
model.load_weights('checkpoint/char_checkpoint')
class_name = np.load('class_name.npy')

# 遍历行
for i in range(0,len(all_char_imgs)):
  row_imgs = all_char_imgs[i]
  # 遍历块
  for j in range(0,len(row_imgs)):
      block_imgs = row_imgs[j]
      block_imgs = np.array(block_imgs)
      # 图片识别
      results = cnn.predict(model, block_imgs, class_name)
      print('recognize result:',results)
      # 计算结果
      result = calculation(results)
      print('calculate result:',result)
      # 获取块的标注坐标
      block_mark = all_mark_boxs[i][j]
      # 获取结果的坐标,写在块的最后一个字
      answer_box = block_mark[-1]
      # 计算最后一个字的位置
      x = answer_box[2]
      y = answer_box[3]
      iw = answer_box[2] - answer_box[0]
      ih = answer_box[3] - answer_box[1]
      # 计算字体大小
      textSize = max(iw,ih)
      # 根据结果设置字体颜色
      if str(result) == "√":
          color = (0, 255, 0)
      elif str(result) == "×":
          color = (255, 0, 0)
      else:
          color = (192, 192,192)
      # 将结果写到原图上
      img_o = cv2ImgAddText(img_o, str(result), answer_box[2], answer_box[1],color, textSize)
# 将写满结果的原图保存
cv2.imwrite('result.jpg', img_o)

结果是下面这样的:


注意

  1. 同级新建fonts文件夹里拷贝一些字体文件,从这里找C:\Windows\Fonts,几十个就行。

  2. get_character_pic.py 生成字体

  3. cnn.py 训练数据

  4. main.py 裁剪指定图片并识别,素材图片新建imgs文件夹,在imgs/question.png下,结果文件保存在imgs/result.png。

  5. 注意如果识别不成功,很可能是question.png的字体你没有训练(这幅图的字体是方正书宋简体,但是你只训练了楷体),这时候可以使用楷体自己编一个算式图。

原文:https://juejin.cn/post/7006732549451939847

收起阅读 »

写了个自动批改小孩作业的代码(上)

最近一些软件的搜题、智能批改类的功能要下线。昨晚我做了一个梦,梦见我实现了这个功能,如下图所示:功能简介:作对了,能打对号;做错了,能打叉号;没做的,能补上答案。二、实现步骤其实,搞定两点就成,第一是能识别数字,第二是能切分数字。前者是图像识别,后者是图像切割...
继续阅读 »

一、亮出效果

最近一些软件的搜题、智能批改类的功能要下线。

退1024步讲,要不要自己做一个自动批改的功能啊?万一哪天孩子要用呢!

昨晚我做了一个梦,梦见我实现了这个功能,如下图所示:


功能简介:作对了,能打对号;做错了,能打叉号;没做的,能补上答案。

醒来后,我环顾四周,赶紧再躺下,希望梦还能接上。

二、实现步骤

基本思路

其实,搞定两点就成,第一是能识别数字,第二是能切分数字。

首先得能认识5是5,这是前提条件,其次是能找到5、6、7、8这些数字区域的位置。

前者是图像识别,后者是图像切割

  • 对于图像识别,一般的套路是下面这样的(CNN卷积神经网络):


  • 对于图像切割,一般的套路是下面的这样(横向纵向投影法):


既然思路能走得通,那么咱们先搞图像识别。准备数据->训练数据并保存模型->使用训练模型预测结果

2.1 准备数据

对于男友,找一个油嘴滑舌的花花公子,不如找一个闷葫芦IT男,亲手把他培养成你期望的样子。

咱们不用什么官方的mnist数据集,因为那是官方的,不是你的,你想要添加±×÷它也没有。

有些通用的数据集,虽然很强大,很方便,但是一旦放到你的场景中,效果一点也不如你的愿。

只有训练自己手里的数据,然后自己用起来才顺手。更重要的是,我们享受创造的过程。

假设,我们只给口算做识别,那么我们需要的图片数据有如下几类:

索引:0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
字符:0 1 2 3 4 5 6 7 8 9 = + - × ÷

如果能识别这些,基本上能满足整数的加减乘除运算了。

好了,图片哪里来?!

是啊,图片哪里来?

吓得我差点从梦里醒来,500万都规划好该怎么花了,居然双色球还没有选号!

梦里,一个老者跟我说,图片要自己生成。我问他如何生成,他呵呵一笑,消失在迷雾中……

仔细一想,其实也不难,打字我们总会吧,生成数字无非就是用代码把字写在图片上。

字之所以能展示,主要是因为有字体的支撑。

如果你用的是windows系统,那么打开KaTeX parse error: Undefined control sequence: \Windows at position 3: C:\̲W̲i̲n̲d̲o̲w̲s̲\Fonts这个文件夹,你会发现好多字体。


我们写代码调用这些字体,然后把它打印到一张图片上,是不是就有数据了。

而且这些数据完全是由我们控制的,想多就多,想少就少,想数字、字母、汉字、符号都可以,今天你搞出来数字识别,也就相当于你同时拥有了所有识别!想想还有点小激动呢!

看看,这就是打工和创业的区别。你用别人的数据相当于打工,你是不用操心,但是他给你什么你才有什么。自己造数据就相当于创业,虽然前期辛苦,你可以完全自己把握节奏,需要就加上,没用就去掉。

2.1.1 准备字体

建一个fonts文件夹,从字体库里拷一部分字体放进来,我这里是拷贝了13种字体文件。


好的,准备工作做好了,肯定很累吧,休息休息休息,一会儿再搞!

2.1.2 生成图片

代码如下,可以直接运行。

from __future__ import print_function
from PIL import Image
from PIL import ImageFont
from PIL import ImageDraw
import os
import shutil
import time

# %% 要生成的文本
label_dict = {0: '0', 1: '1', 2: '2', 3: '3', 4: '4', 5: '5', 6: '6', 7: '7', 8: '8', 9: '9', 10: '=', 11: '+', 12: '-', 13: '×', 14: '÷'}

# 文本对应的文件夹,给每一个分类建一个文件
for value,char in label_dict.items():
  train_images_dir = "dataset"+"/"+str(value)
  if os.path.isdir(train_images_dir):
      shutil.rmtree(train_images_dir)
  os.makedirs(train_images_dir)

# %% 生成图片
def makeImage(label_dict, font_path, width=24, height=24, rotate = 0):

  # 从字典中取出键值对
  for value,char in label_dict.items():
      # 创建一个黑色背景的图片,大小是24*24
      img = Image.new("RGB", (width, height), "black"
      draw = ImageDraw.Draw(img)
      # 加载一种字体,字体大小是图片宽度的90%
      font = ImageFont.truetype(font_path, int(width*0.9))
      # 获取字体的宽高
      font_width, font_height = draw.textsize(char, font)
      # 计算字体绘制的x,y坐标,主要是让文字画在图标中心
      x = (width - font_width-font.getoffset(char)[0]) / 2
      y = (height - font_height-font.getoffset(char)[1]) / 2
      # 绘制图片,在那里画,画啥,什么颜色,什么字体
      draw.text((x,y), char, (255, 255, 255), font)
      # 设置图片倾斜角度
      img = img.rotate(rotate)
      # 命名文件保存,命名规则:dataset/编号/img-编号_r-选择角度_时间戳.png
      time_value = int(round(time.time() * 1000))
      img_path = "dataset/{}/img-{}_r-{}_{}.png".format(value,value,rotate,time_value)
      img.save(img_path)
       
# %% 存放字体的路径
font_dir = "./fonts"
for font_name in os.listdir(font_dir):
  # 把每种字体都取出来,每种字体都生成一批图片
  path_font_file = os.path.join(font_dir, font_name)
  # 倾斜角度从-1010度,每个角度都生成一批图片
  for k in range(-10, 10, 1): 
      # 每个字符都生成图片
      makeImage(label_dict, path_font_file, rotate = k)

上面纯代码不到30行,相信大家应该能看懂!看不懂不是我的读者。

核心代码就是画文字。

draw.text((x,y), char, (255, 255, 255), font)

翻译一下就是:使用某字体在黑底图片的(x,y)位置写白色的char符号。

核心逻辑就是三层循环。


如果代码你运行的没有问题,最终会生成如下结果:



好了,数据准备好了。总共15个文件夹,每个文件夹下对应的各种字体各种倾斜角的字符图片3900个(字符15类×字体13种×角度20个),图片的大小是24×24像素。

有了数据,我们就可以再进行下一步了,下一步是训练使用数据。

2.2 训练数据

2.2.1 构建模型

你先看代码,外行感觉好深奥,内行偷偷地笑。

# %% 导入必要的包 
import tensorflow as tf
import numpy as np
from tensorflow.keras import layers
from tensorflow.keras.models import Sequential
import pathlib
import cv2

# %% 构建模型
def create_model():
  model = Sequential([
      layers.experimental.preprocessing.Rescaling(1./255, input_shape=(24, 24, 1)),
      layers.Conv2D(24,3,activation='relu'),
      layers.MaxPooling2D((2,2)),
      layers.Conv2D(64,3, activation='relu'),
      layers.MaxPooling2D((2,2)),
      layers.Flatten(),
      layers.Dense(128, activation='relu'),
      layers.Dense(15)]
  )
   
  model.compile(optimizer='adam',
              loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
              metrics=['accuracy'])

  return model

这个模型的序列是下面这样的,作用是输入一个图片数据,经过各个层揉搓,最终预测出这个图片属于哪个分类。


这么多层都是干什么的,有什么用?和衣服一样,肯定是有用的,内衣、衬衣、毛衣、棉衣各有各的用处。

2.2.2 卷积层 Conv2D

各个职能部门的调查员,搜集和整理某单位区域内的特定数据。我们输入的是一个图像,它是由像素组成的,这就是R e s c a l i n g ( 1. / 255 , i n p u t s h a p e = ( 24 , 24 , 1 ) ) Rescaling(1./255, input_shape=(24, 24, 1))Rescaling(1./255,input shape=(24,24,1))中,input_shape输入形状是24*24像素1个通道(彩色是RGB 3个通道)的图像。


卷积层代码中的定义是Conv2D(24,3),意思是用3*3像素的卷积核,去提取24个特征。

我把图转到地图上来,你就能理解了。以我大济南的市中区为例子。


卷积的作用就相当于从地图的某级单位区域中收集多组特定信息。比如以小区为单位去提取住宅数量、车位数量、学校数量、人口数、年收入、学历、年龄等等24个维度的信息。小区相当于卷积核。

提取完成之后是这样的。


第一次卷积之后,我们从市中区得到N个小区的数据。

卷积是可以进行多次的。

比如在小区卷积之后,我们还可在小区的基础上再来一次卷积,在卷积就是街道了。


通过再次以街道为单位卷积小区,我们就从市中区得到了N个街道的数据。

这就是卷积的作用。

通过一次次卷积,就把一张大图,通过特定的方法卷起来,最终留下来的是固定几组有目的数据,以此方便后续的评选决策。这是评选一个区的数据,要是评选济南市,甚至山东省,也是这么卷积。这和现实生活中评选文明城市、经济强省也是一个道理。

2.2.3 池化层 MaxPooling2D

说白了就是四舍五入。

计算机的计算能力是强大的,比你我快,但也不是不用考虑成本。我们当然希望它越快越好,如果一个方法能省一半的时间,我们肯定愿意用这种方法。

池化层干的就是这个事情。池化的代码定义是这样的M a x P o o l i n g 2 D ( ( 2 , 2 ) ) MaxPooling2D((2,2))MaxPooling2D((2,2)),这里是最大值池化。其中(2,2)是池化层的大小,其实就是在2*2的区域内,我们认为这一片可以合成一个单位。

再以地图举个例子,比如下面的16个格子里的数据,是16个街道的学校数量。


为了进一步提高计算效率,少计算一些数据,我们用2*2的池化层进行池化。


池化的方格是4个街道合成1个,新单位学校数量取成员中学校数量最大(也有取最小,取平均多种池化)的那一个。池化之后,16个格子就变为了4个格子,从而减少了数据。

这就是池化层的作用。

2.2.4 全连接层 Dense

弱水三千,只取一瓢。

在这里,它其实是一个分类器。

我们构建它时,代码是这样的D e n s e ( 15 ) Dense(15)Dense(15)。

它所做的事情,不管你前面是怎么样,有多少维度,到我这里我要强行转化为固定的通道。

比如识别字母a~z,我有500个神经元参与判断,但是最终输出结果就是26个通道(a,b,c,……,y,z)。

我们这里总共有15类字符,所以是15个通道。给定一个输入后,输出为每个分类的概率。


注意:上面都是二维的输入,比如24×24,但是全连接层是一维的,所以代码中使用了l a y e r s . F l a t t e n ( ) layers.Flatten()layers.Flatten()将二维数据拉平为一维数据([[11,12],[21,22]]->[11,12,21,22])。

对于总体的模型,调用m o d e l . s u m m a r y ( ) model.summary()model.summary()打印序列的网络结构如下:

_________________________________________________________________
Layer (type)                 Output Shape             Param #   
=================================================================
rescaling_2 (Rescaling)     (None, 24, 24, 1)         0         
_________________________________________________________________
conv2d_4 (Conv2D)           (None, 22, 22, 24)       240       
_________________________________________________________________
max_pooling2d_4 (MaxPooling2 (None, 11, 11, 24)       0         
_________________________________________________________________
conv2d_5 (Conv2D)           (None, 9, 9, 64)         13888     
_________________________________________________________________
max_pooling2d_5 (MaxPooling2 (None, 4, 4, 64)         0         
_________________________________________________________________
flatten_2 (Flatten)         (None, 1024)             0         
_________________________________________________________________
dense_4 (Dense)             (None, 128)               131200    
_________________________________________________________________
dense_5 (Dense)             (None, 15)               1935      
=================================================================
Total params: 147,263
Trainable params: 147,263
Non-trainable params: 0
_________________________________________________________________

我们看到conv2d_5 (Conv2D) (None, 9, 9, 64) 经过2*2的池化之后变为max_pooling2d_5 (MaxPooling2 (None, 4, 4, 64)。(None, 4, 4, 64) 再经过F l a t t e n FlattenFlatten拉成一维之后变为(None, 1024),经过全连接变为(None, 128)再一次全连接变为(None, 15),15就是我们的最终分类。这一切都是我们设计的。

m o d e l . c o m p i l e model.compilemodel.compile就是配置模型的几个参数,这个现阶段记住就可以。

2.2.5 训练数据

执行就完了。

# 统计文件夹下的所有图片数量
data_dir = pathlib.Path('dataset')
# 从文件夹下读取图片,生成数据集
train_ds = tf.keras.preprocessing.image_dataset_from_directory(
  data_dir, # 从哪个文件获取数据
  color_mode="grayscale", # 获取数据的颜色为灰度
  image_size=(24, 24), # 图片的大小尺寸
  batch_size=32 # 多少个图片为一个批次
)
# 数据集的分类,对应dataset文件夹下有多少图片分类
class_names = train_ds.class_names
# 保存数据集分类
np.save("class_name.npy", class_names)
# 数据集缓存处理
AUTOTUNE = tf.data.experimental.AUTOTUNE
train_ds = train_ds.cache().shuffle(1000).prefetch(buffer_size=AUTOTUNE)
# 创建模型
model = create_model()
# 训练模型,epochs=10,所有数据集训练10
model.fit(train_ds,epochs=10)
# 保存训练后的权重
model.save_weights('checkpoint/char_checkpoint')

执行之后会输出如下信息:

Found 3900 files belonging to 15 classes. 
Epoch 1/10 122/122 [=========] - 2s 19ms/step - loss: 0.5795 - accuracy: 0.8615 
Epoch 2/10 122/122 [=========] - 2s 18ms/step - loss: 0.0100 - accuracy: 0.9992 
Epoch 3/10 122/122 [=========] - 2s 19ms/step - loss: 0.0027 - accuracy: 1.0000 
Epoch 4/10 122/122 [=========] - 2s 19ms/step - loss: 0.0013 - accuracy: 1.0000 
Epoch 5/10 122/122 [=========] - 2s 20ms/step - loss: 8.4216e-04 - accuracy: 1.0000 
Epoch 6/10 122/122 [=========] - 2s 18ms/step - loss: 5.5273e-04 - accuracy: 1.0000 
Epoch 7/10 122/122 [=========] - 3s 21ms/step - loss: 4.0966e-04 - accuracy: 1.0000 
Epoch 8/10 122/122 [=========] - 2s 20ms/step - loss: 3.0308e-04 - accuracy: 1.0000 
Epoch 9/10 122/122 [=========] - 3s 23ms/step - loss: 2.3446e-04 - accuracy: 1.0000 
Epoch 10/10 122/122 [=========] - 3s 21ms/step - loss: 1.8971e-04 - accuracy: 1.0000

我们看到,第3遍时候,准确率达到100%了。最后结束的时候,我们发现文件夹checkpoint下多了几个文件:

char_checkpoint.data-00000-of-00001
char_checkpoint.index
checkpoint

上面那几个文件是训练结果,训练保存之后就不用动了。后面可以直接用这些数据进行预测。

2.3 预测数据

终于到了享受成果的时候了。

# 设置待识别的图片
img1=cv2.imread('img1.png',0
img2=cv2.imread('img2.png',0
imgs = np.array([img1,img2])
# 构建模型
model = create_model()
# 加载前期训练好的权重
model.load_weights('checkpoint/char_checkpoint')
# 读出图片分类
class_name = np.load('class_name.npy')
# 预测图片,获取预测值
predicts = model.predict(imgs) 
results = [] # 保存结果的数组
for predict in predicts: #遍历每一个预测结果
  index = np.argmax(predict) # 寻找最大值
  result = class_name[index] # 取出字符
  results.append(result)
print(results)

我们找两张图片img1.png,img2.png,一张是数字6,一张是数字8,两张图放到代码同级目录下,验证一下识别效果如何。

图片要通过cv2.imread('img1.png',0) 转化为二维数组结构,0参数是灰度图片。经过处理后,图片转成的数组是如下所示(24,24)的结构:


我们要同时验证两张图,所以把两张图再组成imgs放到一起,imgs的结构是(2,24,24)。

下面是构建模型,然后加载权重。通过调用predicts = model.predict(imgs)将imgs传递给模型进行预测得出predicts。

predicts的结构是(2,15),数值如下面所示:

[[ 16.134243 -12.10675 -1.1994154 -27.766754 -43.4324 -9.633694 -12.214878 1.6287893 2.562174 3.2222707 13.834648 28.254173 -6.102874 16.76582 7.2586184] [ 5.022571 -8.762314 -6.7466817 -23.494259 -30.170597 2.4392672 -14.676962 5.8255725 8.855118 -2.0998626 6.820853 7.6578817 1.5132296 24.4664 2.4192357]]

意思是有2个预测结果,每一个图片的预测结果有15种可能。

然后根据 index = np.argmax(predict) 找出最大可能的索引。

根据索引找到字符的数值结果是[‘6’, ‘8’]。

下面是数据在内存中的监控:


可见,我们的预测是准确的。

下面,我们将要把图片中数字切割出来,进行识别了。

之前我们准备了数据,训练了数据,并且拿图片进行了识别,识别结果正确。

到目前为止,看来问题不大……没有大问题,有问题也大不了。

下面就是把图片进行切割识别了。

下面这张大图片,怎么把它搞一搞,搞成单个小数字的图片。


续:写了个自动批改小孩作业的代码(下)

原文:https://juejin.cn/post/7006732549451939847

收起阅读 »

网传铁饭碗职业排名,公务员仅排第八!

铁饭碗,顾名思义,饭碗乃铁所铸,坚硬非常,难于击破。人们通常将其意延伸,指一个好的单位或部门,工作稳定,收入无忧。今天我们就来看看网传比较火的铁饭碗职业排名,看看有你感兴趣的职业吗?NO.10 事业单位事业单位工作人员的工资构成。基本工资+绩效工资+津贴补贴+...
继续阅读 »
铁饭碗,顾名思义,饭碗乃铁所铸,坚硬非常,难于击破。人们通常将其意延伸,指一个好的单位或部门,工作稳定,收入无忧。


今天我们就来看看网传比较火的铁饭碗职业排名,看看有你感兴趣的职业吗?

NO.10 事业单位


事业单位工作人员的工资构成。基本工资+绩效工资+津贴补贴+其它工资。基本工资包括岗位工资+薪级工资。


事业单位岗位会按照职责和要求分为专业岗位,职员岗位和工勤技能岗位,专业岗位设13个等级,职员岗位设10个等级,工勤技能岗位设5个等级,每个等级对应不同的工资标准,薪级工资分为专业技术人员和管理人员设置65个薪级,工人设置40个薪级,每个薪级对应一个工资标准。


总的来说事业单位的整体待遇水平略低于公务员。具体的就要分类别说了,比如参公的各项待遇就和公务员完全一致,医疗和教育系统的整体工资待遇还要高于公务员。


推荐专业:财会类、经济类、法学类、汉语言文学类、公共管理类等。


NO.9 教师


随着经济的发展,人们早已经解决温饱问题,教师是非常受欢迎的职业。国家多次高层会议明确教师的薪资不得低于当地国家公务员的薪资水平。


而且对于中小学的制度组建完善之中,教师的绩效工资也有了很大的着落,职称等评价也是大大提高了公平。


所以说以后的教师薪资必然会有一个大步的跨越,完全不低于公务员!


今年,教育部最新开展的“优师计划”更是让师范专业火出圈。从2021年起,教育部每年在全国普通本科招生计划中专门安排1万名左右的优秀教师定向培养专项计划,由教育部直属师范大学和地方师范院校承担招生及培养任务,采取在校学习期间免除学费、免缴住宿费并补助生活费的方式,为832个脱贫县和中西部陆地边境县中小学校定向培养优秀教师。


推荐院校:北京师范大学、华东师范大学、华中师范大学、南京师范大学、湖南师范大学、东北师范大学、华南师范大学等。


NO.8 公务员


公务员一直都是社会传统眼中的铁饭碗,这个无需过多的解释。看看每年的报考人数,录取比例就可以窥探。


不过十八大以来,党内法规制度的笼子越扎越紧。随着公务员社保养老金的并轨、公务员津贴资金的规范、公务员阳光工资的实行,各项法律法规规定了公务员不允许兼职,一旦发现就会被开除公职,而且涨点工资全民反对。


公务员职业逐步走下神坛,成为一份普通的职业,其实我们的工资真的不高。


推荐专业:财经类、法律类、中文类、计算机类、新闻传播类、管理类、金融学类、公安类等


NO.7 国有银行


虽然现在银行业不如以前,但依然强于公务员和大多数国企。


中国银行以及中国建设、工商、交通、农业银行成为五大国企银行,薪资待遇是不低于其它外企的,各项福利齐全,而且上班轻松,基本转正后7000左右一月,房补1000左右,比较稳定(各地具体薪资也可能因地域而有增减)



推荐专业:金融学专业、财务会计专业、审计类专业、计算机类专业等。


NO.6 三大运营商


在中国,三大运营商:移动、联通、电信几乎垄断了所有的移动通讯,他们凭借庞大的用户数量,赚取了很多利润。


薪资待遇方面,新人转正税后15-20万。以中国移动为例,上市公司,全国十亿以上用户,保险、公积金、年终奖齐全。


据悉5年以上的员工年终奖不低于1万5,中秋、春节、端午都差不多是1000元的购物卡,每年给员工交医保3000元等。


推荐专业:计算机类、电子信息类、自动化类、电气类、智能科学、通信类、市场营销类、财务类、法学类、管理类等。


NO.5 国家电网


国家电网作为垄断性质的企业,福利在某些情况下比公务员还要高,当然工资肯定是比公务员高,这非常好理解。


因为他们改制之后就是企业,企业完全按照市场化发工资,就不受政府所谓的规定了。



推荐专业:电气类、通信类、计算机类、工科相关专业等。


NO.4 国家烟草


把烟草局放在第4位,那估计很多人也不会反对。这是因为烟草局的隐形福利非常多。


普通的一个正式工作人员,比公务员多好几千那是非常正常的。所以才有了大家眼中认为,要想进入烟草局那就得靠关系靠金钱铺路。




推荐院校:河南农业大学、云南农业大学、安徽农业大学、山东农业大学、郑州轻工业学院、青岛农业大学、贵州大学。


为什么国家烟草不是第一呢?请看后面三位大佬。


NO.3 高校教师


事业单位改革,高校教师编制被打破,变成聘用合同制,对于高校教师来说更多的是福利。


高校教师作为教师队伍中的高薪群体,本身就编制的依赖程度就会小很多。高校教师由国家教育部发文鼓励他们去兼职,去赚钱。


再加上他们的时间非常宽裕,开补习班、开培训班。那一年下来的收入肯定比公务员更多,当然比烟草,电网也会多,这点毋庸置疑。


NO.2 医院医生


医生原来受制于身份限制,很少有人会去外面兼职。尤其是医生,因为一旦到外面医院“走穴”,被发现那可是要开除出医院。我们都知道医生是个技术活,如果没有一定的手术量,你的技术是不可能锻炼出来的。这就是为什么北京协和、四川华西医院、复旦附属中山医院等医院,这些医生都不愿意离开体制内的一个原因。


但是现在身份打破之后,在政策层面就允许医生自己开诊所。试想一下,如果你是技术过硬,那么去做个手术,手术费从几千到几万都是有的。更甚者,你完全可以自己独立出来,开办诊所。一年赚几十万,是不是非常轻松,这样赚钱就更多了。


所以事业单位改革后,但凡有点名气的医生都不一定要被医院束缚,只要他想,年收入高过高校教师和公务员肯定是没有问题的。


这是因为高校的老师的额外收入更多的是在知识转化成果上,而这个成果不会轻而易举就能获得。总体的难度大于医生。所以排在了医生后面。事业单位改革后,就普通的医护人员工资也会大涨。


因为医院作为重点改革对象,为了调动一线医护人员的工作积极性,为了让更多的人加入医护队伍,公立医院工资改革试点已经启动,包括允许医疗卫生机构突破现行事业单位工资调控水平,以及建立调整机制,切实提高医护人员的薪资水平。




推荐专业:临床医学专业、麻醉学专业、精神医学专业、儿科学专业、口腔医学专业、医学影像学专业、眼视光医学专业、中医学专业等。


NO.1 军队文职


这是因为军队文职招考是这一两年才兴起的“编制”工作。而且由于知道的人较少,报考的人也不多,整体的竞争难度就很低。成功上岸的几率就更大。


重要的一点就是军队文职的各项福利保障都要比公务员的好很多。只要考入军队文职工作岗位,各项工资待遇都是参照同级别军官来执行的。


根据《军事科学院系统工程研究院公开招考文职人员宣讲会》内容可知:军队文职于2020年1月上旬前发布公告,2月中上旬开始报名,3月底前进行笔试,4月底前发布笔试成绩。军队文职招考学历一般为本科以及本科以上,部分专业可放宽至大专学历,往届生可以报考。


招考公告明确指出试用期6个月,应届毕业本科生、硕士生、博士生试用期到手月工资分别约为7200元、7600元、8500元,试用期满后到手月工资分别约为9000元、9500元、11000元(以上均以高校毕业生为例,含住房补贴);此外,科研工作岗位高、中、初职分别享受2500-3000、2000和1000元的科研岗位津贴,六险一金,工资待遇远超公务员。据了解转正以后工资都是一万以上,而且文职人员的住房公积金、住房补贴和房租补贴参照现役军官政策确定的标准执行,符合规定条件的人员,军队可以增发住房补助。


为什么把军队文职放在第一位呢?其实很简单,上面的所有工作,刚入职,工资待遇绝对是没有军队文职高,连中国烟草在军队文职面前都没那么香了。


对于网传排名,你有不同意见吗,欢迎留言分享你的想法!
来源:https://mp.weixin.qq.com/s/LXb5xnqkvLhBBsiLy5kkvQ
收起阅读 »

俄乌战火引发芯片危机!光刻机一核心原料70%产自乌克兰,ASML都坐不住了

没想到,疫情之下本就脆弱的半导体供应链,这回因为俄乌开火雪上加霜了。 不是美日等要制裁俄罗斯,限制半导体出口的问题,而是—— 原材料恐将断供,价格危险了。 关键词是氖气。 这种惰性气体,对于半导体光刻环节至关重要,是大部分主流光刻机“光源”不可或缺的原料之一。...
继续阅读 »

没想到,疫情之下本就脆弱的半导体供应链,这回因为俄乌开火雪上加霜了。


不是美日等要制裁俄罗斯,限制半导体出口的问题,而是——


原材料恐将断供,价格危险了。


关键词是氖气。


这种惰性气体,对于半导体光刻环节至关重要,是大部分主流光刻机“光源”不可或缺的原料之一。


而乌克兰,正是全球最大的氖气出口国,其出口的氖气约占全球市场的70%。


pic_13ac36ea.png

据《科创板日报》报道,国内光刻气体标杆厂商凯美特气表示,氖气目前已涨价:



现在两三天一个价格,价格波动很大。我们通过经销商销售,主要销往国外。且国外企业的报价高于国内。



而面对供应中断风险,光刻机巨头ASML已经表示在寻找“备胎”。


此前,克里米亚局势紧张时期,氖气的价格就曾一度上涨600%。


主流光刻技术重要原料之一

所以,氖气究竟在芯片制造环节起到什么作用?


简单来说,它是光刻机中产生“光源”的一种重要原材料,而光刻机对于光源的波长要求非常高。



波长越短,雕刻出来的电路越细致,芯片制程就越小。



利用稀有气体,不仅能获得波长较短的激光,波长也相对更加稳定。


其中,尤以目前大规模应用的、而且仍占主流地位的DUV深紫外光刻机需要大量氖气。


具体来说,DUV光刻机利用光刻气体来制造光源,这是一种稀有气体与氟的混合气。


常见的光刻气体有氩氟氖混合气、氪氖混合气、氩氖混合气、氩氙氖混合气等等。


制造时,需要利用高压激发这些稀有混合气体,产生电子跃迁,从而产生波长稳定的光线,经过聚合滤波等过程形成光源。



原子核外的电子在发生跃迁的时候,如果是跃迁至低能级,就会释放出光子,将它在增益介质中反复放大能量后,发射出去就形成了一个波长稳定的激光。



" class="reference-link">pic_66801a68.png
△图源维基百科

也就是说,作为缓冲气体,氖气普遍存在于各种激光气体中,用于提供高效的能量。


如在目前DUV光刻机主要采用的ArF(氟化氩)激光器中,氖气就占到了气体混合物的96%以上。


有了优质的光源之后,光刻机才能进一步制造芯片。


具体原理有点类似于我们“投影”的效果,利用光对涂在晶圆表面的“保护膜”(光刻胶)进行去除,这样失去光刻胶的部分就能被化学液体腐蚀并形成电路。


" class="reference-link">pic_fb7a2936.png
△光刻胶对黄光不敏感,光刻间通常用黄光

当然,目前光刻机也在进一步发展中。


从最早用汞灯作为光源,到深紫外(DUV)光刻机、再到下一代极紫外(EUV)光刻机,EUV主要通过锡等离子来产生光源,而ASML等厂商也在尽力发展这项新技术。


ASML寻找备胎,三星英特尔:暂无影响

不仅是氖气,乌克兰出口的氪气和氙气也分别占到了全球供应份额的40%和30%。


而另一种重要半导体材料钯,则有40%来自俄罗斯。


美国芯片制造商美光就表示:俄乌冲突升级凸显出半导体供应链的复杂性和脆弱性。


(p.s. 日本首相已宣布,因乌克兰问题将制裁俄罗斯金融机构,并限制半导体和其他敏感技术出口)


pic_16c41ecd.png

面对市场担忧,多家芯片厂商已经公开对此事做出回应。


目前,三星、SK海力士、英特尔等厂商均表示,受益于多元化材料来源,其芯片生产暂未受到影响。


而全球最大芯片代工厂台积电拒绝在“此时”置评。


这些芯片厂商背后的关键供应商——光刻机巨头ASML则表示:公司正在研究氖气的替代来源。


实际上,2014年克里米亚事件之后,氖气价格就经历过一波大幅上涨。许多公司开始转向中国、美国和加拿大寻求多元化供应。


比如ASML,目前所使用的的氖气中只有不到20%来自冲突地区。


但市场分析仍担忧,俄乌局势对芯片生产的影响将在长期显现。


有来自日本芯片行业的知情人士称:



芯片制造商尚未感受到任何直接影响,但为他们提供半导体制造材料的公司会从俄罗斯和乌克兰购买氖气和钯等。这些材料的供应本来就很紧张,所以任何进一步的供应压力都可能推高他们的价格,进而可能导致芯片价格上涨。



另外,数据显示,国内光刻气体标杆厂商华特气体和凯美特气昨日盘中逆市冲高。


One More Thing

实际上,俄乌局势对科技领域的影响,还不止于芯片。


例如早在物理开火之前,各种DDoS攻击就已经在乌克兰的网络上“先发制人”了。



DDoS是一种网络攻击手段,利用生成大量数据包或请求,导致目标计算机网络或系统资源耗尽,从而使得正常用户无法访问。



一方面,据ZDNet表示,乌克兰的政府网站和银行正在面临DDoS的攻击,包括PrivatBank和 Oschadbank等大型银行也都遭遇了停电问题。


据路透社表示,早在2015年,俄罗斯黑客就被认为对乌克兰进行过网络攻击了,当时大约有22.5万人受到断电影响。


另一方面,网络安全公司ESET发现,乌克兰数百台机器被装上了一种“数据清除病毒”,而且早在过去几个月前就已经存在。


目前,乌克兰政府正在征集黑客志愿者,帮助保护关键的一些基础设施,并对俄罗斯军队实施网络间谍任务。


对此有网友表示,即使有意愿对俄罗斯进行网络攻击,需要考虑的事情也太多了,估计最后也没人愿意参与做这事儿。


pic_f9468e40.png

另外,还有网友总结出了一系列与乌克兰有关的互联网产品。


比如WhatsApp、Paypal的创始人都是乌克兰裔,而Snapchat蒙版技术背后的团队,base敖德萨……


参考链接:
[1]https://www.reuters.com/breakingviews/ukraine-war-flashes-neon-warning-lights-chips-2022-02-24/
[2]https://en.wikipedia.org/wiki/Extreme\_ultraviolet\_lithography
[3]https://zh.wikipedia.org/wiki/%E5%85%89%E5%88%BB%E6%9C%BA
[4]https://zh.wikipedia.org/wiki/%E6%BF%80%E5%85%89
[5]https://www.reuters.com/world/exclusive-ukraine-calls-hacker-underground-defend-against-russia-2022-02-24/
[6]https://twitter.com/sapitonmix/status/1496797920812843015


来源:量子位 | 公众号 QbitAI

收起阅读 »

多普勒,一个“令人发指”的天才,却一生都忙着找工作……

你可能听过“多普勒效应”,却未必知道多普勒本人是个多“令人发指”的天才。22岁时,他花了短短4年,学了拉丁语、法语、意大利语、英语,还有哲学和会计,平时在大学教数学物理,闲了再写写文章作作诗。一切顺风顺水。直到找工作时——他竟然被拒了! 从多普勒效应讲起184...
继续阅读 »

你可能听过“多普勒效应”,却未必知道多普勒本人是个多“令人发指”的天才。22岁时,他花了短短4年,学了拉丁语、法语、意大利语、英语,还有哲学和会计,平时在大学教数学物理,闲了再写写文章作作诗。一切顺风顺水。直到找工作时——他竟然被拒了!


从多普勒效应讲起

1845年,荷兰乌特勒支省一条刚竣工两年的铁路旁,出现了一拨号手。其中一名号手在火车头里,其余的人分布在站台上。火车一边经过,他们一边演奏。


如此热闹的场景,实际上是荷兰科学家Buys Ballot在做实验,因为他对于3年前横空出世的多普勒效应表示不服!


实验结果则是:火车接近号手的时候,车上人听到的号声会高半个调;远离乐队时,又低了半个调。


他大费周章从政府那里借来了火车头,甚至亲自坐进了火车头里听声音,试图推翻这个所谓的多普勒效应。结果不出意外,Buys Ballot亲自体验并证实了多普勒效应确实存在……(来源:[3])


pic_dc5fd38a.png实验中用到的火车头的模型,名为“Hercules”。来源:AN HISTORICAL NOTE DOPPLER RESEARCH IN THE NINETEENTH CENTURY

所谓多普勒效应,指的是如果信号源和接受者之间有相对运动,那么接收端接收到的信号频率将发生变化:相向运动则频率增加,反向运动则频率降低。我们每次听到救护车/警车呼啸而过的“呜啊呜啊”声,都有多普勒效应在起作用。


所以多普勒效应只能用来听个响儿吗?不!公路上的测速雷达,医学上的彩超用的都是这个原理。在宇宙学研究中,多普勒效应也大放异彩,研究遥远天体的运动不再是不可能的事情。它甚至还引出了颠覆人们世界观的理论:著名的“宇宙大爆炸理论”——星系都在互相远离,宇宙处于不断膨胀的状态。


虽然多普勒效应有着极大的知名度,但多普勒本人的经历却鲜有人知道——甚至他的全名都曾被谬传了许久,甚至他的大半生都是在被拒绝中度过的,甚至提出多普勒效应的当天,会场下面只坐了6个人。


pic_3087d6e5.png

多普勒肖像


身体虚弱、头脑发达

多普勒出生于一座极具人文艺术气息的城市——奥地利萨尔茨堡。每年都会有无数游客纷至沓来,漫步于萨尔茨堡的巴洛克式老城中。城堡、大教堂、音乐厅和博物馆一同构成了这个城市的迷人剪影。欧洲最伟大的古典主义音乐家之一莫扎特就是出生于此,因而他的故居也成为了游客们必刷的景点。


游客在参观莫扎特故居时,相当于也在不知不觉中参观了一位大科学家的故居:因为多普勒家就在莫扎特家的隔壁(虽然年代差了那么几十年吧)。


1803年11月29日,在莫扎特故居隔壁的石匠家里,又增添了一名成员——克里斯蒂安·安德烈亚斯·多普勒。


P.S. 在很多传记和记录当中,多普勒的名字都被谬传成了他哥哥的名字“克里斯蒂安·约翰·多普勒”。一传十十传百,就这么一直错了下去。


pic_e1430370.png

多普勒本人也不怎么用他自己的中间名,即使是在正式文件当中——比如布拉格的家庭登记表、结婚证等等上,他一般也只写“多普勒”或者“克里斯蒂安·多普勒”。


pic_b8916740.png

图源丨giphy


作为家中的次子,多普勒本应该接手祖传的石匠手艺,可惜他自幼体弱,并不适合石匠这种需要大量体力劳动的工作。于是父亲就让他在学习这条路上一直走了下去,说不定学成之后还能回来帮着照看下家里的业务。在数学老师极其热情的建议下,1822年多普勒被送到帝国皇家理工学院(即如今的维也纳科技大学),学习数学、力学、物理。


pic_cbe45c66.png

1820~1821年,多普勒在奥地利林兹市的这所学校上学。来源:[1]


在维也纳呆了两年多之后,他于1825年1月回到了萨尔茨堡,决定在学会继续完成正式的教育。多普勒开挂一般的才华和天分在这个阶段体现的淋漓尽致:


他只用了一半的时间就学完了6个学期的课程;随后又花了两年学完了哲学的必修课,还跟着当地商人学商贸和会计;同时他也在学拉丁语、法语、意大利语、英语,后来他的意大利语说得贼溜;他赚生活费的方式则是在圣鲁珀特大学教数学和物理;他还偶尔写诗写文章,拿去投稿发表。


1829年,完成了哲学学习之后,已(刚)然(刚)26岁的多普勒启程去了维也纳。


天才少年接连被拒

之前提过,多普勒在1825年1月回到了萨尔茨堡,住了4年。其实1825年,多普勒曾向维也纳的一位教授申请了高等数学的助理职务。1825年10月,教授在报告中高度评价了多普勒的数学成绩并指出:


“这段时间你一直在萨尔茨堡学别的东西,谁知道你现在数学水平啥样啊!”


换个说法,多普勒被拒了。


这是有记录的多普勒收到的第一封拒信,但这只是他被拒生涯的开端。


1829年6月,多普勒不死心,又一次申请了帝国皇家理工学院高等数学的教授助理。在老师的青睐之下,这回多普勒成功上岗,后来还被允许教授基础数学课赚点外快。


pic_da47fc5d.png1823年的维也纳帝国皇家理工学院。来源:Johann Pezzl’s Neueste Beschreibung von Wien. Sechste Auflage, 1823.

然而到了1833年年初,多普勒眼瞅着就要奔30了,他的助教工期也马上就要结束。怎么办?接着投简历吧!


3月份的时候他就申请了布拉格皇家学校的教职,但后续的消息仿佛石沉大海……


刚好意大利东北部的帝国航海学院在招教授,面试笔试地点也正好就在维也纳理工学院。多普勒兴冲冲跑去参加了,然后6月份的时候又被拒了……


直到10月份离开维也纳,多普勒都没拿到一份offer。随后的1834年成为了他人生的低谷,他不断地申请,不断的被拒。最后不得不委身一家棉纺工厂当会计。


pic_a2406b8d.png1833年左右,棉纺工厂里的场景。来源:[1]

1834年底,在经历了无数次失败后,多普勒动了去美国的心思。他和哥哥一起去了一趟慕尼黑,和美国领事讨论能不能在美国谋到一份工作。为了给美国之行凑钱,多普勒还变卖了自己的大部分财物,连书都卖掉了。


偏偏在最绝望的时候,工作又主动找上门了,而且还是双喜临门——一份是在瑞士伯尔尼的中学里当教授;另一份是布拉格州立中学的教授职位。


尽管瑞士的那份工作工资更高,但他出于对祖国的热爱还是选了布拉格(布拉格当时还属于奥地利帝国)。布拉格这份工作一共有14个人申请,他以全优的成绩通过了所有的考核。他尤其擅长数学问题,他的讲课也被评价为“好,易于理解”。


拿到了offer,多普勒也就放弃了去美国的想法,余生也没离开奥地利帝国。


pic_bab3492a.png1815年时奥地利帝国的版图

1835年,多普勒来到布拉格当上了正式的老师,工资也还算可以,第二年他就娶了媳妇儿生了娃。不过他们夫妻俩都不怎么喜欢布拉格,觉得“不太舒服”。更详细原因后文会提到。总之,多普勒一直在寻找跳槽的机会——他又要面临找工作了!


pic_8423463e.png

多普勒的妻子,萨尔茨堡一个金银匠的女儿Mathilde Sturm。来源:多普勒基金会


1837年5月,他参加了维也纳理工学院高等数学教授的面试和考试,失败;


在1836年4月到1837年8月期间,多普勒还申请了3次,均失败;


在别人的推荐下,多普勒申请成为波西米亚皇家科学学会的会员,差点被拒——但,他以7票赞成5票反对的结果涉险过关,成为了准会员。


为什么这么有才华的多普勒会不断被拒?他的孙子说,爷爷在这种需要竞争的氛围当中,总因缺乏勇气而落于下风。


直到1847年,多普勒盼了十年的离开布拉格的机会终于来了。现如今位于斯洛伐克的矿林大学的数理力学教授职位空缺了出来。当年12月,多普勒奔赴新职位。


艰难的布拉格岁月

在布拉格期间,对多普勒来说可能还有点慰藉的就是学生们送给他的礼物了:一幅平板印刷照片的原本。这就是多普勒流传最广的那张肖像画,上面写着:“布拉格理工学院1839-1840年班,出于感激和尊敬所赠”。


pic_855ec088.png

Austrian National Library, Vienna


上文提到,1835年时32岁的多普勒成为了布拉格州立中学的教授,这也是他的第一份正职工作;他还被委任为布拉格理工学院的候补教授;同年,他还发了3篇论文。


有妻儿陪伴,事业也终于有了起色,但是沉重的工作负担却让他根本开心不起来:他每周要给400名学生上8节课,还得自己批作业。


pic_c71cbee7.png布拉格期间的多普勒发表的第一篇文章。来源:[1]

多普勒还总是担心,自己没法养活一家子7口人。


在拥挤的小教室里给那么多人上课,他本就孱弱的身体一点点地被压垮了。从布拉格开始,多普勒就已肺病缠身,也或许,这要追溯到他年幼时就十分虚弱的体格——不仅将他送上了学术道路,也让他在这条道路上走不了太久。


1841年3月,多普勒被任命为布拉格理工学院的正式教授,他的工作负担进一步加重。


pic_520714fd.png

现在的布拉格理工学院。当年多普勒就是在二层楼给学生们上课。来源:[1]


日益虚弱的他要批阅800名学生的报告,此外在皇家科学学会那边他还有事要做。不过科研则是布拉格少有的能令他开心的事了。他强忍着疾病和疲劳,经常在深夜工作。


闪耀的多普勒效应

想象一下船只在河流中航行的场景:


和顺流的船相比,逆行的船会被浪打更多次。既然这个结论在水波里是成立的,那么为什么不试着把它套用到其他的波上呢?


当年多普勒就是在论文中使用了这样形象的类比,从光的波动理论开始入手。


1842年,多普勒在皇家科学学会的自然科学会议上,公布了自己的著作《关于双星还有天上其他星体的色光问题》。文中提出了“多普勒效应”。随后,他名扬天下。但当时多普勒演讲时,台下只有6名观众,包括一名记录员。


pic_7f4225ca.png当天的会议记录。记录员一开始还把月份错写成了6月,后来划掉改成了5月。来源:[2]

“多普勒理论”并不是靠实验观测得出的,他只做了理论工作。100多年前James Bradley的光行差畸变理论给了他很大的启发(Bradley把视差解释为由于地球上观测者的运动造成的),他在论文中也多次引用。


pic_4f0d8e53.png

多普勒赖以成名的论文


虽然多普勒的理论大体上是对的,对于声波的例证也是对的,但关于星星颜色的解释却并不那么正确。现在看来,多普勒效应对星体发光的影响极为微小,以当时的仪器根本无法测量。


接下来的这几年也是多普勒学术生涯最高产的阶段,而他的呼吸疾病也加重了。医生建议他,“要是不想因为过度消耗气管而挂掉的话,最好还是别讲课了。”但多普勒一直坚持,直到1845年严重的咽喉结核让他难以发声。


1844年夏天,为了能在6月赶去萨尔茨堡治疗自己日益恶化的病症,他的课程提早考试;而严苛的分数也让愤怒的家长们纷纷抗议。无奈之下,学校只能宣布考试成绩无效。


本来这不算多大个丑闻,但对于敏感而内向的多普勒来说,这件事促使他决定尽快离开布拉格。


安享余生?不存在的

1847年离开布拉格以后,多普勒前往矿林大学担任教授(当时属于奥地利帝国的匈牙利,现今属于斯洛伐克)。这次迎接他的不是被拒,而是1848年欧洲革命舞台的匈牙利分会场……


多普勒一入职、就感觉到了匈牙利紧张的政治局势。就在他打算夏天出去避一避的时候,匈牙利人民起义了!


pic_df1466f5.png1848年的革命形势 来源:thinglink.com

据多普勒的儿子所说,革命军的一位司令官Artur von Gargey在布拉格学化学的时候就听说过多普勒的大名,因而慕名邀请多普勒前去畅聊。不过多普勒本人显然不想被卷入政治风暴当中,坚持说会谈的话题必须限制在科学问题以内,绝不触及政治。多普勒还请了一位朋友当作见证人。


那天,三人在震耳的炮声中谈了一夜科学。(来源:[2])


pic_fbbdd07b.png

匈牙利爱国诗人裴多菲·山多尔在广场上向群众们朗读《国民歌》。作者:Mihály Zichy


1848年12月,在革命的炮火声中,他被指任为维也纳理工学院的教授,成功躲了出去。1850年1月,维也纳帝国大学的物理所成立了,多普勒被推为第一任院长。在即将46岁之时,多普勒登上了学术生涯的巅峰。


pic_2e4d9cad.png物理所就安置在这所房子里。后面的花园用来做大型实验,多普勒一家子则住在顶层的公寓。 来源:Austrian National Library

难说学术与病痛,究竟哪个才是多普勒一生的主题。1852年11月,多普勒终于放下工作,到了威尼斯养病,然而为时已晚。


他人生的最后5个月,无人知晓。他的父亲和哥哥,也都死于肺病。


pic_c69f654d.png

多普勒去世的地方。来源:[1]


多普勒无处不在

在多普勒生命的最后几年当中,皇家科学院有一部分人开始攻击多普勒的理论,还有他的名声。但实际上,反对者的猛烈抨击,最终看来反而是对于多普勒理论的最好数学证明。


如今:


多普勒超声检测可以得到人体许多部位的血流信息,如血流的方向、速度和状态等,这对诊断心血管疾病、头部及颈部血流疾病等有重要的临床价值;


激光多普勒测速也已经成为了一种实用技术,是研究流体流动的强力手段;


前些年马航MH370航班失联后的飞行方向,也是用此效应推测出来的。


宇宙中天体的运动状态,也可以用多普勒效应测出来。


1929年,著名天文学家哈勃依据多普勒效应,从星系光谱红移中总结出哈勃定律(但他不愿承认宇宙膨胀)。在哈勃提出哈勃定律后,勒梅特等人很快提出了宇宙应该存在一个开端。


1948年的愚人节,伽莫夫和他的同事们发表了标志着大爆炸宇宙模型的博士论文。20世纪60年代以来,大爆炸理论逐渐被广泛接受,以致被天文学家称为宇宙的“标准模型”。


pic_ac1ba9d2.png《生活大爆炸》中谢耳朵的化妆舞会——多普勒效应的服装

其实多普勒效应还可以用来闯红灯:只要你的车开的足够快,红灯在你眼里就是绿灯!速度要多快呢?大概也就是十分之一个光速吧……


看,闪电侠朝我们跑了过来!啊,他变成了绿灯侠!


参考文献:


[1]Peter Maria Schuster Moving the stars : Christian Doppler, his life, his works and principle, and the world after. [2]Eden A. The Search for Christian Doppler[J]. Isis, 1994(4):1-4. [3]Jonkman E J. Doppler research in the nineteenth century[J]. Ultrasound in Medicine & Biology, 1980, 6(1):1-5. [4]https://en.wikipedia.org/wiki/Christian\_Doppler [5]http://www.visit-salzburg.net/sights/christiandoppler.htm


作者:炖着蘑菇的小鸡

收起阅读 »

不可思议!乌克兰国防军队的系统账号和密码分别是 admin 和 123456!

2020年被用烂大街的密码《2020 最烂密码 TOP 200 曝光!》,500 多万个泄漏密码表明,共有近 3% 的人使用“123456”作为密码。而最近知名黑客网站 Have I Been Pwned 上一个密码“ji32k7au4a83”的使用次数引起了...
继续阅读 »

2020年被用烂大街的密码《2020 最烂密码 TOP 200 曝光!》,500 多万个泄漏密码表明,共有近 3% 的人使用“123456”作为密码。而最近知名黑客网站 Have I Been Pwned 上一个密码“ji32k7au4a83”的使用次数引起了热烈讨论。

Have I Been Pwned 是一个可以查询用户的邮箱是否被泄漏的网站,它的一个密码查询功能 Pwned Passwords 记录着在数据泄露中暴露的 551 509 767 个真实密码,用户可以在这里查询某个密码被使用的次数。比如查询一下 2018 年最烂密码“123456”,得到 23 174 662 次的结果: pic_74d7d762.png

但你知道 个人用户对于自己的密码都是如今谨慎,想必上升到企业层面又或者上升到国家层面,他们的密码应该更复杂……吧?比如我们熟悉的五角大楼,多少黑客视它为黑客安全界的珠穆朗马峰,一生都在想征服它。

有媒体爆料 ,乌克兰武装部队的“第聂伯罗”军事自动化控制系统,服务器网络保护十分原始,账号是admin,密码是123456!

pic_7382c1d5.png

然而,似乎并不是所有国家都是将自己的国防系统看得很重要的,日前乌克兰一名记者披露,乌克兰武装部队的“第聂伯罗”军事自动化控制系统,服务器网络保护十分原始,账号是admin,密码是123456!

“123456、admin”在2017年弱密码TOP 100中,分别位列第一位和第十一位。大多数账户系统在注册时基本禁止使用这种“弱密码”,你很难想象这竟然会成为一个国家军方系统的用户名和密码。

他表示,这个漏洞“让敌人直到2018年夏天,都可以随意扫描乌克兰军队信息”,他展示了此前自动化该控制系统“第聂伯罗”的设置与测试文件。

2018年5月乌克兰网络部队“第聂伯河”数据库专家,迪米特里·弗拉乔克发现,许多服务器通过一个标准的用户名和密码就可以访问,即“admin 123456”。不需要技术很高深的黑客就能够轻松访问交换机、路由器、服务器、打印机和扫描仪等设备,能够分析出武装部队大量的机密信息甚至掌握整个夏天乌克兰军队在顿巴斯地区的一切计划。

pic_504a005d.png

他及时汇报了这个安全隐患,但这个报告很快就忽略了。鉴于事情的严重性,5月26日他将该情况汇报给了国家安全与国防事务委员会以及乌克兰情报局。

等待长达一个多月的时间,乌克兰国防部才给出回应,要求乌克兰国防部以及其它武装力量部门禁止使用弱密码,同时定期检查所有工作站。不过,对于一些IP地址的安全问题,他们认为不需要加强。

可笑的是,在7月12日的测试中发现,一些设备与特定的IP地址使用默认用户名和密码仍然可以登录进去。在一些情况下,计算机能够直接连接到国防部的网络,没有密码就可以进入。

所以,在近四个月的时间里,访问国防部部分服务器和计算机的密码一直是最简单的:admin、123456。

安全专家的建议是,设置密码满足这三点:1、密码长度最好8位或以上;2、密码没有明显的组成规律;3、尽量使用三种以上符号,如“字母+数字+特殊符号”。
你设置密码的时候又有什么小窍门呢

整理:开发者技术前线

收起阅读 »

OAuth2.0原理图解:第三方网站为什么可以使用微信登录

1 文章概述假设小明开发了一个A网站,需要支持微信登陆和淘宝账号登陆。如果你是微信或者淘宝开发人员,你会怎么设计这个功能?本文结合淘宝开放平台官方文档以淘宝账号为例。从最简单视角去思考,用户在网站A输入淘宝用户名和密码,网站A调用淘宝接口校验输入信息,校验通过...
继续阅读 »

1 文章概述

假设小明开发了一个A网站,需要支持微信登陆和淘宝账号登陆。如果你是微信或者淘宝开发人员,你会怎么设计这个功能?本文结合淘宝开放平台官方文档以淘宝账号为例。

从最简单视角去思考,用户在网站A输入淘宝用户名和密码,网站A调用淘宝接口校验输入信息,校验通过则登陆成功,整体流程如下图:


01 第三方登陆简单思路.jpg


上述思路存在什么问题?最显著问题就是信息安全问题。问题第一个方面是用户需要将淘宝用户名和密码输入网站A,这样会带来用户名和密码泄露风险。问题第二个方面是如果用户不信任网站A,那么也不会输入淘宝用户名和密码,影响网站A业务开展。


2 OAuth2.0

第三方登陆信息安全问题应该如何解决?OAuth是一种流行标准。如果执行这行这个标准,那么用户可以在不告知A网站淘宝用户名和密码情况下,使用淘宝账号登陆A网站。

目前已经发展到OAuth2.0版本,相较于1.0版本更加关注客户端开发者简易性,而且为桌面应用、web应用、手机设备提供专门认证流程。


2.1 四种角色

OAuth2.0标准定义了四种角色:

  • 客户端(Client)
  • 资源所有者(Resource Owner)
  • 资源服务器(Resource Server)
  • 授权服务器(Authorization Server)

四种角色交互流程:

02 OAuth2_四种角色_01.jpg

本文场景对应四种角色:

02 OAuth2_四种角色_02.jpg


2.2 四种模式

OAuth2.0标准定义了以下四种授权模式:

  • 授权码模式(authorization code)
  • 隐式模式(implicit)
  • 密码模式(password)
  • 客户端模式(client credentials)

四种授权模式中最常用的是授权码模式,例如微信开发平台文档介绍对于网站应用微信OAuth2.0授权登录目前支持授权码模式,所以本文只介绍授权码模式,后续文章会详细比较四种模式。


2.3 实现流程

第一个流程是创建应用,A网站开发者首先去淘宝开放平台创建应用,开放平台会生成一个client_id作为A网站唯一标识。

第二个流程是授权流程,用户在A网站点击使用淘宝账号登陆时,实际上跳转至A网站拼接授权URL页面,这个页面由淘宝提供。用户在授权页面输入淘宝用户名和密码,校验成功后跳转至A网站回调地址,这时A网站会拿到一个code,后台再使用code去获取access_token。

第三个流程是获取信息,获取到access_token相当于获取到一把钥匙,再按照规范调用淘宝对外提供接口就可以获取到用户数据。


03 oauth2_整体流程.jpg


2.4 为什么安全

第一个方面A网站开发人员需要在淘宝开放平台进行申请,需要输入个人信息或者公司信息,这样A网站可靠性有了一定程度保证。

第二个方面在第一章节方案用户需要在A网站输入淘宝用户名和密码,但是在OAuth2.0方案2.4步骤虽然也要输入淘宝用户名密码,但是这个页面由淘宝官方提供,安全性得到了保证。

第三个方面access_token(令牌)并没有在浏览器中传递,而是需要A网站在获取到code之后去后台程序换取,避免了钥匙泄露风险。

第四个方面code(授权码)在浏览器传递有一定风险,但是具有两个特性一定程度保证了安全:

(1) code具有效期,超期未使用需要重新按授权流程获取

(2) code只能使用一次,使用后需要重新按授权流程获取


3 OpenID Connect

3.1 授权与认证

在第二章节详细分析了OAuth2.0协议,在实现流程章节分析了创建应用、授权流程、获取信息三个流程,我们发现一个问题:客户端在获取到令牌之后,还需要调用资源服务器接口获取用户信息,有没有一种协议可以在返回令牌时同时将用户是谁返回呢?回答这个问题之前首先对比一组概念:授权与认证。

授权关注通信实体具有什么权限,认证关注通信实体是谁。OAuth2.0只有授权流程,返回令牌之后授权流程已经完成,OpenID connect在此基础上进行了扩展,这样客户端能够通过认证来识别用户。


3.2 三种角色

OpenID Connect定义了三种角色:

  • 最终用户(End User)
  • 依赖方(Relying Party)
  • 身份认证提供商(Identity Provider)

三种角色交互流程:

04 OIDC_三种角色_01.jpg

本文场景对应三种角色:

04 OIDC_三种角色_02.jpg


3.3 整体流程

05 OIDC_整体流程.jpg


4 相关文档

淘宝开放平台用户授权介绍

网站应用微信登录开发指南


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

Java线程池必知必会

1、线程数使用开发规约阿里巴巴开发手册中关于线程和线程池的使用有如下三条强制规约【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。正例:自定义线程工厂,并且根据外部特征进行分组,比如,来自同一机房的调用,把机房编号赋值给whatFeatureO...
继续阅读 »

1、线程数使用开发规约

阿里巴巴开发手册中关于线程和线程池的使用有如下三条强制规约

【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。

正例:自定义线程工厂,并且根据外部特征进行分组,比如,来自同一机房的调用,把机房编号赋值给whatFeatureOfGroup


public class UserThreadFactory implements ThreadFactory {

private final String namePrefix;

private final AtomicInteger nextId = new AtomicInteger(1);

/**

* 定义线程组名称,在利用 jstack 来排查问题时,非常有帮助

*/


UserThreadFactory(String whatFeatureOfGroup) {

namePrefix = "From UserThreadFactory's " + whatFeatureOfGroup + "-";

}

@Override

public Thread newThread(Runnable task) {

String name = namePrefix + nextId.getAndIncrement();

Thread thread = new Thread(null, task, name, 0);

System.out.println(thread.getName());

return thread;

}

}

【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。

说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。

如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这

样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

说明:Executors 返回的线程池对象的弊端如下:

1) FixedThreadPool 和 SingleThreadPool:

允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。

2) CachedThreadPool:

允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

2、 ThreadPoolExecutor源码

1. 构造函数

UML图: image.png ThreadPoolExecutor的构造函数共有四个,但最终调用的都是同一个:

image.png

2.核心参数

  1. corePoolSize => 线程池核心线程数量

  2. maximumPoolSize => 线程池最大数量

  3. keepAliveTime => 线程池的工作线程空闲后,保持存活的时间。如果任务多而且任务的执行时间比较短,可以调大keepAliveTime,提高线程的利用率。

  4. unit => 时间单位

  5. workQueue => 线程池所使用的缓冲队列,队列类型有:

    • ArrayBlockingQueue,基于数组结构的有界阻塞队列,按FIFO(先进先出)原则对任务进行排序。使用该队列,线程池中能创建的最大线程数为maximumPoolSize

    • LinkedBlockingQueue,基于链表结构的无界阻塞队列,按FIFO(先进先出)原则对任务进行排序,吞吐量高于ArrayBlockingQueue。使用该队列,线程池中能创建的最大线程数为corePoolSize。静态工厂方法 Executor.newFixedThreadPool()使用了这个队列。

    • SynchronousQueue,一个不存储元素的阻塞队列。添加任务的操作必须等到另一个线程的移除操作,否则添加操作一直处于阻塞状态。静态工厂方法 Executor.newCachedThreadPool()使用了这个队列。

    • PriorityBlokingQueue:一个支持优先级的无界阻塞队列。使用该队列,线程池中能创建的最大线程数为corePoolSize。

  6. threadFactory => 线程池创建线程使用的工厂

  7. handler => 线程池对拒绝任务的处理策略,主要有4种类型的拒绝策略:

    • AbortPolicy:无法处理新任务时,直接抛出异常,这是默认策略。

    • CallerRunsPolicy:用调用者所在的线程来执行任务。

    • DiscardOldestPolicy:丢弃阻塞队列中最靠前的一个任务,并执行当前任务。

    • DiscardPolicy:直接丢弃任务。

3.execute()方法

image.png

  1. 如果当前运行的线程少于corePoolSize,则创建新的工作线程来执行任务(执行这一步骤需要获取全局锁)。

  2. 如果当前运行的线程大于或等于corePoolSize,而且BlockingQueue未满,则将任务加入到BlockingQueue中。

  3. 如果BlockingQueue已满,而且当前运行的线程小于maximumPoolSize,则创建新的工作线程来执行任务(执行这一步骤需要获取全局锁)。

  4. 如果当前运行的线程大于或等于maximumPoolSize,任务将被拒绝,并调用RejectExecutionHandler.rejectExecution()方法。即调用饱和策略对任务进行处理。

3、线程池的工作流程

image.png

image.png

执行逻辑说明:

  1. 判断核心线程数是否已满,核心线程数大小和corePoolSize参数有关,未满则创建线程执行任务

  2. 若核心线程池已满,判断队列是否满,队列是否满和workQueue参数有关,若未满则加入队列中

  3. 若队列已满,判断线程池是否已满,线程池是否已满和maximumPoolSize参数有关,若未满创建线程执行任务

  4. 若线程池已满,则采用拒绝策略处理无法执执行的任务,拒绝策略和handler参数有关

4、Executors创建返回ThreadPoolExecutor对象(不推荐)

Executors创建返回ThreadPoolExecutor对象的方法共有三种:

1. Executors#newCachedThreadPool => 创建可缓存的线程池

  • corePoolSize => 0,核心线程池的数量为0

  • maximumPoolSize => Integer.MAX_VALUE,可以认为最大线程数是无限的

  • keepAliveTime => 60L

  • unit => 秒

  • workQueue => SynchronousQueue

弊端:maximumPoolSize => Integer.MAX_VALUE可能会导致OOM

2. Executors#newSingleThreadExecutor => 创建单线程的线程池

SingleThreadExecutor是单线程线程池,只有一个核心线程:

  • corePoolSize => 1,核心线程池的数量为1

  • maximumPoolSize => 1,只可以创建一个非核心线程

  • keepAliveTime => 0L

  • unit => 毫秒

  • workQueue => LinkedBlockingQueue

弊端:LinkedBlockingQueue是长度为Integer.MAX_VALUE的队列,可以认为是无界队列,因此往队列中可以插入无限多的任务,在资源有限的时候容易引起OOM异常

3. Executors#newFixedThreadPool => 创建固定长度的线程池

  • corePoolSize => 1,核心线程池的数量为1

  • maximumPoolSize => 1,只可以创建一个非核心线程

  • keepAliveTime => 0L

  • unit => 毫秒

  • workQueue => LinkedBlockingQueue

它和SingleThreadExecutor类似,唯一的区别就是核心线程数不同,并且由于使用的是LinkedBlockingQueue,在资源有限的时候容易引起OOM异常

5、线程池的合理配置

从以下几个角度分析任务的特性:

  1. 任务的性质:CPU 密集型任务、IO 密集型任务和混合型任务。

  2. 任务的优先级:高、中、低。

  3. 任务的执行时间:长、中、短。

  4. 任务的依赖性:是否依赖其他系统资源,如数据库连接。

任务性质不同的任务可以用不同规模的线程池分开处理。可以通过 Runtime.getRuntime().availableProcessors()方法获得当前设备的 CPU 个数。

  • CPU 密集型任务:配置尽可能小的线程,如配置 cpu核心数+1 个线程的线程池。

  • IO 密集型任务 :由于线程并不是一直在执行任务,则配置尽可能多的线程,如2 ∗ Ncpu

  • 混合型任务:如果可以拆分,则将其拆分成一个 CPU 密集型任务和一个 IO 密集型任务。只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率;如果这两个任务执行时间相差太大,则没必要进行分解。

优先级不同的任务可以使用优先级队列 PriorityBlockingQueue 来处理,它可以让优先级高的任务先得到执行。但是,如果一直有高优先级的任务加入到阻塞队列中,那么低优先级的任务可能永远不能执行。

执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。

依赖数据库连接池的任务,因为线程提交 SQL 后需要等待数据库返回结果,线程数应该设置得较大,这样才能更好的利用 CPU。

建议使用有界队列,有界队列能增加系统的稳定性和预警能力。可以根据需要设大一点,比如几千。使用无界队列,线程池的队列就会越来越大,有可能会撑满内存,导致整个系统不可用。

处理拒绝策略有以下几种比较推荐:

在程序中捕获RejectedExecutionException异常,在捕获异常中对任务进行处理。针对默认拒绝策略使用CallerRunsPolicy拒绝策略,该策略会将任务交给调用execute的线程执行【一般为主线程】,此时主线程将在一段时间内不能提交任何任务,从而使工作线程处理正在执行的任务。此时提交的线程将被保存在TCP队列中,TCP队列满将会影响客户端,这是一种平缓的性能降低自定义拒绝策略,只需要实现RejectedExecutionHandler接口即可如果任务不是特别重要,使用DiscardPolicy和DiscardOldestPolicy拒绝策略将任务丢弃也是可以的如果使用Executors的静态方法创建ThreadPoolExecutor对象,可以通过使用Semaphore对任务的执行进行限流也可以避免出现OOM异常。

6、拒绝策略

有以下几种比较推荐:

  • 在程序中捕获RejectedExecutionException异常,在捕获异常中对任务进行处理。针对默认拒绝策略

  • 使用CallerRunsPolicy拒绝策略,该策略会将任务交给调用execute的线程执行【一般为主线程】,此时主线程将在一段时间内不能提交任何任务,从而使工作线程处理正在执行的任务。此时提交的线程将被保存在TCP队列中,TCP队列满将会影响客户端,这是一种平缓的性能降低

  • 自定义拒绝策略,只需要实现RejectedExecutionHandler接口即可

  • 如果任务不是特别重要,使用DiscardPolicy和DiscardOldestPolicy拒绝策略将任务丢弃也是可以的如果使用Executors的静态方法创建ThreadPoolExecutor对象,可以通过使用Semaphore对任务的执行进行限流也可以避免出现OOM异常。

  • 参考文章:8大拒绝策略

7、线程池的五种运行状态

线程状态:

image.png

不同于线程状态,线程池也有如下几种 状态:

image.png

• RUNNING :该状态的线程池既能接受新提交的任务,又能处理阻塞队列中任务。

• SHUTDOWN:该状态的线程池不能接收新提交的任务,但是能处理阻塞队列中的任务。(政府服务大厅不在允许群众拿号了,处理完手头的和排队的政务就下班)


处于 RUNNING 状态时,调用 shutdown()方法会使线程池进入到该状态。

注意:finalize() 方法在执行过程中也会隐式调用shutdown()方法。

• STOP:该状态的线程池不接受新提交的任务,也不处理在阻塞队列中的任务,还会中断正在执行的任务。(政府服务大厅不再进行服务了,拿号、排队、以及手头工作都停止了。)


在线程池处于 RUNNINGSHUTDOWN 状态时,调用shutdownNow() 方法会使线程池进入到该状态;

• TIDYING:如果所有的任务都已终止,workerCount (有效线程数)=0。


线程池进入该状态后会调用 terminated() 钩子方法进入TERMINATED 状态。

• TERMINATED:在terminated()钩子方法执行完后进入该状态,默认terminated()钩子方法中什么也没有做。


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