注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

算法. 无重复字符的最长子串

一、题目 难度中等 给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。 示例 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
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

算法题每日一练---第37天:打家劫舍

一、问题描述你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装...
继续阅读 »

一、问题描述

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

题目链接:打家劫舍

二、题目要求

样例1

输入: [1,2,3,1]
输出: 4
解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4

样例2

输入: [2,7,9,3,1]
输出: 12
解释: 偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。

数据范围

  • 1 <= nums.length <= 100`
  • 0 <= nums[i] <= 400`

考察

1.动态规划中等题型
2.建议用时5~15min

三、问题分析

这也是一道比较典型的动态规划问题,动态规划没做过的可以看这一篇入门题解:

算法题每日一练---第34天: 青蛙跳台阶

还是用我们的三步走,老套路:

第一步含义搞懂:

首先,使用动态规划一维数组就可以解决问题,那么这个dp[i]到底代表什么?

看看题目问什么,在不触犯警报的情况下,偷到的最大金额数,那么dp[i]就代表从截止到第i个房子,最大的金额数。

第二步变量初始:

假如房子数目为1,那么dp[0]=nums[0]
假如房子数目为2,那么dp[1]=max(nums[0],nums[1])

第三步规律归纳:

那么到底有什么规律呢?我把样例2详细列出来你看一下:

打家劫舍.gif

从第三个数开始,dp[i]是不是满足

dp[i]=max(dp[i−2]+nums[i],dp[i−1])关系式

三步走,打完收工!

四、编码实现

#include<iostream>
#include<algorithm>
using namespace std;
int main()
{
int n,nums[105],i,dp[105];//初始化
cin>>n;//输入数组的大小,n为0或1力扣要判断的,我这里省去了
for(i=1;i<=n;i++)//输入数组的元素
cin>>nums[i];
dp[1]=nums[1],dp[2]=max(nums[1],nums[2]);//初始化动态规划前两位
for(i=3;i<=n;i++)//第三位开始循环
{
dp[i]=max(dp[i-1],nums[i]+dp[i-2]);//找到规律
}
cout<<dp[n];//输出结果
return 0;
}

五、测试结果

2.png


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

Flutter - 这么炫酷的App你见过吗??

前言:今天是1024,先祝各位兄弟们节日快乐,永不脱发,永无Bug😜。说正事:在前几天,我发现了一个动画特别炫酷的一个Flutter项目,一款习惯养成类的App,看了后就真的是爱不释手,功能很丰富,所以我立刻找到了开源作者,向他申请了写作权限。然后开始了对项目...
继续阅读 »

前言:今天是1024,先祝各位兄弟们节日快乐,永不脱发,永无Bug😜。说正事:在前几天,我发现了一个动画特别炫酷的一个Flutter项目,一款习惯养成类的App,看了后就真的是爱不释手,功能很丰富,所以我立刻找到了开源作者,向他申请了写作权限。然后开始了对项目的分析(求个赞!!!相信我,看完这篇你会有收获的👍)

我对他项目的代码进行了部分修改,修改的源代码在文章最后~

开源项目地址:github.com/designDo/fl…

先上效果图:

tt0.top-432794.gif tt0.top-795301.gif

还有很多的功能大家自己下载源码(觉得好的话给开源作者点个star哦,人家不容易!)

本文分析重点:

  • 登录界面的动画、输入框处理以及顶部弹出框
  • 底部导航栏的动画处理
  • 首页动画以及环形进度条处理
  • 适配深色模式(分析一下作者的全局状态管理)

1.登录界面的动画、输入框处理以及顶部弹出框

  • 动画处理

    这里一共有3处动画,输入框的缩放动画,验证码按钮的平移动画,登录界面的缩放动画。

    当我们使用动画时,我们需要定义一个Controller来控制管理动画

    AnimationController _animationController;

    当然使用动画时我们的State是需要混入SingleTickerProviderStateMixin这个类的

    在效果图中我们也不难看出动画直接是有时间间距的,所以我们整个界面仅用一个Controller来控制,使其从上到下逐步显示。

    关于缩放动画呢,在flutter我们需要使用ScaleTransition,其中最重要的一点便是:

    Animation<double> scale //控制widget缩放

    来看看详细使用:

    ScaleTransition(
    //控制缩放从0到1
    scale: Tween<double>(begin: 0, end: 1).animate(CurvedAnimation(
    //控制动画的Controller
    parent: _animationController,
    //0,0.3是动画运行的时间
    //curve用来描述曲线运动的动画
    curve: Interval(0, 0.3, curve: Curves.fastOutSlowIn),
    )),
    child:...
    )

    这里关于其他动画也差不多,区别就在于动画和动画的运行时间

    关键区别:

    验证码的输入框:

    curve: Interval(0.3, 0.6, curve: Curves.fastOutSlowIn),

    获取验证码按钮:

    这里主要区别是position用于处理初始时的绝对位置

    SlideTransition(
    //大家可以将begin: Offset(2, 0)的数据更改,这样就会清晰的体验到它的功能
    position: Tween<Offset>(begin: Offset(2, 0), end: Offset.zero)
    .animate(CurvedAnimation(
    parent: _animationController,
    curve:
    Interval(0.6, 0.8, curve: Curves.fastOutSlowIn))),child:...)

    登录按钮:

    ScaleTransition(
    scale: Tween<double>(begin: 0, end: 1).animate(CurvedAnimation(
    parent: _animationController,
    curve: Interval(0.8, 1, curve: Curves.fastOutSlowIn),
    )),child:...)

    关于动画的实现就是这样,是不是非常的简单~

  • 手机号输入框的限制处理

登录输入框处理.png

我觉得这个样式很炫酷,主要是在平时不是很常见,就分析一下

这里我们封装了一个CustomEditField输入框,可以更好的做动画的处理

动画定义

///文本内容
String _value = '';
TextEditingController editingController;
AnimationController numAnimationController;
Animation<double> numAnimation;

且该组件需要混入(Mixin)TickerProviderStateMixin与AutomaticKeepAliveClientMixin,因为AnimationController需要调用TickerProvider里的createTicker方法(感兴趣可以查看flutter源码)

with TickerProviderStateMixin, AutomaticKeepAliveClientMixin

初始化时:

@override
void initState() {
_value = widget.initValue;
//初始化controller
editingController = TextEditingController(text: widget.initValue);
//初始化限制框的控制器与动画
numAnimationController =
AnimationController(duration: Duration(milliseconds: 500), vsync: this);
numAnimation = CurvedAnimation(
parent: numAnimationController, curve: Curves.easeOutBack);
if (widget.initValue.length > 0) {
numAnimationController.forward(from: 0.3);
}
super.initState();
}

销毁时:

@override
void dispose() {
editingController.dispose();
numAnimationController.dispose();
super.dispose();
}

UI: 使用Stack用于包裹一个输入框和限制框

Stack(
children:[
TextField(),
//限制框的动画,所以在外面套一层ScaleTransition
ScaleTransition(
child:Padding()
)
]
)

使用这个封装的组件时,我们主要处理numDecoration

此处的颜色为全局管理的处理,直接复制该代码需要修改

numDecoration: BoxDecoration(
shape: BoxShape.rectangle,
color: AppTheme.appTheme.cardBackgroundColor(),
borderRadius: BorderRadius.all(Radius.circular(15)),
boxShadow: AppTheme.appTheme.containerBoxShadow()),
numTextStyle: AppTheme.appTheme
.themeText(fontWeight: FontWeight.bold, fontSize: 15),
  • 顶部弹出框的处理

1634777618(1).png

使用了flash这个插件,一个高度可定制、功能强大且易于使用的警告框

为了代码的复用,在这里进行了封装处理

class FlashHelper {
static Future<T> toast<T>(BuildContext context, String message) async {
return showFlash<T>(
context: context,
//显示两秒
duration: Duration(milliseconds: 2000),
builder: (context, controller) {
//弹出框
return Flash.bar(
margin: EdgeInsets.only(left: 24, right: 24),
position: FlashPosition.top,
brightness: AppTheme.appTheme.isDark()
? Brightness.light
: Brightness.dark,
backgroundColor: Colors.transparent,
controller: controller,
child: Container(
alignment: Alignment.center,
padding: EdgeInsets.all(16),
height: 80,
decoration: BoxDecoration(
shape: BoxShape.rectangle,
borderRadius: BorderRadius.all(Radius.circular(16)),
gradient: AppTheme.appTheme.containerGradient(),
boxShadow: AppTheme.appTheme.coloredBoxShadow()),
child: Text(
//显示的文字
message,
style: AppTheme.appTheme.headline1(
textColor: Colors.white,
fontWeight: FontWeight.normal,
fontSize: 16),
),
));
});
}
}

2.底部导航栏的动画处理

tt0.top-150276.gif

这里真的是惊艳到我了,Icon都是画出来的,作者真的是脑洞大开,点赞!

  • Icon的绘制

    房子:

static final home = FluidFillIconData([
//房子
ui.Path()..addRRect(RRect.fromLTRBXY(-10, -2, 10, 10, 2, 2)),
ui.Path()
..moveTo(-14, -2)
..lineTo(14, -2)
..lineTo(0, -16)
..close(),
]);

四个正方形:

static final window = FluidFillIconData([
//正方形
ui.Path()..addRRect(RRect.fromLTRBXY(-12, -12, -2, -2, 2, 2)),
ui.Path()..addRRect(RRect.fromLTRBXY(2, -12, 12, -2, 2, 2)),
ui.Path()..addRRect(RRect.fromLTRBXY(-12, 2, -2, 12, 2, 2)),
ui.Path()..addRRect(RRect.fromLTRBXY(2, 2, 12, 12, 2, 2)),
]);

趋势图:

static final progress = FluidFillIconData([
//趋势图
ui.Path()
..moveTo(-10, -10)
..lineTo(-10, 8)
..arcTo(Rect.fromCircle(center: Offset(-8, 8), radius: 2), -1 * math.pi,
-0.5 * math.pi, true)
..moveTo(-8, 10)
..lineTo(10, 10),
ui.Path()
..moveTo(-6.5, 2.5)
..lineTo(0, -5)
..lineTo(4, 0)
..lineTo(10, -9),
]);

我的:

static final user = FluidFillIconData([
//我的
ui.Path()..arcTo(Rect.fromLTRB(-5, -16, 5, -6), 0, 1.9 * math.pi, true),
ui.Path()..arcTo(Rect.fromLTRB(-10, 0, 10, 20), 0, -1.0 * math.pi, true),
]);

大佬的思路就是强👍

  • 切换时的波浪动画

    这里主要是两个部分,一个是点击切换时的波浪动画,一个是动画结束后的凹凸效果

    这样的效果我们需要通过CustomPainter来进行绘制

    我们需要定义一些参数(指展示最重要的)

    final double _normalizedY;final double _x;

    然后进行绘制

 @override
void paint(canvas, size) {
// 使用基于“_normalizedY”值的各种线性插值绘制两条三次bezier曲线
final norm = LinearPointCurve(0.5, 2.0).transform(_normalizedY) / 2;
final radius = Tween<double>(
begin: _radiusTop,
end: _radiusBottom
).transform(norm);
// 当动画结束后的凹凸效果
final anchorControlOffset = Tween<double>(
begin: radius * _horizontalControlTop,
end: radius * _horizontalControlBottom
).transform(LinearPointCurve(0.5, 0.75).transform(norm));
final dipControlOffset = Tween<double>(
begin: radius * _pointControlTop,
end: radius * _pointControlBottom
).transform(LinearPointCurve(0.5, 0.8).transform(norm));


final y = Tween<double>(
begin: _topY,
end: _bottomY
).transform(LinearPointCurve(0.2, 0.7).transform(norm));
final dist = Tween<double>(
begin: _topDistance,
end: _bottomDistance
).transform(LinearPointCurve(0.5, 0.0).transform(norm));
final x0 = _x - dist / 2;
final x1 = _x + dist / 2;

//绘制工程
final path = Path()
..moveTo(0, 0)
..lineTo(x0 - radius, 0)
..cubicTo(x0 - radius + anchorControlOffset, 0, x0 - dipControlOffset, y, x0, y)
..lineTo(x1, y) //背景的宽高
..cubicTo(x1 + dipControlOffset, y, x1 + radius - anchorControlOffset, 0, x1 + radius, 0)
//背景的宽高
..lineTo(size.width, 0)
..lineTo(size.width, size.height)
..lineTo(0, size.height);

final paint = Paint()
..color = _color;

canvas.drawPath(path, paint);
}

@override
bool shouldRepaint(_BackgroundCurvePainter oldPainter) {
return _x != oldPainter._x
|| _normalizedY != oldPainter._normalizedY
|| _color != oldPainter._color;
}

这样带波浪动画的背景就完成啦~

  • 按钮的弹跳动画

    其实实现方式与波浪动画相同,也是通过CustomPainter来进行绘制

    (只展示核心代码)

//绘制其他无状态的按钮
final paintBackground = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2.4
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round
..color = AppTheme.iconColor;
//绘制点击该按钮时的颜色
final paintForeground = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2.4
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round
..color = AppTheme.appTheme.selectColor();

Icon的背景以及跳跃我们需要定义AnimationController与Animation,进行跳跃动画的绘制

在初始化时处理动画

@override
void initState() {
_animationController = AnimationController(
duration: const Duration(milliseconds: 1666),
reverseDuration: const Duration(milliseconds: 833),
vsync: this);
_animation = Tween<double>(begin: 0.0, end: 1.0).animate(_animationController)
..addListener(() {
setState(() {
});
});
_startAnimation();

super.initState();
}
final offsetCurve = _selected ? ElasticOutCurve(0.38) : Curves.easeInQuint;
final scaleCurve = _selected ? CenteredElasticOutCurve(0.6) : CenteredElasticInCurve(0.6);

final progress = LinearPointCurve(0.28, 0.0).transform(_animation.value);

final offset = Tween<double>(
begin: _defaultOffset,
end: _activeOffset
).transform(offsetCurve.transform(progress));
final scaleCurveScale = 0.50;
final scaleY = 0.5 + scaleCurve.transform(progress) * scaleCurveScale + (0.5 - scaleCurveScale / 2);

用于控制动画的运行与销毁:

@override
void didUpdateWidget(oldWidget) {
setState(() {
_selected = widget._selected;
});
_startAnimation();
super.didUpdateWidget(oldWidget);
}

void _startAnimation() {
if (_selected) {
_animationController.forward();
} else {
_animationController.reverse();
}
}

ui布局:

return GestureDetector(
onTap: _onPressed,
behavior: HitTestBehavior.opaque,
child: Container(
constraints: BoxConstraints.tight(ne),
alignment: Alignment.center,
child: Container(
margin: EdgeInsets.all(ne.width / 2 - _radius),
constraints: BoxConstraints.tight(Size.square(_radius * 2)),
decoration: ShapeDecoration(
color: AppTheme.appTheme.cardBackgroundColor(),
shape: CircleBorder(),
),
transform: Matrix4.translationValues(0, -offset, 0),
//Icon的绘制
child: FluidFillIcon(
_iconData,
LinearPointCurve(0.25, 1.0).transform(_animation.value),
scaleY,
),
),
),
);

这样底部导航栏就完成啦!

3.首页动画以及环形进度条处理

  • 首页整体列表动画处理

    这一部分数据是最为复杂的

    与其他动画相同,我们需要一个controller来控制,在此页面,我们还需要一个List来存放数据

    final AnimationController mainScreenAnimationController;
    final Animation<dynamic> mainScreenAnimation;
    final List<Habit> habits;

    数据存储在此文章暂时不分析,大家可以自己运行源码~

    初始化动画:

@override
void initState() {
animationController = AnimationController(
duration: const Duration(milliseconds: 2000), vsync: this);
super.initState();
}

因为使用到动画的组件很多,所以我们根节点使用AnimatedBuilder,主要使用的动画FadeTransition与Transform,做法于上面相同,在此就不多赘述了。

  • 环形进度条

    我们封装了一个CircleProgressBar用户绘制圆形进度条

    这部分的ui很简单,主要是动画的绘制较为复杂

屏幕截图 2021-10-23 140905.jpg

ui:

return AspectRatio(
aspectRatio: 1,
child: AnimatedBuilder(
animation: this.curve,
child: Container(),
builder: (context, child) {
final backgroundColor =
this.backgroundColorTween?.evaluate(this.curve) ??
this.widget.backgroundColor;
final foregroundColor =
this.foregroundColorTween?.evaluate(this.curve) ??
this.widget.foregroundColor;

return CustomPaint(
child: child,
//重点是这个封装组件,这里是圆形里面的进度条
foregroundPainter: CircleProgressBarPainter(
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
percentage: this.valueTween.evaluate(this.curve),
strokeWidth: widget.strokeWidth
),
);
},
),
);

详细的绘制:

@override
void paint(Canvas canvas, Size size) {
final Offset center = size.center(Offset.zero);
final Size constrainedSize =
size - Offset(this.strokeWidth, this.strokeWidth);
final shortestSide =
Math.min(constrainedSize.width, constrainedSize.height);
final foregroundPaint = Paint()
..color = this.foregroundColor
..strokeWidth = this.strokeWidth
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke;
final radius = (shortestSide / 2);

// Start at the top. 0 radians represents the right edge
final double startAngle = -(2 * Math.pi * 0.25);
final double sweepAngle = (2 * Math.pi * (this.percentage ?? 0));

// Don't draw the background if we don't have a background color
if (this.backgroundColor != null) {
final backgroundPaint = Paint()
..color = this.backgroundColor
..strokeWidth = this.strokeWidth
..style = PaintingStyle.stroke;
canvas.drawCircle(center, radius, backgroundPaint);
}

canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
startAngle,
sweepAngle,
false,
foregroundPaint,
);
}

这里还有一个很实用的功能:

时间定义和欢迎词

屏幕截图 2021-10-23 142038.jpg

这个demo包含了大部分对时间的处理

屏幕截图 2021-10-23 142440.jpg 例如:

///根据当前时间获取,[monthIndex]个月的开始结束日期
static Pair<DateTime> getMonthStartAndEnd(DateTime now, int monthIndex) {
DateTime start = DateTime(now.year, now.month - monthIndex, 1);
DateTime end = DateTime(now.year, now.month - monthIndex + 1, 0);
return Pair<DateTime>(start, end);
}

强烈推荐大家学习,开发中比较常用!

关于此app的大部分动画ui都分析完成了,其他都是在复用,大家觉得还不错的话可以自己下载体验一下,养成好习惯~

4.适配深色模式(分析一下作者的全局状态管理)

作者在这里使用了Bloc用于状态管理

///  theme mode
enum AppThemeMode {
Light,
Dark,
}
///字体模式
enum AppFontMode {
///默认字体
Roboto,
///三方字体
MaShanZheng,
}
///颜色模式,特定view背景颜色
enum AppThemeColorMode {
Indigo, Orange, Pink, Teal, Blue, Cyan, Purple }

在此基础上,定义了颜色,样式,例如:

String fontFamily(AppFontMode fontMode) {
switch (fontMode) {
case AppFontMode.MaShanZheng:
return 'MaShanZheng';
}
return 'Roboto';
}

然后在使用样式时多用三元判断,这样就很简单的实现了状态管理

这样对这个项目的ui已经动画就分析完成了,大家也可以通过这个项目来学习本地存储,看到这里了,不妨点个赞吧😘


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

kotlin 协程 + Retrofit 搭建网络请求方案对比

近期在调研使用Kotlin协程 + Retrofit做网络请求方案的实践,计划后面会引入到新项目中,Retrofit的使用非常的简单,基本上看个文档就能立马接入,也在github上找了大量的Demo来看别人是怎么写的,看了大量网上的文章,但发现很多文章看下来也...
继续阅读 »

近期在调研使用Kotlin协程 + Retrofit做网络请求方案的实践,计划后面会引入到新项目中,Retrofit的使用非常的简单,基本上看个文档就能立马接入,也在github上找了大量的Demo来看别人是怎么写的,看了大量网上的文章,但发现很多文章看下来也只是一个简单的接入Demo,不能满足我当下的业务需求。以下记录近期调研的结果和我们的使用。 首先我们先对比从网上找到的几种方案:

方案一

代码摘自这里 这是一篇非常好的Kotlin 协程 + Retrofit 入门的文章,其代码如下:

  1. 服务的定义
interface ApiService {
@GET("users")
suspend fun getUsers(): List

}
  1. Retrofit Builder
object RetrofitBuilder {

private const val BASE_URL = "https://5e510330f2c0d300147c034c.mockapi.io/"

private fun getRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build() //Doesn't require the adapter
}

val apiService: ApiService = getRetrofit().create(ApiService::class.java)
}
  1. 一些中间层
class ApiHelper(private val apiService: ApiService) {

suspend fun getUsers() = apiService.getUsers()
}
class MainRepository(private val apiHelper: ApiHelper) {

suspend fun getUsers() = apiHelper.getUsers()
}
  1. 在ViewModel中获取网络数据
class MainViewModel(private val mainRepository: MainRepository) : ViewModel() {

fun getUsers() = liveData(Dispatchers.IO) {
emit(Resource.loading(data = null))
try {
emit(Resource.success(data = mainRepository.getUsers()))
} catch (exception: Exception) {
emit(Resource.error(data = null, message = exception.message ?: "Error Occurred!"))
}
}
}

这段代码能够与服务端通信,满足基本的要求,并且也有异常的处理机制。但存在以下问题:

  1. 对异常的处理粒度过大。如果需要对不同的异常进行差异化的处理,就会比较麻烦。
  2. 在每一个调用的地方都需要进行try...catch操作。
  3. 不支持从reponse中获取响应头部, http code 信息。但其实很多APP通常也没有要求做这些处理,如果没有拿到数据,给一个通用的提示就完。所以这种方案在某些情况下是可以直接使用的。

方案二

从Github上找了一个Demo, 链接在这里 和方案一相比,作者在的BaseRepository里面,对接口的调用统一进行了try...catch的处理,这样对于调用方,就不用每一个都添加try...catch了。相关的代码如下:

open class BaseRepository {

suspend fun apiCall(call: suspend () -> WanResponse): WanResponse {
return call.invoke()
}

suspend fun safeApiCall(call: suspend () -> Result, errorMessage: String): Result {
return try {
call()
} catch (e: Exception) {
// An exception was thrown when calling the API so we're converting this to an IOException
Result.Error(IOException(errorMessage, e))
}
}

suspend fun executeResponse(response: WanResponse, successBlock: (suspend CoroutineScope.() -> Unit)? = null,
errorBlock: (suspend CoroutineScope.() -> Unit)? = null)
: Result {
return coroutineScope {
if (response.errorCode == -1) {
errorBlock?.let { it() }
Result.Error(IOException(response.errorMsg))
} else {
successBlock?.let { it() }
Result.Success(response.data)
}
}
}

}

