注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Flutter实现掘金App点赞效果

前言 点赞这个动作不得不说在社交、短视频等App中实在是太常见了,当用户手指按下去的那一刻,给用户一个好的反馈效果也是非常重要的,这样用户点起赞来才会有一种强烈的我点了赞的效果,那么今天我们就用Flutter实现一个掘金App上的点赞效果。 首先我们看下掘金...
继续阅读 »

前言


点赞这个动作不得不说在社交、短视频等App中实在是太常见了,当用户手指按下去的那一刻,给用户一个好的反馈效果也是非常重要的,这样用户点起赞来才会有一种强烈的我点了赞的效果,那么今天我们就用Flutter实现一个掘金App上的点赞效果。



  • 首先我们看下掘金App的点赞组成部分,有一个小手,点赞数字、点赞气泡效果,还有一个震动反馈,接下来我们一步一步实现。


知识点:绘制、动画、震动反馈


1、绘制小手


这里我们使用Flutter的Icon图标中的点赞小手,Icons图标库为我们提供了很多App常见的小图标,如果使用苹果苹果风格的小图标可以使用cupertino_icons: ^1.0.2插件,图标并不是图片,本质上和emoji图标一样,可以添加到文本中使用,所以图标才可以设置不同的颜色属性,对比使用png格式图标可以节省不少的内存。
image.png
接下来我们就将这两个图标绘制出来,首先我们从上图可以看到真正的图标数据其实是IconData类,里面有一个codePoint属性可以获取到Unicode统一码,通过String.fromCharCode(int charCode)可以返回一个代码单元,在Text文本中支持显示。


class IconData{
/// The Unicode code point at which this icon is stored in the icon font.
/// 获取此图标的Unicode代码点
final int codePoint;
}

class String{
/// 如果[charCode]可以用一个UTF-16编码单元表示,则新的字符串包含一个代码单元
external factory String.fromCharCode(int charCode);
}

接下来我们就可以把图标以绘制文本的形式绘制出来了

关键代码:


  // 赞图标
final icon = Icons.thumb_up_alt_outlined;
// 通过TextPainter可以获取图标的尺寸
TextPainter textPainter = TextPainter(
text: TextSpan(
text: String.fromCharCode(icon.codePoint),
style: TextStyle(
fontSize: 30,
fontFamily: icon.fontFamily,// 字体形象家族,这个字段一定要设置,不然显示不出来
color: Colors.black)),
textAlign: TextAlign.center,
textDirection: TextDirection.ltr);
textPainter.layout(); // 进行布局
Size size2 = textPainter.size; // 尺寸必须在布局后获取
//将图标偏移到画布中央
textPainter.paint(canvas, Offset(-size2.width / 2, -size2.height / 2));

通过上方代码我们就实现了将图标绘制到画板当中

image.png

接下来继续绘制点赞数量,

代码:


TextPainter textPainter2 = TextPainter(
text: TextSpan(
text: "点赞",// 点赞数量
style: TextStyle(
fontSize: 9, fontWeight: FontWeight.w500, color: Colors.black)),
textAlign: TextAlign.center,
textDirection: TextDirection.ltr);
textPainter2.layout(); // 进行布局
// 向右上进行偏移在小手上面
textPainter2.paint(canvas, Offset(size.width / 9, -size.height / 2 + 5));

然后图标就变成了这样样子,

image.png

我们看到,掘金App点赞的过程中,周围还有一些小气泡的效果,这里提供一个思路,将这些气泡的坐标点放到一个圆的外环上面,通过动画改变圆的半径达到小圆点由内向外发散,发散的同时改变小圆点的大小,从而达到气泡的效果,
关键代码:


var r = size.width / 2 - 15; // 半径
var d = 4; // 偏移量 气泡的移动距离

// 绘制小圆点 一共4个 掘金也是4个 角度可以自由发挥 这里根据掘金App的发散角度定义的
canvas.drawPoints(
ui.PointMode.points,
[
Offset((r + d * animation2.value) * cos(pi - pi / 18 * 2),
(r + d * animation2.value) * sin(pi - pi / 18 * 2)),
Offset((r + d * animation2.value) * cos(pi + pi / 18 * 2),
(r + d * animation2.value) * sin(pi + pi / 18 * 2)),
Offset((r + d * animation2.value) * cos(pi * 1.5 - pi / 18),
(r + d * animation2.value) * sin(pi * 1.5 - pi / 18)),
Offset((r + d * animation2.value) * cos(pi * 1.5 + pi / 18 * 5),
(r + d * animation2.value) * sin(pi * 1.5 + pi / 18 * 5)),
],

_paint
..strokeWidth = 5
..color = Colors.blue
..strokeCap = StrokeCap.round);

得到现在的图形,
发散前

image.png

发散后

image.png

接下来继续我们来添加交互效果,添加动画,如果有看上一篇吃豆人,相信这里就很so easy了,首先创建两个动画类,控制小手和气泡,再创建两个变量,是否点赞和点赞数量,代码:


late Animation<double> animation; // 赞
late Animation<double> animation2; // 小圆点
ValueNotifier<bool> isZan = ValueNotifier(false); // 记录点赞状态 默认没点赞
ValueNotifier<int> zanNum = ValueNotifier(0); // 记录点赞数量 默认0点赞

这里我们需要使用动画曲线CurvedAnimation这个类,这个类可以实现不同的0-1的运动曲线,根据掘金的点赞效果,比较符合这个曲线规则,快速放大,然后回归正常大小,这个类帮我们实现了很多好玩的运动曲线,有兴趣的小伙伴可以尝试下其他运动曲线。

小手运动曲线

f299df8cd653f6f24de8553bebf055d2.gif

气泡运动曲线:

2222.gif

有了运动曲线之后,接下来我们只需将属性赋值给小手手和小圆点就好了,

完整源码: 封装一下,对外暴露大小,就是一个点赞组件了。


class ZanDemo extends StatefulWidget {
const ZanDemo({Key? key}) : super(key: key);

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

class _ZanDemoState extends State<ZanDemo> with TickerProviderStateMixin {
late Animation<double> animation; // 赞
late Animation<double> animation2; // 小圆点
ValueNotifier<bool> isZan = ValueNotifier(false); // 记录点赞状态 默认没点赞
ValueNotifier<int> zanNum = ValueNotifier(0); // 记录点赞数量 默认0点赞

late AnimationController _controller; // 控制器
late AnimationController _controller2; // 小圆点控制器
late CurvedAnimation cure; // 动画运行的速度轨迹 速度的变化
late CurvedAnimation cure2; // 动画运行的速度轨迹 速度的变化

int time = 0;// 防止快速点两次赞导致取消赞

@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this, duration: const Duration(milliseconds: 500)); //500ms
_controller2 = AnimationController(
vsync: this, duration: const Duration(milliseconds: 800)); //500ms

cure = CurvedAnimation(parent: _controller, curve: Curves.easeInOutBack);
cure2 = CurvedAnimation(parent: _controller2, curve: Curves.easeOutQuint);
animation = Tween(begin: 0.0, end: 1.0).animate(cure);
animation2 = Tween(begin: 0.0, end: 1.0).animate(_controller2);
}

@override
Widget build(BuildContext context) {
return InkWell(
child: Center(
child: CustomPaint(
size: Size(50, 50),
painter: _ZanPainter(animation, animation2, isZan, zanNum,
Listenable.merge([animation, animation2, isZan, zanNum])),
),
),
onTap: () {
if (!isZan.value && !_isDoubleClick()) {
_controller.forward(from: 0);
// 延迟300ms弹窗气泡
Timer(Duration(milliseconds: 300), () {
isZan.value = true;
_controller2.forward(from: 0);
});
Vibrate.feedback(FeedbackType.success);
zanNum.value++;
} else if (isZan.value) {
Vibrate.feedback(FeedbackType.success);
isZan.value = false;
zanNum.value--;
}
},
);
}

bool _isDoubleClick() {
if (time == 0) {
time = DateTime.now().microsecondsSinceEpoch;
return false;
} else {
if (DateTime.now().microsecondsSinceEpoch - time < 800 * 1000) {
return true;
} else {
time = DateTime.now().microsecondsSinceEpoch;
return false;
}
}
}
}

class _ZanPainter extends CustomPainter {
Animation<double> animation;
Animation<double> animation2;
ValueNotifier<bool> isZan;
ValueNotifier<int> zanNum;
Listenable listenable;

_ZanPainter(
this.animation, this.animation2, this.isZan, this.zanNum, this.listenable)
: super(repaint: listenable);

Paint _paint = Paint()..color = Colors.blue;
List<Offset> points = [];

@override
void paint(Canvas canvas, Size size) {
canvas.clipRect(Offset.zero & size);
canvas.translate(size.width / 2, size.height / 2);
// 赞
final icon =
isZan.value ? Icons.thumb_up_alt_rounded : Icons.thumb_up_alt_outlined;
// 通过TextPainter可以获取图标的尺寸
TextPainter textPainter = TextPainter(
text: TextSpan(
text: String.fromCharCode(icon.codePoint),
style: TextStyle(
fontSize: animation.value < 0 ? 0 : animation.value * 30,
fontFamily: icon.fontFamily,
color: isZan.value ? Colors.blue : Colors.black)),
textAlign: TextAlign.center,
textDirection: TextDirection.ltr);
textPainter.layout(); // 进行布局
Size size2 = textPainter.size; // 尺寸必须在布局后获取
//将图标偏移到画布中央
textPainter.paint(canvas, Offset(-size2.width / 2, -size2.height / 2));

var r = size.width / 2 - 15; // 半径
var d = 4; // 偏移量

canvas.drawPoints(
ui.PointMode.points,
[
Offset((r + d * animation2.value) * cos(pi - pi / 18 * 2),
(r + d * animation2.value) * sin(pi - pi / 18 * 2)),
Offset((r + d * animation2.value) * cos(pi + pi / 18 * 2),
(r + d * animation2.value) * sin(pi + pi / 18 * 2)),
Offset((r + d * animation2.value) * cos(pi * 1.5 - pi / 18 * 1),
(r + d * animation2.value) * sin(pi * 1.5 - pi / 18 * 1)),
Offset((r + d * animation2.value) * cos(pi * 1.5 + pi / 18 * 5),
(r + d * animation2.value) * sin(pi * 1.5 + pi / 18 * 5)),
],
_paint
..strokeWidth = animation2.value <= 0.5 ? (5 * animation2.value) / 0.5
: 5 * (1 - animation2.value) / 0.5
..color = Colors.blue
..strokeCap = StrokeCap.round);
TextPainter textPainter2 = TextPainter(
text: TextSpan(
text: zanNum.value == 0 ? "点赞" : zanNum.value.toString(),
style: TextStyle(
fontSize: 9, fontWeight: FontWeight.w500, color: Colors.black)),
textAlign: TextAlign.center,
textDirection: TextDirection.ltr);
textPainter2.layout(); // 进行布局
// 向右上进行偏移在小手上面
textPainter2.paint(canvas, Offset(size.width / 9, -size.height / 2 + 5));
}

@override
bool shouldRepaint(covariant _ZanPainter oldDelegate) {
return oldDelegate.listenable != listenable;
}
}

到这里发现是不是少了点什么,不错,还少了震动的效果,这里我们引入flutter_vibrate: ^1.3.0这个插件,这个插件是用来管理设备震动效果的,Andoroid端记得加入震动权限,

<uses-permission android:name="android.permission.VIBRATE"/>

使用方法也很简单,这个插件封装了一些常见的提示震动,比如操作成功、操作警告、操作失败等,其实就是震动时间的长短,这里我们就在点赞时候调用Vibrate.feedback(FeedbackType.success);有一个点击成功的震动就好了。



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

kotlin协程简介

技术是永无止境的,需要不断地学习总结。 什么是协程? 协程就像非常轻量级的线程。线程是由系统调度的,线程切换或线程阻塞的开销都比较大。而协程依赖于线程,但是协程挂起时不需要阻塞线程,几乎是无代价的,协程是由开发者控制的。所以协程也像用户态的线程,非常轻量级,一...
继续阅读 »

技术是永无止境的,需要不断地学习总结。


什么是协程?


协程就像非常轻量级的线程。线程是由系统调度的,线程切换或线程阻塞的开销都比较大。而协程依赖于线程,但是协程挂起时不需要阻塞线程,几乎是无代价的,协程是由开发者控制的。所以协程也像用户态的线程,非常轻量级,一个线程中可以创建任意个协程。


1. GlobalScope 的使用(不推荐),绑定的为应用的整个生命周期,GlobalScope是生命周期是process级别的,即使Activity或Fragment已经被销毁,协程仍然在执行。所以需要绑定生命周期。


添加依赖如下:


implementation"org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9"

implementation"org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"

kotlin 中 GlobalScope 类提供了几个创建协程的构造函数:


launch: 创建协程


async : 创建带返回值的协程,返回的是 Deferred 类


withContext:不创建新的协程,指定协程上运行代码块


runBlocking:不是 GlobalScope 的 API,可以独立使用,区别是 runBlocking 里面的 delay 会阻塞线程,而 launch 创建的不会


image.png


2、lifecycleScope (推荐使用) lifecycleScope只能在Activity、Fragment中使用,会绑定Activity和Fragment的生命周期


**lifecycleScope会绑定调用者的生命周期,因此通常情况下不需要手动去停止

**


添加依赖如下:


implementation'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'//lifecycleScope

implementation'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'//viewModelScope

image.png


1.在不使用回调的前提下完成来线程的切换,代码看上亲也是干净整洁很多。


2.因为线程没有上下文,不能控制线程执行完成后应该回到哪里,但是协程完全帮我们实现自动化,执行完毕自动回到上下文线程中,一般情况下是主线程,可以通过设置来决定要回到哪个线程中。


3.协程可以通过suspend关键字来标志耗时操作,通过编译器来帮助我们避免一些性能上的问题。


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

穿越到2030年,我找到了窝边草华丽转身的方案

大家好我是外卖小哥,就在昨天晚上大家都在睡觉的时候,我一个人骑车我的小摩托驰骋在安静的夜色中,一不小心竟然穿越到了2030年。我很激动,感觉我的外卖生涯明天就要结束。首先想到的就是去看采飘号码,但是发现搜不到8年前的数据,我还要多送好多年外卖才能迎来命运的转折...
继续阅读 »

大家好我是外卖小哥,就在昨天晚上大家都在睡觉的时候,我一个人骑车我的小摩托驰骋在安静的夜色中,一不小心竟然穿越到了2030年。

我很激动,感觉我的外卖生涯明天就要结束。

首先想到的就是去看采飘号码,但是发现搜不到8年前的数据,我还要多送好多年外卖才能迎来命运的转折点,于是放弃了。

我很快冷静下来,我要以最短的时间最有效的方法把握这次通往财务自由的机会。

我想2030年全球最大的互联网公司不就是FaceChat吗?而2022年FaceChat还没诞生,我要找到它的创始人,并加入他们。

查阅资料发现,FaceChat前身竟是窝边草,我怎么也想不通那么烂的软件怎么在短短几年崛起为IM霸主,继续查阅,我发现了这份方案(下文“我”是FaceChat副总裁):

2022年夏天,我以产品经理的身份发起一款对标窝边草2.0的APP开发工作,并取名FaceChat,采用主流技术现代化审美,预计周期一个月,并预留10天完善。

可是我没有钱也没有人怎么办?走球长时薪招一群摸鱼佬肯定不行。我要高薪聘请三名技术人员,并带他们一起致富。

于是我发出面向996的招聘信息,月薪3万、业绩7万,次月中旬发上月工资。三个全栈按模块分工均参与rn/uniapp/taro跨端开发与服务端开发,我负责产品设计、系统集成和运营,当然也参与开发。

一个月后FaceChat诞生,也急需30万给他们发工资,于是我找到球长,告诉他我有一个FaceChat是比窝边草好一百倍的APP,窝边草有的它都有,当然bugs除外。如果球长愿意收购我的产品和团队只需要300w即可。如果不收购的话窝边草的颜值加内伤是竞争不过FaceChat的。

一个月后,窝边草焕然一新,并以FaceChat的新名字面世。我和几位开发人员也顺利成为窝边草高管,从此,窝边草正式步入快速发展阶段,从国内到国际,从23个用户到23亿用户不过短短3年时间,由于业务发展需要,这期间还收购了环信等一大批优秀企业。

好了各位,剧本都给你们找来了,都对号入座吧。产品经理是谁我就不透露了,三个开发你们可以积极去争取,还有通知下球长准备300w。

有人问我,HX被收购后,阿花呢?小爬呢?他们后来嫁给谁了?他们2030年分别是FaceChat亚太地区产品总监和市场总监,至于嫁给谁了等我送完外卖给你们讲下一个故事。

收起阅读 »

【Flutter】Dart语法之List & Map

【Flutter】学习笔记——Dart中的List & Map的使用 list列表,相当于 OC 中的 NSArray 数组,分为可变和不可变两种。 map键值对,相当于 OC 中的 NSDicti...
继续阅读 »
【Flutter】学习笔记——Dart中的List & Map的使用



  • list列表,相当于 OC 中的 NSArray 数组,分为可变不可变两种。

  • map键值对,相当于 OC 中的 NSDictionary 字典,也分为可变不可变两种。



1. list数组



list默认都是可变的,列表中可以添加不同数据类型的数据。



1.1 可变list

void main() { 
// 直接 list创建
List a = ["1", 2, "3.0", 4.0];
print(a);

// var 创建
var list = [1, 2, "zjp", 3.0];
print(list);
}

运行结果如下:


image.png


1.2 常用方法

获取&修改指定下标数据:


// 直接获取指定下标数据 
print(list[3]);
// 直接修改指定下标数据
list[3] = "reno";

插入数据:


list.insert(1, "hellow"); // list.insert(index, element)
print(list);

删除数据:


list.remove(1); // list.remove(element)
print(list);

清空所有数据:


list.clear();
print(list);

运行结果如下:


image.png


1.3 排序和截取

void main() {
List b = [3, 4, 5, 8, 6, 7];
// 排序
b.sort();
print(b);
// 截取
print(b.sublist(1, 3));
}

运行结果如下:


image.png


1.4 不可变list


不可变的 list 需要使用const修饰。



void main() {
List b = const [3, 4, 5, 8, 6, 7];
b[3] = 10; // 报错
}

不可变list不能修改其元素值,否则会报错


image.png


2. map键值对



map默认也是可变的。



2.1 可变map

void main() {
Map a = {"a": 1, "b": 2};
print(a);

var a1 = {"a1": 1, "a2": 2};
print(a1);
}

运行结果如下:


image.png


2.2 常用方法

获取&修改指定下标数据:


// 直接获取指定下标数据 
print(a["a"]);
// 直接修改指定下标数据
a["a"] = "aa";
print(a["a"]);

获取map长度


print(a.length);

获取map所有的key


print(a.keys);

获取map所有的value


print(a.values);

运行结果如下:


image.png


2.3 不可变map


不可变的 map 也是使用const修饰。



void main() {
Map a = const {"a": 1, "b": 2};
a["a"] = 10; // 报错
}

不可变map不能修改其元素值,否则也会报错


image.png


3. list转map


void main() {
List b = ["zjp", "reno"];
print(b.asMap());
}

运行结果如下:



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

Flutter popUntil 黑屏

在flutter 路由跳转中,我们想要回到特定的一个页面 比如:从 A -> B-> C ->D,我们向从 D页面 pop至 B 页面。我们可以使用 popUtil方法回到 B 页面。Navigator.popUnitil(cont...
继续阅读 »

在flutter 路由跳转中,我们想要回到特定的一个页面 比如:从 A -> B-> C ->D,我们向从 D页面 pop至 B 页面。我们可以使用 popUtil方法回到 B 页面。

Navigator.popUnitil(context, ModalRoute.withName('/B'))

或者使用

   Navigator.popUntil(ctx.context, (route){
if (route.settings.name == "/B"){
return true;
}else {
return false;
}
});

但是 运行结果是 : 黑屏。

我们对 route.setting 进行打印后,发现 route.setting == null只有最后 一个A页面的route.setting有值,其name == '/'

所以,我们在跳转至B页面的时候,需要给B页面的routeSetting进行赋值,

  Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>BPage(),
settings: RouteSettings(name: '/B'),
));

这样就可以回到B页面了


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

收起阅读 »

flutter倒计时控件

使用方式1 默认为倒计时 CountdownWidget( 5, ///倒计时的时间 onClick: () { /// 点击事件的回调 _skip2main(); }, onFinish: () { ///倒计...
继续阅读 »

使用方式1 默认为倒计时


CountdownWidget(
5, ///倒计时的时间
onClick: () { /// 点击事件的回调
_skip2main();
},
onFinish: () { ///倒计时完成的回调
_skip2main();
},
)

使用方式2修改圆角和文案



CountdownWidget(
total: 10,
content: "已发送",
textColor: Colors.blue,
borderRadius: 2,
onClick: () {
_skip2main();
},
onFinish: () {
_skip2main();
},
)

倒计时实现


import 'dart:async';

import 'package:bilibili_flutter/common/base/base_state.dart';
import 'package:bilibili_flutter/common/base/base_widget.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/framework.dart';

///用于splash界面中倒计时按钮

class CountdownWidget extends BiliWidget {
///构造中传入函数
final VoidCallback? onClick;

final VoidCallback? onFinish;
final int total;
final double borderRadius;
final String content;
final double? height;

final Color? focusColor;
final Color? hoverColor;
final Color? highlightColor;

CountdownWidget(
{this.total = 5,
Key? key,
this.height = 40,
this.onClick,
this.onFinish,
this.borderRadius = 20,
this.content = "倒计时",
this.focusColor,
this.hoverColor,
this.highlightColor})
: super(key: key);

@override
State<CountdownWidget> createState() => _CountdownWidgetState();
}

class _CountdownWidgetState extends BiliState<CountdownWidget> {
var _count = 0;

late Timer _timer;

///注册倒计时
@override
void initState() {
super.initState();
var duration = const Duration(seconds: 1);
_timer = Timer.periodic(duration, (timer) {
if (_count < widget.total) {
setState(() {
_count++;
});
} else {
widget.onFinish?.call();
_timer.cancel();
}
});
}

@override
Widget build(BuildContext context) {
return InkWell(
focusColor: widget.focusColor,
hoverColor: widget.hoverColor,
highlightColor: widget.highlightColor,
onTap: () {
widget.onClick?.call();
},
child: SizedBox(
height: widget.height,
child: Card(
elevation: 1,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.all(Radius.circular(widget.borderRadius)),
),
child: Center(
child: Padding(
padding: const EdgeInsets.only(left: 10, right: 10),
child: Text("${widget.content}${widget.total - _count}s"),
),
),
),
),
);
}
}

依赖的两个基类


import 'package:flutter/material.dart';

@immutable
abstract class BiliWidget extends StatefulWidget {
BiliWidget({
Key? key,
}) : super(key: key);

String param = "";

void setParam(String param) {
this.param = param;
}
}

import 'package:flutter/material.dart';

import 'base_state.dart';

abstract class BiliState<T extends BiliWidget> extends State<T> {}

项目源码


github.com/HaiYangCode…


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

Flutter实现心碎的感觉

前言 继续动画探索,今天用Flutter制作一个心碎的感觉,灵感来源于今天的股市,哎,心哇凉哇凉的。废话不多说,开始。 效果图先上: 1、绘制一个心 首先我们使用两段三阶贝塞尔曲线制作一个心型,这里因为需要实现心碎的效果,所以我们需要将心的两段用两段路径pat...
继续阅读 »

前言


继续动画探索,今天用Flutter制作一个心碎的感觉,灵感来源于今天的股市,哎,心哇凉哇凉的。废话不多说,开始。


效果图先上:


1、绘制一个心


首先我们使用两段三阶贝塞尔曲线制作一个心型,这里因为需要实现心碎的效果,所以我们需要将心的两段用两段路径path进行绘制出来,效果:


image.png

绘制代码:


canvas.translate(size.width / 2, size.height / 2);
Paint paint = Paint();
paint
..style = PaintingStyle.stroke
..strokeWidth = 2
..color = Colors.black87;
Path path = Path();
path.moveTo(0, 0);
path.cubicTo(-200, -80, -60, -240, 0, -140);
path.close();
Path path2 = Path();
canvas.save();
canvas.drawPath(
path,
paint
..color = Colors.red
..style = PaintingStyle.stroke);
canvas.restore();
path2.cubicTo(200, -80, 60, -240, 0, -140);
path2.close();
canvas.drawPath(
path2,
paint..color = Colors.black87);

2、绘制心的裂痕


我们看到心确实分成两半了,但是中间还缺少裂痕,接下来我们就绘制心碎的裂痕,也很简单,在两段路径path闭合前进行绘制线,效果:


image.png


绘制代码:


path.relativeLineTo(-10, 30);
path.relativeLineTo(20, 5);
path.relativeLineTo(-20, 30);
path.relativeLineTo(20, 20);
path.relativeLineTo(-10, 20);
path.relativeLineTo(10, 10);

path2.relativeLineTo(-10, 30);
path2.relativeLineTo(20, 5);
path2.relativeLineTo(-20, 30);
path2.relativeLineTo(20, 20);
path2.relativeLineTo(-10, 20);
path2.relativeLineTo(10, 10);

OK,我们已经看到心已经有了裂痕,如何心碎,只需将画布进行翻转一定角度即可,这里我们将画布翻转45°,看下效果:

左边:
image.png

右边:
image.png


3、加入动画


已经有心碎的感觉了,接下来加入动画元素让心碎的过程动起来。

思路: 我们可以想一下,心碎的过程是什么样子,心的颜色慢慢变灰,心然后慢慢裂开,下方的动画运动曲线看起来更符合心碎的过程,里面有不舍,不甘,但最后心还是慢慢的碎了。
xinsui.gif


我们把画笔进行填充将这个动画加入进来看下最终效果。

df5dbcbb-f36b-4f05-9613-0e94149d888f.gif
是不是心碎了一地。


知识点: 这里我们需要找到红色和灰色的RGB色值,通过Color.fromRGBO(r, g, b, opacity)方法赋值颜色的色值。然后通过动画值改变RGB的值即可。
这里我使用的色值是:

红色:Color.fromRGBO(255, 0, 0, 1)

灰色:Color.fromRGBO(169, 169, 169, 1)


最终代码:


class XinSui extends StatefulWidget {
const XinSui({Key? key}) : super(key: key);

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

class _XinSuiState extends State<XinSui> with SingleTickerProviderStateMixin {
late AnimationController _controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 4000))
..repeat();
late CurvedAnimation cure =
CurvedAnimation(parent: _controller, curve: Curves.bounceInOut);

late Animation<double> animation =
Tween<double>(begin: 0.0, end: 1.0).animate(cure);

@override
Widget build(BuildContext context) {
return Container(
child: CustomPaint(
size: Size(double.infinity, double.infinity),
painter: _XinSuiPainter(animation),
),
);
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}
}

class _XinSuiPainter extends CustomPainter {
Animation<double> animation;

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

@override
void paint(Canvas canvas, Size size) {
canvas.translate(size.width / 2, size.height / 2);
Paint paint = Paint();
paint
..style = PaintingStyle.stroke
..strokeWidth = 2
..color = Colors.black87;
Path path = Path();
path.moveTo(0, 0);
path.cubicTo(-200, -80, -60, -240, 0, -140);
path.relativeLineTo(-10, 30);
path.relativeLineTo(20, 5);
path.relativeLineTo(-20, 30);
path.relativeLineTo(20, 20);
path.relativeLineTo(-10, 20);
path.relativeLineTo(10, 10);
path.close();
Path path2 = Path();
canvas.save();
canvas.rotate(-pi / 4 * animation.value);
canvas.drawPath(
path,
paint
..color = Colors.red
..color = Color.fromRGBO(
255 - (86 * animation.value).toInt(),
(animation.value * 169).toInt(),
(animation.value * 169).toInt(),
1)

..style = PaintingStyle.fill);
canvas.restore();
path2.cubicTo(200, -80, 60, -240, 0, -140);
path2.relativeLineTo(-10, 30);
path2.relativeLineTo(20, 5);
path2.relativeLineTo(-20, 30);
path2.relativeLineTo(20, 20);
path2.relativeLineTo(-10, 20);
path2.relativeLineTo(10, 10);
path2.close();
canvas.rotate(pi / 4 * animation.value);
canvas.drawPath(
path2,paint);
}
@override
bool shouldRepaint(covariant _XinSuiPainter oldDelegate) {
return oldDelegate.animation != animation;
}
}

小结


动画曲线Curves配合绘制可以实现很多好玩的东西,这个需要勤加练习方能掌握,仅将此心碎献给今天受伤的股民朋友们(ಥ﹏ಥ)


作者:老李code
来源:https://juejin.cn/post/7090457954415017991
收起阅读 »

指纹解锁分析

systemServer进程会在ZygoteInit中进行创建,而ZygoteInit是Zygote进程启动的。 在systemServer进程的run方法中会启动重要服务其中就包括指纹解锁相对应的服务。 指纹解锁需要和Hal层进行交互,并对上层framewr...
继续阅读 »

systemServer进程会在ZygoteInit中进行创建,而ZygoteInit是Zygote进程启动的。


在systemServer进程的run方法中会启动重要服务其中就包括指纹解锁相对应的服务。
指纹解锁需要和Hal层进行交互,并对上层framewrok提供接口以实现解锁功能


整体流程可以大致分为:


1.SystemServer中调用startOtherService方法根据设备支持的功能启动对应的服务
该例中如果设备支持指纹解锁就执行接下来的方法:
启动指纹解锁对应的Service,也就是FingerprintService这个类


startOtherService方法:
image.png


startService:
image.png


2.可以看到会反射创建这个类的构造方法并把它添加到services中,接着执行这个类的onStart方法


image.png


FingerprintService这个类的onStart方法
image.png
3.FingerprintService这个类的onStart方法中可以看到创建了一个 FingerprintServiceWrapper()这个类。


发布服务保存在SystemServer中,可以看到这个服务对应的接口是
IFingerprintService.Stub


image.png


image.png


可以看到是在用了个线程池在调用这个run方法,接下来去看看这个Runnable接口做了什么操作
image.png


getFingerprintDaemon函数首先调用getService函数不断尝试链接HAL层的进程(IBiometricsFingerprint这个服务是在HAL层初始化的之后讲解),链接成功之后调用setNotify设置回调函数,最后加载用户相关数据。至此,Framework层已经启动完成。


image.png


BiometricsFingerprint


上面讲到FrameWork中会获取BiometricsFingerprint这个服务,这个服务是在哪个地方初始化的呢?


首先需要讲下Android.bp文件:



Android.bp的出现就是为了替换Android.mk文件,随着Android越来越庞大,module越来越多,编译时间也越来越长,而使用ninja在编译的并发处理上较make有很大的提升。Ninja的配置文件就是Android.bp,Android系统使用Blueprint和Soong工具来解析Android.bp转换生成ninja文件



详细内容及自定义文件可参考这篇博客 Android.bp文件详解


这里首先看下一些配置信息
这是一些注释信息:



cc_library_shared :编译成动态库,类似于Android.mk中的BUILD_SHARED_LIBRARY
cc_binary:编译成可执行文件,类似于Android.mk中的BUILD_EXECUTABLE
name :编译出的模块的名称,类似于Android.mk中的LOCAL_MODULE
srcs:源文件,类似于Android.mk中的LOCAL_SRC_FILES
local_include_dirs:指定路径查找头文件,类似于Android.mk中的LOCAL_C_INCLUDES
shared_libs:编译所依赖的动态库,类似于Android.mk中的LOCAL_SHARED_LIBRARIES
static_libs:编译所依赖的静态库,类似于Android.mk中的LOCAL_STATIC_LIBRARIES
cflags:编译flag,类似于Android.mk中的LOCAL_CFLAGS



image.png


Service.cpp是HAL层启动的入口文件。


1.首先通过BiometricsFingerprint::getInstance()实例化一个bio服务,不同厂商的指纹识别算法和逻辑也都在这个bibo服务中体现出来。这个方法里面会进行初始化HAL层关于指纹的一些初始化动作最后讲


2.接着设置用于RPC通信的线程数


3.接着把自己添加到线程池中,用于之后framework获取进行返回bibo服务


image.png


BiometricsFingerprint::getInstance()


该函数单利创建出来一个BiometricsFingerprint对象,接着看他的构造方法


image.png


BiometricsFingerprint构造方法,可以看到调用了openHal方法。
image.png
1.openHal方法第一步首先打开指纹HW模块,也就是获取厂商指纹模组的so



hw_get_module(FINGERPRINT_HARDWARE_MODULE_ID, &hw_mdl)



image.png


2.接着调用open方法


image.png


image.png


3.这个open方法主要是将厂商指纹模组模块的算法识别逻辑结果和HAL层进行绑定,设置回调通知。


image.png


大致流程:


首先将framework中的指纹解锁Service启动接着去获取HAL层的指纹解锁服务Service。
framework层的Service主要用于和HAL层进行通信(获取HAL层的Service)
HAL层的Service收到后会使用厂商自定义的指纹模组so模块对应的逻辑去判断是否是本人
最后结果在给到framework层响应

作者:北洋
来源:https://juejin.cn/post/7090362782767546398
收起阅读 »

一种emoji表情判断方法

Emoji表情输入 常用的utf8编码,最多只会达到3字节,如MySQL的utf8编码。但像emoji表情等Unicode是4字节的(UCS-4),在编码为utf8时,也会占用4字节。在MySQL中,就要使用utf8mb4(most bytes 4)编码,否则...
继续阅读 »

image.png


Emoji表情输入


常用的utf8编码,最多只会达到3字节,如MySQL的utf8编码。但像emoji表情等Unicode是4字节的(UCS-4),在编码为utf8时,也会占用4字节。在MySQL中,就要使用utf8mb4(most bytes 4)编码,否则插入时会报错。


在某些场景下,我们并不希望文本中出现emoji表情等非常用字符,那么如何过滤呢?

对于字符过滤,一般我们第一个想到的大多是正则表达式。然而,实际使用中,由于emoji表情的不断增加或正则表达式本身的缺陷,往往达不到过滤的效果。


image.png


发现问题



欢迎来到王者荣耀😊😊



字符数量10,字符串长度12


一次开发中,使用了el-input的字符数统计属性show-word-limit,发现输入emoji表情统计到的字符数量和实际看到的字符数量不一致。

然后,尝试通过字符串分割成数组,再比较长度,发现str.split('')得到的数组长度和统计到的字符数是一样的,但是和肉眼看到的字符数量还是不一致。


var str = '欢迎来到王者荣耀😊😊'
var arr = str.split('')
console.log(str.length) // 12
console.log(arr.length) // 12

解决问题


那么,是否可以通过字符串的字符数量和字符串长度来判断是否输入了emoji表情呢?

要验证这个问题,关键的是获取到字符串中字符的数量。


那么如何获取字符串中字符的数量呢,通过研究(百度)发现,分割utf8字符串的正确方法是使用 Array.from(str) 而不是str.split('')。


Array.from() 方法对一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。


var str = '欢迎来到王者荣耀😊😊'
var arr2 = Array.from(str)
console.log(str.length) // 12
console.log(arr2.length) // 10

一个大胆的猜想


emoji表情判断,可以通过字符串长度和字符数量的比较判断是否存在emoji表情,当长度和数量不一致的时候,有emoji表情。


isEmojiStr(str) { 
if (typeof (str) === 'string') {
const arr = Array.from(str);
if (str.length !== arr.length) {
return true;
}
}
return false;
}

image.png


参考


# Emoji Unicode Tables

# 深入理解Emoji(一) —— 字符集,字符集编码

# 深入理解Emoji(二) —— 字节序和BOM

# 深入理解Emoji(三) —— Emoji详解


作者:前端老兵
来源:https://juejin.cn/post/7090182766158938120
收起阅读 »

Flutter实现一个牛顿摆

前言牛顿摆大家应该都不陌生,也叫碰碰球、永动球(理论情况下),那么今天我们用Flutter实现这么一个理论中的永动球,可以作为加载Loading使用。 - 知识点:绘制、动画曲线、多动画状态更新 效果图: 1、绘制静态效果 首先我们需要把线和小圆球绘制出来,...
继续阅读 »

前言

  • 牛顿摆大家应该都不陌生,也叫碰碰球、永动球(理论情况下),那么今天我们用Flutter实现这么一个理论中的永动球,可以作为加载Loading使用。

- 知识点:绘制、动画曲线、多动画状态更新


效果图:


638bdf30-7b2a-4c3e-ad14-94da128b68f1.gif


1、绘制静态效果


首先我们需要把线和小圆球绘制出来,对于看过我之前文章的小伙伴来说这个就很简单了,效果图:

 
image.png

关键代码:


// 小圆球半径
double radius = 6;

/// 小球圆心和直线终点一致
//左边小球圆心
Offset offset = Offset(20, 60);
//右边小球圆心
Offset offset2 = Offset(20 * 6 * 8, 60);

Paint paint = Paint()
..color = Colors.black87
..strokeWidth = 2;

/// 绘制线
canvas.drawLine(Offset.zero, Offset(90, 0), paint);
canvas.drawLine(Offset(20, 0), offset, paint);
canvas.drawLine(
Offset(20 + radius * 2, 0), Offset(20 + radius * 2, 60), paint);
canvas.drawLine(
Offset(20 + radius * 4, 0), Offset(20 + radius * 4, 60), paint);
canvas.drawLine(
Offset(20 + radius * 6, 0), Offset(20 + radius * 6, 60), paint);
canvas.drawLine(Offset(20 + radius * 8, 0), offset2, paint);

/// 绘制小圆球
canvas.drawCircle(offset, radius, paint);
canvas.drawCircle(Offset(20 + radius * 2, 60), radius, paint);
canvas.drawCircle(Offset(20 + radius * 4, 60), radius, paint);
canvas.drawCircle(Offset(20 + radius * 6, 60), radius, paint);
canvas.drawCircle(offset2, radius, paint);

2、加入动画


思路: 我们可以看到5个小球一共2个小球在运动,左边小球运动一个来回之后传递给右边小球,右边小球开始运动,右边一个来回再传递给左边开始,也就是左边运动周期是:0-1-0,正向运动一次,反向再运动一次,这样就是一个周期,右边也是一样,左边运动完传递给右边,右边运动完传递给左边,这样就简单实现了牛顿摆的效果。


两个关键点


小球运动路径: 小球的运动路径是一个弧度,以竖线的起点为圆心,终点为半径,那么我们只需要设置小球运动至最高点的角度即可,通过角度就可计算出小球的坐标点。


运动曲线: 当然我们知道牛顿摆小球的运动曲线并不是匀速的,他是有一个加速减速过程的,撞击之后,小球先加速然后减速达到最高点速度为0,之后速度再从0慢慢加速进行撞击小球,周而复始。

下面的运动曲线就是先加速再减速,大概符合牛顿摆的运动曲线。我们就使用这个曲线看看效果。

 
ndb.gif

完整源码:


class OvalLoading extends StatefulWidget {
const OvalLoading({Key? key}) : super(key: key);

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

class _OvalLoadingState extends State
with TickerProviderStateMixin
{
// 左边小球
late AnimationController _controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 300))
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reverse(); //反向执行 1-0
} else if (status == AnimationStatus.dismissed) {
_controller2.forward();
}
})
..forward();
// 右边小球
late AnimationController _controller2 =
AnimationController(vsync: this, duration: Duration(milliseconds: 300))
..addStatusListener((status) {
// dismissed 动画在起始点停止
// forward 动画正在正向执行
// reverse 动画正在反向执行
// completed 动画在终点停止
if (status == AnimationStatus.completed) {
_controller2.reverse(); //反向执行 1-0
} else if (status == AnimationStatus.dismissed) {
// 反向执行完毕左边小球执行
_controller.forward();
}
});
late var cure =
CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic);
late var cure2 =
CurvedAnimation(parent: _controller2, curve: Curves.easeOutCubic);

late Animation animation = Tween(begin: 0.0, end: 1.0).animate(cure);

late Animation animation2 =
Tween(begin: 0.0, end: 1.0).animate(cure2);

@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsetsDirectional.only(top: 300, start: 150),
child: CustomPaint(
size: Size(100, 100),
painter: _OvalLoadingPainter(
animation, animation2, Listenable.merge([animation, animation2])),
),
);
}

@override
void dispose() {
_controller.dispose();
_controller2.dispose();
super.dispose();
}
}

class _OvalLoadingPainter extends CustomPainter {
double radius = 6;
final Animation animation;
final Animation animation2;
final Listenable listenable;

late Offset offset; // 左边小球圆心
late Offset offset2; // 右边小球圆心

final double lineLength = 60; // 线长

_OvalLoadingPainter(this.animation, this.animation2, this.listenable)
: super(repaint: listenable) {
offset = Offset(20, lineLength);
offset2 = Offset(20 * radius * 8, lineLength);
}

// 摆动角度
double angle = pi / 180 * 30; // 30°

@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = Colors.black87
..strokeWidth = 2;

// 左边小球 默认坐标 下方是90度 需要+pi/2
var dx = 20 + 60 * cos(pi / 2 + angle * animation.value);
var dy = 60 * sin(pi / 2 + angle * animation.value);
// 右边小球
var dx2 = 20 + radius * 8 - 60 * cos(pi / 2 + angle * animation2.value);
var dy2 = 60 * sin(pi / 2 + angle * animation2.value);

offset = Offset(dx, dy);
offset2 = Offset(dx2, dy2);

/// 绘制线
canvas.drawLine(Offset.zero, Offset(90, 0), paint);
canvas.drawLine(Offset(20, 0), offset, paint);
canvas.drawLine(
Offset(20 + radius * 2, 0), Offset(20 + radius * 2, 60), paint);
canvas.drawLine(
Offset(20 + radius * 4, 0), Offset(20 + radius * 4, 60), paint);
canvas.drawLine(
Offset(20 + radius * 6, 0), Offset(20 + radius * 6, 60), paint);
canvas.drawLine(Offset(20 + radius * 8, 0), offset2, paint);

/// 绘制球
canvas.drawCircle(offset, radius, paint);
canvas.drawCircle(
Offset(20 + radius * 2, 60),
radius,
paint);

canvas.drawCircle(Offset(20 + radius * 4, 60), radius, paint);
canvas.drawCircle(Offset(20 + radius * 6, 60), radius, paint);
canvas.drawCircle(offset2, radius, paint);
}
@override
bool shouldRepaint(covariant _OvalLoadingPainter oldDelegate) {
return oldDelegate.listenable != listenable;
}
}

去掉线的效果:

b6a23e8a-9c4a-4aa8-9518-46a53b756a88.gif


总结


本文展示了实现牛顿摆的原理,其实并不复杂,关键点就是小球的运动轨迹和运动速度曲线,如果用到项目中当做Loading还有很多优化的空间,比如加上小球影子、修改小球颜色或者把小球换成好玩的图片等等操作会看起来更好看一点,本篇只展示了实现的原理,希望对大家有一些帮助~


作者:老李code
来源:https://juejin.cn/post/7090123854135164935 收起阅读 »

Android自动生成代码,可视化脚手架之环境搭建

系列文章Github开源地址(源码及各项资料不间断进行更新):github.com/AbnerMing88…Hello,各位老铁,系列文章上一篇,简单大概熟悉了一下基本的功能,当然了这只是其中的一部分,随着需求的增加,各种方便我们日常开发的功能都会研发出来,那...
继续阅读 »

系列文章Github开源地址(源码及各项资料不间断进行更新):

github.com/AbnerMing88…

Hello,各位老铁,系列文章上一篇,简单大概熟悉了一下基本的功能,当然了这只是其中的一部分,随着需求的增加,各种方便我们日常开发的功能都会研发出来,那么对于这样的一个可视化工具,我们该如何开发出来呢?又需要掌握什么技术呢?环境如何搭建呢?这篇,咱们就简单的聊一聊。

可能很多老铁有疑问,为什么不直接以插件的形式在Android Studio中使用呢,这样直接IDE中就可以操作了,也不用再打开其他工具了,岂不是更方便,哎!小老弟,一开始我就是整的插件,还写了好几个功能,但有一个致命的问题是,视图的绘制,贼麻烦,大家感兴趣的可以试试,多个控件的摆放,还有,拖拽View的实现,亲自操刀试试就知道了,正因为各个视图的绘制比较麻烦,最终才选择了可视化工具的开发。

目前可视化工具采用的是Electron进行开发的,Electron 是一个使用 JavaScript、HTML 和 CSS 构建跨平台的桌面应用程序,它基于 Node.js 和 Chromium,被 Atom 编辑器和许多其他应用程序使用,也就是说使用Electron,您必须有一定的web开发经验,如果没有也没关系,后续您可以直接在我的模板中进行对应的修改即可,当然了,为了能够自己灵活的可视化,建议还是掌握一些Web的经验,编程语言之间的语法,基本互通,学起来也比较容易。

对于Electron,网上流传着一些风言风语,说微软要放弃Electron了,这里简单辟谣一下,微软自始至终,就没有放弃Electron,也不会放弃Electron,只是旗下的Teams产品打算把Electron框架换成WebView2而已,况且微软内部有很多软件都是基于Electron开发的,比如VSCode和GitHubDesktop,不仅仅是只有Teams这么一个产品在用它,非但微软内部,包括Facebook、MongoDB、twitch、Slack、迅雷、字节跳动、阿里、拼多多、京东等大企业都在用这个框架,这么一个好东西,微软怎么会放弃它呢?所以,各位老铁,不要在听信网上的谣言了,桌面开发工具Electron,兼容 Mac、Windows 和 Linux,可以构建出三个平台的应用程序,学起来,指定没错!

Electron官网:http://www.electronjs.org/

关于Electron的教程,网上一搜一大堆,咱们言简意赅,直奔主题,老铁们,跟好脚步,我们发车!

1、安装 Node.js

别问为什么,问就是,Electron开发依赖Node.js,因为Node.js中允许使用 JavaScript 开发服务端以及命令行程序,我们可以去官网nodejs.org下载最新版本的安装程序,也可以下载我给大家准备好的安装包,都在上面github开源地址中。

下载后,怎么安装,就不用我来教了吧,一路一路下一步,中间会有个选择安装路径,这个尽量自己选一个,不要用默认的,安装完成后会自动配置环境变量,如果没有配置,那就需要自己去环境变量下配置一下:

自己配置的话,首先找到你的安装路径,复制一下:


然后配置到环境变量里,以windows为例子


一切搞定之后,打开命令窗口,输入node -v,检验下是否安装成功,回显当前版本,证明安装成功!


2、安装 Electron

打开命令窗口,输入下面命令:

npm install -g electron

下载慢的话,可以先执行下面的命令,electron安装包指向淘宝的镜像

npm config set electron_mirror "https://npm.taobao.org/mirrors/electron/"

等待安装完成之后,在命令行输入electron -v能够显示版本号代表安装成功。


如果想删除 Electron,可以使用下面的命令。

npm uninstall electron

如果想升级 Electron,则可以使用这个命令。

npm update electron -g

大家也可以指定版本进行安装,有一些版本升高之后,会有一些兼容性问题,目前,我的版本是15.0.0,大家可以和我保持一致。

cnpm install electron@^15.0.0 -g

以上两步执行完毕之后,环境就搭建完毕,剩下的就是愉快的敲代码时刻。

搞一个Hello,World!

随便找一个空的文件夹,进入到目录下,执行下面的命令,或者在命令窗口找到你的目录,都行

npm init 
npm install --save-dev electron 或者安装制定版本 npm install --save-dev electron@^15.0.0

如下图,我新建的一个code目录:


进入到当前目录命令下,执行上面的命令:


当执行npm init时,会按照步骤,让输入很多东西,如果你不想一步一步输入,每次直接回车即可,反正也是可以修改的。

如果想进行一步一步输入,具体流程如下,中间不想输入,可以回车略过:

package name 包名,也就是工程名,默认是括号中的内容 
version:版本号,默认是括号中的内容
description:描述信息
entry point:入口文件名,默认是括号中的内容
test command:测试命令
git repository:git仓库地址
keywords: 密码
author: 作者名字
license: (ISC)许可证

我自己执行的程序如下:


执行完成之后,就会在你刚才选中的目录下,生成一个,package.json文件:


我们打开看一下,其实就是我们一步一步输入的内容:


接着我们在去执行第二个命令,我是选择指定版本进行安装的:


命令执行完毕后,会生成如下图所示:


node_modules,是安装node后,用来存放下载安装的包文件夹。

执行完命令之后,我们就可以书写主入口了,之前执行npm init命令时,有个主入口的输入,还记得吗,就是下面这个:


新建index.js文件


内容如下:

const { app, BrowserWindow } = require('electron')

function createWindow () {  
 // 创建浏览器窗口
 let win = new BrowserWindow({
   width: 800,
   height: 600,
   webPreferences: {
     nodeIntegration: true
  }
})

 // 加载index.html文件
 win.loadFile('index.html')
}

// 应用程序准备就绪后打开一个窗口
app.whenReady().then(createWindow)

紧接着新建一个index.js中对应的index.html文件:


内容如下:

<!DOCTYPE html>
<html>
 <head>
   <meta charset="UTF-8">
   <title>Android可视化工具</title>
 </head>
 <body>
   <h1>Hello,World!</h1>
 </body>
</html>

最后修改package.json,添加Electron运行时


回到目录下,打开命令窗口,执行npm start命令,如下图


执行命令之后,随之就会,弹出来一个可视化窗口,如下图:


ok,一个简单的Demo就完成了,是不是贼简单。

老铁们,第二章的内容,虽然有点多,但基本上都是些操作的步骤,环境的安装以及简单的项目运行,还是希望大家从头到尾的执行一遍,都是一些流程化的操作,并不是很难,下一章,我们讲讲述可视化工具的一些配置项,敬请期待!

作者:二流小码农
来源:https://juejin.cn/post/7090322746260848671

收起阅读 »

春节钱包大流量奖励系统入账及展示的设计与实现

字节跳动开放平台-钱包团队整体负责字节系八端 2022 年春节活动奖励链路的入账、展示与使用,下文是对这段工作的介绍和总结,先整体介绍一下业务背景与技术架构,然后说明了各个难点的具体实现方案,最后进行抽象总结,希望对后续的活动起指导作用。1. 背景&挑...
继续阅读 »

字节跳动开放平台-钱包团队整体负责字节系八端 2022 年春节活动奖励链路的入账、展示与使用,下文是对这段工作的介绍和总结,先整体介绍一下业务背景与技术架构,然后说明了各个难点的具体实现方案,最后进行抽象总结,希望对后续的活动起指导作用。

1. 背景&挑战&目标

1.1 业务背景

(1)支持八端:2022 年字节系产品春节活动需要支持八端 APP 产品(包含抖音/抖音火山/抖音极速版/西瓜/头条/头条极速版/番茄小说/番茄畅听)的奖励互通。用户在上述任意一端都可以参与活动,得到的奖励在其他端都可以提现与使用。

(2)玩法多变:主要有集卡、朋友页红包雨、红包雨、集卡开奖与烟火大会等。

(3)多种奖励:奖励类型包含现金红包、补贴视频红包、商业化广告券、电商券、支付券、消费金融券、保险券、信用卡优惠券、喜茶券、电影票券、dou+券、抖音文创券、头像挂件等。

1.2 核心挑战

(1)设计&实现八端奖励入账与展示互通的大流量的方案,最高预估有 360w QPS 发奖。

(2)多种发奖励的场景,玩法多变;奖励类型多,共 10 余种奖励。对接多个下游系统。

(3)从奖励系统稳定性、用户体验、资金安全与运营基础能力全方位保障,确保活动顺利进行 。

1.3 最终目标

(1)奖励入账:设计与实现八端奖励互通的奖励入账系统,对接多个奖励下游系统,抹平不同奖励下游的差异,对上游屏蔽底层奖励入账细节,设计统一的接口协议提供给业务上游。提供统一的错误处理机制,入账幂等能力和奖励预算控制。

(2)奖励展示/使用:设计与实现活动钱包页,支持在八端展示用户所获得的奖励,支持用户查看、提现(现金),使用卡券/挂件等能力。

(3)基础能力:

  • 【基础 sdk】提供查询红包余额、累计收入、用户在春节活动是否获得过奖励等基础 sdk,供业务方查询使用。

  • 【预算控制】与上游奖励发放端算法策略打通,实现大流量卡券入账的库存控制能力,防止超发。

  • 【提现控制】在除夕当天多轮奖励发放后,提供用户提现的灰度放量能力、提现时尚未入账的处理能力。

  • 【运营干预】活动页面灵活的运营配置能力,支持快速发布公告,及时触达用户。为应对黑天鹅事件,支持批量卡券和红包补发能力。

(4)稳定性保障:在大流量的入账场景下,保证钱包核心路径稳定性与完善,通过常用稳定性保障手段如资源扩容、限流、熔断、降级、兜底、资源隔离等方式保证用户奖励方向的核心体验。

(5)资金安全:在大流量的入账场景下,通过幂等、对账、监控与报警等机制,保证资金安全,保证用户资产应发尽发,不少发。

(6)活动隔离:实现内部测试活动、灰度放量活动和正式春节活动三个阶段的奖励入账与展示的数据隔离,不互相影响。

2. 产品需求介绍

用户可以在任意一端参与字节的春节活动获取奖励,以抖音红包雨现金红包入账场景为例,具体的业务流程如下:

登录抖音 → 参与活动 → 活动钱包页 → 点击提现按钮 → 进入提现页面 → 进行提现 → 提现结果页,另外从钱包页也可以进入活动钱包页。

img

奖励发放核心场景:

  1. 集卡:集卡抽卡时发放各类卡券,集卡锦鲤还会发放大额现金红包,集卡开奖时发放瓜分奖金和优惠券;

  2. 红包雨:发红包、卡券以及视频补贴红包,其中红包和卡券最高分别 180w QPS;

  3. 烟火大会:发红包、卡券以及头像挂件。

dd6af98d51f5db710872248ae51a30e4.png

3. 钱包资产中台设计与实现

在 2022 年春节活动中,UG 主要负责活动的玩法实现,包含集卡、红包雨以及烟火大会等具体的活动相关业务逻辑和稳定性保障。而钱包方向定位是大流量场景下实现奖励入账、奖励展示、奖励使用与资金安全保障的相关任务。其中资产中台负责奖励发放与奖励展示部分。

3.1 春节资产资产中台总体架构图如下:

768d79e164975d1794d875abaf74b627.png

钱包资产中台核心系统划分如下:

  1. 资产订单层:收敛八端奖励入账链路,提供统一的接口协议对接上游活动业务方如 UG、激励中台、视频红包等的奖励发放功能,同时对上游屏蔽对接奖励业务下游的逻辑处理,支持预算控制、补偿、订单号幂等。

  2. 活动钱包 api 层:收敛八端奖励展示链路,同时支持大流量场景

3.2 资产订单中心设计

核心发放模型:

37c250c4a4b5e91b0dc4ce1e236ae00f.png

说明:

  1. 活动 ID 唯一区分一个活动,本次春节分配了一个单独的母活动 ID

  2. 场景 ID 和具体的一种奖励类型一一对应,定义该场景下发奖励的唯一配置,场景 ID 可以配置的能力有:发奖励账单文案;是否需要补偿;限流配置;是否进行库存控制;是否要进行对账。提供可插拔的能力,供业务可选接入。

实现效果:

  1. 实现不同活动之间的配置隔离

  2. 每个活动的配置呈树状结构,实现一个活动发多种奖励,一种奖励发多种奖励 ID

  3. 一种奖励 ID 可以有多种分发场景,支持不同场景的个性化配置

订单号设计:

资产订单层支持订单号维度的发奖幂等,订单号设计逻辑为${actID}_${scene_id}_${rain_id}_${award_type}_${statge},从单号设计层面保证不超发,每个场景的奖励用户最多只领一次。

  1. 核心难点问题解决

4.1 难点一:支持八端奖励数据互通

前文背景已经介绍过了,参与 2022 年春节活动一共有八个产品端,其中抖音系和头条系 APP 是不同的账号体系,所以不能通过用户 ID 打通奖励互通。具体解决方案是字节账号中台打通了八端的账号体系给每个用户生成唯一的 actID(手机号优先级最高,如果不同端登录的手机号一样,在不同端的 actID 是一致的)。钱包侧基于字节账号中台提供的唯一 actID 基础上,设计实现了支持八端奖励入账、查看与使用的通用方案,即每个用户的奖励数据是绑定在 actID 上的,入账和查询是通过 actID 维度实现的,即可实现八端奖励互通。

示意图如下:

d5e9bb782bba0c3e5fd950a64664d2c6.png

4.2 难点二:高场景下的奖励入账实现

每年的春节活动,发现金红包都是最关键的一环,今年也不例外。有几个原因如下:

  1. 预估发现金红包最大流量有 180w TPS。

  2. 现金红包本身价值高,需要保证资金安全。

  3. 用户对现金的敏感度很高,在保证用户体验与功能完整性同时也要考虑成本问题。

终上所述,发现金红包面临比较大的技术挑战。

发红包其实是一种交易行为,资金流走向是从公司成本出然后进入个人账户。

(1)从技术方案上是要支持订单号维度的幂等,同一订单号多次请求只入账一次。订单号生成逻辑为${actID}_${scene_id}_${rain_id}_${award_type}_${statge},从单号设计层面保证不超发。

(2)支持高并发,有以下 2 个传统方案:

具体方案类型实现思路优点缺点
同步入账申请和预估流量相同的计算和存储资源1.开发简单; 2.不容易出错;浪费存储成本。 拿账户数据库举例,经实际压测结果:支持 30w 发红包需要 152 个数据库实例,如果支持 180w 发红包,至少需要 1152 个数据库实例,还没有算上 tce 和 redis 等其他计算和存储资源。
异步入账申请部分计算和存储资源资源,实际入账能力与预估有一定差值1.开发简单; 2.不容易出错; 3.不浪费资源;用户体验受到很大影响。 入账延迟较大,以今年活动举例会有十几分钟延迟。用户参与玩法得到奖励后在活动钱包页看不到奖励,也无法进行提现,会有大量客诉,影响抖音活动的效果。

以上两种传统意义上的技术方案都有明显的缺点,那么进行思考,既能相对节约资源又能保证用户体验的方案是什么?
最终采用的是红包雨 token 方案,具体方案是使用异步入账加较少量分布式存储和较复杂方案来实现,下面具体介绍一下。

4.2.1 红包雨 token 方案:

本次春节活动在红包雨/集卡开奖/烟火大会的活动下有超大流量发红包的场景,前文介绍过发奖 QPS 最高预估有 180w QPS,按照现有的账户入账设计,需要大量存储和计算资源支撑,根据预估发放红包数/产品最大可接受发放时间,计算得到钱包实际入账最低要支持的 TPS 为 30w,所以实际发放中有压单的过程。

设计目标:

在活动预估给用户发放(180w)与实际入账(30w)有很大 gap 的情况下,保证用户的核心体验。用户在前端页面查看与使用过当中不能感知压单的过程,即查看与使用体验不能受到影响,相关展示的数据包含余额,累计收入与红包流水,使用包含提现等。

具体设计方案:

我们在大流量场景下每次给用户发红包会生成一个加密 token(使用非对称加密,包含发红包的元信息:红包金额,actID,与发放时间等),分别存储在客户端和服务端(容灾互备),每个用户有个 token 列表。每次发红包的时候会在 Redis 里记录该 token 的入账状态,然后用户在活动钱包页看到的现金红包流水、余额等数据,是合并已入账红包列表+token 列表-已入账/入账中 token 列表的结果。同时为保证用户提现体验不感知红包压单流程,在进入提现页或者点击提现时将未入账的 token 列表进行强制入账,保证用户提现时账户的余额为应入账总金额,不 block 用户提现流程。

示意图如下:

fcf21c86fa2ef5ea8c132ca58883cf91.png

token 数据结构:

token 使用的是 pb 格式,经单测验证存储消耗实际比使用 json 少了一倍,节约请求网络的带宽和存储成本;同时序列化与反序列化消耗 cpu 也有降低。

// 红包雨token结构
type RedPacketToken struct {
  AppID     int64 `protobuf: varint,1,opt json: AppID,omitempty ` // 端ID
  ActID     int64 `protobuf: varint,2,opt json: UserID,omitempty ` // ActID
  ActivityID string `protobuf: bytes,3,opt json: ActivityID,omitempty ` // 活动ID
  SceneID   string `protobuf: bytes,4,opt json: SceneID,omitempty ` // 场景ID
  Amount     int64 `protobuf: varint,5,opt json: Amount,omitempty ` // 红包金额
  OutTradeNo string `protobuf: bytes,6,opt json: OutTradeNo,omitempty ` // 订单号
  OpenTime   int64 `protobuf: varint,7,opt json: OpenTime,omitempty ` // 开奖时间
  RainID     int32 `protobuf: varint,8,opt,name=rainID json: rainID,omitempty ` // 红包雨ID
  Status     int64 `protobuf: varint,9,opt,name=status json: status,omitempty ` //入账状态
}

token 状态机流转:

在调用账户真正入账之前会置为处理中(2)状态,调用账户成功为成功(8)状态,发红包没有失败的情况,后续都是可以重试成功的。

token 安全性保障:

采用非对称加密算法来保障存储在的客户端尽可能不被破解,其中加密算法为秘密仓库,限制其他人访问。同时考虑极端情况下如果 token 加密算法被黑产破译,可监控报警发现,可降级。

4.2.2 活动钱包页展示红包流水

需求背景:

活动钱包页展示的红包流水是现金红包入账流水、提现流水、c2c 红包流水三个数据源的合并,按照创建时间倒叙排列,需要支持分页,可降级,保证用户体验不感知发现金红包压单过程。

c5ff3a6471d9230554d6b50395c7c946.png

4.3 难点三:发奖励链路依赖多的稳定性保障

发红包流程降级示意图如下:

8677db831f49ccf481df9fc43f6fcd6e.png

根据历史经验,实现的功能越复杂,依赖会变多,对应的稳定性风险就越高,那么如何保证高依赖的系统稳定性呢?

解决方案:

现金红包入账最基础要保障的功能是将用户得到的红包进行入账,同时支持幂等与预算控制(避免超发),红包账户的幂等设计强依赖数据库保持事务一致性。但是如果极端情况发生,中间的链路可能会出现问题,如果是弱依赖需要支持降级掉,不影响发放主流程。钱包方向发红包最短路径为依赖服务实例计算资源和 MySQL 存储资源实现现金红包入账。

发红包强弱依赖梳理图示:

psm依赖服务是否强依赖降级方案降级后影响
资产中台tcc降级读本地缓存
bytkekv主动降级开关,跳过 bytekv,依赖下游做幂等
资金交易层分布式锁 Redis被动降级,调用失败,直接跳过基本无
token Redis主动降级开关,不调用 Redis用户能感知到入账有延迟,会有很多客诉
MySQL主有问题,联系 dba 切主故障期间发红包不可用

4.4 难点四:大流量发卡券预算控制

需求背景:

春节活动除夕晚上 7 点半会开始烟火大会,是大流量集中发券的一个场景,钱包侧与算法策略配合进行卡券发放库存控制,防止超发。

具体实现:

(1)钱包资产中台维护每个卡券模板 ID 的消耗发放量。

(2)每次卡券发放前算法策略会读取钱包 sdk 获取该卡券模板 ID 的消耗量以及总库存数。同时会设置一个阈值,如果卡券剩余量小于 10%后不发这个券(使用兜底券或者祝福语进行兜底)。

(3) 同时钱包资产中台方向在发券流程累计每个券模板 ID 的消耗量(使用 Redis incr 命令原子累加消耗量),然后与总活动库存进行比对,如果消耗量大于总库存数则拒绝掉,防止超发,也是一个兜底流程。

具体流程图:

9074ea1e5bc8d3a06b7958c380876833.png

优化方向:

(1)大流量下使用 Redis 计数,单 key 会存在热 key 问题,需要拆分 key 来解决。

(2)大流量场景下操作 Redis 会存在超时问题,返回上游处理中,上游继续重试发券会多消耗库存少发,本次春节活动实际活动库存在预估库存基础上加了 5%的量级来缓解超时带来的少发问题。

4.5 难点五:高 QPS 场景下的热 key 的读取和写入稳定性保障

需求背景:

在除夕晚上 7 点半开始会开始烟火大会活动,展示所有红包雨与烟火大会红包的实时累计发放总额,最大流量预估读取有 180wQPS,写入 30wQPS。

这是典型的超大流量,热点 key、更新延迟不敏感,非数据强一致性场景(数字是一直累加),同时要做好容灾降级处理,最后实际活动展示的金额与产品预计发放数值误差小于 1%。

bad435d6650fdb6b002016da05a29b1d.png

4.5.1 方案一

提供 sdk 接入方式,复用了主会场机器实例的资源。高 QPS 下的读取和写入单 key,比较容易想到的是使用 Redis 分布式缓存来进行实现,但是单 key 读取和写入的会打到一个实例上,压测过单实例的瓶颈为 3w QPS。所以做的一个优化是拆分多个 key,然后用本地缓存兜底。

具体写入流程:

设计拆分 100 个 key,每次发红包根据请求的 actID0 使用 incr 命令累加该数字,因为不能保证幂等性,所以超时不重试。

8029924d3b366d73aff105bf7b2874bc.png

读取流程:

与写入流程类似,优先读取本地缓存,如果本地缓存值为为 0,那么去读取各个 Redis 的 key 值累加到一起,进行返回。

73a7b7c3a4dfd9c721119a3a55e511a5.png

问题:

(1)拆分 100 个 key 会出现读扩散的问题,需要申请较多 Redis 资源,存储成本比较高。而且可能存在读取超时问题,不能保证一次读取所有 key 都读取成功,故返回的结果可能会较上一次有减少。

(2)容灾方案方面,如果申请备份 Redis,也需要较多的存储资源,需要的额外存储成本。

4.5.2 方案二

设计思路:

在方案一实现的基础上进行优化,并且要考虑数字不断累加、节约成本与实现容灾方案。在写场景,通过本地缓存进行合并写请求进行原子性累加,读场景返回本地缓存的值,减少额外的存储资源占用。使用 Redis 实现中心化存储,最终大家读到的值都是一样的。

具体设计方案:

每个 docker 实例启动时都会执行定时任务,分为读 Redis 任务和写 Redis 任务。

读取流程:

  1. 本地的定时任务每秒执行一次,读取 Redis 单 key 的值,如果获取到的值大于本地缓存那么更新本地缓存的值。

  2. 对外暴露的 sdk 直接返回本地缓存的值即可。

  3. 有个问题需要注意下,每次实例启动第一秒内是没有数据的,所以会阻塞读,等有数据再返回。

写入流程:

  1. 因为读取都是读取本地缓存(本地缓存不过期),所以处理好并发情况下的写即可。

  2. 本地缓存写变量使用 go 的 atomic.AddInt64 支持原子性累加本地写缓存的值。

  3. 每次执行更新 Redis 的定时任务,先将本地写缓存复制到 amount 变量,然后再将本地写缓存原子性减去 amount 的值,最后将 amount 的值 incr 到 Redis 单 key 上,实现 Redis 的单 key 的值一直累加。

  4. 容灾方案是使用备份 Redis 集群,写入时进行双写,一旦主机群挂掉,设计了一个配置开关支持读取备份 Redis。两个 Redis 集群的数据一致性,通过定时任务兜底实现。

本方案调用 Redis 的流量是跟实例数成正比,经调研读取侧的服务为主会场实例数 2 万个,写入侧服务为资产中台实例数 8 千个,所以实际 Redis 要支持的 QPS 为 2.8 万/定时任务执行间隔(单位为 s),经压测验证 Redis 单实例可以支持单 key2 万 get,8k incr 的操作,所以设置定时任务的执行时间间隔是 1s,如果实例数更多可以考虑延长执行时间间隔。

具体写入流程图如下:

e109f0690f711922dc101c6702e3eaaf.png

4.5.3 方案对比

优点缺点
方案一1. 实现成本简单1. 浪费存储资源; 2. 难以做容灾; 3. 不能做到一直累加;
方案二1. 节约资源; 2. 容灾方案比较简单,同时也节约资源成本;1. 实现稍复杂,需要考虑好并发原子性累加问题

结论:

从实现效果,资源成本和容灾等方面考虑,最终选择了方案二上线。

4.6 难点六:进行母活动与子活动的平滑切换

需求背景:

为了保证本次春节活动的最终上线效果和交付质量,实际上分了三个阶段进行的。

(1)第一阶段是内部人员测试阶段。

(2)第二个阶段是外部演练阶段,圈定部分外部用户进行春节活动功能的验证(灰度放量),也是发现暴露问题以及验证对应解决机制最有效的手段,影响面可控。

(3)第三个阶段是正式春节活动。

而产品的需求是这三个阶段是分别独立的阶段,包含用户获得奖励、展示与使用奖励都是隔离的。

1f1592a1838cc5a1a85b5f411246e972.png

技术挑战:

有多个上游调用钱包发奖励,同时钱包有多个奖励业务下游,所以大家一起改本身沟通成本较高,配置出错的概率就比较大,而且不能同步改,会有较大的技术安全隐患。

设计思路:

作为奖励入账的唯一入口,钱包资产中台收敛了整个活动配置切换的实现。设计出母活动和子活动的分层配置,上游请求参数统一传母活动 ID 代表春节活动,钱包资产中台根据请求时间决定采用哪个子活动配置进行发奖,以此来实现不同时间段不同活动的产品需求。降低了沟通成本,减少了配置出错的概率,并且可以同步切换,较大地提升了研发与测试人效。

示意图:

24ac8b5a51a9db3f3ff016dfd9cfe086.png

4.7 难点七:大流量场景下资金安全保障

钱包方向在本次春节活动期间做了三件事情来保障大流量大预算的现金红包发放的资金安全:

  1. 现金红包发放整体预算控制的拦截

  2. 单笔现金红包发放金额上限的拦截

  3. 大流量发红包场景的资金对账

  • 小时级别对账:支持红包雨/集卡/烟火红包发放 h+1 小时级对账,并针对部分场景设置兜底 h+2 核对。

  • 准实时对账:红包雨已入账的红包数据反查钱包资产中台和活动侧做准实时对账

多维度核对示意图:

ac8168ba12977b430d33d7a5f3c27f3c.png

准实时对账流程图:

2e0ae5cd61906fc5d264ae6f5fa4e411.png

说明:

准实时对账监控和报警可以及时发现是否异常入账情况,如果报警发现会有紧急预案处理。

5. 通用模式抽象

在经历过春节超大流量活动后的设计与实现后,有一些总结和经验与大家一起分享一下。

5.1 容灾降级层面

大流量场景,为了保证活动最终上线效果,容灾是一定要做好的。参考业界通用实现方案,如降级、限流、熔断、资源隔离,根据预估活动参与人数和效果进行使用存储预估等。

5.1.1 限流层面

(1)限流方面应用了 api 层 nginx 入流量限流,分布式入流量限流,分布式出流量限流。这几个限流器都是字节跳动公司层面公共的中间件,经过大流量的验证。

(2)首先进行了实际单实例压测,根据单实例扛住的流量与本次春节活动预估流量打到该服务的流量进行扩容,并结合下游能抗住的情况,在 tlb 入流量、入流量限流以及出流量限流分别做好了详细完整的配置并同。

限流目标:

保证自身服务稳定性,防止外部预期外流量把本身服务打垮,防止造成雪崩效应,保证核心业务和用户核心体验。

简单集群限流是实例维度的限流,每个实例限流的 QPS=总配置限流 QPS/实例数,对于多机器低 QPS 可能会有不准的情况,要经过实际压测并且及时调整配置值。

对于分布式入流量和出流量限流,两种使用方式如下,每种方式都支持高低 QPS,区别只是 SDK 使用方式和功能不同。一般低 QPS 精度要求高,采用 redis 计数方式,使用方提供自己的 redis 集群。高 QPS 精度要求低,退化为总 QPS/tce 实例数的单实例限流。

5.1.2 降级层面

对于高流量场景,每个核心功能都要有对应的降级方案来保证突发情况核心链路的稳定性。

(1)本次春节奖励入账与活动活动钱包页方向做好了充分的操作预案,一共有 26 个降级开关,关键时刻弃车保帅,防止有单点问题影响核心链路。

(2)以发现金红包链路举例,钱包方向最后完全降级的方案是只依赖 docker 和 MySQL,其他依赖都是可以降级掉的,MySQL 主有问题可以紧急联系切主,虽说最后一个都没用上,但是前提要设计好保证活动的万无一失。

5.1.3 资源隔离层面

(1)提升开发效率不重复造轮子。因为钱包资产中台也日常支持抖音资产发放的需求,本次春节活动也复用了现有的接口和代码流程支持发奖。

(2)同时针对本次春节活动,服务层面做了集群隔离,创建专用活动集群,底层存储资源隔离,活动流量和常规流量互不影响。

5.1.4 存储预估

(1)不但要考虑和验证了 Redis 或者 MySQL 存储能抗住对应的流量,同时也要按照实际的获取参与和发放数据等预估存储资源是否足够。

(2)对于字节跳动公司的 Redis 组件来讲,可以进行垂直扩容(每个实例增加存储,最大 10G),也可以进行水平扩容(单机房上限是 500 个实例),因为 Redis 是三机房同步的,所以计算存储时只考虑一个机房的存储上限即可。要留足 buffer,因为水平扩容是很慢的一个过程,突发情况遇到存储资源不足只能通过配置开关提前下掉依赖存储,需要提前设计好。

5.1.5 压测层面

本次春节活动,钱包奖励入账和活动钱包页做了充分的全链路压测验证,下面是一些经验总结。

  1. 在压测前要建立好压测整条链路的监控大盘,在压测过程当中及时和方便的发现问题。

  2. 对于 MySQL 数据库,在红包雨等大流量正式活动开始前,进行小流量压测预热数据库,峰值流量前提前建链,减少正式活动时的大量建链耗时,保证发红包链路数据库层面的稳定性。

  3. 压测过程当中一定要传压测标,支持全链路识别压测流量做特殊逻辑处理,与线上正常业务互不干扰。

  4. 针对压测流量不做特殊处理,压测流量处理流程保持和线上流量一致。

  5. 压测中要验证计算资源与存储资源是否能抗住预估流量

  • 梳理好压测计划,基于历史经验,设置合理初始流量,渐进提升压测流量,实时观察各项压测指标。

  • 存储资源压测数据要与线上数据隔离,对于 MySQL 和 Bytekv 这种来讲是建压测表,对于 Redis 和 Abase 这种来讲是压测 key 在线上 key 基础加一下压测前缀标识 。

  • 压测数据要及时清理,Redis 和 Abase 这种加短时间的过期时间,过期机制处理比较方便,如果忘记设置过期时间,可以根据写脚本识别压测标前缀去删除。

  1. 压测后也要关注存储资源各项指标是否符合预期。

5.2 微服务思考

在日常技术设计中,大家都会遵守微服务设计原则和规范,根据系统职责和核心数据模型拆分不同模块,提升开发迭代效率并不互相影响。但是微服务也有它的弊端,对于超大流量的场景功能也比较复杂,会经过多个链路,这样是极其消耗计算资源的。本次春节活动资产中台提供了 sdk 包代替 rpc 进行微服务链路聚合对外提供基础能力,如查询余额、判断用户是否获取过奖励,强制入账等功能。访问流量最高上千万,与使用微服务架构对比节约了上万核 CPU 的计算资源。

6. 系统的未来演进方向

(1)梳理上下游需求和痛点,优化资产中台设计实现,完善基础能力,优化服务架构,提供一站式服务,让接入活动方可以更专注进行活动业务逻辑的研发工作。

(2)加强实时和离线数据看板能力建设,让奖励发放数据展示的更清晰更准确。

(3)加强配置化和文档建设,对内减少对接活动的对接成本,对外提升活动业务方接入效率。

来源:字节跳动技术团队

收起阅读 »

浙大教授盘和林:未来NFT或元宇宙,会出现万亿市值以上的企业

目前学界或者业界对元宇宙都没有一个非常准确的定义。我个人认为元宇宙是虚拟世界向人类现实世界的一种延伸、一种融合联通, 元宇宙实际上是将虚拟世界的体验改善:让虚拟世界能够做到现实世界中的事情,比如工作、生活,以前我们要开车奔赴目的地,我们现在可以在元宇宙上实现,...
继续阅读 »

目前学界或者业界对元宇宙都没有一个非常准确的定义。我个人认为元宇宙是虚拟世界向人类现实世界的一种延伸、一种融合联通, 元宇宙实际上是将虚拟世界的体验改善:让虚拟世界能够做到现实世界中的事情,比如工作、生活,以前我们要开车奔赴目的地,我们现在可以在元宇宙上实现,以前工厂上班单调乏味,现在我们改造成元宇宙,可能就会不一样。

但元宇宙和以前我们所说的虚拟世界还是有区别的。元宇宙有几个基本概念,比如它有经济系统、用户创作等。简单地说,原来的虚拟世界是一个跟现实世界有很多脱节的地方,元宇宙理想的状态是我们在现实世界当中有一个人,在虚拟世界也有一个人。元宇宙目前最需要改善的就是我们的感官体验。现在的虚拟世界可能只有视觉感受。在真正的元宇宙中,感官、嗅觉这些都要和现实无限接近和逼真。

单一从VR和AR技术限制的角度来看,目前学界和业界的专家大都认为,元宇宙至少要到2040年以后才能达到理想的状态,现在无非就是现实增强(VR)可能比原来的体验更高,甚至加入了一些互动元素而已,和真正严格意义上的元宇宙中的场景还是有很大差距的。不过,我个人是技术乐观派,有可能在各种资本的催化下,或许科技进步的脚步比我们想象的来得更快,我们也可能在2040年之前就能够感受到元宇宙实质性的进步。

从根本上来说,互联网平台不能提供所有的内容,需要所有的用户参与内容的制作,否则这个元宇宙可能就是寡淡的单线游戏。所以元宇宙一个非常重要的特征,就是开放用户参与,内容创作这一块可能是年轻人参与的部分。未来人们不再满足物质消费,而是千金难买我乐意,未来搞怪的包括个性化的艺术作品、道具、虚拟偶像,很可能都是一个比较好的变现渠道。只要有自己生产的内容,且能够满足一些爱好者的偏好,就有可能抓住意想不到的变现机会。

我个人认为NFT或者元宇宙,起码要出几家万亿市值以上的头部企业,这个我坚信一定会出现的,其实我们现在都能够感受到这种变化的雏形,能看到呼之欲出的样子。比如,现在开或还是多高端的论坛,一定要有在线视频支持,以前很少有嘉宾在线发言,现在几乎很多的会议与论坛都有视频嘉宾发言,这种体验效果已经非常好了。未来随着5G或者6G带宽实现高速、低时延,在线未必能够实现连嗅觉都能传递,但一定会很大程度增强用户体验,从而促进线下产业的效率,拓展线下产业的边界,因而,线上线下深度融合带来的效率转换成伟大公司或产业,想象空间还是非常大的,也是值得期待的。

虽然现在有反对的声音,认为元宇宙会让人沉迷在一个虚拟的空间里,逃避现实,甚至有人认为影响到生育。尽管虚拟空间确实会让人沉迷,但我不完全赞同就是洪水猛兽。

沉迷于虚拟世界,可不影响男女接触了吗?但实际上持持观点的人不了解Z世代,1995-2010年出生的这批年轻人,你就正常让他们接触,他们现实世界的社交圈子也是很窄,那时候还是独生子女,又整日电子产品相伴,天天猫在互联网,你让他们怎么找到彼此?所以,才会有很多网络上的婚恋网站,社交网站,去撮合。所以,这完全是一个杞人忧天,互联网也好,元宇宙也好,其核心是连接,而不是断开,连接就包括社交,有了社交接触,婚姻反而更好实现。如今的问题是什么?问题是社交媒体没有为社交增加可信度,存在虚伪包装,如果减少这些虚伪包装,网络红娘效果一定比现实红娘效果要好。元宇宙时代是多元文化时代,这个时代又多年轻人以部落状态在一起社交,比如国潮、国风、二次元。相同爱好的年轻人在一起,当然有更多交集,也更容易走向婚姻,不是吗?

我认为,元宇宙和现实不能说是一种对立关系,而是弥补。即使不考虑疫情的场景,假如像今天为了做一场直播和节目,往返于广州和北京的航班上,我们的时间成本是非常高的。原来,我们之所以没有像今天的这么线上会议或直播,很大程度上是是因为数字技术限制,当然还有用户习惯,而今天5G等数字技术的进步,以及疫情的用户习惯培育,才显得那么理所应当。实事求是地说,我们今天的直播很难有真正坐着面对面聊天的效果,不过已经能够在很大程度上满足了交流需要,其便捷性已经超过了小微不好体验。当然。我们完全可以憧憬未来,随着通信网络传递实现低时延与增强带宽,即网速提升,未来直播等在线视频实际上是能够做到更接近真实的,这完全可以期待的。未来,我们可以在元宇宙中实现更多学习、生活、工作的场景。

技术进步必然带来产业的大发展。为什么我说有万亿级别的企业呢?就拿服装产业来说吧,现在虽然也可以在网购服装,但颜色、尺寸等体验,离现实效果总是有不小的差异,影响了用户体验和消费欲望。如果互联网、VR、AR等数字技术能够接近到95%甚至更高,很可能对服装产业的商业模式产生颠覆性改变,也许我们以后逛服装店,就看不到实体衣服了,而是在元宇宙中试衣服后,工厂就快递过来了。实际上,类似这种个性化制已经发生在我们现实当中,只不过目前的数字技术还影响消费体验而已,未来在元宇宙中无限接近现实世界的体验。我们可以试想,类似服装的很多传统产业,都可能在用户体验、改善供需匹配、产业链资源配置效率等方面得到大幅度提升,其带来的巨大价值或整个产业生产效率提升,那将是以万亿为单位的。

有人问,什么东西是非得在元宇宙里做不可的,就是这个东西我只能在元宇宙里做,在别的地方做就不行?比如我必须在现实世界里买房,但元宇宙毕竟是虚拟空间,在元宇宙买房可有可无,那它能成为一门生意吗?

实际上,生意有好多种,一个是经济价值,一个是情感价值,还有完全个性化的个人价值,这些价值产生支付都可以视为生意,因此,未必就是房子才是刚需。也就是说,可能我们这一代人永远搞不懂那些玩游戏的人,为什么在游戏里面可以大把地花钱去买一些工具。人是有多种复杂需求的,除了我们常规理解的食物、衣着是刚需,慢慢地娱乐、社交实际上也是一些人的刚需。

并不是说必须在元宇宙里实现的,才会有客户,才会有购买力。像用户创作或者参与内容变现,体验价值可能才是元宇宙里的“刚需”。类似现在的抖音,你看到有很多“非常无聊”的视频也有惊人的点击量,那么刷抖音是不是一种“刚需”呢?还有元宇宙中的虚拟人也是一个产业,已经人格化的虚拟人,背后有一些系列的社会及经济属性。也许,我们在元宇宙中有一个我们一模一样的虚拟人也是一种“刚需”呢。补充说明一下前面说到的元宇宙与虚拟世界的区别这个问题,原来的虚拟世界里没有人格化的虚拟人,这也是元宇宙与此前的虚拟世界一个非常重要的区别。比如元宇宙已经出现了性骚扰案,那就是已经把它人格化了。而虚拟世界就没有人格化问题。

也就是说,在元宇宙中,我们不能按常规理解只有吃饭、睡觉这一类的是刚需。娱乐化或者线上会议在有些地方就是刚需。在美国已经出现了元宇宙的毕业典礼,国内也有大型元宇宙会议,如果由于距离无法在线下实现的时候,它某种程度上成为了一种刚需。数字技术改变着我们当下的经济社会,元宇宙就带有“社会性”和“经济性”。

回到今天的主题上来,靠元宇宙发财现实吗?现在元宇宙这么火,这也是大家现在都很关注的问题。我认为短时间内肯定不会,至少绝大多数人无法靠元宇宙发财,而且即使在未来,元宇宙是多数人参与的游戏,但只会是少数人的发财游戏。很简单,现在你参与不进去, NFT的门槛和风险绝对不是普通人能够玩的,打个比方,抖音的视频高流量毕竟是少数。因此,元宇宙可能是我们参与的理想世界和美好家园,那里可以摆脱现实世界诸多的“痛苦”,或许有很多精神财富,但未必每个人都能够赢得货币财富。

到目前为止,绝大多数的专家也没有讲出一个关于元宇宙的所以然,或者成熟的商业模式。元宇宙怎么能够成为绝大多数普通人的创富工具呢?不排除有些人敢于玩NFT的,可能赢得了一笔财富,但这占人群中的比重是非常小的。

假如非得要说创富,如果未来元宇宙普及,年轻人怎么在元宇宙中赚到第一桶金?个人的看法,依然是制造内容,而不是倒卖内容。元宇宙的一大特征是数字资产确权,利用NFT缔造了经济系统,由于元宇宙元素很多,互联网平台不可能提供所有的内容和服务,此时元宇宙需要什么?需要所有用户参与制造内容,否则这个元宇宙就是寡淡的单线游戏,元宇宙时代要开放游戏和用户参与。所以元宇宙要用户来创造内容。此时包括虚拟偶像、道具装备、个性化艺术涂鸦等等,都是一个很好的变现渠道,不需要你多优秀,要你做出来的东西,有足够多的爱好者,所谓千金难买我乐意,千金难买我喜欢。搞怪、鬼畜、赛博朋克、意识流,都没关系,只要符合一部分人的审美喜好就能够实现变现。所以,年轻人要在3D建模、剪辑、设计、区块链NFT、AI上去学习。

而在当下,元宇宙的概念是从华尔街讲故事讲出来的,目前第一波还是资本玩的游戏,无论是从游戏也好、还是社交也好,第一波人可能把握一定的财富机会,但终归是少数人。

我认为,现在元宇宙的样子,只是人类依据过去互联网技术与发展趋势,以及对未来互联网世界的美好想象而折射到今天的一个产物,但未来会长成什么样子,甚至用什么名词更合适,现在都是不确定的。从既往互联网发展的经验来看,我们对未来的判断,很可能是盲人摸象式的,我们今天认为元宇宙的一些特征,未必就是未来真正的样子。但是人类还是要靠想象去谋划。就像人类研究未来学一样,我们对未来数字世界也要有想象力,无论叫第三代互联网也好,还是第四代互联网也好,人类都需要有一个憧憬,或者有一个发展方向,在这个过程当中不断去调试。这绝不仅仅是资本讲故事的需要,人类世界因为想象而变得更加美好。

目前来看,元宇宙很多商业模式是很难实现的。当然,元宇宙即便在当下也是能够实实在在促进产业的,例如VR和AR技术及相关产业,还有社交、游戏应用场景是比较确定的,这些行业已经分享到元宇宙的“想象力红利”。至于炒房和NFT,都是风险泡沫非常大的,例如元宇宙房地产,我们没办法在元宇宙里面住,肉身还得寄放在高房价和拥堵的城市森林之中。

而且现在还有一个很大的误区,我们说数字藏品、NFT,现在只讲一个独特性、稀缺性,不讲其中的价值,可能存在很大问题。我们知道,商品或服务的独特性一定要跟价值关联起来,比如小孩子画了一幅画,它非常有独特性、稀缺性,因为世界上只有他一个人画了这一幅,但是它不一定产生艺术价值。现在,国内外都有这种情况,一种毫无艺术价值的NFT被拍出了天价,这就是泡沫。

目前很多的数字藏品过于追求稀缺性。其实,稀缺性往往是很容易获得的,比如我今天随便乱写几个字、乱画几笔也是有稀缺性,但它不一定代表有价值。当然,应该说泡沫未必都是坏事,就像啤酒的泡沫,产业也相类似,一个新兴产业就是要靠泡沫想象或者实践往前走的。泡沫下面才是甘美的啤酒。

泡沫破裂往往才会沉淀出新的产业。遥想当年互联网撕杀也是异常惨烈,几次波及全世界的互联网泡沫之后才有今天的互联网产业,那真是“一将功成万骨枯”,最后起来那么几个互联网巨头,但背后参与竞争的可能是成千上万个互联网企业,没有一个产业是风轻云淡就起来的,都是异常的市场搏杀。元宇宙赛道也会是这样的,为什么我一再主张,哪怕是投资,哪怕是故事,都交给市场去选择,每一个市场的形成都是残酷的。市场的成熟发展,没有这些 “万骨枯”就没有“一将功成”。当然,这种容忍度也是要设置底线的,就是自愿参与原则,即市场主体按照自己的意愿参与,同时还要考虑参与者的风险识别和承受能力,对一些没有多少风险识别能力、低承受能力的人要进行规劝、规避,甚至设置行政法规的规制,比如NFT就不能让跳广场舞的大妈来参与了,未来需要形成一系列的参与规则。

现在很多元宇宙或者NFT被导入到一些传销的概念里去了,这些人实际上无论从知识结构还是从风险承受能力都是很弱的,市场对这一类参与者应该尽可能地采取一些规则,我们要考虑弱势群体的一些福利和承受能力。其他的还是要鼓励,比如资本的参与,哪怕资本最终“血流成河”也是体现“市场风险与利润并存”的理念的,我认为这就是市场经济的代价,不能因为代价就裹足不前。

主张“破坏性创新”的创新之父熊比特曾经说过,企业及企业家就像旅鼠一样,一只只义无反顾地向着大海奔赴死亡之旅,用“旅鼠”来形容企业及企业家其实十分形象、生动,也让我非常感慨,正是一代又一代的企业及企业家向“死”而生,才让经济社会生生不息!每一个产业中的企业及人,未尝不都是“旅鼠”,我们的互联网产业也是这样走过来的, 元宇宙产业的形成过程中未尝不是如此。

在这个过程当中,谁能够更准确地找准用户的需求,谁就更容易在残酷的市场中胜出。另外就是一定要符合社会的基本价值和规则,否则的话你做了一个非常大的市值,最后会付出非常惨痛的代价。如果你不符合社会的价值、逻辑和伦理的一些商业模式,最终还是会被淘汰的,这样的例子在当前的现实中并不鲜见。

在抓住需求的时候,一定要去考虑多方相关利益者的约束条件。如果一种商业模式一再利用成瘾机制,导致用户人性扭曲,或者是社会价值的扭曲,这个商业模式哪怕再有用户支付率,再多忠实消费者,并不代表它是一个非常伟大的商业模式。

毫无疑问,这些都是值得元宇宙产业值得思考的。

因此,元宇宙发展可能还是要立足于实现价值、满足需求的维度来促进我们的产业效率提升。如果是符合客户及社会价值需求的,我们就可以大胆去尝试。我认为,国内发展元宇宙产业也应该是这样的,把边界约束好,当然产业的边界未必很清晰,但价值的边界不能出偏差,其他的还是要鼓励去发展,发展成什么样子,还是要予以审慎包容监管心态,来对待元宇宙的发展。女大十八变,元宇宙还是刚刚出生的女婴,未来长成什么样子,我们还不知道,但我完全有理由去憧憬是一个曼妙少女。这不是空想,毕竟我们前面说了,元宇宙是人们基于过去互联网发展现实与现状、未来发展趋势判断而形成的,并非异想天开的空想。

接下来,谈谈同样热门的碳中和与个人碳账户话题。

我认为,这个不确定的世界里,未来有两件事情是非常确定的:第一是数字化,其中包括产业数字化,就是现实世界往数字走,AIoT(人工智能物联网),也包括元宇宙,数字世界和现实世界深度融合。我在我即将出版的《从AIoT到元宇宙:关键技术、产业图景与投资机遇》(暂定名)一书中描述了一个场景,期待AIoT与元宇宙相向而行,说的也是现实世界与数字世界相向而行。第二个是碳中和,这事关人类生存空间。关于碳中和,我先说一个观点,环境经济学认为,满足人类基本需求的污染不应该被视为一种需要规制的环境污染。比如举行一场篝火晚会,篝火会带来一定的碳排放,但不能因为影响环境就禁止篝火晚会。

本质上来说,碳中和也好,元宇宙也好,核心还是依赖技术进步,而不是简单的规制。比如,按照以前能源悲观主义者的观点,石油早消耗殆尽了。而后来,石油开发技术进步,页岩气开发,包括现在的风能和光伏的利用,未来的能源是否枯竭依然要依赖于技术进步。元宇宙所需要的算力资源也是同理,我们还是要通过技术去实现。

普通人如何参与开展碳中和呢?我认为,普通人只要按照规矩办事,顺着政策指导的方向,从需求出发,不过度浪费,就是对碳中和最好的支持。比如新能源车,你和老百姓说很多道理,说锂电动车节能环保,碳排放少,这些都可能无法触及灵魂。但近年来锂电池车大卖,原因很简单,锂电池动力车成本比汽油车低,不仅仅是制造成本,还有使用成本,维护成本,保养成本。老百姓一算账就明白了,。当然除了性价比高,当前充电桩设施也普及了,所以消费行为就导向了锂电池汽车。但反过来说,现在氢能汽车就不是很好推,原因很简单,氢能的运输成本太高,因为体积大,密度低,所以加氢站多不起来,成本太高。这个时候个人的观点,就是在氢能运输问题解决之前,我们也不能因为要碳中和而去大面积推广氢能汽车。所以对于普通人来说,碳中和就是根据你的需要,算好经济账,不要浪费。你普通家庭在耗能上实现经济了,省钱了,那么这个世界的碳排放自然就少了。

其实,作为我经济学者,更关注碳中和的产业问题,而不是个人减碳行为。我认为,“双碳”目标是中国作为一个大国的政治承诺,也是履行国际义务的体现。但未来真正的竞争不是在于碳中和,而是在于碳足迹(全生命周期碳排放),在于碳关税。碳关税可能是一个贸易竞争的核心焦点,这一块如何争取,可能比我们关注个人碳账户更有价值,至少更迫切。无论是产业视角的碳足迹,还是个体行为的个人碳账户,我个人觉得最重要的还是要在底层数据以及宏观产业层面去解决“双碳”问题。当然,我不是说个人碳账户不重要,俗话说众人拾柴火焰高,多数人的自觉绿色环保的行为,可能会给生活环境带来很大的变化。不过,我更关注碳足迹、碳关税所带来的产业竞争,这也关系到每个人的就业、收入等切身福利。

对中国实现双碳目标来说,第一个就是对高能耗的产业进行一定的测算。

第二个角度是产业竞争。无论怎么样,未来都不可能实现地球村,总是会有竞争的。现在的竞争,不会是战争的竞争,虽然也有俄乌冲突,更多是产业的竞争,本质上还是要回归到产业和企业效率上竞争,一个国家的产业效率越高,企业的竞争力越强,你在世界上获取的经济效益份额可能就越大。

因此,中国还是要去关注那些外向型即国际贸易高度相关的产业,一定想办法把这些产业的碳足迹核算好,不用说领先于全球,起码要同步欧美可能采取贸易壁垒的产业先扎扎实实做起来,否则欧美做出一整套、有说服力的碳足迹数据来,我们未来就会在产业上吃亏,他们必然高举碳关税来打击我们的产业,设置新的国际贸易壁垒。

产业看起来离我们普通老百姓很远,但事实上这几年的产业转移,包括企业的竞争,事实上已经关系到老百姓的就业和收入。

还是那句话,说一尺不如干一寸。我们对碳足迹也好,或者对元宇宙也好,不能停留在想象中的好处和坏处,我们一定要在实践中实现它,要往前迈一步。国家前段时间已经出台了要进行碳核查,建立县一级的碳核查机制的文件。我觉得,当前最迫切的,除了宏观层面,还是要往产业上靠,尤其是会受到影响的外贸型产业。
来源:
mp.weixin.qq.com/s/4azKexPbPlfqVmsLErp2QA

收起阅读 »

剑指 Offer 10- I. 斐波那契数列

题目描述: 写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项(即 F(N))。斐波那契数列的定义如下: F(0) = 0,   F(1) = 1 F(N) = F(N - 1) + F(N - 2), 其中...
继续阅读 »

题目描述:


写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项(即 F(N))。斐波那契数列的定义如下:


F(0) = 0,   F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。


答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。


来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/fe…
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。


第一个想法使用递归:但是这个题目用递归写会超时。所以直接使用非递归的解法。


    public int fib(int n) {
final int MOD = 1000000007;
if(n == 0){
return 0;
}
if(n<=2){
return 1;
}
//因为已经确定第一个值和第二个值了,所以直接从第三个数开始做循环
int i1 = 1;
int i2 = 1;
int sum = 0;
for (int i=3;i<=n;i++){
sum = (i1+i2) % MOD;

i1 = i2;
i2 = sum;
}
return sum;
}

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

Android-ViewBinding的内存泄露

场景 在MainActivity中分别加载两个Fragment处理业务。 首先触发加载SecondFragment: //MainActivity触发 supportFragmentManager.commit { add(R.id.content...
继续阅读 »
场景

在MainActivity中分别加载两个Fragment处理业务。
首先触发加载SecondFragment:


//MainActivity触发
supportFragmentManager.commit {
add(R.id.contentLayout, FirstFragment())
addToBackStack(null)//点击返回键可以回到FirstFragment
}

//FirstFragment布局中有一个自定义MyButton且有bitmap属性
class MyButton @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyle: Int = 0
) : Button(
context, attrs, defStyle
) {
private val bitmap = BitmapFactory.decodeResource(context.resources, R.drawable.a)
}

然后触发加载SecondFragment;


//MainActivity触发
supportFragmentManager.commit {
replace(R.id.contentLayout, SecondFragment())
addToBackStack(null)
}

Android Profile可以发现有内存泄露

1.png


- MyButton中的bitmap无法释放。


为什么认为bitmap无法释放就是内存泄漏呢?

内存泄漏:简单点说,就是该释放的内存无法得到释放,而且内存不能被使用。
从Fragment的生命周期说起,从FirstFragment切换到SecondFragment,前者生命周期从onPause->onStop->onDestoryView,注意这里只走到onDestoryView,并没有onDetach以及onDestory。其实也很好理解,FirstFragment是加入了回退栈,后续是要被恢复,所以保留了Fragment对象,但为了不占用过多的内存,View会被销毁释放资源。
当FirstFragment从回退栈回到前台,会再次触发onCreateView重建View。既然View会重建,那么之前的View就是不需要的,留着也没用,就应该销毁掉。


该释放的View、Bitmap没有被释放,所以就出现了泄漏。


例子比较简单,只是为了说明问题,如果FirstFragment View持有大量占内存的对象,而且SecondFragment的加载需要耗费比较多的内存且存在跳转的其他页面的可能性,那么FirstFragment View的释放就显得很有必要。


补充引用链:FirstFragment-MyButton-Bitmap


onDestoryView官方注释

注意到这句“The next time the fragment needs
* to be displayed, a new view will be created”,当Fragment恢复时,会创建新的view添加到Fragment,也就是重走onCreateView,那么我理解旧的view就应该可以被销毁。


    /**
* Called when the view previously created by {@link #onCreateView} has
* been detached from the fragment. The next time the fragment needs
* to be displayed, a new view will be created. This is called
* after {@link #onStop()} and before {@link #onDestroy()}. It is called
* <em>regardless</em> of whether {@link #onCreateView} returned a
* non-null view. Internally it is called after the view's state has
* been saved but before it has been removed from its parent.
*/
@MainThread
@CallSuper
public void onDestroyView() {
mCalled = true;
}

LeakCanary日志

建议在onDestroyView要释放掉View


LeakCanary: Watching instance of androidx.constraintlayout.widget.ConstraintLayout (com.yang.myapplication.MyFragment received Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks)) with key 0f101dfe-5e4e-4448-95cc-f5d08bbdf06e

解决方案

将ViewBinding置空就欧了。其实这也是官方的建议,当你新建项目的时候,就能看到这样的案列。


   override fun onDestroyView() {
super.onDestroyView()
_binding = null
}

总结

当出现Fragment没有被销毁(onDestory没有回调),而view需要被销毁时(onDestoryView),要注意把ViewBinding置空,以免出现内存泄露。


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

在Flutter上优雅的请求网络数据

当你点进来看这篇文章时,应该和我一样在思考如何优雅的请求网络、处理加载状态、处理加载异常。希望这篇文章和案例能给你带来不一样的思考。 解决的问题 通用异常处理 请求资源状态可见(加载成功,加载中,加载失败) 通用重试逻辑 效果展示 为了演示请求失败的处理,...
继续阅读 »

当你点进来看这篇文章时,应该和我一样在思考如何优雅的请求网络、处理加载状态、处理加载异常。希望这篇文章和案例能给你带来不一样的思考。


解决的问题



  • 通用异常处理

  • 请求资源状态可见(加载成功,加载中,加载失败)

  • 通用重试逻辑


效果展示


为了演示请求失败的处理,特意在wanApi抛了两次错
LBeZ5Q.gif


正文


搜索一下关于flutter网络封装的多半都是dio相关的封装,简单的封装、复杂的封装百花齐放,思路都是工具类的封装。今天换一个思路来实现,引入repository对数据层进行操作,在repository里使用dio作为一个数据源供repository使用,需要使用数据就对repository进行操作不直接调用数据源(在repositoy里是不允许直接操作数据源的)。用WanAndroid的接口写个示例demo


定义数据源


使用retrofit作为数据源,感兴趣的小伙伴可以看下retrofit这个库


@RestApi(baseUrl: "https://www.wanandroid.com")
abstract class WanApi {
factory WanApi(Dio dio, {String baseUrl}) = _WanApi;

@GET("/banner/json")
Future<BannerModel> getBanner();

@GET("/article/top/json")
Future<TopArticleModel> getTopArticle();

@GET("/friend/json")
Future<PopularSiteModel> getPopularSite();
}

生成的代码


class _WanApi implements WanApi {
_WanApi(this._dio, {this.baseUrl}) {
baseUrl ??= 'https://www.wanandroid.com';
}

final Dio _dio;

String? baseUrl;

@override
Future<BannerModel> getBanner() async {
const _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
final _data = <String, dynamic>{};
final _result = await _dio.fetch<Map<String, dynamic>>(
_setStreamType<BannerModel>(
Options(method: 'GET', headers: _headers, extra: _extra)
.compose(_dio.options, '/banner/json',
queryParameters: queryParameters, data: _data)
.copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl)));
final value = BannerModel.fromJson(_result.data!);
return value;
}

@override
Future<TopArticleModel> getTopArticle() async {
const _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
final _data = <String, dynamic>{};
final _result = await _dio.fetch<Map<String, dynamic>>(
_setStreamType<TopArticleModel>(
Options(method: 'GET', headers: _headers, extra: _extra)
.compose(_dio.options, '/article/top/json',
queryParameters: queryParameters, data: _data)
.copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl)));
final value = TopArticleModel.fromJson(_result.data!);
return value;
}

@override
Future<PopularSiteModel> getPopularSite() async {
const _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
final _data = <String, dynamic>{};
final _result = await _dio.fetch<Map<String, dynamic>>(
_setStreamType<PopularSiteModel>(
Options(method: 'GET', headers: _headers, extra: _extra)
.compose(_dio.options, '/friend/json',
queryParameters: queryParameters, data: _data)
.copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl)));
final value = PopularSiteModel.fromJson(_result.data!);
return value;
}

RequestOptions _setStreamType<T>(RequestOptions requestOptions) {
if (T != dynamic &&
!(requestOptions.responseType == ResponseType.bytes ||
requestOptions.responseType == ResponseType.stream)) {
if (T == String) {
requestOptions.responseType = ResponseType.plain;
} else {
requestOptions.responseType = ResponseType.json;
}
}
return requestOptions;
}
}

repository封装


Resource是封装的资源加载状态类,用于包装资源


enum ResourceState { loading, failed, success }

class Resource<T> {
final T? data;
final ResourceState state;
final dynamic error;
Resource._({required this.state, this.error, this.data});

factory Resource.failed(dynamic error) {
return Resource._(state: ResourceState.failed, error: error);
}

factory Resource.success(T data) {
return Resource._(state: ResourceState.success, data: data);
}

factory Resource.loading() {
return Resource._(state: ResourceState.loading);
}

bool get isLoading => state == ResourceState.loading;
bool get isSuccess => state == ResourceState.success;
bool get isFailed => state == ResourceState.failed;
}

接下来我们在Repository里使用WanApi来封装,我们通过流的方式返回了资源加载的状态可供View层根据状态展示不同的界面,使用try-catch保证网络请求的健壮性


class WanRepository extends BaseRepository {
late WanApi wanApi = GetInstance().find();
///获取首页所需的所有数据
Stream<Resource<HomeDataMapper>> homeData() async* {
//加载中
yield Resource.loading();
try {
var result = await Future.wait<dynamic>([
wanApi.getBanner(),
wanApi.getPopularSite(),
wanApi.getTopArticle()
]);
final BannerModel banner = result[0];
final PopularSiteModel site = result[1];
final TopArticleModel article = result[2];
//加载成功
yield Resource.success(
HomeDataMapper(site.data, banner.data, article.data));
} catch (e) {
//加载失败
yield Resource.failed(e);
}
}
}

咋一看感觉没啥问题细思之下问题很多,每一个请求还多了try-catch以外那么多的模板方法,实际开发中只写try包裹的内容才符合摸鱼佬的习惯。ok,我们把模板方法提取出来到一个公共方法里去,就变成了这样:


class WanRepository extends BaseRepository {
late WanApi wanApi = GetInstance().find();
///获取首页所需的所有数据
Stream<Resource<HomeDataMapper>> homeData() async* {
///定义加载函数
loadHomeData()async*{
var result = await Future.wait<dynamic>([
wanApi.getBanner(),
wanApi.getPopularSite(),
wanApi.getTopArticle()
]);
final BannerModel banner = result[0];
final PopularSiteModel site = result[1];
final TopArticleModel article = result[2];
//加载成功
yield Resource.success(
HomeDataMapper(site.data, banner.data, article.data));
}
///将加载函数放在一个包装器里执行
yield* MyWrapper.customStreamWrapper(loadHomeData);
}
}

得益于Dart中函数可以作为参数传递,所以我们可以定义一个包装方法,入参是具体业务的函数,出参和业务函数一致,在这个方法里可以处理各种异常,甚至可以实现通用的请求重试(只需要在失败的时候弹窗提醒用户重试,获得认可后再次执行function就可以了,更关键的是此时状态管理里对repository的调用依旧是完整的,也就是说这是一个通用的重试功能)
包装器代码:


class MyWrapper {
//流的方式
static Stream<Resource<T>> customStreamWrapper<T>(
Stream<Resource<T>> Function() function,
{bool retry = false}) async* {
yield Resource.loading();
try {
var result = function.call();
await for(var data in result){
yield data;
}
} catch (e) {
//重试代码
if (retry) {
var toRetry = await Get.dialog(const RequestRetryDialog());
if (toRetry == true) {
yield* customStreamWrapper(function,retry: retry);
} else {
yield Resource.failed(e);
}
} else {
yield Resource.failed(e);
}
}
}
}

其实就是把相同的地方封装成一个通用方法,不同的地方单独拎出来编写,然后作为一个参数传到包装器里执行。显然这样的方法却不够优雅,每次在写repository的时候都得创建一个函数在里面编写请求数据的逻辑然后交给包装器执行。我们肯定希望repository里代码长成这个样子:


@Repo()
abstract class WanRepository extends BaseRepository {
late WanApi wanApi = GetInstance().find();

///获取首页所需的所有数据
@ProxyCall()
@Retry()
Stream<Resource<HomeDataMapper>> homeData() async* {
var result = await Future.wait<dynamic>(
[wanApi.getBanner(), wanApi.getPopularSite(), wanApi.getTopArticle()]);
final BannerModel banner = result[0];
final PopularSiteModel site = result[1];
final TopArticleModel article = result[2];
yield Resource.success(
HomeDataMapper(site.data, banner.data, article.data));
}
}

是的没错,最终的repository就长这个样子,你只需要在类上打个注解@Repo在需要代理调用的方法上注解@ProxyCall,运行 flutter pub run build_runner build 就可以生成对应的包装代码:


// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'wan_repository.dart';

// **************************************************************************
// RepositoryGenerator
// **************************************************************************

class WanRepositoryImpl = WanRepository with _WanRepository;

mixin _WanRepository on WanRepository {
@override
Stream<Resource<HomeDataMapper>> homeData() {
return MyWrapper.customStreamWrapper(() => super.homeData(), retry: true);
}
}

结语


感谢你的阅读,这只是一个网络请求封装的思路不是最优解,但希望给你带来新思考


附demo地址:gitee.com/cysir/examp…


flutter版本:2.8


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

阿里四面,居然栽在一道排序算法上

前言 算法是程序的灵魂,一个优秀的程序是可以在海量的数据中,仍保持高效计算。目前各大厂的面试要求也越来越高,算法肯定会要去。如果你不想去大厂,只想去小公司,获取并不需要要求算法。但是你永远只能当一个代码工人,也就是跟搬砖的没区别。可能一两年后你就会被淘汰。 ...
继续阅读 »

前言



算法是程序的灵魂,一个优秀的程序是可以在海量的数据中,仍保持高效计算。目前各大厂的面试要求也越来越高,算法肯定会要去。如果你不想去大厂,只想去小公司,获取并不需要要求算法。但是你永远只能当一个代码工人,也就是跟搬砖的没区别。可能一两年后你就会被淘汰。
如果不想永远当个代码工人,就在业余时间学学数据结构和算法。



今天就来分享一个朋友阿里四面挂了的排序算法题912. 排序数组,
排序数组这道题本身是没有规定使用什么排序算法的,但面试官指定需要使用归并排序算法来解答,肯定是有他道理的。


我们知道,排序算法有很多,大致有如下几种:


MESA Monitor


其中归并排序应该是使用的最多的几种之一,Java中Arrays.sort()采用了一种名为TimSort的排序算法,就是归并排序的优化版本。归并排序自身的优点有二,首先是因为它的平均时间复杂度低,为O(N*logN);其次它是稳定的排序,即相等元素的顺序不会改变;除了这两点优点之外,其蕴含的分治思想,是可以用来解决我们许多算法问题的,这也是面试官为什么要指定归并排序的原因。好了,废话不多说,我们接下来具体看看归并排序算法是如何实现的吧。


归并排序(递归版)


归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治策略,即分为两步:分与治。


1. 分:先递归分解数组成子数组


2. 治:将分阶段得到的子数组按顺序合并


我们来具体看看例子,假设我们现在给定一个数组:[6,3,2,7,1,3,5,4],我们需要使用归并算法对其排序,其大致过程如下图所示:


MESA Monitor


阶段可以理解为就是递归拆分子序列的过程,递归的深度为log2n。而治的阶段则是将两个子序列进行排序的过程,我们通过图解看看治阶段最后一步中是如何将[2,3,6,7]和[1,3,4,5]这两个数组合并的。


MESA Monitor


图中左边是复制的临时数组,而右边是原数组,我们将左右指针对应的值进行大小比较,将较小的那个数放入原数组中,然后将相应的指针右移。比如第一步中,我们比较左边指针L指向的2和右指针R指向的1,R指向的1小,则把1放入原数组中的第一个位置中,然后R指针向右移动。后面再继续,直到左边临时数组的元素都按序覆盖了右边的原数组。最后我们通过上图再结合源码来看看吧:


class Solution {
public int[] sortArray(int[] nums) {
sort(0, nums.length - 1, nums);
return nums;
}

// 分:递归二分
private void sort(int l, int r, int[] nums) {
if (l >= r) return;

int mid = (l + r) / 2;
sort(l, mid, nums);
sort(mid + 1, r, nums);
merge(l, mid, r, nums);
}


// 治:将nums[l...mid]和nums[mid+1...r]两部分进行归并
private void merge(int l, int mid, int r, int[] nums) {
int[] aux = Arrays.copyOfRange(nums, l, r + 1);

int lp =l, rp = mid + 1;

for (int i = lp; i <= r; i ++) {
if (lp > mid) { // 如果左半部分元素已经全部处理完毕
nums[i] = aux[rp - l];
rp ++;
} else if (rp > r) { // 如果右半部分元素已经全部处理完毕
nums[i] = aux[lp - l];
lp ++;
} else if (aux[lp-l] > aux[rp - l]) { // 左半部分所指元素 > 右半部分所指元素
nums[i] = aux[rp - l];
rp ++;
} else { // 左半部分所指元素 <= 右半部分所指元素
nums[i] = aux[lp - l];
lp ++;
}
}
}
}

我们可以看到,分阶段的时间复杂度是logN,而合并阶段的时间复杂度是N,所以归并算法的时间复杂度是O(N*logN),因为每次合并都需要对应范围内的数组,所以其空间复杂度是O(N);


归并排序(迭代版)


上面的归并排序是通过递归二分的方法进行数组切分的,其实我们也可以通过迭代的方法来完成分这步,看下图:


MESA Monitor


其因为数组,所以我们直接通过迭代从1开始合并,其中sz就是合并的长度,这种方法也可以称为自底向上的归并,其具体的代码如下


class Solution {
public int[] sortArray(int[] nums) {
int n = nums.length;
// sz= 1,2,4,8 ... 排序
for (int sz = 1; sz < n; sz *= 2) {
// 对 arr[i...i+sz-1] 和 arr[i+sz...i+2*sz-1] 进行归并
for (int i = 0; i < n - sz; i += 2*sz ) {
merge(i, i + sz - 1, Math.min(i+sz+sz-1, n-1), nums);
}
}
return nums;
}

// 和递归版一样
private void merge(int l, int mid, int r, int[] nums) {
int[] aux = Arrays.copyOfRange(nums, l, r + 1);

int lp =l, rp = mid + 1;

for (int i = lp; i <= r; i ++) {
if (lp > mid) {
nums[i] = aux[rp - l];
rp ++;
} else if (rp > r) {
nums[i] = aux[lp - l];
lp ++;
} else if (aux[lp-l] > aux[rp - l]) {
nums[i] = aux[rp - l];
rp ++;
} else {
nums[i] = aux[lp - l];
lp ++;
}
}
}
}

总结


归并排序是一种十分高效的排序算法,其时间复杂度为O(N*logN)。归并排序的最好,最坏的平均时间复杂度均为O(nlogn),排序后相等的元素的顺序不会改变,所以也是一种稳定的排序算法。归并排序被应用在许多地方,其java中Arrays.sort()采用了一种名为TimSort的排序算法,其就是归并排序的优化版本。


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

Kotlin-apply、also、run、let、区别

apply、also介绍 两者都是T的扩展函数,也就是任何类型对象都调用apply、also; 两者的返回值都是this,也就是函数调用者; apply的闭包使用this来访问函数调用者,also的闭包使用it来访问函数的调用者。 一看看apply、als...
继续阅读 »
apply、also介绍


  • 两者都是T的扩展函数,也就是任何类型对象都调用apply、also;

  • 两者的返回值都是this,也就是函数调用者;

  • apply的闭包使用this来访问函数调用者,also的闭包使用it来访问函数的调用者。


一看看apply、also源码

public inline fun <T> T.apply(block: T.() -> Unit): T {//1
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()//2
return this//返回值为this,也就是apply的调用者
}

public inline fun <T> T.also(block: (T) -> Unit): T {//3
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block(this)//4
return this 返回值为this,也就是also的调用者
}


  • 注释1:apply接受的闭包类型为T.() -> Unit,也就是调用者的扩展函数,例子tv.apply{},闭包{}为tv的扩展函数,所以this可以访问到调用者;

  • 注释2:直接调用闭包,完成apply的逻辑;

  • 注释3:also接受的闭包类型为 (T) -> Unit,也就是任意函数,只要函数入参类型为also调用类型返回为Unit都可以;

  • 注释3:把this作为闭包的参数传入,例子tv.also{},闭包的入参为tv,所以it能访问到tv;

  • apply this可以访问调用者本身,因为闭包是扩展函数,而also用it访问调用者本身,因为调用者是作为参数传入闭包的。


apply、also适用场景

因为返回值为调用者this,所以它们非常适合对同一个对象连续操作的链式调用。
以下代码以apply为例,链式调用对tv进行一系列操作。注意:例子不一定合理,只是想表达相应的意思而已。


    private fun init() {
val tv = TextView(this)
tv.apply {
this.text = "name" //操作1
}.apply {
this.setOnClickListener { //操作2
Log.d("MainActivity", "setOnClickListener")
}
}.apply {
this.gravity = Gravity.CENTER //操作3
}
}

run、let介绍


  • 两者都是T的扩展函数,也就是任何类型对象都调用run、let;

  • 两者的返回值是:最后一行非赋值代码作为闭包的返回值,否则返回Unit;

  • run的闭包使用this来访问函数调用者,let的闭包使用it来访问函数的调用者。


一起看看 run、let源码

public inline fun <T, R> T.run(block: T.() -> R): R {//1
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()//2
}

public inline fun <T, R> T.let(block: (T) -> R): R {//3
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)//4
}


  • 注释1:run接受的闭包类型为T.() -> Unit,也就是调用者的扩展函数,this可以访问到调用者,这点跟apply一样;

  • 注释2:直接调用闭包,将闭包的返回值返回;

  • 注释3:let接受的闭包类型为block: (T) -> Unit,也就是任意函数,只要函数入参类型为also调用者类型返回为Unit都可以;

  • 注释4:直接调用闭包,将this作为参数传入闭包;

  • run this可以访问调用者本身,因为闭包是扩展函数,而let用it访问调用者本身,因为是作为参数传入闭包。


run、let适用场景

它们都可以有返回值,所以非常适合上一个操作返回值作用于下一个操作的链式调用。以下代码以let为例,操作1返回值作用于操作2,操作2返回值作用于操作3。注意:例子不一定合理,只是想表达相应的意思而已。


    private fun init(data: Int): Int {
return data.let {
if (data == 1) it + 1 else it + 2 //操作1
}.let {
if (data == 2) it + 3 else it + 4 //操作2
}.let {
if (data == 3) it + 5 else it + 6 //操作3
}
}

作用函数更重要的作用

确保操作的作用域,以下代码确保tv不为空的情况下执行,保证操作的作用域。


        val tv = TextView(this)
tv?.apply {
text = count.toString()
setOnClickListener {
Log.d("MainActivity", "setOnClickListener")
}
gravity = Gravity.CENTER
}

为什么有的用this访问调用者,有的则用it?

前面分析源码的时候可以看到,



  • apply、run接收的闭包类型为调用者的扩展函数,既然是扩展函数,那么当然是用this来访问调用者;

  • also、let接受的闭包类型为任意类型的函数,只要函数入参类型为调用者类型返回为Unit都可以,既然是参数,那么就能用不能用this来访问,就得用其他字符来访问,定义it来访问也未尝不可;


总结


  • apply、also,闭包的返回值都是this,前者apply接受的闭包类型调用者的扩展函数,后者接受的闭包类型为 入参为调用者类型的函数;

  • also、apply,非常适合对同一个对象连续操作的链式调用;

  • run、let,闭包的返回值为最后一行非赋值代码,前者run接受的闭包类型调用者的扩展函数,后者接受的闭包类型为 入参为调用者类型的函数;

  • run、let,非常适合上一个操作返回值作用于下一个操作的调用;


以上分析有不对的地方,请指出,互相学习,谢谢哦!


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

开箱即用,5 个功能强悍的 JSON 神器!

大家好,我是小 G。自 1999 年开始,JSON 作为用户体验较好的数据交换格式,开始被各界广为采纳,并逐渐应用到 Web 开发及各种 NoSQL 数据库领域。身为程序员,想必大家平日也是跟 JSON 打交道颇多。我近期刚好业务上有需求,得基于 JSON 实...
继续阅读 »

大家好,我是小 G。

自 1999 年开始,JSON 作为用户体验较好的数据交换格式,开始被各界广为采纳,并逐渐应用到 Web 开发及各种 NoSQL 数据库领域。

身为程序员,想必大家平日也是跟 JSON 打交道颇多。我近期刚好业务上有需求,得基于 JSON 实现一些小功能,因此便到 GitHub 了解了一下关于 JSON 的开发者工具。

逛了一圈之后,可谓是收获颇丰。

下面,就挑选几个我认为比较不错的,在日常开发场景中,也会时不时用到的 JSON 工具,给大家做下分享。

JSON 数据可视化

JSON Visio,一个开源的 JSON 数据可视化工具,可通过图表节点,完美呈现 JSON 数据间的结构关系与详情。


GitHub:https://github.com/AykutSarac/jsonvisio.com

凭借这款工具,你可以快速捕捉到 JSON 中的错误信息,搜索节点,并且,还能使用不同布局来展开 JSON 数据,让你可以更直观的看出数据间的关系。

链式操作 JSON

Dasel,一个比较实用的 JSON 命令行工具,可通过类似链式语法的方式,对 JSON、YAML、CSV 等文件进行增删改查、转换等操作。

用作者的原话说,就是当你掌握了 dasel 之后,便可以一劳永逸,在多种数据格式中,无缝切换,快速查找、修改数据。


GitHub:https://github.com/TomWright/dasel

该工具支持多种结构化数据文件,如 JSON、YAML、TOML、XML、CSV 等。

数据检索、查询

DataStation,是一款面向开发者的开源数据 IDE。

简单来说,就是可通过 SQL 语句,快速查询 JSON、CSV、Excel、日志记录库等文件中的数据,并为之创建可视化图表。

DataStation:https://github.com/multiprocessio/datastation

这款 IDE 支持 Linux、macOS、Windows 等主流操作系统,以及 18 种 SQL 和 NoSQL 数据库、文件、HTTP 服务器。

此外,作者还提供了命令行工具:DSQ,除了数据查询外,还支持多个文件合并查询,不同格式的数据源文件转化(比如将 CSV 转为 JSON)等功能。

DSQ:https://github.com/multiprocessio/dsq

在线存储 JSON

之前在 GitHub 热榜,火过一个跟 JSON 有关的开源项目,叫 JSONBox。

它能为开发者提供一个特定链接,通过向这个链接发送 HTTP 请求,可以用来存储、读取、修改 JSON 数据。

整个过程无需其他操作,完全免费,开箱即用,非常便捷。


GitHub:https://github.com/vasanthv/jsonbox

不过,我还是建议你在使用这个工具时,最好是基于自己的服务器来托管数据,这样安全性才比较有保障。

快速生成表单

通过上面几个项目,你应该能大概感知出 JSON 的灵活性与可扩展性有多强了。因此,基于这两大特点,国内有位开发者做了一款在线动态表单生成器:Form Create。

用户只需上传 JSON 数据,即可快速生成表单:


GitHub:https://github.com/xaboy/form-create

生成的表单,可具备动态渲染、数据收集、验证和提交功能等功能。另外还内置了 20 种常用表单组件和自定义组件,再复杂的表单都可以轻松搞定。

文中所提到的所有开源项目,已收录至 GitHubDaily 的开源项目列表中,有需要的,可访问下方 GitHub 地址或点击「阅读原文」查看:

GitHub:https://github.com/GitHubDaily/GitHubDaily

好了,今天的分享到此结束,感谢大家抽空阅读,我们下期再见,Respect!

来源:blog.csdn.net/sinat_33224091/article/details/124263178

收起阅读 »

复盘前端工程师必知的javascript设计模式

前言 设计模式是一个程序员进阶高级的必备技巧,也是评判一个工程师工作经验和能力的试金石.设计模式是程序员多年工作经验的凝练和总结,能更大限度的优化代码以及对已有代码的合理重构.作为一名合格的前端工程师,学习设计模式是对自己工作经验的另一种方式的总结和反思,也是...
继续阅读 »

前言

设计模式是一个程序员进阶高级的必备技巧,也是评判一个工程师工作经验和能力的试金石.设计模式是程序员多年工作经验的凝练和总结,能更大限度的优化代码以及对已有代码的合理重构.作为一名合格的前端工程师,学习设计模式是对自己工作经验的另一种方式的总结和反思,也是开发高质量,高可维护性,可扩展性代码的重要手段.

我们所熟知的金典的几大框架,比如jquery, react, vue内部也大量应用了设计模式, 比如观察者模式, 代理模式, 单例模式等.所以作为一个架构师,设计模式是必须掌握的.

在中高级前端工程师的面试的过程中,面试官也会适当考察求职者对设计模式的了解,所以笔者结合多年的工作经验和学习探索, 总结并画出了针对javascript设计模式的思维导图和实际案例,接下来就来让我们一起来探索习吧.

你将收获

  • 单例模式

  • 构造器模式

  • 建造者模式

  • 代理模式

  • 外观模式

  • 观察者模式

  • 策略模式

  • 迭代器模式

正文

我们先来看看总览.设计模式到底可以给我们带来什么呢?


以上笔者主要总结了几点使用设计模式能给工程带来的好处, 如代码可解耦, 可扩展性,可靠性, 条理性, 可复用性. 接下来来看看我们javascript的第一个设计模式.

1. 单例模式


1.1 概念解读

单例模式: 保证一个类只有一个实例, 一般先判断实例是否存在,如果存在直接返回, 不存在则先创建再返回,这样就可以保证一个类只有一个实例对象.

1.2 作用

  • 模块间通信

  • 保证某个类的对象的唯一性

  • 防止变量污染

1.3 注意事项

  • 正确使用this

  • 闭包容易造成内存泄漏,所以要及时清除不需要的变量

  • 创建一个新对象的成本较高

1.4 实际案例

单例模式广泛应用于不同程序语言中, 在实际软件应用中应用比较多的比如电脑的任务管理器,回收站, 网站的计数器, 多线程的线程池的设计等.

1.5 代码实现

(function(){
// 养鱼游戏
let fish = null
function catchFish() {
  // 如果鱼存在,则直接返回
  if(fish) {
    return fish
  }else {
    // 如果鱼不存在,则获取鱼再返回
    fish = document.querySelector('#cat')
    return {
      fish,
      water: function() {
        let water = this.fish.getAttribute('weight')
        this.fish.setAttribute('weight', ++water)
      }
    }
  }
}

// 每隔3小时喂一次水
setInterval(() => {
  catchFish().water()
}, 3*60*60*1000)
})()

2. 构造器模式


2.1 概念解读

构造器模式: 用于创建特定类型的对象,以便实现业务逻辑和功能的可复用.

2.2 作用

  • 创建特定类型的对象

  • 逻辑和业务的封装

2.3 注意事项

  • 注意划分好业务逻辑的边界

  • 配合单例实现初始化等工作

  • 构造函数命名规范,第一个字母大写

  • new对象的成本,把公用方法放到原型链上

2.4 实际案例

构造器模式我觉得是代码的格局,也是用来考验程序员对业务代码的理解程度.它往往用于实现javascript的工具库,比如lodash等以及javascript框架.

2.5 代码展示

function Tools(){
if(!(this instanceof Tools)){
  return new Tools()
}
this.name = 'js工具库'
// 获取dom的方法
this.getEl = function(elem) {
  return document.querySelector(elem)
}
// 判断是否是数组
this.isArray = function(arr) {
  return Array.isArray(arr)
}
// 其他通用方法...
}

3. 建造者模式


3.1 概念解读

建造者模式: 将一个复杂的逻辑或者功能通过有条理的分工来一步步实现.

3.2 作用

  • 分布创建一个复杂的对象或者实现一个复杂的功能

  • 解耦封装过程, 无需关注具体创建的细节

3.3 注意事项

  • 需要有可靠算法和逻辑的支持

  • 按需暴露一定的接口

3.4 实际案例

建造者模式其实在很多领域也有应用,笔者之前也写过很多js插件,大部分都采用了建造者模式, 可以在笔者github地址徐小夕的github学习参考. 其他案例如下:

  • jquery的ajax的封装

  • jquery插件封装

  • react/vue某一具体组件的设计

3.5 代码展示

笔者就拿之前使用建造者模式实现的一个案例:Canvas入门实战之用javascript面向对象实现一个图形验证码, 那让我们使用建造者模式实现一个非常常见的验证码插件吧!

// canvas绘制图形验证码
(function(){
  function Gcode(el, option) {
      this.el = typeof el === 'string' ? document.querySelector(el) : el;
      this.option = option;
      this.init();
  }
  Gcode.prototype = {
      constructor: Gcode,
      init: function() {
          if(this.el.getContext) {
              isSupportCanvas = true;
              var ctx = this.el.getContext('2d'),
              // 设置画布宽高
              cw = this.el.width = this.option.width || 200,
              ch = this.el.height = this.option.height || 40,
              textLen = this.option.textLen || 4,
              lineNum = this.option.lineNum || 4;
              var text = this.randomText(textLen);
   
              this.onClick(ctx, textLen, lineNum, cw, ch);
              this.drawLine(ctx, lineNum, cw, ch);
              this.drawText(ctx, text, ch);
          }
      },
      onClick: function(ctx, textLen, lineNum, cw, ch) {
          var _ = this;
          this.el.addEventListener('click', function(){
              text = _.randomText(textLen);
              _.drawLine(ctx, lineNum, cw, ch);
              _.drawText(ctx, text, ch);
          }, false)
      },
      // 画干扰线
      drawLine: function(ctx, lineNum, maxW, maxH) {
          ctx.clearRect(0, 0, maxW, maxH);
          for(var i=0; i < lineNum; i++) {
              var dx1 = Math.random()* maxW,
                  dy1 = Math.random()* maxH,
                  dx2 = Math.random()* maxW,
                  dy2 = Math.random()* maxH;
              ctx.strokeStyle = 'rgb(' + 255*Math.random() + ',' + 255*Math.random() + ',' + 255*Math.random() + ')';
              ctx.beginPath();
              ctx.moveTo(dx1, dy1);
              ctx.lineTo(dx2, dy2);
              ctx.stroke();
          }
      },
      // 画文字
      drawText: function(ctx, text, maxH) {
          var len = text.length;
          for(var i=0; i < len; i++) {
              var dx = 30 * Math.random() + 30* i,
                  dy = Math.random()* 5 + maxH/2;
              ctx.fillStyle = 'rgb(' + 255*Math.random() + ',' + 255*Math.random() + ',' + 255*Math.random() + ')';
              ctx.font = '30px Helvetica';
              ctx.textBaseline = 'middle';
              ctx.fillText(text[i], dx, dy);
          }
      },
      // 生成指定个数的随机文字
      randomText: function(len) {
          var source = ['a', 'b', 'c', 'd', 'e',
          'f', 'g', 'h', 'i', 'j',
          'k', 'l', 'm', 'o', 'p',
          'q', 'r', 's', 't', 'u',
          'v', 'w', 'x', 'y', 'z'];
          var result = [];
          var sourceLen = source.length;
          for(var i=0; i< len; i++) {
              var text = this.generateUniqueText(source, result, sourceLen);
              result.push(text)
          }
          return result.join('')
      },
      // 生成唯一文字
      generateUniqueText: function(source, hasList, limit) {
          var text = source[Math.floor(Math.random()*limit)];
          if(hasList.indexOf(text) > -1) {
              return this.generateUniqueText(source, hasList, limit)
          }else {
              return text
          }  
      }
  }
  new Gcode('#canvas_code', {
      lineNum: 6
  })
})();
// 调用
new Gcode('#canvas_code', {
lineNum: 6
})

4. 代理模式


4.1 概念解读

代理模式: 一个对象通过某种代理方式来控制对另一个对象的访问.

4.2 作用

  • 远程代理(一个对象对另一个对象的局部代理)

  • 虚拟代理(对于需要创建开销很大的对象如渲染网页大图时可以先用缩略图代替真图)

  • 安全代理(保护真实对象的访问权限)

  • 缓存代理(一些开销比较大的运算提供暂时的存储,下次运算时,如果传递进来的参数跟之前相同,则可以直接返回前面存储的运算结果)

4.3 注意事项

使用代理会增加代码的复杂度,所以应该有选择的使用代理.

实际案例

我们可以使用代理模式实现如下功能:

  • 通过缓存代理来优化计算性能

  • 图片占位符/骨架屏/预加载等

  • 合并请求/资源

4.4 代码展示

接下来我们通过实现一个计算缓存器来说说代理模式的应用.

// 缓存代理
function sum(a, b){
return a + b
}
let proxySum = (function(){
let cache = {}
return function(){
    let args = Array.prototype.join.call(arguments, ',');
    if(args in cache){
        return cache[args];
    }

    cache[args] = sum.apply(this, arguments)
    return cache[args]
}
})()

5. 外观模式


5.1 概念解读

外观模式(facade): 为子系统中的一组接口提供一个一致的表现,使得子系统更容易使用而不需要关注内部复杂而繁琐的细节.

5.2 作用

  • 对接口和调用者进行了一定的解耦

  • 创造经典的三层结构MVC

  • 在开发阶段减少不同子系统之间的依赖和耦合,方便各个子系统的迭代和扩展

  • 为大型复杂系统提供一个清晰的接口

5.3 注意事项

当外观模式被开发者连续调用时会造成一定的性能损耗,这是由于每次调用都会进行可用性检测

5.4 实际案例

我们可以使用外观模式来设计兼容不同浏览器的事件绑定的方法以及其他需要统一实现接口的方法或者抽象类.

5.5 代码展示

接下来我们通过实现一个兼容不同浏览器的事件监听函数来让大家理解外观模式如何使用.

function on(type, fn){
// 对于支持dom2级事件处理程序
if(document.addEventListener){
    dom.addEventListener(type,fn,false);
}else if(dom.attachEvent){
// 对于IE9一下的ie浏览器
    dom.attachEvent('on'+type,fn);
}else {
    dom['on'+ type] = fn;
}
}

6. 观察者模式


6.1 概念解读

观察者模式: 定义了一种一对多的关系, 所有观察对象同时监听某一主题对象,当主题对象状态发生变化时就会通知所有观察者对象,使得他们能够自动更新自己.

6.2 作用

  • 目标对象与观察者存在一种动态关联,增加了灵活性

  • 支持简单的广播通信, 自动通知所有已经订阅过的对象

  • 目标对象和观察者之间的抽象耦合关系能够单独扩展和重用

6.3 注意事项

观察者模式一般都要注意要先监听, 再触发(特殊情况也可以先发布,后订阅,比如QQ的离线模式)

6.4 实际案例

观察者模式是非常经典的设计模式,主要应用如下:

  • 系统消息通知

  • 网站日志记录

  • 内容订阅功能

  • javascript事件机制

  • react/vue等的观察者

6.5 代码展示

接下来我们我们使用原生javascript实现一个观察者模式:

class Subject {
constructor() {
  this.subs = {}
}

addSub(key, fn) {
  const subArr = this.subs[key]
  if (!subArr) {
    this.subs[key] = []
  }
  this.subs[key].push(fn)
}

trigger(key, message) {
  const subArr = this.subs[key]
  if (!subArr || subArr.length === 0) {
    return false
  }
  for(let i = 0, len = subArr.length; i < len; i++) {
    const fn = subArr[i]
    fn(message)
  }
}

unSub(key, fn) {
  const subArr = this.subs[key]
  if (!subArr) {
    return false
  }
  if (!fn) {
    this.subs[key] = []
  } else {
    for (let i = 0, len = subArr.length; i < len; i++) {
      const _fn = subArr[i]
      if (_fn === fn) {
        subArr.splice(i, 1)
      }
    }
  }
}
}

// 测试
// 订阅
let subA = new Subject()
let A = (message) => {
console.log('订阅者收到信息: ' + message)
}
subA.addSub('A', A)

// 发布
subA.trigger('A', '我是徐小夕')   // A收到信息: --> 我是徐小夕

7. 策略模式


7.1 概念解读

策略模式: 策略模式将不同算法进行合理的分类和单独封装,让不同算法之间可以互相替换而不会影响到算法的使用者.

7.2 作用

  • 实现不同, 作用一致

  • 调用方式相同,降低了使用成本以及不同算法之间的耦合

  • 单独定义算法模型, 方便单元测试

  • 避免大量冗余的代码判断,比如if else等

7.3 实际案例

  • 实现更优雅的表单验证

  • 游戏里的角色计分器

  • 棋牌类游戏的输赢算法

7.4 代码展示

接下来我们实现一个根据不同类型实现求和算法的模式来带大家理解策略模式.

const obj = {
A: (num) => num * 4,
B: (num) => num * 6,
C: (num) => num * 8
}

const getSum =function(type, num) {
return obj[type](num)
}

8. 迭代器模式


8.1 概念解读

迭代器模式: 提供一种方法顺序访问一个聚合对象中的各个元素,使用者并不需要关心该方法的内部表示.

8.2 作用

  • 为遍历不同集合提供统一接口

  • 保护原集合但又提供外部访问内部元素的方式

8.3 实际案例

迭代器模式模式最常见的案例就是数组的遍历方法如forEach, map, reduce.

8.4 代码展示

接下来笔者使用自己封装的一个遍历函数来让大家更加理解迭代器模式的使用,该方法不仅可以遍历数组和字符串,还能遍历对象.lodash里的.forEach(collection, [iteratee=.identity])方法也是采用策略模式的典型应用.

function _each(el, fn = (v, k, el) => {}) {
// 判断数据类型
function checkType(target){
  return Object.prototype.toString.call(target).slice(8,-1)
}

// 数组或者字符串
if(['Array', 'String'].indexOf(checkType(el)) > -1) {
  for(let i=0, len = el.length; i< len; i++) {
    fn(el[i], i, el)
  }
}else if(checkType(el) === 'Object') {
  for(let key in el) {
    fn(el[key], key, el)
  }
}
}

最后

如果想了解本文完整的思维导图, 更多H5游戏, webpacknodegulpcss3javascriptnodeJScanvas数据可视化等前端知识和实战,欢迎在公号《趣谈前端》加入我们一起学习讨论,共同探索前端的边界。

来源:https://mp.weixin.qq.com/s/xTp3jY0IvXiOWBZhZ5H9fQ

收起阅读 »

前端-SSO单点登录方案

一个完整形态的项目和产品,必然绕不开登录,作为一名前端开发工程师,了解单点登录还是非常有必要的。本文就简单分享一下前端所写到的单点登录。什么是单点登录概念一大堆,长话短说。单点登录就是指通过用户的一次性鉴别登陆,其他子项目在需要验证用户信息的时候,无需再做登录...
继续阅读 »

一个完整形态的项目和产品,必然绕不开登录,作为一名前端开发工程师,了解单点登录还是非常有必要的。本文就简单分享一下前端所写到的单点登录。

什么是单点登录

概念一大堆,长话短说。单点登录就是指通过用户的一次性鉴别登陆,其他子项目在需要验证用户信息的时候,无需再做登录操作,自动识别登录。

为什么要选择单点登录

  • [🌰] 举个栗子 目前有一个产品,产品下有三个子项目,如果每个子项目都写一遍登录,那么后面维护的时候,开发人员需要打开三处的登录去修改同样的逻辑,这样会发生一种情况就是在改逻辑的时候,如果有另外一个bug着急修改,再回来的时候发现自己不知道改到哪了。(别问,问就是发生在我身上了!)这仅仅是站在前端开发的角度上,维护起来非常累。

  • [ ✔] 使用案例 单点登录在大型网站里使用得非常频繁,例如,阿里旗下有淘宝、天猫等网站,还有背后的成百上千的子系统,用户一次操作或交易可能涉及到几十个子系统的协作,如果每个子系统都需要用户认证,不仅用户会疯掉,各子系统也会为这种重复认证授权的逻辑搞疯掉。

话不多说直接上图

图片有点抽象,不过让我们清晰认知到单点登录要解决的就是,用户只需要登录一次就可以访问所有相互信任的应用系统。

单点登录的实现方式

单点登录的本质就是在多个应用系统中共享登录状态,所以实现单点登录的关键在于,如何让Token在多个域中共享。

1、同域下的单点登录

一个企业一般情况下只有一个域名,通过二级域名区分不同的系统。

比如我有个域名:clnct.cn,同时有三个业务系统分别为:

cpc.clnct.cn
cmk.clnct.cn
ckn.clnct.cn

我们要做单点登录(SSO),需要一个登录系统,叫做:cuc.clnct.cn。

我们只要在cuc.clnct.cn登录,cpc.clnct.cn、cmk.clnct.cn、ckn.clnct.cn也登录了。

实现方式:其实这里就是利用了 二级域名 写 一级域名的 Cookie 。cuc.clnct.cn登录以后,可以将Cookie的域设置为顶域,即.clnct,这样所有子域的系统都可以访问到顶域的Cookie。

此种实现方式比较简单,但不支持跨主域名,局限性限于一级域名是一样的。

2、不同域下的单点登录

同域下的单点登录是巧用了Cookie顶域的特性,如果是不同域呢,比如:下面三个是不同域的

cpc.dun.cn
cmk.qun.cn
ckn.nun.cn

实现方式:我们可以部署一个SSO认证中心,认证中心就是一个专门负责处理登录请求。


所有的请求(登录、退出、获取用户信息、当前用户状态)都请求sso系统,sso系统维护用户信息。

此种实现方式相对复杂,支持跨域,扩展性好,是单点登录的标准做法。

逻辑分析

  • 输入用户名密码,登陆成功,接口返回token

有token,调取换code的接口。 1、接口如果获取的code值为空,清除本地的cookies,再登录;2、如果code有值,将url中的redirectUrl后拼接接口拿到的code 重定向到想要去的页面。

  • 判断域名

这里需用用到
document.domain 获取浏览器的域名

获取到浏览器域名后,匹配当前获取的域名是同域还是来自第三方系统。

如果是同域名,直接将redirectUrl返回,无需携带code接口返回的值。
如果是第三方系统,需要处理redirectUrl。因为可能用户会做登录成功再退出,那么带到登录系统的redirectUrl就会携带code值,我们需要通过js的方法去替换原来的code值。
  • 成功返回redirectUrl

一切流程通了之后,通过 location.replace(redirectUrl) 浏览器跳转返回到重定向页面

目标达成: 子系统在未登录的情况下,点击【登录】按钮,跳转到统一用户中心。统一用户中心判断当前cookies是否有token存在,如果不存在--登录;如果存在就去校验token的合法性(调取code接口),调取code接口成功,重定向到原页面。那么同域下所有的子系统,都无需登录。第三方系统进来的时候,因为做了domian的校验,因此登录成功之后,将code码放在redirectUrl,重定向到第三方系统。

总结一下

这虽然并不是最规范的SSO单点登录。但事实上比起一搜一堆概念性的文章,我认为这仅此是我个人的一种做法,至于逻辑对与错,希望大家给出合理的意见和建议,互相学习。


作者:上班摸鱼看日记
来源:https://juejin.cn/post/7088978055737114638

收起阅读 »

仿海报工厂效果的自定义View

之前做了一个自定义View,效果有些类似海报工厂,当做自定义View的入门学习吧~先看下效果图: 就是一个背景图,中间挖了若干个形状不同的“洞”,每个“洞”里放着一张图片,用手可以拖拽、缩放、旋转该图片,并且当前图片备操作时会有红色的高亮边框。点击选中某个图...
继续阅读 »

之前做了一个自定义View,效果有些类似海报工厂,当做自定义View的入门学习吧~先看下效果图:


这里写图片描述


就是一个背景图,中间挖了若干个形状不同的“洞”,每个“洞”里放着一张图片,用手可以拖拽、缩放、旋转该图片,并且当前图片备操作时会有红色的高亮边框。点击选中某个图片的时候,底部会弹出菜单栏,菜单栏有三个按钮,分别是对该图片进行旋转90度、对称翻转图片、和保存整个海报到手机内置sd卡根目录。


这就类似海报工厂效果,选择若干张图片还有底部模板(就是背景图片和挖空部分的位置和形状),然后通过触摸改变选择的图片的大小位置角度,来制作一张自己喜爱的海报。


这里主要是一个自定义View,项目中叫做JigsawView完成的。它的基本结构是最底层绘制可操作的图片,第二层绘制背景图片,第三层绘制镂空的部分,镂空部分通过PorterDuffXfermode来实现,镂空部分的形状由对应手机目录的svg文件确定。


在用Android中的Canvas进行绘图时,可以通过使用PorterDuffXfermode将所绘制的图形的像素与Canvas中对应位置的像素按照一定规则进行混合,形成新的像素值,从而更新Canvas中最终的像素颜色值,这样会创建很多有趣的效果。关于PorterDuffXfermode详细可以参考
Android中Canvas绘图之PorterDuffXfermode使用及工作原理详解


首先这里要关掉硬件加速,因为硬件加速可能会使效果丢失。在View的初始化语句中调用


setLayerType(View.LAYER_TYPE_SOFTWARE, null);

即可。


由于JigsawView的代码不少,所以这里只展示比较重要的部分,完整代码请见文章末尾的GitHub链接。


首先需要两支画笔:


 //绘制图片的画笔
Paint mMaimPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
//绘制高亮边框的画笔
Paint mSelectPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

这里图片的模型是PictureModel 。PictureModel 主要都是包含了位置和缩放信息以及镂空部分的HollowModel,而图片的具体位置和大小由HollowModel确定,图片时填充镂空部分的,以类似ImageView的CenterCrop方式填充,这个在JigsawView的makePicFillHollow方法中处理。


HollowModel持有解析svg文件得到的path对象集合,该集合可以表示一个svg文件表示的路径。具体的解析工作由自定义的SvgParseUtil类处理,SvgParseUtil从手机的内置sd卡中(当然路径可以灵活配置)读取对应的svg文件,然后解析为可以绘制的Path集合对象。SvgParseUtil本质是解析xml文件(以为svg就是一个xml文件),对于svg路径直接拷贝系统的PathParser处理,其他的圆形矩形多边形就自己处理。这里具体代码这里就不展示了,详细请看GitHub上的源码。


以下是完整的onDraw方法:


 @Override
protected void onDraw(Canvas canvas) {
if (mPictureModels != null && mPictureModels.size() > 0 && mBitmapBackGround != null) {
//循环遍历画要处理的图片
for (PictureModel pictureModel : mPictureModels) {
Bitmap bitmapPicture = pictureModel.getBitmapPicture();
int pictureX = pictureModel.getPictureX();
int pictureY = pictureModel.getPictureY();
float scaleX = pictureModel.getScaleX();
float scaleY = pictureModel.getScaleY();
float rotateDelta = pictureModel.getRotate();

HollowModel hollowModel = pictureModel.getHollowModel();
ArrayList<Path> paths = hollowModel.getPathList();
if (paths != null && paths.size() > 0) {
for (Path tempPath : paths) {
mPath.addPath(tempPath);
}
drawPicture(canvas, bitmapPicture, pictureX, pictureY, scaleX, scaleY, rotateDelta, hollowModel, mPath);
} else {
drawPicture(canvas, bitmapPicture, pictureX, pictureY, scaleX, scaleY, rotateDelta, hollowModel, null);
}
}
//新建一个layer,新建的layer放置在canvas默认layer的上部,当我们执行了canvas.saveLayer()之后,我们所有的绘制操作都绘制到了我们新建的layer上,而不是canvas默认的layer。
int layerId = canvas.saveLayer(0, 0, canvas.getWidth(), canvas.getHeight(), null, Canvas.ALL_SAVE_FLAG);

drawBackGround(canvas);

//循环遍历画镂空部分
for (PictureModel pictureModel : mPictureModels) {
int hollowX = pictureModel.getHollowModel().getHollowX();
int hollowY = pictureModel.getHollowModel().getHollowY();
int hollowWidth = pictureModel.getHollowModel().getWidth();
int hollowHeight = pictureModel.getHollowModel().getHeight();
ArrayList<Path> paths = pictureModel.getHollowModel().getPathList();
if (paths != null && paths.size() > 0) {
for (Path tempPath : paths) {
mPath.addPath(tempPath);
}
drawHollow(canvas, hollowX, hollowY, hollowWidth, hollowHeight, mPath);
mPath.reset();
} else {
drawHollow(canvas, hollowX, hollowY, hollowWidth, hollowHeight, null);
}
}

//把这个layer绘制到canvas默认的layer上去
canvas.restoreToCount(layerId);

//绘制选择图片高亮边框
for (PictureModel pictureModel : mPictureModels) {
if (pictureModel.isSelect() && mIsNeedHighlight) {
canvas.drawRect(getSelectRect(pictureModel), mSelectPaint);
}
}
}
}

思路还是比较清晰的。第3行到第22行为绘制可操作图片。第19行的drawPicture就是绘制所有的可操作图片,而当该图片对应的镂空部分没有相应的svg时,就绘制HollowModel的位置尺寸对应的矩形作为镂空部分,即20行的drawPicture。


看下drawPicture方法:


private void drawPicture(Canvas canvas, Bitmap bitmapPicture, int coordinateX, int coordinateY, float scaleX, float scaleY, float rotateDelta
, HollowModel hollowModel, Path path) {
int picCenterWidth = bitmapPicture.getWidth() / 2;
int picCenterHeight = bitmapPicture.getHeight() / 2;
mMatrix.postTranslate(coordinateX, coordinateY);
mMatrix.postScale(scaleX, scaleY, coordinateX + picCenterWidth, coordinateY + picCenterHeight);
mMatrix.postRotate(rotateDelta, coordinateX + picCenterWidth, coordinateY + picCenterHeight);
canvas.save();

//以下是对应镂空部分相交的处理,需要完善
if (path != null) {
Matrix matrix1 = new Matrix();
RectF rect = new RectF();
path.computeBounds(rect, true);

int width = (int) rect.width();
int height = (int) rect.height();

float hollowScaleX = hollowModel.getWidth() / (float) width;
float hollowScaleY = hollowModel.getHeight() / (float) height;

matrix1.postScale(hollowScaleX, hollowScaleY);
path.transform(matrix1);
//平移path
path.offset(hollowModel.getHollowX(), hollowModel.getHollowY());
//让图片只能绘制在镂空内部,防止滑动到另一个拼图的区域中
canvas.clipPath(path);
path.reset();
} else {
int hollowX = hollowModel.getHollowX();
int hollowY = hollowModel.getHollowY();
int hollowWidth = hollowModel.getWidth();
int hollowHeight = hollowModel.getHeight();
//让图片只能绘制在镂空内部,防止滑动到另一个拼图的区域中
canvas.clipRect(hollowX, hollowY, hollowX + hollowWidth, hollowY + hollowHeight);
}
canvas.drawBitmap(bitmapPicture, mMatrix, null);
canvas.restore();
mMatrix.reset();
}

这里主要是运用了Matrix处理图片的各种变化。在onTouchEvent方法中,会根据触摸的事件不同对正在操作的PictureModel对象的位置、缩放、角度进行对应的赋值,所以在drawPicture中将每次触摸后的赋值参数取出来,交给Matrix对象处理,最后通过


canvas.drawBitmap(bitmapPicture, mMatrix, null);

就能将触摸后的变化图片显示出来。
另外第26行的canvas.clipPath(path);是将图片的可绘制区域限定在镂空部分中,防止图片滑动到其他的镂空区域。


注意onDraw的第25行的


int layerId = canvas.saveLayer(0, 0, canvas.getWidth(), canvas.getHeight(), null, Canvas.ALL_SAVE_FLAG);

为了正确显示PorterDuffXfermode所带来的的效果,需要新建一个图层,具体可以参见上面链接引用的博文。


onDraw第26行的drawBackGround方法就是绘制背景,这个很简单就不必说了。


第28行到第44行为绘制镂空部分,主要是先把HollowModel中存储的Path集合取出,再通过addPath方法将路径数据交给mPath对象,再由drawHollow方法去真正绘制镂空部分。


private void drawHollow(Canvas canvas, int hollowX, int hollowY, int hollowWidth, int hollowHeight, Path path) {
mMaimPaint.setXfermode(mPorterDuffXfermodeClear);
//画镂空
if (path != null) {
canvas.save();
canvas.translate(hollowX, hollowY);
//缩放镂空部分大小使得镂空部分填充HollowModel对应的矩形区域
scalePathRegion(canvas, hollowWidth, hollowHeight, path);
canvas.drawPath(path, mMaimPaint);
canvas.restore();
mMaimPaint.setXfermode(null);
} else {
Rect rect = new Rect(hollowX, hollowY, hollowX + hollowWidth, hollowY + hollowHeight);
canvas.save();
canvas.drawRect(rect, mMaimPaint);
canvas.restore();
mMaimPaint.setXfermode(null);
}
}

这里首先对设置画笔的PorterDuffXfermode:


mMaimPaint.setXfermode(mPorterDuffXfermodeClear);

这里为了镂空效果,PorterDuffXfermode使用PorterDuff.Mode.CLEAR。


然后对画布进行平移,然后通过scalePathRegion方法让表示镂空路径的Path对象进行缩放,使得镂空的路径填充HollowModel对应的矩形区域。接着使用


canvas.drawRect(rect, mMaimPaint);

将镂空的路径绘制上去。


最后别忘了


canvas.restore();
mMaimPaint.setXfermode(null);

恢复画布和画笔的状态。


然后onDraw的第47行把这个layer绘制到canvas默认的layer上去:


 canvas.restoreToCount(layerId);

onDraw最后的


 //绘制选择图片高亮边框
for (PictureModel pictureModel : mPictureModels) {
if (pictureModel.isSelect() && mIsNeedHighlight) {
canvas.drawRect(getSelectRect(pictureModel), mSelectPaint);
}
}

在onTouchEvent中,将通过触摸事件判断哪个图片当前被选择,然后在onDraw中让当前被选择的图片绘制对应的HollowModel的边框。


onDraw到此结束。


再看下onTouchEvent方法:


@Override
public boolean onTouchEvent(MotionEvent event) {
if (mPictureModels == null || mPictureModels.size() == 0) {
return true;
}
switch (event.getActionMasked()) {
case MotionEvent.ACTION_POINTER_DOWN:

//双指模式
if (event.getPointerCount() == 2) {
//mPicModelTouch为当前触摸到的操作图片模型
mPicModelTouch = getHandlePicModel(event);
if (mPicModelTouch != null) {
// mPicModelTouch.setSelect(true);
//重置图片的选中状态
resetNoTouchPicsState();
mPicModelTouch.setSelect(true);
//两手指的距离
mLastFingerDistance = distanceBetweenFingers(event);
//两手指间的角度
mLastDegree = rotation(event);
mIsDoubleFinger = true;
invalidate();
}
}
break;

//单指模式
case MotionEvent.ACTION_DOWN:
//记录上一次事件的位置
mLastX = event.getX();
mLastY = event.getY();
//记录Down事件的位置
mDownX = event.getX();
mDownY = event.getY();
//获取被点击的图片模型
mPicModelTouch = getHandlePicModel(event);
if (mPicModelTouch != null) {
//每次down重置其他picture选中状态
resetNoTouchPicsState();
mPicModelTouch.setSelect(true);
invalidate();
}
break;
case MotionEvent.ACTION_MOVE:
switch (event.getPointerCount()) {
//单指模式
case 1:
if (!mIsDoubleFinger) {
if (mPicModelTouch != null) {
//记录每次事件在x,y方向上移动
int dx = (int) (event.getX() - mLastX);
int dy = (int) (event.getY() - mLastY);
int tempX = mPicModelTouch.getPictureX() + dx;
int tempY = mPicModelTouch.getPictureY() + dy;

if (checkPictureLocation(mPicModelTouch, tempX, tempY)) {
//检查到没有越出镂空部分才真正赋值给mPicModelTouch
mPicModelTouch.setPictureX(tempX);
mPicModelTouch.setPictureY(tempY);
//保存上一次的位置,以便下次事件算出相对位移
mLastX = event.getX();
mLastY = event.getY();
//修改了mPicModelTouch的位置后刷新View
invalidate();
}
}
}
break;

//双指模式
case 2:
if (mPicModelTouch != null) {
//算出两根手指的距离
double fingerDistance = distanceBetweenFingers(event);
//当前的旋转角度
double currentDegree = rotation(event);
//当前手指距离和上一次的手指距离的比即为图片缩放比
float scaleRatioDelta = (float) (fingerDistance / mLastFingerDistance);
float rotateDelta = (float) (currentDegree - mLastDegree);

float tempScaleX = scaleRatioDelta * mPicModelTouch.getScaleX();
float tempScaleY = scaleRatioDelta * mPicModelTouch.getScaleY();
//对缩放比做限制
if (Math.abs(tempScaleX) < 3 && Math.abs(tempScaleX) > 0.3 &&
Math.abs(tempScaleY) < 3 && Math.abs(tempScaleY) > 0.3) {
//没有超出缩放比才真正赋值给模型
mPicModelTouch.setScaleX(tempScaleX);
mPicModelTouch.setScaleY(tempScaleY);
mPicModelTouch.setRotate(mPicModelTouch.getRotate() + rotateDelta);
//修改模型之后,刷新View
invalidate();
//记录上一次的两手指距离以便下次计算出相对的位置以算出缩放系数
mLastFingerDistance = fingerDistance;
}
//记录上次的角度以便下一个事件计算出角度变化值
mLastDegree = currentDegree;
}
break;
}
break;
//两手指都离开屏幕
case MotionEvent.ACTION_UP:
// for (PictureModel pictureModel : mPictureModels) {
// pictureModel.setSelect(false);
// }
mIsDoubleFinger = false;
double distance = getDisBetweenPoints(event);

if (mPicModelTouch != null) {
//是否属于滑动,非滑动则改变选中状态
if (distance < ViewConfiguration.getTouchSlop()) {
if (mPicModelTouch.isLastSelect()) {
mPicModelTouch.setSelect(false);
mPicModelTouch.setLastSelect(false);
if (mPictureCancelSelectListner != null) {
mPictureCancelSelectListner.onPictureCancelSelect();
}

} else {
mPicModelTouch.setSelect(true);
mPicModelTouch.setLastSelect(true);
//选中的回调
if (mPictureSelectListener != null) {
mPictureSelectListener.onPictureSelect(mPicModelTouch);
}
}
invalidate();
} else {
//滑动则取消所有选择的状态
mPicModelTouch.setSelect(false);
mPicModelTouch.setLastSelect(false);
//取消状态之后刷新View
invalidate();
}
} else {
//如果没有图片被选中,则取消所有图片的选中状态
for (PictureModel pictureModel : mPictureModels) {
pictureModel.setLastSelect(false);
}
//没有拼图被选中的回调
if (mPictureNoSelectListener != null) {
mPictureNoSelectListener.onPictureNoSelect();
}
//取消所有图片选中状态后刷新View
invalidate();
}
break;
//双指模式中其中一手指离开屏幕,取消当前被选中图片的选中状态
case MotionEvent.ACTION_POINTER_UP:
if (mPicModelTouch != null) {
mPicModelTouch.setSelect(false);
invalidate();
}
}
return true;
}

虽然比较长,但是并不难理解,基本是比较套路化的东西,看注释应该就能懂。


总的流程就是:
首先在Down事件:
不管单手还是双手模式,都将选择当前点击到的图片模型,这也是为了以后的事件中可以修改选中的图片模型以在onDraw中改变图片的显示。


Move事件中:
单手模式的话,针对每个MOVE事件带来的位移给PictureModel的位置赋值,然后就调用invalidate进行刷新界面。


如果是双手模式,则根据每个MOVE事件带来的角度变化和两个手指间的距离变化分别给PictureModel的角度和缩放比赋值,然后调用invalidate进行刷新界面。


Up事件:
单指模式下,先判断是否已经滑动过(滑动距离小于ViewConfiguration.getTouchSlop()就认为不是滑动而是点击),不是滑动的话就以改变当前的图片选中状态处理,切换选中状态。
是滑动过的话则取消所有图片的选中状态。


双指状态下均取消所有图片的选中状态。


这里为了使得缩放旋转体验更好,所以只要手指DOWN事件落在镂空部分中,在没有Up事件的情况下即使滑出镂空部分仍然可以继续对选中的图片进行操作,避免因为镂空部分小带来的操作不便,这也和海报工厂的效果一致。


源码地址:github.com/yanyinan/Ji…


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

Flutter制作一个吃豆人加载Loading

知识点:绘制、动画、多状态监听 国际惯例,先看效果图: 具体效果就是吃豆人会根据吃不同颜色的豆子改变身体的颜色。 1、绘制静态吃豆人、豆豆、眼睛 首先,我们需要将这个静态的吃豆人绘制出来,我们可以把吃豆人看做是一个实心圆弧,豆豆和眼睛就是一个圆。 关键...
继续阅读 »
  • 知识点:绘制、动画、多状态监听


国际惯例,先看效果图:


fcb99a9b-0c08-43b8-ad80-e6f058cd6d58.gif



  • 具体效果就是吃豆人会根据吃不同颜色的豆子改变身体的颜色。


1、绘制静态吃豆人、豆豆、眼睛


首先,我们需要将这个静态的吃豆人绘制出来,我们可以把吃豆人看做是一个实心圆弧,豆豆和眼睛就是一个圆。

关键代码:


//画头
_paint
..color = color.value
..style = PaintingStyle.fill;
var rect = Rect.fromCenter(
center: Offset(0, 0), width: size.width, height: size.height);
/// 起始角度
var a = 40 / 180 * pi;
// 绘制圆弧
canvas.drawArc(rect, 0, 2 * pi - a * 2, true, _paint);

// 画豆豆
canvas.drawOval(
Rect.fromCenter(
center: Offset(
size.width / 2 +
ddSize -
angle2.value * (size.width / 2 + ddSize),
0),
width: ddSize,
height: ddSize),
_paint..color = color2.value);

//画眼睛
canvas.drawOval(
Rect.fromCenter(
center: Offset(0, -size.height / 3), width: 8, height: 8),
_paint..color = Colors.black87);

动画属性: 嘴巴的张合:通过圆弧的角度不断改变实现,豆豆移动:从头的右侧源源不断的有豆子向左移动,改变豆豆x轴的坐标即可,接下来我们让吃豆人动起来吧。


2、加入动画属性


这里我们需要创建2个动画控制器,一个控制头,一个控制豆豆,我们看到因为头部一开一合属于动画正向执行一次然后再反向执行一次,相当于执行了两次,豆豆的从右边到嘴巴只执行了一次,所以头的执行时间是豆豆执行时间的两倍,嘴巴一张一合才能吃豆子嘛,吃豆完毕,将豆子颜色赋值给头改变颜色,豆子随机获取另一个颜色,不断的吃豆。 这里的绘制状态有多种情况,嘴巴的张合、豆子的平移、颜色的改变都需要进行重新绘制,这里我们可以使用 Listenable.merge方法来进行监听,接受一个Listenable数组,可以将我们需要改变的状态放到这个数组里,返回一个 Listenable赋值给CustomPainter构造函数repaint属性即可,然后在监听只需判断这个Listenable即可。


factory Listenable.merge(List<Listenable?> listenables) = _MergingListenable;
复制代码

关键代码: 动画执行相关。


late Animation<double> animation; // 吃豆人
late Animation<double> animation2; // 豆豆
late AnimationController _controller = AnimationController(
vsync: this, duration: Duration(milliseconds: 500)); //1s
late AnimationController _controller2 = AnimationController(
vsync: this, duration: Duration(milliseconds: 1000)); //2s

//初始化吃豆人、豆豆颜色
ValueNotifier<Color> _color = ValueNotifier<Color>(Colors.yellow.shade800);
ValueNotifier<Color> _color2 =
ValueNotifier<Color>(Colors.redAccent.shade400);

// 动画轨迹
late CurvedAnimation cure = CurvedAnimation(
parent: _controller, curve: Curves.easeIn); // 动画运行的速度轨迹 速度的变化

@override
void initState() {
super.initState();
animation = Tween(begin: 0.2, end: 1.0).animate(_controller)
..addStatusListener((status) {
// dismissed 动画在起始点停止
// forward 动画正在正向执行
// reverse 动画正在反向执行
// completed 动画在终点停止
if (status == AnimationStatus.completed) {
_controller.reverse(); //反向执行 100-0
} else if (status == AnimationStatus.dismissed) {
_color.value = _color2.value;
// 获取一个随机彩虹色
_color2.value = getRandomColor();
_controller.forward(); //正向执行 0-100
// 豆子已经被吃了 从新加载豆子动画
_controller2.forward(from: 0); //正向执行 0-100
}
});
animation2 = Tween(begin: 0.2, end: 1.0).animate(_controller2);
// 启动动画 正向执行
_controller.forward();
// 启动动画 0-1循环执行
_controller2.forward();
// 这里这样重复调用会导致两次动画执行时间不一致 时间长了就不对应了
// _controller2.repeat();
}

@override
void dispose() {
_controller.dispose();
_controller2.dispose();

super.dispose();
}

@override
Widget build(BuildContext context) {
return Center(
child: CustomPaint(
size: Size(50, 50),
painter: Pain2Painter(
_color,
_color2,
animation,
animation2,
Listenable.merge([
animation,
animation2,
_color,
]),
ddSize: 8),
));
}

// 获取一个随机颜色
Color getRandomColor() {
Random random = Random.secure();
int randomInt = random.nextInt(6);
var colors = <Color>[
Colors.red,
Colors.orange,
Colors.yellow,
Colors.green,
Colors.blue,
Colors.indigo,
Colors.purple,
];
Color color = colors[randomInt];
while (color == _color2.value) {
// 重复再选一个
color = colors[random.nextInt(6)];
}
return color;
}

绘制吃豆人源码:


class Pain2Painter extends CustomPainter {
final ValueNotifier<Color> color; // 吃豆人的颜色
final ValueNotifier<Color> color2; // 豆子的的颜色
final Animation<double> angle; // 吃豆人
final Animation<double> angle2; // 豆
final double ddSize; // 豆豆大小
final Listenable listenable;

Pain2Painter(
this.color, this.color2, this.angle, this.angle2, this.listenable,
{this.ddSize = 6})
: super(repaint: listenable);
Paint _paint = Paint();

@override
void paint(Canvas canvas, Size size) {
canvas.clipRect(Offset.zero & size);
canvas.translate(size.width / 2, size.height / 2);
// 画豆豆
canvas.drawOval(
Rect.fromCenter(
center: Offset(
size.width / 2 +
ddSize -
angle2.value * (size.width / 2 + ddSize),
0),
width: ddSize,
height: ddSize),
_paint..color = color2.value);
//画头
_paint
..color = color.value
..style = PaintingStyle.fill;

var rect = Rect.fromCenter(
center: Offset(0, 0), width: size.width, height: size.height);

/// 起始角度
/// angle.value 动画控制器的值 0.2~1 0是完全闭合就是 起始0~360° 1是完全张开 起始 40°~ 280° 顺时针
var a = angle.value * 40 / 180 * pi;
// 绘制圆弧
canvas.drawArc(rect, a, 2 * pi - a * 2, true, _paint);
//画眼睛
canvas.drawOval(
Rect.fromCenter(
center: Offset(0, -size.height / 3), width: 8, height: 8),
_paint..color = Colors.black87);
canvas.drawOval(
Rect.fromCenter(
center: Offset(-1.5, -size.height / 3 - 1.5), width: 3, height: 3),
_paint..color = Colors.white);
}

@override
bool shouldRepaint(covariant Pain2Painter oldDelegate) {
return oldDelegate.listenable != listenable;
}
}

至此,一个简单的吃豆人加载Loading就完成啦。再也不要到处都是菊花转的样式了。。。


总结


通过这个加载Loading动画可以重新复习下Flutter中绘制、动画的使用的联动使用、还有多状态重绘机制,通过动画还可以改变吃豆的速度和吃豆的时间运动轨迹,有兴趣可以试试哦,希望这篇文章对你有所帮助,喜欢的话点个赞再走呗~


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

【Flutter 组件集录】Autocomplete 自动填充

简单来说,Autocomplete 意为 自动填充 。其作用就是在输入时,进行 关键字联想。在输入框下方展示列表,如下所示:注意,这是目前 Flutter 框架内部的组件,非三方组件。目前已收录入 FlutterUnit ,下面效果的源码详见之,大家可以更新查...
继续阅读 »

简单来说,Autocomplete 意为 自动填充 。其作用就是在输入时,进行 关键字联想。在输入框下方展示列表,如下所示:注意,这是目前 Flutter 框架内部的组件,非三方组件。目前已收录入 FlutterUnit ,下面效果的源码详见之,大家可以更新查看体验:















FlutterUnit 中输入时联想效果

下面是动态搜索的效果展示:





1. Autocomplete 组件最简代码


我们先一步步来了解 Autocomplete 组件,先实现如下的最简代码:



使用 Autocomplete 时,必须提供的是 optionsBuilder 参数,另外可以通过 onSelected 回调来监听选中的条目。



Autocomplete<String>(
optionsBuilder: buildOptions,
onSelected: onSelected,
)

optionsBuilder 是一个 AutocompleteOptionsBuilder<T> 类型的函数,从下面的定义中可以发现,该函数会回调 TextEditingValue 对象,且返回 FutureOr<Iterable<T>> 。这说明这个函数是一个异步函数,我们可以在此进行网络请求,数据库查询等工作,来返回一个 Iterable<T> 的可迭代对象。


用脚指头想一下也知道,这个可迭代对象,就决定这输入框下面的联想词是哪些。


final AutocompleteOptionsBuilder<T> optionsBuilder;

typedef AutocompleteOptionsBuilder<T extends Object> =
FutureOr<Iterable<T>> Function(TextEditingValue textEditingValue);



比如下面通过 searchByArgs 模拟网络请求,通过 args 参数搜索数据,


Future<Iterable<String>> searchByArgs(String args) async{
// 模拟网络请求
await Future.delayed(const Duration(milliseconds: 200));
const List<String> data = [
'toly', 'toly49', 'toly42', 'toly56',
'card', 'ls', 'alex', 'fan sha',
];
return data.where((String name) => name.contains(args));
}



这样,buildOptions 的逻辑如下,这就完成了 输入--> 搜索 --> 展示联想词 的流程。这也是 Autocomplete 组件最简单的使用。


Future<Iterable<String>> buildOptions( TextEditingValue textEditingValue ) async {
if (textEditingValue.text == '') {
return const Iterable<String>.empty();
}
return searchByArgs(textEditingValue.text);
}



2. 自定义 Autocomplete 组件内容


其实上面那样的默认样式很丑,而且没有提供 直接 的属性设置样式。所以了解如何自定义是非常关键的,否则只是一个玩具罢了。如下,我们先来实现搜索高亮显示的自定义,其中也包括对输入框的自定义。





Autocomplete 中提供了 fieldViewBuilderoptionsViewBuilder 分别用于构造输入框浮层面板



如下,代码中通过 _buildOptionsView_buildFieldView 进行相应组件构造:


Autocomplete<String>(
optionsBuilder: buildOptions,
onSelected: onSelected,
optionsViewBuilder: _buildOptionsView,
fieldViewBuilder: _buildFieldView,
);



如下是 _buildOptionsView 方法的实现,其中会回调 onSelected 回调函数,和 options 数据,我们需要做的就是依靠数据,构建组件进行展示即可。另外,默认浮层面板和输入框底部平齐,可以通过 Padding 进行下移。另外,由于是浮层,展示文字时,上面需要嵌套 Material 组件。



至于高亮某个关键字,下面是我封装的一个小方法,拿来即用。


---->[高亮某些文字]----
final TextStyle lightTextStyle = const TextStyle(
color: Colors.blue,
fontWeight: FontWeight.bold,
);
InlineSpan formSpan(String src, String pattern) {
List<TextSpan> span = [];
List<String> parts = src.split(pattern);
if (parts.length > 1) {
for (int i = 0; i < parts.length; i++) {
span.add(TextSpan(text: parts[i]));
if (i != parts.length - 1) {
span.add(TextSpan(text: pattern, style: lightTextStyle));
}
}
} else {
span.add(TextSpan(text: src));
}
return TextSpan(children: span);
}



另外,对于输入框的构建,通过如下的 _buildFieldView 实现,其中有 _controller 记录一下 TextEditingController,是因为 optionsViewBuilder 回调中并没有回调输入的 arg 字符,所以想要输入的关键字高亮,只能出此下策。这样,在 TextFormField 构建时,你可以指定自己需要的装饰。



到此,我们就实现了上面,输入过程中,浮层面板内容关键字高亮显示的效果。




3.关于 Autocomplete 中的泛型


泛型的作用非常明显,它最主要的是对浮层面板的构建,如果浮层中的条目不止是 String ,我们就需要使用泛型,来提供某个的数据类型。比如下面的效果,其中浮层面板的条目是可以显示更多的信息:



先定义一个数据类 User ,记录信息:


class User {
final String name;
final bool man;
final String image;

const User(this.name, this.man, this.image);

@override
String toString() {
return 'User{name: $name, man: $man, image: $image}';
}
}



然后在 Autocomplete 的泛型中使用 User 即可。



这样在 _buildOptionsView 中,回调的就是 User 的可迭代对象。如下。封装一个 _UserItem 组件,对条目进行显示。





4、Autocomplete 源码简看


Autocomplete 本质上依赖于 RawAutocomplete 组件进行构建,可见它是一层简单的封装,简化使用。为我们提供了默认的 optionsViewBuilderfieldViewBuilder ,显示一个很丑的界面。也就是说,如果你了解如何定制这两部分内容,你也就会了 RawAutocomplete 组件。





我们先看一下 AutocompleteoptionsViewBuilder 提供的默认显示,其返回的是 _AutocompleteOptions 组件。如下,其实和我们自己实现的也没有太大的区别,只是个默认存在,方便使用的小玩意而已。





另外,对于输入框的构建,使用 _defaultFieldViewBuilder 静态方法完成。



该方法,返回 _AutocompleteField 组件,本质上也就是构建了一个 TextFormField 组件。









Autocomplete 来说,只是 RawAutocomplete 套了个马甲,本质上的功能还是在 RawAutocomplete 的状态类中完成的。如下是 _RawAutocompleteState 的部分代码,可以看出这里的浮层面板,是通过 Overlay 实现的,另外通过 CompositedTransformTargetCompositedTransformFollower 对浮层进行定位。



那本文就这样,如果想要简单地实现搜索联想词,Autocomplete 是一个很不错的选择。


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

Flutter 应用程序创建一个扩展面板列表

正文 了解如何在您的 Flutter 应用程序创建一个扩展面板列表 在本文中,我们将探讨 ExpansionPanelList In Flutter. 。我们将实施一个扩展面板列表演示程序,并学习如何自定义其风格与不同的属性在您的 Flutter 应用程序。...
继续阅读 »

正文


了解如何在您的 Flutter 应用程序创建一个扩展面板列表



在本文中,我们将探讨 ExpansionPanelList In Flutter. 。我们将实施一个扩展面板列表演示程序,并学习如何自定义其风格与不同的属性在您的 Flutter 应用程序。


Expansion Panel List:


它是一个类似于 listView 的实质性 Flutter 小部件。它可以只有扩展面板作为儿童。在某些情况下,我们可能需要显示一个列表,其中子元素可以展开/折叠以显示/隐藏一些详细的数据。为了显示这样的列表 flutter,提供了一个名为 ExapansionPanelList 的小部件。


在这个列表中,每个子元素都是 expsionpanel 小部件。在这个列表中,我们不能使用不同的窗口小部件作为子窗口。我们可以借助于 expsioncallback 属性来处理事物的状态调整(扩展或崩溃)。



演示模块:




这个演示视频显示了如何在一个 Flutter 扩展面板清单。它显示如何扩展面板列表将工作在您的 Flutter 应用程序。它显示了一个列表,在这个列表中孩子们可以展开/折叠以显示/隐藏一些详细信息。它会显示在你的设备上。


Constructor:



要使用 ExpansionPanelList,需要调用下面的构造函数:



const ExpansionPanelList({
Key? key,
this.children = const <ExpansionPanel>[],
this.expansionCallback,
this.animationDuration = kThemeAnimationDuration,
this.expandedHeaderPadding = _kPanelHeaderExpandedDefaultPadding,
this.dividerColor,
this.elevation = 2,
})

Properties:



ExpansionPanelList 的一些属性如下:




  • > children: 此属性用于扩展面板 List 的子元素。它们的布局类似于[ListBody]。

  • > expansionCallback: 此属性用于每当按下一个展开/折叠按钮时调用的回调。传递给回调的参数是按下的面板的索引,以及面板当前是否展开。

  • > animationDuration: 这个属性用于展开或折叠时,我们可以观察到一些动画在一定时间内发生。我们可以通过使用扩展面板 List 的 animationDuration 属性来更改持续时间。我们只需要提供以微秒、毫秒或分钟为单位的持续时间值。

  • > expandedHeaderPadding: 此属性用于展开时围绕面板标头的填充。默认情况下,16px 的空间是在扩展期间垂直添加到标题(上面和下面)。

  • > dividerColor: 当[ expsionpanel.isexpanded ]为 false 时,此属性用于定义分隔符的颜色。如果‘ dividerColor’为空,则使用[ DividerThemeData.color ]。如果为 null,则使用[ ThemeData.dividerColor ]。

  • > elevation: 此属性用于在扩展时定义[ expsionpanel ]的提升。这使用[ kElevationToShadow ]来模拟阴影,它不使用[ Material ]的任意高度特性。默认情况下,仰角的值为 2。


如何实现 dart 文件中的代码:


你需要分别在你的代码中实现它:



lib 文件夹中创建一个名为 main.dart 的新 dart 文件。



首先,我们将生成虚拟数据。我们将创建一个列表 <Map<String, dynamic>> 并添加变量 _ items 等于生成一个列表。在这个列表中,我们将添加 number、 id、 title、 description 和 isExpanded。


List<Map<String, dynamic>> _items = List.generate(
10,
(index) => {
'id': index,
'title': 'Item $index',
'description':
'This is the description of the item $index. Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
'isExpanded': false
});

在正文中,我们将添加 ExpansionPanelList() 小部件。在这个小部件中,我们将添加标高为 3,在括号中添加 expsioncallback 索引和 isExpanded。我们将添加 setState ()方法。在方法中,我们将添加 _ items [ index ][‘ isexpanded’] equal not isExpanded。


ExpansionPanelList(
elevation: 3,
expansionCallback: (index, isExpanded) {
setState(() {
_items[index]['isExpanded'] = !isExpanded;
});
},
animationDuration: Duration(milliseconds: 600),
children: _items
.map(
(item) => ExpansionPanel(
canTapOnHeader: true,
backgroundColor:
item['isExpanded'] == true ? Colors._cyan_[100] : Colors._white_,
headerBuilder: (_, isExpanded) => Container(
padding:
EdgeInsets.symmetric(vertical: 15, horizontal: 30),
child: Text(
item['title'],
style: TextStyle(fontSize: 20),
)),
body: Container(
padding: EdgeInsets.symmetric(vertical: 15, horizontal: 30),
child: Text(item['description']),
),
isExpanded: item['isExpanded'],
),
)
.toList(),
),

我们将增加 animationDuration 为 600 毫秒。我们将添加子节点,因为 variable_items 映射到 expsionpanel ()小部件。在这个小部件中,我们将添加 canTapOnHeader was true,backgroundColor,headerBuilder 返回 Container ()小部件。在这个小部件中,我们将添加填充,并在其子属性上添加文本。在正文中,我们将添加 Conatiner 及其子属性,我们将添加文本。当我们运行应用程序时,我们应该获得屏幕输出,就像下面的屏幕截图一样。



全部代码


import 'package:flutter/material.dart';
import 'package:flutter_expansion_panel_list/splash_screen.dart';

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors._teal_,
),
home: Splash());
}
}

class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);

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

class _HomePageState extends State<HomePage> {
List<Map<String, dynamic>> _items = List.generate(
10,
(index) => {
'id': index,
'title': 'Item $index',
'description':
'This is the description of the item $index. Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
'isExpanded': false
});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text('Flutter Expansion Panel List Demo'),
),
body: SingleChildScrollView(
child: ExpansionPanelList(
elevation: 3,
// Controlling the expansion behavior
expansionCallback: (index, isExpanded) {
setState(() {
_items[index]['isExpanded'] = !isExpanded;
});
},
animationDuration: Duration(milliseconds: 600),
children: _items
.map(
(item) => ExpansionPanel(
canTapOnHeader: true,
backgroundColor:
item['isExpanded'] == true ? Colors._cyan_[100] : Colors._white_,
headerBuilder: (_, isExpanded) => Container(
padding:
EdgeInsets.symmetric(vertical: 15, horizontal: 30),
child: Text(
item['title'],
style: TextStyle(fontSize: 20),
)),
body: Container(
padding: EdgeInsets.symmetric(vertical: 15, horizontal: 30),
child: Text(item['description']),
),
isExpanded: item['isExpanded'],
),
)
.toList(),
),
),
);
}
}

结语


在本文中,我已经简单地解释了 ExpansionPanelList 的基本结构; 您可以根据自己的选择修改这段代码。这是一个小的介绍扩展/panellist On User Interaction 从我这边,它的工作使用 Flutter。


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

在Flutter上优雅的请求网络数据

当你点进来看这篇文章时,应该和我一样在思考如何优雅的请求网络、处理加载状态、处理加载异常。希望这篇文章和案例能给你带来不一样的思考。 解决的问题通用异常处理请求资源状态可见(加载成功,加载中,加载失败)通用重试逻辑 效果展示 为了演示请求失败的处理,特意在wa...
继续阅读 »

当你点进来看这篇文章时,应该和我一样在思考如何优雅的请求网络、处理加载状态、处理加载异常。希望这篇文章和案例能给你带来不一样的思考。


解决的问题

  • 通用异常处理
  • 请求资源状态可见(加载成功,加载中,加载失败)
  • 通用重试逻辑


效果展示


为了演示请求失败的处理,特意在wanApi抛了两次错
LBeZ5Q.gif


正文


搜索一下关于flutter网络封装的多半都是dio相关的封装,简单的封装、复杂的封装百花齐放,思路都是工具类的封装。今天换一个思路来实现,引入repository对数据层进行操作,在repository里使用dio作为一个数据源供repository使用,需要使用数据就对repository进行操作不直接调用数据源(在repositoy里是不允许直接操作数据源的)。用WanAndroid的接口写个示例demo


定义数据源


使用retrofit作为数据源,感兴趣的小伙伴可以看下retrofit这个库

class _WanApi implements WanApi {
_WanApi(this._dio, {this.baseUrl}) {
baseUrl ??= 'https://www.wanandroid.com';
}

final Dio _dio;

String? baseUrl;

@override
Future<BannerModel> getBanner() async {
const _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
final _data = <String, dynamic>{};
final _result = await _dio.fetch<Map<String, dynamic>>(
_setStreamType<BannerModel>(
Options(method: 'GET', headers: _headers, extra: _extra)
.compose(_dio.options, '/banner/json',
queryParameters: queryParameters, data: _data)
.copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl)));
final value = BannerModel.fromJson(_result.data!);
return value;
}

@override
Future<TopArticleModel> getTopArticle() async {
const _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
final _data = <String, dynamic>{};
final _result = await _dio.fetch<Map<String, dynamic>>(
_setStreamType<TopArticleModel>(
Options(method: 'GET', headers: _headers, extra: _extra)
.compose(_dio.options, '/article/top/json',
queryParameters: queryParameters, data: _data)
.copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl)));
final value = TopArticleModel.fromJson(_result.data!);
return value;
}

@override
Future<PopularSiteModel> getPopularSite() async {
const _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
final _data = <String, dynamic>{};
final _result = await _dio.fetch<Map<String, dynamic>>(
_setStreamType<PopularSiteModel>(
Options(method: 'GET', headers: _headers, extra: _extra)
.compose(_dio.options, '/friend/json',
queryParameters: queryParameters, data: _data)
.copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl)));
final value = PopularSiteModel.fromJson(_result.data!);
return value;
}

RequestOptions _setStreamType<T>(RequestOptions requestOptions) {
if (T != dynamic &&
!(requestOptions.responseType == ResponseType.bytes ||
requestOptions.responseType == ResponseType.stream)) {
if (T == String) {
requestOptions.responseType = ResponseType.plain;
} else {
requestOptions.responseType = ResponseType.json;
}
}
return requestOptions;
}
}



repository封装


Resource是封装的资源加载状态类,用于包装资源


enum ResourceState { loading, failed, success }

class Resource {
final T? data;
final ResourceState state;
final dynamic error;
Resource._({required this.state, this.error, this.data});

factory Resource.failed(dynamic error) {
return Resource._(state: ResourceState.failed, error: error);
}

factory Resource.success(T data) {
return Resource._(state: ResourceState.success, data: data);
}

factory Resource.loading() {
return Resource._(state: ResourceState.loading);
}

bool get isLoading => state == ResourceState.loading;
bool get isSuccess => state == ResourceState.success;
bool get isFailed => state == ResourceState.failed;
}

接下来我们在Repository里使用WanApi来封装,我们通过流的方式返回了资源加载的状态可供View层根据状态展示不同的界面,使用try-catch保证网络请求的健壮性


class WanRepository extends BaseRepository {
late WanApi wanApi = GetInstance().find();
///获取首页所需的所有数据
Stream> homeData() async* {
//加载中
yield Resource.loading();
try {
var result = await Future.wait([
wanApi.getBanner(),
wanApi.getPopularSite(),
wanApi.getTopArticle()
]);
final BannerModel banner = result[0];
final PopularSiteModel site = result[1];
final TopArticleModel article = result[2];
//加载成功
yield Resource.success(
HomeDataMapper(site.data, banner.data, article.data));
} catch (e) {
//加载失败
yield Resource.failed(e);
}
}
}

咋一看感觉没啥问题细思之下问题很多,每一个请求还多了try-catch以外那么多的模板方法,实际开发中只写try包裹的内容才符合摸鱼佬的习惯。ok,我们把模板方法提取出来到一个公共方法里去,就变成了这样:


class WanRepository extends BaseRepository {
late WanApi wanApi = GetInstance().find();
///获取首页所需的所有数据
Stream> homeData() async* {
///定义加载函数
loadHomeData()async*{
var result = await Future.wait([
wanApi.getBanner(),
wanApi.getPopularSite(),
wanApi.getTopArticle()
]);
final BannerModel banner = result[0];
final PopularSiteModel site = result[1];
final TopArticleModel article = result[2];
//加载成功
yield Resource.success(
HomeDataMapper(site.data, banner.data, article.data));
}
///将加载函数放在一个包装器里执行
yield* MyWrapper.customStreamWrapper(loadHomeData);
}
}

得益于Dart中函数可以作为参数传递,所以我们可以定义一个包装方法,入参是具体业务的函数,出参和业务函数一致,在这个方法里可以处理各种异常,甚至可以实现通用的请求重试(只需要在失败的时候弹窗提醒用户重试,获得认可后再次执行function就可以了,更关键的是此时状态管理里对repository的调用依旧是完整的,也就是说这是一个通用的重试功能)
包装器代码:


class MyWrapper {
//流的方式
static Stream> customStreamWrapper(
Stream> Function() function,
{bool retry = false}) async* {
yield Resource.loading();
try {
var result = function.call();
await for(var data in result)
{
yield data;
}
} catch (e) {
//重试代码
if (retry) {
var toRetry = await Get.dialog(const RequestRetryDialog());
if (toRetry == true) {
yield* customStreamWrapper(function,retry: retry);
}
else
{
yield Resource.failed(e);
}
} else {
yield Resource.failed(e);
}
}
}
}

其实就是把相同的地方封装成一个通用方法,不同的地方单独拎出来编写,然后作为一个参数传到包装器里执行。显然这样的方法却不够优雅,每次在写repository的时候都得创建一个函数在里面编写请求数据的逻辑然后交给包装器执行。我们肯定希望repository里代码长成这个样子:


@Repo()
abstract class WanRepository extends BaseRepository {
late WanApi wanApi = GetInstance().find();

///获取首页所需的所有数据
@ProxyCall()
@Retry()
Stream> homeData() async* {
var result = await Future.wait(
[wanApi.getBanner(), wanApi.getPopularSite(), wanApi.getTopArticle()]);
final BannerModel banner = result[0];
final PopularSiteModel site = result[1];
final TopArticleModel article = result[2];
yield Resource.success(
HomeDataMapper(site.data, banner.data, article.data));
}
}

是的没错,最终的repository就长这个样子,你只需要在类上打个注解@Repo在需要代理调用的方法上注解@ProxyCall,运行 flutter pub run build_runner build 就可以生成对应的包装代码:


// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'wan_repository.dart';

// **************************************************************************
// RepositoryGenerator
// **************************************************************************

class WanRepositoryImpl = WanRepository with _WanRepository;

mixin _WanRepository on WanRepository {
@override
Stream> homeData() {
return MyWrapper.customStreamWrapper(() => super.homeData(), retry: true);
}
}

结语


感谢你的阅读,这只是一个网络请求封装的思路不是最优解,但希望给你带来新思考


附demo地址:gitee.com/cysir/examp…


flutter版本:2.8


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

这么牛的毕业生,来当CTO吧!

时光如风飘渺,眨眼间已经在行业浸润多年了,见过无数厉害的人物,也见过更多更多的挫B。前几天刚上班,就接到面试一个毕业生的任务,让我感叹人与人之间的差距。他的水平,绝对的完爆工作多年的架构师。在下佩服之~我们的话题,是关于怎么构建一个可伸缩的高可用、高可靠大型网...
继续阅读 »

时光如风飘渺,眨眼间已经在行业浸润多年了,见过无数厉害的人物,也见过更多更多的挫B。

前几天刚上班,就接到面试一个毕业生的任务,让我感叹人与人之间的差距。

他的水平,绝对的完爆工作多年的架构师。在下佩服之~

我们的话题,是关于怎么构建一个可伸缩的高可用、高可靠大型网站。嗯,就让我们开始吧。

1.要发问了

大家都知道,如今的互联网,数据量爆炸,服务请求飙升,即使是非常小的公司,也可能因为某个产品产生不同于往日的数十倍流量,当然这有时候是个梦想而已。

流量增加就意味着对后端服务能力的增加,如何构建每秒处理GB数据、QPS超过数十万的大型系统,就变成了一个挑战。尤其是某些天才的秒杀创意,让这个流量变的越发变态不可预料。

在有效的资源下,如何让系统保持良好的反馈?以支撑老板们的梦想呢?你有什么处理方式?或者你有什么体系化的心得体会要和我分享一下的?

毕业生微微一笑:“我在这方面正好有点总结,我可以多花点时间聊聊这个”。

好吧,洗耳恭听。

2.服务建设的重要指标

“我首先要说明的是,服务的建设要关注几个指标。有了目标就有了方向,大体上我总结了四个”。

  1. 可用性。 我们要保证服务的可用性,也就是SLA指标。只有服务能够正常响应,错误率保持在较低的水平,我们的服务才算正常。
  2. 服务性能。 可用性和性能是相辅相成的,服务性能高了,有限的资源就能够支撑更多的请求,可用性就会提高。所以服务性能优化是一个持续性的工作,在巨量流量下每1ms的平均性能提升,都是值得追求的。
  3. 可靠性。 分布式服务的组件非常多,每个组件都可能会产生问题,影响面也不尽相同。如何保证每个组件的运行时高可靠性,如何保证数据的一致性,都是有挑战的。
  4. 可观测性。 想要获取服务优化的指标数据,就要求我们的服务,在设计开始能够保证服务的可观测性。宏观上能够识别组件类故障,微观上能为性能优化提供依据。在HPA等自动化伸缩场景中,遥测数据甚至是自动化决策的唯一依据。

对于一个服务来说,扩容的手段主要有两种。scale-up:垂直扩展,scale-out:水平扩展。

垂直扩展通过增加单台机器的配置来增加单节点的处理能力。这在某些业务场景下是非常有必要的。但我们的服务更多的是追求水平扩展,用更多的机器来支撑业务的发展。

只要服务满足了横向扩展的能力,满足无状态的特点,剩下的事情就是堆硬件了。听起来很美好,但实际上,对整个体系架构的挑战非常的大。

毕业生的一番分析像极了野鸡CTO的发言,废话连篇。我暗自点头,鼓励他继续深入、细化下去,拿出点不一样的东西。

3.幂等性

如果接口调用失败怎么办?在早期的互联网中,因为网络原因,这样的情况可能更严重。HTTP状态码504,就是典型的代表网关超时的状态。第一次请求可能会超时失败,第二次可能就成功了。现实中需要严格重试的接口还是蛮多的,尤其是异步化的加入,使得重试变得更加重要。

但我们也要考虑由于快速重试所造成的重试风暴,因为超时本身可能就意味着服务器已经不堪重负,我们没有任何理由火上浇油。所以,重试都会有退避算法(exponential backoff),直到真正的结束请求进入异常处理流程。

可以看出,由于超时和重试机制的引入,服务的幂等变的格外重要。它不仅仅是在单台机器上支持重复的调用,在整个分布式集群环境中同样保证可以重入多次。

在数学上,它甚至有一个优美的函数公式。

f(f(f(x))) = f(f(x)) = f(x)

一旦接口拥有了幂等性,就有了能够忍受故障的能力。当我们因为偶发的网络故障、机器故障造成少量的服务调用失败时,可以通过重试和幂等很容易的最终完成调用。

对于查询操作来说,在数据集合不变的情况下,它天然是幂等的,不需要做什么额外的处理。比较有挑战的是添加和更新操作。

有不少的技术手段来保证幂等,比如使用数据库的唯一索引,使用提前生成好的交易ID,或者使用token机制来保证唯一调用。其中,token机制被越来越多的使用,其做法是在请求之前,先请求一个唯一的tokenId,此后的调用幂等就围绕着tokenId进行编程。

4.健康检查

自从k8s把健康检查这个东西标准化之后,健康检查就成为了一个服务的必备选项。在k8s中,分为活跃探针(liveness probe)和 就绪探针(readiness probe)。

活跃探测主要用来查明应用程序是否处于活动状态。它只展示应用本身的状态,而不应依赖于外部其他系统的健康状态;就绪探测指示应用程序是否已准备好接受流量,如果应用程序实例的就绪状态为未就绪,则不会将流量路由到该实例。

如果你使用了SpringBoot的actuator组件,通过health接口,将很容易获取这部分功能。当容器或者注册中心通过health接口判断到服务出现了问题,会自动的把问题节点从节点列表中摘除,然后再通过一系列探测机制在服务恢复正常的时候再把它挂上去。

通过健康检查机制,能够避免流量被调度到错误的机器上去。

5.服务自动发现

早期的软件开发人员,对服务上线的机制摸的门清,不是因为他们想要这样,而是不得不这样做。

比如,我要扩容一台机器,需要首先测试这台机器的存活性,然后部署服务,最后再在负载均衡软件比如nginx中将这台机器配置上。通常情况下,还要看一下日志,到底有没有流量到这台机器上来。

借助于微服务和持续集成,我们再也不需要这么繁杂的上线流程,只需要在页面上点一下构架、发布,服务就能够自动上线,并被其他服务发现。

注册中心在服务发现方面承担了非常重要的角色。它相当于一个信息集中地,所有的服务启动、关闭,都要上报到这里;同样,我想要调用某些服务,也需要到同一个注册中心去查询。

注册中心相当于一个中介,将这些频繁的上下线需求和查询需求,全部统一起来进行管理,现在已经成为微服务的必备设施。

这些查询需求可能是非常频繁的,所以在调用方本地,同样也会存储一份副本,这样在注册中心出现问题的时候,不至于因为大脑缺氧而造成大规模故障。有了副本就有了一致性问题,有注册中心通过Pull的方式更新信息,存在数据一致性的实效性。实效性处理的比较好的是有Push(通知)机制的组件,能够在较快的时间感知服务的变化。

许多组件可以充当服务注册中心,只要它有分布式存储数据的能力和数据一致性的能力。比如Eureka、Nacos、Zookeeper、Consul、Redis,甚至数据库,都能胜任这个角色。

6.限流

web开发中,tomcat默认是200个线程池,当更多的请求到来,没有新的线程能够去处理这个请求,那这个请求将会一直等待在浏览器方。表现的形式是,浏览器一直在转圈(还没超过acceptCount),即使你请求的是一个简单的Hello world。

我们可以把这个过程,也看作是限流。它在本质上,是设置一个资源数量上限,超出这个上限的请求,将被缓冲,或者直接失败。

对于高并发场景下的限流来说,它有特殊的含义:它主要是用来保护底层资源的。如果你想要调用某些服务,你需要首先获取调用它的许可。限流一般由服务提供方来提供,对调用方能够做事的能力进行限制。

比如,某个服务为A、B、C都提供了服务,但根据提前申请的流量预估,限制A服务的请求为1000/秒、B服务2000/秒,C服务1w/秒。在同一时刻,某些客户端可能会出现被拒绝的请求,而某些客户端能够正常运行,限流被看作是服务端的自我保护能力。

常见的限流算法有:计数器、漏桶、令牌桶等。但计数器算法无法实现平滑的限流,在实际应用中使用较少。

7.熔断

自从施耐德发明了断路器,这个熔断的概念席卷了全球。从A股熔断,到服务熔断,大有异曲同工之妙。

熔断的意思是:当电路闭合时,电流可以通过,当断路器打开时,电流停止。

通常情况下,用户的一个请求,需要后端多个服务配合才能完成工作。后端的这些服务,并不是每一个都是必须的,如果因为其中的某个服务有问题,就把用户的整个请求给拒绝掉,那是非常不合理的。

熔断期望某些服务,在发生问题时,返回一些默认值。整个请求依然可以正常进行下去。

比如风控。如果在某个时间风控服务不可用了,用户其实是应该能够正常交易的。这时候我们应该默认风控是通过的,然后把这些异常交易倒到另外一个地方,在风控恢复后再尽快赶在发货的之前处理。

从上面的描述可以看出,有的服务,熔断后简单的返回些默认数据就行,比如推荐服务;但有的服务就需要有对应的异常流程支持,算是一个if else;更要命的是,有些业务不支持熔断,那就只能Fail Fast。

一股脑的处理是没有思考的技术手段,不是我们所推荐的。

Hystrix、resilience4j、Sentinel等组件,是Java系广泛使用的工具。通过SpringBoot的集成,这些框架一般用起来都比较方便,可以达到配置化编程。

8.降级

降级是一个比较模糊的说法。限流、熔断,在一定程度上,也可以看作是降级的一种。但通常所说的降级,切入的层次更加高级一些。

降级一般考虑的是分布式系统的整体性,从源头上切断流量的来源。比如在双11的时候,为了保证交易系统,将会暂停一些不重要的服务,以免产生资源争占。服务降级有人工参与,人为使得某些服务不可用,多属于一种业务降级方式。

在什么地方最适合做降级呢?就是入口。比如Nginx,比如DNS等。

在某些互联网应用中,会存在MVP(Minimum Viable Product)这个概念,意为最小化可行产品,它的SLA要求非常高。围绕着最小可行性产品,会有一系列的服务拆分操作,当然某些情况甚至需要重写。

比如,一个电商系统,在极端情况下,只需要把商品显示出来,把商品卖出去就行。其他一些支撑性的系统,比如评论、推荐等,都可以临时关掉。在物理部署和调用关系上,就要考虑这些情况。

9.预热

请看下面一种情况。

一个高并发环境下的DB,进程死亡后进行重启。由于业务处在高峰期间,上游的负载均衡策略发生了重分配。刚刚启动的DB瞬间接受了1/3的流量,然后load疯狂飙升,直至再无响应。

原因就是:新启动的DB,各种Cache并没有准备完毕,系统状态与正常运行时截然不同。可能平常1/10的量,就能够把它带入死亡。

同理,一个刚刚启动的JVM进程,由于字节码并未被JIT编译器优化,在刚启动的时候,所有接口的响应时间都比较慢。如果调用它的负载均衡组件,并没有考虑这种刚启动的情况,1/n的流量被正常路由到这个节点,就很容易出现问题。

所以,我们希望负载均衡组件,能够依据JVM进程的启动时间,动态的慢慢加量,进行服务预热,直到达到正常流量水平。

10.背压

考虑一下下面两种场景:

  1. 没有限流。请求量过高,有多少收多少,极容易造成后端服务崩溃或者内存溢出
  2. 传统限流。你强行规定了某个接口最大的承受能力,超出了直接拒绝,但此时后端服务是有能力处理这些请求的

如何动态的修改限流的值?这就需要一套机制。调用方需要知道被调用方的处理能力,也就是被调用方需要拥有反馈的能力。背压,英文Back Pressure,其实是一种智能化的限流,指的是一种策略。

背压思想,被请求方不会直接将请求端的流量直接丢掉,而是不断的反馈自己的处理能力。请求端根据这些反馈,实时的调整自己的发送频率。比较典型的场景,就是TCP/IP中使用滑动窗口来进行流量控制。

反应式编程(Reactive)是观察者模式的集大成者。它们大多使用事件驱动,多是非阻塞的弹性应用,基于数据流进行弹性传递。在这种场景下,背压实现就简单的多。

背压,让系统更稳定,利用率也更高,它本身拥有更高的弹性和智能。比如我们常见的HTTP 429状态码头,表示的意思就是请求过多,让客户端缓一缓,不要那么着急,算是一个智能的告知。

11.隔离

即使在同一个instance中,同类型的资源,有时候也要做到隔离。一个比较浅显的比喻,就是泰坦尼克号,它有多个船舱。每个船舱都相互隔离,避免单个船舱进水造成整个船沉了。

当然,泰坦尼克号带着骚气的jack沉了,那是因为船舱破的太多的缘故。

在有些公司的软件中,报表查询服务、定时任务、普通的服务,都放在同一个tomcat中。它们使用同一套数据库连接池,当某些报表接口的请求一上升,其他正常的服务也无法使用。这就是混用资源所造成的后果。

除了遵循CQRS来把服务拆分,一个快速的机制就是把某类服务的使用资源隔离。比如,给报表分配一个单独的数据库连接池,分配一个单独的限流器,它将无法影响其他服务。

耦合除了出现在无状态服务节点,同时还会出现在存储节点。与其把报表服务的存储和正常业务的存储放在一个数据库,不如把它们拆开,分别提供服务。

一个和尚挑水喝,两个和尚也可以挑水喝。原因就是他们在两个庙。

12.异步

如果你比较过BIO和NIO的区别,就可以看到,我们的服务其实大部分时间都是在等待返回,CPU根本就没有跑满。当然,NIO是底层的机制,避免了线程膨胀和频繁的上下文切换。

服务的异步化和NIO有点类似,采用之后可以避免无谓的等待。尤其是当调用路径冗长的时候,异步不会阻塞,响应也会变的迅速。

单机时候,我们会采用NIO;而在分布式环境中,我们会采用MQ。虽然它们是不同的技术,但道理都是相通的。

异步通常涉及到编程模型的改变。同步方式,请求会一直阻塞,直到有成功,或者失败结果的返回。虽然它的编程模型简单,但应对突发的、时间段倾斜的流量,问题就特别大,请求很容易失败。异步操作可以平滑的横向扩容,也可以把瞬时压力时间上后移。同步请求,就像拳头打在钢板上;异步请求,就像拳头打在海绵上。你可以想象一下这个过程,后者肯定是富有弹性,体验更加友好。

13.缓存

缓存可能是软件中使用最多的优化技术了。比如,在最核心的CPU里,就存在着多级缓存;为了消除内存和存储之间的差异,各种类似Redis的缓存框架更是层出不穷。

缓存的优化效果是非常好的,可以让原本载入非常缓慢的页面,瞬间秒开;也能让本是压力山大的数据库,瞬间清闲下来。

缓存,本质上是为了协调两个速度差异非常大的组件,通过加入一个中间层,将常用的数据存放在相对高速的设备中。

在应用开发中,缓存分为本地缓存和分布式缓存。

那什么叫分布式缓存呢?它其实是一种集中管理的思想。如果我们的服务有多个节点,堆内缓存在每个节点上都会有一份;而分布式缓存,所有的节点,共用一份缓存,既节约了空间,又减少了管理成本。

在分布式缓存领域,使用最多的就是Redis。Redis支持非常丰富的数据类型,包括字符串(string)、列表(list)、集合(set)、有序集合(zset)、哈希表(hash)等常用的数据结构。当然,它也支持一些其他的比如位图(bitmap)一类的数据结构。

所以加下来的问题一定集中在缓存穿透、击穿和雪崩,以及一致性上,这个我就不多聊了。

14.Plan-B

一个成熟的系统都有B方案,除了异地多活和容灾等处置方案,Plan-B还以为着我们要为正常的服务提供异常的通道。

比如,专门运行一个最小可行性系统,运行公司的核心业务。在大面积故障的时候,将请求全面切换到这个最小系统上。

Plan-B通常都是全局性的,它保证了公司最基本的服务能力,我们期望它永远用不上。

15.监控报警

问题之所以成为问题,是因为它留下了证据。没有证据的问题,你虽然看到了影响结果,但是你无法找到元凶。

而且问题通常都具有人性化,当它发现无法发现它的时候,它总会再次出现。就如同罪犯发现了漏洞,还会再次尝试利用它。

所以,要想处理线上问题,你需要留下问题发生的证据,这是重中之重。如果没有这些东西,你的公司,绝对会陷入无尽的扯皮之中。

日志是最常见的做法。通过在程序逻辑中进行打点,配合Logback等日志框架,可以快速定位到发生问题的代码行。我们需要看一下bug的详细发生过程,对可能发生问题的逻辑进行详细的日志记录,进行更加细致的日志输出,在发生问题的时候,就可以切换到debug进行调试。

如果是大范围的bug,那么强烈建议直接在线上进行调试。不太推荐使用Arthas等工具动态的修改字节码进行测试,当然也不推荐IDEA的远程调试。相反,推荐使用类似金丝雀发布的方式,导出非常小的一部分流量,构造一个新的版本进行测试。如果你没有金丝雀发布平台,类似Nginx的负载均衡工具也可以通过权重做到类似的事情。

日志系统与监控系统,对硬件的需求是比较大的,尤其是你的请求体和返回体比较大的情况下,对存储和计算资源的额要求更是高。它的硬件成本,在整个基础设施中,占比也是比较高的。但这些证据信息,对分析问题来说,是非常有必要的。所以即使比较贵,很多公司依然会有很大的投入在这上面,包括硬件投入和人力投入。

MTTD和MTTR是两个非常重要的指标,我们一定要加大关注。

16.结尾

我看了一下表,这家伙很能说,预定的时间很快用完了。我挥挥手打住:”你还会哪些东西?简单的说一下吧!“

”也不是很多。像怎么构建一个DevOps团队支撑我们开发、测试、线上环境,如何进行更深入的性能优化,如何进行实际的故障排查。以及一些细节问题,比如怎么优化操作系统,网络编程和多线程,我这些还都没有聊。“

我说,”够了,你已经非常优秀了“。

”你把自己叫作毕业生,已经碾压绝大多数人了。你到底是哪里的毕业生啊!“

”我是B站的,昨天刚毕业~“,他腼腆的笑了。

我盯着他的眼睛,也笑了。枯木逢春犹再发,人可两度再少年!妙啊。

来源:https://mp.weixin.qq.com/s/d5KGmRcZJkoM4ciMBdbFKg

收起阅读 »

有些程序员,本质是一群羊!

羊都是以群论的。如果你感觉它的单位是只,那只能证明你太穷。真正牧羊的,从来不会因为晚上烤了只全羊,而感觉到自己的羊少了。不信重温一下李安的《断背山》看看,关注点别搞错了,我们是在谈羊。当在它们耳朵上钉上红绳子,或者激进点彩绘一下,你就再也无法分辨哪只羊是张三,...
继续阅读 »

羊都是以群论的。如果你感觉它的单位是只,那只能证明你太穷。

真正牧羊的,从来不会因为晚上烤了只全羊,而感觉到自己的羊少了。不信重温一下李安的《断背山》看看,关注点别搞错了,我们是在谈羊。

当在它们耳朵上钉上红绳子,或者激进点彩绘一下,你就再也无法分辨哪只羊是张三,哪只羊是李四。牧羊犬也不会关心这东西,它只关心它的晚饭。

所以大多数羊没有身份。它只是只羊而已。

软件开发中,有些公司把程序员们当作资源池来利用,那么他们的本质就是羊(没错就是外包)。羊群的特征很有意思,但单体并没有什么存在感。大多数时候,低下头吃草就够了。

来瞧瞧吧。

羊吃草

羊的工作就是吃草。

它不管草有多少,也不管为什么会被发放到某个地方吃草,它吃草甚至是不用经过大脑思考。牧羊人圈定了一片草原,它们就会从左吃到右,然后再从右吃到左,直到把这片草原吃光为止。

羊的嘴很臭,被啃过的草一年内不会发出新芽。

如果你不管它们,草吃光了,有些羊就开始吃草根,草根吃光了就慢慢饿死。它从来不会想着到外面的世界看一看。

羊会反刍,就是把嚼过的东西,重新再弄到嘴里嚼一遍。这种重复的动作,使得羊的智商非常的低,只能进行这些低级的循环 。羊有疝气,它们嘴里的东西其实很难闻但它自己感觉不到,只有目光呆滞的在那里蠕动它的牙齿,直到嚼出草绿色的津液来。

羊惊群

有时候,羊会表现出让人莫名其妙的动作。

一个阳光明媚的下午,所有的羊都在认真吃草。突然,有一只羊悚然之间,竖起了耳朵瞪大了眼睛,仿佛看到了极具危险的事物。

或许是它的脑子短路,突然想到了一些不该属于它的东西;或许是草里的蚂蚱吓了它一跳。只要有一只羊表现出了这种态度,正在吃草的羊,就会在几秒之内全部停止咀嚼,竖起耳朵瞪大了眼睛。

这个状态会持续十几秒,直到危险解除--更大可能是根本就没有危险。

在群体中,个体的警觉会突然之间就传遍整个集体,但谁也弄不明白危险到底来自何方 。或许只有在这十几秒钟,在停止吃草的时间,羊才会思考为什么自己是只羊,而不是趴在旁边的牧羊犬。

更多时候,羊会低下头继续吃草,所有的羊都会快速忘掉这种集体性的思考。

羊不怕死

在杀猪的时候,猪会一直嚎叫;杀牛的时候,牛会流泪。

羊不一样,它不吭一声,甚至连害怕的神情都没有。

我们都习惯称羊为沉默的羔羊,就是因为它很安静。安静是因为它智商低,而不是因为它勇敢。

我杀过羊。从绑住它的蹄子吊起来,到刀子刺进它的脖子里放血。羊会因为痛挣扎几下,但整个过程出奇的顺利。所以杀羊的人,从来没有什么心理压力,因为这个过程太柔滑了。

有时候刀子刺的不准,羊会饶有兴趣的看着自己的血液躺下来,并伸出舌头感受一下。它可能会被一只蚂蚱吓得竖起双耳,但它并不怎么怕拿刀子的你。

它可能认为自己的主人,并不会拿它开刀。即使你在它面前宰了它的同伴,它也会觉得自己是特殊的。

搞笑的是,羊的温顺都是表现在对外方面,多数羊在内部并不老实。

两只羊,会因为一些谁都闹不清楚的原因,跳起来碰对方的头。头破血流的,掉羊角的比比皆是。那阵势,就是要以命相抵,越斗越来劲。

我越来越觉得,羊对生命并不渴望,对更好的生活也没有追求。  我甚至觉得,它希望死亡。

它从来不像狗一样,挖个坑储藏食物;也不像乌鸦一样,就喜欢收集发光的东西。羊从一生下来,眼光中就透露着生无所恋,只有在发情期的时候,才能表现的像个正常的动物一样。


它并不怕死,因为它从来没搞懂为什么活着。况且一直吃草、咀嚼的一生,本就没什么值得留恋的地方。

牧羊犬

羊的温顺,枯燥,会让人觉得没有攻击性,所以喜欢羊的人很多(我不是说伊拉克被解放的羊群们)。但羊也并没有什么忠诚度,它能因为某只小母羊,就串到别的羊群里,让你再也找不到它。

认清楚每一只羊,对人来说很困难,因为这整个过程会让人感到乏味、枯燥且没必要。豢养几只牧羊犬,是最常见的方式。牧羊犬会尽职尽责的盯梢,追逐拖后腿的、不听话的羊。牧羊犬多数情况下并不需要劳动,也不需要啃草皮 ,所以阳光明媚的时候,它可以翻着肚皮晒太阳,把狗屎排在任何地方。

牧羊犬会自己找乐子。 它会戏弄某只羊,追着它跑,虽然最后都忘掉了;它会制定自己的规则,比如某些强迫症的牧羊犬会要求羊群必须以某个路线行走。

羊惊群的时候,也是牧羊犬最警觉的时候。哪怕十几秒钟,如果羊有独立思考的时间,整个生态就会有大的变化。只要有一只羊不正常的奔跑起来,整个群体就会发生踩踏,牧羊犬就会疲于奔命。

这种可能影响晚饭的场景,牧羊犬会特别上心。

牧羊人的晚饭,偶尔是烤全羊,而牧羊犬期待着落下的骨头。至于羊,并不太在乎这种结局。

看起来大家都很满意,只有看客们多虑了。我们还是数一下下面的图有多少只羊吧。


数不清楚?只能说明不适合当牧羊犬,没什么好值得伤心的。

来源:小姐姐味道

收起阅读 »

过度设计是罪恶的!

软件开发的哪个阶段最容易招人喷?如果你严格按照什么瀑布模式、敏捷模式开发的话,你会发现永远是概要设计的评审阶段。 这个时候,屎山还没有成为既定的事实。多位理想主义达人,就会搬出各种规则、规范,来给你的方案下套子。 他们是为了你的方案更好么?大多数情况未必。有的...
继续阅读 »

软件开发的哪个阶段最容易招人喷?如果你严格按照什么瀑布模式、敏捷模式开发的话,你会发现永远是概要设计的评审阶段。


这个时候,屎山还没有成为既定的事实。多位理想主义达人,就会搬出各种规则、规范,来给你的方案下套子。


他们是为了你的方案更好么?大多数情况未必。有的人,多说几句是为了凸显自己的价值;有的人是刚看了几本书,感觉不吐不快;还有的人,本身就是完美主义者,看不得你的方案有任何瑕疵。总结下来,完美主义者还是有点作用的。


但当你把开发任务扔给这些指挥和挑刺的人,你会发现他们大多数不仅仅实现不了自己给套上的套子,连最基本的功能实现都是问题。


每当这时候,我内心都会大喊:让这些假洋鬼子去死吧!


组件替换问题


如果我们的技术栈,选用的是MySQL,我们会采用JDBC、MyBatis、JPA等一系列的基础的编码工具。但这种选择,对追求接口和实现分离的同学来说,却是不可忍受的。


这些人会搬出无数的理由,来说明,如果不加入一个中间层的话,代码是无法复用的。他们追求的是,如果你将来把数据库从MySQL切换到ElasticSearch,那么你几乎不需要改动任何代码。


“你有没有想过?如果你ES也不用了,把数据存储在Hbase中呢?”


这也是操蛋的DDD所追求和说明的,把一个简单的数据库操作给拆的七零八落。


如果把这种设计哲学推广开来的话,你会发现几乎每个地方都有问题。


项目中使用了Kafka,如果将来换成Pulsar呢?项目中使用了Http,如果将来要换成Socket呢?最让人担心的是,项目中使用了Java语言,如果后面使用Golang呢?是不是也要发明一个第三方语言来规避语言的差异?


值得注意的是,Spring家族在这些完美的目标上,产出了不少优秀的组件,比如Spring Data、Spring Cloud Stream等。


但这不代表你可以过度设计。因为用来屏蔽实现的这部分实现,本身就是风险的存在。


耦合有错么?


只要需求落在代码上,就一定会产生耦合,想要去除所有的耦合,那是根本不可能的。


在开发中,你为什么不想着为开发语言的耦合创造一个第三方语言呢?这个成本是大的,而且是非常没有必要的,如果真的有这种需求,你可以把它放在重构上。


同样的话,我也可以送给纠结底层数据库存储的同学。一旦你做了某个决定,想要完整的抽象就变的非常的奢侈,它不会比更换开发语言有更少的工作量。


这是一种思维惯式,也是一个度的问题。


在评审会议上喷一下非常的爽,但没有人会多想一想背后的工期、需求和必要性。


但如果放任耦合无限制的产生,显然也不是我们想要的,这个度的度量需要一定的学问。


内部技术和外部协作


我觉得冲突产生的根本原因,是评审者甚至开发者,没有弄清项目的边界是什么。


拿SpringCloud来说,只要定义好Feign接口的协作方式和规范,把文档写好命名做好,另外一个团队并不是很关心你后面到底是Java写的,还是挂了个sidecar的Golong程序。


再拿消息队列来说,全公司定下了Kafa作为数据交换的通道,虽然它没有JMS这样的协议兼容,你也不会蛋疼的去封装一层去兼容。大家默认Kafka的Topic/Partition机制,并基于这样的特性调整代码。


至于我的后端数据库,是用MyBatis去处理,还是用JPA去处理。是MVC三层模型,还是直接把SQL写在Controller里。只要这些是我的私有数据,外部团队永远不会用到的话,任何人都没必要对其指手画脚。


只要边界问题处理好,就不会产生大的乱子。


End


一刀切,在公司技术部门懒政的环境中,普遍存在。


在制定规范和标准的时候,大家都习惯兼容并包,照顾所有的业务线,做上一份。但在实践中,这种标准的问题通常问题多多,为业务方造成许多的困扰。


人要因材施教,规范也应该区分环境。制定规范的人活儿多一些,执行的人,生活就快乐一些!


作者:小姐姐味道
来源:https://juejin.cn/post/7088474327124819975
收起阅读 »

实现一个悬浮在软键盘上的输入栏

前言 我们要实现一个悬浮在软键盘上的输入栏(即一个悬浮栏),过程中遇到了很多问题,查阅了一些网上的文章,结果发现不少是错误的,走了一些弯路,这里就一一记录一下。 悬浮栏 实现悬浮栏很简单 chatInputPanel.setVisibility(View.VI...
继续阅读 »

前言


我们要实现一个悬浮在软键盘上的输入栏(即一个悬浮栏),过程中遇到了很多问题,查阅了一些网上的文章,结果发现不少是错误的,走了一些弯路,这里就一一记录一下。


悬浮栏


实现悬浮栏很简单


chatInputPanel.setVisibility(View.VISIBLE);
chatInputEt.setFocusable(true);
chatInputEt.setFocusableInTouchMode(true);
chatInputEt.requestFocus();
InputMethodManager inputManager = (InputMethodManager)chatInputEt.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
inputManager.showSoftInput(chatInputEt, 0);

chatInputPanel就是悬浮栏整个layout,mChatPanelContent才是悬浮栏实际部分,chatInputEt是其中的EditText,对它做一些设置就可以实现将chatInputPanel悬浮在软件盘上。


这里chatInputPanel是全屏的(点击mChatPanelContent以外部分隐藏键盘),mChatPanelContent是在它的bottom底部,默认隐藏(INVISIBLE)。


横屏时软键盘全屏


横屏时,安卓默认会将软键盘全屏显示,这样无法实现悬浮栏。所以需要取消全屏显示


在EditText中使用android:imeOptinos可对Android自带的软键盘进行一些界面上的设置




  • android:imeOptions="flagNoExtractUi" //使软键盘不全屏显示,只占用一部分屏幕




  • android:imeOptions="actionNone" //输入框右侧不带任何提示




  • android:imeOptions="actionGo" //右下角按键内容为'开始'




  • android:imeOptions="actionSearch" //右下角按键为放大镜图片,搜索




  • android:imeOptions="actionSend" //右下角按键内容为'发送'




  • android:imeOptions="actionNext" //右下角按键内容为'下一步'




  • android:imeOptions="actionDone" //右下角按键内容为'完成'




所以我们为EditText设置android:imeOptions="flagNoExtractUi"即可实现在横屏时不全屏显示。同时,可能EditText添加相应的监听器,捕捉用户点击了软键盘右下角按钮的监听事件,以便进行处理。


editText.setOnEditorActionListener(new OnEditorActionListener() {   

@Override

public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {

Toast.makeText(MainActivity.this, "text2", Toast.LENGTH_SHORT).show();

return false;

}

});

监听软键盘(该方法不可靠,废弃,下面有靠谱的)


注意:这是网上的一个错误方法,所以特意拿出来说一下,不感兴趣直接去看(3)即可。


显示没问题了,但是软键盘隐藏的时候要求悬浮栏同步隐藏起来。


系统并没有提供监听软键盘收起的api,所以我们只能自己实现。


chatInputPanel.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if(chatInputPanel.getBottom() > container.getRootView().getHeight() / 2){
chatInputPanel.setVisibility(View.INVISIBLE);
}
else{
chatInputPanel.setVisibility(View.VISIBLE);
}
}
});

监听chatInputPanel(悬浮栏整体布局)的布局变化,当底部大于rootview高度一半的时候隐藏,否则显示。


因为我们的功能是横屏的,所以键盘弹起时,chatInputPanel因为悬浮在键盘上,所以底部一定小于rootview高度(屏幕宽度)一半。


当收起键盘,chatInputPanel会回到最底部(设置是在父布局底部),所以底部一定大于一半。


这个方法不靠谱,而且重绘会导致onGlobalLayout频繁的执行,虽然可以加上一个时间来控制,但是不推荐使用这个方式来监听软键盘,下面看看另外一种方式。


靠谱的监听软键盘的方法


上面的方法为什么不考虑,是因为全屏显示FLAG_FULLSCREEN(隐藏通知栏)导致问题


当我们需要全屏显示隐藏通知栏时,会使用FLAG_FULLSCREEN属性


getActivity().getWindow().setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);

但是会影响上面的悬浮栏,因为发现chatInputPanel.getBottom()始终没变化,但是我们判断显示隐藏就靠这个变化。


没变化是因为android在全屏FLAG_FULLSCREEN的处理方式导致的,全屏时软键盘会出现很多问题,这个网上有很多。


如何解决?


我们换一种方式监听软键盘即可


getActivity().getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
Rect rect = new Rect();
rootView.getWindowVisibleDisplayFrame(rect);
int rootHeight=rootView.getRootView().getHeight();

int displayHeight=rect.height();
int diffHeight=rootHeight-displayHeight;
if(diffHeight==0){
//键盘收起
chatInputPanel.setVisibility(View.INVISIBLE);
}else{
//键盘弹出
chatInputPanel.setVisibility(View.VISIBLE);
}
}
});

通过监听根布局种的content布局的变化来判断,目前这个方法是最靠谱的。


但是还存在一个小问题,就是全屏状态下键盘会覆盖悬浮栏底部的一小部分,这个怎么办?


终极悬浮方式


上面解决了软键盘的监听问题,但是全屏状态下悬浮栏总会被遮住一部分,那怎么办?


其实这里还有一个问题,当显示键盘后,app中的布局整体被向上推起,这样导致部分组件缩小等情况。


我们要首先解决这个问题,让app的布局整体保持不动,键盘覆盖在其上面,这需要在弹起键盘前手动设置一下,如下:


mChatInput.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
DisplayMetrics metric = new DisplayMetrics();
getActivity().getWindowManager().getDefaultDisplay().getMetrics(metric);
chatInputPanel.setY(-metric.heightPixels);//解决首次可能向上推的问题

chatInputEt.setFocusable(true);
chatInputEt.setFocusableInTouchMode(true);
chatInputEt.requestFocus();
InputMethodManager inputManager = (InputMethodManager)chatInputEt.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
inputManager.showSoftInput(chatInputEt, 0);
}
});

这样将悬浮栏移到了最顶部以上,就不会出现上推的情况了(猜测与键盘的机制有关,因为键盘弹出如果遮挡了有焦点的输入组件就好重新调整窗口,我们将悬浮窗放在最上面,键盘怎么也不会遮挡到焦点的EditText,所以不会重新调整窗口)。


但是这样悬浮栏就一直看不见了,而且我们可以看到在这里去掉了chatInputPanel.setVisibility(View.VISIBLE);代码,那么如何显示?


上面我们提到使用OnGlobalLayoutListener方式监听键盘,我们就在这里显示即可,同时优化一下显示的位置,在这里计算窗口显示区域上移多少,让chatInputPanel也上移相应位置即可,如:


private int mLastHeight = 0;
getActivity().getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
Rect rect = new Rect();
getActivity().getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
int height = rect.height();
int rawHeight = getResources().getDisplayMetrics().heightPixels - rect.top;
if (height == mLastHeight)
return;

if (height < rawHeight) {
UiThreadHandler.postDelayed(new Runnable() {
@Override
public void run() {
chatInputPanel.setVisibility(View.VISIBLE);
chatInputPanel.setTranslationY(-(rawHeight - height));
}
}, 200);
} else {
UiThreadHandler.postDelayed(new Runnable() {
@Override
public void run() {
chatInputPanel.setVisibility(View.GONE);
}
}, 100);
}

mLastHeight = height;
}
});

可以看到先得到当前窗口的显示高度和屏幕的实际高度(窗口部分)


然后先判断窗口显示区域是否变化了,如果没变化则不处理。


如果有变化,则判断变大还是变小了。


如果变小了


说明键盘弹起,这时候显示chatInputPanel,同时设置translationY为-(rawHeight - height)


首先chatInputPanel初始位置底部是与屏幕底部对齐的,虽然设置了setY,但是setY实际上就是setTranslationY,初始位置没变,源码:


public void setY(float y) {
setTranslationY(y - mTop);
}

而弹起键盘后想要显示在键盘以上,那么就需要从最底部向上移动一个键盘的高度,键盘高度就是rawHeight - height,所以向上移动是将translationY设置为-(rawHeight - height)。


如果变大了


说明键盘收起,隐藏chatInputPanel即可。


这样不仅解决了窗口推起的问题,也同时解决了软键盘遮挡部分悬浮栏的问题,因为悬浮栏的位置是通过计算得到的,不是通过软键盘上推导致布局调整而改变位置的。


最终代码


最后想将这个形成一个独立的组件,直接可用,再编写过程中发现出现好多问题,解决所有问题后发现与上面的代码都不一样,不过思路是一致的,只不过细节调整了,比如获取键盘高度等。


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

优雅读取Activity的Intent、Fragment的Argument

属性委托实现方式有两种,这里直接通过实现接口的形式实现: var修饰的属性实现属性委托需要实现ReadWriteProperty接口 val修饰的属性实现属性委托需要实现ReadOnlyProperty接口 这里由于我们只需要读取值,所以直接实现ReadO...
继续阅读 »

属性委托实现方式有两种,这里直接通过实现接口的形式实现:



  • var修饰的属性实现属性委托需要实现ReadWriteProperty接口

  • val修饰的属性实现属性委托需要实现ReadOnlyProperty接口


这里由于我们只需要读取值,所以直接实现ReadOnlyProperty接口即可。下面直接上Activity的Intent委托读取代码 :


class IntentWrapper<T>(private val default: T) : ReadOnlyProperty<AppCompatActivity, T?> {
override fun getValue(thisRef: AppCompatActivity, property: KProperty<*>): T? {
return when(default) {
is Int -> thisRef.intent.getIntExtra(property.name, default)
is String -> thisRef.intent.getStringExtra(property.name)
else -> throw Exception()
} as? T
}
}

需要注意,在这里读取Activity的Intent使用的key默认为属性名称:property.name,这也就意味着通过Intent存储值的时候key也要使用属性名称。


如果需要读写Intent的话key不想要使用属性名称,那就得对这个属性委托类IntentWrapper稍微改造下,构造方法中支持从外部传入key键值


上面的属性委托类IntentWrapper中,只是简单处理了StringInt类型,其他的类型BooleanFloat等类型需要请自行添加。


看下使用:


private val data by IntentWrapper(56)

//读
printlin(data)

上面使用起来还是有一丢丢不优雅,每次都得手动创建IntentWrapper并传入默认值,我们可以封装几个常用类型的方法,实现起来更加方便:


fun intIntent(default: Int = 0) = IntentWrapper(default)

fun stringIntent(default: String = "") = IntentWrapper(default)

intIntent()方法给了一个默认值为0,外部可以选择性传入的默认值,其他的类型也是一样处理。


然后就可以这样使用:


private val data by intIntent()

上面主要展示的是读取Activity的Intent,Fragment的Argument处理类似:


class ArgumentWrapper<T>(private val default: T) : ReadOnlyProperty<Fragment, T?> {
override fun getValue(thisRef: Fragment, property: KProperty<*>): T? {
return when(default) {
is Int -> thisRef.arguments?.getInt(property.name, default)
is String -> thisRef.arguments?.getString(property.name)
else -> throw Exception()
} as? T
}
}

使用起来也和Activity类似,这里就不做展示了。当然了,这里可以定义几个常用类型的方法创建ArgumentWrapper,参考上面Activity的处理即可。


后续准备出一篇文章,从类委托的角度考虑封装下Activity的Intent、Fragment的Argument的读取。


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

解析 InheritedWidget

概览 打开源码可以看到对 InheritedWidget的解释 Base class for widgets that efficiently propagate information down the tree --在UI树上能够有效传递信息的一个基本组件...
继续阅读 »

概览


打开源码可以看到对 InheritedWidget的解释


Base class for widgets that efficiently propagate information down the tree
--在UI树上能够有效传递信息的一个基本组件。

我们可以使用这个类来定制一个组件来传递我们需要在整个界面的状态传递。可以在官方的介绍上看到一个案例:


   ###### 创建一个继承InheritedWidget的组件

其中我们会使用一个惯例的写法,创建一个 static 的方法 of 去获得他对应的InheritedWidget。此方法会间接的调用 BuildContext的


dependOnInheritedWidgetOfExactType方法,范型传入我们当前的 FrogColor组件。


///创建一个InheritedWidget的继承类,用于管理 color 信息的传递
class FrogColor extends InheritedWidget {
 const FrogColor({Key key, @required this.color, @required Widget child})
    : assert(color != null),
       assert(child != null),
       super(key: key, child: child);
 final Color color;
 ///一般的写法,官方推荐
 static FrogColor of(BuildContext context) {
   ///这个方法做了什么事情,待会会进行分析解释!
   return context.dependOnInheritedWidgetOfExactType<FrogColor>();
}
 @override
 bool updateShouldNotify(covariant FrogColor oldWidget) {
   return color != oldWidget.color;
}
}

当然我们有时候也可能会直接使用一个Wiget去包裹一个InheritedWidget去传递组件的状态,典型的一个就是Theme组件,参考Theme的源码,我们知道他内部包裹了 _InheritedTheme 和 CupertinoTheme 并且 在 他的字组件中传递了 ThemeData 。所以我们能够很方便的全局的去修改我们的主题。


使用这个InheritedWidgt进行信息的传递

这里我们使用了一个Builder来创建需要使用FrogColor配置信息的子组件。其实我们使用的这个context就是一个Element,这个Element属于FrogColor的Element中的子Element。这一点很重要(关于context的知识需要参考下 Builder的源码)。


class MyPage extends StatelessWidget{
 @override
 Widget build(BuildContext context) {
      return Scaffold(
          appBar: AppBar(),
          body: FrogColor(
            color: Colors.green,
            child: Builder(builder: (BuildContext context){
              //这里我门打印了一下context 。 Builder(dirty, dependencies: [FrogColor])
              //这里打印的是 Widget的一些信息,其实最终调用的是Element的 toString 方法
              /// @override
///     String toStringShort() {
///     return widget != null ? widget.toStringShort() : '[${objectRuntimeType(this, 'Element')}]';
/// }
              print(context);
              print(context as Element) ///并不会报错
              return Text("This is InnerText" ,style: TextStyle(
                color: FrogColor.of(context).color
              ));
            }),
          ),
      );
}
}

为什么我们需要将需要FrogColor信息的组件外面包裹一层Builder,因为 FrogColor.of(context) 这个context 必须是 FrogColor的子 context


(其实是Element),若是使用 MyPage的context , 这个context 位于FrogColor的父级,那么我们需要的效果肯定会达不到的。


为什么会出现这样的效果,我们应该从 我们 惯例定义的 static 方法 中说起。


dependOnInheritedWidgetOfExactType


这个方法我们调用的是 BuildContext.dependOnInheritedWidgetOfExactType , 其中唯一实现这个方法的类就是 Element,我们看 Element


中的这个方法的实现:



  1. 去 _inheritedWidgets 中找输入范型对应的 InheritedElement,对应 FrogColor中的Element。在FrogColorElement创建的时候会将其加入到 _inheritedWidgets 中。

  2. 在当前 Builder对应的Element中的_dependencies加入 找到的 FrogColorElement ,这样两者就建立了相关联系。

  3. ancestor.updateDependencies(this, aspect) 在 FrogColorElement更新依赖关系。



///from Eelement
@override
T dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object aspect}) {
 assert(_debugCheckStateIsActiveForAncestorLookup());

 //还记得我么给 这个 方法传入了一 FrogColor的范型。这里 _inheritedWidgets 将类型作为Key 查找 对应的 Element;
 /// _inheritedWidgets 这个 变量 会在Element 的 mount 方法 和 active 方法中调用,就是:如果当前的Elemnt属于InheritedElement便将此Element放入 _inheritedWidgets 中,不是的话向上追溯加入。
 final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
 ///查到的情况下
 if (ancestor != null) {
   assert(ancestor is InheritedElement);
   /// 关键点 : 添加依赖关系
   return dependOnInheritedElement(ancestor, aspect: aspect) as T;
}
 _hadUnsatisfiedDependencies = true;
 return null;
}

/// from Element 添加依赖关系
@override
 InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }) {
   assert(ancestor != null);
   _dependencies ??= HashSet<InheritedElement>();
   ///在本Element的依赖链中添加,一切的操作都在本Element中操作而非 FrogColor对应的Element中,
   ///这也是为什么我们在前面的打印中看到了 : Builder(dirty, dependencies: [FrogColor])
   _dependencies.add(ancestor);
   ///刷新依赖关系(这个依赖关系刷新的是 FrogColor中的依赖关(这个一会再讲)
   ancestor.updateDependencies(this, aspect);
   return ancestor.widget;
}

分析到这一步,其实我们便可以使用 InheritedWidget提供的一些属性信息,但是当我们更新数据的时候(比如:FrogColor 动态更新 color属性),若是子组件的Widget属性没有发生变化(参考 setState()原理)的时候,子组件是不会发生变化的。也就是说只能在第一次构建的时候生效,那么我们全局修改主题,全局修改 ForgColor 的属性怎么办。我们需要解析 ancestor.updateDependencies(this, aspect) 内在都干了什么,怎么运行的。


InHeritedWidget 动态更新


经过以上的分析,我们知道,在 添加依赖关系的时候,会分别在自己的 _dependencies 中添加父级,然后在父级中的 _dependencies 添加 调用的Element。为什么要添加这种依赖关系?我们应分析当 Element更新的时候对这种依赖关系操作了什么。


熟悉Element更新原理的情况下(不知道顺着思路走)我们知道:属性更改后会调用update方法。而 InheritedElement 继承在 ProxyElement ,我们来分析 ProxyElement(这个类里面包含了通知客户端更新的定义方法) 中的update 做了什么。


/// from ProxyElement
@override
void update(ProxyWidget newWidget) {
 final ProxyWidget oldWidget = widget;
 assert(widget != null);
 assert(widget != newWidget);
 super.update(newWidget);
 assert(widget == newWidget);
 ///调用 updated 方法
 updated(oldWidget);
 _dirty = true;
 rebuild();
}
@protected
void updated(covariant ProxyWidget oldWidget) {
  /// 通知Clients ;   这个方法在ProxyElement中是空的
   notifyClients(oldWidget);
}

ProxyElement最终会调用 notifyClients方法,这个方法在 InhertedElement中进行了实现。



/// from InhertedElement
 /// Notifies all dependent elements that this inherited widget has changed, by
 /// calling [Element.didChangeDependencies].
@override
 void notifyClients(InheritedWidget oldWidget) {
   assert(_debugCheckOwnerBuildTargetExists('notifyClients'));
   //查找所有的依赖关系
   for (final Element dependent in _dependents.keys) {
     ///断言==一些判断。
     assert(() {
       // check that it really is our descendant
       Element ancestor = dependent._parent;
       while (ancestor != this && ancestor != null)
         ancestor = ancestor._parent;
       return ancestor == this;
    }());
     // check that it really depends on us
     assert(dependent._dependencies.contains(this));
     ///通知依赖
     notifyDependent(oldWidget, dependent);
  }
}
}

/// from InhertedElement
 @protected
 void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
   ///调用 element的 didChangeDependencies 方法;
   dependent.didChangeDependencies();
}

 ///from Element
 @mustCallSuper
 void didChangeDependencies() {
   assert(_active); // otherwise markNeedsBuild is a no-op
   assert(_debugCheckOwnerBuildTargetExists('didChangeDependencies'));
   /// 标记需要重新进行绘制;
   markNeedsBuild();
}


总结一句话:重新构建的时候,InheritedElement将_dependencies中的ELement全部标记为 重新构建。这样,当 InheritedWidget的属性发生变化的时候,便会很快的有目标的将使用到它属性的Widget进行更新。


这也是为什么很多基于组件基于InheritedWidget,这样的效率很高,而且使用起来很方便。一些状态管理框架也是基于InheritedWIdget进行了一定的封装。


PS:_inheritedWidgets的注册和继承


上面有一点我们是一句话带过的 :在FrogColorElement创建的时候会将其加入到 _inheritedWidgets 中。 这里我们需要分析 Element的mount方法中执行了什么操作;


/// from Element 
@mustCallSuper
void mount(Element parent, dynamic newSlot) {
  ////---省略
 ///最终调用
 _updateInheritance();
 assert(() {
   _debugLifecycleState = _ElementLifecycle.active;
   return true;
}());
}

 /// from Element 若不是InheritedElement的情况下,会查找父级的 _inheritedWidgets
 void _updateInheritance() {
   assert(_active);
   _inheritedWidgets = _parent?._inheritedWidgets;
}

/// from InheritedElement 若是InheritedElement的情况下
@override
 void _updateInheritance() {
   assert(_active);
   /// 将父级的拿过来
   final Map<Type, InheritedElement> incomingWidgets = _parent?._inheritedWidgets;
   if (incomingWidgets != null)
     _inheritedWidgets = HashMap<Type, InheritedElement>.from(incomingWidgets);
   else
     _inheritedWidgets = HashMap<Type, InheritedElement>();
   /// 加入自己
   _inheritedWidgets[widget.runtimeType] = this;
}

总结:通过 _updateInheritance 查找上级的 _inheritedWidgets 若本Element是InheritedElement那么将自己加入进去。


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

对话MySQL之父:中国程序员都想转型做管理,这是最大错误

MySQL之父:代码应该一次写成而不是后面再改在开源数据库领域中,Michael “Monty” Widenius(通常称为Monty)绝对是不得不提的代表人物。有着四十多年编程经验的Monty是MySQL和MariaDB的作者,也是开源软件运动的著名倡导者,...
继续阅读 »

编者按:MySQL之父Monty有着四十多年的编程经验,从儿时的兴趣到长大后的深耕,他在编程领域不断钻研,最终成为编程大师。《新程序员004》带你走进Monty的程序人生,谈谈他在编程方面的最新感悟以及对未来的预测。

MySQL之父:代码应该一次写成而不是后面再改

如今,我们正处于数据爆炸的时代,软件崛起的背后是数据的支持。而随着开源技术的发展,越来越多的数据库选择创建开源社区,让更多开发者参与到数据库的建设中来。

在开源数据库领域中,Michael “Monty” Widenius(通常称为Monty)绝对是不得不提的代表人物。有着四十多年编程经验的Monty是MySQL和MariaDB的作者,也是开源软件运动的著名倡导者,即便是现在他也在坚持写代码。作为影响了几代技术人的数据库,MySQL所取得的成就无需多言。而最初作为MySQL分支立项的MariaDB也在迅速成长,同样在数据库中赢得了一席之地。

作为在技术界游历半生的资深“程序员”,Monty对编程的理解也有许多独到之处,他认为只有学习编程20年以上,才能像读懂音乐一样,看出编程之美。除此之外,他还表示:“写代码时要尽量将代码一次性写成,而不是写完后再没完没了的修改。”只有做到这一点,才能称得上是一名优秀的程序员。而这也是他长久以来所遵循的“编程法则”。

近期,《新程序员》有机会邀请Monty分享他的程序人生,谈谈他对于技术的感悟,以及对于数据库发展的看法与心得。

“我在编程方面有一定的天赋”

1962年,Monty出生在芬兰首都赫尔辛基,小时候的他便对计算机有着浓厚的兴趣。1978年,年仅16岁的Monty用他一整个暑假打工攒的钱买了人生中的第一台电脑,并且用BASIC语言写下了第一行代码REM,从此以后他便与编程结下了不解之缘。三年后,Monty被北欧著名高校赫尔辛基理工大学录取,但由于自己的学习理念与学校不同,他感到在学校学不到什么东西,因此没过多久就辍学了。1981年。离开了校园的Monty开始在荷兰的一家叫做Tapio Laakso Oy的公司当程序员。在近十年之后,34岁的Monty开发出了历史上最流行的开源数据库之一——MySQL。


Monty能开发出MySQL并非偶然,他在编程上投入了大量的时间。根据早期的资料显示,就连别人去参加聚会时,他也在家里写代码。在他看来,好的代码不需要一次又一次地重写,而是在开始写之前,就抱有一次写成的心态。正因为如此,直到多年后的今天,Monty仍然直言“自己在编程方面具有一定的天赋。”

除了Monty,MySQL的诞生还离不开David Axmark和Allan Larsson。早在1980年,17岁的Monty打算将自己的计算机内存从8KB提高到16KB。机缘巧合之下,他去往瑞典Allan Larsson的电脑店寻求帮助,在那里认识了同样也是写代码的David Axmark,之后三人就成为了亲密的合作伙伴,经常一起写代码,解决编程过程中遇到的问题。1995年,三人创立了MySQL AB,MySQL AB就是MySQL的雏形。这其中Monty负责了大部分的开发工作。最终,在1996年10月,MySQL首个版本发布,从此掀开了数据库历史的重要一章。

到了1999年,MySQL的迅速发展已经引起了许多人的注意, Oracle表示要以5000万美元的价格收购MySQL。然而Monty三人并不想止步于此,也不想失去对MySQL的控制,因此拒绝了这次收购。

随着时间的推移,MySQL迅速发展,但同时市场上也出现了包括PostgreSQL在内的竞争对手数据库。为了在竞争中脱颖而出,MySQL开始接受融资,以获得更大的发展机会。到了2003年,MySQL实现了高达400万的安装次数,较两年前翻了一番,成为了当时全世界最受欢迎的开源数据库。

2008年1月16日,Sun Microsystems以高达10亿美元的价格收购MySQL(然而次年Sun又被Oracle收购)。当时Monty担心MySQL可能会受到Oracle的控制而变得商业化,并且如果Oracle一家独大的话,可能会引发数据库领域的不良竞争。于是他发起了一场拯救MySQL的请愿活动,并在MySQL闭源前将其分化,以其小女儿Maria的名字命名创建了MariaDB。

MariaDB开源数据库可以看做是MySQL的一个分支,主要由开源社区维护,目的是要完全兼容MySQL,甚至包括API和命令行。MariDB推出后,不少MySQL的员工都转而投向MariaDB,甚至是原先使用MySQL的各大公司也将数据库迁移到MariaDB上,其中就包括谷歌和维基百科。Monty表示:“与MySQL相比,MariaDB更加成熟,拥有更大的研发优势,并且在安全性修复方面也更加出色。”直到现在,Monty依旧亲自参与MariaDB的开发维护,可以说他的工作重心都在MariaDB上。

MariaDB,坚持开源的背后

《新程序员》:你在创建MariaDB时,曾提到要把它打造成第二个MySQL,并且确保它是开源的。那么对于数据库而言,为什么开源这么重要呢?

Monty:对于任何大型项目来说,开源都是非常重要的。既然要和巨头竞争,你就要有和他们一样的工具。在我看来,开源很适合用于软件开发,尤其是当公司规模还不大的时候。这个时候你很难兼顾公司和用户的需求,因此需要听取别人的想法。而开源就意味着可以获得社区的帮助,能够了解其他人的观点。有了开源,你可以开发出更好的产品,同时产品也能够获得更大的影响力。

《新程序员》:不过开源的一大弊端就是声音太多,需求不一,这种情况下该如何保证数据库能满足大多数人的需求呢?

Monty:要解决这个问题,就需要确保数据库足够灵活,这样才能满足大多数人的需求。在这一点上,MySQL和MariaDB的做法是建立各种性能不一的存储引擎,人们可以针对具体需求开发自己的存储引擎 。

事实上,对于那些有需求的人来说,MariaDB依旧是一个优秀的工具。而对于要求数据库体量较小且运行较快的人来说,MariaDB同样是一个不错的选择。在开发MariaDB时,我们考虑到了各种可能性,使它能够保持良好的性能。

《新程序员》:AI技术的发展让人们对数据库的期待发生了转变,今天数据库是否能够与AI技术结合,从而拥有数据决策能力?

Monty:对于数据库来说,最重要的是要处理AI需要的不同结构。因此我们添加了对JSON的支持,用于在MariaDB中支持动态列。这样人们就可以储存并检索数据,同时保留自己想要的格式。通常AI并不是要创造内容,更多的是实现文件自动化,这就是我们对于MariaDB所抱的期望。因此这两者完全是不同的工具集。

除此之外,我们还需要一个良好的环境,其中每一个部分都是可替代的,要确保自己不被束缚。一旦有了束缚的存在,那么你的应用程序就需要与静态系统相结合,这会大大降低灵活性。我认为对于数据库来说,要注意的一点就是,要确保数据库容易上手,而这恰恰意味着更多的AI技术能够整合到数据库中。

仍然每天坚持写代码

《新程序员》:在IT行业中,在中国有这样一种现象,认为程序员过了35岁就要转型,进入管理层或是其他领域。对此你怎么看?

Monty:这在很多地方都很常见。这个现象的主要原因在于程序员在管理岗位上的工资要比单纯做编程高。因为很少有公司会重视优秀的程序员,这就导致了收入的差异。我认为,如今程序员没有晋升的空间。与其让他们被迫转型,不如建立一个能提升他们收入的新环境。要想做到这一点,公司就得让他们承担更多的责任。要程序员担任管理岗位也行,但前提是仍然要保证他们每天写代码的时间。毕竟好的经理人到处都是,好的程序员却千里挑一。

《新程序员》:据我所知,你仍然每天在坚持写代码,但同时也要负责MariaDB的运营和管理。那么,你如何平衡这两个身份呢?

Monty:我认为在写代码这方面,我还是有一点天分的,所以我想坚持下去。我会雇用经理人为我工作,这样我就可以做我最擅长的事情。我会参与代码审查、社区运营以及MariaDB的相关决策。但同时我也会花很多时间维系客户,与不同国家的开发者交流,其中有许多中国的开发者。我认为,除了写代码之外,这是我做的最重要的事。总而言之,我会雇佣经理人来做一部分管理,让我有足够的时间在真正重要的事情上。

《新程序员》:听闻你从20世纪80年代就开始在家办公,如今这一办公方式也开始流行起来,对于远程办公你有什么看法?

Monty:事实上我认为远程办公是非常灵活的工作方式,自1981年开始我就在家办公(MySQL和MariaDB团队都是在家办公)。我们招人之前可能从来没见过他们,甚至都不知道对面是个人还是团队。但是我们的效率一直都在线。能做到这一点的前提,是要对跟自己联系密切的同事有足够的了解。至少熟悉他们的样貌。

我认为对于八成的开发者而言,在家办公是一个不错的选择。可能有一小部分开发者,他们的工作负担比较重,在家提不起精神来。这就需要他们出去走走,见见朋友或是接触新事物。我刚开始在家办公的时候,也会担心这样是不是会被孤立。所以后来我会定期在家里举行派对,我也会亲自下厨。我们团队每年也会在一起待上一段时间。

一个好的程序员能抵五个一般的程序员

《新程序员》:对于你来说,在过去几年数据库领域发生了哪些大的变化?

Monty:在过去的五年或七年间,学习SQL(结构化查询语言)开始成为一种趋势。但是人们发现SQL过于复杂,因此还需要学习其他语言。于是许多公司开始创新,采用NoSQL(非关系型数据库) 进行开发。但在过去的几年里,人们逐渐意识到NoSQL并不是万金油。但选择关系型数据库是否能够涵盖NoSQL提供的功能?很明显,有的可以 ,有的不行。因此我认为,在当下的环境中,对于数据库的要求在于要保证云端以及本地部署。

我们不能被一个数据库束缚。云端提供的是灵活性,你能在数据库中运行软件,即使是有成百上千个软件,而且本地部署的价格更低,控制权限更高,这一点是云端无法提供的。但我依然认为云端有它的优势,我们要在两者之间找到平衡。

《新程序员》:30年前我从大学毕业时,人们提到数据库一般是指去银行办业务。现在看来,人们有了更多的选择,我们能够借助数据库实现许多功能。但提到数据库开发时,人们往往指的是“后端”。那么,对于一个开发者或是毕业生想要进入数据库领域的人来说,你会给他们怎样的职业建议?

Monty:在我看来,从开源数据库开始入门更简单。现在开源数据库很多,如果你的确想成为专家级别的人,想要得到一份很好的工作,你可以找一个合适的数据库,并学习如何进行优化。但同时你也需要了解人们的需求,你可以和从事这一行的同学交流,并且学习解决数据库中的实际问题。

《新程序员》:除了多参与开源项目之外,对于中国开发者你还有哪些想说的?

Monty:我和来自中国的开发者有过非常多的互动,他们非常棒,在编程上表现得非常优秀。不过我在感到惊喜得同时,也感到非常惋惜,因为他们都想转型做管理。我认为这是最大的错误。他们需要让老板给自己派更多的任务,当然也可以做管理,但前提是能让自己写代码。还是那句话:找到一个好经理很容易,但找到一个好的程序员很难。一个非常出色的程序员可以抵五个一般的程序员,关键是你想当一个好的程序员还是一个平庸的经理。对于所有中国开发者,我只想说,请坚持你的工作,你已经做得非常好了,一定不要停止写代码。

【参考资料】
https://zh.wikipedia.org/wiki/%E7%B1%B3%E5%8D%A1%E5%9F%83%E7%88%BE%C2%B7%E7%B6%AD%E5%BE%B7%E7%B4%90%E6%96%AF
https://blog.openocean.vc/founder-stories-a-hackers-hacker-6d5054c90564
https://huskyintelligence.com/leverage-open-source-code/
http://monty-says.blogspot.com/2009/12/help-saving-mysql.html
https://www.geeksforgeeks.org/introduction-of-mariadb/
http://www.josetteorama.com/from-mysql-to-mariadb-michael-%e2%80%9cmonty%e2%80%9d-widenius-talks-about-databases-and-his-projects/
https://dri.es/the-history-of-mysql-ab
https://mariadb.org/wp-content/uploads/2019/11/MySQL-MariaDB-story.pdf

来源:blog.csdn.net/programmer_editor/article/details/124173842

收起阅读 »

十年积累,5.4 万 GitHub Star 一朝清零:开源史上最大意外损失

我们找 GitHub CEO 求助,但为时已晚。2022 年 2 月 15 日,GitHub 通过推特平台广播了一则消息:「我们的朋友 HTTPie 最近不小心将自己设为了私密,丢掉了所有的 Star。如果你仍然爱它,就给它一颗 Star 作为情人节礼物。」1...
继续阅读 »

我们找 GitHub CEO 求助,但为时已晚。


2022 年 2 月 15 日,GitHub 通过推特平台广播了一则消息:「我们的朋友 HTTPie 最近不小心将自己设为了私密,丢掉了所有的 Star。如果你仍然爱它,就给它一颗 Star 作为情人节礼物。」


10 年攒下的 Star 突然清零?这是怎么回事?

昨天,项目作者 Jakub Roztočil 在博客中正式回应了这一事件。

十年获得 5.4W Star 的开源项目

HTTPie 项目的第一次提交还是在十年之前。

可能一些人对这个项目不够熟悉,这是一个开源 CLI HTTP 客户端。团队从头开始构建了它,以使终端的 API 交互尽可能人性化。

HTTPie(发音为 aitch-tee-tee-pie)可用于测试、调试以及通常与 API 和 HTTP 服务器交互。http&https 命令允许创建和发送任意 HTTP 请求。它们使用简单自然的语法,并提供格式化和彩色输出。

从 2012 年 2 月 25 日在哥本哈根的第一次公开提交之后,项目作者 Jakub Roztočil 就一直在 GitHub 平台上托管该项目。他自己也是 GitHub 平台的忠实粉丝。

这些年来,HTTPie 逐渐成长为平台上最受欢迎的 API 工具,收获了 5.4W 个 Star 和 1k 的关注。

HTTPie 项目受益于 GitHub 的「social coding」功能,同时,GitHub 也受益于自身平台上托管的这个受欢迎的项目。在过去十年中,可能有数百万开发人员访问了 HTTPie 项目的 GitHub 页面。

但在几周前,HTTPie 项目积累的 5.4W Star 一夜清零。

在这篇博客中,项目作者 Jakub Roztočil 详细介绍了事情经过:

发生了什么?

我不小心将项目的 repo 设为了私有,GitHub 级联删除了我们花费 10 年时间建立的社区。

这意味着什么?

如果你是一位下游维护者,或者曾经关注过 HTTPie 以获取通知,你可能需要重新关注一下 repo。

Star 也是一样,如果过去十年里你曾为该项目加注 Star,那现在 HTTPie 应该已不再是你的 Star 项目列表中的一员。

为什么要将 repo 设为私有?

将 repo 设为私有会永久删除所有关注者和 Star,这是 GitHub 的一个特性。我知道这一点,而且我显然无意 httpie/httpie 隐藏。

最直接的原因是我认为我在另一个 repo 中——一个没有内容且 0 Star 的项目。我真正打算做的是隐藏 HTTPie 组织的配置文件 README,这是我在一周前创建但没有机会填充的。

让我走上错误道路的是一个完全不相关的操作:我刚刚在我的个人资料上做了同样的事情(即隐藏了一个空的 README),将其设为 jakubroztocil/jakubroztocil 私有。

在配置文件和存储库方面,GitHub 的概念模型会将用户和组织视为非常相似的实体。在这种情况下,由于我只是想在我们组织的个人资料上重复相同的操作,我的大脑切换到了「自动驾驶」模式。

目前我没有意识到这个包含配置文件 README 的特殊 repo 的命名存在不一致问题,并且它对于用户和组织有所不同:name/name 与 name/.github.

这就是为什么我一开始要隐藏 httpie/httpie,而不是 httpie/.github,并且没有意识到我的错误。

但是,还有一个确认流程?

确实有一个确认框,旨在阻止像我这样的情况下的用户做一些愚蠢的事情。它会告诉你「你将永远失去这个存储库的所有 Star 和关注者」。

问题在于,对于没有提交和任何 Star 的 repo ,它的提示框和具有 10 年历史及 55k Star 与关注者的 repo 是完全一样的。它说的是:「警告:这是一个潜在的破坏性行动。」

套用一句话:一个盒子告诉你「你要拆房子!如果里面有人,他们都会死。」但如果你混淆了地址并认为你正准备拆的是一个空房子,那后果将不堪设想。

实际上,此处的对话应该更加结合上下文,并且再次解释一下情况,比如说「你即将杀死 55000 人。」那肯定会让我停下来。

一番操作之后

当我回到组织页面时,你可以想象我的困惑,我不仅仍然可以看到空的 README,同时我们最受欢迎的 repo 找不到了。片刻之后,我意识到发生了什么事。所以我回到 repo 的设置来翻转开关。但 GitHub 不允许我这样做——整整半个小时。

为什么这么久呢?因为这是 GitHub 级联删除我们 10 年来的 Star 和关注者所花费的时间。而且我没有办法阻止这个过程。我所能做的就是开始发消息给 GitHub 寻求支持,刷新页面并等待 Star 数量达到零,然后才能再次将其公开。

为什么 GitHub 不给我们恢复呢?

GitHub 显然有备份,并且有恢复 repo 的方法。GitHub 团队曾经自己不小心将 GitHub 桌面应用程序 repo 设为私有,然后他们在几个小时内就恢复了一切,当时前 GitHub CEO 给出的解释是:

然而,在我们的事件中,他们拒绝这样做,理由是操作具有不良影响,并且会消耗资源成本。我们甚至提出为所需的任何资源提供经济补偿,但遗憾的是,他们还是拒绝了。相对于在 GitHub 上恢复最受欢迎的社区项目之一以外,他们还有其他优先事项。

因此,GitHub 恢复存储库的前提是他们自己的项目,而不是社区项目。

经验教训

这次危机让我们得到了很多教训,这里主要分享 3 点:

教训 1:UI/UX 设计

弹出的对话框要清晰明了,减少抽象的文字说明。以一种不需要用户思索的方式设计确认对话框。当用户要删除或损坏某些文件时,不要用抽象的语言描述,以免让用户难以了解即将发生的状况,特别是会造成级联删除的行为。例如,以下是我们在 HTTPie for Desktop 中的处理方式:

对话框需要反映操作影响的严重性。当完全没有额外影响时,对话框应该尽量简单,否则会浪费用户有限的注意力,从而降低用户的敏感度:

教训 2:数据库设计

使用软删除(soft-delete)。人非圣贤,孰能无过。人们在删除操作上可能会犯错误,因此应该尽可能使用软删除,延迟硬删除(hard-delete)操作。

教训 3:与 GitHub 的关系

这是我们的人为错误,GitHub 明确表示他们没有法律义务帮助我们。我们长达十年的互惠互利关系是根据 GitHub 的服务条款确定的,除此之外,再无其他。

毕竟,GitHub 有过有争议的行为,这些行为违背了开源社区的精神。微软(已收购 GitHub)尽管拥有一定的开源精神,但并不总是有很好的声誉。

我们希望 GitHub / 微软 有朝一日能够改变他们的态度,并恢复我们的项目社区,他们拥有实现这一点的所有数据和方法。我们也希望他们改进 UI 和数据库设计,以防止这种情况未来发生在其他团队身上。

最后,尽管我们的 GitHub star 量化为虚无,但 HTTPie 现在发展得非常好,从最初作为一个副项目到现在变成了一家公司,我们的团队正在将 HTTPie 发展成一个 API 开发平台。用于 Web 和桌面的 HTTPie 私有测试版收到了很好的反馈,我们迫不及待地想在接下来的几周内公开发布它。

参考链接:
https://httpie.io/blog/stardust

来源:https://mp.weixin.qq.com/s/wbXgE9t0YSI2UKyj070V_g

收起阅读 »

华为员工利用公司系统 Bug 越权访问机密数据,引发网友热议

网友评论堪称“人间清醒”对此案件,不少网友纷纷表达了自己的看法,其中不乏科技大厂里设计技术层面的员工及程序员群体。我们看到有网友在热搜话题下方评论称:“感觉就是幸亏这个人收受的贿赂金额没有太过。如果收受的金额太大,估计华为也不会出具谅解书了,而且判刑也会更重。...
继续阅读 »

4 月 12 日,一条#华为员工越权访问机密文件被判刑#的话题冲上了微博热搜榜头条,由此引发了广大网友关于该事件的激烈讨论。





网友评论堪称“人间清醒”


对此案件,不少网友纷纷表达了自己的看法,其中不乏科技大厂里设计技术层面的员工及程序员群体。


我们看到有网友在热搜话题下方评论称:


“感觉就是幸亏这个人收受的贿赂金额没有太过。如果收受的金额太大,估计华为也不会出具谅解书了,而且判刑也会更重。


“不管是在哪个行业,建议各位多注意一下保密的问题,平常的一些小问题可能不会有人追究,但是像这种直接把公司的机密直接透露给供应商就有点太离谱了,最骚的是这人用的还是公司邮箱?”


“去窃取公司机密数据,还天真的认为公司不知道吗?实在是得不偿失!”


“虽然华为公司谅解了,但是触犯了刑法,还是会按照法律程序进行审判。”


“既然已经调离岗位了,就直接离开就行...但这位员工则是未清理 ERP 登陆信息,然后利用bug 越权访问,并将华为多个供应商的信息发送给第三方,最后被判处有期徒刑一年。”


“先不说越权访问机密数据这件事,咱就是说就为了 1.6 万元,就把自己公司的信息出卖给别的公司,这也是违法的啊。”


“作为华为的员工,你说一年挣多少钱,你差这 1.6 万块钱吗?不理解。又不是几十万或者几百万,就获利 1.6 万元,真的是因小失大。”


“越权访问公司机密数据?如果只是单纯的越权访问公司数据,那也就是违反公司规定,顶多被辞退......关键是,你利用越权的数据进行违法交易,这就是不可容忍的了。”


......



案件回顾:非法牟利16437.6元,真“得不偿失”


据中国裁判文书网披露,该“华为员工利用公司系统 Bug 越权访问机密数据”的案件显示:


2010 年 12 月,Yi(音)某从华为公司线缆物控部调任后,未按华为公司的要求将 ERP 账户线缆类编码物料价格的查询权限清理。直至 2017 年底,Yi 某违反规定多次通过越权查询、借用同事账号登录的方式在 ERP 系统内获取线缆物料的价格信息。


2016 年 12 月 27 日- 2018 年 2 月 28 日期间,Yi 某又多次通过公司邮箱将华为多个供应商共 1183 个(剔除重复部分共 918 个)线缆类编码物料的采购价格发送给金信诺公司。


在 2012 年- 2017 年 6 月 30 日期间,Yi 某收受金信诺公司购物卡共计 7000 元、篮球鞋 5 双(价值共计人民币 16437.6 元)。


在案发后,华为公司出具了谅解书,表示对Yi某侵害华为公司的行为予以谅解。


而作为被卷入其中的华为供应商、上市公司,金信诺证券部人士则透露称,早在 2018 年就已对内部涉事员工的违纪行为进行了处理,且“这仅是个人行为。”


对此案件,最终法院以“非法获取计算机信息系统数据罪”将 Yi 某判处有期徒刑一年,并处罚金2万元,同时依法追缴其违法所得 23437.6 元上缴国库。



华为:每年都要处理员工受贿问题


一直以来,企业员工受贿的现象一直备受关注,且屡禁不止。据了解,#华为员工越权访问机密文件被判刑#此次事件的情况貌似也并非个例。


据相关报道,华为每年都会处理许多类似“员工受贿”的情况。


早在 2021 年 5 月份的“华为2021中国生态大会”上,时任华为轮值董事长徐志军就曾谈到“华为每年都要处理很多员工受贿案件,有些人甚至会被判拘留或监禁”。而当这些员工的亲属向公司求饶并希望华为原谅他们时,作为东家华为也非常尴尬。


因此,华为在每次的公开场合都会向合作伙伴强调“华为应该与合作伙伴共同营造一个健康、诚信的合作环境。与此同时,华为也在加大内部监管的力度,改进工作流程和机制,尽可能减少此类事情的发生概率。


任正非曾表示,“华为维持生存的根本就是不能腐败。”不能让腐败阻碍公司的发展,更不因为发展而宽容腐败,这需要每个岗位每个人的监督。


据了解,就在去年那次大会举办之前,华为就已经通过了相关决议,严格要求华为所有员工,尤其是高级管理人员和退休高级管理人员,不能利用现在和过去的职位之便,来帮助他们的亲友成为华为的合作伙伴、代理商或供应商。



专家建议:尽快建立严格监管制度


众所周知,世界从信息时代迈进智能时代的过程中,数据是开启新时代大门的关键“资产”。特别是对于华为这样在全球领域都极具影响力的科技企业来说,数据更是安全红线。




当然,此次案件的发生也暴露了一些问题,由于其中涉及到侵犯商业秘密罪,因此也引发了不少法律界专家学者的关注。有专家建议,需要尽快建立更严格的监管和安全制度,以杜绝此类问题再发生。


一直以来,华为都非常重视数据安全。对于任何一位合格的科技巨头的员工,都必然该知道什么内容可以访问,什么内容不能访问。否则,一旦越线,就会面临严重后果,甚至面临“牢狱之灾”。

来源:https://mp.weixin.qq.com/s/M55FUYp6r7o65X_joYXvDQ

收起阅读 »

用compose撸一个雷达图

介绍项目中需要使用雷达图来展示各个属性的不同比例,文字根据控件大小自动换行。效果图如何实现1、绘制背景的三个圆形从外圆向内圆绘制,这样内圆的颜色正确覆盖在外圆上,style = Stroke(2f)用来绘制圆形的border。val CIRCLE_TURN =...
继续阅读 »

介绍

项目中需要使用雷达图来展示各个属性的不同比例,文字根据控件大小自动换行。

效果图

untitled.gif

如何实现

1、绘制背景的三个圆形

从外圆向内圆绘制,这样内圆的颜色正确覆盖在外圆上,style = Stroke(2f)用来绘制圆形的border。

val CIRCLE_TURN = 3
val center = Offset(size.width / 2, size.height / 2)
val textNeedRadius = 25.dp.toPx() // 文本绘制范围
val radarRadius = center.x - textNeedRadius
val turnRadius = radarRadius / CIRCLE_TURN

for (turn in 0 until CIRCLE_TURN) {
drawCircle(colors[turn], radius = turnRadius * (CIRCLE_TURN - turn))
drawCircle(colors[3], radius = turnRadius * (CIRCLE_TURN - turn), style = Stroke(2f))
}

2、绘制圆环内的虚线

使用360/data.size算出每个区块需要的角度。

我们知道,竖直向上为-90度,当区块数量为奇数时,第一条虚线应当在竖直方向上,即起始绘制角度为-90度;当区块数量为偶数时,虚线绘制应当左右对称,所以将初始角度设置为-90 - itemAngle / 2inCircleOffset()是用来获取在圆形中的xy位置,点击kotlin.math.cos()/sin()查看方法的描述Computes the cosine of the angle x given in radians可知,我们需要传入一个弧度,角度换算弧度的推导如下。

image.png

val itemAngle = 360 / data.size
val startAngle = if (data.size % 2 == 0) {
-90 - itemAngle / 2
} else {
-90
}

for (index in data.indices) {
// 绘制虚线
val currentAngle = startAngle + itemAngle * index
val xy = inCircleOffset(center, progress * radarRadius, currentAngle)
drawLine(colors[4], center, xy, pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f)))
}

/**
* 根据圆心,半径以及角度获取圆形中的xy坐标
*/
fun DrawScope.inCircleOffset(center: Offset, radius: Float, angle: Int): Offset {
return Offset((center.x + radius * cos(angle * PI / 180)).toFloat(), (center.y + radius * sin(angle * PI / 180)).toFloat())
}

3、绘制雷达范围

在最大值为100的情况下,根据bean的value换算出应当绘制点的radius。并算出对应的xy的位置,将其记录到path中方便连成闭合区间绘制。

data class RadarBean(
val text: String,
val value: Float
)
for (index in data.indices) {
val pointData = data[index]
val pointRadius = radarRadius * pointData.value / 100
val fixPoint = inCircleOffset(center, pointRadius, currentAngle)
if (index == 0) {
path.moveTo(fixPoint.x, fixPoint.y)
} else {
path.lineTo(fixPoint.x, fixPoint.y)
}
}
drawPath(path, colors[5]) // 绘制闭合区间
drawPath(path, colors[6], style = Stroke(5f)) // 绘制区间的深色描边

4、绘制文字位置

接下来就是绘制最重要的文字的位置啦,首先我们先了解什么是StaticLayout,这里面有1.4小节介绍StaticLayout是如何使用的。 image.png 观察效果图,我们先分析出位置的绘制规律:

  1. 垂直方向的文字x轴在文字宽度的正中间,y轴在文字的底部
  2. 水平方向的文字x轴与y轴皆在文字的正中间
  3. 左上角的文字x轴在文字的最右边,y轴在最后一行文字的中间
  4. 右上角的文字x轴在文字的最左边,y轴在最后一行文字的中间
  5. 左下角的文字x轴在文字的最右边,y轴在第一行文字的中间
  6. 右下角的文字x轴在文字的最左边,y轴在第一行文字的中间

根据以上规律,需要对文字绘制区域进行区分:

private fun quadrant(angle: Int): Int {
return if (angle == -90 || angle == 90) {
0 // 垂直
} else if (angle == 0) {
-1 // 水平右边
} else if (angle == 180) {
-2 // 水平左边
} else if (angle > -90 && angle < 0) {
1 // 右上角
} else if (angle > 0 && angle < 90) {
2 // 右下角
} else if (angle > 90 && angle < 180) {
3 // 左下角
} else {
4 // 左上角
}
}

设置文本的最大宽度:绿色虚线为左半边的文字最大宽度,蓝色虚线为右半边的文字最大宽度。通过quadrant(currentAngle)获取文字需要绘制的区域,垂直区域的文字最大宽度设置为雷达控件的一半,绿色虚线的文字最大宽度为offset.x,蓝色虚线的文字最大宽度为size.width - offset.x

fun DrawScope.wrapText(
text: String, // 绘制的文本
textPaint: TextPaint, // 文字画笔
width: Float, // 雷达控件的宽度
offset: Offset, // 未调整前的文字绘制的xy位置
currentAngle: Int, // 当前文字绘制所在的角度
chineseWrapWidth: Float? = null // 用来处理UI需求中文每两个字符换行
) {
val quadrant = quadrant(currentAngle)
var textMaxWidth = width
when (quadrant) {
0 -> {
textMaxWidth = width / 2
}
-1, 1, 2 -> {
textMaxWidth = size.width - offset.x
}
-2, 3, 4 -> {
textMaxWidth = offset.x
}
}
}

创建StaticLayout,传入文本绘制的最大宽度textMaxWidth,该控件会根据设置的最大宽度对文本自动换行。

val staticLayout = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
StaticLayout.Builder.obtain(text, 0, text.length, textPaint, textMaxWidth.toInt()).apply {
this.setAlignment(Layout.Alignment.ALIGN_NORMAL)
}.build()
} else {
StaticLayout(text, textPaint, textMaxWidth.toInt(), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0f, false)
}

通过staticLayout获取文本的高度,文本的行数。这里不能使用staticLayout.width来获取文本的宽度,因为假设设置的textMaxWidth=100,而文本绘制后的宽度只有50,通过staticLayout.width获取的宽度为100,这不是我们想要的。所以通过lines>1来判断文本是否换行,如果未换行,直接通过textPaint.measureText获取文本的真实宽度;如果换行,则staticLayout.getLineWidth(0)用来获取文本第一行的宽度就是文本的真实宽度。

val textHeight = staticLayout.height
val lines = staticLayout.lineCount
val isWrap = lines > 1
val textTrueWidth = if (isWrap) staticLayout.getLineWidth(0) else textPaint.measureText(text)

使用canvas绘制文本,这里的save() translate() staticLayout.draw(canvas) restore()是使用StaticLayout绘制的四步曲。

// 绘制文字
val textPointRadius = progress * radarRadius + 10f
val offset = inCircleOffset(center, textPointRadius, currentAngle)
val text = data[index].text
wrapText(
text,
textPaint,
size.width,
offset,
currentAngle,
if (specialHandle) textPaint.textSize * 2 else null
)

drawContext.canvas.nativeCanvas.save()
when (quadrant) {
0 -> { // 规律1
drawContext.canvas.nativeCanvas.translate(offset.x - textTrueWidth / 2, offset.y - textHeight)
}
-1 -> { // 规律2
drawContext.canvas.nativeCanvas.translate(offset.x, offset.y - textHeight / 2)
}
-2 -> { // 规律2
drawContext.canvas.nativeCanvas.translate(offset.x - textTrueWidth, offset.y - textHeight / 2)
}
1 -> { // 规律4
drawContext.canvas.nativeCanvas.translate(
offset.x,
if (!isWrap) offset.y - textHeight / 2 else offset.y - (textHeight - textHeight / lines / 2)
)
}
2 -> { // 规律6
drawContext.canvas.nativeCanvas.translate(offset.x, if (!isWrap) offset.y - textHeight / 2 else offset.y - textHeight / lines / 2)
}
3 -> { // 规律5
drawContext.canvas.nativeCanvas.translate(
offset.x - textTrueWidth,
if (!isWrap) offset.y - textHeight / 2 else offset.y - textHeight / lines / 2
)
}
4 -> { // 规律3
drawContext.canvas.nativeCanvas.translate(
offset.x - textTrueWidth,
if (!isWrap) offset.y - textHeight / 2 else offset.y - (textHeight - textHeight / lines / 2)
)
}
}
staticLayout.draw(drawContext.canvas.nativeCanvas)
drawContext.canvas.nativeCanvas.restore()

这样就画好了,但是产品看完效果图后不喜欢换行的效果,希望每两个字就换行,于是新增如下判断。

image.png

// 需要特殊处理换行&&包含中文字符&&文本绘制一行的宽度>文本最大宽度
if (chineseWrapWidth != null && isContainChinese(text) && textPaint.measureText(text) > textMaxWidth) {
textMaxWidth = chineseWrapWidth
}
private fun isContainChinese(str: String): Boolean {
val p = Pattern.compile("[\u4e00-\u9fa5]")
val m = p.matcher(str)
return m.find()
}

5、增加个小动画

当雷达图从屏幕中出现的时候,做一个绘制值从0到实际值的动画

var enable by remember {
mutableStateOf(false)
}
val progress by animateFloatAsState(if (enable) 1f else 0f, animationSpec = tween(2000))

Modifier.onGloballyPositioned {
enable = it.boundsInRoot().top >= 0 && it.boundsInRoot().right > 0
}

如何使用

private val list = listOf(
RadarBean("基本财务", 43f),
RadarBean("基本财务财务", 90f),
RadarBean("基", 90f),
RadarBean("基本财务财务", 90f),
RadarBean("基本财务", 83f),
RadarBean("技术择时择时", 50f),
RadarBean("景气行业行业", 83f)
)
ComposeRadarView(
modifier = Modifier
.padding(horizontal = 4.dp)
.size(180.dp),
list
)

项目地址

最后贴上项目的地址:ComposeRadar

如果觉得对您有帮助就点个👍吧~


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

收起阅读 »

Android自定义View- 画一幅实时心电测量图

概述 这次来讲讲心电图的绘制,这也是项目当中用到过的。心电图继承自View,概括一下主要有以下内容要实现:**实时显示动态心电测量数据、心电波形左右滑动、惯性滑动及波形 X轴和 Y轴方向双指滑动缩放。**下面我们来看看效果图,图片上传大小有限制,所以分两张: ...
继续阅读 »
概述

这次来讲讲心电图的绘制,这也是项目当中用到过的。心电图继承自View,概括一下主要有以下内容要实现:**实时显示动态心电测量数据、心电波形左右滑动、惯性滑动及波形 X轴和 Y轴方向双指滑动缩放。**下面我们来看看效果图,图片上传大小有限制,所以分两张:



Screenrecorder-2021-08-09-18-44-54-1282021891847387.gif



ECG_2.gif


下面我们将功能拆解,分步实现:



  • 画背景绿色网格线

  • 绘制实时动态心电曲线

  • 实现单指曲线左右平移

  • 实现曲线惯性滑动

  • 实现 X轴及 Y轴方向上曲线的双指滑动缩放(多点触控改变曲线增益)

  • 左上角显示当前增益


1、画网格线

这个就比较简单了。首先确定每一小格的边长,然后获取控件宽高。这样就能分别计算出水平方向及竖直方向有多少小格,也就是可以确定横线和竖线一共要画多少条。然后就可以用循环画出所有的线条,其中每隔5条进行线条加粗,而且画实线,这样就形成了实线大格。下面先看实现:


// 画 Bitmap
protected Bitmap gridBitmap;
// 画 Canvas
protected Canvas bitmapCanvas;
// 控件宽高
protected int viewWidth, viewHeight;
@Override
protected void onSizeChange() {
// 获取控件宽高
viewWidth = mBaseChart.getWidth();
viewHeight = mBaseChart.getHeight();
// 初始化网格 Bitmap
gridBitmap = Bitmap.createBitmap(viewWidth, viewHeight, Bitmap.Config.ARGB_8888);
bitmapCanvas = new Canvas(gridBitmap);
Log.d(TAG, "onSizeChange - " + "-- width = " +
mBaseChart.getWidth() + "-- height = " + mBaseChart.getHeight());
}

/**
* 准备好画网格的 Bitmap
*/
private void initBitmap(){
// 计算横线和竖线条数
hLineCount = (int) (viewHeight / gridSpace) + 2;
vLineCount = (int) (viewWidth / gridSpace) + 2;
// 画横线
for (int h = 0; h < hLineCount; h ++){
float startX = 0f;
float startY = gridSpace * h;
float stopX = viewWidth;
float stopY = gridSpace * h;
// 每个 5根画一条粗实线
if (h % 5 != 0){
linePaint.setPathEffect(pathEffect);
linePaint.setStrokeWidth(1.5f);
}else {
linePaint.setPathEffect(null);
linePaint.setStrokeWidth(3f);
}
// 画线
bitmapCanvas.drawLine(startX, startY, stopX,stopY, linePaint);
}
// 画竖线
for (int v = 0; v < vLineCount; v ++){
float startX = gridSpace * v;
float startY = 0f;
float stopX = gridSpace * v;
float stopY = viewHeight;
// 每隔 5根画一条粗实线
if (v % 5 != 0){
linePaint.setPathEffect(pathEffect);
linePaint.setStrokeWidth(1.5f);
}else {
linePaint.setPathEffect(null);
linePaint.setStrokeWidth(3f);
Log.d(TAG, "v = " + v);
}
// 画线
bitmapCanvas.drawLine(startX, startY, stopX,stopY, linePaint);
}
}

@Override
protected void onDraw(Canvas canvas) {
// 注释 1,Bitmap左边缘位置为getScrollX(),防止网格滑动
canvas.drawBitmap(gridBitmap, mBaseChart.getScrollX(), 0, null);
}

这里想提一下的是,这里网格线并不是直接画在控件 onDraw方法的 Canvas上的。而是在控件初始化时,事先将网格所有线条画在一张 Bitmap上,然后绘制时直接绘制 Bitmap。这样搞就不用每次绘制时都计算一遍线条的位置了。


还有就是上面注释 1处,绘制网格 Bitmap的左边缘的位置是 getScrollX()。因为后面要实现曲线左右滑动,但网格要固定不动。


2、绘制动态实时心电曲线

这就是心电图最主要的实现了。心电在测量的时候会实时传递电压值,我们需要把电压值实时存进数组里。然后把电压值换算成 Y坐标值,再根据事先确定好的 X轴方向两个数据点的距离来确定每个电压值在 X轴方向的坐标。然后从左到右确定曲线的路径Path,再将Path绘制到Canvas上就可以了。


我们观察上面效果图会发现,这里的实现是最后一个到达的数据的显示不会超过控件右边缘。也就是当曲线 X方向的长度不超过控件宽度时,曲线第一个点的横坐标 x = 0。当曲线 X方向长度大于控件宽度时,曲线 Path的第一个点的横坐标就向左移,也就是 x为负的了。这样就实现上面效果中,测量实时心电时,曲线会向左移。这样新来的数据就显示在控件可见范围内,早来的数据逐步向左移出控件可见范围。下面画个草图吧,草图大概就这么个意思:



心电.png


下面看一下实现:


    /**
* 创建曲线
*/
private boolean createPath() {
// 曲线长度超过控件宽度,曲线起点往左移
// 根据控件宽度和数组长度以及 X增益算出数组第一个数的 X坐标
float startX = (this.data.size() * dataSpaceX > viewWidth) ?
(viewWidth - (this.data.size() * dataSpaceX)) : 0f;
// 曲线复位
dataPath.reset();
for (int i = 0; i < this.data.size(); i++) {
// 确定 X轴坐标
float x = startX + i * this.dataSpaceX;
// 确定 Y轴坐标
float y = getVisibleY(this.data.get(i));
// 绘制曲线
if (i == 0) {
dataPath.moveTo(x, y);
} else {
dataPath.lineTo(x, y);
}
}
return true;
}
/**
* 电压 mv(毫伏)在 Y轴方向的换算
* 屏幕向上往下是 Y 轴正方向,所以电压值要乘以 -1进行翻转
* 目前默认每一大格代表 1000 mv,而真正一大格的宽度只有 150,所以 data要以两数换算
* Y == 0,是在 View的上边缘,所以要向下偏移将波形显示在中间
*
* @param data
* @return
*/
// 注释 2
private float getVisibleY(int data) {
// 电压值换算成 Y值
float visibleY = -smallGridSpace * 5 / mvPerLargeGrid * data;
// 向下偏移
visibleY = visibleY + smallGridSpace * 5 * offset;
return visibleY;
}

@Override
protected void onDraw(Canvas canvas) {
// 绘制心电曲线
canvas.drawPath(dataPath, linePaint);
}

上面有一点需要注意的,就是我们的 Y值的换算。我们知道Android屏幕自上而下是 Y轴正方向,所以我们如果直接把电压值画在屏幕上它是倒挂的。另外,这里默认的一大格代表1000mv电压值(可设),而真正一大格的边长是150。所以我们需要将电压值换算成屏幕像素。具体看上面注释 2的getVisibleY方法上面注释。


3、实现曲线左右平移

当心电测量完之后,我们需要实现曲线随手指滑动平移。这样才能看到心电图的全部内容。这个实现原理也简单,也就是监听onTouch事件,根据手指位移使用View的scrollBy方法来实现内容平移就可以了:


 /**
* @param event 单指事件
*/
private void singlePoint(MotionEvent event) {
mVelocityTracker.addMovement(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = event.getX();
break;
case MotionEvent.ACTION_MOVE:
float deltaX = event.getX() - lastX;
delWithActionMove(deltaX);
lastX = event.getX();
break;
case MotionEvent.ACTION_UP:
// 计算滑动速度
computeVelocity();
break;
}
}

/**
* @param deltaX 处理 MOVE事件
*/
private void delWithActionMove(float deltaX) {
if (this.data.size() * dataSpaceX <= viewWidth) return;
int leftBorder = getLeftBorder(); // 左边界
int rightBorder = getRightBorder(); // 右边界
int scrollX = mBaseChart.getScrollX(); // X轴滑动偏移量

if ((scrollX <= leftBorder) && (deltaX > 0)) {
mBaseChart.scrollTo((int) (viewWidth - this.data.size() * dataSpaceX), 0);
} else if ((scrollX >= rightBorder) && (deltaX < 0)) {
mBaseChart.scrollTo(0, 0);
} else {
// 内容平移
mBaseChart.scrollBy((int) -deltaX, 0);
}
}

注意上面左右边界的设定,别让曲线划出屏幕了。


4、惯性滑动

惯性滑动的实现,这里使用的套路是 VelocityTracker。先追踪手指滑动速度,然后使用 Scroller并结合 View的 computeScroll()方法和 scrollTo方法,实现手指离开屏幕后的惯性滑动。这部分内容在我上一篇文章画一个FM调频收音机刻度表

有讲,这里不再重复。


5、实现双指滑动,在横纵坐标方向缩放曲线

在实现双指滑动曲线缩放功能之前,我们先讲讲一小部分 MotionEvent的基础知识。为什么说只讲一小部分呢?因为 MotionEvent这个事件体系还蛮大。我们只讲一下这次用到的部分。



onTou.png



onTouch2.png


好吧,还是直接画表格吧。这样也直观一点,不用解释那么多。上面红色圈圈圈出来的几个哥们是我们这次要用到的。




  • event.getActionMasked() :上面也有解释,这个方法和 getAction()类似。只不过我们这次要处理多点触控,所以一定要用 getActionMasked() 来获取事件类型。




  • event.getPointerCount() :上面也有解释,获取屏幕上手指个数。因为我们这次要处理双指滑动,所以要用 (getPointerCount() == 2)进行判断。两根手指以外的事件我们不做缩放处理。




  • ACTION_POINTER_DOWN :上面又有解释,第一根手指之后,按下的其他手指。如果结合 (getPointerCount() == 2)这个前提条件,那么我们可以认为这次ACTION_POINTER_DOWN 就是第二根手指按下所触发的事件。




  • event.getX(int pointerIndex):上面也有介绍,获取某个手指当前的 X坐标。我们在获取到两个手指当前的 X坐标之后,就可以算出两指当前在 X轴方向的距离。然后再结合 ACTION_POINTER_DOWN 时所记录的坐标值,就可以计算出两个手指在 X方向上是靠近了还是疏远了(收缩了还是放大了)。getY(int pointerIndex) 方法同理,不做解释了。




  • ACTION_MOVE :两指滑动当然也要用到 MOVE事件,只不过这里 ACTION_MOVE 和单指的使用方法一样,就不做解释了。




好了,我们再看看 X轴方向缩放具体实现吧:


  /**
* 处理onTouch事件
*
* @param event 事件
* @return 拦截
*/
@Override
protected boolean onTouchEvent(MotionEvent event) {
Log.d(TAG, "pointerCount = " + event.getPointerCount());
if (event.getPointerCount() == 1) { // 单指平滑
singlePoint(event);
}
if (event.getPointerCount() == 2) { // 双指缩放
doublePoint(event);
}
return true;
}

/**
* @param event 双指事件
*/
private void doublePoint(MotionEvent event) {
if (pointOne == null) pointOne = new PointF();
if (pointTwo == null) pointTwo = new PointF();

switch (event.getActionMasked()) {
case MotionEvent.ACTION_POINTER_DOWN: // 第二根手指按下
Log.d(TAG, "ACTION_POINTER_DOWN");
// 记录第二根手指按下时,两指的坐标点
saveLastPoint(event);
numbersPerLargeGridOnThisTime = getDataNumbersPerGrid();
mvPerLargeGridOnThisTime = getMvPerLargeGrid();
break;
case MotionEvent.ACTION_MOVE: // 双指拉伸
Log.d(TAG, "ACTION_MOVE");
// 计算 X方向缩放量
getScaleX(event);
// 计算 Y轴方向所放量
getScaleY(event);
break;
case MotionEvent.ACTION_POINTER_UP: // 先离开的手指
Log.d(TAG, "ACTION_POINTER_UP");
break;
}
}

/**
* 处理 X方向的缩放
*
* @param event 事件
* @return 拉伸量
*/
private float getScaleX(MotionEvent event) {
float pointOneX = event.getX(0);
float pointTwoX = event.getX(1);
// 算出 X轴方向的拉伸量
float deltaScaleX = Math.abs(pointOneX - pointTwoX) - Math.abs(pointOne.x - pointTwo.x);
// 设置拉伸敏感度
int inDevi = mBaseChart.getWidth() / 54;
// 计算拉伸时增益偏移量
int inDe = (int) deltaScaleX / inDevi;
// 算出最终增益
int perNumber = numbersPerLargeGridOnThisTime - inDe;
// 设置增益
setDataNumbersPerGrid(perNumber);
return deltaScaleX;


好了,该解释的原理上面都做了解释。上面代码要解释的无非就是缩放敏感度调节的问题,代码里做了解释。缩放量计算出来之后,我们就可以改变心电曲线的增益了。比如说 X方向两点数据之间的距离做了调整、Y方向心电数值计算因子做了调整,然后重新算出曲线 Path再重绘,也就可以了。


6、左上角显示当前增益

最后我们要把当前增益显示出来,比如说 X轴方向一大格绘制了多少点数据、Y轴方向一大格代表多少毫伏。这两个参数都是在上一步双指缩放时动态改变的,所以要留一个对外接口让外界获取到这两个参数。


 /**
* 获取每大格显示的数据个数,再结合医疗版的采样率,就可以算出一格显示了多长时间的数据
*
* @return
*/
public int getDataNumbersPerGrid() {
return this.dataNumbersPerGrid;
}
/**
* @return 获取每大格代表多少毫伏
*/
public float getMvPerLargeGrid() {
return this.mvPerLargeGrid;
}


因为这次心电图的绘制比以往的文章都涉及到更多的细节,所以之前文章里讲过的一些实现细节这里就没重复讲。另外,这次自定义 View使用了 Base模板设计模式,用好几个类来实现了这幅心电图,所以没把完整代码贴在这里。代码还是直接放Github吧 :心电图


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

Compose的UI刷新机制是啥?和Flutter一样么?

前言 我去年发现Android新出了个UI框架Compose,看了Demo后发现,纳尼!和Flutter太像了吧,无论是编程逻辑、控件名,都有很多相似的地方。谷歌自己抄自己?一直想去尝试,感受一下,直到今年才有时间去写了小项目。 思考 我在写代码的时候和Flu...
继续阅读 »

前言


我去年发现Android新出了个UI框架Compose,看了Demo后发现,纳尼!和Flutter太像了吧,无论是编程逻辑、控件名,都有很多相似的地方。谷歌自己抄自己?一直想去尝试,感受一下,直到今年才有时间去写了小项目。


思考


我在写代码的时候和Flutter去做对比,就想到它俩的UI刷新机制是否有相似点,如果不一样,那Compose的刷新机制是什么?


在看Compose的刷新机制前,我回忆了一下Flutter的刷新机制。



简单讲Flutter是通过调用StatefulWidgetsetState方法,重新走一遍build方法中的代码。那原理就是setState调用时会自己添加到BuildOwnerdirtyElements脏链表中,然后调用window.scheduleFrame来注册Vsync回调,当下一次vsync信号的到来时会重新绘制UI。



所以我觉得Flutter更像是一个屏幕,调用setState方法不断重新构建UI页面,一帧一帧的。那Compose是不是也是这样呢?


尝试


Demo如下,在页面上显示一个Text控件,和一个按钮,每一次点击,Text显示的数字自增。


@Composable
fun demo() {
// 关键代码
var versionCode by remember { mutableStateOf(0) }

Column {
Button(
onClick = { versionCode++ }) {
Text("Add +")
}
Text( versionCode.toString() )
}
}

UI发生变化的关键代码就是 by remember { mutableStateOf(0) } ,这行代码删除后,怎么按UI都不会变化。我发每次versionCode变化的时候会重新走一遍demo()内的代码,这时该类中有其他方法,其他方法代码并不会重走。如果把Text中的versionCode引用删除,写一个固定值,这个再点击按钮,也不会重新走一遍代码。



问题



  1. Compose 是如何进行更新的?

  2. 如何做到更新时只有引用的方法内刷新?

  3. 不引用为何不刷新方法?



分析


带着以上三个问题,点进mutableStateOf中去,看一看源码。
1.png
进入 createSnapshotMutableState
2.png
进入 ParcelableSnapshotMutableState
3.png
进入SnapshotMutableStateImpl,下面这个类中,红框标记的是关键代码,这个方法的注解是



A single value holder whose reads and writes are observed by Compose.Additionally, writes to it are transacted as part of the [Snapshot] system.



我理解的意思是,对value这个值做了监听,只要是 Compose的UI 引用了value,当其发生变化时就能自动更新


4.png


这里的 value 就是给 versionCode 赋的值,这里的 get() 方法中会调用readable() ,把当前 state 保存起来(我认为这里的 state 可以理解为 Flutter 的 state)。set() 方法内会进行对比,在 equivalent() 方法中比对的是对象,当两个对象的地址不一致时,会触发监听通知,重写用的Compose方法,



这也就解释了之前说的三个问题。



  1. Compose的刷新本质是对 value 的监听通知;

  2. 为了避免过度刷新,将刷新范围固定到最小的标记@Compose的注解的方法内,包括方法内的其他方法(和Flutter的StatefulWidget的刷新范围一样),前提是其他Compose方法有参数传入(我理解是有新的参数传入,会生成新的对象),否则除了第一次不会再进行刷新;为提高性能,会把频繁刷新的View(Flutter中的widget)封装为单独的Compose方法(Stful);

  3. 对value的引用,调用了get内的注册方法,不引用,也就不会引起刷新。



以上就是我对Compose的刷新机制的理解,


应用


了解了Compose的刷新机制后,怎么才能高效的使用这中逻辑编程呢?我在踩了一堆坑后,有以下几个写代码的注意点,仅供参考。


一、


如果在方法内使用mutableStateOf,需要包一层remember函数,它的作用是运行完里面的代码后,就会存在缓存里,再次执行这行代码,不会再次初始化,会从缓存中拿出 State的对象,防止多次初始化。 如果你偏不,可以试试看有什么神奇的现象🙃。’
(PS :在remember函数中有个熟悉的参数 Key,熟悉Flutter的估计都明白干啥的了,这就不再啰嗦了)


// 成员变量可以不用套,因为本身就初始化一次
var versionCode by mutableStateOf(0)

@Composable
fun demo() {
// 方法内这么写
var versionCode by remember { mutableStateOf(0) }

Column {
Button(
onClick = { versionCode++ }) {
Text("Add +")
}
Text( versionCode.toString() )
}
}

二、


value的刷新是设置新的值,是对比对象本身,如果只改变了对象内的值,是不会放生刷新的。如代码中A对象并没有改变。


data class A(var a: Int = 0)
@Composable
fun demo() {
val versionCode by remember { mutableStateOf(A(0)) }
Column {
Button(
onClick = { versionCode.a++ }) {
Text("Add +")
}
Text( versionCode.toString() )
}
}

遇到这种情况需要刷新有两种办法



  1. 复制对象,改变对象地址


        Button(
onClick = { versionCode.copy(a = versionCode.a++) }) {
Text("Add +")
}


  1. 给对象内的变量实现mutableStateOf


data class A(var a: MutableState<Int> = mutableStateOf(0)) {
// 或写在下面用 by 代理的方式
var a by mutableStateOf(0)
}


三、


单个对象好复制,好改,遇到列表,数组类的对象刷新,该怎么做呢,换列表对象?不合适。好在官方已经替咱们想到了这一点。神器 mutableStateListOf,当列表内的数据发生变化时,会刷新UI


val data = mutableStateListOf(1, 2, 3)
@Composable
fun demo() {
Column {
Button(onClick = {
data.add(data.last() + 1)
}) {
Text(text = "onclick")
}
for (d in data) {
Text(text = "data:${d}")
}
}
}

我大概遇到的UI刷新上的坑,可以归为以上三种,还有其他的欢迎交流补充。


总结


Compose的刷新机制简单来讲是有范围,有组织的用观察者模式进行刷新。
分析了个大概,更深层次的原理,以后慢慢啃。
Compose的编程模式和Flutter很像,会flutter很容易上手,还挺有意思的。


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

尴尬又暖心!学生知乎上提问导师人品如何,没想到导师亲自回答了...

在研究生阶段,学生选择导师一定是慎之又慎,除了学术造诣之外,导师的人品也非常重要。因此很多学生会在选择导师之前进行多方打听,以求对老师进行全面的了解,免得做出让自己后悔的决定。藏龙卧虎的知乎平台自然是很多学子的首选之地。就这两天,有学生在知乎上提问北航的程林老...
继续阅读 »

在研究生阶段,学生选择导师一定是慎之又慎,除了学术造诣之外,导师的人品也非常重要。因此很多学生会在选择导师之前进行多方打听,以求对老师进行全面的了解,免得做出让自己后悔的决定。

藏龙卧虎的知乎平台自然是很多学子的首选之地。就这两天,有学生在知乎上提问北航的程林老师的情况,想要向广大网友提问一下课题组的环境,以及导师的人品如何。

万万没想到,这一问结果引来了老师的亲自回答,这个互动真的太暖心了吧!

得到了老师的亲自回应后,提问者也表明了自己的身份,生怕别人以为是老师自问自答,故意宣传自己。

在评论区,我们可以看到的评论都是对程老师对褒奖:

在程林老师的个人主页上,包含关于招生和团队建设方面的内容,从中也可以看到他的独特。

基本原则:与学生交朋友、以学生成长为目的,打造积极努力、乐观融洽的学术型科研团队~

学生培养方面:与学生交朋友,保持沟通畅达;以学术方向发展为导向,与学生一道确立个人成长路线;积极辅助构建/完善知识体系、学术跟踪和解决能力、碎片化学习能力、任务管理能力,与学生一道成长~

程林教授主页:

关于push不push的问题,整体原则是:我尊重学生的选择。但私下来讲,我自己努力,当然也希望自己的学生们也能够努力,这对学生自己和课题组都有好处。具体科研和项目进展不顺利,我也会着急,甚至责怪两句,但是我都是尽可能的动之以情、晓之以理,毕竟老师是弱势群体,我也没啥大棒子可以威胁。人品好不好,我也不知道,自认为性格开朗,朋友蛮多,家庭和美。

最后一点,能不能学到真本事,我就真的不能保证了,研究生区别于高中和本科,独立科研的比重特别大,导师更多的是一个旁观者和引路人,师傅领进门,修行靠个人。我唯一可以承诺的就是,在关键的时候、在你需要的时候,我会拼尽全力的帮你。

最最后一点,我会强迫你建立自己的知识体系,因为我觉得这是我能教你最重要的东西,希望未来的你能够理解。

版权声明

本文来源:知乎、谷粉学术等

收起阅读 »

如何优雅地处理重复请求(并发请求)

对于一些用户请求,在某些情况下是可能重复发送的,如果是查询类操作并无大碍,但其中有些是涉及写入操作的,一旦重复了,可能会导致很严重的后果,例如交易的接口如果重复请求可能会重复下单。重复的场景有可能是:黑客拦截了请求,重放前端/客户端因为某些原因请求重复发送了,...
继续阅读 »

对于一些用户请求,在某些情况下是可能重复发送的,如果是查询类操作并无大碍,但其中有些是涉及写入操作的,一旦重复了,可能会导致很严重的后果,例如交易的接口如果重复请求可能会重复下单。

重复的场景有可能是:

  1. 黑客拦截了请求,重放

  2. 前端/客户端因为某些原因请求重复发送了,或者用户在很短的时间内重复点击了

  3. 网关重发

  4. ….

本文讨论的是如何在服务端优雅地统一处理这种情况,如何禁止用户重复点击等客户端操作不在本文的讨论范畴。

利用唯一请求编号去重

可能会想到的是,只要请求有唯一的请求编号,那么就能借用Redis做这个去重——只要这个唯一请求编号在redis存在,证明处理过,那么就认为是重复的

代码大概如下:

    String KEY = "REQ12343456788";//请求唯一编号
  long expireTime = 1000;// 1000毫秒过期,1000ms内的重复请求会认为重复
  long expireAt = System.currentTimeMillis() + expireTime;
  String val = "expireAt@" + expireAt;

  //redis key还存在的话要就认为请求是重复的
  Boolean firstSet = stringRedisTemplate.execute((RedisCallback<Boolean>) connection -> connection.set(KEY.getBytes(), val.getBytes(), Expiration.milliseconds(expireTime), RedisStringCommands.SetOption.SET_IF_ABSENT));

  final boolean isConsiderDup;
  if (firstSet != null && firstSet) {// 第一次访问
      isConsiderDup = false;
  } else {// redis值已存在,认为是重复了
      isConsiderDup = true;
  }

业务参数去重

上面的方案能解决具备唯一请求编号的场景,例如每次写请求之前都是服务端返回一个唯一编号给客户端,客户端带着这个请求号做请求,服务端即可完成去重拦截。

但是,很多的场景下,请求并不会带这样的唯一编号!那么我们能否针对请求的参数作为一个请求的标识呢?

先考虑简单的场景,假设请求参数只有一个字段reqParam,我们可以利用以下标识去判断这个请求是否重复。用户ID:接口名:请求参数

String KEY = "dedup:U="+userId + "M=" + method + "P=" + reqParam;

那么当同一个用户访问同一个接口,带着同样的reqParam过来,我们就能定位到他是重复的了。

但是问题是,我们的接口通常不是这么简单,以目前的主流,我们的参数通常是一个JSON。那么针对这种场景,我们怎么去重呢?

计算请求参数的摘要作为参数标识

假设我们把请求参数(JSON)按KEY做升序排序,排序后拼成一个字符串,作为KEY值呢?但这可能非常的长,所以我们可以考虑对这个字符串求一个MD5作为参数的摘要,以这个摘要去取代reqParam的位置。

String KEY = "dedup:U="+userId + "M=" + method + "P=" + reqParamMD5;

这样,请求的唯一标识就打上了!

注:MD5理论上可能会重复,但是去重通常是短时间窗口内的去重(例如一秒),一个短时间内同一个用户同样的接口能拼出不同的参数导致一样的MD5几乎是不可能的。

继续优化,考虑剔除部分时间因子

上面的问题其实已经是一个很不错的解决方案了,但是实际投入使用的时候可能发现有些问题:某些请求用户短时间内重复的点击了(例如1000毫秒发送了三次请求),但绕过了上面的去重判断(不同的KEY值)。

原因是这些请求参数的字段里面,是带时间字段的,这个字段标记用户请求的时间,服务端可以借此丢弃掉一些老的请求(例如5秒前)。如下面的例子,请求的其他参数是一样的,除了请求时间相差了一秒:

    //两个请求一样,但是请求时间差一秒
  String req = "{\n" +
          "\"requestTime\" :\"20190101120001\",\n" +
          "\"requestValue\" :\"1000\",\n" +
          "\"requestKey\" :\"key\"\n" +
          "}";

  String req2 = "{\n" +
          "\"requestTime\" :\"20190101120002\",\n" +
          "\"requestValue\" :\"1000\",\n" +
          "\"requestKey\" :\"key\"\n" +
          "}";

这种请求,我们也很可能需要挡住后面的重复请求。所以求业务参数摘要之前,需要剔除这类时间字段。还有类似的字段可能是GPS的经纬度字段(重复请求间可能有极小的差别)。

请求去重工具类,Java实现

public class ReqDedupHelper {

  /**
    *
    * @param reqJSON 请求的参数,这里通常是JSON
    * @param excludeKeys 请求参数里面要去除哪些字段再求摘要
    * @return 去除参数的MD5摘要
    */
  public String dedupParamMD5(final String reqJSON, String... excludeKeys) {
      String decreptParam = reqJSON;

      TreeMap paramTreeMap = JSON.parseObject(decreptParam, TreeMap.class);
      if (excludeKeys!=null) {
          List<String> dedupExcludeKeys = Arrays.asList(excludeKeys);
          if (!dedupExcludeKeys.isEmpty()) {
              for (String dedupExcludeKey : dedupExcludeKeys) {
                  paramTreeMap.remove(dedupExcludeKey);
              }
          }
      }

      String paramTreeMapJSON = JSON.toJSONString(paramTreeMap);
      String md5deDupParam = jdkMD5(paramTreeMapJSON);
      log.debug("md5deDupParam = {}, excludeKeys = {} {}", md5deDupParam, Arrays.deepToString(excludeKeys), paramTreeMapJSON);
      return md5deDupParam;
  }

  private static String jdkMD5(String src) {
      String res = null;
      try {
          MessageDigest messageDigest = MessageDigest.getInstance("MD5");
          byte[] mdBytes = messageDigest.digest(src.getBytes());
          res = DatatypeConverter.printHexBinary(mdBytes);
      } catch (Exception e) {
          log.error("",e);
      }
      return res;
  }
}

下面是一些测试日志:

public static void main(String[] args) {
  //两个请求一样,但是请求时间差一秒
  String req = "{\n" +
          "\"requestTime\" :\"20190101120001\",\n" +
          "\"requestValue\" :\"1000\",\n" +
          "\"requestKey\" :\"key\"\n" +
          "}";

  String req2 = "{\n" +
          "\"requestTime\" :\"20190101120002\",\n" +
          "\"requestValue\" :\"1000\",\n" +
          "\"requestKey\" :\"key\"\n" +
          "}";

  //全参数比对,所以两个参数MD5不同
  String dedupMD5 = new ReqDedupHelper().dedupParamMD5(req);
  String dedupMD52 = new ReqDedupHelper().dedupParamMD5(req2);
  System.out.println("req1MD5 = "+ dedupMD5+" , req2MD5="+dedupMD52);

  //去除时间参数比对,MD5相同
  String dedupMD53 = new ReqDedupHelper().dedupParamMD5(req,"requestTime");
  String dedupMD54 = new ReqDedupHelper().dedupParamMD5(req2,"requestTime");
  System.out.println("req1MD5 = "+ dedupMD53+" , req2MD5="+dedupMD54);

}

日志输出:

req1MD5 = 9E054D36439EBDD0604C5E65EB5C8267 , req2MD5=A2D20BAC78551C4CA09BEF97FE468A3F
req1MD5 = C2A36FED15128E9E878583CAAAFEFDE9 , req2MD5=C2A36FED15128E9E878583CAAAFEFDE9

日志说明:

  • 一开始两个参数由于requestTime是不同的,所以求去重参数摘要的时候可以发现两个值是不一样的

  • 第二次调用的时候,去除了requestTime再求摘要(第二个参数中传入了”requestTime”),则发现两个摘要是一样的,符合预期。

总结

至此,我们可以得到完整的去重解决方案,如下:

String userId= "12345678";//用户
String method = "pay";//接口名
String dedupMD5 = new ReqDedupHelper().dedupParamMD5(req,"requestTime");//计算请求参数摘要,其中剔除里面请求时间的干扰
String KEY = "dedup:U=" + userId + "M=" + method + "P=" + dedupMD5;

long expireTime = 1000;// 1000毫秒过期,1000ms内的重复请求会认为重复
long expireAt = System.currentTimeMillis() + expireTime;
String val = "expireAt@" + expireAt;

// NOTE:直接SETNX不支持带过期时间,所以设置+过期不是原子操作,极端情况下可能设置了就不过期了,后面相同请求可能会误以为需要去重,所以这里使用底层API,保证SETNX+过期时间是原子操作
Boolean firstSet = stringRedisTemplate.execute((RedisCallback<Boolean>) connection -> connection.set(KEY.getBytes(), val.getBytes(), Expiration.milliseconds(expireTime),
      RedisStringCommands.SetOption.SET_IF_ABSENT));

final boolean isConsiderDup;
if (firstSet != null && firstSet) {
  isConsiderDup = false;
} else {
  isConsiderDup = true;
}

转自:薛定谔的风口猪

链接:https://jaskey.github.io/blog/2020/05/19/handle-duplicate-request/

收起阅读 »

Kotlin flow实践总结

背景 最近学了下Kotlin Flow,顺便在项目中进行了实践,做一下总结。 Flow是什么 按顺序发出多个值的数据流。 本质就是一个生产者消费者模型,生产者发送数据给消费者进行消费。 冷流:当执行collect的时候(也就是有消费者的时候),生产者才开...
继续阅读 »

背景


最近学了下Kotlin Flow,顺便在项目中进行了实践,做一下总结。


image.png


Flow是什么


按顺序发出多个值的数据流。

本质就是一个生产者消费者模型,生产者发送数据给消费者进行消费。


Image.png



  • 冷流:当执行collect的时候(也就是有消费者的时候),生产者才开始发射数据流。

    生产者与消费者是一对一的关系。当生产者发送数据的时候,对应的消费者才可以收到数据。

  • 热流:不管有没有执行collect(也就是不管有没有消费者),生产者都会发射数据流到内存中。

    生产者与消费者是一对多的关系。当生产者发送数据的时候,多个消费者都可以收到数据


实践场景


场景一:简单列表数据的加载状态


简单的列表显示场景,可以使用onStart,onEmpty,catch,onCompletion等回调操作符,监听数据流的状态,显示相应的加载状态UI。



  • onStart:在数据发射之前触发,onStart所在的线程,是数据产生的线程

  • onCompletion:在数据流结束时触发,onCompletion所在的线程,是数据产生的线程

  • onEmpty:当数据流结束了,缺没有发出任何元素的时候触发。

  • catch:数据流发生错误的时候触发

  • flowOn:指定上游数据流的CoroutineContext,下游数据流不会受到影响


private fun coldFlowDemo() {
//创建一个冷流,在3秒后发射一个数据
val coldFlow = flow<Int> {
delay(3000)
emit(1)
}
lifecycleScope.launch(Dispatchers.IO) {
coldFlow.onStart {
Log.d(TAG, "coldFlow onStart, thread:${Thread.currentThread().name}")
mBinding.progressBar.isVisible = true
mBinding.tvLoadingStatus.text = "加载中"
}.onEmpty {
Log.d(TAG, "coldFlow onEmpty, thread:${Thread.currentThread().name}")
mBinding.progressBar.isVisible = false
mBinding.tvLoadingStatus.text = "数据加载为空"
}.catch {
Log.d(TAG, "coldFlow catch, thread:${Thread.currentThread().name}")
mBinding.progressBar.isVisible = false
mBinding.tvLoadingStatus.text = "数据加载错误:$it"
}.onCompletion {
Log.d(TAG, "coldFlow onCompletion, thread:${Thread.currentThread().name}")
mBinding.progressBar.isVisible = false
mBinding.tvLoadingStatus.text = "加载完成"
}
//指定上游数据流的CoroutineContext,下游数据流不会受到影响
.flowOn(Dispatchers.Main)
.collect {
Log.d(TAG, "coldFlow collect:$it, thread:${Thread.currentThread().name}")
}
}
}

比如上面的例子。
使用flow构建起函数,创建一个冷流,3秒后发送一个值到数据流中。
使用onStart,onEmpty,catch,onCompletion操作符,监听数据流的状态。


日志输出:


coldFlow onStart, thread:main
coldFlow onCompletion, thread:main
coldFlow collect:1, thread:DefaultDispatcher-worker-1

场景二:同一种数据,需要加载本地数据和网络数据


在实际的开发场景中,经常会将一些网络数据保存到本地,下次加载数据的时候,优先使用本地数据,再使用网络数据。

但是本地数据和网络数据的加载完成时机不一样,所以可能会有下面几种场景。



  1. 本地数据比网络数据先加载完成:那先使用本地数据,再使用网络数据

  2. 网络数据比本地数据先加载完成:



  • 网络数据加载成功,那只使用网络数据即可,不需要再使用本地数据了。

  • 网络数据加载失败,可以继续尝试使用本地数据进行兜底。



  1. 本地数据和网络数据都加载失败:通知上层数据加载失败


实现CacheRepositity


将上面的逻辑进行简单封装成一个基类,CacheRepositity。

相应的子类,只需要实现两个方法即可。



  • CResult:代表加载结果,Success 或者 Error。

  • fetchDataFromLocal(),实现本地数据读取的逻辑

  • fetchDataFromNetWork(),实现网络数据获取的逻辑


abstract class CacheRepositity<T> {
private val TAG = "CacheRepositity"

fun getData() = channelFlow<CResult<T>> {
supervisorScope {
val dataFromLocalDeffer = async {
fetchDataFromLocal().also {
Log.d(TAG,"fetchDataFromLocal result:$it , thread:${Thread.currentThread().name}")
//本地数据加载成功
if (it is CResult.Success) {
send(it)
}
}
}

val dataFromNetDeffer = async {
fetchDataFromNetWork().also {
Log.d(TAG,"fetchDataFromNetWork result:$it , thread:${Thread.currentThread().name}")
//网络数据加载成功
if (it is CResult.Success) {
send(it)
//如果网络数据已加载,可以直接取消任务,就不需要处理本地数据了
dataFromLocalDeffer.cancel()
}
}
}

//本地数据和网络数据,都加载失败的情况
val localData = dataFromLocalDeffer.await()
val networkData = dataFromNetDeffer.await()
if (localData is CResult.Error && networkData is CResult.Error) {
send(CResult.Error(Throwable("load data error")))
}
}
}

protected abstract suspend fun fetchDataFromLocal(): CResult<T>

protected abstract suspend fun fetchDataFromNetWork(): CResult<T>

}

sealed class CResult<out R> {
data class Success<out T>(val data: T) : CResult<T>()
data class Error(val throwable: Throwable) : CResult<Nothing>()
}

测试验证


写个TestRepositity,实现CacheRepositity的抽象方法。

通过delay延迟耗时来模拟各种场景,观察日志的输出顺序。


private fun cacheRepositityDemo(){
val repositity=TestRepositity()
lifecycleScope.launch {
repositity.getData().onStart {
Log.d(TAG, "TestRepositity: onStart")
}.onCompletion {
Log.d(TAG, "TestRepositity: onCompletion")
}.collect {
Log.d(TAG, "collect: $it")
}
}
}

本地数据比网络数据加载快


class TestRepositity : CacheRepositity<String>() {
override suspend fun fetchDataFromLocal(): CResult<String> {
delay(1000)
return CResult.Success("data from fetchDataFromLocal")
}

override suspend fun fetchDataFromNetWork(): CResult<String> {
delay(2000)
return CResult.Success("data from fetchDataFromNetWork")
}
}

模拟数据:本地加载delay1秒,网络加载delay2秒

日志输出:collect 执行两次,先收到本地数据,再收到网络数据。


onStart
fetchDataFromLocal result:Success(data=data from fetchDataFromLocal) , thread:main
collect: Success(data=data from fetchDataFromLocal)
fetchDataFromNetWork result:Success(data=data from fetchDataFromNetWork) , thread:main
collect: Success(data=data from fetchDataFromNetWork)
onCompletion

网络数据比本地数据加载快


class TestRepositity : CacheRepositity<String>() {
override suspend fun fetchDataFromLocal(): CResult<String> {
delay(2000)
return CResult.Success("data from fetchDataFromLocal")
}

override suspend fun fetchDataFromNetWork(): CResult<String> {
delay(1000)
return CResult.Success("data from fetchDataFromNetWork")
}
}

模拟数据:本地加载delay 2秒,网络加载delay 1秒

日志输出:collect 只执行1次,只收到网络数据。


onStart
fetchDataFromNetWork result:Success(data=data from fetchDataFromNetWork) , thread:main
collect: Success(data=data from fetchDataFromNetWork)
onCompletion

网络数据加载失败,使用本地数据


class TestRepositity : CacheRepositity<String>() {
override suspend fun fetchDataFromLocal(): CResult<String> {
delay(2000)
return CResult.Success("data from fetchDataFromLocal")
}

override suspend fun fetchDataFromNetWork(): CResult<String> {
delay(1000)
return CResult.Error(Throwable("fetchDataFromNetWork Error"))
}
}

模拟数据:本地加载delay 2秒,网络数据加载失败

日志输出:collect 只执行1次,只收到本地数据。


onStart
fetchDataFromNetWork result:Error(throwable=java.lang.Throwable: fetchDataFromNetWork Error) , thread:main
fetchDataFromLocal result:Success(data=data from fetchDataFromLocal) , thread:main
collect: Success(data=data from fetchDataFromLocal)
onCompletion

网络数据和本地数据都加载失败


class TestRepositity : CacheRepositity<String>() {
override suspend fun fetchDataFromLocal(): CResult<String> {
delay(2000)
return CResult.Error(Throwable("fetchDataFromLocal Error"))
}

override suspend fun fetchDataFromNetWork(): CResult<String> {
delay(1000)
return CResult.Error(Throwable("fetchDataFromNetWork Error"))
}
}

模拟数据:本地数据加载失败,网络数据加载失败

日志输出: collect 只执行1次,结果是CResult.Error,代表加载数据失败。


onStart
fetchDataFromNetWork result:Error(throwable=java.lang.Throwable: fetchDataFromNetWork Error) , thread:main
fetchDataFromLocal result:Error(throwable=java.lang.Throwable: fetchDataFromLocal Error) , thread:main
collect: Error(throwable=java.lang.Throwable: load data error)
onCompletion

场景三:多种数据源,按照顺序合并进行展示


Image.png


在实际的开发场景中,经常一个页面的数据,是需要发起多个网络请求之后,组合数据之后再进行显示。
比如类似这种页面,3种数据,需要由3个网络请求获取得到,然后再进行相应的显示。


实现目标:



  1. 接口间不需要互相等待,哪些数据先回来,就先展示哪部分

  2. 控制数据的显示顺序


flow combine操作符


可以合并多个不同的 Flow 数据流,生成一个新的流。
只要其中某个子 Flow 数据流有产生新数据的时候,就会触发 combine 操作,进行重新计算,生成一个新的数据。


例子


class HomeViewModel : ViewModel() {

//暴露给View层的列表数据
val list = MutableLiveData<List<String?>>()

//多个子Flow,这里简单都返回String,实际场景根据需要,返回相应的数据类型即可
private val bannerFlow = MutableStateFlow<String?>(null)
private val channelFlow = MutableStateFlow<String?>(null)
private val listFlow = MutableStateFlow<String?>(null)


init {
//使用combine操作符
viewModelScope.launch {
combine(bannerFlow, channelFlow, listFlow) { bannerData, channelData, listData ->
Log.d("HomeViewModel", "combine bannerData:$bannerData,channelData:$channelData,listData:$listData")
//只要子flow里面的数据不为空,就放到resultList里面
val resultList = mutableListOf<String?>()
if (bannerData != null) {
resultList.add(bannerData)
}
if (channelData != null) {
resultList.add(channelData)
}
if (listData != null) {
resultList.add(listData)
}
resultList
}.collect {
//收集combine之后的数据,修改liveData的值,通知UI层刷新列表
Log.d("HomeViewModel", "collect: ${it.size}")
list.postValue(it)
}
}
}

fun loadData() {
viewModelScope.launch(Dispatchers.IO) {
//模拟耗时操作
async {
delay(1000)
Log.d("HomeViewModel", "getBannerData success")
bannerFlow.emit("Banner")
}
async {
delay(2000)
Log.d("HomeViewModel", "getChannelData success")
channelFlow.emit("Channel")
}
async {
delay(3000)
Log.d("HomeViewModel", "getListData success")
listFlow.emit("List")
}
}
}
}

HomeViewModel



  1. 提供一个 LiveData 的列表数据给View层使用

  2. 内部有3个子 flow ,分别负责相应数据的生产。(这里简单都返回String,实际场景根据需要,返回相应的数据类型即可)。

  3. 通过 combine 操作符,组合这3个子flow的数据。

  4. collect 接收生成的新数据,并修改liveData的数据,通知刷新UI


View层使用


private fun flowCombineDemo() {
val homeViewModel by viewModels<HomeViewModel>()
homeViewModel.list.observe(this) {
Log.d("HomeViewModel", "observe size:${it.size}")
}
homeViewModel.loadData()
}

简单的创建一个 ViewModel ,observe 列表数据对应的 LiveData。

通过输出的日志发现,触发数据加载之后,每次子 Flow 流生产数据的时候,都会触发一次 combine 操作,生成新的数据。


日志输出:
combine bannerData:null,channelData:null,listData:null
collect: 0
observe size:0

getBannerData success
combine bannerData:Banner,channelData:null,listData:null
collect: 1
observe size:1

getChannelData success
combine bannerData:Banner,channelData:Channel,listData:null
collect: 2
observe size:2

getListData success
combine bannerData:Banner,channelData:Channel,listData:List
collect: 3
observe size:3

总结


具体场景,具体分析。刚好这几个场景,配合Flow进行使用,整体实现也相对简单了一些。


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

Kotlin的 :: 符号是个啥?

前言 在阅读Kotlin的代码时,经常有看到 :: 这个符号,这个符号专业术语叫做成员引用,在代码中使用可以简化代码,那到底怎么使用呢以及使用的范围,这篇文章就来好好捋一下。 正文 这里虽然很熟悉,但是我们还是从简单说起,需要了解为什么这样设计。 传递函数优化...
继续阅读 »

前言


在阅读Kotlin的代码时,经常有看到 :: 这个符号,这个符号专业术语叫做成员引用,在代码中使用可以简化代码,那到底怎么使用呢以及使用的范围,这篇文章就来好好捋一下。


正文


这里虽然很熟悉,但是我们还是从简单说起,需要了解为什么这样设计。


传递函数优化


这里我们举个栗子,就看这个熟悉的sortBy排序函数,先定义People类:


//测试代码
data class People(val name: String,val age: Int){
//自定义的排序条件
fun getMax() : Int{
return age * 10 + name.length
}
}

然后我们来进行排序:


val people = People("zyh",10)
val people1 = People("zyh1",100)
val peopleList = arrayListOf(people,people1)
//给sortBy传入lambda
peopleList.sortBy { people -> people.getMax() }

这里我们给sortBy函数传递一个lambda,由于sortBy函数是内联的,所以传递给它的lambda会被内联,但是假如现在有个问题,就是这些lambda已经被定义成了函数变量,比如我定义了一个顶层函数:


//定义了一个顶层函数
fun getMaxSort(people: People): Int{
return (people.age) * 10 + people.name.length
}

或者排序条件已经定义成了一个变量值:


//排序条件
val condition = { people: People -> people.getMax() }

那这时如果我想再进行排序必须要这么写了:


//调用一遍函数
peopleList.sortBy { getMaxSort(it) }
//传递参数
peopleList.sortBy(condition)

然后这里我们可以利用成员引用 :: 符号来优化一下:


//直接就会调用顶层函数getMaxSort
peopleList.sortBy(::getMaxSort)
//直接就会调用People类的getMax函数
peopleList.sortBy(People::getMax)

这里看起来就是语法糖,可以简化代码。


成员引用 ::


你有没有想过这里是为什么,这里使用了 :: 符号其实就是把函数转换成了一个值,首先我们使用


val condition = { people: People -> people.getMax() }

这种时,其实condition就是一个函数类型的变量,这个我们之前文章说过,Kotlin支持完整的函数类型,而使用高阶函数可以用lambda,但是getMaxSort()函数它就是一个函数了,它不是一个值,除非你再外面给它包裹一层构成lambda,所以这里调用condition传递进的是sortBy()中,而getMaxSort(it)是以lambda的形式又包裹了一层。


但是使用 :: 符号后,也就是把函数转换成了一个值,比如 People::getMax 这就是一个值,它代表的就是People内的getMax函数。


而 ::getMaxSort 也是一个值,它表示getMaxSort函数。


使用范围


前面2个例子其实也就表明了这种成员引用的使用范围,一个是类的函数或者属性,还有就是顶层函数,它没有类名,可以省略。


绑定引用


这里再额外说一个知识点,前面说成员引用都是 类名:属性名 这种格式,比如 People::getMax ,但是它在后面KT版本变化后进行了优化,可以看下面代码:


//定义一个people实例
val people = People("zyh",10)
//利用成员引用,把函数转换成值
val ageFun = People::age
val age = ageFun(people)
//直接在对象实例上使用 ::
val ageValue = people::age

从上面我们发现,ageValue的值可以从实例上通过成员引用调用得到,不过这里区别就大了,ageFun是一个函数类型,而ageValue则是一个int值。


总结


总结一下,其实成员引用 :: 很简单,它就是把函数给转成了值,而这个值可以看成是函数类型,这样说就十分好理解了。


不过这个真实原理可不是这么简单,并不是利用lambda又把函数包裹了一层,这里应该是反射的相关知识,我们后续再具体来说其原理,刚好后续有反射相关的文章,大家可以点赞、关注一波。


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

一文扫清DDD核心概念理解障碍

引言 在前面的几篇文章中分别从DDD概念、核心思想以及代码落地等层面阐述了DDD的落地实践的过程,但是很多同学表示对于DDD的某些概念还是觉得不太好理解,影响了对于DDD的学习以及实际应用。因此本文针对大家反馈的问题进行详细的说明,希望可以用大白话的方式把这些...
继续阅读 »

引言


在前面的几篇文章中分别从DDD概念、核心思想以及代码落地等层面阐述了DDD的落地实践的过程,但是很多同学表示对于DDD的某些概念还是觉得不太好理解,影响了对于DDD的学习以及实际应用。因此本文针对大家反馈的问题进行详细的说明,希望可以用大白话的方式把这些看似复杂的概念形象化、简单化。


领域、子域、核心域等这么多域到底怎么理解?


在DDD的众多概念中,首先需要搞清楚的就是到底什么是领域。因为DDD是领域驱动设计,所以领域是DDD的核心基础概念。那么到底什么是领域呢?领域是指某个业务范围以及在范围内进行的活动。根据这个定义,我们知道领域最重要的一个核心点就是范围,只有限定了问题研究的范围,才能针对具体范围内的问题进行研究分析,在后期 进行微服务拆分的的时候也是根据范围来进行的。


我们开发的软件平台是为了解决用户问题,既然我们需要研究问题并解决问题,那就得先确定问题的范围到底是什么。如果我们做的是电商平台,那么我们研究的是电商这个领域或者说电商这个范围的问题,实体店内的销售情况就不是我们研究的问题范围了。因此我们可以把领域理解为我们需要解决指定业务范围内的问题域。再举个生活中的例子,派出所实际都是有自己的片区的也就是业务范围,户籍管理、治安等都是归片区的派出所负责的。这里的片区实际就是领域,派出所专注解决自己片区内的各种事项。


既然我们研究的领域确定了,或者说研究的问题域以及范围确定了,那么接下来就需要对领域进行进一步的划分和切割。实际上这和我们研究事物的一般方法手段是一致的,一旦某个问题太大无从下手的时候,都会将问题进行一步步的拆解,再逐个进行分析和解决。那么放到DDD中,我们在进行分析领域的时候,如果领域对应的业务范围过大,那么就需要对领域进行拆解划分,形成对应的子域或者说更小的问题域,所以说子域对应的是相对于领域来说,更小的业务范围以及问题域。


回到我们刚才所说的电商领域,它就是一个非常大的领域,因为电商实际还包含了商品、用户、营销、支付、订单、物流等等各种复杂的业务。因此支付域、物流域等就是相对于电商来说更小的业务范围或者更小的问题域,那么这部分领域就是对于电商这个领域的子域,相当于对电商这个业务范围的进一步的划分。


image.png


搞清楚了领域和子域的区别之后,那么怎么理解核心域、通用域以及支撑域这么多其他的域呢(域太多了,我脑袋开始嗡嗡响了)?领域和子域是按照范围大小进行区分的,那么核心域、通用域等实际就是按照功能属性进行划分的。


核心域:平台的核心竞争力、最重要的业务,比如对于阿里来说,电商就是核心域。


通用域:其他子域沉淀的通用能力,没有定制化的能力,比如公司的数据中台。


支撑域:不包含核心业务能力也不是各个子域的通用能力沉淀。


那么为什么划分了子域之后,还要分什么核心域、通用域呢?实际上这样划分的目的是根据子域的属性,确定公司对于不同域的资源投入。将优势的资源投入到具备核心竞争力的域上,也是为了让产品更加具备竞争力,就是所谓的钱要花到刀刃上。


限界上下文?限界是什么?上下文又是什么?


限界上下文我觉得是DDD中一个不太好理解的概念,光看这个不明觉厉的名字,甚至有点不知道它到底想表达什么样的意思。我们先来看下限界上下文的原文---Bounded Context,通过原文我们可以看得出来,实际上限界上下文这个翻译增加了我们的理解成本。而反观Bounded Context这个原文实际更好理解一点,即为有边界的上下文。这里给大家一个小建议,如果技术上某个概念不好理解,那么不妨去看看它的原文是什么,大部分情况下原文会比翻译过来的更好理解,更能反映设计者想要表达的真实含义。


image.png


大家都知道我们的语言是有上下文环境的,有的时候同样一句话在不同的语言环境或者说语言上下文中,所代表的意思是不一样的。打个比方假如你有一个女朋友,你们约好晚上一起去吃饭,你在去晚上吃饭地方的路上,这个时候你收到一条来自女朋友的语音:“我已经到龙翔桥了,你出来后往苹果店走。如果你到了,我还没到,你就等着吧。如果我到了,你还没到,你就等着吧。”这里的你就等着吧,在不同的语境下包含的意思是不同的,一个是陈述事实,一个让你瑟瑟发抖。


因此,既然语言本身就有上下文,那么用通用语言描述的业务肯定也是有边界的。DDD中的限界上下文就是用来圈定业务范围的,目的是为了确保业务语言在限界上下文内的表达唯一,不会产生额外的歧义。这个时候大家会不会有另外一个问题,那么这个限界上下文到底是一个逻辑概念还是代码层面会有一个实实在在的边界呢?


按照我自己的理解,限界上下文既是概念上的业务边界,也是代码层面的逻辑逻辑边界。为什么这么说呢?我们在进行业务划分的时候,领域划分为子域集合,子域再划分为子子域集合,那么子子域的业务边界有时候就会和限界上下文的边界重合,也就是说子子域本身就是限界上下文,那么此时限界上下文就是业务边界。在代码落地的过程中,用户服务涉及到用户的创建、用户信息的修改等操作。肯定不会到订单服务中去做这些事情。因为他们属于不同的业务域,也就是说订单相关的操作已经超越了用户的边界上下文,因此它应该在订单的边界上下文中进行。


域和边界上下文的关系是一对一或者一对多的关系,实际上我认为域和限界上下文本质上一致的,应该是为什么这么说呢,比如我们做的微服务当中用户服务,比如,肯定不会到订单服务中去做这些事情。因为他们属于不同的业务域,也就是说订单相关的操作已经超越了用户的边界上下文,因此它应该在订单的限界上下文中进行。限界上下文最主要的作用就是限定哪些业务面描述以及业务动作在这个限界当中。


image.png


总结
DDD在实际落地实践过程中会遇到各种各样的问题,首当其冲的就是一些核心概念晦涩难懂,阻碍了技术人员对DDD的理解和掌握。本文对DDD比较难理解的核心概念进行了详细的描述,相信通过本文大家对于这些核心概念的理解能够更加深入。


创作不易,如果各位同学觉得文章还不错的话,麻烦点赞+收藏+评论交流哦。老样子,文末和大家分享一首诗词。


西江月▪中秋和子由


世事一场大梦,人生几度秋凉?夜来风叶已鸣廊,看取眉头鬓上。


酒贱常愁客少,月明多被云妨。中秋谁与共孤光,把盏凄然北望。


作者:慕枫技术笔记
来源:https://juejin.cn/post/7084074668147081230 收起阅读 »

Redis 缓存穿透与缓存击穿

一、🐕缓存穿透(查不到数据) 比如 用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是先持久层数据库查询,发现也没有,于是本次查询失败,当用户很多的时候,缓存都没有命中时(一般为秒杀活动),于是都会去请求持久层数据库,这就会导致 给持...
继续阅读 »

一、🐕缓存穿透(查不到数据)


比如 用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是先持久层数据库查询,发现也没有,于是本次查询失败,当用户很多的时候,缓存都没有命中时(一般为秒杀活动),于是都会去请求持久层数据库,这就会导致 给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透



解决方案




布隆过滤器


布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力
在这里插入图片描述




缓存空对象


当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了持久层数据库
在这里插入图片描述


但是这种方法会存在两个问题


1、如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,并且当中还有还能多空值的键


2、即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响



二、🐂缓存击穿(量太大,缓存过期)


这里需要注意和缓存击穿的区别,缓存击穿,是指一个key非常热点,在不停的杠大并发,大并发集中对这个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一堵墙上戳穿了一个洞
当某个key 在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且写回缓存,会导致数据库瞬间压力过大



解决方案



设置热点数据永不过期



从缓存层面上看,没有设置过期时间,所以不会出现热点key过期后产生的问题



加互斥锁



分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获取分布式锁的权限,因此只需要等待即可,这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大



三、🐅缓存雪崩


缓存雪崩,是指在某一个时间段,缓存集中过期失效,Redis宕机


产生雪崩的原因之一,比如在双十一零点,有一波商品比较热门,所以这波商品会放在缓存,假设缓存一个小时,那么到了1点钟时,缓存即将过期,此时如果再对此波商品进行访问查询,压力最终就会落到数据库上,对于数据库而言,就会产生周期性的压力波峰,于是所有的请求都会到达存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。


在这里插入图片描述


其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存,此时,数据库也是可以顶住压力的。无非就是对数据库产生周期性的压力而已。而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。



解决方案


1、Redis 高可用


既然 redis 有可能挂掉,那我就设多几台 redis ,其实就是搭建集群


2、限流降级


在缓存失效后,通过加锁或者 队列来控制读数据库写缓存的线程数量,比如对某个key 只允许一个线程查询数据和写缓存,其他线程等待


3、数据预热


数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中,在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀



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

Python 中的万能之王 Lambda 函数

Python 提供了非常多的库和内置函数。有不同的方法可以执行相同的任务,而在 Python 中,有个万能之王函数:lambda 函数,它可以以不同的方式在任何地方使用。今天将和大家一起研究下这个万能之王!Lambda 函数简介Lambda函数也被称为匿名(没...
继续阅读 »

Python 提供了非常多的库和内置函数。有不同的方法可以执行相同的任务,而在 Python 中,有个万能之王函数:lambda 函数,它可以以不同的方式在任何地方使用。今天将和大家一起研究下这个万能之王!

Lambda 函数简介

Lambda函数也被称为匿名(没有名称)函数,它直接接受参数的数量以及使用该参数执行的条件或操作,该参数以冒号分隔,并返回最终结果。为了在大型代码库上编写代码时执行一项小任务,或者在函数中执行一项小任务,便在正常过程中使用lambda函数。
lambda argument_list:expersion
argument_list是参数列表,它的结构与Python中函数(function)的参数列表是一样的

a,b
a=1,b=2
*args
**kwargs
a,b=1,*args

....

expression是一个关于参数的表达式,表达式中出现的参数需要在argument_list中有定义,并且表达式只能是单行的。

1
None
a+b
sum(a)
1 if a >10 else 0
[i for i in range(10)]
...

普通函数和Lambda函数的区别

  1. 没有名称
    Lambda函数没有名称,而普通操作有一个合适的名称。

  2. Lambda函数没有返回值
    使用def关键字构建的普通函数返回值或序列数据类型,但在Lambda函数中返回一个完整的过程。假设我们想要检查数字是偶数还是奇数,使用lambda函数语法类似于下面的代码片段。

    b = lambda x: "Even" if x%2==0 else "Odd" b(9)

  3. 函数只在一行中
    Lambda函数只在一行中编写和创建,而在普通函数的中使用缩进

  4. 不用于代码重用
    Lambda函数不能用于代码重用,或者不能在任何其他文件中导入这个函数。相反,普通函数用于代码重用,可以在外部文件中使用。

为什么要使用Lambda函数?
一般情况下,我们不使用Lambda函数,而是将其与高阶函数一起使用。高阶函数是一种需要多个函数来完成任务的函数,或者当一个函数返回任何另一个函数时,可以选择使用Lambda函数。

什么是高阶函数?
通过一个例子来理解高阶函数。假设有一个整数列表,必须返回三个输出。

  • 一个列表中所有偶数的和

  • 一个列表中所有奇数的和

  • 一个所有能被三整除的数的和
    首先假设用普通函数来处理这个问题。在这种情况下,将声明三个不同的变量来存储各个任务,并使用一个for循环处理并返回结果三个变量。该方法常规可正常运行。

现在使用Lambda函数来解决这个问题,那么可以用三个不同的Lambda函数来检查一个待检验数是否是偶数,奇数,还是能被三整除,然后在结果中加上一个数。

def return_sum(func, lst):
result = 0
for i in lst:
  #if val satisfies func
  if func(i):
    result = result + i
return result
lst = [11,14,21,56,78,45,29,28]
x = lambda a: a%2 == 0
y = lambda a: a%2 != 0
z = lambda a: a%3 == 0
print(return_sum(x, lst))
print(return_sum(y, lst))
print(return_sum(z, lst))

这里创建了一个高阶函数,其中将Lambda函数作为一个部分传递给普通函数。其实这种类型的代码在互联网上随处可见。然而很多人在使用Python时都会忽略这个函数,或者只是偶尔使用它,但其实这些函数真的非常方便,同时也可以节省更多的代码行。接下来我们一起看看这些高阶函数。

Python内置高阶函数

Map函数

map() 会根据提供的函数对指定序列做映射。

Map函数是一个接受两个参数的函数。第一个参数 function 以参数序列中的每一个元素调用 function 函数,第二个是任何可迭代的序列数据类型。返回包含每次 function 函数返回值的新列表。

map(function, iterable, ...)
Map函数将定义在迭代器对象中的某种类型的操作。假设我们要将数组元素进行平方运算,即将一个数组的每个元素的平方映射到另一个产生所需结果的数组。

arr = [2,4,6,8] 
arr = list(map(lambda x: x*x, arr))
print(arr)

我们可以以不同的方式使用Map函数。假设有一个包含名称、地址等详细信息的字典列表,目标是生成一个包含所有名称的新列表。

students = [
          {"name": "John Doe",
            "father name": "Robert Doe",
            "Address": "123 Hall street"
            },
          {
            "name": "Rahul Garg",
            "father name": "Kamal Garg",
            "Address": "3-Upper-Street corner"
          },
          {
            "name": "Angela Steven",
            "father name": "Jabob steven",
            "Address": "Unknown"
          }
]
print(list(map(lambda student: student['name'], students)))
>>> ['John Doe', 'Rahul Garg', 'Angela Steven']

上述操作通常出现在从数据库或网络抓取获取数据等场景中。

Filter函数

Filter函数根据给定的特定条件过滤掉数据。即在函数中设定过滤条件,迭代元素,保留返回值为True 的元素。Map 函数对每个元素进行操作,而 filter 函数仅输出满足特定要求的元素。

假设有一个水果名称列表,任务是只输出那些名称中包含字符“g”的名称。

fruits = ['mango', 'apple', 'orange', 'cherry', 'grapes'] 
print(list(filter(lambda fruit: 'g' in fruit, fruits)))

filter(function or None, iterable) --> filter object

返回一个迭代器,为那些函数或项为真的可迭代项。如果函数为None,则返回为真的项。

Reduce函数

这个函数比较特别,不是 Python 的内置函数,需要通过from functools import reduce 导入。Reduce 从序列数据结构返回单个输出值,它通过应用一个给定的函数来减少元素。

reduce(function, sequence[, initial]) -> value

将包含两个参数的函数(function)累计应用于序列(sequence)的项,从左到右,从而将序列reduce至单个值。

如果存在initial,则将其放在项目之前的序列,并作为默认值时序列是空的。

假设有一个整数列表,并求得所有元素的总和。且使用reduce函数而不是使用for循环来处理此问题。

from functools import reduce
lst = [2,4,6,8,10]
print(reduce(lambda x, y: x+y, lst))
>>> 30

还可以使用 reduce 函数而不是for循环从列表中找到最大或最小的元素。

lst = [2,4,6,8]
# 找到最大元素
print(reduce(lambda x, y: x if x>y else y, lst))
# 找到最小元素
print(reduce(lambda x, y: x if x<y else y, lst))

高阶函数的替代方法

列表推导式

其实列表推导式只是一个for循环,用于添加新列表中的每一项,以从现有索引或一组元素创建一个新列表。之前使用map、filter和reduce完成的工作也可以使用列表推导式完成。然而,相比于使用Map和filter函数,很多人更喜欢使用列表推导式,也许是因为它更容易应用和记忆。

同样使用列表推导式将数组中每个元素进行平方运算,水果的例子也可以使用列表推导式来解决。

arr = [2,4,6,8]
arr = [i**2 for i in arr]
print(arr)
fruit_result = [fruit for fruit in fruits if 'g' in fruit]
print(fruit_result)

字典推导式

与列表推导式一样,使用字典推导式从现有的字典创建一个新字典。还可以从列表创建字典。

假设有一个整数列表,需要创建一个字典,其中键是列表中的每个元素,值是列表中的每个元素的平方。

lst = [2,4,6,8]
D1 = {item:item**2 for item in lst}
print(D1)
>>> {2: 4, 4: 16, 6: 36, 8: 64}
# 创建一个只包含奇数元素的字典
arr = [1,2,3,4,5,6,7,8]
D2 = {item: item**2 for item in arr if item %2 != 0}
print(D2)
>>> {1: 1, 3: 9, 5: 25, 7: 49}

一个简单应用

如何快速找到多个字典的公共键

方法一

dl = [d1, d2, d3] # d1, d2, d3为字典,目标找到所有字典的公共键
[k for k in dl[0] if all(map(lambda d: k in d, dl[1:]))]

dl = [{1:'life', 2: 'is'}, 
{1:'short', 3: 'i'},
{1: 'use', 4: 'python'}]
[k for k in dl[0] if all(map(lambda d: k in d, dl[1:]))]
# 1

解析

# 列表表达式遍历dl中第一个字典中的键
[k for k in dl[0]]
# [1, 2]

# lambda 匿名函数判断字典中的键,即k值是否在其余字典中
list(map(lambda d: 1 in d, dl[1:]))
# [True, True]
list(map(lambda d: 2 in d, dl[1:]))
#[False, False]

# 列表表达式条件为上述结果([True, True])全为True,则输出对应的k值
#1

方法二

# 利用集合(set)的交集操作
from functools import reduce
# reduce(lambda a, b: a*b, range(1,11)) # 10!
reduce(lambda a, b: a & b, map(dict.keys, dl))

写在最后
目前已经学习了Lambda函数是什么,以及Lambda函数的一些使用方法。随后又一起学习了Python中的高阶函数,以及如何在高阶函数中使用lambda函数。除此之外,还学习了高阶函数的替代方法:在列表推导式和字典推导式中执行之前操作。虽然这些方法看似简单,或者说你之前已经见到过这类方法,但你很可能很少使用它们。你可以尝试在其他更加复杂的函数中使用它们,以便使代码更加简洁。

作者:编程学习网
来源:https://juejin.cn/post/7084062324981497870

收起阅读 »

Flutter bottomSheet 高度自适应及溢出处理

最近在创建 bottomSheet的时候遇到一个问题:弹窗的高度无法根据其内容自适应 先放上显示弹窗的代码,如下: Future<T?> showSheet<T>( BuildContext context, Widg...
继续阅读 »

最近在创建 bottomSheet的时候遇到一个问题:弹窗的高度无法根据其内容自适应



先放上显示弹窗的代码,如下:


Future<T?> showSheet<T>(
BuildContext context,
Widget body, {
bool scrollControlled = false,
Color bodyColor = Colors.white,
EdgeInsets? bodyPadding,
BorderRadius? borderRadius,
}) {
const radius = Radius.circular(16);
borderRadius ??= const BorderRadius.only(topLeft: radius, topRight: radius);
bodyPadding ??= const EdgeInsets.all(20);
return showModalBottomSheet(
context: context,
elevation: 0,
backgroundColor: bodyColor,
shape: RoundedRectangleBorder(borderRadius: borderRadius),
barrierColor: Colors.black.withOpacity(0.25),
// A处
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height - MediaQuery.of(context).viewPadding.top),
isScrollControlled: scrollControlled,
builder: (ctx) => Padding(
padding: EdgeInsets.only(
left: bodyPadding!.left,
top: bodyPadding.top,
right: bodyPadding.right,
// B处
bottom: bodyPadding.bottom + MediaQuery.of(ctx).viewPadding.bottom,
),
child: body,
));
}


其中,A处、B处的作用就是,让弹窗的内容始终显示在安全区域内



高度自适应问题


首先,我们在弹窗中显示点内容:


showSheet(context, Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: const [
Text('这是第一行'),
],
));

效果如下图所示:




此时,我们只需要将显示内容的代码改为如下:


showSheet(context, Column(
mainAxisSize: MainAxisSize.min, // 这一行是关键所在
crossAxisAlignment: CrossAxisAlignment.stretch,
children: const [
Text('这是第一行'),
],
));

现在的效果图如下:




现在我们可以看到,弹窗的高度已经根据内容自适应了。


内容溢出问题


前面的解决方式,仅在内容高度小于默认高度时有效。当内容过多,高度大于默认高度时,就会出现溢出警告,如下图所示:




此时,我们该怎么办呢?


答案是:运用 showModalBottomSheet 的 isScrollControlled 参数,将其设置为true即可,代码如下:


showSheet(context, Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: const [
Text('这是第一行'),
Text('这是很长长..(此处省略若干字)..的一段话,')
],
), scrollControlled: true); // 这一行用于告诉系统,弹窗的内容完全由我们自己管理

此时,效果图如下:



showSheet 补充说明


对前面showSheet代码中,A处、B处的进一步说明:


A处:如果不对内容的高度进行限制,则内容会显示在状态栏之后,而引起用户交互问题。如下图所示:



B处:如果不加 MediaQuery.of(ctx).viewPadding.bottom 这一句,则内容有可能会显示在底部横条的下方,此时也不利于交互


最终版本图样


内容较少(高度跟随内容自适应):



内容很多(顶部、底部均显示在安全区域内):



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

2022了,来体验下 flutter web

前言 flutter从 17年 推出,18年12月 开始发布 1.0 版本,2021年3月 发布 2.0 增加了对桌面和 web 应用的支持。 最大特点是基于skia实现自绘引擎,使用dart语言开发,既支持JIT(just in time: 即时编译)又支持...
继续阅读 »

前言


flutter从 17年 推出,18年12月 开始发布 1.0 版本,2021年3月 发布 2.0 增加了对桌面和 web 应用的支持。
最大特点是基于skia实现自绘引擎,使用dart语言开发,既支持JIT(just in time: 即时编译)又支持AOT(ahead of time: 提前编译),开发阶段使用JIT模式提高时效性,同时在发布阶段使用AOT模式提高编译性能。
作为前端的话,还是更关注flutter web的支持情况。为了体验flutter web,特意用flutter写了个小游戏来看编译后的代码在web上运行如何。


开始之前


早在3年前的 19年初 1.0 出来没多久的时候就尝试用flutter来写一些常见的菜单注册登录等页面demo来,那时候flutter的生态还在发展中,除了官方提供的一些解决方案,三方的一些包很多都不成体系,应用范围较小,由于当时是抱着前端的固有思路来尝鲜flutter,flutter 刚发展起来,轮子远没有那么多,发现写起来远没有Vue、React 这类生态成熟的框架写起来舒服,除了 widget 组件多,写起UI来可以直接看文档写完很方便外,网络请求,路由管理、状态管理(这些像vue有axios/vue-router/vuex)用官方的方法写起来相当麻烦(也可能是我不会用,对新手不友好),维护起来就更麻烦了。


过去3年了,再看flutter,2.0版本发布也快一年了,当再次想用flutter写个demo的时候,发现了社区已经出现了一些经过几年发展的provider、getx之类的状态管理框架,能帮助新手快速入门,用了 getx 感觉是个脚手架,又不仅仅是脚手架,简直是大而全的轮子,状态管理、路由管理一应俱全,生成的目录结构清晰,你只需要去填充 UI 和处理数据。用法也很简单,对新手很友好。


flutter + getx 写一个小游戏


既然选好了那就用 getx 生成项目目录,开始开发,选用了一个很常见的小游戏:数字华容道,功能也简单。 项目地址


项目可以打包成原生应用,也可以打包成 web 应用


数字华容道web版


flutter web 渲染模式


不同的渲染器在不同场景下各有优势,因此 Flutter 同时支持以下两种渲染模式:


HTML 渲染器: 结合了 HTML 元素、CSS、Canvas 和 SVG。该渲染模式的下载文件体积较小。
CanvasKit 渲染器: 渲染效果与 Flutter 移动和桌面端完全一致,性能更好,widget 密度更高,但增加了约 2MB 的下载文件体积。
为了针对每个设备的特性优化您的 Flutter web 应用,渲染模式默认设置为自动。这意味着您的应用将在移动浏览器上使用 HTML 渲染器运行,在桌面浏览器上使用 CanvasKit 渲染器运行。官方文档




使用 HTML 渲染


flutter run -d chrome --web-renderer html
复制代码

使用 HTML,CSS,Canvas 和 SVG 元素来渲染,应用的大小相对较小,元素数量多,请求都是http2


元素如下



请求如下



使用 CanvasKit 渲染


CanvasKit 是以 WASM 为编译目标的Web平台图形绘制接口,其目标是将 Skia 的图形 API 导出到 Web 平台。


flutter run -d chrome --web-renderer canvaskit
复制代码

默认 CanvasKit 渲染,元素数量比html少很多,就是需要请求 canvaskit.wasm,该文件大小7MB左右、默认在 unpkg.com 国内加载速度慢,可以将文件放到国内 cdn 以提升请求效率



元素如下



请求如下,部分还使用了http3



小结


flutter web 通过编译成浏览器可运行的代码,经实践来看,性能还是有些问题,不过如果是单单想要写SPA,那恐怕还是js首选。目前来说flutter的生态经过几年的发展已经有了很多开源轮子,但要说稳定性还无法击败js,要不要用 flutter web 就要根据实际需求来决定了。


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

Flutter 项目复盘 纯纯的实战经验

一. 项目开始 1. 新建flutter项目时首先明确native端用的语言 java还是kotlin , objectC 还是swift ,否则选错了后期换挺麻烦的 2. 选择自己的路由管理和状态管理包,决定项目架构 以我而言 第一个项目用的 fluro 和...
继续阅读 »

一. 项目开始


1. 新建flutter项目时首先明确native端用的语言 java还是kotlin , objectC 还是swift ,否则选错了后期换挺麻烦的


2. 选择自己的路由管理和状态管理包,决定项目架构
以我而言 第一个项目用的 fluro 和 provider 一个路由管理一个状态管理,项目目录新建store和route文件夹,分别存放provider的model文件和fluro的配置文件,到了第二个项目,发现了Getx,一个集合了依赖注入,路由管理,状态管理的包,用起来! 项目目录结构有了很大的变化,整体条理整洁


第一个 image.png


第二个 image.png


3. 常用包配置,比如 Getx 需要把外层MaterialApp换成GetMaterialApp, flutter_screenutil 需要初始化设计图比例,provider全局导入,Dio 封装,拦截器,网络提示等等


二. 全局配置


1. 复用样式


1. 由于flutter 某些小widget复用性很高,而App 需要统一样式 ,样式颜色之类的预设文件放在command文件夹内


colours.dart ,可以预设静态class,存储常用主题色
image.png


styles.dart,可以预设 字体样式 分割线样式 各种固定值间隔


image.png


2. 建议全局管理后端接口,整洁还便于维护,舒服


3. models 文件夹


models 文件夹,可能在web端并不常用,但是在dart里我觉得很需要,后端返回的Json 字符串,一定要通过model类 格式化为一个类,可以极大地减少拼写错误或者类型错误, . 语法也比 [''] 用起来舒服的多推荐一个网站 quickType 输入json对象,一键输出model类!


4. 是否强制横竖屏?


需要在main.dart里配置好


// 强制横屏
SystemChrome.setPreferredOrientations(
[DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]);

5. 是否需要修改顶部 底部状态栏布局以及样式?


SystemUiOverlayStyleSystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle); 来配置


6. 设置字体不跟随系统


参考地址


class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}

class _MyAppState extends State with WidgetsBindingObserver {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Container(color: Colors.white),
builder: (context, widget) {
return MediaQuery(
//设置文字大小不随系统设置改变
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
child: widget,
);
},
);
}
}

7. 国际化配置


使用部分widget会显示英文,比如IOS风格的dialog,显示中文这需要设置一下了,
首先需要一个包支持,


  flutter_localizations:
sdk: flutter

引入包,然后在main.dart MetrialApp 的配置项中加入


   // 设置本地化,部分原生内容显示中文,例如长按选中文本显示复制、剪切
localizationsDelegates: [
GlobalMaterialLocalizations.delegate, //国际化
GlobalWidgetsLocalizations.delegate,//国际化
const FallbackCupertinoLocalisationsDelegate() // 这里是为了解决一个报错 看第 8 条
],
//国际化
supportedLocales: [
const Locale('zh', 'CH'),
const Locale('en', 'US'),
],

8. 使用CupertinoAlertDialog报错:The getter 'alertDialogLabel' was called on null


解决方法:
在main.dart中 加入如下类,然后在MetrialApp 的 localizationsDelegates 中实例化 见第 7 条


class FallbackCupertinoLocalisationsDelegate
extends LocalizationsDelegate
{
const FallbackCupertinoLocalisationsDelegate();

@override
bool isSupported(Locale locale)
=> true;

@override
Future
load(Locale locale) =>
DefaultCupertinoLocalizations.load(locale);

@override
bool shouldReload(FallbackCupertinoLocalisationsDelegate old)
=> false;
}

9. ImageCache


最近版本的flutter更新,限制了catchedImage的上限, 100张 1000mb ,而业务需求却需要缓存更多,设置一下了这需要


    class ChangeCatchImage extends WidgetsFlutterBinding {
@override
createImageCache() *{*
Global.myImageCatche = ImageCache()
..maximumSize = 1000
..maximumSizeBytes = 1000 << 20; // 1000MB
return Global.myImageCatche;
}
}

然后在main.dart runApp那里实例化一下ChangeCatchImage() 就可以了


三. 业务模块


常见的业务模块代码分析,比如登录页,闪屏页,首页,退出登录等

1. 首先安利一下Getx


一个文件夹就是一个业务模块,独自管理数据,通过依赖注入数据共享,
舒服


image.png


包括 logic 逻辑控制层 state 数据管理层 view 视图组件层 ,当前业务的复用widget写在文件夹下


2. 登录模块


作为app的入口门户,炫酷美观是少不了的,这就需要关注性能优化,而输入的地方,验证的逻辑要有安全设计



  • 首先关于动画性能优化,最关键的一点是精准的更新需要变化的组件,我们可以通过devtool的工具查看更新范围


image.png



  • 其次时安全设计,简单的来看,限制登录次数,禁止简易密码,加密传输,验证token等,进阶版的比如,防止参数注入,过滤敏感字符等

  • 登录之前的账户验证,密码验证,必填项等,然后登录请求,需要加loading,按钮禁用,就不需要防抖了

  • 登录之后保存到本地用户基本信息(可能存在安全问题,暂未深究),然后下次登陆默认检测是否存在基本信息,并验证过期时间,和token,之后隐式登录到首页


3. splash闪屏模块


app登陆首页的准备页面,可以嵌入广告,或者定制软件宣传动画,提示三秒后跳过
如何优雅的加入app闪屏页?
其实就是在main.dart里把初始化页面设置为splash页面,之后通过跳转逻辑
判断去首页还是登录注册页面
比如这里我用了Getx 就简单配置一下


image.png


4. 操作引导模块


第一次使用app,或者重大更新之后往往会有操作引导
我的项目里用到了两种类型的操作引导


成果图
第一种


1.jpg


第二种


image.png
image.png


二者都是基于overlayEntry()Overlay.of(context).insert(overlayEntry)实现的
第二种用了一个包 操作引导 flutter_intro: ^2.2.1,绑定Widget的GlobalKey,来获取Element信息,拿到位置大小,确保框选的位置正确,外层遮罩与第一种一样都是用overlayEntry()创建的


k.png


创建之后,展示出来
Overlay.of(context).insert(your_overlayEntry)
在某个按钮处切换下一个 比如点击我知道了,下一页之类的


     onPressed: () {
// 执行 remove 方法销毁 第一个overlayEntry 实例
overlayEntryTap.remove();
// 第二个
Overlay.of(context).insert(overlayEntryScale);
},

关于第二个实现涉及的flutter_intro包,粘一下我的代码,详细的可以参照pub食用


final intro = Intro(
// 一共有几步,这里就会创建2个GlobalKey,一会用到
stepCount: 2,
// 点击遮罩下一个
maskClosable: true,
// 高亮区域与 widget 的内边距
padding: EdgeInsets.all(0),
// 高亮区域的圆角半径
borderRadius: BorderRadius.all(Radius.circular(4)),
// use defaultTheme
widgetBuilder: StepWidgetBuilder.useDefaultTheme(
texts: ["点击添加收藏", "下拉添加书签"],
buttonTextBuilder: (currPage, totalPage) {
return currPage < totalPage - 1
? '我知道了 ${currPage + 3}/${totalPage + 2}'
: '完成 ${currPage + 3}/${totalPage + 2}';
},
),
);

......
// 这里用到key来绑定任意Widget
Positioned(
key: intro.keys[1],
top: 0,
right: 20,
...
)
......

5. CustomPaint 绘图画板模块


成果图


image.png


当初选择flutter就是因为,有大量的绘制需求,看中了自带skia,绘制效率高且流畅而且具备平台一致性
结果坑也不少


首先来讲一下 猪脚 CustomPaint


顾名思义,这是一个个性化绘制组件,他的工作就是给你创建一个画布,你想怎么画怎么画,我们直接看怎么用
首先格式化写法



  • 首先 需要写在widget 树里吧
    Container( 
    child: CustomPaint(
    painter: myPainter(),
    ),

    看一下参数列表,发现painter 接收一个CustomePainter对象,这里可以注意一下child参数,很奇怪明明绘制界面都放在painter里了,留一个child干啥用??? 其实有大用,这里面放是他的子widget,但是不参与绘制更新的,通俗一点就是我绘制一片流动的云彩,但是有个静止的太阳,云彩的位置是实时repaint的,这时就可以把太阳widget放在child中,优化性能


image.png



  • 接下来我们创建myPainter()


    class myPainter extends CustomPainter { 
@override
void paint(Canvas canvas, Size size) {
// 创建画笔
final Paint paint = Paint();
// 绘制一个圆
canvas.drawCircle(Offset(50, 50), 5, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;



  • 到这里 我们需要实现两个重要的函数(如上代码)


第一个paint()函数,自带了画布对象 canvas,和画布尺寸 size,这样我们就可以使用Canvas的内置绘制函数了!
而绘制函数,都需要接收一个Paint 画笔对象


image.png


这个画笔对象,就是用来设置画笔颜色,粗细,样式,接头样式等等


Paint paint = Paint(); 
//设置画笔
paint ..style = PaintingStyle.stroke
..color = Colors.red
..strokeWidth = 10;


第二个函数shouldRepaint() 顾名思义判断是否需要重绘,如果返回false就是不需要重绘,只执行一次paine(),返回true就是总是重绘,依据实际需求设置
如果需要绘制类似于 根据数值不断变高的柱状图动画


代码如下(搬走就能用哦)


class BarChartPainter extends CustomPainter {
final List datas;
final List datasrc;
final List xAxis;
final double max;
final Animation animation;

BarChartPainter(
{@required this.xAxis,
@required this.datas,
this.max,
this.datasrc,
this.animation})
: super(repaint: animation);

@override
void paint(Canvas canvas, Size size) {
_darwBars(canvas, size);
_drawAxis(canvas, size);
}

@override
bool shouldRepaint(BarChartPainter oldDelegate) => true;

// 绘制坐标轴
void _drawAxis(Canvas canvas, Size size) {
final double sw = size.width;
final double sh = size.height;

// 使用 Paint 定义路径的样式
final Paint paint = Paint()
..color = Colors.grey
..style = PaintingStyle.stroke
..strokeWidth = 1
..strokeCap = StrokeCap.round;

// 使用 Path 定义绘制的路径,从画布的左上角到左下角在到右下角
final Path path = Path()
..moveTo(40, sh)
..lineTo(sw - 20, sh);

// 使用 drawPath 方法绘制路径
canvas.drawPath(path, paint);
}

// 绘制柱形
void _darwBars(Canvas canvas, Size size) {
final sh = size.height;
final paint = Paint()..style = PaintingStyle.fill;
final double _barWidth = size.width / 20;
final double _barGap = size.width / 25 * 2 + 18;
final double textFontSize = 14.0;

for (int i = 0; i < datas.length; i++) {
final double data = datas[i] * ((size.height - 15) / max);
final top = sh - data;
// 矩形的左边缘为当前索引值乘以矩形宽度加上矩形之间的间距
final double left = i * _barWidth + (i * _barGap) + _barGap;
// 使用 Rect.fromLTWH 方法创建要绘制的矩形
final rect = RRect.fromLTRBAndCorners(
left, top, left + _barWidth, top + data,
topLeft: Radius.circular(5), topRight: Radius.circular(3));
// 使用 drawRect 方法绘制矩形

final offset = Offset(
left + _barWidth / 2 - textFontSize / 2 - 8,
top - textFontSize - 5,
);
paint.color = Color(0xFF59C8FD);

//绘制bar
canvas.drawRRect(rect, paint);

// 使用 TextPainter 绘制矩形上放的数值
TextPainter(
text: TextSpan(
text: datas[i] == 0.0 ? '' : datas[i].toStringAsFixed(0) + " %",
style: TextStyle(
fontSize: textFontSize,
color: paint.color,
// color: Colours.gray_33,
),
),
textAlign: TextAlign.center,
textDirection: TextDirection.ltr,
)
..layout(
minWidth: 0,
maxWidth: textFontSize * data.toString().length,
)
..paint(canvas, offset);

final xData = xAxis[i];
final xOffset = Offset(left, sh + 6);
// 绘制横轴标识
TextPainter(
textAlign: TextAlign.center,
text: TextSpan(
text: '$xData' != ''
? '$xData'.substring(0, 4) + '-' + '$xData'.substring(4, 6)
: '',
style: TextStyle(
fontSize: 12,
color: Colors.black,
),
),
textDirection: TextDirection.ltr,
)
..layout(
minWidth: 0,
maxWidth: size.width,
)
..paint(canvas, xOffset);
}
}
}

好了,customPainter,大体就这么用,下面回归话题,绘制画板
其实整体任务相当复杂,这里刨析一处,其他的融会贯通


拿最经典的铅笔画图来说
其实单纯的实现铅笔画图,甚至带笔锋,类似于签名,都很简单,网上教程一堆


大体思路就是 加一个GestureDetector ,主要用 onPanUpdate事件实时触发绘制动作,用canvas绘制出来
绘制简单,但是性能优化复杂


这里直接给出我测试的最优解
先把新的坐标点与之前的点连成线,可以一次多连接几个,也就是类似于节流的处理手法,
比如等panUpate触发了五次回调,先都把这五个点连接成线,第六次再统一绘制一条线(要是还有啥好办法,希望不吝赐教!)
详细的以后单独整理出来一个项目


6. websocket 即时通讯模块


成果图


Inked2_LI.jpg
只做了最基本的文字 图片 文件功能


简单把各项功能实现说一下,以后会详细整理,并加入音视频



  • 关于websocket


    首先肯定是连接websocket,用到一个包 web_socket_channel


    然后初始化websocket


    // 初始化websocket
    initWebsocket(){
    Global.channel = IOWebSocketChannel.connect(
    WebsocketUrl, // websocket地址
    //这个参数注意一下, 这里是每隔10000毫秒发送ping,如果间隔10000ms没收到pong,就默认断开连接
    //所以收网速等影响,这个参数如果太小,比如100ms就会,出现过一阵子自己断开连接的问题,参考实际设置
    pingInterval: Duration(milliseconds: 10000),
    );
    // 监听服务端消息
    Global.channel.stream.listen(
    (mes) => onMessage(mes),// 处理消息
    onError: (error) => {onError(error)}, // 连接错误
    onDone: () => {onDone()}, // 断开连接
    cancelOnError: true //设置错误时取消订阅
    );
    }



  • 处理消息


    进入页面加载聊天消息,长列表还是得用ListView.build(),消息多的时候体验好很多


    每次监听到新消息,加入到数组中,并更新视图,这一步不同的状态管理方法不同.


    加入消息这里就有难点了


    首先分四种情况 a. 自己发的并且在ListView底部,b. 自己发的但是不在ListView底部, c. 别人发的消息并且在底部,d. 别人发的不在底部.


    a 和 b,c: 只要是自己发得就滚动到底部,在底部时就滚动的慢点,有种消息上拉的感觉


    // 这里要确保在LIstView中已经加入并渲染完成新消息
    // 我的处理就是加了一个延迟,再滚动
    // 直接滚动到ListView底部
    scrollController.jumpTo(scrollController.position.maxScrollExtent);
    // 滚动到某个确定的元素
    Scrollable.ensureVisible(
    // 给每一条消息对象加GlobalKey,获取到当前上下文
    state.messageList[index].key.currentContext,
    duration: Duration(milliseconds: 100),
    curve: Curves.easeInOut,
    // 控制对齐方式
    alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd);

    d : 这种情况,做了个类似于微信的提示




image.png


但是点击定位到消息有坑了,因为用的listView.build,当你在翻阅上边的消息,下面的消息并没有加载,因此获取不到currentContext,因为元素并没有渲染,也就定位错乱了,目前最理想的解决办法就是,往上翻的时候,之下的记录全部渲染,往下滑时再依次清.




  • 文件和图片


    用到了几个包 file_picker, open_file, path_provider


    file_picker ,用来选择文件和图片,可以配置单选多选,需要在安卓的配置文件里加权限


    open_file , 类似于微信点击文件,先下载,然后调用本地默认程序打开文件


    path_provider,提供系统可用路径,用于创建文件目录


    具体使用如下


     // 访问不到app私有目录 导致我卡了很久...
    // Directory dirloc = await getTemporaryDirectory();
    // 访问外置存储目录
    final dirPath = await getExternalStorageDirectory();
    Directory file = Directory(dirPath.path + "/" + "temFile");
    // 不存在就创建目录
    try {
    bool exists = await file.exists();
    if (!exists) {
    await file.create(); // 创建了temFile 目录 用于缓存文件
    }
    } catch (e) {
    print(e);
    }
    // 下边就很关键了 可能不同的后端数据不同实现
    // 请求存储权限 需要一个包 permission_handler: ^6.1.1
    Permission.storage.request().then((value) async {
    //如果许可
    if (value.isGranted) {
    // 判断文件是否存在 wjmc 就是一个变量存储着文件名
    File _tempFile = File(file.path + '/' + wjmc);
    if (!await _tempFile.exists()) {
    try {
    //1、创建文件地址 带扩展 我用了getx cstate
    // final ChatState cState = Get.find().state;
    // 这是一个通用组件 不管理数据 从chatState里注入
    cState.path = file.path + '/' + wjmc;
    //2、下载文件到本地
    cState.downloading.value = nbbh;
    var response = await dio.get(fileUrl);
    Stream resp = response.data.stream;
    //4. 转为uint8类型
    final Uint8List bytes =
    await consolidateHttpClientResponseBytes(resp);
    //5. 转为List 并写入文件
    final List _filelist = List.from(bytes);
    final filePath = File(cState.path);
    await filePath.writeAsBytes(_filelist,
    mode: FileMode.append, flush: true);
    } catch (e) {
    print(e);
    }
    }
    cState.downloading.value = '';
    // 6.这里可以记录位置,保存path到一个数组里,退出软件之后清除缓存 我没做
    open(cState.path);
    }
    });
    // 读取Stream 文件流 处理为Uint8List
    Future consolidateHttpClientResponseBytes(Stream response) {
    final Completer completer = Completer.sync();
    final List> chunks = >[];
    int contentLength = 0;
    response.listen((chunk) {
    chunks.add(chunk);
    contentLength += chunk.length;
    }, onDone: () {
    final Uint8List bytes = Uint8List(contentLength);
    int offset = 0;
    for (List chunk in chunks) {
    bytes.setRange(offset, offset + chunk.length, chunk);
    offset += chunk.length;
    }
    completer.complete(bytes);
    }, onError: completer.completeError, cancelOnError: true);

    return completer.future;
    }
    void open(path) {
    // 下载完成 准备打开文件
    showCupertinoDialog(
    context: Get.context,// 舒服
    builder: (context) {
    return Material(
    color: Colors.transparent,
    child: CupertinoAlertDialog(
    title: Padding(
    padding: EdgeInsets.only(bottom: 10),
    child: Text("提示"),
    ),
    content: Padding(
    padding: EdgeInsets.only(left: 5),
    child: Text("是否打开文件?"),
    ),
    actions: [
    CupertinoButton(
    child: Text(
    "取消",
    style: TextStyle(color: Colours.gray_88),
    ),
    onPressed: () {
    Get.back();
    },
    ),
    CupertinoButton(
    child: Text("确定"),
    onPressed: () async {
    Get.back();
    // 直接调用就能打开,会通过系统默认程序打开 比如.doc 默认用office等.
    await OpenFile.open(
    cState.path,
    );
    }),
    ]),
    );
    },
    );
    }

    音视频用的小鱼易连,但是木有Flutter SDK ,只能基于安卓的去封装,以后有机会再讲讲.


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

系统模块划分设计的思考

系统模块划分设计的思考前言首先明确一下,这里所说的系统模块划分,是针对client,service,common这样的技术划分,而不是针对具体业务的模块划分。避免由于歧义,造成你的时间浪费。直接原因公司内部某技术团队,在引用我们系统的client包时,启动失败...
继续阅读 »

系统模块划分设计的思考

前言

首先明确一下,这里所说的系统模块划分,是针对client,service,common这样的技术划分,而不是针对具体业务的模块划分。避免由于歧义,造成你的时间浪费。

直接原因

公司内部某技术团队,在引用我们系统的client包时,启动失败。
失败原因是由于client下有一个cache相关的依赖,其注入失败导致的。

然后,就发出了这样一个疑问:我只是希望使用一个hsf接口,为什么还要引入诸如缓存,web处理工具等不相关的东西。

这也就自然地引出了前辈对我的一句教导:对外的client需要尽可能地轻便。

很明显,我们原有的client太重了,包含了对外的RPC接口,相关模型(如xxxDTO),工具包等等。

可能有人就要提出,直接RPC+模型一个包,其它内容一个包不就OK了嘛?

问题真的就这么简单嘛?

根本原因

其实出现上述问题,是因为在系统设计之初,并没有深入思考client包的定位,以及日后可能遇到的情况。

这也就导致了今天这样的局面,所幸目前的外部引用并不多,更多是内部引用。及时调整,推广新的依赖,与相关规范为时不晚。

常见模块拆分

先说说我以前的模块拆分。最早的拆分是每个业务模块主要拆分为:

  • xxx-service:具体业务实现模块。

  • xxx-client:对外提供的RPC接口模块。

  • xxx-common:对外的工具,以及模型。

这种拆分方式,是我早期从一份微服务教程中看到的。优点是简单明了,调用方可选择性地选择需要的模块引入。

至于一些通用的组件,如统一返回格式(如ServerResponse,RtObject),则放在了最早的核心(功能核心,但内容很少)模块上。

后来,认为这样并不合适,不应该将通用组件放在一个业务模块上。所以建立了一个base模块,用来将通用的组件,如工具,统一返回格式等都放入其中。

另外,将每个服务都有的xxx-common模块给取消了。将其中的模型,放入了xxx-client,毕竟是外部调用需要的。将其中的工具,根据需要进行拆分:

  • base:多个服务都会使用的。

  • xxx-service:只有这个服务本身使用

  • xxx-client:有限的服务使用,并且往往是服务提供方和服务调用方都要使用。但是往往这种情况,大多是由于接口设计存在问题导致的。所以多为过渡方案。

上述这个方案,也就是我在负责某物联网项目时采用的最终模块划分方式。

在当时的业务下,该方案的优点是模块清晰,较为简洁,并且尽可能满足了迪米特原则(可以参考《阿里Java开发手册相关实践》)。缺点则是需要一定的技术水平,对组件的功能域认识清晰。并且需要有一定的设计思考与能力(如上述工具拆分的第三点-xxx-client,明白为什么这是设计缺陷导致,并能够解决)。

新的问题

那么,既然上述的方案挺不错的,为什么不复用到现在的项目呢?

因为业务变了,导致应用场景变了。而这也带来了新的问题,新的考虑角度。

原先的物联网业务规模并不大,所以依赖也较为简单,也并不需要进行依赖的封装等,所以针对主要是client的内/外这一维度考虑的。

但是现有的业务场景,由于规模较大,模块依赖层级较多,导致上层模块引入过多的依赖。如有一个缓存模块,依赖tair-starter(一个封装的key-value的存储),然后日志模块依赖该缓存模块(进行性能优化),紧接着日志模块作为一个通用模块,被放入了common模块中。依赖链路如下:

调用方 -> common模块 -> 日志模块 -> 缓存模块 -> tair-starter依赖

但是有的调用方表示,根本就不需要日志模块,却引入了tair-starter这一重依赖(starter作为封装,都比较重),甚至由于tair-starter的内部依赖与自身原有依赖冲突,得去排查依赖,进行exclude。
但是同时,也有的调用方,系统通过rich客户端,达到性能优化等目标。

所以,现有的业务场景除了需要考虑client的内/外这一维度,还需要考虑client的pool/rich这一维度。

可能有的小伙伴,看到这里有点晕乎乎的,这两个维度考量的核心在哪里?

内/外,考虑的是按照内外这条线,尽量将client设计得简洁,避免给调用方引入无用依赖。

而pool/rich,考虑的是性能,用户的使用成本(是否开箱即用)等。

最终解决方案

最终的解决方案是对外提供3+n

  • xxx-client(1个):所有外部系统引用都需要的内容,如统一返回格式等。

  • xxx-yyy-client(n个):对具体业务依赖的引用,进行了二次拆分。如xxx-order-client(这里是用订单提花那你一下,大家理解意思就OK)。

  • xxx-pool-client(1个):系统引用所需要的基本依赖,如Lindorm的依赖等。

  • xxx-rich-client(1个):系统引用所需要的依赖与对应starter,如一些自定义的自动装载starter(不需要用户进行配置)。

这个方案,换个思路,理解也简单。
我们提供相关的能力,具体如何选择,交给调用方决定。

其实,讨论中还提到了BOM方案(通过DependentManagement进行jar包版本管理)。不过分析后,我们认为BOM方案更适合那些依赖集比较稳定的client,如一些中间件。而我们目前的业务系统,还在快速发展,所以并不适用。

总结

简单来说,直接从用户需求考虑(这里的用户就是调用方):

  • 外部依赖:

    • 额外引入的依赖尽可能地少,最好只引入二方依赖(我们提供的jar),不引入第三方依赖。

    • 引入的二方依赖不“夹带”私货(如二方jar引入了一堆大第三方依赖)。

  • 自动配置:

    • 可以傻瓜式使用。如引入对应的starter依赖,就可以自动装配对应默认配置。

    • 也可以自定义配置。用户可以在自定义配置,并不用引入无效的配置(因为starter经常引入不需要的依赖)。

  • 性能:

    • 可以通过starter,提供一定的封装,保证一定的性能(如接口缓存,请求合并等)。

    • 可以自定义实现基础功能。因为有些人并不放心功能封装(虽然只是少数,但是稳定性前辈提出的)。

补充

这里补充一点,我对讨论中一个问题的回答,这里提一下。

有人提到工具类,应该如何划分。因为有的工具类,是不依赖于服务状态的,如CookieUtil进行Cookie处理。有的工具类,是依赖于服务状态的,如RedisUtil包含RedisPool状态,直连Redis,处理Redis请求与响应。

其实这里有两个细节:

  • 工具应该按照上面的方式进行划分为两种。单一模块系统,不依赖服务状态的工具往往置于util包,依赖服务状态的工具往往置于common包中。这里还有一个明显的区分点:前者的方法可以设为static,而后者并不能(因为依赖于new出来的状态)。

  • 依赖于状态的工具类,其实是一种拆分不完全的体现。如RedisUtil,可以拆分为连接状态管理的RedisPool与请求响应处理的RedisUitl。两者的组合方式有很多,取决于使用者的需要,以后有机会写一篇相关的博客。不过,我希望大家记住 面向接口编程的原则。

愿与诸君共进步。

原文链接:https://blog.csdn.net/cureking/article/details/105663951

收起阅读 »

从零到阿里的三年

一、背景三年的时间,可以做些什么呢?又或者说,可以做成什么呢?每个人都有各自的机遇、背负、努力。所以这永远没有一个标准答案,有的只是每个人自己的答案。而我能做的,就说出自己的故事,给大家一份参考。如果可以帮助到大家,那就是极好的了。关键词:真实、履历、思考、效...
继续阅读 »

一、背景

三年的时间,可以做些什么呢?又或者说,可以做成什么呢?
每个人都有各自的机遇、背负、努力。所以这永远没有一个标准答案,有的只是每个人自己的答案。
而我能做的,就说出自己的故事,给大家一份参考。如果可以帮助到大家,那就是极好的了。

关键词:真实、履历、思考、效率、执行、不足

履历

先说一下履历吧。毕竟没有履历,可能被人认为是贩卖焦虑的软文。

  • 18.5~18.12:杭州某在线教育公司

    • 岗位:产品+项目+管理+前端+运营
    • 职责:属于啥都干,哪里需要,填哪里。技术方面只做过H5课件,未涉及后端。
    • 薪资:6k x 12
    • 成长:职场
    • 学习:书、证书考试驱动的学习(培训)、慕课网与网易云课堂的各小课程、各类技术白皮书&实时讯息(区块链项目需要)。
    • 证书:计算机技术与软件专业技术资格考试(即”软考“)-系统架构设计师(高级)
  • 19.2~19.12:杭州某物联网公司

    • 岗位:架构师&技术负责
    • 职责:带领团队,0-1搭建工业物联网。真正后端技术的起点。
    • 薪资:11k x 12
    • 成长:技术
    • 学习:书、证书考试驱动的学习(培训)、慕课网各小课、网易云课堂微专业大课_高级开发工程师
    • 证书:计算机技术与软件专业技术资格考试(即”软考“)-系统分析师(高级)
  • 19.12~20.3:某三方外包公司(参与阿里某核心中间件团队开发)

    • 岗位:高级开发工程师~技术专家(外包公司评级)
    • 职责:参与中间件平台非核心系统开发
    • 薪资:20k x 12
    • 成长:三个月试用期都没有,成长有限。
  • 20.3~21.10:某二方外包公司(参与阿里巴巴-盒马开发)

    • 岗位:高级开发工程师
    • 职责:主要负责盒马门店域数字化作业系统开发(主要是智能决策&终端作业)
    • 薪资:(25+2)k x (13 + 3)
    • 成长:业务、产研认识
    • 学习:书、证书考试驱动的学习(培训)、慕课网各小课、慕课网体系大课_Java架构师-十项全能、网易云课堂微专业大课_大数据工程师、九章算法大课_算法班&系统设计课
    • 证书:PMP
  • 21.12~现在:阿里巴巴集团

    • 岗位:保密
    • 职责:参与核心团队开发
    • 薪资:保密

PS:这里简单介绍一下我经历的所谓大小课、培训。

小结

就职业生涯而言,我的开局并不糟糕,起码也算是本科毕业了。这已经超过了不少人了。
但相对很多大佬而言,真的很一般的开局。既不是985、211,也不是研究生。相对团队里一个个清华研究生、东南大学研究生,那这学历属实有点寒碜了。
至于真材实料方面,大学平均成绩也很一般,偏科严重。
不过,大学生涯还是给我留了点东东的。一方面感兴趣的专业课真的学得好,学院前列。另一方面读的书挺多的,整个大学生涯借了图书馆几百本书吧。不敢说每本书都深入看过,但起码每本书都翻过,不少书还做过笔记啥的。

毕业季的时候,想去阿里(当时的杭州大学生都比较向往)。然而学校不怎么出名,阿里根本就没宣讲,当时迷迷糊糊就错过了。现在想来,当时就算有机会,就我那学历和成绩,也够呛。所以,没有赶上校招的快车道(但塞翁失马焉知非福)。

三年多时间(只算后端技术,还不到三年)的学习后,经过“超乎常人”的选拔(足足九轮面试,还不是因为自身原因加餐。。。放心,后面会有面经滴),加入到了阿里的核心部门。

总的来说,打分7/10吧。就结果而言,虽然略低于预期计划,但结果可以接受。就过程而言,由于缺乏经验&指导,以及自身由于各种原因,浪费了不少时间,导致整个过程的效率没有达到预期。这造成,我现在还得补前面三年欠的沉淀债务。

二、剖析

简单剖析一下这三年,离不开三个东西:驱动、思考、执行。
首先得有上层驱动力,才可以推动整个事物前进。其次,要把握好方向盘,确定正确方向。最后,将方向落地到执行层面
简单来说,念头->方向->执行

1.驱动

从某种意义上来说,我也算是职场经验丰富了。毕业三年多,已经经历了五家公司(算上那个三个月不到的三方外包的话)。
从小公司,到大公司,接触了许多人。他们给我最大的印象区别,就是自我驱动力。这份自我驱动力,我有时称之为野心。自我驱动力不同的人,最终表现出来的差十分之大。
近些年比较火的舒适圈理论、还有公司画饼,其实都与自我驱动力相关。

这时候,我就要举例子了。我曾经有一位同事的技术态度就是:可以用(他)现有技术栈解决的,坚决不学新技术。不可以用现有技术栈解决的,就反馈这个问题无解。
另外,我刚进一家公司的时候,那位老板给我画了个饼:希望公司的小伙伴,以后都可以在杭州买房。。。只能说,真是“好大”一个饼。当时,我就泄了一半的气。

简单来说,做人要有梦想,万一真的实现了呢。
如果真的很佛系,可以去看一下励志演讲,喝喝鸡汤。如果嫌鸡汤不好喝,去B站看看美国那边千万刀的豪宅(国内的房子就算了,那鸡汤就不香了),欧洲那边的广袤牧场。
不要老想着鸡汤有毒。鸡汤有毒也许只是一个扭曲的骗局呢?只能说按照博弈的角度来说,创业有成者一定表示,创业者是困难无比,而且一分靠努力,九分靠天命。看懂,掌声。

有人说,当所有人忘记你,便是你的死亡。我和我朋友开玩笑说,当我彻底不蹦哒了,就是我的第一次死亡。

2.思考

思考的三大输出体现:

  • 对过去的总结(复盘等)。
  • 对现状的辨识(厘清组织结构、人际关系等)。
  • 对未来的规划(新年梦想、执行清单等)。

技术上有这么一个准则:
一个需求下来了,如果某人立马开写,要么他做过一样需求,要么他是个憨批。
无论需求多小、多简单,只要么做过,我们就需要进行分析,再进行设计,最后编码、测试、上线。
从架构来说,编码之前需要分析&设计(概要设计&详细设计)。

**人生没有彩排,但可以提前规划。**每个人对自己的职业都应该有短期、中期、长期的规划。短期规划精准,满足Smart原则,中期规划富有弹性,可按照需要进行有效转变。长期规划,则必须follow自己的长期目标(目标质量很重要)。
短期目标,可以借助清单进行执行。中期目标可以和亲友进行探讨,多倾听不同意见(比如不同行业的)。长期目标,就得问自己了,自我价值实现在哪里。
很多人可能并没有自己为之奋斗终生的自我价值,甚至可以说大部分人进了棺材都没有。那可以选择和我一样,令自己拥有更多的选择权,直到找到自我价值的那天。

思考是一种习惯,当习惯了它,就经常会有文思泉涌的感觉。

3.执行

执行方面,我比较认同PDCA,但不需要过于形式化。对自己而言,更多是形成这样的处理模式。

a.计划

前面有提到,通过在脑海里的推演,找到一个切实可行的高效路线。
事前就多思考,执行就蒙头干。

b.执行

执行没啥说的。照做就多了。
中途坚持不下来了,就喝喝鸡汤,听听激昂的音乐(个人方法),就又有干劲了。
当然,如果真的感觉身体很累,那就去锻炼健身吧。我曾经有过半年多的专业健身,效果还是很不错的。

如果真的有一件事无法执行,或者说需要调整计划,那就问问自己,究竟有没有偷懒的成分,如果没有,那就去改变计划吧。

c.复盘&调整

对完成的事情,进行复盘总结,找出优点与不足。
比如,我每年都会写年度总结。平日里,也经常在白板/平板上,对之前做的事情进行分析。
但不可否认,过去的三年里,相对于大量的输入,复盘还是不足。准确说,其实是知识的内化&沉淀不足。

4.小结

总结一下,就一句话。
驱动决定可能,思考决定上限,执行决定下限。

三、方法

前面提到的方向到执行,跨度过大。比如我想学技术、学XXX技术。具体怎么学呢?
所以,这就涉及到方案层面了,或者说流程方法层面了。
举个不恰当的例子,一位p8表示这个财年要做平台商业化,下面一群p6嗷嗷叫咋做。这就需要几位p7,配合p8拆分目标。最后几位p7有了交互逻辑解耦、领域模型抽象等工作,它们就可以将它们细化成具体系统,交给下面的p6。
(准确来说,中间都是在做拆解,并不是一定只有7才有方案/流程方法的考量,粒度不同而已)

俗语说得好,磨刀不误砍柴工。

1.工具

这里的工具只针对狭义工具。更多是集中于软件。广义的工具,是包涵达成目标的多种手段。后面会有一片文章与之相关。

这个时代,不会还有死读书的吧。
合理利用工具,可以大幅提升学习、工作等效率。就像Mybatis取代JDBC、Spring取代EJB、SpringBoot取代Spring一般。

我这里简单提及一些我使用的工具。如果大家感兴趣,我后续会写一篇详细文档,进行阐述的。
Idea Ultimate、Sublime、MarginNote3、XMind、ProcessOn、GoodNotes5、Notability、印象笔记、印象笔记-剪切插件等app
设备方面,随着今年MacBook max的到手,苹果全家桶齐全了。

这里,我提一下我对工具花费的看法。我这边所有软件都是正版的,设备则是按需购买。这些加起来花费不少。但是效率的提升,真的完全赚回来了。

虽然我有marginNote3看pdf文档,但是很多时候还是喜欢看实体书,所以我买了很多实体书。后来甚至为了能够方便看实体ppt等,我直接买了个支持airprint的打印机。。。不过,确实方便了很多。

按照工具的必要程度、效率提升、价格,再根据腰包进行购买。但,你得先知道有这么个东东。囧

我认为,思考与工具(广义上的工具),就是最大的效率提升工具。后面,我会有一篇专门的文档进行这方面的阐述。毕竟也有多个小伙伴问到我效率方面的问题。诸如你这么多书,看得完嘛?你学那么多课,学得过来嘛?项目比较大时,你怎么同时进行项目管理、方案设计,以及核心开发。简而言之,如何实现有效提升效率。

PS:有关破解的问题,个人认为如果可以,还是付费支持一下的说。大家都经历过从破解到付费的时代。开发最常见的,就是jetbrains产品了。早两年,我也是破解,不过后面买得起了,就买了全家桶支持。

2.流程

如果只是单一的工具,那么整体效率终究有限。尤其单一工具带来的局限性,真的令强迫症发狂。
所以,你需要流程,甚至多个流程组成的生态(比如学习生态)。
就学习而言,我目前有两个纵向闭环流程。一个是依据marginNote3的阅读-学习-复习模式构建,这是marginNote官方流程,大家可以了解一下。另一个是基于印象笔记-剪切插件、印象笔记构建,但是复习效果还存在不足。

后续也有想过利用Notion建立流程,或者利用对开发很友好的语雀建立流程。具体整个流程体系,我还在优化迭代中,欢迎大家给出意见。

PS:其实,之前出于定制化的需求,我都想自己写一个知识库工具了。

3.内化

内化,就是把知识变成自己的。
知识看了、收藏了、下载了,都不一定属于你的。甚至做了笔记,这个知识可能还是不属于你的。所以,我们需要有意识地进行知识的内化。

内化的方式有很多。前面说的看、做笔记都是,只是效率低而已。更高一级,是去做实践。比如手写AQS,再比对比对源码,做做思考笔记,你就可以吊打大部分面试官(实测)。再高一级,就是去教别人。比如写博客、线上/线下教课等。效果还是很不错的(实测)。

我自己,就会做笔记(印象笔记),写博客(博客园、CSDN),技术分享(群组、团队、公司等)

5.小结

总结一句话,方案决定整体效率。
需要时常反思自己的流程&工具构建的方案效率,是否可以再提高。平时多留意一些效率方案的文章等。

四、警醒

1.不足之处

那么回首这三年多,是否有不足呢?那当然是肯定的。
自身的不足,体现在三个方面:收集有余,沉淀不足;时间浪费;缺乏锻炼,身体素质下降

a.收集有余,沉淀不足

最近三年虽说没有成为收集癖,大多数信息也是个人相关的。
但是从课程(大课五个、培训三个、小课几十),到文章、再到各类书籍,信息收集得太多了。好在大课整理进度85+%,部分小课被大课覆盖内容,就只是随便看看了。但是后续内化程度不足,水平也是参差不齐。
最近趁着有时间,推进了进度,后续还需要持续推进内化进度。

b.时间浪费

时常回去啥也不想做,就想发呆&看视频/直播,晃荡一两个小时,才回过神,开始做事情。个人觉得一个很重要的原因是白天注意力比较集中,刚回来的时候心思比较活跃,所以注意力难以集中。再就是整体精力不足。

解决无非开源节流:

  • 开源:增强身体锻炼,提高个人精力上线。
  • 节流:需要合理安排时间&精力,提高时间“质量”(详见精英控系列,后续详述)。

PS:我属于白天一干活,就可以开心坐一天的那种,除非特殊情况,否则就真的感觉像心流那样度过一天。但晚上就真的不怎么想加班,尤其实际没啥事情。

c.健身时间变少导致身体素质下降

虽然整体来看,还不错。但是相较于大学时候的身体素质,那确实有所下降。后面需要安排上的说。

2.客观认识

过去三年多,有很多运气的成分,这不得不承认。
比如被第一家公司老板挖到,是因为我那段时间对区块链技术比较关注,被他发现了。。。
比如被第二家公司老板挖到,是因为我在第一家公司考虑软考-系统架构设计师,所以在我对后端技术只学习了一两个月的情况下,被挖过去当技术负责。。。
比如被第三家公司上司挖到,是因为我在第二家公司时,虽然通过了阿里一面,但被我拒绝了后续流程。然后,我的简历就一直在阿里人才库,并且评价还不错。。。
第四家公司原因同上。。。

再比如我遇到上司与同事,大多都挺不错的,简直是职场最大lucky。

如果刨除这些运气,今天的我又会是什么样呢?

五、总结

如果工作的信念只剩一个,我希望是自我驱动。
如果人生的核心只剩一个,我希望是思考。

至于为什么要做这样的分享呢?大学的时候,我学到一句话,我很有感触。这个社会让你知道的,是它想让你知道的。
博弈使得每一位成功者成功后,都会选择包装自己,使得自己的成功更为顺理成章。而我能做的,只有从一开始就展示自己的一点一滴,没有包装的真实。那么如果最终我可以获得一些成就,说明我的道路是正确可行、可持续的。
过去中国几十年的阶级越迁已经越来越难见,那么我们看到的道路真的还是正确的吗?结果与过程真的相匹配嘛?这里无法如数学那样可以明确推理的答案,所以需要我们这些人去探寻。而我能做的,就是用自己的经历去验证自己的想法。再将这份经历真实地展现出来,供大家参考

欢迎大家就文章中的一些问题,如职场生存、职业规划、效率提升、面试经验等,与我进行交流。

最后,愿与诸君共进步。

这次总结更像是对过去三年多的一种粗粒度总结。不过,后续会有面经、工具、工作经验等方面的详细文章。

原文链接:https://blog.csdn.net/cureking/article/details/122179291

收起阅读 »