在Repository里面这样写

class HomeRepository : BaseRepository() {

suspend fun getBanners(): Result> {
return safeApiCall(call = {requestBanners()},errorMessage = "")
}

private suspend fun requestBanners(): Result> =
executeResponse(WanRetrofitClient.service.getBanner())

}

方案三

在网上看到这个博客, 作者利用一个CallAdapter进行转换,将http错误转换成异常抛出来(后面我自己的方案一也是按照这个思路来的)。核心代码如下:

class ApiResultCallAdapter(private val type: Type) : CallAdapter>> {
override fun responseType(): Type = type

override fun adapt(call: Call): Call> {
return ApiResultCall(call)
}
}

class ApiResultCall(private val delegate: Call) : Call> {
/**
* 该方法会被Retrofit处理suspend方法的代码调用,并传进来一个callback,如果你回调了callback.onResponse,那么suspend方法就会成功返回
* 如果你回调了callback.onFailure那么suspend方法就会抛异常
*
* 所以我们这里的实现是永远回调callback.onResponse,只不过在请求成功的时候返回的是ApiResult.Success对象,
* 在失败的时候返回的是ApiResult.Failure对象,这样外面在调用suspend方法的时候就不会抛异常,一定会返回ApiResult.Success 或 ApiResult.Failure
*/

override fun enqueue(callback: Callback>) {
//delegate 是用来做实际的网络请求的Call对象,网络请求的成功失败会回调不同的方法
delegate.enqueue(object : Callback {

/**
* 网络请求成功返回,会回调该方法(无论status code是不是200)
*/

override fun onResponse(call: Call, response: Response) {
if (response.isSuccessful) {//http status 是200+
//这里担心response.body()可能会为null(还没有测到过这种情况),所以做了一下这种情况的处理,
// 处理了这种情况后还有一个好处是我们就能保证我们传给ApiResult.Success的对象就不是null,这样外面用的时候就不用判空了
val apiResult = if (response.body() == null) {
ApiResult.Failure(ApiError.dataIsNull.errorCode, ApiError.dataIsNull.errorMsg)
} else {
ApiResult.Success(response.body()!!)
}
callback.onResponse(this@ApiResultCall, Response.success(apiResult))
} else {//http status错误
val failureApiResult = ApiResult.Failure(ApiError.httpStatusCodeError.errorCode, ApiError.httpStatusCodeError.errorMsg)
callback.onResponse(this@ApiResultCall, Response.success(failureApiResult))
}

}

/**
* 在网络请求中发生了异常,会回调该方法
*
* 对于网络请求成功,但是业务失败的情况,我们也会在对应的Interceptor中抛出异常,这种情况也会回调该方法
*/

override fun onFailure(call: Call, t: Throwable) {
val failureApiResult = if (t is ApiException) {//Interceptor里会通过throw ApiException 来直接结束请求 同时ApiException里会包含错误信息
ApiResult.Failure(t.errorCode, t.errorMsg)
} else {
ApiResult.Failure(ApiError.unknownException.errorCode, ApiError.unknownException.errorMsg)
}

callback.onResponse(this@ApiResultCall, Response.success(failureApiResult))
}

})
}
...
}

作者有提供一个Demo, 如果想拿来用,需要自己再新增一个返回数据的包装类。该方案的缺点是不能获取响应体中的header,还是那句话,毕竟这个需求不常见,可以忽略。

总结一下,当前网上的这些方案可能有的局限:

  1. 如果服务器出错了,不能拿到具体的错误信息。比如,如果服务器返回401, 403,这些方案中的网络层不能将这些信息传递出去。
  2. 如果服务端通过header传递数据给前端,这些方案是不满足需求的。

针对上面的两个问题,我们来考虑如何完善框架的实现。

调整思路

我们期望一个网络请求方案能满足如下目标:

  1. 与服务器之间的正常通信
  2. 能拿到响应体中的header数据
  3. 能拿到服务器的出错信息(http code,message)
  4. 方便的异常处理

调整后的方案

以下代码的相关依赖库版本

implementation 'com.squareup.retrofit2:retrofit:2.8.1'
implementation "com.squareup.retrofit2:converter-gson:2.8.1"

//Coroutine
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6"
  1. 约定常见的错误类型

我们期望ApiException中也能够返回HTTP Code, 为此约定,错误信息的code从20000开始,这样就不会和HTTP的Code有冲突了。

  • ApiError
object ApiError {
var unknownError = Error(20000, "unKnown error")
var netError = Error(20001, "net error")
var emptyData = Error(20002, "empty data")
}

data class Error(var errorCode: Int, var errorMsg: String)
  1. 返回数据的定义ApiResult.kt

用来承载返回的数据,成功时返回正常的业务数据,出错时组装errorCode, errorMsg, 这些数据会向上抛给调用方。

sealed class ApiResult() {
data class Success(val data: T):ApiResult()
data class Failure(val errorCode:Int,val errorMsg:String):ApiResult()
}
data class ApiResponse(var errorCode: Int, var errorMsg: String, val data: T)

方案一

该方案支持获取HTTP Code,并返回给调用方, 不支持从HTTP Response中提取header的数据。

  1. 服务的定义WanAndroidApi
interface WanAndroidApi {
@GET("/banner/json")
suspend fun getBanner(): ApiResult>>
}
  1. 定义一个ApiCallAdapterFactory.kt

在这里面会对响应的数据进行过滤,对于出错的情况,向外抛出错误。

class ApiCallAdapterFactory : CallAdapter.Factory() {
override fun get(returnType: Type, annotations: Array, retrofit: Retrofit): CallAdapter<*, *>? {=
check(getRawType(returnType) == Call::class.java) { "$returnType must be retrofit2.Call." }
check(returnType is ParameterizedType) { "$returnType must be parameterized. Raw types are not supported" }

val apiResultType = getParameterUpperBound(0, returnType)
check(getRawType(apiResultType) == ApiResult::class.java) { "$apiResultType must be ApiResult." }
check(apiResultType is ParameterizedType) { "$apiResultType must be parameterized. Raw types are not supported" }

val dataType = getParameterUpperBound(0, apiResultType)
return ApiResultCallAdapter(dataType)
}
}
class ApiResultCallAdapter(private val type: Type) : CallAdapter>> {
override fun responseType(): Type = type

override fun adapt(call: Call): Call> {
return ApiResultCall(call)
}
}

class ApiResultCall(private val delegate: Call) : Call> {

override fun enqueue(callback: Callback>) {
delegate.enqueue(object : Callback {

override fun onResponse(call: Call, response: Response) {
if (response.isSuccessful) {
val apiResult = if (response.body() == null) {
ApiResult.Failure(ApiError.emptyData.errorCode, ApiError.emptyData.errorMsg)
} else {
ApiResult.Success(response.body()!!)
}
callback.onResponse(this@ApiResultCall, Response.success(apiResult))
} else {
val failureApiResult = ApiResult.Failure(response.code(), response.message())
callback.onResponse(this@ApiResultCall, Response.success(failureApiResult))
}

}

override fun onFailure(call: Call, t: Throwable) {
//Interceptor里会通过throw ApiException 来直接结束请求 同时ApiException里会包含错误信息
val failureApiResult = if (t is ApiException) {
ApiResult.Failure(t.errorCode, t.errorMessage)
} else {
ApiResult.Failure(ApiError.netError.errorCode, ApiError.netError.errorMsg)
}
callback.onResponse(this@ApiResultCall, Response.success(failureApiResult))
}
})
}

override fun clone(): Call> = ApiResultCall(delegate.clone())

override fun execute(): Response> {
throw UnsupportedOperationException("ApiResultCall does not support synchronous execution")
}


override fun isExecuted(): Boolean {
return delegate.isExecuted
}

override fun cancel() {
delegate.cancel()
}

override fun isCanceled(): Boolean {
return delegate.isCanceled
}

override fun request(): Request {
return delegate.request()
}

override fun timeout(): Timeout {
return delegate.timeout()
}
}
  1. 在Retrofit 初始化时指定CallAdapterFactory, 定义文件ApiServiceCreator.kt 如下:
object ApiServiceCreator {

private const val BASE_URL = "https://www.wanandroid.com/"
var okHttpClient: OkHttpClient = OkHttpClient().newBuilder().build()

private fun getRetrofit() = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(ApiCallAdapterFactory())
.build()

fun create(serviceClass: Class): T = getRetrofit().create(serviceClass)
inline fun create(): T = create(T::class.java)
}
  1. 在ViewModel中使用如下:
viewModelScope.launch {
when (val result = api.getBanner()) {
is ApiResult.Success<*> -> {
var data = result.data as ApiResponse>
Log.i("API Response", "--------->data size:" + data.data.size)
}
is ApiResult.Failure -> {
Log.i("API Response","errorCode: ${result.errorCode} errorMsg: ${result.errorMsg}")

}
}
}

方案二

该方案在方案一的基础之上,支持从HTTP Response Header中获取数据。

  1. 服务的定义WanAndroidApi
interface WanAndroidApi {
@GET("/banner/json")
fun getBanner2(): Call>>
}

需要注意此处的getBanner2()方法前面没有suspend关键字,返回的是一个Call类型的对象,这个很重要。

  1. 定义一个CallWait.kt文件, 为Call类添加扩展方法awaitResult, 该方法内部有部份逻辑和上面的CallAdapter中的实现类似。CallWait.kt文件也是借鉴了这段代码
suspend fun  Call.awaitResult(): ApiResult {
return suspendCancellableCoroutine { continuation ->
enqueue(object : Callback {
override fun onResponse(call: Call?, response: Response) {
continuation.resumeWith(runCatching {
if (response.isSuccessful) {
var data = response.body();
if (data == null) {
ApiResult.Failure(ApiError.emptyData.errorCode, ApiError.emptyData.errorMsg)
} else {
ApiResult.Success(data!!)
}
} else {
ApiResult.Failure(response.code(), response.message())
}
})
}

override fun onFailure(call: Call, t: Throwable) {
// Don't bother with resuming the continuation if it is already cancelled.
if (continuation.isCancelled) return
if (t is ApiException) {
ApiResult.Failure(t.errorCode, t.errorMessage)
} else {
ApiResult.Failure(ApiError.netError.errorCode, ApiError.netError.errorMsg)
}
}
})
}
}
  1. Retrofit的初始化

和方案一不一样,在Retrofit 初始化时不需要指定CallAdapterFactory, 定义文件ApiServiceCreator.kt

object ApiServiceCreator {

private const val BASE_URL = "https://www.wanandroid.com/"
var okHttpClient: OkHttpClient = OkHttpClient().newBuilder().build()

private fun getRetrofit() = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()

fun create(serviceClass: Class): T = getRetrofit().create(serviceClass)
inline fun create(): T = create(T::class.java)
}
  1. ViewModel中使用, 和方法一基本一致,只是这里需要调用一下awaitResult方法
viewModelScope.launch {
when (val result = api.getBanner2().awaitResult()) {
is ApiResult.Success<*> -> {
var data = result.data as ApiResponse>
Log.i("API Response", "--------->data size:" + data.data.size)
}
is ApiResult.Failure -> {
Log.i("API Response","errorCode: ${result.errorCode} errorMsg: ${result.errorMsg}")

}
}
}
  1. 如果我们想从reponse的header里面拿数据, 可以使用Retrofit提供的扩展函数awaitResponse, 如下:
try {
val result = api.getBanner2().awaitResponse()
//拿HTTP Header中的数据
Log.i("API Response", "-----header---->Server:" + result.headers().get("Server"))

if (result.isSuccessful) {
var data = result.body();
if (data != null && data is ApiResponse>) {
Log.i("API Response", "--------->data:" + data.data.size)
}
} else {
//拿HTTP Code
Log.i("API Response","errorCode: ${result.code()}")
}
} catch (e: Exception) {
Log.i("API Response","exception: ${e.message}");
}

方案三

如果我们用Java去实现一套

  • 定义服务
public interface WanAndroidApiJava {
@GET("/banner/json")
public Call>> getBanner();
}
  • ApiException中去封装错误信息
public class ApiException extends Exception {
private int errorCode;
private String errorMessage;

public ApiException(int errorCode, String message) {
this.errorCode = errorCode;
this.errorMessage = message;
}

public ApiException(int errorCode, String message, Throwable e) {
super(e);
this.errorCode = errorCode;
this.errorMessage = message;
}

public String getErrorMessage() {
return this.errorMessage;
}

public int getErrorCode() {
return this.errorCode;
}

interface Code {
int ERROR_CODE_DATA_PARSE = 20001;
int ERROR_CODE_SEVER_ERROR = 20002;
int ERROR_CODE_NET_ERROR = 20003;
}

public static final ApiException PARSE_ERROR = new ApiException(Code.ERROR_CODE_DATA_PARSE, "数据解析出错");
public static final ApiException SERVER_ERROR = new ApiException(Code.ERROR_CODE_SEVER_ERROR, "服务器响应出错");
public static final ApiException NET_ERROR = new ApiException(Code.ERROR_CODE_NET_ERROR, "网络连接出错");
}
  • NetResult封装服务器的响应
public class NetResult {
private T data;
private int code;
private String errorMsg;
...//省略get/set
}
  • 自定义一个Callback去解析数据
public abstract class RetrofitCallbackEx implements Callback> {

@Override
public void onResponse(Call> call, Response> response) {
//如果返回成功
if (response.isSuccessful()) {
NetResult data = response.body();
//返回正确, 和后端约定,返回的数据中code == 0 代表业务成功
if (data.getCode() == 0) {
try {
onSuccess(data.getData());
} catch (Exception e) {
//数据解析出错
onFail(ApiException.PARSE_ERROR);
}
} else {
onFail(ApiException.SERVER_ERROR);
}
} else {
//服务器请求出错
Log.i("API Response", "code:" + response.code() + " message:" + response.message());
onFail(ApiException.SERVER_ERROR);
}
}

@Override
public void onFailure(Call> call, Throwable t) {
onFail(ApiException.NET_ERROR);
}

protected abstract void onSuccess(T t);

protected abstract void onFail(ApiException e);

}
  1. 使用
api.getBanner().enqueue(new RetrofitCallbackEx>() {
@Override
protected void onSuccess(List banners) {
if (banners != null) {
Log.i("API Response", "data size:" + banners.size());
}
}

@Override
protected void onFail(ApiException e) {
Log.i("API Response", "exception code:" + e.getErrorCode() + " msg:" + e.getErrorMessage() + " root cause: " + e.getMessage());
}
});

其它

  1. 在实际项目中,可能经常会碰到需要对HTTP Code进行全局处理的,比如当服务器返回401的时候,引导用户去登录页,这种全局的拦截直接放到interceptor 里面去做就好了。
  2. 架构的方案是为了满足业务的需求,这里也只是针对自己碰到的业务场景来进行梳理调研。当然实际项目中通常会有更多的要求,比如环境的切换导致域名的不同,网络请求的通用配置,业务异常的上报等等,一个完整的网络请求方案需要再添加更多的功能。
  3. Kotlin语言非常的灵活,扩展函数的使用能使代码非常的简洁。Kotlin在我们项目中用的不多, 不是非常精通,协程 + Retrofit应该会有更优雅的写法,欢迎交流。


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

每个程序员第一份工作前应该知道的10件事

前言这篇文章对我来说是“事后诸葛亮”,以下是我进入编程行业以来的一些经验和教训。0000 - 被喜欢是很容易的事如果你有时间观念,衣着得体,保持微笑,不做一些愚蠢的事情,至少会有一些人喜欢你。事实上,让你的同事喜欢你是一件很容易的事。你想拥有很多朋友吗?在你的...
继续阅读 »

前言

这篇文章对我来说是“事后诸葛亮”,以下是我进入编程行业以来的一些经验和教训。

0000 - 被喜欢是很容易的事

如果你有时间观念,衣着得体,保持微笑,不做一些愚蠢的事情,至少会有一些人喜欢你。事实上,让你的同事喜欢你是一件很容易的事。你想拥有很多朋友吗?在你的桌上放一碗满满的糖果,你就会发现你有多少朋友。

0001 - 受人尊敬是很难的事

不管你在这个行业有20年经验或没有经验,当你刚进入一家公司时,没有人会尊敬你。这并不意味着他们不喜欢你,或者对你没有好感。这是因为你还没有做任何事,给别人尊敬你的理由。获得尊敬最快的方法就是把工作做到最好。做到最好并不是浮夸和惊艳,而是有效和团结。当大家看到你能有效率并正确的完成工作,不用害怕赢得不了尊敬。

0010 - 你在大学学到的一切都是没用的

第一份工作的前6个月,会比你整个大学学到的还多。你知道吗?工作中压力会很大。因为总有一天老板会要求你去做你简历中写的那些很棒的那些东西。你懂得,那些东西是你在学校能学到的?如果你搞砸了,在大学你会补考、留级。对不起,在这里你会被炒鱿鱼。在最近在工作中发现了这种状况。有些人因为某些技能被聘用,也因为不能解决问题而被解雇。很多有趣的事情会发生在第一份工作中。

大部分公司都有新员工的试用期,一般为30~90天。基本上,如果你搞砸了项目或者老板发现你在简历上撒了谎,不用想别的了,卷铺盖走人吧。千万记住别撒谎!

0011 - 不要停止学习

作为新人,抱着“我什么都不知道,但我想学”的心态去工作会对你帮助很大。当你意识到自己多么愚蠢时,至少不会那么尴尬。你要意识到几乎所有人都比你有经验,比你懂的更多。好消息是,如果你问一些聪明的问题并关注他们做的事情,大多数人会乐意与你分享他们的知识。每个人都下意识认为自己的观点和经验是正确的,所以不要犹豫,想办法改善你的工作方式。只要你问问题,你就能从每一个和你一起工作的人身上学到东西。询问一些技巧、方法和其它同事可能精通的东西。观察他们如何使用命令行。每个人做事的方式都不一样,还有许多未知等着你去发现。技术糟糕(和/或年长的)程序员倾向于认为“他们”的方式是最好的,所以他们从不征求建议。聪明的程序员愿意接受这样一个事实:可能有更好的做事方法。这意味着你必须愿意切换编程语言、环境、操作系统或文本编辑器。

“什么!?叫我放弃

<这里插入自己的做事方式>

?为什么?谁都知道这是最棒的!”你必须克服这种心态。如果你想解决新的问题,必须学会谦卑和接受挫折。这种感觉糟透了,但是克服它!编程很难,做一个优秀的程序员更难。难过就自己一个人躲着哭吧。

0100 - 你的编辑器决定你的死活,所以请明智的选择

如果你觉得我夸大其词了,可以去网上问问哪款编辑器是最好的。得到的回复会是: Vi、Emacs 和一些其他的 IDE(通常会是Eclipse 或 Xcode)。事实上,你也许会惊奇还有多少人把 Vi 作为自己的主编辑器(我认为这些人应该清醒过来并回到80年代,虽然我也是其中一员)。问题的关键是,如果你不会使用其中的一种编辑器,你可能会碰到一些问题。去找到办公室最好的程序员,看看他用的什么编辑器。然后,点头表示认同他的答案,打印小抄,开始学习编辑器。快去!

0101 - 没有人真正关心你是哪所大学毕业的

如果有人问你哪所大学毕业的,不用担心,他们不是在评判你,这只是礼貌的谈话。如果你的编程技术很棒,没人会在乎那一纸文凭。想知道相比文凭他们更关心什么吗?你的绩效。不要跟别人说这件事,因为这会让你看起来像个蠢货。我不能再强调了,说的已经够多了。

0110 - 沉默永不过时

刚工作,不知道要说什么?那就什么都别说。点头并且微笑。同事不让你加入他们的讨论吗?是挺尴尬的,大家不认识你,你也不认识大家。给彼此一点时间,他们中的大多数会主动来找你的。做朋友需要时间。是的,我知道作为新人很难受。我知道一个新的环境是个挑战,但你要振作起来,时间会帮你解决这些问题。如果你不是一个脾气古怪的人,并且做好工作,那么你终将成为团队的一员。除非你是辣妹或美男,否则很难直接融入团体。祝你好运。

0111 - 你会遇到古怪、消极的人,对付他

有些人就是不喜欢你,有时是因为他们有严重的情感问题,他们不喜欢任何人,所以他们会下意识地讨厌你。这些人从不改变,所以你得学会和如何与他们一起工作。如果你很快的找到了一个敌人,那么你要做的就是如何与他成为朋友,因为你不知道接下来几年会发生什么。我见过几次这种事,你今天敌视的家伙明天成了你的上司。

1000 - 和计算机做朋友

想确保你总有最合适的闲聊话题?那就和计算机做朋友。这里有一个给新员工的小提示,大部分人都是因为需要或出问题了才会突然想起计算机技术人员。一段时间后,这些事情会使你的世界观变得恶化。所以定期去他们办公室找他们谈话。不要抱怨,不要乞求,只要问好并且询问他们生活如何。这是一项总是有利的投资。(前提是你得会修电脑...这段有点难翻译,绕口)

1001 - 你永远逃不掉办公室政治

作为一个新人(如果你从足够低的位置开始),你不必太担心这一点。但请放心,当你的责任越来越重时,你会进入这场游戏。你逃不掉,无论你怎么努力。你可以选择不玩,但你也会尝到苦果。这对于顽固的极客来说,这是一门很难的课程,因为相比人来说,我们更喜欢计算机。也许你进入IT行业仅仅是很单纯的喜欢信息技术。如果是这样的话,很抱歉,如果你想超越某个职业水平,必须成为管理层的一员,并加入政治竞技场。

翻译:余震(Freak)
来源:https://juejin.cn/post/6844903497964453896
原文:http://www.applematters.com/article/10-things-every-programmer-should-know-for-their-first-job/

收起阅读 »

程序员要如何变帅?

胖可以瘦土可以救关键要有一颗想改变的心!面对生活,我们所有人最有效的努力方式,不是抱怨自己拿到的牌不够好,而是尽可能地打好手上的每一张牌。这种投资的初期难点在于——意识到品味是多么奢侈的东西。与它正相关的有包括家庭条件、社会环境、圈子质量、自身的悟性与受教育程...
继续阅读 »

提升外形包括很多方面啊,比如减肥、着装、气质等等。最近就发现程序外形变得越来越顺眼(emmmm)可能是他恋情有了新进展(划掉)的缘故

胖可以瘦


土可以救


关键要有一颗想改变的心!面对生活,我们所有人最有效的努力方式,不是抱怨自己拿到的牌不够好,而是尽可能地打好手上的每一张牌。

1,高效投资。


任何人投资都是为了得到回报。穿着打扮方面的投资,要不就回报在事业上,要不就回报在感情上。人生大事就这么两件,在这里头花钱,是非常值得的。

这种投资的初期难点在于——意识到品味是多么奢侈的东西。与它正相关的有包括家庭条件、社会环境、圈子质量、自身的悟性与受教育程度在内的无数变量,于是不停试错成了无数不够幸运的普通人的宿命。比胡乱烧钱更好的策略是在条件有限的情况下,花尽可能小的代价把事情做得尽可能好。

众所周知,男装中能比较容易穿出体面与高端感觉的颜色,只有黑和白,最多再加一个「高级灰」。根据我的观察,白色与灰色的东西如果不花上足够的钱,廉价的材质所带来的廉价感会比黑色显眼一些。所以保守起见,作为男生,先从黑色玩起,试错成本最低。(黑色的另一个优点是衣服不容易脏,许多黑色的秋冬衣物一年只需要洗一两次。)在黑的基础上,偶尔加上一些白色作为搭配,在日常生活中就已经足够应付任何场面了。全身上下的颜色超过三种还能搭得很好看的能力是很昂贵的,不要进行没有把握的冒险。

2,注意安全。


留普通人的普通发型,穿普通人的普通衣服。作为男生,如非必要,不要烫染,不要把头发搞得多有设计感多邪门。发型这种东西,首先要融入一个人,犹有余力的时候,再来追求提升这个人。

若干年前,有一次我一边和当时的女友亲亲一边看新闻的时候,得知姚明在美国理发一次花掉了几百美元,吓了一跳。因为他那时的发型怎么看都是很普通的样子。我看着姚明亲了女友很久才想明白,如果他的发型非常特别,就会把所有人的目光都引向他的脸,而他的优势并不在那里。(虽然这是很有价值的启示,但是后来所有漂亮姑娘在我眼里都长得有一点像姚明的这笔血债,恐怕只有未来某天我看着哪个姑娘亲姚明很久才能抹平了。)

于是乎,前些时间,听说科比做一次头发要花近千美元的时候,很多小伙伴都笑了,那可接近是一个光头啊,我就没有笑。我仔细看了那则新闻的配图,科比的「光头」配合他的脸型五官,看起来非常舒适。大家走上街去多看几眼路上来往的行人,就知道「舒适」二字有多么不容易了,更何况是一个发际线有问题的光头?优秀的发型设计师们输出的美学,贵就贵在这毫厘之间的分寸感。

穿衣服的分寸感也是一样要紧的。脖子上挂一串骷髅头的那不是程序员,是巫师;气场能压住大披风的每一个都是超人。对设计师们的想象力和身边朋友的吐槽欲要有敬畏之心,在对自己需要什么心里还没底的阶段,少逛淘宝、多逛商场,多试试各种非潮牌陈列在橱窗的黑色系搭配。虽然可能比在淘宝购物要多花一些钱,但买到了非潮牌陈列师们恰如其分的品味,值得。这笔钱比自己横冲直撞、胡乱摸索的试错费要低一个数量级。

3,扬长避短。

穿衣打扮的首要功能是什么?是突出和放大我们自身的优势,掩饰与弱化我们固有的缺陷。这里所说的优势与缺陷是非常复杂的东西,魔鬼全都在细节里,不是一个局外人一二三四就能替你列出来的。

一般来说,只有从小陪你长大、摸过看过你裸体无数次的你自己才能弄得清。许多身着看似普通的衣服看起来却特别好看的人,可能身上穿的都是自己为自己搭建的私人订制;许多按照淘宝卖家的介绍和杂志网络上的攻略来穿衣服的人之所以屡屡倒霉,就是没有想清楚这个道理。

4,建立自信。


没有比自信更迷人的气质了,人人都知道这东西的重要性,却不是人人都知道去哪里才能弄到它。

其实很简单的啊,除了自大狂,自信只属于那些有准备的人。举个应景的例子:含胸驼背……这种情况很常见。虽然人人都知道站有站姿才是美,但是作为一个普通人,很难有办法确认自己站多直才合适,往往胸挺起来了,心还是虚的。于是许多没当过兵没学过芭蕾舞的人都会在生活中偶尔不自觉地站成这种比较省力的姿势。

解决这个大问题有非常好的小办法:报一个短期的形体训练班。在这种训练班里,可以学到怎么站、怎么坐、怎么走,一般来说四面墙上都有镜子,学员能够非常清晰地看到自己的改变、确认自己最优雅的样子,做错的时候有导师的鞭策,做对的时候有导师的认可。

我们需要这样的形式感,需要一个这样的密集训练,来帮助我们形成肌肉记忆。我小时候没有养成好的习惯,成年后要我时时刻刻保持挺拔,会很疲倦。因此在独处的时候、在亲朋好友面前,我偶尔也会放纵自己,站得东倒西歪。

但是在必要的场合、必要的时候,我有自信,绝对不会丢脸地走成一只猩猩——因为我曾经偶然地参与过一次这样的训练。许多年过去了,我仍然清楚地记得把自己的身体绷到什么位置是最好看的。

切记,站姿并不是全部,站姿只是构建完整自信的其中一个环节。

勤剪指甲,就不怕与人握手;勤洗袜子,就不怕脱鞋走进朋友的家门;内裤买好一点,谈新女朋友不怕见胱死;在镜子里研究过自己笑容的弧度,与人合影的时候就不会笑得像刚喝完半杯福尔马林。

5,进阶思考。


许多人反感成年男性过度打扮的原因大致是现代人通常认为男性之美应该体现在男人的社会属性与政治属性之中,而不是流于表面的精致妆容与华丽衣着。

世界上有资格什么事都任性的人是很少的,你没法假设你就是其中一个,在某一方面任性,就得靠自己在其它方面加倍的不任性来买单。没有必要。

作为一个社会人,我们在这个领域的修行不能没完没了,必须要找到一个适合自己的程度停下来。

我会请我能找到的最厉害的发型师来帮我做设计,但是我会拜托他用尽全力地帮我剪成看起来没有花太大功夫去打理就很得体的样子——也就是精致的「不精致」。我会买尽可能高品质的衣服,但是不会有任何一件带着显眼的 logo,不会去追求令人感到血脉偾张的设计感——也就是奢侈的「不奢侈」。因为我希望对自己外表的维护是一个防御性的动作,没有侵略性与攻击性。防御别人对邋遢的不屑,也防御别人对华丽的反感,防御别人对廉价的鄙夷,也防御别人对奢侈的憎恨。

我觉得外在上的自我提升只需要达到让心仪的女孩不反感你的靠近的程度就行,要得到她的心,还是得靠智慧、品格、幽默、无耻、下贱、温柔、爱。等等。另外,最后一个建议:追到她以后就没必要再坚持只穿黑色衣服了。保护好你的「野性」与「童心」,无伤大雅的时候,还是要带着它们出来溜溜。我偶尔戴绿帽子上街、偶尔不穿裙子裸奔,我爸妈都还不知道呢。(恩?)

以上内容来自梁边妖的答案


作者:程序人生
链接:https://mp.weixin.qq.com/s/kgNQv7L9aoZd6r0U0oVjTQ

收起阅读 »

月入五万的西二旗人教你如何活得像月薪五千

--Illustration by Steve Scott西二旗人有一种独特的能力,那就是把明明高薪的日子过得看似很穷。比如我曾经实习过的BAT某司,有一个级别大我很多的前辈,收入至少是五万起步,每天却穿着看似同一件的条纹T恤,踩着个大拖鞋,成天背着手在我们工...
继续阅读 »


--Illustration by Steve Scott

西二旗人有一种独特的能力,那就是把明明高薪的日子过得看似很穷。

比如我曾经实习过的BAT某司,有一个级别大我很多的前辈,收入至少是五万起步,每天却穿着看似同一件的条纹T恤,踩着个大拖鞋,成天背着手在我们工位旁边转悠,乍看上去宛如一个要伺机打扫卫生的保洁大叔。

再比如,我刚来现在这家公司的时候,隔壁部门有个四十来岁的员工,每天斜挎着一个淘宝爆款的背包,骑着一辆破旧的自行车上下班,我当时心中暗想:自己以后一定不要混成这个样子。后来发现,那是他们部门的技术leader,月薪也就大概是我的四五倍吧。

还有,如果你跟西二旗人接触多的话,你会发现,大部分西二旗人理发能选38的最低档就绝不会选58的总监档,出门能坐公交就绝不会打车,能去上地华联解决的购物绝对不会去朝阳大悦城,一个个都是大写的质朴。

所以说,西二旗人简直就是装逼界的一股清流。

在这个浮躁的社会,不知多少人都是月入一万假装月入十万,只有西二旗人,月入十万却过得像是月入几千。

全世界的“收入装逼守恒”,大概都是由西二旗人来守护。

西二旗人具体是怎么月入五万却过得像是月入五千的呢?

  • 比如说穿衣。

穿衣是西二旗人永远的槽点。

月薪五万,在国贸的白领会买几身Armani的西装,每天穿着在去往几十层的电梯里争奇斗艳。

在SOHO工作的创业者,每个月去三里屯买上几件潮牌streetwear,每天不重样换着穿。

月薪五万的西二旗人,一身优衣库,加起来不超过一千,前段时间奢侈了一把,消费升级到了Gap,不能再高了。

你要是说这是西二旗人的低调?对不起,你错了。

一个成功的国贸精英,都是穿得看似低调,其实翻开领口一看,都是几万块的大牌,这才叫低调。

而西二旗人都是穿的看似淘宝货,翻开领口一看,可能真的就是淘宝货。

  • 再比如说吃饭。

爱穿淘宝爆款不要紧,西二旗人每天脑力劳动这么重,吃饭上一定不会亏待自己了吧?

每天中午下馆子?晚上六菜一汤?

不,大部分西二旗人午饭都是在公司食堂搞定。晚饭呢?当然是吃免费的加班餐了。

哦,有时也出去下馆子奢侈一把,比如来到门口的驴肉火烧店,两个火烧加一碗紫菜汤,一算,15块钱。

  • 再再比如说开车。

西二旗那么多期权套现、身家千万的程序员,一定是名车如云吧?

没有红色的法拉利、紫色的兰博基尼、白色的玛莎拉蒂,至少也得是黑色的大众吧?

对不起,西二旗人只有中意两辆车:黄色的ofo和橙色的摩拜。

为什么呢?选择了互联网你还希望能有户口去摇号?

  • 再再再比如说购物。

月薪五万,商场一楼的品牌随便买吧?——对不起,都不认识。

过生日给自己买个奢侈品?——对不起,同事们都不认识。

看这个样子真的就是月薪五六千的样子。

当然,给自己买电子产品的时候西二旗人从不吝啬,几万块的游戏本,转眼就刷了。

*

那么问题来了,西二旗人挣的钱都去哪儿了呢?

1 攒钱买房

攒钱买房是西二旗人的梦想,省钱还贷是西二旗人的信仰。

没有什么能比一套房子给西二旗人更多安全感的。

什么?月薪五万的西二旗人都有一套房了?那有什么,攒钱买下一套呗。

2 买电子产品、手办、玩具

西二旗人的马斯洛需求从低到高是这样的:房子—车子—电子产品。

当一个西二旗人不再操心房子车子,那他就要开始买各种各样的电子产品了。

手机至少苹果、安卓各一个,智能手表必须要有,显示器专挑大个儿的买,还不止一个。

另外,手办这种属于高级玩具,月薪不到五万的西二旗人还是别玩了。

3 攒着

“这么多钱,我也不知道该花到哪里,先干脆攒着吧。”

最后,你要问我为什么西二旗人这么不会生活?

大概在西二旗人眼里,人生的乐趣不需要从生活中获取吧——

编程和工作带来的乐趣,就足够驱动着这群人,一往无前。

作者:景岁
来源:http://mp.weixin.qq.com/s/CX7DViOtcqxFEKsClvMjyw

收起阅读 »

程序员如何高效休息?

过两天,就开始进入长假了。所以趁着摸鱼的时间,把《高效休息法》一口气给读完了。作为一个东方人,大家都会把冥想与宗教联系在一起,比如和尚打坐。而这本书尝试从科学角度来论述冥想并非一种宗教行为,我们更可以从实用主义出发,让它为我们的日常休息做服务。什么是正念冥想?...
继续阅读 »


过两天,就开始进入长假了。所以趁着摸鱼的时间,把《高效休息法》一口气给读完了。

这本书讲的是日本一个女子前往美国留学,中间遭遇挫折,最后在教授的帮助下自我拯救的一个过程。这个故事多多少少有点鸡汤,但其实它是一本以故事的形式介绍正念冥想的书籍。

作为一个东方人,大家都会把冥想与宗教联系在一起,比如和尚打坐。而这本书尝试从科学角度来论述冥想并非一种宗教行为,我们更可以从实用主义出发,让它为我们的日常休息做服务。

那我们程序员作为卷王之王,如何利用正念冥想做到连休息也高效呢?

什么是正念冥想?

这本书提及我们的疲惫包括身体的,还有大脑的。即使我们睡觉的时候,大脑也在高速运转,如果能降低大脑的运转速度,那它就能得到休息。冥想就是通过识别大脑里的念头,然后把注意力回到当下,回到呼吸上,来减少大脑自身的消耗的。

同样的,负面的情绪也是大脑的一种自我消耗。通过冥想,标识负面情绪,给自己正念暗示,回到当下正在做的事情,也可以降低这种消耗。

在一次次的冥想锻炼中,我们的大脑能更积极地应对负面情绪,能更快地进入专注状态,该休息的也能很好地得到休息。

正念冥想就这样让我们从专注当下到放松身心,获得良好的正向循环,从而获得更多的幸福感。

个人经历

说说我个人的经历,我其实是在公司的一次培训课上真实地接触到冥想的。

培训老师上课前,我还以为她会让大家伸伸腰之类的热身运动。没想到的是,她让大家闭上眼静坐。然后让大家跟着她去标记自己杂乱的念头、想法,注意呼吸。

每次培训课前都有这样5-10分钟给我们冥想的时间。

这对我来说算是很新奇的一种体验,虽然以前有听过这种东西,但是自己亲身体验还是很不一样的。起码来说,它破除了我对冥想就是宗教行为的偏见。我虽然没有感动落泪等激动的行为,但是的确有感觉到一种内心的安宁。这种安宁,在我曾经长达半年的早睡早起的日子里体验过。而在冥想时,偶尔也能体会到。

总之,冥想多少减少了我的焦虑,让我更关注于当下。

在这之后,我虽不经常做冥想,但经常在睡前听华为手机的免费的深度睡眠引导,来获得不错的睡眠质量。

五日休息法

最后,再介绍一下这本书提供的一套“五日休息法”:

  • 第一日:偷懒日,什么事都不做

  • 第二日:逛逛附近没去过的地方

  • 第三日:与他人联系

  • 第四日:放纵日(要做好预算)

  • 第五日:盘点,并规划下一次休息

这个长假,如果你们有兴趣,也可以尝试一下这个“五日休息法"。如果无法做到五天,这里的“偷懒日”也是一个很不错的休息方案。

在这一天,放下手机,什么事都不做,去户外走走即可。

祝大家都能过一个轻松愉快的假期!

作者:陈佬昔没带相机
来源:
https://juejin.cn/post/7057558464066912263

收起阅读 »

本着什么原则,才能写出优秀的代码?

作为一名程序员,最不爱干的事情,除了开会之外,可能就是看别人的代码。有的时候,新接手一个项目,打开代码一看,要不是身体好的话,可能直接气到晕厥。风格各异,没有注释,甚至连最基本的格式缩进都做不到。这些代码存在的意义,可能就是为了证明一句话:又不是不能跑。在这个...
继续阅读 »

作为一名程序员,最不爱干的事情,除了开会之外,可能就是看别人的代码。

有的时候,新接手一个项目,打开代码一看,要不是身体好的话,可能直接气到晕厥。

风格各异,没有注释,甚至连最基本的格式缩进都做不到。这些代码存在的意义,可能就是为了证明一句话:又不是不能跑。

在这个时候,大部分程序员的想法是:这烂代码真是不想改,还不如直接重写。

但有的时候,我们看一些著名的开源项目时,又会感叹,代码写的真好,优雅。为什么好呢?又有点说不出来,总之就是好。

那么,这篇文章就试图分析一下好代码都有哪些特点,以及本着什么原则,才能写出优秀的代码。

初级阶段

先说说比较基本的原则,只要是程序员,不管是高级还是初级,都会考虑到的。

img

这只是列举了一部分,还有很多,我挑选四项简单举例说明一下。

  1. 格式统一

  2. 命名规范

  3. 注释清晰

  4. 避免重复代码

以下用 Python 代码分别举例说明:

格式统一

格式统一包括很多方面,比如 import 语句,需要按照如下顺序编写:

  1. Python 标准库模块

  2. Python 第三方模块

  3. 应用程序自定义模块

然后每部分间用空行分隔。

import os
import sys

import msgpack
import zmq

import foo
复制代码

再比如,要添加适当的空格,像下面这段代码;

i=i+1
submitted +=1
x = x*2 - 1
hypot2 = x*x + y*y
c = (a+b) * (a-b)
复制代码

代码都紧凑在一起了,很影响阅读。

i = i + 1
submitted += 1
x = x * 2 - 1
hypot2 = x * x + y * y
c = (a + b) * (a - b)
复制代码

添加空格之后,立刻感觉清晰了很多。

还有就是像 Python 的缩进,其他语言的大括号位置,是放在行尾,还是另起新行,都需要保证统一的风格。

有了统一的风格,会让代码看起来更加整洁。

命名规范

好的命名是不需要注释的,只要看一眼命名,就能知道变量或者函数的作用。

比如下面这段代码:

a = 'zhangsan'
b = 0
复制代码

a 可能还能猜到,但当代码量大的时候,如果满屏都是 abcd,那还不得原地爆炸。

把变量名稍微改一下,就会使语义更加清晰:

username = 'zhangsan'
count = 0
复制代码

还有就是命名要风格统一。如果用驼峰就都用驼峰,用下划线就都用下划线,不要有的用驼峰,有点用下划线,看起来非常分裂。

注释清晰

看别人代码的时候,最大的愿望就是注释清晰,但在自己写代码时,却从来不写。

但注释也不是越多越好,我总结了以下几点:

  1. 注释不限于中文或英文,但最好不要中英文混用

  2. 注释要言简意赅,一两句话把功能说清楚

  3. 能写文档注释应该尽量写文档注释

  4. 比较重要的代码段,可以用双等号分隔开,突出其重要性

举个例子:

# =====================================
# 非常重要的函数,一定谨慎使用 !!!
# =====================================

def func(arg1, arg2):
   """在这里写函数的一句话总结(如: 计算平均值).

  这里是具体描述.

  参数
  ----------
  arg1 : int
      arg1的具体描述
  arg2 : int
      arg2的具体描述

  返回值
  -------
  int
      返回值的具体描述

  参看
  --------
  otherfunc : 其它关联函数等...

  示例
  --------
  示例使用doctest格式, 在`>>>`后的代码可以被文档测试工具作为测试用例自动运行

  >>> a=[1,2,3]
  >>> print [x + 3 for x in a]
  [4, 5, 6]
  """
复制代码

避免重复代码

随着项目规模变大,开发人员增多,代码量肯定也会增加,避免不了的会出现很多重复代码,这些代码实现的功能是相同的。

虽然不影响项目运行,但重复代码的危害是很大的。最直接的影响就是,出现一个问题,要改很多处代码,一旦漏掉一处,就会引发 BUG。

比如下面这段代码:

import time


def funA():
   start = time.time()
   for i in range(1000000):
       pass
   end = time.time()

   print("funA cost time = %f s" % (end-start))


def funB():
   start = time.time()
   for i in range(2000000):
       pass
   end = time.time()

   print("funB cost time = %f s" % (end-start))


if __name__ == '__main__':
   funA()
   funB()
复制代码

funA()funB() 中都有输出函数运行时间的代码,那么就适合将这些重复代码抽象出来。

比如写一个装饰器:

def warps():
   def warp(func):
       def _warp(*args, **kwargs):
           start = time.time()
           func(*args, **kwargs)
           end = time.time()
           print("{} cost time = {}".format(getattr(func, '__name__'), (end-start)))
       return _warp
   return warp
复制代码

这样,通过装饰器方法,实现了同样的功能。以后如果需要修改的话,直接改装饰器就好了,一劳永逸。

进阶阶段

当代码写时间长了之后,肯定会对自己有更高的要求,而不只是格式注释这些基本规范。

但在这个过程中,也是有一些问题需要注意的,下面就来详细说说。

炫技

第一个要说的就是「炫技」,当对代码越来越熟悉之后,总想写一些高级用法。但现实造成的结果就是,往往会使代码过度设计。

这不得不说说我的亲身经历了,曾经有一段时间,我特别迷恋各种高级用法。

有一次写过一段很长的 SQL,而且很复杂,里面甚至还包含了一个递归调用。有「炫技」嫌疑的 Python 代码就更多了,往往就是一行代码包含了 N 多魔术方法。

然后在写完之后漏出满意的笑容,感慨自己技术真牛。

结果就是各种被骂,更重要的是,一个星期之后,自己都看不懂了。

img

其实,代码并不是高级方法用的越多就越牛,而是要找到最适合的。

越简单的代码,越清晰的逻辑,就越不容易出错。而且在一个团队中,你的代码并不是你一个人维护,降低别人阅读,理解代码的成本也是很重要的。

脆弱

第二点需要关注的是代码的脆弱性,是否细微的改变就可能引起重大的故障。

代码里是不是充满了硬编码?如果是的话,则不是优雅的实现。很可能导致每次性能优化,或者配置变更就需要修改源代码。甚至还要重新打包,部署上线,非常麻烦。

而把这些硬编码提取出来,设计成可配置的,当需要变更时,直接改一下配置就可以了。

再来,对参数是不是有校验?或者容错处理?假如有一个 API 被第三方调用,如果第三方没按要求传参,会不会导致程序崩溃?

举个例子:

page = data['page']
size = data['size']
复制代码

这样的写法就没有下面的写法好:

page = data.get('page', 1)
size = data.get('size', 10)
复制代码

继续,项目中依赖的库是不是及时升级更新了?

积极,及时的升级可以避免跨大版本升级,因为跨大版本升级往往会带来很多问题。

还有就是在遇到一些安全漏洞时,升级是一个很好的解决办法。

最后一点,单元测试完善吗?覆盖率高吗?

说实话,程序员喜欢写代码,但往往不喜欢写单元测试,这是很不好的习惯。

有了完善,覆盖率高的单元测试,才能提高项目整体的健壮性,才能把因为修改代码带来的 BUG 的可能性降到最低。

重构

随着代码规模越来越大,重构是每一个开发人员都要面对的功课,Martin Fowler 将其定义为:在不改变软件外部行为的前提下,对其内部结构进行改变,使之更容易理解并便于修改。

重构的收益是明显的,可以提高代码质量和性能,并提高未来的开发效率。

但重构的风险也很大,如果没有理清代码逻辑,不能做好回归测试,那么重构势必会引发很多问题。

这就要求在开发过程中要特别注重代码质量。除了上文提到的一些规范之外,还要注意是不是滥用了面向对象编程原则,接口之间设计是不是过度耦合等一系列问题。

那么,在开发过程中,有没有一个指导性原则,可以用来规避这些问题呢?

当然是有的,接着往下看。

高级阶段

最近刚读完一本书,Bob 大叔的《架构整洁之道》,感觉还是不错的,收获很多。

img

全书基本上是在描述软件设计的一些理论知识。大体分成三个部分:编程范式(结构化编程、面向对象编程和函数式编程),设计原则(主要是 SOLID),以及软件架构(其中讲了很多高屋建翎的内容)。

总体来说,这本书中的内容可以让你从微观(代码层面)和宏观(架构层面)两个层面对整个软件设计有一个全面的了解。

其中 SOLID 就是指面向对象编程和面向对象设计的五个基本原则,在开发过程中适当应用这五个原则,可以使软件维护和系统扩展都变得更容易。

五个基本原则分别是:

  1. 单一职责原则(SRP)

  2. 开放封闭原则(OCP)

  3. 里氏替换原则(LSP)

  4. 接口隔离原则(ISP)

  5. 依赖倒置原则(DIP)

单一职责原则(SRP)

A class should have one, and only one, reason to change. – Robert C Martin

一个软件系统的最佳结构高度依赖于这个系统的组织的内部结构,因此每个软件模块都有且只有一个需要被改变的理由。

这个原则非常容易被误解,很多程序员会认为是每个模块只能做一件事,其实不是这样。

举个例子:

假如有一个类 T,包含两个函数,分别是 A()B(),当有需求需要修改 A() 的时候,但却可能会影响 B() 的功能。

这就不是一个好的设计,说明 A()B() 耦合在一起了。

开放封闭原则(OCP)

Software entities should be open for extension, but closed for modification. – Bertrand Meyer, Object-Oriented Software Construction

如果软件系统想要更容易被改变,那么其设计就必须允许新增代码来修改系统行为,而非只能靠修改原来的代码。

通俗点解释就是设计的类对扩展是开放的,对修改是封闭的,即可扩展,不可修改。

看下面的代码示例,可以简单清晰地解释这个原则。

void DrawAllShape(ShapePointer list[], int n)
{
int i;
for (i = 0; i < n; i++)
{
struct Shape* s = list[i];
switch (s->itsType)
{
case square:
DrawSquare((struct Square*)s);
break;
case circle:
DrawSquare((struct Circle*)s);
break;
default:
break;
}
}
}
复制代码

上面这段代码就没有遵守 OCP 原则。

假如我们想要增加一个三角形,那么就必须在 switch 下面新增一个 case。这样就修改了源代码,违反了 OCP 的封闭原则。

缺点也很明显,每次新增一种形状都需要修改源代码,如果代码逻辑复杂的话,发生问题的概率是相当高的。

class Shape
{
public:
virtual void Draw() const = 0;
}

class Square: public Shape
{
public:
virtual void Draw() const;
}

class Circle: public Shape
{
public:
virtual void Draw() const;
}

void DrawAllShapes(vector<Shape*>& list)
{
vector<Shape*>::iterator I;
for (i = list.begin(): i != list.end(); i++)
{
(*i)->Draw();
}
}
复制代码

通过这样修改,代码就优雅了很多。这个时候如果需要新增一种类型,只需要增加一个继承 Shape 的新类就可以了。完全不需要修改源代码,可以放心扩展。

里氏替换原则(LSP)

Require no more, promise no less.– Jim Weirich

这项原则的意思是如果想用可替换的组件来构建软件系统,那么这些组件就必须遵守同一个约定,以便让这些组件可以相互替换。

里氏替换原则可以从两方面来理解:

第一个是继承。如果继承是为了实现代码重用,也就是为了共享方法,那么共享的父类方法就应该保持不变,不能被子类重新定义。

子类只能通过新添加方法来扩展功能,父类和子类都可以实例化,而子类继承的方法和父类是一样的,父类调用方法的地方,子类也可以调用同一个继承得来的,逻辑和父类一致的方法,这时用子类对象将父类对象替换掉时,当然逻辑一致,相安无事。

第二个是多态,而多态的前提就是子类覆盖并重新定义父类的方法。

为了符合 LSP,应该将父类定义为抽象类,并定义抽象方法,让子类重新定义这些方法。当父类是抽象类时,父类就是不能实例化,所以也不存在可实例化的父类对象在程序里,也就不存在子类替换父类实例(根本不存在父类实例了)时逻辑不一致的可能。

举个例子:

看下面这段代码:

class A{
public int func1(int a, int b){
return a - b;
}
}

public class Client{
public static void main(String[] args){
A a = new A();
System.out.println("100-50=" + a.func1(100, 50));
System.out.println("100-80=" + a.func1(100, 80));
}
}
复制代码

输出;

100-50=50
100-80=20
复制代码

现在,我们新增一个功能:完成两数相加,然后再与 100 求和,由类 B 来负责。即类 B 需要完成两个功能:

  1. 两数相减

  2. 两数相加,然后再加 100

现在代码变成了这样:

class B extends A{
public int func1(int a, int b){
return a + b;
}

public int func2(int a, int b){
return func1(a,b) + 100;
}
}

public class Client{
public static void main(String[] args){
B b = new B();
System.out.println("100-50=" + b.func1(100, 50));
System.out.println("100-80=" + b.func1(100, 80));
System.out.println("100+20+100=" + b.func2(100, 20));
}
}
复制代码

输出;

100-50=150
100-80=180
100+20+100=220
复制代码

可以看到,原本正常的减法运算发生了错误。原因就是类 B 在给方法起名时重写了父类的方法,造成所有运行相减功能的代码全部调用了类 B 重写后的方法,造成原本运行正常的功能出现了错误。

这样做就违反了 LSP,使程序不够健壮。更通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖、聚合,组合等关系代替。

接口隔离原则(ISP)

Clients should not be forced to depend on methods they do not use. –Robert C. Martin

软件设计师应该在设计中避免不必要的依赖。

ISP 的原则是建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法要尽量少。

也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。

在程序设计中,依赖几个专用的接口要比依赖一个综合的接口更灵活。

单一职责与接口隔离的区别:

  1. 单一职责原则注重的是职责;而接口隔离原则注重对接口依赖的隔离。

  2. 单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节; 而接口隔离原则主要约束接口。

举个例子:

img

首先解释一下这个图的意思:

「犬科」类依赖「接口 I」中的方法:「捕食」,「行走」,「奔跑」; 「鸟类」类依赖「接口 I」中的方法「捕食」,「滑翔」,「飞翔」。

「宠物狗」类与「鸽子」类分别是对「犬科」类与「鸟类」类依赖的实现。

对于具体的类:「宠物狗」与「鸽子」来说,虽然他们都存在用不到的方法,但由于实现了「接口 I」,所以也 必须要实现这些用不到的方法,这显然是不好的设计。

如果将这个设计修改为符合接口隔离原则的话,就必须对「接口 I」进拆分。

img

在这里,我们将原有的「接口 I」拆分为三个接口,拆分之后,每个类只需实现自己需要的接口即可。

依赖倒置原则(DIP)

High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.– Robert C. Martin

高层策略性的代码不应该依赖实现底层细节的代码。

这话听起来就让人听不明白,我来翻译一下。大概就是说在写代码的时候,应该多使用稳定的抽象接口,少依赖多变的具体实现。

举个例子:

看下面这段代码:

public class Test {

public void studyJavaCourse() {
System.out.println("张三正在学习 Java 课程");
}

public void studyDesignPatternCourse() {
System.out.println("张三正在学习设计模式课程");
}
}
复制代码

上层直接调用:

public static void main(String[] args) {
Test test = new Test();
test.studyJavaCourse();
test.studyDesignPatternCourse();
}
复制代码

这样写乍一看并没有什么问题,功能也实现的好好的,但仔细分析,却并不简单。

第一个问题:

如果张三又新学习了一门课程,那么就需要在 Test() 类中增加新的方法。随着需求增多,Test() 类会变得非常庞大,不好维护。

而且,最理想的情况是,新增代码并不会影响原有的代码,这样才能保证系统的稳定性,降低风险。

第二个问题:

Test() 类中方法实现的功能本质上都是一样的,但是却定义了三个不同名字的方法。那么有没有可能把这三个方法抽象出来,如果可以的话,代码的可读性和可维护性都会增加。

第三个问题:

业务层代码直接调用了底层类的实现细节,造成了严重的耦合,要改全改,牵一发而动全身。

基于 DIP 来解决这个问题,势必就要把底层抽象出来,避免上层直接调用底层。

img

抽象接口:

public interface ICourse {
void study();
}
复制代码

然后分别为 JavaCourseDesignPatternCourse 编写一个类:

public class JavaCourse implements ICourse {

@Override
public void study() {
System.out.println("张三正在学习 Java 课程");
}
}

public class DesignPatternCourse implements ICourse {

@Override
public void study() {
System.out.println("张三正在学习设计模式课程");
}
}
复制代码

最后修改 Test() 类:

public class Test {

   public void study(ICourse course) {
       course.study();
  }
}
复制代码

现在,调用方式就变成了这样:

public static void main(String[] args) {
   Test test = new Test();
   test.study(new JavaCourse());
   test.study(new DesignPatternCourse());
}
复制代码

通过这样开发,上面提到的三个问题得到了完美解决。

其实,写代码并不难,通过什么设计模式来设计架构才是最难的,也是最重要的。

所以,下次有需求的时候,不要着急写代码,先想清楚了再动手也不迟。

这篇文章写的特别辛苦,主要是后半部分理解起来有些困难。而且有一些原则也确实没有使用经验,单靠文字理解还是差点意思,体会不到精髓。

其实,文章中的很多要求我都做不到,总结出来也相当于是对自己的一个激励。以后对代码要更加敬畏,而不是为了实现功能草草了事。写出健壮,优雅的代码应该是每个程序员的目标,与大家共勉。


作者:yongxinz
链接:https://mp.weixin.qq.com/s/xWZmP4qBI8cm68UZH6AXOg

收起阅读 »

中年程序员写给35岁的自己

笔者是一名程序员老司机,局限于笔者文笔一般,想到哪写到哪__胡乱写一通,篇幅较长,希望通过文章的方式简单的回顾过去、总结现在和展望未来,顺便记录一下,方便以后总结。开端最近刚刷完《开端》这个电视剧,感慨万千,男女主在一次次的循环里,逐步完善信息,排除每个人的嫌...
继续阅读 »

笔者是一名程序员老司机,局限于笔者文笔一般,想到哪写到哪__胡乱写一通,篇幅较长,希望通过文章的方式简单的回顾过去、总结现在和展望未来,顺便记录一下,方便以后总结。

开端

最近刚刷完《开端》这个电视剧,感慨万千,男女主在一次次的循环里,逐步完善信息,排除每个人的嫌疑,最终找到阻止高压锅爆炸的办法。

人生亦如此,经过一次次的失败之后,最终都会到达自己的目的,结局有好有坏罢了。

人生的每一次的总结,其实就是下一次循环的起点。

时间都去哪了

一晃神自己都已经35了,虽然不愿意承认,但是时间就这么一点一滴地过去了。

记得09年毕业刚工作的时候,听着同事们谈论买房、炒股、养娃的事情,当时感觉这个事情离自己是多么的遥远,但现在看来真的就好像是昨天发生的事情,当时的情景还很清晰的印在脑海里,甚至他们谈论时候的表情、语气、肢体语言都还清晰的记得。

自从过了30岁以后,每一年过生日都会比较感慨,总是会问一下自己,这一年的时间都去哪了呢,如果这一年可以重新循环一次,自己又会怎样去选择。

回想

2022年,一个充满2的一年,自己的第14个工作年,回想我这14年的工作经历,有得有失,有太多的心酸。

第一个5年可以说是比较顺利的,从一个小兵,通过自己的全身心地投入,逐渐成长为一个小组的负责人。

前面为第二个5年奠定了一个新的起点,并在新的角色上继续沉淀自己的技术和团队管理。用最近的话说,也是一路“卷”过来的。

第3个5年比较坎坷,自己并不满意,未能达成自己期望的目标,时间也耗费过去了,有遗憾、有决策失误、有不甘心,但不能就此认命,不能后悔,因为未来的路上仍需要全力以赴。

地下室

09年7月,一毕业就自己拖着一个行李箱,坐上了开往北京的火车,开启了北漂之旅。

大学期间就听说北京的程序员很好找工作,一个月能够拿到1万5的高薪,当时北京的房价估计也就1万不到,当时就想自己抓紧学习,也能早点拿到1万5的薪水,所以坚定地选择了北漂。

初生牛犊不怕虎,拿着家里最后一次给的3千块钱,工作和住宿都还没有着落,还好当时的北京地下室很流行,都是给很多想要来北京打拼,但是又支付不起高昂的房租的人准备的,里面很多像我这样的年轻人,还有起早贪黑做路边早餐生意的摊贩,所以我很有幸成为了其中的一员。

找工作也算比较顺利,是来北京之前,在技术论坛里联系的一个版主老闫,来北京第二天就去他公司面试了,当着老闫和宫姐(后来才知道,宫姐是老闫的媳妇,从国企离职了,老闫专门为她开的一个公司)的面用PHP写了一个表单上传界面,直接就被录取了,对于没有经验的我来说,这是对我很大的肯定。

之后就踏实的开启了一年的地下室-公司两点一线的生活,早晨从负三层走上地面,每天从黑暗走向光明,出门挤公交到公司,晚上接着回去地下三层,只有2-4平米的地下室,没有网络,没有娱乐,偶尔去网吧和当时的女朋友(现在的老婆)视频,这一年非常充实,2000块钱工资仅够生活,周末加一天班还能多拿几百,但是精力充沛,学习为主,非常充实。

到现在我还很怀念这一年经历,虽然已经很久没有联系过老闫和宫姐了,非常感谢能有这一段工作经历,教会了我很多。

养成强大的内心

为什么要拥有一颗强大的内心?因为无论是工作中,还是生活中,肯定会遇到各式各样的困难与挫折,如果轻而易举的就被压垮,轻而易举的说放弃,那如何能够在职业道路上走的更远,又怎样担负起自己的家庭的责任。

头一两年工作的时候,其实很多不懂的东西,只能靠自己不断的百度和Google,所以经常会晚上经常梦到自己程序写不下去的情况,压力很大,特别是接到一个比较大的项目的时候,晚上还会睡不着。

但这些年通过项目的历练,和与更优秀的人共事,逐渐让自己养成了强大的内心。当你需要同时肩负家庭与工作的双重压力时,如果内心不够强大的话,崩溃或者抑郁就会找上来,每个人都是一步步走过来的,放眼我们这代人,我接触的人当中都是非常皮实,都是在一次次被压垮崩溃之后,自我调节又振作起来,让自己的内心逐渐的强大起来。

我在给团队招人的时候,优先会考察候选人皮实、乐观这两个方面。

“皮实”的反义词是“玻璃心”,是人都会犯错,都会有迷茫的时候,皮实的人能够听得进去别人的建议,并且把压力化解为动力,从而逐渐承担更多的职责。

乐观的人不仅能够带动组内的气氛,还是个积极向上的人,不会轻易否定自己,经过适当的培养和引导,很快就能成为一个独当一面的角色。

个人成长

我最精力旺盛最能拼搏的阶段是在优酷,和后来的阿里大文娱,快7年的时间。在这段时间,买房、结婚、生娃,可以说是把最重要的事情都做了,这段时间里从一个小兵,踏实的完成好领导安排的每一项任务,把自己当成owner的角色要求自己,到后来晋升为技术leader,也正好经历了互联网最辉煌的那几年。

非常喜欢优酷的文化氛围,合作第一,一人一口,很怀念和当时的同事们一起共事的经历。

这7年里收获了许多做事情的方式和方法,跟对了靠谱的领导,也结识了很多优秀的同事。总结下来就是,强大的内心,真诚待人,踏实稳重,厚积薄发

创业

有创业的念头是在第二个5年的后半段,当时事业上进入了舒适区,也可以说是瓶颈期,感觉安逸下来就会落后。所以在挣扎了比较长的一段时间后,放弃了留在阿里拿着股票修福报的机会,裸辞寻找创业机会(这个决定其实是有点头脑发热的,在选择的话,估计会骑驴找马),同时照顾也想参与一下娃的成长阶段。

我所说的创业,并不是大家想象中的,自己当老板那种创业,我期望的是能跟着一个早期的业务,经历一步步的做大做强的过程,也就是0-1的过程,从而能够更加的皮实自己。

所以后来创业经历过短视频、区块链、民宿这些业务的,项目从0-1的过程是我最大的收获,投入度和抗压能力,这个是创业必备的,只要能看着数据库的用户数据一天比一天成倍的增长,能验证项目的价值,这就是创业能坚持下去的动力,虽然最想要的“钱”没到位。

创业都会有风险,周期长,充满不确定性,会受政策和资金的影响,有成功有失败,这一点在从阿里出来的时候,就已经做好了心理准备,这也是经历了多个项目的原因,1-10 这个阶段不是努力就能达到的,天时地利人和一个都少不了。

未来的世界总是在不断的变化,谁也没能想到2019年“新冠”这个病毒就这样默默的流行起来了,这是所有人人生计划当中的一个很大的变数,疫情下面一切都变得不再那么重要,活着最重要,家人健康最重要。所以创业的计划也就没法继续下去了,还是选择先回归到了大厂继续“卷”吧,毕竟还有家庭的责任需要担起来,希望人类战胜疫情的那一天早日到来。

技术

技术是程序员吃饭的工具,也可以说是本能,任何时候都应该保持这种本能,所以不断更新自己的技术储备,是作为一个程序员的必修课。

危机感

活到老学到老,这个应该不是只有程序员这个工种需要做的事情,任何岗位都应该如此(机关单位除外),在这个竞争如此激烈的环境里,不进则退,危机感是每个人都应该时刻保持的状态,除非你真的可以做到躺平。

一线编程是时刻都要保持的能力,再忙也还是需要抽出时间去练习巩固,架构能力固然重要,落地能力也很重要。

希望等我50、60岁的时候,也还能够保持技术的能力,虽然不知道未来的世界会是怎样,但秉承内心即可。

学会正确的做事

工作到了一定阶段,只要是技术能够搞定的事情,那都是最简单的工作。想要把技术做好,真正实施之前,先要透过现象看清事物的本质,谋定而后动厚积而薄发。这个也是和之前的领导那里学到的,所以“和优秀的人共事,你也会变得优秀”

培养做事的方式方法,列提纲,想清楚,写明白,凡事都要写下来,然后印在脑海里。

以下是我习惯的流程提纲,能完善好其中的每个点,我认为是能够在工作中做到应对自如的:

事件分析
背景调查
人物关系
痛点梳理
方向确立
制定短期、中期、长期目标
详细方案
时间线
过程跟踪
阶段复盘,调整方向
复制代码

正确看待技术的更新迭代

我现在虽然是做着最前端的工作,但是我并不仅仅把自己定义为前端工程师这个角色,我不想把自己局限在某个设定好的框架里。同样头几年做后端的时候,到后来让我带领H5团队的时候,我并没有觉得这个是一个不好的选择,相反正因为有了后端的经历,才让我在前端这个领域上持续深耕,更加的从容和淡定。

时代在变,人也在变,技术也在变,技术总是不断地更新迭代,作为研发,需要有一颗进取的心,努力跟上时代的潮流,而不是被后浪拍死在沙滩上。

就拿前端来说,我最早接触的框架是雅虎的Yui,后来到jQuery,再到后来nodejs的出现,使得前端技术有了爆炸式的增长和变化,所以才有了Angular、React、Vue等这些框架的出现。

这几年前端领域就出现了很多新名词新框架,比如跨端、severless、lowcode、D2C、P2C、esbuild、Flutter等等,这些名词背后的核心,其实还是围绕着如何提升研发效能,如何最大化研发效率,如何优化运行性能展开的。所以无论技术如何的变化,事物的本质并没有变,而是我们通过不断的研究和探索,不断的找到了更优的解决办法,从而替代之前的架构更好更快的达成目标。

摆正姿态,正确对待技术的更新迭代,同时也要不断的把自身的基建打牢,打通任督二脉,这样无论来什么武功都能融会贯通。

某一方面的技术专家

“技术专家”这个词,我个人理解的是:在某个技术领域里,不仅有业界影响力,而且技术的深度也是非常厉害,能够利用所掌握的技能,完美解决工作中的各种疑难杂症,带着团队其他人一起进步。

不得不承认,我自己并没有达到自己所想的预期,虽然顶着技术专家的头衔,但技术深度远远不够的,而且业界也确实没啥影响力啊,有点惭愧,未来任重道远啊。

这么多年技术语言接触了不少,比如PHP、Python、Java、Javascript、go,但是似乎每一项都是点到为止,都是为了解决当下业务痛点,临时上手学习,并且运用到实际的业务中,之后再没有更深入的去研究了。除了前端,这个是因为真的感兴趣,所以一直也在持续的关注和投入,所以未来应该还是会在这个方向上一直走下去,结合实际的业务更加的深入,争取达到自己内心的期望。

焦虑

说一下“焦虑”,也可以说是“中年危机”,带来的焦虑,最近正好在看一本书《认知觉醒:开启自我改变的原动力》,作者写下这段话的时候,正好36岁:

在很长一段时间内,我就像一个没有睡醒的人,对自己不了解,对生活没主张,对命运无选择。那时的我,虽然对本职工作非常投入,但业余时间几乎被不需要动脑筋的事情占据:有空就找朋友们聚会,时常喝到烂醉;经常熬夜,从不主动看书、运动;打发时间的方式就是看搞笑视频、读八卦新闻、玩手机游戏;实在没事可做,就裹起被子睡大觉……下意识中,我觉得这种无忧的生活会继续下去。

人之所以会焦虑,是因为每个人都会有自己的期望,当担心这个期望无法达成,亦或者短时间内无法达成的时候,人就会产生一种焦虑的情绪。

作为一个35岁的码农,很自然地会把自己和“中年危机”关联到一起。

因为担心自己精力不够,无法胜任高强度的工作。

因为担心自己思维不再灵活,无法做到全面地思考。

因为担心自己家庭原因,无法平衡家庭与工作。

因为担心自己期望过高,无法得到很好的上升空间。

因为担心自己失业,无法再承担起家庭的责任。

非常害怕迷迷糊糊地到了某个年纪,突然发现自己对这个世界已经无能为力了:梦想与现实落差巨大,生活和工作压力缠身,而优秀的同龄人已绝尘而去。一时间,焦虑急躁又如梦初醒:

“为什么没有早点知道这个世界的真相?”

“为什么没有在最好的年纪及时觉醒?”

但即使含泪拷问,也似乎错过了最佳时机,毕竟人生是个单行道,无法从头再来。

最后不得不敲碎那颗高傲的心,在无奈和叹息中默默接受平庸的人生。

我接下来要做的,就是努力找到一个平衡,学会与焦虑共存,有欲望就会有焦虑,比如看书、写文章、利用好业余时间充实自己,当自己足够强大的时候,或许就能进入另外一种心境。

工作与家庭

新的工作

自己已经在新的岗位上满一年了,这一年在地图的领域上学到了不少新的知识,跟着优秀的人一起共事,很幸运能和渲染大牛一起推进,地图上的数据可视化引擎,围绕着webgl,将时空数据落地到地理三维空间上,从调研,到研发,到落地,这个过程需要吸收很多新鲜的名词,比如墨卡托、瓦片、Geojson等,并且真正落地到了具体的代码上。

一年的工作非常快,在适应和彷徨中,就这么过去了,甚至还没来得及思考总结,实际产出距离自己的期望还有很大的差距,和政策、和业务、和投入度也存在一些关系,但技术上也确实得到了不少新的积累。

娃上学

今年恰巧赶上大娃上小学,小娃上小班,所以可能更多时间也确实是投入到了家庭上,最痛苦的莫过于,不知道如何教育孩子。这个年代的小孩和当初小时候的我们,思维和眼界都有很大的提升,所以无形中会把自己小时候未完成的期望强加到自己的儿女身上,这样反而带来了自己更多的焦虑。

感谢媳妇,感谢两个妈一直照顾孩子,不然真是难以平衡工作与家庭,但是教育和健康方面,还是需要我和媳妇自己多上心,包括自己的健康、儿女的健康、老人的健康。

自我认知

如果你觉得自己已经错过所谓的最好年纪,其实也没有关系,因为“现在”永远都是开始的最好时机——这不是什么安慰人的话,这是事实。“摩西奶奶”76岁开始学画、80岁举办个人展,王德顺79岁走上T台,褚时健74岁开始创业种橙子……就算你今年60岁,他们仍可以对你说:“孩子,别着急,你至少还有20年可以随时重来……”

如文章开头的大图,在一片金黄色的麦田里,一个小女孩牵着气球在里面行走,金黄色的麦穗是前35年的积累,绿色的气球代表对未来充满希望,我把自己看成了图中的小女孩,希望自己能在一个全新的世界重新学习,重新认识自己,找到一条全新的通往未来的道路。

沟通

我是一个慢热型的人,在成为leader之前,我其实还是典型的程序员性格,只愿意和代码打交道,所以也不会主动去结交别人,完全沉浸在自己的世界里。

成为leader之后,自己由内向主动向外向靠拢,逼着自己主动去和别人交流,为了团队,为了自己,努力创造共同的话题,从不排斥参加任何社交活动。

独立思考

这一点上做得不是很够,很多时候还是会照顾其他人的感受,所以还是应该多问为什么,多想想还有没有更好的答案。

情绪管理

性格温和,待人真诚,工作中除了有几次为了团队内的同学受委屈,和其他的团队leader红过脸外,基本上没有因为工作的事情发过脾气。

耐心

这点需要深深的反思和检讨,在教育小孩这件事情上,已经没有耐心可言了,经常就会因为孩子事情莫名的烦躁,特别是工作压力大的时候,心中要默念一万遍“亲生的”,才能慢慢压制心中的火气,有时候确实还会忍不住揍娃。

气场

应该没有啥气场吧,当leader也好,当大头兵也好,我一贯的作风都是,和气生财,所以也不会表现出太大的气场去压制别人。

未来的自己

希望自己能够更加的有主见,承担起更多的工作,承担起更多家庭的责任,合理利用自己的碎片时间,把时间都花在有价值的地方。找到一种减压的方式,不发脾气,认真享受生活,结交更多的人脉。

明年的Flag

目标不宜过多,这些目标都是业余时间完成。

读书

每个月一本书,一年完成至少10本书的学习计划,学以致用,而不是读完就忘。

写文章

一周完成一篇原创文章,不限类别

早睡早起

每天不晚于11:30休息

总结

总共6千多字的篇幅,以散文的形式,写给35岁的自己,简单地回顾过去、总结现在、展望未来,希望当36岁的自己回过头来看的时候,能够鄙视现在的自己,写出更好的《写给36岁的自己》。


作者:唐小锅
来源:https://juejin.cn/post/7058907526842023973

收起阅读 »

十分钟搞懂手机号码一键登录

手机号码一键登录是最近两三年出现的一种新型应用登录方式,比之前常用的短信验证码登录又方便了不少。登陆时,应用首先向用户展示带有本机号码掩码的授权登录页面,用户点击“同意授权”的按钮之后,应用即可获取到完整的本机号码,从而完成用户的登录认证。在这个过程中,应用只...
继续阅读 »

手机号码一键登录是最近两三年出现的一种新型应用登录方式,比之前常用的短信验证码登录又方便了不少。登陆时,应用首先向用户展示带有本机号码掩码的授权登录页面,用户点击“同意授权”的按钮之后,应用即可获取到完整的本机号码,从而完成用户的登录认证。在这个过程中,应用只要确认登录用的手机号码是在绑定了此号码的手机上发起的即可认证成功,从这一点来看,它和短信验证码登录并无本质区别,都是一种设备认证登录方式。这篇文章就来捋一下其中的技术门道。

这几年为了保护用户的隐私安全,Android和iOS系统都限制了应用获取本机号码的能力,即使通过某些技术手段获取到了本机号码,这个号码还可能是被篡改的,所以应用直接读取本机号码用于登录是不可行的。那么这些应用是怎么获取到真实的本机号码的呢?答案是电信运营商,手机要打电话、要上网、要计费,运营商肯定能对应到正确的手机号码。国内的运营商就是移动、联通、电信这三家,它们都开放了这种能力。对于在互联网大潮中被管道化的运营商来说,不失为一种十分有意义的积极进取。

手机流量上网的原理

手机号码一键登录是借助手机流量上网来实现,所以先要搞清楚流量上网的原理。

目前网上已有很多关于一键登录的技术文章,但是内容基本雷同,关于获取手机号码的部分,所述都是通过运营商的数据网关能力,语焉不详,对于有追求的技术人来说,难以忍受。这个章节就来介绍下这种从数据网关获取手机号码的能力是如何实现的,因为通信专业知识十分繁杂,我也没有经过专业的学习,大家也不想接触到很多的专业名词,所以这里只保留一些关键的专业名词,尽量以通俗易懂的方式来理清这个机制。

五层网络模型

对网络比较熟悉的同学,应该了解五层协议,那么手机流量上网时的五层网络模型有何不同呢?


从上图可以看出,手机流量上网的主要区别在数据链路层和物理层。在数据链路层,流量上网没有MAC地址的概念,它采用一种点对点协议(PPP),手机端通过拨号方式建立这种PPP连接,然后发送数据。在物理层,流量上网通过手机内置的基带模块进行无线信号的调制、解调工作,从而实现与移动基站之间的电磁波通信。

流量上网的机制

点对点协议支持身 验证功能,手机端发起连接时会携带自己的身 粉证明,一般就是手机卡内置的IMSI,这个IMSI也会保存在运营商的数据库中,因此基站就可以验证连接用户的身 ,当然这个验证过程不是简单的对比IMSI,会有更多安全机制。为了更清楚的了解流量上网机制,下面再来一张4G流量上网时手机与运营商的交互示意图:


核心组件

手机:这其中对流量上网起到关键作用的就是手机卡和基带模块。手机卡中保存了IMSI,全称International Mobile Subscriber Identification Number,国际移动用户识别码。IMSI是手机卡的身 标识。

基站:就是外边常见的铁架子信号塔,是一种能覆盖一定范围的无线电收发信息电台,手机会连接到它,然后它再通过光纤连接到运营商网络,从而实现移动通信。

MME:Mobility Management Entity,移动控制单元。手机建立连接时会先访问到这里,负责:手机与基站的接入控制,手机卡的鉴权、会话管理、安全传输,漫游控制、跨运营商通信等。

HSS:Home Subscriber Server,归属签约用户服务器。保存本地签约的手机卡信息,包括手机卡IMSI与手机号的对应关系,手机号的套餐信息、手机号的归属地信息等。

S-GW:Service Gateway,服务网关。4G环境下,用户侧与运营商核心网之间的业务网关。访问能不能进入,能做什么业务,去哪里做业务,是在这里控制的。跨运营商计费、漫游计费等也在这里完成。

P-GW:PDN Gateway,PDN网关。运营商核心网与互联网之间的网关,手机真正上网就是通过它了。它会给手机分配一个IP地址,控制上网的速度,对流量进行计费等。

PCRF:Policy and Charging Rules Function,策略与计费控制单元,保存每个用户的网络访问策略和计费规则。

上网过程

为了方便理解,这里将上网的过程大致分为两个部分(和上图的1、2对应):

  • 1 接入:建立连接时,手机携带IMSI信息,通过基站访问到MME,MME通过HSS验证IMSI信息,然后MME进行一些初始化工作,返回一些鉴权参数给手机,手机再进行一些计算,然后把计算结果返回给MME,MME验证手机的计算结果,验证通过则允许接入。这个过程保证了接入的安全,MME还为后续的数据传输提供了加密传输支持,保护数据不被窃听和篡改,有兴趣的同学可以去详细了解下。

    如果手机卡销售的时候没有写入手机号,手机卡首次注册登记的时候,运营商会从HSS中取出手机号,然后再写入手机卡中。

    实际应用中,为了防止跟踪和攻击,不是每次通信时都要携带IMSI,MME会生成一个临时的GUTI对应到IMSI,就像Web程序中的SessionId。MME还有一定的机制控制GUIT的重新分配。

  • 2 传输:手机网络流量的传输,还是先要通过基站,然后下一步进入S-GW,S-GW会检查用户的授权,就像Web程序中检查前端提交过来的SessionId,再看看用户有没有权限进行其提交的业务,这里就是看看用户有没有开通流量上网,这是S-GW通过连接MME实现的。S-GW处理完毕后,数据包会进入P-GW,P-GW在手机使用流量上网时会给用户分配一个IP地址,然后数据包通过网关进入互联网,访问到相关的资源。P-GW还会对上网行为进行速率控制、流量计费等操作,这些策略来源于PCRF,PCRF中的规则是根据HSS中的用户套餐、用户等级等计算出来的。

    对P-GW来说S-GW屏蔽了用户的移动性,手机在多个基站切换时,S-GW不变。

以上就是手机流量上网的基本原理了,可以看到,运营商通过IMSI或者GUTI完全有能力获取到当前上网用户的手机号码。对于运营商的一键登录具体是怎么实现的,我并没有找到相关的介绍,但是可以设想下:手机应用通过运营商的SDK发起获取手机号码的业务请求,此时会携带IMSI或者GUTI,业务请求到达S-GW,S-GW鉴权通过,然后将这个业务请求路由到运营商核心网中获取手机号码的服务,服务根据业务规则从HSS中取出手机号码并进行若干处理。

一键登录的原理

理解了手机流量上网的原理,再来看下一键登录业务是如何实现的,这个部分属于上层应用程序开发,大家应该相对熟悉一些。

如果你接入过微信的第三方应用登录,或者其他类似的第三方应用登录,过程是差不多的。还是先来看图:


这里对一些关键步骤进行说明:

  • 2预取手机号掩码:这个手机号掩码需要在请求用户授权的页面展示给用户看,因为获取这个信息要通过电信运营商的网络,所以可能会比较慢,为了提升用户体验,可以在应用启动的时候就去获取,然后缓存一段时间。

  • 8授权请求:因为应用获取用户手机号这个事比较敏感,必须让用户清楚的了解并授权之后才能进行,为了确保这件事,运营商的认证SDK提供了这个授权请求页面,用户确认授权后,SDK直接向运营商认证服务发起请求认证,认证服务会返回一个认证Token给应用。应用再通过自己的服务端拿着这个Token找运营商获取手机号码。

  • 17生成应用授权Token:应用要维护自己用户的登录状态,这里可以采用传统的Session机制,也可以使用JWT机制。

  • 3预取手机号掩码 和 11请求认证,都需要通过手机蜂窝网络通信,也就说需要通过手机流量上网。如果手机同时开启了流量和WIFI,认证SDK会将手机短暂切换到流量上网模式。如果手机没有开启流量,有些SDK还会在上次成功取号之后多缓存一个临时Token,这样也能成功实现一次一键登录,不过这个限制性很大。

这里其实还有一个安全问题

14登录请求:用户如果随便造一个认证Token,然后就向应用服务提交请求,应用服务再向认证服务提交请求,这属于一种跨站攻击。虽然这个Token可以被阻止,但是不免浪费资源,给服务端带来压力。

这一点微信第三方应用登录做的比较好,用户登录前,应用服务端先生成一个随机数,然后应用前端向应用服务端提交时,带着这个随机数,应用服务端可以验证这个随机数。

号码验证场景

除了用于登录,运营商网关的这种取号能力,还可以用在验证手机号上,在某些关键业务上,比如支付过程中,要求用户输入本机手机号码或者其中的某几位,然后通过运营商认证服务验证手机号是否本机号码。

隐私保护问题

设备唯一标识问题

现在大家对隐私问题关注的越来越多了,经常会出现这种情况:你在某电商网站搜索了某个商品,然后访问其它网站时,都向你推荐这类商品的广告。还有一种感觉很恐怖的情况,你刚和某个人谈论了某件事,然后就在某个App上看到了关于这件事的推荐,有人猜测是App在偷听,不过基于目前的舆论和监督,偷听风险太大,这其中的原因可能真的只是算法太厉害了。

最近几年Android和iOS系统都对App获取手机唯一标识进行了限制,比如IMEI、Mac地址、序列号、广告Id等,目的就是防止用户的信息在多个App之间进行关联,导致泄漏用户的隐私,产生一些安全问题和法律风险,前述跨App的广告行为也自然受到了抑制。

在了解一键登录的技术原理时,看到某运营商提供了一种和SIM卡绑定的设备唯一Id服务,宣传语就是为了应对移动操作系统限制访问手机唯一标识的问题,在现今越来越重视隐私保护的前提下,如果这种能力开放给了广告平台,就是开历史的倒车了。

手机号作为身 份标识的问题

对于国内普遍使用手机号登录的方式,从技术上很难限制App之间进行手机号关联,然后综合分析用户的行为。比如某家大厂运营了多款不同种类的热门App,它就有能力更全面的了解某个用户,如果要限制可能就得通过法律层面来解决了。至于不同厂商之间的手机号关联行为,基于商业利益的保护,不太可能会出现。

在国内这种商业环境下,如果你真的对自己的隐私很关注,最好只使用账号密码的方式登录,否则经常更换手机号可能是一种没办法的办法。

手机号重新销售问题

手机号的总量是有限的,为了有效利用手机号资源,手机号注销以后,经过一段时间就会被运营商重新销售。如果新的手机号拥有者拿着这个手机号登录某个APP,而这个手机号之前已经在这个App上注册过,产生了大量的使用记录,那么此手机号前拥有者的隐私就会被泄漏。所以大家现在都不太敢随便更换手机号,因为注册过的地方太多了,留下了数不清的使用痕迹。

在了解一键登录的技术原理时,还看到某运营商提供了一种“手机号更换绑定SIM卡通知”的服务,应用可以据此解绑重新销售的手机号与应用账号之间的关系,从而保护用户的隐私。在上文中已经提过手机卡使用IMSI进行标识,如果手机号被重新销售,就会绑定新的IMSI,运营商可以据此产生通知。当然运营商还需要排除手机卡更换和携号转网的情况,这些情况下手机号也会绑定新的IMSI。

不得不说运营商的这个服务还是挺赞的👍。


作者:萤火架构
来源:https://juejin.cn/post/7059182505101885471

收起阅读 »

Google 如何看待 Kotlin 与 Android

先进 简洁 安全。 在语法表现上,Kotlin够简洁明了。不防看看:你应该切换到Kotlin开发,它包含了零默认值和不可变性的安全特性,使你的Android应用程序在默认情况下是安全的 并且性能是良好的。 代码更安全 编写更安全的代码,并在应用程序中避免 发生...
继续阅读 »

先进 简洁 安全。


在语法表现上,Kotlin够简洁明了。不防看看:你应该切换到Kotlin开发,它包含了零默认值和不可变性的安全特性,使你的Android应用程序在默认情况下是安全的 并且性能是良好的。


代码更安全


编写更安全的代码,并在应用程序中避免 发生Nullpointerexception。


var output: String
output = null // Compilation error==================================val name: String? = null // Nullable type
println(name.length()) // Compilation error

语法更易读和简洁


Data Classes


更加专注于表达你自己的代码创意设计,无需编写更多的样板代码。


// Create a POJO with getters, setters, equals(), hashCode(), toString(), and copy() with a single line:
data class User(val name: String, val email: String)

Lambdas语法


使用lambda来简化你的代码。


button.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v){
doSomething();
}
});

button.setOnClickListener { doSomething() }    

默认的命名参数


通过使用默认参数减少重载函数的数量。使用命名参数调用函数,使自己的代码更具有可读性。


fun format(str: String,
normalizeCase: Boolean = true,
upperCaseFirstLetter: Boolean = true,
divideByCamelHumps: Boolean = false,
wordSeparator: Char = ' ') {

}==================================// Call function with named arguments.
format(str, normalizeCase = true, upperCaseFirstLetter = true)

和 findViewById 说再见


在你自己的代码中避免findViewById() 调用。专注于写你的逻辑,而不需要那么繁琐。


import kotlinx.android.synthetic.main.content_main.*class MainActivity : AppCompatActivity() {   override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// No need to call findViewById(R.id.textView) as TextView
textView.text = "Kotlin for Android rocks!" }
}

扩展功能, 而不是用继承


扩展函数和属性使你可以轻松地扩展类的功能,而无需继承它们。调用代码是可读和自然的。


// Extend ViewGroup class with inflate function
fun ViewGroup.inflate(layoutRes: Int): View {
return LayoutInflater.from(context).inflate(layoutRes, this, false)
}==================================// Call inflate directly on the ViewGroup instance
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val v = parent.inflate(R.layout.view_item)
return ViewHolder(v)
}

100%的和Java可互操作性


在你非常不是想用Java的情况下,尽量多地使用Kotlin。Kotlin是一种与Java完全可互操作的JVM语言。


// Calling Java code from Kotlin
class KotlinClass {
fun kotlinDoSomething() {
val javaClass = JavaClass()
javaClass.javaDoSomething()
println(JavaClass().prop)
}
}==================================// Calling Kotlin code from Java
public class JavaClass {
public String getProp() { return "Hello"; }
public void javaDoSomething() {
new KotlinClass().kotlinDoSomething();
}
}

强大的开发工具支持


Android Studio 3.0 提供了不错的工具来帮助你开始使用Kotlin开发。在将Java代码粘贴到Kotlin文件时,可以转换整个Java文件或转换一段代码片段。很稳!


image.png


Kotlin 是开放的


与Android一样,Kotlin是Apache 2.0下的一个开源项目。Google对 Kotlin 的选择重申了Android对开发者 开放生态系统的承诺,随着 Google 的发展和 Android平台的发展,Google 希望 kotlin 语言的发展, 也很高兴看到 kotlin 语言的发展。


image.png


Tamic的一些话


Java 10 的 新特性也刚好( Java 10 新特性解密)迎合kotlin的某些特性一样,以后即将用var 来定义变量和类。 因此我们发现Koltin将来必定是开发者所关注的一名语言趋势,假如有一天,Google像抛弃 Eclispe,投坏Android Studio一样,放弃对Java的支持,到时候,至少你还能掌握kotlin开发,不然,你是要转行吗?


相关视频


Android进阶开发:函数与方法有本质区别 你知道吗


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

超难面试题:Android 为什么设计只有主线程更新UI

选择方案的选择 单线程更新UI 多线程更新UI 从问题本身考虑就两个方案不是单线程就是多线程。 下面是个人根据具体情况分析,如果有什么不对的地方,欢迎指正。 从开发效率来看,开发一个 单线程UI 库,会显得非常容易,而且,每个控件的运行效率肯定会比多线...
继续阅读 »

选择方案的选择



  1. 单线程更新UI

  2. 多线程更新UI


从问题本身考虑就两个方案不是单线程就是多线程。



下面是个人根据具体情况分析,如果有什么不对的地方,欢迎指正。



从开发效率来看,开发一个 单线程UI 库,会显得非常容易,而且,每个控件的运行效率肯定会比多线程的效率高,比如我们单线程可以使用HashMap,多线程就需要使用JUC 框架下的类库了,这个效率肯定比 HashMap低很多,这样就很好理解。编写一个多线程的UI库,很可能每个控件,都会加锁,控件本身效率就低了,但是这样还不够 ,后面会解释。


还有一个简单方案,就是对真个UI库,加锁,而不是具体某个控件,就是确保同一时刻,只能有一个线程,对整个UI系统更新,这个已经有点单线程更新UI的意思了 。但是锁的粒度会很大,如果一个页面100个控件,相当于每个控件都加锁了。


这个方案实现起来倒是不复杂,只需要设计一个boolean变量就可以,任何线程需要更新UI 都会访问这个变量获取锁,这个方案会造成所有的线程都竞争同一把锁,单从运行效率分析,应该是很高的,但是这个竞争特别激烈,可能造成的问题就是,事件响应不够及时,


单线程更新UI方案简单成熟


单线程更新UI方案,从上面的分析来看,优势就很明显,整体设计可能是最简单的,每个控件的设计只需要考虑单线程运行就可以,完全不必关系其他线程更新UI。


而且这套方案非常成熟,在Android 之前,swing qt windows 几乎绝大部分图形界面api 都会使用这个单线程方案。


从执行效率看


前面说了,如果一个加锁的api 和不加锁的api 比较,那肯定不加锁效率高对吧,但是,这么说确实很笼统,如果合理设计一套多线程更新ui 的库,整体性能未必会比单线程差,只是想实现这样一套系统的复杂程度,可能不只是翻倍那么简单,设计越复杂,带来的问题是 潜在bug 可能会多,但是这些,在设计ui系统 的时候未必是这样考虑的,如果业务复杂,效果会更好,那么我相信大部分企业还是会设计一个复杂的系统的。


综合考虑?


多线程更新UI,不管如何设计都会绕不开一个问题,就是竞争,而这个竞争,是整个UI系统的,而不是单独一个控件,大部分情况下,一个线程可能同时更新的是过个控件,而要确保我一次更新的所有控件是同步更新的,所以要保证这个逻辑,其实我们就要确保一个问题,同一时刻。永远只允许一个线程去更新UI。不能保证这一点,就会造成业务逻辑可能各种问题,甚至各种死锁。


既然同一个时刻只能一个线程更新,那设计成单线程是不是就更好呢,到这里,其实还是不够全面的,还有个因素就是事件相应。如果多线程更新的情况下,其实这个是不容易实现的, 反而单线程,就好实现一些。


总结


通过分析总结几个点。



  1. 一般UI还是要保证同一时刻只有一个线程在更新,所以效率不会更高。

  2. 多线程更新UI实现上会复杂一些,Java的内部人员发布过文章也说过这个几乎不可实现。

  3. 从响应速度角度分析,单线程可以设计出更好的响应速度的api

  4. 单线程更新,也是一个被证明效果非常好的方案。


从过个角度分析 Android 为什么设计只有主线程更新UI 都是最好的选择。


不过回答这个问题需要理解的不全是结论,而是对这个问题,和图形界面开发的理解。
如果说效率高,安全,也需要回答出来为什么。这些不是凭空说的。真的效率高吗?高在哪里?都需要说清楚,可能会有不正确的地方。但是只要把需要考虑的点表达清晰就好


引用


负责Swing开发的一个大师的一篇博客《Multithreaded toolkits: A failed dream?》


也有人说单新ui 效率会高,因为多线程会加锁。如果有人能把这个细节解释清楚呢,希望留言。因为正常设计也只是锁更新那一行代码而已,我的总结就是效率不分伯仲,希望大家探讨吧。


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

IDEA 中玩转 Git

Git
Git 有很多客户端工具,不过感觉还是命令行操作最好用,方便又快捷,不过命令行操作需要小伙伴们对 Git 命令比较熟练,这可能对有的人来说会有一些难度,所以,客户端工具有时候也不能抛弃,如果非要推荐一个 Git 客户端工具,我觉得还是 IDEA 中的 Git ...
继续阅读 »

Git 有很多客户端工具,不过感觉还是命令行操作最好用,方便又快捷,不过命令行操作需要小伙伴们对 Git 命令比较熟练,这可能对有的人来说会有一些难度,所以,客户端工具有时候也不能抛弃,如果非要推荐一个 Git 客户端工具,我觉得还是 IDEA 中的 Git 插件吧。其他的 Git 客户端工具松哥之前也有体验过一些,不过感觉还是 IDEA 中的用起来更加省事。


今天这篇文章算是我第二次教大家在开发工具中使用 Git 了,刚毕业的时候,松哥写过一篇文章,教大家在 Eclipse 中使用 Git,那时候在 Eclipse 中使用 Git 是真的麻烦,光是插件就要安装半天,刚刚翻了一下那篇文章,已经是七年前的事情了。



七年之后,Eclipse 也没了往日的风光,IDEA 逐渐成了开发的主流工具,咱们今天就来捋一捋 IDEA 中使用 Git。


1. 基本配置


首先你要安装 Git,这个就不需要我多说了,IDEA 上默认也是安装了 Git 插件,可以直接使用。


为了给小伙伴们演示方便,我这里使用 GitHub 作为远程仓库,如果还有人不清楚 GitHub 和 Git 的区别,可以在公众号江南一点雨底部菜单栏查看 Git 教程,看完了就明白了。


从 2021.08.13 号开始,IDEA 上配置 GitHub 有一个小小的变化,即不能使用用户名密码的方式登录了,如果你尝试用用户名/密码的方式登录 GitHub 提交代码,会收到如下提示:


Support for password authentication was removed on August 13, 2021. 
Please use a personal access token instead.

在 IDEA 上使用用户名/密码的方法登录 GitHub 也会报如下错误:



需要我们点击右上角的 Use Token,使用令牌的方式登录 GitHub,令牌的生成方式如下:



  1. 网页上登录你的 GitHub 账号。

  2. 点击右上角,选择 Settings:




  1. 拉到最下方,选择左边的 Developer settings:




  1. 选择左边的 Personal access tokens,然后点击右上角的 Generate new token:




  1. 填一下基本信息,选一下权限即可(权限需要选择 repo 和 gist,其他根据自己的需求选择):




  1. 最后会生成一个令牌,拷贝到 IDEA 中即可,如下:




这就是基本配置。


小伙伴们在公司做开发,一般是不会将 GitHub 作为远程仓库的,那么这块根据自己实际情况来配置就行了。


2. clone


头一天上班,首先上来要先 clone 项目下来,IDEA 中有对应的 clone 工具,我们直接使用即可:




这块也可以直接选择下面的 GitHub,然后直接从自己的 GitHub 仓库上拉取新代码。


clone 完成之后,IDEA 会提示是否打开该项目,选择 yes 即可。


代码 clone 下来之后,就可以根据松哥前文介绍的 Git Flow 开始开发了。


3. 分支


假设我们先创建 develop 和 release 分支,创建方式如下,选中当前工程,右键单击,然后依次选择 Git->Repository->Branches...



或者依次点击顶部的 VCS->Git->Branches...



当然两个方式都比较麻烦,直接点击 IDEA 的右下角最为省事,也是最常用的办法,如下图:



选择 New Branch,然后创建新的分支,勾选上 Checkout 表示分支创建成功后,切换到该分支上,如下:



选择一个分支,然后点击 Checkout,可以切换到该分支上:



接下来我们把 develop 分支提交到远程仓库,如下:




我们没有修改代码,所以直接点击 Push 按钮提交即可。


提交完成后,develop 后面多了 origin 前缀,Remote Branches 中也多了 develop 分支,说明提交成功。



现在假设我们想从 develop 上拉一个名为 feature-login 的分支,来完成登录功能,如下:




从创建的日志中,我们能看到 feature-login 确实是来自 develop:



好啦,接下来我们就可以愉快的开启一天的工作啦~


feature-login 上的功能开发完成后,首先点击 IDEA 的右上角完成本地仓库的提交,如下图:




填入提交的 Message,下方也能看到不同版本的内容对比,点击右下角完成代码提交,注意这个只是提交到本地仓库。


由于我们并不会将 feature-login 提交到远程仓库,所以接下来我们要将 feature-login 合并到 develop 然后将最新的 develop push 到远程仓库,操作方式如下:



  1. 切换回 develop 分支。

  2. 选择 feature-login->Merge into Current 进行合并。



合并完成后,如需删除 feature-login 分支,也可以在 IDEA 日志中顺手删除:



不过上面介绍的合并是快速合并,即让 develop 的指针指向了 feature-login,很多时候我们可能需要加上 --no-ff 参数来合并,那么步骤如下:


从 feature-login 切换回 develop 分支,然后如下:




此时我们看一眼提交日志,如下:



从这日志中也可以看出,此时不是快速合并模式了!


最后,选择 develop->Push,将代码提交到远程仓库。


4. pull


在 IDEA 中,如需从远程仓库中更新代码,点击右上角的按钮即可,如下图:



好啦,这就是一个大致的流程。


当然 Git 博大精深,IDEA 中支持的功能也非常多,其他功能就需要小伙伴们自己来摸索了,有不明白的欢迎留言讨论。


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

求你别自己瞎写工具类了,Spring 自带的这些他不香吗?

断言 断言是一个逻辑判断,用于检查不应该发生的情况 Assert 关键字在 JDK1.4 中引入,可通过 JVM 参数-enableassertions开启 SpringBoot 中提供了 Assert 断言工具类,通常用于数据合法性检查 // ...
继续阅读 »

断言



  1. 断言是一个逻辑判断,用于检查不应该发生的情况

  2. Assert 关键字在 JDK1.4 中引入,可通过 JVM 参数-enableassertions开启

  3. SpringBoot 中提供了 Assert 断言工具类,通常用于数据合法性检查


// 要求参数 object 必须为非空(Not Null),否则抛出异常,不予放行  
// 参数 message 参数用于定制异常信息。  
void notNull(Object object, String message)  
// 要求参数必须空(Null),否则抛出异常,不予『放行』。  
// 和 notNull() 方法断言规则相反  
void isNull(Object object, String message)  
// 要求参数必须为真(True),否则抛出异常,不予『放行』。  
void isTrue(boolean expression, String message)  
// 要求参数(List/Set)必须非空(Not Empty),否则抛出异常,不予放行  
void notEmpty(Collection collection, String message)  
// 要求参数(String)必须有长度(即,Not Empty),否则抛出异常,不予放行  
void hasLength(String text, String message)  
// 要求参数(String)必须有内容(即,Not Blank),否则抛出异常,不予放行  
void hasText(String text, String message)  
// 要求参数是指定类型的实例,否则抛出异常,不予放行  
void isInstanceOf(Class type, Object obj, String message)  
// 要求参数 `subType` 必须是参数 superType 的子类或实现类,否则抛出异常,不予放行  
void isAssignable(Class superType, Class subType, String message)  

对象、数组、集合


ObjectUtils



  1. 获取对象的基本信息


// 获取对象的类名。参数为 null 时,返回字符串:"null"   
String nullSafeClassName(Object obj)  
// 参数为 null 时,返回 0  
int nullSafeHashCode(Object object)  
// 参数为 null 时,返回字符串:"null"  
String nullSafeToString(boolean[] array)  
// 获取对象 HashCode(十六进制形式字符串)。参数为 null 时,返回 0   
String getIdentityHexString(Object obj)  
// 获取对象的类名和 HashCode。 参数为 null 时,返回字符串:""   
String identityToString(Object obj)  
// 相当于 toString()方法,但参数为 null 时,返回字符串:""  
String getDisplayString(Object obj)  


  1. 判断工具


// 判断数组是否为空  
boolean isEmpty(Object[] array)  
// 判断参数对象是否是数组  
boolean isArray(Object obj)  
// 判断数组中是否包含指定元素  
boolean containsElement(Object[] array, Object element)  
// 相等,或同为 null时,返回 true  
boolean nullSafeEquals(Object o1, Object o2)  
/*  
判断参数对象是否为空,判断标准为:  
   Optional: Optional.empty()  
      Array: length == 0  
CharSequence: length == 0  
 Collection: Collection.isEmpty()  
        Map: Map.isEmpty()  
*/  
boolean isEmpty(Object obj)  


  1. 其他工具方法


// 向参数数组的末尾追加新元素,并返回一个新数组  
<A, O extends A> A[] addObjectToArray(A[] array, O obj)  
// 原生基础类型数组 --> 包装类数组  
Object[] toObjectArray(Object source)  

StringUtils



  1. 字符串判断工具


// 判断字符串是否为 null,或 ""。注意,包含空白符的字符串为非空  
boolean isEmpty(Object str)  
// 判断字符串是否是以指定内容结束。忽略大小写  
boolean endsWithIgnoreCase(String str, String suffix)  
// 判断字符串是否已指定内容开头。忽略大小写  
boolean startsWithIgnoreCase(String str, String prefix)   
// 是否包含空白符  
boolean containsWhitespace(String str)  
// 判断字符串非空且长度不为 0,即,Not Empty  
boolean hasLength(CharSequence str)  
// 判断字符串是否包含实际内容,即非仅包含空白符,也就是 Not Blank  
boolean hasText(CharSequence str)  
// 判断字符串指定索引处是否包含一个子串。  
boolean substringMatch(CharSequence str, int index, CharSequence substring)  
// 计算一个字符串中指定子串的出现次数  
int countOccurrencesOf(String str, String sub)  


  1. 字符串操作工具


// 查找并替换指定子串  
String replace(String inString, String oldPattern, String newPattern)  
// 去除尾部的特定字符  
String trimTrailingCharacter(String str, char trailingCharacter)   
// 去除头部的特定字符  
String trimLeadingCharacter(String str, char leadingCharacter)  
// 去除头部的空白符  
String trimLeadingWhitespace(String str)  
// 去除头部的空白符  
String trimTrailingWhitespace(String str)  
// 去除头部和尾部的空白符  
String trimWhitespace(String str)  
// 删除开头、结尾和中间的空白符  
String trimAllWhitespace(String str)  
// 删除指定子串  
String delete(String inString, String pattern)  
// 删除指定字符(可以是多个)  
String deleteAny(String inString, String charsToDelete)  
// 对数组的每一项执行 trim() 方法  
String[] trimArrayElements(String[] array)  
// 将 URL 字符串进行解码  
String uriDecode(String source, Charset charset)  


  1. 路径相关工具方法


// 解析路径字符串,优化其中的 “..”   
String cleanPath(String path)  
// 解析路径字符串,解析出文件名部分  
String getFilename(String path)  
// 解析路径字符串,解析出文件后缀名  
String getFilenameExtension(String path)  
// 比较两个两个字符串,判断是否是同一个路径。会自动处理路径中的 “..”   
boolean pathEquals(String path1, String path2)  
// 删除文件路径名中的后缀部分  
String stripFilenameExtension(String path)   
// 以 “. 作为分隔符,获取其最后一部分  
String unqualify(String qualifiedName)  
// 以指定字符作为分隔符,获取其最后一部分  
String unqualify(String qualifiedName, char separator)  

CollectionUtils



  1. 集合判断工具


// 判断 List/Set 是否为空  
boolean isEmpty(Collection<?> collection)  
// 判断 Map 是否为空  
boolean isEmpty(Map<?,?> map)  
// 判断 List/Set 中是否包含某个对象  
boolean containsInstance(Collection<?> collection, Object element)  
// 以迭代器的方式,判断 List/Set 中是否包含某个对象  
boolean contains(Iterator<?> iterator, Object element)  
// 判断 List/Set 是否包含某些对象中的任意一个  
boolean containsAny(Collection<?> source, Collection<?> candidates)  
// 判断 List/Set 中的每个元素是否唯一。即 List/Set 中不存在重复元素  
boolean hasUniqueObject(Collection<?> collection)  


  1. 集合操作工具


// 将 Array 中的元素都添加到 List/Set 中  
<E> void mergeArrayIntoCollection(Object array, Collection<E> collection)    
// 将 Properties 中的键值对都添加到 Map 中  
<K,V> void mergePropertiesIntoMap(Properties props, Map<K,V> map)  
// 返回 List 中最后一个元素  
<T> T lastElement(List<T> list)    
// 返回 Set 中最后一个元素  
<T> T lastElement(Set<T> set)   
// 返回参数 candidates 中第一个存在于参数 source 中的元素  
<E> E findFirstMatch(Collection<?> source, Collection<E> candidates)  
// 返回 List/Set 中指定类型的元素。  
<T> T findValueOfType(Collection<?> collection, Class<T> type)  
// 返回 List/Set 中指定类型的元素。如果第一种类型未找到,则查找第二种类型,以此类推  
Object findValueOfType(Collection<?> collection, Class<?>[] types)  
// 返回 List/Set 中元素的类型  
Class<?> findCommonElementType(Collection<?> collection)  

文件、资源、IO 流


FileCopyUtils



  1. 输入


// 从文件中读入到字节数组中  
byte[] copyToByteArray(File in)  
// 从输入流中读入到字节数组中  
byte[] copyToByteArray(InputStream in)  
// 从输入流中读入到字符串中  
String copyToString(Reader in)  


  1. 输出


// 从字节数组到文件  
void copy(byte[] in, File out)  
// 从文件到文件  
int copy(File in, File out)  
// 从字节数组到输出流  
void copy(byte[] in, OutputStream out)   
// 从输入流到输出流  
int copy(InputStream in, OutputStream out)   
// 从输入流到输出流  
int copy(Reader in, Writer out)  
// 从字符串到输出流  
void copy(String in, Writer out)  

ResourceUtils



  1. 从资源路径获取文件


// 判断字符串是否是一个合法的 URL 字符串。  
static boolean isUrl(String resourceLocation)  
// 获取 URL  
static URL getURL(String resourceLocation)   
// 获取文件(在 JAR 包内无法正常使用,需要是一个独立的文件)  
static File getFile(String resourceLocation)  


  1. Resource


// 文件系统资源 D:...  
FileSystemResource  
// URL 资源,如 file://... http://...  
UrlResource  
// 类路径下的资源,classpth:...  
ClassPathResource  
// Web 容器上下文中的资源(jar 包、war 包)  
ServletContextResource  

// 判断资源是否存在  
boolean exists()  
// 从资源中获得 File 对象  
File getFile()  
// 从资源中获得 URI 对象  
URI getURI()  
// 从资源中获得 URI 对象  
URL getURL()  
// 获得资源的 InputStream  
InputStream getInputStream()  
// 获得资源的描述信息  
String getDescription()  

StreamUtils



  1. 输入


void copy(byte[] in, OutputStream out)  
int copy(InputStream in, OutputStream out)  
void copy(String in, Charset charset, OutputStream out)  
long copyRange(InputStream in, OutputStream out, long start, long end)  


  1. 输出


byte[] copyToByteArray(InputStream in)  
String copyToString(InputStream in, Charset charset)  
// 舍弃输入流中的内容  
int drain(InputStream in)   

反射、AOP


ReflectionUtils



  1. 获取方法


// 在类中查找指定方法  
Method findMethod(Class<?> clazz, String name)   
// 同上,额外提供方法参数类型作查找条件  
Method findMethod(Class<?> clazz, String name, Class<?>... paramTypes)   
// 获得类中所有方法,包括继承而来的  
Method[] getAllDeclaredMethods(Class<?> leafClass)   
// 在类中查找指定构造方法  
Constructor<T> accessibleConstructor(Class<T> clazz, Class<?>... parameterTypes)   
// 是否是 equals() 方法  
boolean isEqualsMethod(Method method)   
// 是否是 hashCode() 方法   
boolean isHashCodeMethod(Method method)   
// 是否是 toString() 方法  
boolean isToStringMethod(Method method)   
// 是否是从 Object 类继承而来的方法  
boolean isObjectMethod(Method method)   
// 检查一个方法是否声明抛出指定异常  
boolean declaresException(Method method, Class<?> exceptionType)   


  1. 执行方法


// 执行方法  
Object invokeMethod(Method method, Object target)    
// 同上,提供方法参数  
Object invokeMethod(Method method, Object target, Object... args)   
// 取消 Java 权限检查。以便后续执行该私有方法  
void makeAccessible(Method method)   
// 取消 Java 权限检查。以便后续执行私有构造方法  
void makeAccessible(Constructor<?> ctor)   


  1. 获取字段


// 在类中查找指定属性  
Field findField(Class<?> clazz, String name)   
// 同上,多提供了属性的类型  
Field findField(Class<?> clazz, String name, Class<?> type)   
// 是否为一个 "public static final" 属性  
boolean isPublicStaticFinal(Field field)   


  1. 设置字段


// 获取 target 对象的 field 属性值  
Object getField(Field field, Object target)   
// 设置 target 对象的 field 属性值,值为 value  
void setField(Field field, Object target, Object value)   
// 同类对象属性对等赋值  
void shallowCopyFieldState(Object src, Object dest)  
// 取消 Java 的权限控制检查。以便后续读写该私有属性  
void makeAccessible(Field field)   
// 对类的每个属性执行 callback  
void doWithFields(Class<?> clazz, ReflectionUtils.FieldCallback fc)   
// 同上,多了个属性过滤功能。  
void doWithFields(Class<?> clazz, ReflectionUtils.FieldCallback fc,   
                 ReflectionUtils.FieldFilter ff)   
// 同上,但不包括继承而来的属性  
void doWithLocalFields(Class<?> clazz, ReflectionUtils.FieldCallback fc)   

AopUtils



  1. 判断代理类型


// 判断是不是 Spring 代理对象  
boolean isAopProxy()  
// 判断是不是 jdk 动态代理对象  
isJdkDynamicProxy()  
// 判断是不是 CGLIB 代理对象  
boolean isCglibProxy()  


  1. 获取被代理对象的 class


// 获取被代理的目标 class  
Class<?> getTargetClass()  

AopContext



  1. 获取当前对象的代理对象


Object currentProxy()

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

使用MyBatis拦截器后,摸鱼时间又长了。?

场景 在后端服务开发时,现在很流行的框架组合就是SSM(SpringBoot + Spring + MyBatis),在我们进行一些业务系统开发时,会有很多的业务数据表,而表中的信息从新插入开始,整个生命周期过程中可能会进行很多次的操作。 比如,我们在某网站购...
继续阅读 »

场景


在后端服务开发时,现在很流行的框架组合就是SSM(SpringBoot + Spring + MyBatis),在我们进行一些业务系统开发时,会有很多的业务数据表,而表中的信息从新插入开始,整个生命周期过程中可能会进行很多次的操作。


比如,我们在某网站购买一件商品,会生成一条订单记录,在支付完金额后订单状态会变为已支付,等最后我们收到订单商品,这个订单状态会变成已完成等。


假设我们的订单表t_order结果如下:



当订单创建时,需要设置insert_byinsert_timeupdate_byupdate_time的值;


在进行订单状态更新时,则只需要更新update_byupdate_time的值。


那应该如何处理呢?


麻瓜做法


最简单的做法,也是最容易想到的做法,就是在每个业务处理的代码中,对相关的字段进行处理。


比如订单创建的方法中,如下处理:


public void create(Order order){
// ...其他代码
// 设置审计字段
Date now = new Date();
order.setInsertBy(appContext.getUser());
order.setUpdateBy(appContext.getUser());
order.setInsertTime(now);
order.setUpdateTime(now);
orderDao.insert(order);
}

订单更新方法则只设置updateByupdateTime


public void update(Order order){
// ...其他代码

// 设置审计字段
Date now = new Date();
order.setUpdateBy(appContext.getUser());
order.setUpdateTime(now);
orderDao.insert(order);
}

这种方式虽然可以完成功能,但是存在一些问题:



  • 需要在每个方法中按照不同的业务逻辑决定设置哪些字段;

  • 在业务模型变多后,每个模型的业务方法中都要进行设置,重复代码太多。


那我们知道这种方式存在问题以后,就得找找有什么好方法对不对,往下看!


优雅做法


因为我们持久层框架更多地使用MyBatis,那我们就借助于MyBatis的拦截器来完成我们的功能。


首先我们来了解一下,什么是拦截器?


什么是拦截器?


MyBatis的拦截器顾名思义,就是对某些操作进行拦截。通过拦截器可以对某些方法执行前后进行拦截,添加一些处理逻辑。


MyBatis的拦截器可以对Executor、StatementHandler、PameterHandler和ResultSetHandler 接口进行拦截,也就是说会对这4种对象进行代理。


拦截器设计的初衷就是为了让用户在MyBatis的处理流程中不必去修改MyBatis的源码,能够以插件的方式集成到整个执行流程中。


比如MyBatis中的ExecutorBatchExecutorReuseExecutorSimpleExecutorCachingExecutor,如果这几种实现的query方法都不能满足你的需求,我们可以不用去直接修改MyBatis的源码,而通过建立拦截器的方式,拦截Executor接口的query方法,在拦截之后,实现自己的query方法逻辑。


在MyBatis中的拦截器通过Interceptor接口表示,该接口中有三个方法。


public interface Interceptor {

Object intercept(Invocation invocation) throws Throwable;

Object plugin(Object target);

void setProperties(Properties properties);

}

plugin方法是拦截器用于封装目标对象的,通过该方法我们可以返回目标对象本身,也可以返回一个它的代理。


当返回的是代理的时候我们可以对其中的方法进行拦截来调用intercept方法,当然也可以调用其他方法。


setProperties方法是用于在Mybatis配置文件中指定一些属性的。


使用拦截器更新审计字段


那么我们应该如何通过拦截器来实现我们对审计字段赋值的功能呢?


在我们进行订单创建和修改时,本质上是通过MyBatis执行insert、update语句,MyBatis是通过Executor来处理的。


我们可以通过拦截器拦截Executor,然后在拦截器中对要插入的数据对象根据执行的语句设置insert_by,insert_time,update_by,update_time等属性值就可以了。


自定义拦截器


自定义Interceptor最重要的是要实现plugin方法和intercept方法。


plugin方法中我们可以决定是否要进行拦截进而决定要返回一个什么样的目标对象。


intercept方法就是要进行拦截的时候要执行的方法。


对于plugin方法而言,其实Mybatis已经为我们提供了一个实现。Mybatis中有一个叫做Plugin的类,里面有一个静态方法wrap(Object target,Interceptor interceptor),通过该方法可以决定要返回的对象是目标对象还是对应的代理。


但是这里还存在一个问题,就是我们如何在拦截器中知道要插入的表有审计字段需要处理呢?


因为我们的表中并不是所有的表都是业务表,可能有一些字典表或者定义表是没有审计字段的,这样的表我们不需要在拦截器中进行处理。


也就是说我们要能够区分出哪些对象需要更新审计字段


这里我们可以定义一个接口,让需要更新审计字段的模型都统一实现该接口,这个接口起到一个标记的作用。


public interface BaseDO {
}

public class Order implements BaseDO{

private Long orderId;

private String orderNo;

private Integer orderStatus;

private String insertBy;

private String updateBy;

private Date insertTime;

private Date updateTime;
//... getter ,setter
}

接下来,我们就可以实现我们的自定义拦截器了。


@Component("ibatisAuditDataInterceptor")
@Intercepts({@Signature(method = "update", type = Executor.class, args = {MappedStatement.class, Object.class})})
public class IbatisAuditDataInterceptor implements Interceptor {

private Logger logger = LoggerFactory.getLogger(IbatisAuditDataInterceptor.class);

@Override
public Object intercept(Invocation invocation) throws Throwable {
// 从上下文中获取用户名
String userName = AppContext.getUser();

Object[] args = invocation.getArgs();
SqlCommandType sqlCommandType = null;

for (Object object : args) {
// 从MappedStatement参数中获取到操作类型
if (object instanceof MappedStatement) {
MappedStatement ms = (MappedStatement) object;
sqlCommandType = ms.getSqlCommandType();
logger.debug("操作类型: {}", sqlCommandType);
continue;
}
// 判断参数是否是BaseDO类型
// 一个参数
if (object instanceof BaseDO) {
if (SqlCommandType.INSERT == sqlCommandType) {
Date insertTime = new Date();
BeanUtils.setProperty(object, "insertedBy", userName);
BeanUtils.setProperty(object, "insertTimestamp", insertTime);
BeanUtils.setProperty(object, "updatedBy", userName);
BeanUtils.setProperty(object, "updateTimestamp", insertTime);
continue;
}
if (SqlCommandType.UPDATE == sqlCommandType) {
Date updateTime = new Date();
BeanUtils.setProperty(object, "updatedBy", userName);
BeanUtils.setProperty(object, "updateTimestamp", updateTime);
continue;
}
}
// 兼容MyBatis的updateByExampleSelective(record, example);
if (object instanceof ParamMap) {
logger.debug("mybatis arg: {}", object);
@SuppressWarnings("unchecked")
ParamMap<Object> parasMap = (ParamMap<Object>) object;
String key = "record";
if (!parasMap.containsKey(key)) {
continue;
}
Object paraObject = parasMap.get(key);
if (paraObject instanceof BaseDO) {
if (SqlCommandType.UPDATE == sqlCommandType) {
Date updateTime = new Date();
BeanUtils.setProperty(paraObject, "updatedBy", userName);
BeanUtils.setProperty(paraObject, "updateTimestamp", updateTime);
continue;
}
}
}
// 兼容批量插入
if (object instanceof DefaultSqlSession.StrictMap) {
logger.debug("mybatis arg: {}", object);
@SuppressWarnings("unchecked")
DefaultSqlSession.StrictMap<ArrayList<Object>> map = (DefaultSqlSession.StrictMap<ArrayList<Object>>) object;
String key = "collection";
if (!map.containsKey(key)) {
continue;
}
ArrayList<Object> objs = map.get(key);
for (Object obj : objs) {
if (obj instanceof BaseDO) {
if (SqlCommandType.INSERT == sqlCommandType) {
Date insertTime = new Date();
BeanUtils.setProperty(obj, "insertedBy", userName);
BeanUtils.setProperty(obj, "insertTimestamp", insertTime);
BeanUtils.setProperty(obj, "updatedBy", userName);
BeanUtils.setProperty(obj, "updateTimestamp", insertTime);
}
if (SqlCommandType.UPDATE == sqlCommandType) {
Date updateTime = new Date();
BeanUtils.setProperty(obj, "updatedBy", userName);
BeanUtils.setProperty(obj, "updateTimestamp", updateTime);
}
}
}
}
}
return invocation.proceed();
}

@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}

@Override
public void setProperties(Properties properties) {
}
}

通过上面的代码可以看到,我们自定义的拦截器IbatisAuditDataInterceptor实现了Interceptor接口。


在我们拦截器上的@Intercepts注解,type参数指定了拦截的类是Executor接口的实现,method 参数指定拦截Executor中的update方法,因为数据库操作的增删改操作都是通过update方法执行。


配置拦截器插件


在定义好拦截器之后,需要将拦截器指定到SqlSessionFactoryBeanplugins中才能生效。所以要按照如下方式配置。


<bean id="transSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="transDataSource" />
<property name="mapperLocations">
<array>
<value>classpath:META-INF/mapper/*.xml</value>
</array>
</property>
<property name="plugins">
<array>
<!-- 处理审计字段 -->
<ref bean="ibatisAuditDataInterceptor" />
</array>
</property>

到这里,我们自定义的拦截器就生效了,通过测试你会发现,不用在业务代码中手动设置审计字段的值,会在事务提交之后,通过拦截器插件自动对审计字段进行赋值。


小结


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

当Synchronized遇到这玩意儿,有个大坑,要注意!

你好呀,我是歪歪。 前几天在某技术平台上看到别人提的关于 Synchronized 的一个用法问题,我觉得挺有意思的,这个问题其实也是我三年前面试某公司的时候遇到的一个真题,当时不知道面试官想要考什么,没有回答的特别好,后来研究了一下就记住了。 所以看到这个问...
继续阅读 »

你好呀,我是歪歪。


前几天在某技术平台上看到别人提的关于 Synchronized 的一个用法问题,我觉得挺有意思的,这个问题其实也是我三年前面试某公司的时候遇到的一个真题,当时不知道面试官想要考什么,没有回答的特别好,后来研究了一下就记住了。


所以看到这个问题的时候觉得特别亲切,准备分享给你一起看看:



首先为了方便你看文章的时候复现问题,我给你一份直接拿出来就能跑的代码,希望你有时间的话也把代码拿出来跑一下:


public class SynchronizedTest {

    public static void main(String[] args) {
        Thread why = new Thread(new TicketConsumer(10), "why");
        Thread mx = new Thread(new TicketConsumer(10), "mx");
        why.start();
        mx.start();
    }
}

class TicketConsumer implements Runnable {

    private volatile static Integer ticket;

    public TicketConsumer(int ticket) {
        this.ticket = ticket;
    }

    @Override
    public void run() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + "开始抢第" + ticket + "张票,对象加锁之前:" + System.identityHashCode(ticket));
            synchronized (ticket) {
                System.out.println(Thread.currentThread().getName() + "抢到第" + ticket + "张票,成功锁到的对象:" + System.identityHashCode(ticket));
                if (ticket > 0) {
                    try {
                        //模拟抢票延迟
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "抢到了第" + ticket-- + "张票,票数减一");
                } else {
                    return;
                }
            }
        }
    }
}

程序逻辑也很简单,是一个模拟抢票的过程,一共 10 张票,开启两个线程去抢票。


票是共享资源,且有两个线程来消费,所以为了保证线程安全,TicketConsumer 的逻辑里面用了 synchronized 关键字。


这是应该是大家在初学 synchronized 的时候都会写到的例子,期望的结果是 10 张票,两个人抢,每张票只有一个人能抢到。


但是实际运行结果是这样的,我只截取开始部分的日志:



截图里面有三个框起来的部分。


最上面的部分,就是两个人都在抢第 10 张票,从日志输出上看也完全没有任何毛病,最终只有一个人抢到了票,然后进入到第 9 张票的争夺过程。


但是下面被框起来的第 9 张票的争夺部分就有点让人懵逼了:


why抢到第9张票,成功锁到的对象:288246497
mx抢到第9张票,成功锁到的对象:288246497

为什么两个人都抢到了第 9 张票,且成功锁到的对象都一样的?


这玩意,超出认知了啊。


这两个线程怎么可能拿到同一把锁,然后去执行业务逻辑呢?


所以,提问者的问题就浮现出来了。



  • 1.为什么 synchronized 没有生效?

  • 2.为什么锁对象 System.identityHashCode 的输出是一样的?


为什么没有生效?


我们先来看一个问题。


首先,我们从日志的输出上已经非常明确的知道,synchronized 在第二轮抢第 9 张票的时候失效了。


经过理论知识支撑,我们知道 synchronized 失效,肯定是锁出问题了。


如果只有一把锁,多个线程来竞争同一把锁,synchronized 绝对是不会有任何毛病的。


但是这里两个线程并没有达成互斥的条件,也就是说这里绝对存在的不止一把锁。


这是我们可以通过理论知识推导出来的结论。



先得出结论了,那么我怎么去证明“锁不止一把”呢?


能进入 synchronized 说明肯定获得了锁,所以我只要看各个线程持有的锁是什么就知道了。


那么怎么去看线程持有什么锁呢?


jstack 命令,打印线程堆栈功能,了解一下?


这些信息都藏在线程堆栈里面,我们拿出来一看便知。


在 idea 里面怎么拿到线程堆栈呢?


这就是一个在 idea 里面调试的小技巧了,我之前的文章里面应该也出现过多次。


首先为了方便获取线程堆栈信息,我把这里的睡眠时间调整到 10s:



跑起来之后点击这里的“照相机”图标:



点击几次就会有对应点击时间点的几个 Dump 信息



由于我需要观察前两次锁的情况,而每次线程进入锁之后都会等待 10s 时间,所以我就在项目启动的第一个 10s 和第二个 10s 之间各点击一次就行。


为了更直观的观察数据,我选择点击下面这个图标,把 Dump 信息复制下来:



复制下来的信息很多,但是我们只需要关心 why 和 mx 这两个线程即可。


这是第一次 Dump 中的相关信息:



mx 线程是 BLOCKED 状态,它在等待地址为 0x000000076c07b058 的锁。


why 线程是 TIMED_WAITING 状态,它在 sleeping,说明它抢到了锁,在执行业务逻辑。而它抢到的锁,你说巧不巧,正是 mx 线程等待的 0x000000076c07b058。


从输出日志上来看,第一次抢票确实是 why 线程抢到了:



从 Dump 信息看,两个线程竞争的是同一把锁,所以第一次没毛病。


好,我们接着看第二次的 Dump 信息:



这一次,两个线程都在 TIMED_WAITING,都在 sleeping,说明都拿到了锁,进入了业务逻辑。


但是仔细一看,两个线程拿的锁是不相同的锁。


mx 锁的是 0x000000076c07b058。


why 锁的是 0x000000076c07b048。


由于不是同一把锁,所以并不存在竞争关系,因此都可以进入 synchronized 执行业务逻辑,所以两个线程都在 sleeping,也没毛病。


然后,我再把两次 Dump 的信息放在一起给你看一下,这样就更直观了:



如果我用“锁一”来代替 0x000000076c07b058,“锁二”来代替 0x000000076c07b048。


那么流程是这样的:


why 加锁一成功,执行业务逻辑,mx 进入锁一等待状态。


why 释放锁一,等待锁一的 mx 被唤醒,持有锁一,继续执行业务。


同时 why 加锁二成功,执行业务逻辑。


从线程堆栈中,我们确实证明了 synchronized 没有生效的原因是锁发生了变化。


同时,从线程堆栈中我们也能看出来为什么锁对象 System.identityHashCode 的输出是一样的。



第一次 Dump 的时候,ticket 都是 10,其中 mx 没有抢到锁,被 synchronized 锁住。


why 线程执行了 ticket-- 操作,ticket 变成了 9,但是此时 mx 线程被锁住的 monitor 还是 ticket=10 这个对象,它还在 monitor 的 _EntryList 里面等着的,并不会因为 ticket 的变化而变化。


所以,当 why 线程释放锁之后,mx 线程拿到锁继续执行,发现 ticket=9。


而 why 也搞到一把新锁,也可以进入 synchronized 的逻辑,也发现 ticket=9。


好家伙,ticket 都是 9, System.identityHashCode 能不一样吗?


按理来说,why 释放锁一后应该继续和 mx 竞争锁一,但是却不知道它在哪搞到一把新锁。


那么问题就来了:锁为什么发生了变化呢?



谁动了我的锁?


经过前面一顿分析,我们坐实了锁确实发生了变化,当你分析出这一点的时候勃然大怒,拍案而起,大喊一声:是哪个瓜娃子动了我的锁?这不是坑爹吗?



按照我的经验,这个时候不要急着甩锅,继续往下看,你会发现小丑竟是自己:



抢完票之后,执行了 ticket-- 的操作,而这个 ticket 不就是你的锁对象吗?


这个时候你把大腿一拍,恍然大悟,对着围观群众说:问题不大,手抖而已。


于是大手一挥,把加锁的地方改成这样:


synchronized (TicketConsumer.class)

利用 class 对象来作为锁对象,保证了锁的唯一性。


经过验证也确实没毛病,非常完美,打完收工。


但是,真的就收工了吗?



其实关于锁对象为什么发生了变化,还隔了一点点东西没有说出来。


它就藏在字节码里面。


我们通过 javap 命令,反查字节码,可以看到这样的信息:



Integer.valueOf 这是什么玩意?



让人熟悉的 Integer 从 -128 到 127 的缓存。


也就是说我们的程序里面,会涉及到拆箱和装箱的过程,这个过程中会调用到 Integer.valueOf 方法。具体其实就是 ticket-- 的这个操作。


对于 Integer,当值在缓存范围内的时候,会返回同一个对象。当超过缓存范围,每次都会 new 一个新对象出来。


这应该是一个必备的八股文知识点,我在这里给你强调这个是想表达什么意思呢?


很简单,改动一下代码就明白了。


我把初始化票数从 10 修改为 200,超过缓存范围,程序运行结果是这样的:



很明显,从第一次的日志输出来看,锁都不是同一把锁了。


这就是我前面说的:因为超过缓存范围,执行了两次 new Integer(200) 的操作,这是两个不同的对象,拿来作为锁,就是两把不一样的锁。


再修改回 10,运行一次,你感受一下:



从日志输出来看,这个时候只有一把锁,所以只有一个线程抢到了票。


因为 10 是在缓存范围内的数字,所以每次是从缓存中获取出来,是同一个对象。


我写这一小段的目的是为了体现 Integer 有缓存这个知识点,大家都知道。但是当它和其他东西揉在一起的时候因为这个缓存会带来什么问题,你得分析出来,这比直接记住干瘪的知识点有效一点。


但是...


我们的初始票是 10,ticket-- 之后票变成了 9,也是在缓存范围内的呀,怎么锁就变了呢?


如果你有这个疑问的话,那么我劝你再好好想想。


10 是 10,9 是 9。


虽然它们都在缓存范围内,但是本来就是两个不同的对象,构建缓存的时候也是 new 出来的:



为什么我要补充这一段看起来很傻的说明呢?


因为我在网上看到其他写类似问题的时候,有的文章写的不清楚,会让读者误认为“缓存范围内的值都是同一个对象”,这样会误导初学者。


总之一句话:请别用 Integer 作为锁对象,你把握不住。


但是...



stackoverflow


但是,我写文章的时候在 stackoverflow 上也看到了一个类似的问题。


这个哥们的问题在于:他知道 Integer 不能做为锁对象,但是他的需求又似乎必须把 Integer 作为锁对象。



stackoverflow.com/questions/6…




我给你描述一下他的问题。


首先看标号为 ① 的地方,他的程序其实就是先从缓存中获取,如果缓存中没有则从数据库获取,然后在放到缓存里面去。


非常简单清晰的逻辑。


但是他考虑到并发的场景下,如果有多个线程同一时刻都来获取同一个 id,但是这个 id 对应的数据并没有在缓存里面,那么这些线程都会去执行查询数据库并维护缓存的动作。


对应查询和存储的动作,他用的是 fairly expensive 来形容。


就是“相当昂贵”的意思,说白了就是这个动作非常的“重”,最好不要重复去做。


所以只需要让某一个线程来执行这个 fairly expensive 的操作就好了。


于是他想到了标号为 ② 的地方的代码。


用 synchronized 来把 id 锁一下,不幸的是,id 是 Integer 类型的。


在标号为 ③ 的地方他自己也说了:不同的 Integer 对象,它们并不会共享锁,那么 synchronized 也没啥卵用。


其实他这句话也不严谨,经过前面的分析,我们知道在缓存范围内的 Integer 对象,它们还是会共享同一把锁的,这里说的“共享”就是竞争的意思。


但是很明显,他的 id 范围肯定比 Integer 缓存范围大。


那么问题就来了:这玩意该咋搞啊?


我看到这个问题的时候想到的第一个问题是:上面这个需求我好像也经常做啊,我是怎么做的来着?


想了几秒恍然大悟,哦,现在都是分布式应用了,我特么直接用的是 Redis 做锁呀。


根本就没有考虑过这个问题。


如果现在不让用 Redis,就是单体应用,那么怎么解决呢?


在看高赞回答之前,我们先看看这个问题下面的一个评论:



开头三个字母:FYI。


看不懂没关系,因为这个不是重点。


但是你知道的,我的英语水平 very high,所以我也顺便教点英文。


FYI,是一个常用的英文缩写,全称是 for your information,供参考的意思。


所以你就知道,他后面肯定是给你附上一个资料了,翻译过来就是: Brian Goetz 在他的 Devoxx 2018 演讲中提到,我们不应该把 Integer 作为锁。



你可以通过这个链接直达这一部分的讲解,只有不到 30s秒的时间,随便练练听力:http://www.youtube.com/watch?v=4r2…



那么问题又来了?


Brian Goetz 是谁,凭什么他说的话看起来就很权威的样子?



Java Language Architect at Oracle,开发 Java 语言的,就问你怕不怕。


同时,他还是我多次推荐过的《Java并发编程实践》这本书的作者。


好了,现在也找到大佬背书了,接下来带你看看高赞回答是怎么说的。



前部分就不详说了,其实就是我们前面提到的那一些点,不能用 Integer ,涉及到缓存内、缓存外巴拉巴拉的...


关注划线的部分,我加上自己的理解给你翻译一下:


如果你真的必须用 Integer 作为锁,那么你需要搞一个 Map 或 Integer 的 Set,通过集合类做映射,你就可以保证映射出来的是你想要的明确的一个实例。而这个实例,就那可以拿来做锁。


然后他给出了这样的代码片段:



就是用 ConcurrentHashMap 然后用 putIfAbsent 方法来做一个映射。


比如多次调用 locks.putIfAbsent(200, 200),在 map 里面也只有一个值为 200 的 Integer 对象,这是 map 的特性保证的,无需过多解释。


但是这个哥们很好,为了防止有人转不过这个弯,他又给大家解释了一下。


首先,他说你也可以这样的写:



但这样一来,你就会多产生一个很小成本,就是每次访问的时候,如果这个值没有被映射,你都会创建一个 Object 对象。


为了避免这一点,他只是把整数本身保存在 Map 中。这样做的目的是什么?这与直接使用整数本身有什么不同呢?


他是这样解释的,其实就是我前面说的“这是 map 的特性保证的”:



当你从 Map 中执行 get() 时,会用到 equals() 方法比较键值。


两个相同值的不同 Integer 实例,调用 equals() 方法是会判定为相同的 。



因此,你可以传递任何数量的 "new Integer(5)" 的不同 Integer 实例作为 getCacheSyncObject 的参数,但是你将永远只能得到传递进来的包含该值的第一个实例。


就是这个意思:



汇总一句话:就是通过 Map 做了映射,不管你 new 多少个 Integer 出来,这多个 Integer 都会被映射为同一个 Integer,从而保证即使超出 Integer 缓存范围时,也只有一把锁。


除了高赞回答之外,还有两个回答我也想说一下。


第一个是这个:



不用关心他说的内容是什么,只是我看到这句话翻译的时候虎躯一震:



skin this cat ???


太残忍了吧。



我当时就觉得这个翻译肯定不太对,这肯定是一个小俚语。于是考证了一下,原来是这个意思:



免费送你一个英语小知识,不用客气。


第二个应该关注的回答排在最后:



这个哥们叫你看看《Java并发编程实战》的第 5.6 节的内容,里面有你要寻找的答案。


巧了,我手边就有这本书,于是我翻开看了一眼。


第 5.6 节的名称叫做“构建高效且可伸缩的结果缓存”:



好家伙,我仔细一看这一节,发现这是宝贝呀。


你看书里面的示例代码:



不就和提问题的这个哥们的代码如出一辙吗?



都是从缓存中获取,拿不到再去构建。


不同的地方在于书上把 synchronize 加在了方法上。但是书上也说了,这是最差的解决方案,只是为了引出问题。


随后他借助了 ConcurrentHashMap、putIfAbsent 和 FutureTask 给出了一个相对较好的解决方案。


你可以看到完全是从另外一个角度去解决问题的,根本就没有在 synchronize 上纠缠,直接第二个方法就拿掉了 synchronize。


看完书上的方案后我才恍然大悟:好家伙,虽然前面给出的方案可以解决这个问题,但是总感觉怪怪的,又说不出来哪里怪。原来是死盯着 synchronize 不放,思路一开始就没打开啊。


书里面一共给出了四段代码,解决方案层层递进,具体是怎么写的,由于书上已经写的很清楚了,我就不赘述了,大家去翻翻书就行了。


没有书的直接在网上搜“构建高效且可伸缩的结果缓存”也能搜出原文。


我就指个路,看去吧。


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

Flutter之GetX依赖注入Bindings使用详解

作用Bindings 主要配合 GetX 路由和依赖一起使用,作用是在路由跳转页面加载时注入当前页面所需的依赖关系。Bindings 的好处是能统一管理页面的依赖关系,当业务复杂时可能一个页面需要注入大量的依赖,此时使用 Bindings 能更方便的维护页面的...
继续阅读 »

作用

Bindings 主要配合 GetX 路由和依赖一起使用,作用是在路由跳转页面加载时注入当前页面所需的依赖关系。Bindings 的好处是能统一管理页面的依赖关系,当业务复杂时可能一个页面需要注入大量的依赖,此时使用 Bindings 能更方便的维护页面的依赖关系。

使用

前面说了 Bindings 需要结合 GetX 路由一起使用,而 GetX 路由分为普通路由别名路由,接下来分别看看如何使用。

首选创建一个自定义 Bindings 继承自 Bindings,比如计数器界面,创建一个 CounterBindings 在 dependencies 方法中注入 CounterController, 代码如下:

class CounterBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut(() => CounterController());
}
}

上面通过 lazyPut 懒加载方式注入的,也可以使用前面讲到的其他注入方式注入。

普通路由

普通路由使用 Bindings 很简单,在路由跳转时加上 binding 参数传入创建的自定义 Bindings 对象即可:

Get.to(CounterPage(), binding: CounterBinding());

Get.off(CounterPage(), binding: CounterBinding());

Get.offAll(CounterPage(), binding: CounterBinding());

这样通过路由进入 CounterPage 时就会自动调用 CounterBinding 的 dependencies 方法初始化注入对应的依赖,在 CounterPage 中就能正常使用 Get.find 获取到注入的 CounterController 对象。

别名路由

Flutter应用框架搭建(一)GetX集成及使用详解 一文中介绍了别名路由的使用,需要先创建 GetPage 确定别名与页面的关系并配置到 GetMaterialApp 的 getPages 中,使用时通过 Get.toNamed 进行路由跳转,而 Get.toNamed 方法并没有 binding 参数用于传入 Bindings。

使用别名路由时需要在创建 GetPage 时就传入 Bindings 对象,如下:

GetPage(name: "/counter", page: () => CounterPage(), binding: CounterBinding());

跳转时正常使用 Get.toNamed 就能达到同样的效果。

Get.toNamed("/counter");

别名路由与普通路由对于 Bindings 的使用上还有一个区别,普通路由只有一个 binding 参数,只能传入一个 Bindings 对象,而别名路由除了 binding 参数以外还有一个 bindings 参数,可传入 Bindings 数组。使用如下:

GetPage(
name: "/counter",
page: () => CounterPage(),
binding: CounterBinding(),
bindings: [PageABinding(), PageBBinding(), PageCBinding()]);

那 bindings 的作用是什么呢?为什么需要传入一个数组?

通常一个页面只需要一个 Bindings 用来管理页面的依赖,但是当使用到 ViewPager 等嵌套组件或者存在页面嵌套时,因为页面中嵌套的页面不是通过路由加载出来的所以无法自动调用 Bindings 的 dependencies 方法来初始化依赖关系,而嵌套的页面有可能也需要单独显示,为了提高页面的复用性也会为嵌套页面创建 Bindings ,这样当页面嵌套使用时就可以把嵌套页面的 Bindings 传入到主页面路由的 bindings 中,使用如下:

/// ViewPager 页面路由
GetPage(
name: "/viewpager",
page: () => ViewPagerPage(),
binding: ViewPagerBinding(),
bindings: [PageABinding(), PageBBinding(), PageCBinding()]);

/// 单独 PageA pageB pageC 路由
GetPage(
name: "/pageA",
page: () => PageAPage(),
binding: PageABinding(),);
GetPage(
name: "/pageB",
page: () => PageBPage(),
binding: PageBBinding(),);
GetPage(
name: "/pageC",
page: () => PageCPage(),
binding: PageCBinding(),);

/// 使用
Get.toNamed("/viewpager");

Get.toNamed("/pageA");
Get.toNamed("/pageB");
Get.toNamed("/pageC");

这样就能实现,当在 ViewPager 中使用时也能初始化 ViewPager 中嵌套页面的依赖,单独使用某个 Page 时也能正常加载依赖。

原理

前面讲了 Bindings 的作用和使用方法,下面通过源码简单分析一下 Bindings 的原理。

Bindings 是一个抽象类,只有一个 dependencies 抽象方法,源码如下:

abstract class Bindings {
void dependencies();
}

在页面路由中注册 Bindings 后,页面初始化时会调用 Bindings 的 dependencies 方法,初始化页面依赖,其调用是在 GetPageRoute 的 buildContent 中,而 GetPageRoute 是继承至 Flutter 的 PageRoute 即在路由跳转加载页面内容时调用, 核心源码如下:

Widget _getChild() {
if (_child != null) return _child!;
final middlewareRunner = MiddlewareRunner(middlewares);

/// 获取 Bindings
final localbindings = [
if (bindings != null) ...bindings!,
if (binding != null) ...[binding!]
];
/// 调用中间件的 onBindingsStart 方法
final bindingsToBind = middlewareRunner.runOnBindingsStart(localbindings);

/// 调用 Bindings 的 dependencies 方法
if (bindingsToBind != null) {
for (final binding in bindingsToBind) {
binding.dependencies();
}
}

final pageToBuild = middlewareRunner.runOnPageBuildStart(page)!;
_child = middlewareRunner.runOnPageBuilt(pageToBuild());
return _child!;
}

@override
Widget buildContent(BuildContext context) {
return _getChild();
}

源码核心代码就是在创建页面 Widget 时获取路由传入的 Bindings ,然后依次调用 Bindings 的 dependencies 方法。

其中:

  /// 获取 Bindings
final localbindings = [
if (bindings != null) ...bindings!,
if (binding != null) ...[binding!]
];
/// 调用中间件的 onBindingsStart 方法
final bindingsToBind = middlewareRunner.runOnBindingsStart(localbindings);

/// 调用 Bindings 的 dependencies 方法
if (bindingsToBind != null) {
for (final binding in bindingsToBind) {
binding.dependencies();
}
}

就是将路由中传入的 bindings 和 binding 取出放入同一个数组。然后依次调用 dependencies 方法,其中 binding 就是路由或 GetPage 中传入的 binding 参数,而 bindings 就是使用别名路由时在 ``GetPage 中传入的 Bindings 数组。

总结

本文通过介绍在 GetX 依赖注入中 Bindings 的作用以及使用方法,再结合 GetX 的源码分析了 Bindings 的实现原理,更进一步了解了 Bindings 为什么能实现页面依赖注入的管理,希望通过源码让大家更好的理解 GetX 中的 Bindings ,从而在开发中灵活使用 Bindings 管理页面所需的依赖。


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

收起阅读 »

解决 Flutter 嵌套过深,是选择函数还是自定义类组件?

前言 初学 Flutter 的时候,一个很大的感受就是组件嵌套层级很深,写下来的代码找对应的括号都找不到。比如下面这种情况,从最外层的 Scaffold 到最里层的 Image.asset,一共有7层组件嵌套。这还不算多的,最夸张是见过一个表单页面写了10多层...
继续阅读 »

前言


初学 Flutter 的时候,一个很大的感受就是组件嵌套层级很深,写下来的代码找对应的括号都找不到。比如下面这种情况,从最外层的 Scaffold 到最里层的 Image.asset,一共有7层组件嵌套。这还不算多的,最夸张是见过一个表单页面写了10多层,代码的阅读体验非常糟糕,而且如果不小心删除了一个括号要找半天才对应得上。当然,通过 VSCode 彩虹括号(Rainbow Brackets)这个插件能够一定程度上解决括号对称查找得问题,但是代码的可维护性、阅读体验还是很差。自然而然,大家会想到拆分。拆分有两种方式,一种是使用返回Widget 的函数,另一种是使用 StatelessWidget,那这两种该如何选择呢?


image.png


拆分原则


在关于这个问题的讨论上,2年前 StackOverflow 有一个经典的回答:使用函数和使用类来构建可复用得组件有什么区别?,大家可以去看看。其中提到得一个关键因素是 Flutter 框架能够检测组件树的类对象,从而提高复用性。而对于私有的方法来说 Flutter 在更新的时候并不知道该如何处理。


image.png


答主也对比了使用类和函数的优劣势。使用类构建的方式:



  • 支持性能优化,比如使用 const 构造方法,更细颗粒度的刷新;

  • 两个不同的布局切换时,能够正确地销毁对应得资源。这个我们在上篇讲 StatefulWidget 的时候有介绍过。

  • 保证正确的方式进行热重载,而使用函数可能破坏热重载。

  • 在 Widget Inspector 中可以查看得到,从而可以方便我们定位和调试问题。

  • 更友好的错误提示。当组件树出现错误时,框架会给出当前构建得组件名称,而如果使用函数的话则得不到清晰得名词。

  • 可以使用 key 提高性能。

  • 可以使用 context 提供的方法(函数式组件除非显示地传递 context)。


使用函数构建组件唯一的优势就是代码量会更少(这可以通过 functional_widget 插件解决,functional_widget 是一个通过注解将和函数式组件构建方式自动转换为类组件的代码生成插件)。


示例对比


下面我们看一段没有拆分的代码,这个仅仅是示例代码,没有任何实际意义。


class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int _counter = 0;

@override
Widget build(BuildContext context) {
return Row(
children: [
Text('Counter: $_counter'),
Container(
child: Column(
children: [
Text('Hello'),
Row(
children: [
Text('there'),
Text('world!'),
],
),
],
),
),
],
);
}
}

括号有点多,对吧,一眼看过去都懵圈了 —— 这也是很多初次接触 Flutter 的人吐槽地方,可以说让不少人直接放弃了! 最直接的方式就是将部分代码抽离成为一个私有方法,比如像下面这样。


class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int _counter = 0;

Widget _buildNonsenseWidget() {
return Container(
child: Column(
children: [
Text('Hello'),
Row(
children: [
Text('there'),
Text('world!'),
],
),
],
),
);
}

@override
Widget build(BuildContext context) {
return Row(
children: [
Text('Counter: $_counter'),
_buildNonsenseWidget(),
],
);
}
}

将深度嵌套的组件代码单独抽成了一个返回 Widget 的私有方法,看起来确实让代码简洁不少。
那么问题就解决了吗?我们来看一下当状态改变的时候会发生什么。
我们知道,当状态变量_counter改变后,Flutter 会调用 build 方法刷新组件。这会导致 _buildNonsenseWidget 这个方法在刷新的时候每次都会被调用,意味着每次都会创建新的组件来替换旧的组件,即便两个组件没有任何改变。而事实上,我们应该只重建那些变化的组件,从而提高性能。
现在再来看使用类组件的方式,实际上有代码模板的情况下,编写一个 StatelessWidget 非常简单。使用类组件后的代码如下所示。代码确实会比函数的方式多,但是实际上大部分不需要我们手敲。


class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int _counter = 0;

@override
Widget build(BuildContext context) {
return Row(
children: [
Text('Counter: $_counter'),

// The deeply nesting widget is now refactored into a
// stateless const widget. No more needless rebuilding!
const _NonsenseWidget(),
],
);
}
}

class _NonsenseWidget extends StatelessWidget {
const _NonsenseWidget();

@override
Widget build(BuildContext context) {
return Container(
child: Column(
children: [
Text('Hello'),
Row(
children: [
Text('there'),
Text('world!'),
],
),
],
),
);
}
}

这里注意,以为这个_NonsenseWidget 在组件得声明周期不会改变,因此使用了 const 的构造方法。这样在刷新过程中,就不会重新构建了!关于 const 可以参考之前的两篇文章。


关于 StatefulWidget,你不得不知道的原理和要点!


解密 Flutter 的 const 关键字


总结


相比使用函数构建复用的组件代码,请尽可能地使用类组件的方式,而且尽可能地将组件拆分为小一点的单元。这样一方面可以提供精确的刷新,另一方面则是可以将组件复用到其他页面中。如果你不想改变自己得习惯,那么可以考虑使用 functional_widget 这个插件来自动生成类组件。


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

一天一个经典算法:桶排序

桶排序也叫箱排序,工作的原理是将数组分到有限数量的桶里。每个桶再进行排序排序,可能再使用别的排序算法或者是以递归的方式继续使用桶排序进行排序,桶排序是鸽巢排序的一种归纳结果。 当输入在一个范围内均匀分布时,桶排序非常好用。 例如:对范围从0.0到1.0且均匀分...
继续阅读 »

桶排序也叫箱排序,工作的原理是将数组分到有限数量的桶里。每个桶再进行排序排序,可能再使用别的排序算法或者是以递归的方式继续使用桶排序进行排序,桶排序是鸽巢排序的一种归纳结果。


当输入在一个范围内均匀分布时,桶排序非常好用。


例如:对范围从0.0到1.0且均匀分布在该范围内的大量浮点数进行排序。


创建桶算法的方法:



  1. 创建n个空桶(列表)。

  2. 对每个数组元素arr[i]插入bucket[n*array[i]]

  3. 使用插入排序对各个桶进行排序

  4. 连接所有的排序桶


Java示例:


import java.util.*;
import java.util.Collections;

class GFG {

// 使用桶排序对大小为 n 的 arr[] 进行排序
static void bucketSort(float arr[], int n)
{
if (n <= 0)
return;

// 1) 创建 n 个空桶
@SuppressWarnings("unchecked")
Vector<Float>[] buckets = new Vector[n];

for (int i = 0; i < n; i++) {
buckets[i] = new Vector<Float>();
}

// 2) 将数组元素放在不同的桶中
for (int i = 0; i < n; i++) {
float idx = arr[i] * n;
buckets[(int)idx].add(arr[i]);
}

// 3) 对单个存储桶进行排序
for (int i = 0; i < n; i++) {
Collections.sort(buckets[i]);
}

// 4) 将所有桶连接到 arr[]
int index = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < buckets[i].size(); j++) {
arr[index++] = buckets[i].get(j);
}
}
}

public static void main(String args[])
{
float arr[] = { (float)0.897, (float)0.565,
(float)0.656, (float)0.1234,
(float)0.665, (float)0.3434 };

int n = arr.length;
bucketSort(arr, n);

System.out.println("排序后的数组为 ");
for (float el : arr) {
System.out.print(el + " ");
}
}
}


输出


排序后的数组为
0.1234 0.3434 0.565 0.656 0.665 0.897

性能


时间复杂度: 如果我们假设在桶中插入需要 O(1) 时间,那么上述算法的第 1 步和第 2 步显然需要 O(n) 时间。如果我们使用链表来表示桶,O(1) 很容易实现。第 4 步也需要 O(n) 时间,因为所有桶中都会有 n 个项目。 
分析的主要步骤是步骤 3。如果所有数字均匀分布,这一步平均也需要 O(n) 时间。


包含负数的情况


上面的例子是桶排序时在对大于零的数组进行排序,对于包含负数的情况需要用下述的方法解决。



  1. 将数组拆分为两部分创建两个空向量 Neg[], Pos[](分别存正数和负数)通过转换将所有负,元素存储在 Neg[],变为正数(Neg[i] = -1 * Arr[i]),将所有 +ve 存储在 pos[] (pos[i] = Arr[i])

  2. 调用函数bucketSortPositive(Pos, pos.size()),调用函数 bucketSortPositive(Neg, Neg.size()),bucketSortPositive(arr[], n)

  3. 创建n个空桶(或列表)。

  4. 将每个数组元素 arr[i] 插入 bucket[n*array[i]]

  5. 使用插入排序对单个桶进行排序。

  6. 连接所有排序的桶。


Java示例


import java.util.*;
class GFG
{

// 使用桶排序对大小为 n 的 arr[] 进行排序
static void bucketSort(Vector<Double> arr, int n)
{

// 1) 创建 n 个空桶
@SuppressWarnings("unchecked")
Vector<Double> b[] = new Vector[n];
for (int i = 0; i < b.length; i++)
b[i] = new Vector<Double>();

// 2) 将数组元素放在不同的桶中
for (int i = 0; i < n; i++)
{
int bi = (int)(n*arr.get(i)); // 桶中索引
b[bi].add(arr.get(i));
}

// 3) 对单个存储桶进行排序
for (int i = 0; i < n; i++)
Collections.sort(b[i]);

// 4) 将所有桶连接到 arr[]
int index = 0;
arr.clear();
for (int i = 0; i < n; i++)
for (int j = 0; j < b[i].size(); j++)
arr.add(b[i].get(j));
}

// 这个函数主要是把数组一分为二,然后对两个数组调用bucketSort()。
static void sortMixed(double arr[], int n)
{
Vector<Double>Neg = new Vector<>();
Vector<Double>Pos = new Vector<>();

// 遍历数组元素
for (int i = 0; i < n; i++)
{
if (arr[i] < 0)

// 通过转换为 +ve 元素来存储 -Ve 元素
Neg.add (-1 * arr[i]) ;
else

// 存储 +ve 元素
Pos.add (arr[i]) ;
}
bucketSort(Neg, (int)Neg.size());
bucketSort(Pos, (int)Pos.size());

// 首先通过转换为 -ve 存储 Neg[] 数组的元素
for (int i = 0; i < Neg.size(); i++)
arr[i] = -1 * Neg.get( Neg.size() -1 - i);

// 排序
for(int j = Neg.size(); j < n; j++)
arr[j] = Pos.get(j - Neg.size());
}

public static void main(String[] args)
{
double arr[] = {-0.897, 0.565, 0.656,
-0.1234, 0, 0.3434};
int n = arr.length;
sortMixed(arr, n);

System.out.print("排序后的数组: \n");
for (int i = 0; i < n; i++)
System.out.print(arr[i] + " ");
}0
}

**输出: **


排序后的数组:
-0.897 -0.1234 0 0.3434 0.565 0.656

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

面试官:你都工作3年了,这个算法题都不会?

前言 金三银四,又到了换工作的最佳时机,我幻想着只要跳个槽,就能离开这个”鸟地方“,拿着更多的钱,干着最爽的事... 然而现实总是残酷的,最近有个学妹在换工作,面试前什么手写Priomise、vue双向绑定原理,webpack优化方式,准备了一大堆,本以...
继续阅读 »

前言



金三银四,又到了换工作的最佳时机,我幻想着只要跳个槽,就能离开这个”鸟地方“,拿着更多的钱,干着最爽的事...




然而现实总是残酷的,最近有个学妹在换工作,面试前什么手写Priomisevue双向绑定原理,webpack优化方式,准备了一大堆,本以为成竹在胸,结果却在算法上吃了大亏,心仪的offer没有拿到,一度怀疑人生。到底是什么算法题能让面试官对妹子说出你都工作3年了,这个算法题都不会?这样的狠话?



有效的括号问题



这是一道leetcode上的原题,本意是在考察候选人对数据结构的掌握。来看看题目



给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。
有效字符串需满足:



  1. 左括号必须用相同类型的右括号闭合。

  2. 左括号必须以正确的顺序闭合。


示例



示例 1:
输入:s = "()"
输出:true

示例 2:
输入:s = "()[]{}"
输出:true

示例 3:
输入:s = "(]"
输出:false

示例 4:
输入:s = "([)]"
输出:false

示例 5:
输入:s = "{[]}"
输出:true


解题信息



如果咱们确实没有刷过算法,不知道那么多套路,通过题目和示例尽可能的获取到更多的信息就很重要了。



根据题目推断出:



  1. 字符串s的长度一定是偶数,不可能是奇数(一对对匹配)。

  2. 右括号前面一定跟着左括号,才符合匹配条件,具备对称性。

  3. 右括号前面如果不是左括号,一定不是有效的括号。


暴力消除法



得到了以上这些信息后,胖头鱼想既然是[]{}()成对的出现,我能不能把他们都挨个消除掉,如果最后结果是空字符串,那不就意味着符合题意了吗?



举个例子


输入:s = "{[()]}"

第一步:可以消除()这一对,结果s还剩{[]}

第二步: 可以消除[]这一对,结果s还剩{}

第三步: 可以消除{}这一对,结果s还剩'' 所以符合题意返回true


代码实现


const isValid = (s) => {
while (true) {
let len = s.length
// 将字符串按照匹配对,挨个替换为''
s = s.replace('{}', '').replace('[]', '').replace('()', '')
// 有两种情况s.length会等于len
// 1. s匹配完了,变成了空字符串
// 2. s无法继续匹配,导致其长度和一开始的len一样,比如({],一开始len是3,匹配完还是3,说明不用继续匹配了,结果就是false
if (s.length === len) {
return len === 0
}
}
}


暴力消除法最终还是可以通过leetcode的用例,就是性能差了点,哈哈


image.png


栈解题法



解题信息中的第2条强调对称性,而栈(后入先出)入栈和出栈恰好是反着来,形成了鲜明的对称性。



入栈:abc,出栈:cba


abc
cba


所以可以试试从的角度来解析:


输入:s = "{[()]}"

第一步:读取ch = {,属于左括号,入栈,此时栈内有{
第二步:读取ch = [,属于左括号,入栈,此时栈内有{[
第三步:读取ch = (,属于左括号,入栈,此时栈内有{[(
第四步:读取ch = ),属于右括号,尝试读取栈顶元素(和)正好匹配,将(出栈,此时栈内还剩{[
第五步:读取ch = ],属于右括号,尝试读取栈顶元素[和]正好匹配,将[出栈,此时栈内还剩{
第六步:读取ch = },属于右括号,尝试读取栈顶元素{和}正好匹配,将{出栈,此时栈内还剩''
第七步:栈内只能'',s = "{[()]}"符合有效的括号定义,返回true


代码实现


const isValid = (s) => {
// 空字符串符合条件
if (!s) {
return true
}

const leftToRight = {
'(': ')',
'[': ']',
'{': '}'
}
const stack = []

for (let i = 0, len = s.length; i < len; i++) {
const ch = s[i]
// 左括号
if (leftToRight[ch]) {
stack.push(ch)
} else {
// 右括号开始匹配
// 1. 如果栈内没有左括号,直接false
// 2. 有数据但是栈顶元素不是当前的右括号
if (!stack.length || leftToRight[ stack.pop() ] !== ch) {
return false
}
}
}

// 最后检查栈内还有没有元素,有说明还有未匹配则不符合
return !stack.length
}


暴力解法虽然符合我们日常的思维,但是果然还是栈结构解法好了不少。


image.png


结尾



面试中,算法到底该不该成为考核候选人的重要指标咱们不吐槽,但是近几年几乎每个大厂都将算法放进了前端面试的环节,为了获得心仪的offer,重温数据结构,刷刷题还是很有必要的,愿你我都被算法温柔以待。


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