注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Flutter 基础 | Dart 语法

该系列记录了从零开始学习 Flutter 的学习路径,第一站就是 Dart 语法。本文可以扫除看 Flutter 教程,写 Flutter 代码中和语言有关的绝大部分障碍。值得收藏~声明并初始化变量int i = 1; // 非空类型必须被初始化 int? k...
继续阅读 »

该系列记录了从零开始学习 Flutter 的学习路径,第一站就是 Dart 语法。本文可以扫除看 Flutter 教程,写 Flutter 代码中和语言有关的绝大部分障碍。值得收藏~

声明并初始化变量

int i = 1; // 非空类型必须被初始化
int? k = 2; // 可空类型
int? h; // 只声明未初始化,则默认为 null
var j = 2; // 自动推断类型为int
late int m; // 惰性加载
final name = 'taylor'; // 不可变量
final String name = 'taylor'; // 不可变量

Dart 中语句的结尾是带有分号;的。

Dart 中声明变量时可以选择是否为它赋初始值。但非空类型必须被初始化。

Dart 中声明变量可以显示指明类型,类型分为可空和非空,前者用类型?表示。也可以用var来声明变量,此时编译器会根据变量初始值自动推断类型。

late关键词用于表示惰性加载,它让非空类型惰性赋值成为可能。得在使用它之前赋值,否则会报运行时错误。

惰性加载用于延迟计算耗时操作,比如:

late String str = readFile();

str 的值不会被计算,直到它第一次被使用。

??可为空类型提供默认值

String? name;
var ret = name ?? ''

如果 name 为空则返回空字串,否则返回 name 本身。

数量

在 Dart 中intdouble是两个有关数量的内建类型,它们都是num的子类型。

若声明变量为num,则可同时被赋值为intdouble

num i = 1;
i = 2.5;

字串

''""都可以定义一个字串

var str1 = 'this is a str';
var str2 = "this is another str";

字串拼接

使用+拼接字符串

var str = 'abc'+'def'; // 输出 abcdef

多行字串

使用'''声明多行字符串

var str = '''
this is a
multiple line string
'''
;

纯字串

使用r声明纯字符串,其中不会发生转义。

var str = r'this is a raw \n string'; // 输出 this is a raw \n string

字串内嵌表达式

字符串中可以内嵌使用${}来包裹一个有返回值的表达式。

var str = 'today is ${data.get()}';

字串和数量相互转化:

int.parse('1'); // 将字串转换为 int
double.parse('1.1'); // 将字串转换为 double
1.toString(); // 将 int 转换为字串
1.123.toStringAsFixed(2); // 将 double 转换为字串,输出 '1.12'

集合

声明 List

与有序列表对应的类型是List

[]声明有序列表,并用,分割列表元素,最后一个列表元素后依然可以跟一个,以消灭复制粘贴带来的错误。

var list = [1,2,3,];

存取 List 元素

列表是基于索引的线性结构,索引从 0 开始。使用[index]可以获取指定索引的列表元素:

var first = list[0]; // 获取列表第一个元素
list[0] = 1; //为列表第一个元素赋值

展开操作符

...是展开操作符,用于将一个列表的所有元素展开:

var list1 = [1, 2, 3];
var list2 = [...list1, 4, 5, 6];

上述代码在声明 list2 时将 list1 展开,此时 list2 包含 [1,2,3,4,5,6]

除此之外,还有一个可空的展开操作符...?,用于过滤为null的列表:

var list; // 声明时未赋初始值,则默认为 null
var list2 = [1, ...?list]; // 此时 list2 内容还是[1]

条件插入

iffor是两个条件表达式,用于有条件的向列表中插入内容:

var list = [
'aa',
'bb',
if (hasMore) 'cc'
];

如果 hasMore 为 true 则 list 中包含'cc',否则就不包含。

var list = [1,2,3];
var list2 = [
'0',
for (var i in list) '$i'
];// list2 中包含 0,1,2,3

在构建 list2 的时候,通过遍历 list 来向其中添加元素。

Set

Set中的元素是可不重复的。

{}声明Set,并用,分割元素:

var set = {1,2,3}; // 声明一个 set 并赋初始元素
var set2 = {}; // 声明一个空 set
var set3 = new Set(); // 声明一个空 set
var set4 = Set(); // 声明一个空 setnew 关键词可有可无

Map

Map是键值对,其中键可以是任何类型但不能重复。

var map = {
'a': 1,
'b': 2,
}; // 声明并初始化一个 map,自动推断类型为 Map

var map2 = Map(); // 声明一个空 map
map2['a'] = 1; // 写 map
var value = map['a']; //读 map

读写Map都通过[]

const

const是一个关键词,表示一经赋值则不可修改:

// list
var list = const [1,2,3];
list.add(4); // 运行时报错,const list 不可新增元素

// set
var set = const {1,2,3};
set.add(4); // 运行时报错,const set 不可新增元素

// map
var map = const {'a': 1};
map['b'] = 2; // 运行时报错,const map 不能新增元素。

声明类

class Pointer {
double x;
double y;

void func() {...} // void 表示没有返回值
double getX(){
return x;
}
}
  • 用关键词 class声明一个类。
  • 类体中用类型 变量名;来声明类成员变量。
  • 类体中用返回值 方法名(){方法体}来声明类实例方法。

构造方法

上述代码会在 x ,y 这里报错,说是非空字段必须被初始化。通常在构造方法中初始化成员变量。

构造方法是一种特殊的方法,它返回类实例且签名和类名一模一样。

class Point {
double x = 0;
double y = 0;
// 带两个参数的构造方法
Point(double x, double y) {
this.x = x;
this.y = y;
}
}

这种给成员变量直接赋值的构造方法有一种简洁的表达方式:

class Point {
double x = 0;
double y = 0;

Point(this.x, this.y); // 当方法没有方法体时,得用;表示结束
}

命名构造方法

Dart 中还有另一个构造方法,它的名字不必和类名一致:

class Point {
double x;
double y;

Point.fromMap(Map map)
: x = map['x'],
y = map['y'];
}

为 Point 声明一个名为fromMap的构造方法,其中的:表示初始化列表,初始化列表用来初始化成员变量,每一个初始化赋值语句用,隔开。

初始化列表的调用顺序是最高的,在一个类实例化时会遵循如下顺序进行初始化:

  1. 初始化列表
  2. 父类构造方法
  3. 子类构造方法

Point.fromMap() 从一个 Map 实例中取值并初始化给成员变量。

然后就可以像这样使用命名构造方法:

Map map = {'x': 1.0, 'y': 2.0};
Point point = Point.fromMap(map);

命名构造方法的好处是可以将复杂的成员赋值的逻辑隐藏在类内部。

继承构造方法

子类的构造方法不能独立存在,而是必须调用父类的构造方法:

class SubPoint extends Point {
SubPoint(double x, double y) {}
}

上述 SubPointer 的声明会报错,提示得调用父类构造方法,于是改造如下:

class SubPoint extends Point {
SubPoint(double x, double y) : super(x, y);
}

在初始化列表中通过super调用了父类的构造方法。父类命名构造方法的调用也是类似的:

class SubPoint extends Point {
SubPoint(Map map) : super.fromMap(map);
}

构造方法重定向

有些构造方法的目的只是调用另一个构造方法,为此可以在初始化列表中通过this实现:

class Point {
double x = 0;
double y = 0;

Point(this.x, this.y);
Point.onlyX(double x): this(x, 0);
}

Point.onlyX() 通过调用另一个构造方法并为 y 值赋值为 0 来实现初始化。

方法

Dart 中方法也是一种类型,对应Function类,所以方法可以被赋值给变量或作为参数传入另一个方法。

// 下面声明的两个方法是等价的。
bool isValid(int value){
return value != 0;
}

isValid(int value){// 可自动推断返回值类型为 bool
return value != 0;
}

声明一个返回布尔值的方法,它需传入一个 int 类型的参数。

其中方法返回值bool是可有可无的。

bool isValid(int value) => value != 0;

如果方法体只有一行表达式,可将其书写成单行方法样式,方法名和方法体用=>连接。

Dart 中的方法不必隶属于一个类,它也可以顶层方法的形式出现(即定义在.dart文件中)。定义在类中的方法没有可见性修饰符public private protected ,而是简单的以下划线区分,_开头的函数及变量是私有的,否则是公有的。

可选参数 & 命名参数

Dart 方法可以拥有任意数据的参数,对于非必要参数,可将其声明为可选参数,调用方法时,就不用为其传入实参:

bool isValid(int value1, [int value2 = 2, int value3 = 3]){...}

定义了一个具有两个可选参数的方法,其中第二三个参数用[]包裹,表示是可选的。而且在声明方法时为可选参数提供了默认值,以便在未提供相应实参时使用。所以如下对该方法的调用都是合法的。

var ret = isValid(1) // 不传任何可选参数
var ret2 = isValid(1,2) // 传入1个可选参数
var ret3 = isValid(1,2,3) // 传入2个可选参数

使用[]定义可选参数时,如果想只给 value1,value3 传参,则无法做到。于是乎就有了{}

bool isValid(int value1, {int value2 = 2, int value3 = 3}) {...}

然后就可以跳过 value2 直接给 value3 传参:

var ret = isValid(1, value3 : 3)

这种语法叫可选命名参数

Dart 还提供了关键词required指定在众多可选命名参数中哪些是必选的:

bool isValid(int value1, {int value2, required int value3}) {...}

匿名方法

匿名方法表示在给定参数上进行一顿操作,它的定义语法如下:

(类型 形参) {
方法体
};

如果方法体只有一行代码可以将匿名函数用单行表示:

(类型 形参) => 方法体;

操作符

三元操作符

三元操作符格式如下:布尔值 ? 表达式1 : 表达式2;

var ret = isValid ? 'good' : 'no-good';

如果 isValid 为 true 则返回表达式1,否则返回表达式2。

瀑布符

该操作符..用于合并在同一对象上的多个连续操作:

val paint = Paint()
..color = Colors.black
..strokeCap = StrokeCap.round
..strokeWidth = 5.0

构建一个画笔对象并连续设置了 3 个属性。

如果对象可控则需使用?..

paint?..color = Colors.black
..strokeCap = StrokeCap.round
..strokeWidth = 5.0

类型判定操作符

as 是强转操作符,表示将一个类型强转为另一个类型。

is 是类型判定操作符,用于判断某个实例是否是指定类型。

is! 是与 is 相反的判定。

流程控制

if-else

if (isRaining()) {
you.bringRainCoat();
} else if (isSnowing()) {
you.wearJacket();
} else {
car.putTopDown();
}

for

for (var i = 0; i < 5; i++) {
message.write('!');
}

如果不需要关心循环的索引值,则可以这样:

for (var item in list) {
item.do();
}

while

while (!isDone()) {
doSomething();
}
do {
printLine();
} while (!atEndOfPage());

break & continue

break & continue 可用于 for 和 while 循环。

break用于跳出循环

var i = 0
while (true) {
if (i > 2) break;
print('$i');
i++;
} // 输出 0,1,2

continue用于跳过当前循环的剩余代码:

for (int i = 0; i < 10; i++) {
if (i % 2 == 0) continue;
print('$i');
}// 输出 1,3,5,7,9

switch-case

Dart 中的 switch-case 支持 String、int、枚举的比较,以 String 为例:

var command = 'OPEN';
switch (command) {
case 'CLOSED':
case 'PENDING': // 两个 case 共用逻辑
executePending();
break; // 必须有 break
case 'APPROVED':
executeApproved();
break;
case 'DENIED':
executeDenied();
break;
case 'OPEN':
executeOpen();
break;
default: // 当所有 case 都未命中时执行 default 逻辑
executeUnknown();
}

关键词

所有的关键词如下所示:

abstract 2elseimport 2show 1
as 2enuminstatic 2
assertexport 2interface 2super
async 1extendsisswitch
await 3extension 2late 2sync 1
breakexternal 2library 2this
casefactory 2mixin 2throw
catchfalsenewtrue
classfinalnulltry
constfinallyon 1typedef 2
continueforoperator 2var
covariant 2Function 2part 2void
defaultget 2required 2while
deferred 2hide 1rethrowwith
doifreturnyield 3
dynamic 2implements 2set 2


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

收起阅读 »

从技术演化,看元宇宙未来

文丨陈根21世纪是一个技术井喷的的时代,从互联网、云计算、大数据到通信技术、人工智能、数字孪生等等,一系列的技术都随着其发展和成熟日渐融入人们所生活的社会,并共同雕刻着这个属于技术的时代。元宇宙就是这个时代里一系列创新技术集大成的重要标志之一。元宇宙构建了一个...
继续阅读 »

文丨陈根

21世纪是一个技术井喷的的时代,从互联网、云计算、大数据到通信技术、人工智能、数字孪生等等,一系列的技术都随着其发展和成熟日渐融入人们所生活的社会,并共同雕刻着这个属于技术的时代。元宇宙就是这个时代里一系列创新技术集大成的重要标志之一

元宇宙构建了一个脱胎于现实世界,又与现实世界平行、相互影响,并且始终在线的虚拟世界。在元宇宙理想形态背后,是基于扩展现实技术提供沉浸式体验,基于数字孪生技术生成现实世界的镜像,基于区块链技术搭建经济体系,并且允许每个用户进行内容生产和世界编辑。

技术的发展是元宇宙初现的前提,技术的集成则是元宇宙爆发的背景。尽管当前元宇宙已经吸引了足够多的市场注意力和资本的目光,但站在技术演化的角度,元宇宙又该如何顺应技术发展趋势?元宇宙的终点又是什么技术?

技术聚合,进发元宇宙

乔布斯曾提出一个著名的“项链”比喻,iPhone的出现,串联了多点触控屏、iOS、高像素摄像头、大容量电池等单点技术,重新定义了手机,开启了激荡十几年的移动互联网时代。

正如iPhone的出现一样,元宇宙是一系列连点成线技术创新的总和。元宇宙是算力持续提升、高速无线通信网络、云计算、区块链、虚拟引擎、VR/AR、数字孪生等技术创新逐渐聚合的结果,是整合多种新技术而产生的新型虚实相融的互联网应用和社会形态。

1969年,互联网诞生,元宇宙的实现有了全球化的软硬件基础。1989年,万维网标准开始制定,为互联网技术的发展奠定了基础。1990年到1998年,人类用户主要通过字符化的电子公告牌(BBS)进行信息交互。彼时,在数字空间中没有数字世界环境的构建,没有明确的个人角色,用户交互界面还以字符为主。

1998年到2003年,博客开始兴起,互联网进入2.0时代。数字空间出现了独立的“数字人”角色,主要表现形式为字符和图片构成博客个人空间的主体。自此,互联网用户有了个人身份和角色,并逐渐向一个完善的数字生态系统过渡。

以博客为例,在博客中发言和展示个人信息内容,是数字空间的微观层面;某一个个体的博客平台所吸纳的人群,是数字空间中观层面;整个博客世界则是其宏观层面。博客生态系统不是一个简单的“写”与“看”的供求关系,甚至也不是一种简单的“表演”与“观看”的关系,而是由人们在社会整体生态环境影响下形成的多重需求构成的生态关系

2004年,社交网络开始兴起,Facebook正式上线,QQ、Twitter、微博不断出现并高速发展。数字空间中,数字人(个人用户空间)与数字人可以进行信息交互,形成社交网络和社交关系。2009年,移动互联网进入3G时代,随着智能手机的广泛应用,数字空间伴随社交网络的应用范围扩大而进一步扩大大量的图片,声音元素丰富了虚拟数字空间的内容

2014年,以Facebook购买虚拟现实公司Oculus为标志,虚拟现实成为产业热点。但是由于当时的VR眼镜还不成熟,VR追求的是沉浸式和场景化体验,但由于用户的参与感太过薄弱,只充当观众的用户量级显然无法支撑VR的全民热情,再加上社交网络巨头和消费者对虚拟现实社交网络准备不足。几年后,虚拟现实热潮退去数字空间向维化阶段进化的第一次努力失败

同年,4G网络兴起,视频开始成为社交网络为基础的数字空间的重要内容形式。2018年,电子商务和知识付费、NFT、虚拟货币在社交网络为基础的数字空间中初步成熟并广泛应用。

2019年5G商用正式宣告了5G时代的来临。5G技术具有万物互联、高速度、泛在网、低时延、低功耗、重构安全等特点和优势。5G技术的发展使整个人类社会的生产和生活产生深刻变革,5G构建起万物互联的核心基础能力,不仅带来了更快更好的网络通信,更肩负起赋能各行各业的历史使命。

2020年,以城市大脑、数字政府、组织化转型为主的产业化持续拓展社交网络和数字空间向万物互联、万物交互方向演进,但仍然处于萌芽状态。

在这样的背景下,囊括了上述技术、带着强烈科幻色彩的元宇宙随着开放式游戏创建平台Roblox上市成为了网络讨论的热点。在大厂布局、资本追捧下,现在,“元宇宙”概念已然成为市场最炙手可热的名词。

元宇宙,发展进行时

元宇宙的兴起是技术的聚合,从目前已经呈现的前端征兆和发展趋势看,在通信网络、云计算、区块链、虚拟现实等技术的支持下,元宇宙将生成一个与人类物理世界全方位连接起来的虚拟宇宙,人们得以感受到由此生成的超大尺度、无限扩张、层级丰富和谐运行的虚拟系统,呈现在人们面前的将是现实世界与数字世界融合的全新的文明景观

元宇宙连接虚拟和现实,丰富人的感知,提升体验,延展人的创造力和更多可能。虚拟世界从物理的世界的模拟、复刻,变成物理世界的延伸和拓展,进而反作用于物理世界,最终模糊虚拟世界和现实世界的界限。从这一角度来说,元宇宙的兴起可以看做是数字空间向三维化阶段进化的第二次尝试

虽然当前人们还不能准确描绘出元宇宙的景观,但事实上,现在人们已经以不同的方式生活在元宇宙之中。人们正不断地构建着数字世界,数字化着自己以及物理世界,而元宇宙的变化过程也会从不同的现实变量出发,比如教育、就业、消费等影响着真实社会的生产和生活。

对于元宇宙来说,不同的阶段有着不同的成熟度如果说信息化和数字化,是元宇宙兴起的前提,那么数字孪生,就是元宇宙发展的初级阶段。2011年,迈克尔教授在《几乎完美:通过产品全声明周期管理驱动创新和精益产品》中引用了其合作者约翰·维克斯描述概念模型的名词“数字孪生”,并一直沿用至今。

正如我在《数字孪生》所说的“数字孪生就是在一个设备或系统‘物理实体’的基础上,创造一个数字版的‘虚拟模型’。这个‘虚拟模型’被创建在信息化平台上提供服务”。值得一提的是,与电脑的设计图纸又不同,相比于设计图纸,数字孪生体最大的特点在于它是对实体对象的动态仿真

明眼望去,数字孪生是物理实体的“灵魂”。当前,数字孪生技术在经历了技术准备期、概念产生期和应用探索期后,正在进入大浪淘沙的领先应用期。随着图书馆、博物馆、各种景点孪生体数字孪生还在加速发展,而数字孪生发展的终极,就是走向元宇宙。

2021年初举行的计算机图形学顶级学术会议SIGGRAPH 2021上,英伟达就通过一部纪录片,自曝了2021年4月公司发布会中,英伟达CEO黄仁勋通过数字孪生技术制造了演讲中14秒片段的数字替身。尽管只有短暂的14秒,但黄仁勋标志性的皮衣,表情、动作、头发却足以乱真,几乎骗过了所有人。

未来,数字孪生技术将为元宇宙中的各种虚拟对象提供了丰富的数字孪生体模型,并通过从传感器和其他连接设备收集的实时数据与现实世界中的数字孪生化对象相关联,使得元宇宙环境中的虚拟对象能够镜像、分析和预测其数字孪生化对象的行为。可以说,作为对现实世界的动态模拟,“数字孪生是元宇宙从未来伸过来的一根触角

技术的进路,本质的到达

当然,虽然元宇宙在数字孪生中有所体现,但必然不止于数字孪生。Beamable公司创始人Jon Radoff提出了元宇宙的七层架构:基础设施、人机交互、去中心化、空间计算、创作者经济、发现和体验。

其中,基础设施包括支持元宇宙的设备、将它们连接到网络并提供内容的技术;人机交互则主要是智能可穿戴设备;去中心化是构建元宇宙人与人关系的重要转折,可以把元宇宙的所有资源更公平的分配;计算层将真实计算和虚拟计算进行混合,以消除物理世界和虚拟世界之间的障碍;创作者经济层包含创作者每天用来制作人们喜欢的体验的所有技术;发现层类似于互联网的门户网站和搜索引擎;体验层则是用户直接面对的游戏、社交平台等。

从元宇宙的七层架构可以看出,元宇宙是个比数字孪生更庞大、更复杂的体系。如果数字孪生已经是个复杂技术体系的话,那么元宇宙就是个复杂的技术-社会体系。问题是,这个复杂的技术-社会体系就是人类技术发展的终点吗?元宇宙的尽头又在哪里

显然,元宇宙作为功能意义上的一种技术群存在,是信息技术发展的高级阶段。从社会角度来看元宇宙的生成与发展依赖于现实世界,又反作用于现实世界的发展。

一方面,元宇宙不完全是一种人造的数字化空间或现实世界的数字化映像。元宇宙作为人类制造出来的一种现实性的非实在事物,其非实在性在于,元宇宙中的一切事物包括它本身都是信息的集合而非物质的集合。另一方面,元宇宙也只能部分地、有条件地反映出人类思维空间中的事物。

这意味着,元宇宙是虚拟演化的最终形态,宇宙发展的根本,还是为了促进现实世界的发展。2019年出版的《崛起的超级智能》曾经绘制了一幅世界数字大脑的发育示意图,书中就预言了在混合智能和云反射弧之后,世界数字大脑的思维空间和梦境空间将成为新的热点,而它们与元宇宙存在着密切的关系。

根据《崛起的超级智能》,社交网络对应了神经元网络;3G/4G/5G对应神经纤维;人工智能对应驱动世界数据大脑运转的基础和动力;城市大脑、工业大脑对应世界数字大脑的初步成型。作一个类脑复杂巨系统,世界数据大脑的思维空间和梦境空间将在大数据、虚拟现实、数字孪生等技术的推动下不断成熟。

从这一维度来说,元宇宙从本质上可以看作是思维空间和梦境空间的产业化名称。元宇宙是组成这个世界数字大脑的一部分,承担了它意识和梦境的构造,主要特征是大社交网络为核心的数字空间开始从二维向三维进化,能够将现实世界映射到数字空间,也可以将人类的幻想具象化,带给人类梦境般真实体验。

世界数字大脑的作用是提高人类社会的运行效率,解决人类社会发展过程中面临的复杂问题,更好地为人类协同发展提供支撑。世界神经系统将为人类社会的协同发展,构建起全球统一的类脑智能支撑平台,实现对世界的认知、判断、决策、反馈和改造,共同应对来自自然的各种挑战和风险,满足人类社会的各种需求。

归根到底,从信息化到数字化,从数字化向数字孪生进化,再向元宇宙进发。技术的演化之路也是人类的进化之路,使得人类们对虚拟和现实、物理和数字有更本质的认识。

https://baijiahao.baidu.com/s?id=1716003827693985340&wfr=spider&for=pc

收起阅读 »

原来我一直在错误的使用 setState()?

导语 任何前端系统与用户关系最密切的部分就是UI。一个按钮,一个标签,都是通过对应的UI元素展示与交互。初学时,我们往往只关注如何使用。但如果只知道如何使用,遇到问题我们很难找到解决的办法和思路,也无法针对一些特定场景进行优化。本期针对Flutter的UI系统...
继续阅读 »

导语


任何前端系统与用户关系最密切的部分就是UI。一个按钮,一个标签,都是通过对应的UI元素展示与交互。初学时,我们往往只关注如何使用。但如果只知道如何使用,遇到问题我们很难找到解决的办法和思路,也无法针对一些特定场景进行优化。本期针对Flutter的UI系统和大家一起进阶学习:


1、原来我一直在错误的使用 setState()?


2、面试必问:说说Widget和State的生命周期


3、Flutter的布局约束原理


4、15个例子解析Flutter布局过程


读完本文你将收获:Flutter的渲染机制以及setState()背后的原理




引言


初学Flutter的时候,当需要更新页面数据时,我们通常会想到调用setState()。但很多博客以及官方文章并不建议我们在页面的节点使用setState()因为这样会带来不必要的开销(仅针对页面节点,当然Flutter的Widget刷新一定离不开setState()),很多状态管理方案也是为了达到所谓的“局部刷新”。到这我们不仅要思考为什么使用setState()能刷新页面,又为何可能会带来额外的损耗?这个函数背后做了什么逻辑?这篇文章和大家一一揭晓。




一、为什么setState()能刷新页面


1、setState()


我们的demo从一个最简单的计数器开始



在页面中点击底部的➕号,本地变量加一,之后调用了当前页面的setState(),页面重新构建,显示的数据增加。从现象推断,整个流程必然会经过setState()-···················->当前State的build()-················->页面绘制-············->屏幕刷新。
那么下面我们看看setState()到底做了什么?


State#setState(VoidCallback fn)


@protected
void setState(VoidCallback fn) {
final dynamic result = fn() as dynamic;
_element.markNeedsBuild();
}

在去掉所有的断言之后,其实setState只做了两件事儿


1、调用我们传入的VoidCallback fn


2、调用_element.markNeedsBuild()




2、element.markNeedsBuild()


Flutter开发中我们一般和Widget打交道,但Widget上有这样一个注释。



Describes the configuration for an [Element].



abstract class Widget extends DiagnosticableTree {
final Key key;
Element createElement();
String toStringShort() {
return key == null ? '$runtimeType' : '$runtimeType-$key';
}

Widget只是用于描述Element的一个配置文件,实际在Framework层管理页面的构建,渲染等,都是通过Element完成,Element由Widget创建,并且持有Widget对象,每一种Widget都会对应的一种Element



在上面的demo中,我们在HomePageState调用了setState(),这里的Element有HomePage对象创建。HomePage(Widget) - HomePageState(State) - HomePageElement(StatefulElement) 三者一一对应。



Element#markNeedsBuild()


/// The object that manages the lifecycle of this element.
/// 负责管理所有element的构建以及生命周期
@override
BuildOwner get owner => _owner;

void markNeedsBuild() {
//将自己标记为脏
_dirty = true;
owner.scheduleBuildFor(this);
}

调用了BuildOwner.scheduleBuildFor(element),这里的BuildOwnerWidgetsBinding的初始化中完成实例化,负责管理widget框架,每个Element对象在mount到element树中之后都会从父节点获得它的引用


WidgetsBinding#initInstances()


void initInstances() {
super.initInstances();
_instance = this;
_buildOwner = BuildOwner();
buildOwner.onBuildScheduled = _handleBuildScheduled;
/······/
}

BuildOwner#scheduleBuildFor(Element element)


void scheduleBuildFor(Element element) {
//添加到_dirtyElements集合中
_dirtyElements.add(element);
element._inDirtyList = true;
}

最后将自己添加到BuildOwner中维护的一个脏element集合。



总结:1、Element: 持有Widget,存放上下文信息,RenderObjectElement 额外持有 RenderObject。通过它来遍历视图树,支撑UI结构。


2、setState()过程其实只是将当前对应的Element标记为脏(demo中对应HomePageState),并且添加到_dirtyElements合中。





3、Flutter渲染机制


上面的过程看起来没做任何渲染相关的事儿,那么页面是如何重新绘制?关键点就在于Flutter的渲染机制



开始FrameWork层会通知Engine表示自己可以进行渲染了,在下一个Vsync信号到来之时,Engine层会通过Windows.onDrawFrame回调Framework进行整个页面的构建与绘制。(这里我想为什么要先由Framework发起通知,而不是直接由Vsync驱动。如果一个页面非常卡顿,恰好每一帧绘制的时间大于一个Vsync周期,这样每帧都不能在一个Vsync的时间段内完成绘制。而先由framework保证上完成构建与绘制后,发起通知在下一个Vsync信号再绘制则可以避免这样的情况)。每次收到渲染页面的通知后,Engine调用Windows.onDrawFrame最终交给_handleDrawFrame()方法进行处理。


@protected
void ensureFrameCallbacksRegistered() {
//构建帧前的处理,主要是进行动画相关的计算
window.onBeginFrame ??= _handleBeginFrame;
//Windows.onDrawFrame交给_handleDrawFrame进行处理
window.onDrawFrame ??= _handleDrawFrame;
}
复制代码

SchedulerBinding#handleDrawFrame()


void handleDrawFrame() {
try {
// PERSISTENT FRAME CALLBACKS
// 关键回调
for (FrameCallback callback in _persistentCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp);
// POST-FRAME CALLBACKS
final List<FrameCallback> localPostFrameCallbacks =
List<FrameCallback>.from(_postFrameCallbacks);
_postFrameCallbacks.clear();
for (FrameCallback callback in localPostFrameCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp);
} finally {
/·····························/
}
}

Flutter AnimationController回调原理一期中我们提到过,在Flutter的SchedulerBinding中维护了这样三个队列




  • Transient callbacks,由系统的[Window.onBeginFrame]回调,用于同步应用程序的行为 到系统的展示。例如,[Ticker]s和[AnimationController]s触发器来自与它。

  • Persistent callbacks 由系统的[Window.onDrawFrame]方法触发回调。例如,框架层使用他来驱动渲染管道进行build, layout,paint

  • Post-frame callbacks在下一帧绘制前回调,主要做一些清理和准备工作 Non-rendering tasks 非渲染的任务,可以通过此回调获取一帧的渲染时间进行帧率相关的性能监控



SchedulerBinding.handleDrawFrame()中对_persistentCallbacks_postFrameCallbacks集合进行了回调。根据上面的描述可知,_persistentCallbacks中是一些固定流程的回调,例如build,layout,paint。跟踪这个_persistentCallbacks这个集合,发现在RendererBinding.initInstances()初始化中调用了addPersistentFrameCallback(_handlePersistentFrameCallback)方法。这个方法只有一行调用就是drawFrame()



总结:



  • SchedulerBinding中维护了这样三个队列TransientCallbacks(动画处理),PersistentCallbacks(页面构建渲染),PostframeCallbacks(每帧绘制完成后),并在合适的时机对其进行回调。

  • 当收到Engine的渲染通知之后通过Windows.onDrawFrame方法回调到Framework层调用handleDrawFrame

  • handleDrawFrame回调PersistentCallbacks(页面构建渲染),最终调用drawFrame()





4、drawFrame()


查看drawFrame()方法一般会直接点击到RendererBinding


RendererBinding#drawFrame()


void drawFrame() {
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
renderView.compositeFrame(); // this sends the bits to the GPU
pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
}

从这几个方法名能大致看出,这里调用了布局,绘制,渲染帧的。而且看类名,这是负责渲染的Binding,并没有调用Widget的构建。这是因为WidgetsBinding是onRendererBinding的(理解为继承),其中重写了drawFrame(),实际上调用的应该是WidgetsBinding.drawFrame()


WidgetsBinding#drawFrame()


@override
void drawFrame() {
try {
if (renderViewElement != null)
// buildOwner就是前面提到的负责管理widgetbuild的对象
// 这里的renderViewElement是整个UI树的根节点
buildOwner.buildScope(renderViewElement);
super.drawFrame();
//将不再活跃的节点从UI树中移除
buildOwner.finalizeTree();
} finally {
/·················/
}
}

super.drawFrame()之前,先调用 buildOwner.buildScope(renderViewElement)
BuildOwner#buildScope(Element context, [ VoidCallback callback ])


void buildScope(Element context, [ VoidCallback callback ]) {
if (callback == null && _dirtyElements.isEmpty)
return;
try {
_scheduledFlushDirtyElements = true;
_dirtyElementsNeedsResorting = false;
_dirtyElements.sort(Element._sort);
_dirtyElementsNeedsResorting = false;
int dirtyCount = _dirtyElements.length;
int index = 0;
while (index < dirtyCount) {
try {
///关键在这
_dirtyElements[index].rebuild();
} catch (e, stack) {
/···············/
}
}
} finally {
for (Element element in _dirtyElements) {
element._inDirtyList = false;
}
_dirtyElements.clear();
}
}

前面在setState()之后,将homePageState添加到_dirtyElements里面。而这个方法会对集合内的每一个对象调用rebuild()rebuild()这个方法最终走到performRebuild(),这是一个Element中的一个抽象方法。




二、为什么高位置的setState ()会消耗性能


1、performRebuild()


查看StatelessElementStatefulElement共同祖先CompantElement中的实现


CompantElement#performRebuild()


void performRebuild() {
Widget built;
try {
built = build();
} catch (e, stack) {
built = ErrorWidget.builder();
}
try {
_child = updateChild(_child, built, slot);
} catch (e, stack) {
built = ErrorWidget.builder();
_child = updateChild(null, built, slot);
}

}

这个方法直接调用子类的build方法返回了一个Widget,对应调用前面的HomePageState()中的build方法。


将这个新build()出来的widget和之前挂载在Element树上的_child(Element类型)作为参数,传入updateChild(_child, built, slot)中。setState()的核心逻辑就在 updateChild(_child, built, slot)


2、updateChild(_child, built, slot)


StatefulElement#updateChild(Element child, Widget newWidget, dynamic newSlot)


@protected
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
if (newWidget == null) {
if (child != null)
//child == null && newWidget == null
deactivateChild(child);
//child != null && newWidget == null
return null;
}
if (child != null) {
if (child.widget == newWidget) {
//child != null && newWidget == child.widget
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
return child;
}
if (Widget.canUpdate(child.widget, newWidget)) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
//child != null && Widget.canUpdate(child.widget, newWidget)
child.update(newWidget);
return child;
}
deactivateChild(child);
}
// child != null && !Widget.canUpdate(child.widget, newWidget)
return inflateWidget(newWidget, newSlot);
}

这个方法上官方提供了这样的注释:






















newWidget == nullnewWidget != null
child == nullReturns null.Returns new [Element].
child != nullOld child is removed, returns null.Old child updated if possible, returns child or new [Element].

总的来说,根据之前挂载在Element树上的_child以及再次调用build()出来的newWidget对象,共有四种情况




  • 如果之前的位置child为null

    • A、如果newWidget为null的话,说明这个位置始终没有子节点,直接返回null即可。

    • B、如果newWidget不为null,说明这个位置新增加了子节点调用inflateWidget(newWidget, newSlot)生成一个新的Element返回



  • 如果之前的child不为null

    • C、如果newWidget为null的话,说明这个位置需要移除以前的节点,调用 deactivateChild(child)移除并且返回null

    • D、如果newWidget不为null的话,先调用Widget.canUpdate(child.widget, newWidget)对比是否能更新。这个方法会对比两个Widget的runtimeTypekey,如果一致则说明子Widget没有改变,只是需要根据newWidget(配置清单)更新下当前节点的数据child.update(newWidget);如果不一致说明这个位置发生变化,则deactivateChild(child)后返回inflateWidget(newWidget, newSlot)





而在demo中,观察代码我们可以知道



在homePageState中调用setState()后,child和newWidget都不为空都是Scaffold类型,并且由于我们没有显示的指定key,所以会走child.update(newWidget)方法**(注意这里的child已经变成Scaffold)**。


3、递归更新


update(covariant Widget newWidget)是一个抽象方法,不同element有不同实现,以StatulElement为例


void update(StatefulWidget newWidget) {
super.update(newWidget);
assert(widget == newWidget);
final StatefulWidget oldWidget = _state._widget;
// Notice that we mark ourselves as dirty before calling didUpdateWidget to
// let authors call setState from within didUpdateWidget without triggering
// asserts.
_dirty = true;
_state._widget = widget;
try {
final dynamic debugCheckForReturnedFuture = _state.didUpdateWidget(oldWidget) as dynamic;
} finally {
_debugSetAllowIgnoredCallsToMarkNeedsBuild(false);
}
rebuild();
}

这个方法先回调用_state.didUpdateWidget我们可以在State中重写这个方法,走到最后发现最终再次调用了rebuild()。但这里需要注意这次调用rebuild()的已经不是HomePageState了,而是他的第一个子节点Scaffold。所以整个过程又会再次走到performRebuild(),又在再次调用updateChild(_child, built, slot)更新子节点。不断的递归直到页面的最子一级节点。如图



build()过程虽然只是调用一个组件的构造方法,不涉及对Element树的挂载操作。但因为我们一个组件往往是N多个Widget的嵌套组合,每个都遍历一遍开销算下来并不小(感兴趣可以数数Scaffold有多少层嵌套)。


回到我们的demo中,其实我们的诉求只是点击+号改变以前显示的数据。



但直接在页面节点调用setState()将会重新调用所有Widget(包括他们中的各种嵌套)的build()方法,如果我们的需求是一个较为复杂的页面,这样带来的开销消耗可想而知。


而要想解决这个问题可以参考告别setState()! 优雅的UI与Model绑定 Flutter DataBus使用~




总结


当我们在一个高节点调用setState()的时候会构建再次build所有的Widget,虽然不一定挂载到Element树中,但是平时我们使用的Widget中往往嵌套多个其他类型的Widget,每个build()方法走下来最终也会带来不小的开销,因此通过各种状态管理方案,Stream等方式,只做局部刷新,是我们日常开发中应该养成的良好习惯。




最后


本期我们分析了setState()过程,重点分析了递归更新的过程。正如安卓Activity或者Fragment的生命周期,Flutter中Widget和State同样也提供了对应的回调,如initState()build()。这些方法背后是谁在调用,他们的调用时序是如何?Element的生命周期是如何调用的?将会在下一期和大家一一分析~


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

最近大火的元宇宙到底是什么?

准确地说,元宇宙不是一个新的概念,它更像是一个经典概念的重生。1992年,美国著名科幻大师尼尔·斯蒂芬森在其小说《雪崩》中这样描述元宇宙:“戴上耳机和目镜,找到连接终端,就能够以虚拟分身的方式进入由计算机模拟、与真实世界平行的虚拟空间。”今年3月,元宇宙概念第...
继续阅读 »

准确地说,元宇宙不是一个新的概念,它更像是一个经典概念的重生。1992年,美国著名科幻大师尼尔·斯蒂芬森在其小说《雪崩》中这样描述元宇宙:“戴上耳机和目镜,找到连接终端,就能够以虚拟分身的方式进入由计算机模拟、与真实世界平行的虚拟空间。”

今年3月,元宇宙概念第一股罗布乐思(Roblox)在美国纽约证券交易所正式上市;5月,Facebook表示将在5年内转型成一家元宇宙公司;8月,字节跳动斥巨资收购VR创业公司Pico……今年,元宇宙无疑成为了科技领域最火爆的概念之一。

那么,元宇宙到底是什么?为何各大数字科技巨头纷纷入局元宇宙?我国元宇宙产业又该如何布局与发展?

元宇宙目前尚无公认定义

准确地说,元宇宙不是一个新的概念,它更像是一个经典概念的重生,是在扩展现实(XR)、区块链、云计算、数字孪生等新技术下的概念具化。

1992年,美国著名科幻大师尼尔·斯蒂芬森在其小说《雪崩》中这样描述元宇宙:“戴上耳机和目镜,找到连接终端,就能够以虚拟分身的方式进入由计算机模拟、与真实世界平行的虚拟空间。”

当然,核心概念缺乏公认的定义是前沿科技领域的一个普遍现象。元宇宙虽然备受各方关注和期待,但同样没有一个公认的定义。回归概念本质,可以认为元宇宙是在传统网络空间基础上,伴随多种数字技术成熟度的提升,构建形成的既映射于、又独立于现实世界的虚拟世界。同时,元宇宙并非一个简单的虚拟空间,而是把网络、硬件终端和用户囊括进一个永续的、广覆盖的虚拟现实系统之中,系统中既有现实世界的数字化复制物,也有虚拟世界的创造物。

当前,关于元宇宙的一切都还在争论中,从不同视角去分析会得到差异性极大的结论,但元宇宙所具有的基本特征则已得到业界的普遍认可。

其基本特征包括:沉浸式体验,低延迟和拟真感让用户具有身临其境的感官体验;虚拟化分身,现实世界的用户将在数字世界中拥有一个或多个ID身份;开放式创造,用户通过终端进入数字世界,可利用海量资源展开创造活动;强社交属性,现实社交关系链将在数字世界发生转移和重组;稳定化系统,具有安全、稳定、有序的经济运行系统。

受到科技巨头、政府部门的青睐

8月以来,元宇宙概念更加炙手可热,日本社交巨头GREE宣布将开展元宇宙业务、英伟达发布会上出场了十几秒的“数字替身”、微软在Inspire全球合作伙伴大会上宣布了企业元宇宙解决方案……事实上,不仅是各大科技巨头在争相布局元宇宙赛道,一些国家的政府相关部门也积极参与其中。5月18日,韩国科学技术和信息通信部发起成立了“元宇宙联盟”,该联盟包括现代、SK集团、LG集团等200多家韩国本土企业和组织,其目标是打造国家级增强现实平台,并在未来向社会提供公共虚拟服务;7月13日,日本经济产业省发布了《关于虚拟空间行业未来可能性与课题的调查报告》,归纳总结了日本虚拟空间行业亟须解决的问题,以期能在全球虚拟空间行业中占据主导地位;8月31日,韩国财政部发布2022年预算,计划斥资2000万美元用于元宇宙平台开发。

元宇宙为何能受到科技巨头、风险投资企业、初创企业,甚至政府部门的青睐?

从企业来看,目前元宇宙仍处于行业发展的初级阶段,无论是底层技术还是应用场景,与未来的成熟形态相比仍有较大差距,但这也意味着

元宇宙相关产业可拓展的空间巨大。因此,拥有多重优势的数字科技巨头想要守住市场,数字科技领域初创企业要获得弯道超车的机会,就必须提前布局,甚至加码元宇宙赛道。

从政府来看,元宇宙不仅是重要的新兴产业,也是需要重视的社会治理领域。伴随着元宇宙产业的快速发展,随之而来的将是一系列新的问题和挑战。元宇宙资深研究专家马修·鲍尔提出:“元宇宙是一个和移动互联网同等级别的概念。”以移动互联网去类比元宇宙,就可以更好地理解政府部门对其关注的内在逻辑。政府希望通过参与元宇宙的形成和发展过程,以便前瞻性考虑和解决其发展所带来的相关问题。

在技术、标准等方面做好前瞻性布局

元宇宙是一个极致开放、复杂、巨大的系统,它涵盖了整个网络空间以及众多硬件设备和现实条件,是由多类型建设者共同构建的超大型数字应用生态。为了加快推动元宇宙从概念走向现实,并在未来的全球竞争中抢占先机,我国应在技术、标准、法律3个方面做好前瞻性布局。

从技术方面来看,技术局限性是元宇宙目前发展的最大瓶颈,XR、区块链、人工智能等相应底层技术距离元宇宙落地应用的需求仍有较大差距。元宇宙产业的成熟,需要大量的基础研究做支撑。对此,要谨防元宇宙成为一些企业的炒作噱头,应鼓励相关企业加强基础研究,增强技术创新能力,稳步提高相关产业技术的成熟度。

从行业标准方面来看,只有像互联网那样通过一系列标准和协议来定义元宇宙,才能实现元宇宙不同生态系统的大连接。对此,应加强元宇宙标准统筹规划,引导和鼓励科技巨头之间展开标准化合作,支持企事业单位进行技术、硬件、软件、服务、内容等行业标准的研制工作,积极地参与制定元宇宙的全球性标准。

从法律方面来看,随着元宇宙的发展,以及逐步走向成熟,平台垄断、税收征管、监管审查、数据安全等一系列问题也将随之产生,提前思考如何防止和解决元宇宙所产生的法律问题成为必不可少的环节。对此,应加强数字科技领域立法工作,在数据、算法、交易等方面及时跟进,研究元宇宙相关法律制度。

可以肯定的是,在技术演进和人类需求的共同推动下,元宇宙场景的实现,元宇宙产业的成熟,只是一个时间问题。作为真实世界的延伸与拓展,元宇宙所带来的巨大机遇和革命性作用是值得期待的,但正因如此,我们更需要理性看待当前的元宇宙热潮,推动元宇宙产业健康发展。

(作者系中国社会科学院数量经济与技术经济研究所副研究员)

收起阅读 »

Flutter 毁了客户端和 Web 开发!

Google 重磅发布了专为 Web、移动和桌面而构建的 Flutter 2.0!将 Flutter 从移动开发框架扩展成可移植框架,因而开发者无需重写代码即可将应用扩展至桌面或网页。看似为了帮助Web和移动开发者,实际上不然,而本文作者认为,现在不应该再去想...
继续阅读 »

Google 重磅发布了专为 Web、移动和桌面而构建的 Flutter 2.0!将 Flutter 从移动开发框架扩展成可移植框架,因而开发者无需重写代码即可将应用扩展至桌面或网页。看似为了帮助Web和移动开发者,实际上不然,而本文作者认为,现在不应该再去想创建一个需要部署到所有平台的应用程序,Flutter反而毁了Web和移动开发。


以下为译文: 大家好,我是一名软件开发人员,我叫 Luke。


由于我选择了这个相当大胆的标题,为了避免误会,我要对其进行详细的解释。从技术角度来讲,Flutter 的确是一个跨平台的框架。也不止其,所有跨断技术都是非常糟糕的设计。


但是,我有点不同的看法。


从 Flutter 2.0 发布以来,我就察觉到它被炒的有点过了。但不应该再去想创建一个需要部署到所有平台的应用程序,Flutter反而毁了Web和移动开发。


请不要误会,我并不是要否定它,其实我也是 Flutter 的粉丝,亦将一如既往的拥护它。


我在日常工作中经常使用 Flutter 来开发 iOS 和 Android 应用程序。由于早前我是用 Kotlin 或者 Swift 来开发原生的应用,支持多种特性,如:扫描 / 页面识别、pin/biometric 应用程序认证、通知、firebase 统计和一些高级的用户流,现在用 Flutter 来开发应用,我对 Flutter 的优缺点的了解更加透彻。


1、六大平台


image.png


通过今年的 Flutter Engage 会议我们可知已经可以使用 Flutter 在 iOS、 Android、 Mac、 Windows、 Linux 和 Web 这六个平台中的任何一个平台上开发应用。这太棒了!但事情远没有这么简单...你的确可以在这 6 个平台上部署你的应用程序,但是说实话,我很少这么做。我很难想象一个人会在不同的平台上部署同一个应用程序,我认为应该根据不同的平台特点使用不同的设计模式。在大型设备上使用底部弹窗、应用程序条、简洁的列表就很别扭。一般来说,适合在移动设备上的组件和设计模式在桌面设备上却不合时宜,反之亦然。


我的一个非常好的朋友 Filip Hracek 在 Flutter Engage 演讲中提到“神奇的设计开发者”的相关话题,我非常赞同他的看法。我认为需要有更多的开发者真正知道他们正在做的是什么,而且不是盲目地跟从迭代面板。


Scrum Sprint 是一个可重复的固定时间框,在这个时间框内创造一个高价值的产品。-- 维基百科


强烈推荐大家观看 Filip 在 Youtube 上的相关视频片段http://www.youtube.com/watch?v=MIe…


接下来,我们重新回到 Flutter 这个话题:


2、不应该再去想创建一个需要部署到所有平台的应用程序


你更应该去想如何将你要编写的应用程序模块化,以便在未来更好地复用这些模块。给你们举个例子:在我的公司,我们正在开发专注于用户数据的应用程序。


这就需要创建自定义和高级的调查报告,我们不希望每次添加新问题时都要编写新的窗口小部件。我们的做法是:编写一个包含所有可能的调查逻辑的模块,在许多其他项目中复用它(而不需要每次都重写一遍相似的代码)


我给你举上面这个例子的目的是提醒你在构建一个应用程序时,你更应该着重思考你要做的应用程序或整个业务的重点是什么。更应该去重点思考,它背后的业务逻辑是什么?


在计算机软件中,业务逻辑或领域建模也是程序的一部分,它对真实世界的业务规则进行编码,确定如何创建、存储和修改数据。


当你明确了领域划分,你可以将一个领域封装成独立的模块,你可以将该模块在需要开发的 Flutter 应用程序中复用。


但 Luke,这有什么好大惊小怪的吗?


对,这是一个好问题!


对于相同的业务逻辑,你可以用不同的用户流来创建多个 Flutter 应用。你可以将要开发的 Flutter 应用进行分类(如:移动应用、桌面应用和 Web应用),这将能帮助关注到不同平台的差异,对特定平台进行特定处理最终将获得更好的用户体验。


3、针对不同平台要编****写多个应用程序


虽然 Flutter 还算是一个相对比较新的技术,还主要针对小公司和个人开发者,但这不妨碍它成为一个人人皆可用的伟大工具。


我参与开发过多个企业级应用程序。根据我的经验,系统的每个部分都需要有一个清晰的工作流程。开发一个系统通常需要前端、后端等。为了节约成本,编写一个应用程序,在不同的平台运行也越发流行。为了实现这个目的,你需要雇一个团队进行专门开发。你敢想象,十几个人的团队开发同一套代码来实现所有平台的特性吗?这简直是管理层的噩梦。很可能出现:一部分开发人员开发的桌面特性与移动团队正在开发的特性相冲突的情况。


其次,应用程序包也会越来越臃肿,然而很多时候并不是每个平台都需要有一份软件包。现在,正值 Flutter 2.0 发布的时候,由于我并没有将所有的包都进行升级,还不支持 null 安全还需要手动解决依赖冲突的问题。


4、为什么 Flutter 不是一个跨平台的框架


在读了这篇文章之后,或许你能够理解为什么我会认为 Flutter 不是一个真正的跨平台框架。Flutter 是一个为我们提供了为每个平台构建应用程序所需的功能的工具。我认为,真正实现跨平台不应该只开发一个应用程序,更应该开发一组由相同的业务逻辑驱动的应用程序集合。


此外,当我们编写 Flutter 应用程序时,我们并没有跨越任何平台。我们这种所谓的跨平台,不过是用 Xamarin 或其他工具将写好的代码翻译成原生元素。


如果非要把 Flutter 和其他东西进行类比的话,那么与之相似的就是游戏引擎(如 Unity)。我们不需要专门在 Windows 或者 Mac 系统上开发对应平台的游戏。我们可以使用 Unity 编写,然后将其导出到一个特定的平台。使用 Unity 编写一个游戏然后导出到多个平台和真正的跨平台完全也是两码事。


因为每个项目都有技术债务,你应该停止抱怨,并开始重构。每次开发新功能之前都应该进行小型代码重构。但接入 Flutter 大规模的重构和重写永远不会有好结果。


5、结尾


全文都在讨论跨平台相关话题, 以上就是我认为 flutter 毁了 Web 开发的原因。很多人对这一说法很感兴趣,并热切地加入了辩论。如果你认为 flutter 并没有那么糟糕,或许你会持有不同意见 。


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

freeze、seal、preventExtensions对比

在Object常用的方法中,Object.freeze和Object.seal对于初学者而言,是两个较为容易混淆的概念,常常傻傻分不清两者的区别和应用场景 概念 先看看两者定义 Object.freeze在MDN中的定义 Object.freeze() 方法...
继续阅读 »

Object常用的方法中,Object.freezeObject.seal对于初学者而言,是两个较为容易混淆的概念,常常傻傻分不清两者的区别和应用场景


概念


先看看两者定义


Object.freeze在MDN中的定义



Object.freeze() 方法可以冻结一个对象。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。freeze() 返回和传入的参数相同的对象。



Object.seal在MDN中的定义



Object.seal()方法封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置。当前属性的值只要原来是可写的就可以改变。



从两者定义可以得到两者差异:Object.freeze核心是冻结,强调的是不可修改。Object.seal核心是封闭,强调的是不可配置,不影响老的属性值修改


差异


定义一个对象,接下来的对比围绕这个对象进行


"use strict";
const obj = {
name: "nordon"
};

使用Object.freeze


Object.freeze(obj);
Object.isFrozen(obj); // true
obj.name = "wy";

使用Object.isFrozen可以检测数据是否被Object.freeze冻结,返回一个Boolean类型数据


此时对冻结之后的数据进行修改,控制台将会报错:Uncaught TypeError: Cannot assign to read only property 'name' of object '#<Object>'


使用Object.seal


Object.seal(obj);
Object.isSealed(obj); // true
obj.name = "wy";

使用Object.isSealed可以检测数据是否被Object.seal封闭,返回一个Boolean类型数据


此时修改name是成功的,此时的obj也成功被修改


注意:若是不开启严格模式,浏览器会采用静默模式,不会在控制台抛出异常信息


共同点


主要以Object.seal演示


不可删除


delete obj.name;

控制台将会抛出异常:Uncaught TypeError: Cannot delete property 'name' of #<Object>


不可配置


可以修改原有的属性值


Object.defineProperty(obj, 'name', {
value: 'wy'
})

不可增加新的属性值


Object.defineProperty(obj, "age", {
value: 12,
});

控制台将会抛出异常:Uncaught TypeError: Cannot define property age, object is not extensible


深层嵌套


两者对于深层嵌套的数据都表现为:无能为力


定义一个嵌套的对象


"use strict";
const obj = {
name: "nordon",
info: {
foo: 'bar'
}
};

对于obj而言,无论是freeze还是seal,操作info内部的数据都无法做到对应的处理


obj.info.msg = 'msg'

数据源obj被修改,不受冻结或者冰封的影响


若是想要做到嵌套数据的处理,需要递归便利数据源处理,此操作需要注意:数据中包含循环引用时,将会触发无限循环


preventExtensions


最后介绍一下Object.preventExtensions,为何这个方法没有放在与Object.freezeObject.seal一起对比呢?因为其和seal基本可保持一致,唯一的区别就是可以delete属性,因此单独放在最后介绍


看一段代码


"use strict";
const obj = {
name: "nordon",
};

Object.preventExtensions(obj);
Object.isExtensible(obj); // false, 代表其不可扩展

delete obj.name;

作者:Nordon
链接:https://juejin.cn/post/7028389571561947172

收起阅读 »

【喵猫秀秀秀】用CSS向你展示猫立方!!

前言 这次,我们用vue2+scss,带大家来实现一个六面体的猫立方。 本次的逻辑我们不适用任何的js代码,仅仅只依靠css来完成。 所以,通过本片文章,你可以收获一些css动画相关的技巧。 先看看效果 预习 本次我们要用到的知识点 transform ...
继续阅读 »

前言


这次,我们用vue2+scss,带大家来实现一个六面体的猫立方。


本次的逻辑我们不适用任何的js代码,仅仅只依靠css来完成。


所以,通过本片文章,你可以收获一些css动画相关的技巧。


先看看效果


cat3D.gif


预习


本次我们要用到的知识点



  1. transform


解释:transform 属性向元素应用 2D 或 3D 转换。该属性允许我们对元素进行旋转、缩放、移动或倾斜。


要用哪个,可以对着这个表格查


image.png



  1. transform-style


解释:transform--style属性指定嵌套元素是怎样在三维空间中呈现。


注意:  使用此属性必须先使用 transform 属性.



  1. transition


解释:transition 属性是一个简写属性,用于设置四个过渡属性:



  • transition-property

  • transition-duration

  • transition-timing-function

  • transition-delay


注释:请始终设置 transition-duration 属性,否则时长为 0,就不会产生过渡效果。


分析


我们先拆解下这个猫3D的特点,它有以下特点



  1. 它一直在不停的转

  2. 它由两个六面体组成,外面一个,里面一个

  3. 鼠标靠近外面的六面体,六面体的六个面会往外扩,露出里面的小六面体


开始


1.因为我们做的是六面体,有2个六面体,一个在里面,一个在外面。2个六面体,12个面,先准备12张猫主子的图片。


image.png



  1. 然后我们新建img3D.vue文件,开干


image.png


步骤一


先来完成第一个特点不停的转


cat3D1.gif
代码如下:


<template>
<div>
<div class="container">
</div>
</div>
</template>

<script>

</script>

<style lang="scss" scoped>
* {
margin: 0px;
padding: 0px;
}

.container {
position: relative;
margin: 0px auto;
margin-top: 9%;
margin-left: 42%;
width: 200px;
height: 200px;
transform: rotateX(-30deg) rotateY(-80deg);
transform-style: preserve-3d;
animation: rotate 15s infinite;
/* 我这里用一个边框线来更容易看到效果 */
border: 1px solid red;
}

/* 旋转立方体 */
@keyframes rotate {
from {
transform: rotateX(0deg) rotateY(0deg);
}

to {
transform: rotateX(360deg) rotateY(360deg);
}
}
</style>

步骤二


弄外面的六面体,并且六面体在鼠标悬停的时候,需要往外扩
效果如下:


cat3D2.gif


<template>
<div>
<div class="container">
<!-- 外层立方体 -->
<div class="outer-container">
<div class="outer-top">
<img :src="cat1" />
</div>
<div class="outer-bottom">
<img :src="cat2" />
</div>
<div class="outer-front">
<img :src="cat3" />
</div>
<div class="outer-back">
<img :src="cat4" />
</div>
<div class="outer-left">
<img :src="cat5" />
</div>
<div class="outer-right">
<img :src="cat6" />
</div>
</div>
</div>
</div>
</template>

<script>
import cat1 from "@/assets/cats/cat1.png";
import cat2 from "@/assets/cats/cat2.png";
import cat3 from "@/assets/cats/cat3.png";
import cat4 from "@/assets/cats/cat4.png";
import cat5 from "@/assets/cats/cat5.png";
import cat6 from "@/assets/cats/cat6.png";

export default {
data() {
return {
cat1,
cat2,
cat3,
cat4,
cat5,
cat6,
};
},
};
</script>

<style lang="scss" scoped>
...
/* 设置图片可以在三维空间里展示 */
.container .outer-container {
transform-style: preserve-3d;
}
/* 设置图片尺寸固定宽高200px */
.outer-container img {
width: 200px;
height: 200px;
}

/* 外层立方体样式 */
.outer-container .outer-top,
.outer-container .outer-bottom,
.outer-container .outer-right,
.outer-container .outer-left,
.outer-container .outer-front,
.outer-container .outer-back {
position: absolute;
top: 0;
left: 0;
width: 200px;
height: 200px;
border: 1px solid #fff;
opacity: 0.3;
transition: all 0.9s;
}

/* 全靠transform 属性撑起来了外面的六面体 */
.outer-top {
transform: rotateX(90deg) translateZ(100px);
}

.outer-bottom {
transform: rotateX(-90deg) translateZ(100px);
}

.outer-front {
transform: rotateY(0deg) translateZ(100px);
}

.outer-back {
transform: translateZ(-100px) rotateY(180deg);
}

.outer-left {
transform: rotateY(90deg) translateZ(100px);
}

.outer-right {
transform: rotateY(-90deg) translateZ(100px);
}

/* 鼠标悬停 外面的六面体,六条边都往外扩出去,并且透明度变浅了,猫猫图片更清晰了 */
.cube:hover .outer-top {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateX(90deg) translateZ(200px);
}

.container:hover .outer-bottom {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateX(-90deg) translateZ(200px);
}

.container:hover .outer-front {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateY(0deg) translateZ(200px);
}

.container:hover .outer-back {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: translateZ(-200px) rotateY(180deg);
}

.container:hover .outer-left {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateY(90deg) translateZ(200px);
}

.container:hover .outer-right {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateY(-90deg) translateZ(200px);
}
</style>


步骤三


弄里面的六面体,这个六边形比较简单,没有移入移出,鼠标悬停等的样式效果
效果如下:


cat3D.gif
代码如下:


<template>
<div>
<div class="container">
<!-- 外层立方体 -->
...
<!-- 内层立方体 -->
<div class="inner-container">
<div class="inner-top">
<img :src="cat7" />
</div>
<div class="inner-bottom">
<img :src="cat8" />
</div>
<div class="inner-front">
<img :src="cat9" />
</div>
<div class="inner-back">
<img :src="cat10" />
</div>
<div class="inner-left">
<img :src="cat11" />
</div>
<div class="inner-right">
<img :src="cat12" />
</div>
</div>
</div>
</div>
</template>

<script>
...
import cat7 from "@/assets/cats/cat7.png";
import cat8 from "@/assets/cats/cat8.png";
import cat9 from "@/assets/cats/cat9.png";
import cat10 from "@/assets/cats/cat10.png";
import cat11 from "@/assets/cats/cat11.png";
import cat12 from "@/assets/cats/cat12.png";

export default {
data() {
return {
...
cat7,
cat8,
cat9,
cat10,
cat11,
cat12,
};
},
};
</script>

<style lang="scss" scoped>
...
/* 设置图片可以在三维空间里展示 */
.container .outer-container,
.container .inner-container {
transform-style: preserve-3d;
}

...

/* 设置里面的六面体样式 */
/* 嵌套的内层立方体样式 */
.inner-container > div {
position: absolute;
top: 35px;
left: 35px;
width: 130px;
height: 130px;
}
/* 里面的六面体尺寸宽高设置130px */
.inner-container img {
width: 130px;
height: 130px;
}

.inner-top {
transform: rotateX(90deg) translateZ(65px);
}

.inner-bottom {
transform: rotateX(-90deg) translateZ(65px);
}

.inner-front {
transform: rotateY(0deg) translateZ(65px);
}

.inner-back {
transform: translateZ(-65px) rotateY(180deg);
}

.inner-left {
transform: rotateY(90deg) translateZ(65px);
}

.inner-right {
transform: rotateY(-90deg) translateZ(65px);
}
</style>

最后完整代码:


<template>
<div>
<div class="container">
<!-- 外层立方体 -->
<div class="outer-container">
<div class="outer-top">
<img :src="cat1" />
</div>
<div class="outer-bottom">
<img :src="cat2" />
</div>
<div class="outer-front">
<img :src="cat3" />
</div>
<div class="outer-back">
<img :src="cat4" />
</div>
<div class="outer-left">
<img :src="cat5" />
</div>
<div class="outer-right">
<img :src="cat6" />
</div>
</div>
<!-- 内层立方体 -->
<div class="inner-container">
<div class="inner-top">
<img :src="cat7" />
</div>
<div class="inner-bottom">
<img :src="cat8" />
</div>
<div class="inner-front">
<img :src="cat9" />
</div>
<div class="inner-back">
<img :src="cat10" />
</div>
<div class="inner-left">
<img :src="cat11" />
</div>
<div class="inner-right">
<img :src="cat12" />
</div>
</div>
</div>
</div>
</template>

<script>
import cat1 from "@/assets/cats/cat1.png";
import cat2 from "@/assets/cats/cat2.png";
import cat3 from "@/assets/cats/cat3.png";
import cat4 from "@/assets/cats/cat4.png";
import cat5 from "@/assets/cats/cat5.png";
import cat6 from "@/assets/cats/cat6.png";
import cat7 from "@/assets/cats/cat7.png";
import cat8 from "@/assets/cats/cat8.png";
import cat9 from "@/assets/cats/cat9.png";
import cat10 from "@/assets/cats/cat10.png";
import cat11 from "@/assets/cats/cat11.png";
import cat12 from "@/assets/cats/cat12.png";

export default {
data() {
return {
cat1,
cat2,
cat3,
cat4,
cat5,
cat6,
cat7,
cat8,
cat9,
cat10,
cat11,
cat12,
};
},
};
</script>

<style lang="scss" scoped>
* {
margin: 0px;
padding: 0px;
}

.container {
position: relative;
margin: 0px auto;
margin-top: 9%;
margin-left: 42%;
width: 200px;
height: 200px;
transform: rotateX(-30deg) rotateY(-80deg);
transform-style: preserve-3d;
animation: rotate 15s infinite;
/* 我这里用一个边框线来更容易看到效果 */
/* border: 1px solid red; */
}

/* 旋转立方体 */
@keyframes rotate {
from {
transform: rotateX(0deg) rotateY(0deg);
}

to {
transform: rotateX(360deg) rotateY(360deg);
}
}
/* 设置图片可以在三维空间里展示 */
.container .outer-container,
.container .inner-container {
transform-style: preserve-3d;
}
/* 设置图片尺寸固定宽高200px */
.outer-container img {
width: 200px;
height: 200px;
}

/* 外层立方体样式 */
.outer-container .outer-top,
.outer-container .outer-bottom,
.outer-container .outer-right,
.outer-container .outer-left,
.outer-container .outer-front,
.outer-container .outer-back {
position: absolute;
top: 0;
left: 0;
width: 200px;
height: 200px;
border: 1px solid #fff;
opacity: 0.3;
transition: all 0.9s;
}

/* 全靠transform 属性撑起来了外面的六面体 */
.outer-top {
transform: rotateX(90deg) translateZ(100px);
}

.outer-bottom {
transform: rotateX(-90deg) translateZ(100px);
}

.outer-front {
transform: rotateY(0deg) translateZ(100px);
}

.outer-back {
transform: translateZ(-100px) rotateY(180deg);
}

.outer-left {
transform: rotateY(90deg) translateZ(100px);
}

.outer-right {
transform: rotateY(-90deg) translateZ(100px);
}

// 鼠标悬停 外面的六面体,六条边都往外扩出去,并且透明度变浅了,猫猫图片更清晰了
.cube:hover .outer-top {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateX(90deg) translateZ(200px);
}

.container:hover .outer-bottom {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateX(-90deg) translateZ(200px);
}

.container:hover .outer-front {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateY(0deg) translateZ(200px);
}

.container:hover .outer-back {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: translateZ(-200px) rotateY(180deg);
}

.container:hover .outer-left {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateY(90deg) translateZ(200px);
}

.container:hover .outer-right {
right: -70px;
bottom: -70px;
opacity: 0.8;
transform: rotateY(-90deg) translateZ(200px);
}

/* 设置里面的六面体样式 */
/* 嵌套的内层立方体样式 */
.inner-container > div {
position: absolute;
top: 35px;
left: 35px;
width: 130px;
height: 130px;
}
/* 里面的六面体尺寸宽高设置130px */
.inner-container img {
width: 130px;
height: 130px;
}

.inner-top {
transform: rotateX(90deg) translateZ(65px);
}

.inner-bottom {
transform: rotateX(-90deg) translateZ(65px);
}

.inner-front {
transform: rotateY(0deg) translateZ(65px);
}

.inner-back {
transform: translateZ(-65px) rotateY(180deg);
}

.inner-left {
transform: rotateY(90deg) translateZ(65px);
}

.inner-right {
transform: rotateY(-90deg) translateZ(65px);
}
</style>


都看到这里了,求各位观众大佬们点个赞再走吧,你的赞对我非常重要



收起阅读 »

一款强大到没朋友的图片编辑插件,爱了爱了!

前言 最近用户提出了一个新的需求,老师可以批改学生的图片作业,需要对图片进行旋转、缩放、裁剪、涂鸦、标注、添加文本等。乍一听,又要掉不少头发。有没有功能强大的插件实现以上功能,让我有更多的时间去阻止女票双十一剁手呢?答案当然是有的。 效果展示涂鸦 裁剪 ...
继续阅读 »

前言


最近用户提出了一个新的需求,老师可以批改学生的图片作业,需要对图片进行旋转、缩放、裁剪、涂鸦、标注、添加文本等。乍一听,又要掉不少头发。有没有功能强大的插件实现以上功能,让我有更多的时间去阻止女票双十一剁手呢?答案当然是有的。


效果展示

涂鸦



涂鸦2.jpg


裁剪


裁剪.jpg


标注


标注2.jpg


旋转


旋转2.jpg


滤镜


1636088844(1).jpg


是不是很强大!还有众多功能我就不一一展示了。那么还等什么,跟我一起用起来吧~


安装


npm i tui-image-editor
// or
yarn add tui-image-editor

使用

快速体验



复制以下代码,将插件引入到自己的项目中。


<template>
<div class="drawing-container">
<div id="tui-image-editor"></div>
</div>

</template>
<script>
import "tui-image-editor/dist/tui-image-editor.css";
import "tui-color-picker/dist/tui-color-picker.css";
import ImageEditor from "tui-image-editor";
export default {
data() {
return {
instance: null,
};
},
mounted() {
this.init();
},
methods: {
init() {
this.instance = new ImageEditor(
document.querySelector("#tui-image-editor"),
{
includeUI: {
loadImage: {
path: "https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c1d7a1feb60346449c1a64893888989a~tplv-k3u1fbpfcp-watermark.image",
name: "image",
},
initMenu: "draw", // 默认打开的菜单项
menuBarPosition: "bottom", // 菜单所在的位置
},
cssMaxWidth: 1000, // canvas 最大宽度
cssMaxHeight: 600, // canvas 最大高度
}
);
document.getElementsByClassName("tui-image-editor-main")[0].style.top = "45px"; // 图片距顶部工具栏的距离
},
},
};
</script>


<style lang="scss" scoped>
.drawing-container {
height: 900px;
}
</style>


可以看到活生生的图片编辑工具就出现了,是不是很简单:


初始效果.jpg


国际化


由于是老外开发的,默认的文字描述都是英文,这里我们先汉化一下:


const locale_zh = {
ZoomIn: "放大",
ZoomOut: "缩小",
Hand: "手掌",
History: '历史',
Resize: '调整宽高',
Crop: "裁剪",
DeleteAll: "全部删除",
Delete: "删除",
Undo: "撤销",
Redo: "反撤销",
Reset: "重置",
Flip: "镜像",
Rotate: "旋转",
Draw: "画",
Shape: "形状标注",
Icon: "图标标注",
Text: "文字标注",
Mask: "遮罩",
Filter: "滤镜",
Bold: "加粗",
Italic: "斜体",
Underline: "下划线",
Left: "左对齐",
Center: "居中",
Right: "右对齐",
Color: "颜色",
"Text size": "字体大小",
Custom: "自定义",
Square: "正方形",
Apply: "应用",
Cancel: "取消",
"Flip X": "X 轴",
"Flip Y": "Y 轴",
Range: "区间",
Stroke: "描边",
Fill: "填充",
Circle: "圆",
Triangle: "三角",
Rectangle: "矩形",
Free: "曲线",
Straight: "直线",
Arrow: "箭头",
"Arrow-2": "箭头2",
"Arrow-3": "箭头3",
"Star-1": "星星1",
"Star-2": "星星2",
Polygon: "多边形",
Location: "定位",
Heart: "心形",
Bubble: "气泡",
"Custom icon": "自定义图标",
"Load Mask Image": "加载蒙层图片",
Grayscale: "灰度",
Blur: "模糊",
Sharpen: "锐化",
Emboss: "浮雕",
"Remove White": "除去白色",
Distance: "距离",
Brightness: "亮度",
Noise: "噪音",
"Color Filter": "彩色滤镜",
Sepia: "棕色",
Sepia2: "棕色2",
Invert: "负片",
Pixelate: "像素化",
Threshold: "阈值",
Tint: "色调",
Multiply: "正片叠底",
Blend: "混合色",
Width: "宽度",
Height: "高度",
"Lock Aspect Ratio": "锁定宽高比例",
};

this.instance = new ImageEditor(
document.querySelector("#tui-image-editor"),
{
includeUI: {
loadImage: {
path: "https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c1d7a1feb60346449c1a64893888989a~tplv-k3u1fbpfcp-watermark.image",
name: "image",
},
initMenu: "draw", // 默认打开的菜单项
menuBarPosition: "bottom", // 菜单所在的位置
locale: locale_zh, // 本地化语言为中文
},
cssMaxWidth: 1000, // canvas 最大宽度
cssMaxHeight: 600, // canvas 最大高度
}
);

效果如下:


汉化.jpg


自定义样式


默认风格为暗黑系,如果想改成白底,或者想改变按钮的大小、颜色等样式,可以使用自定义样式。


const customTheme = {
"common.bi.image": "", // 左上角logo图片
"common.bisize.width": "0px",
"common.bisize.height": "0px",
"common.backgroundImage": "none",
"common.backgroundColor": "#f3f4f6",
"common.border": "1px solid #333",

// header
"header.backgroundImage": "none",
"header.backgroundColor": "#f3f4f6",
"header.border": "0px",

// load button
"loadButton.backgroundColor": "#fff",
"loadButton.border": "1px solid #ddd",
"loadButton.color": "#222",
"loadButton.fontFamily": "NotoSans, sans-serif",
"loadButton.fontSize": "12px",
"loadButton.display": "none", // 隐藏

// download button
"downloadButton.backgroundColor": "#fdba3b",
"downloadButton.border": "1px solid #fdba3b",
"downloadButton.color": "#fff",
"downloadButton.fontFamily": "NotoSans, sans-serif",
"downloadButton.fontSize": "12px",
"downloadButton.display": "none", // 隐藏

// icons default
"menu.normalIcon.color": "#8a8a8a",
"menu.activeIcon.color": "#555555",
"menu.disabledIcon.color": "#ccc",
"menu.hoverIcon.color": "#e9e9e9",
"submenu.normalIcon.color": "#8a8a8a",
"submenu.activeIcon.color": "#e9e9e9",

"menu.iconSize.width": "24px",
"menu.iconSize.height": "24px",
"submenu.iconSize.width": "32px",
"submenu.iconSize.height": "32px",

// submenu primary color
"submenu.backgroundColor": "#1e1e1e",
"submenu.partition.color": "#858585",

// submenu labels
"submenu.normalLabel.color": "#858585",
"submenu.normalLabel.fontWeight": "lighter",
"submenu.activeLabel.color": "#fff",
"submenu.activeLabel.fontWeight": "lighter",

// checkbox style
"checkbox.border": "1px solid #ccc",
"checkbox.backgroundColor": "#fff",

// rango style
"range.pointer.color": "#fff",
"range.bar.color": "#666",
"range.subbar.color": "#d1d1d1",

"range.disabledPointer.color": "#414141",
"range.disabledBar.color": "#282828",
"range.disabledSubbar.color": "#414141",

"range.value.color": "#fff",
"range.value.fontWeight": "lighter",
"range.value.fontSize": "11px",
"range.value.border": "1px solid #353535",
"range.value.backgroundColor": "#151515",
"range.title.color": "#fff",
"range.title.fontWeight": "lighter",

// colorpicker style
"colorpicker.button.border": "1px solid #1e1e1e",
"colorpicker.title.color": "#fff",
};

this.instance = new ImageEditor(
document.querySelector("#tui-image-editor"),
{
includeUI: {
loadImage: {
path: "https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c1d7a1feb60346449c1a64893888989a~tplv-k3u1fbpfcp-watermark.image",
name: "image",
},
initMenu: "draw", // 默认打开的菜单项
menuBarPosition: "bottom", // 菜单所在的位置
locale: locale_zh, // 本地化语言为中文
theme: customTheme, // 自定义样式
},
cssMaxWidth: 1000, // canvas 最大宽度
cssMaxHeight: 600, // canvas 最大高度
}
);

效果如下:


自定义样式.jpg


按钮优化


通过自定义样式,我们看到右上角的 Load 和 Download 按钮已经被隐藏了,接下来我们再隐藏掉其他用不上的按钮(根据业务需要),并添加一个保存图片的按钮。


<template>
<div class="drawing-container">
<div id="tui-image-editor"></div>
<el-button class="save" type="primary" size="small" @click="save">保存</el-button>
</div>

</template>

// ...
methods: {
init() {
this.instance = new ImageEditor(
document.querySelector("#tui-image-editor"),
{
includeUI: {
loadImage: {
path: "https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c1d7a1feb60346449c1a64893888989a~tplv-k3u1fbpfcp-watermark.image",
name: "image",
},
menu: ["resize", "crop", "rotate", "draw", "shape", "icon", "text", "filter"], // 底部菜单按钮列表 隐藏镜像flip和遮罩mask
initMenu: "draw", // 默认打开的菜单项
menuBarPosition: "bottom", // 菜单所在的位置
locale: locale_zh, // 本地化语言为中文
theme: customTheme, // 自定义样式
},
cssMaxWidth: 1000, // canvas 最大宽度
cssMaxHeight: 600, // canvas 最大高度
}
);
document.getElementsByClassName("tui-image-editor-main")[0].style.top ="45px"; // 调整图片显示位置
document.getElementsByClassName("tie-btn-reset tui-image-editor-item help") [0].style.display = "none"; // 隐藏顶部重置按钮
},
// 保存图片,并上传
save() {
const base64String = this.instance.toDataURL(); // base64 文件
const data = window.atob(base64String.split(",")[1]);
const ia = new Uint8Array(data.length);
for (let i = 0; i < data.length; i++) {
ia[i] = data.charCodeAt(i);
}
const blob = new Blob([ia], { type: "image/png" }); // blob 文件
const form = new FormData();
form.append("image", blob);
// upload file
},
}

<style lang="scss" scoped>
.drawing-container {
height: 900px;
position: relative;
.save {
position: absolute;
right: 50px;
top: 15px;
}
}
</style>

效果如下:


按钮优化.jpg


可以看到顶部的重置按钮,以及底部的镜像和遮罩按钮都已经不见了。右上角多了一个我们自己的保存按钮,点击按钮,可以获取到 base64 文件和 blob 文件。


完整代码


<template>
<div class="drawing-container">
<div id="tui-image-editor"></div>
<el-button class="save" type="primary" size="small" @click="save">保存</el-button>
</div>

</template>
<script>
import 'tui-image-editor/dist/tui-image-editor.css'
import 'tui-color-picker/dist/tui-color-picker.css'
import ImageEditor from 'tui-image-editor'
const locale_zh = {
ZoomIn: '放大',
ZoomOut: '缩小',
Hand: '手掌',
History: '历史',
Resize: '调整宽高',
Crop: '裁剪',
DeleteAll: '全部删除',
Delete: '删除',
Undo: '撤销',
Redo: '反撤销',
Reset: '重置',
Flip: '镜像',
Rotate: '旋转',
Draw: '画',
Shape: '形状标注',
Icon: '图标标注',
Text: '文字标注',
Mask: '遮罩',
Filter: '滤镜',
Bold: '加粗',
Italic: '斜体',
Underline: '下划线',
Left: '左对齐',
Center: '居中',
Right: '右对齐',
Color: '颜色',
'Text size': '字体大小',
Custom: '自定义',
Square: '正方形',
Apply: '应用',
Cancel: '取消',
'Flip X': 'X 轴',
'Flip Y': 'Y 轴',
Range: '区间',
Stroke: '描边',
Fill: '填充',
Circle: '圆',
Triangle: '三角',
Rectangle: '矩形',
Free: '曲线',
Straight: '直线',
Arrow: '箭头',
'Arrow-2': '箭头2',
'Arrow-3': '箭头3',
'Star-1': '星星1',
'Star-2': '星星2',
Polygon: '多边形',
Location: '定位',
Heart: '心形',
Bubble: '气泡',
'Custom icon': '自定义图标',
'Load Mask Image': '加载蒙层图片',
Grayscale: '灰度',
Blur: '模糊',
Sharpen: '锐化',
Emboss: '浮雕',
'Remove White': '除去白色',
Distance: '距离',
Brightness: '亮度',
Noise: '噪音',
'Color Filter': '彩色滤镜',
Sepia: '棕色',
Sepia2: '棕色2',
Invert: '负片',
Pixelate: '像素化',
Threshold: '阈值',
Tint: '色调',
Multiply: '正片叠底',
Blend: '混合色',
Width: '宽度',
Height: '高度',
'Lock Aspect Ratio': '锁定宽高比例'
}

const customTheme = {
"common.bi.image": "", // 左上角logo图片
"common.bisize.width": "0px",
"common.bisize.height": "0px",
"common.backgroundImage": "none",
"common.backgroundColor": "#f3f4f6",
"common.border": "1px solid #333",

// header
"header.backgroundImage": "none",
"header.backgroundColor": "#f3f4f6",
"header.border": "0px",

// load button
"loadButton.backgroundColor": "#fff",
"loadButton.border": "1px solid #ddd",
"loadButton.color": "#222",
"loadButton.fontFamily": "NotoSans, sans-serif",
"loadButton.fontSize": "12px",
"loadButton.display": "none", // 隐藏

// download button
"downloadButton.backgroundColor": "#fdba3b",
"downloadButton.border": "1px solid #fdba3b",
"downloadButton.color": "#fff",
"downloadButton.fontFamily": "NotoSans, sans-serif",
"downloadButton.fontSize": "12px",
"downloadButton.display": "none", // 隐藏

// icons default
"menu.normalIcon.color": "#8a8a8a",
"menu.activeIcon.color": "#555555",
"menu.disabledIcon.color": "#ccc",
"menu.hoverIcon.color": "#e9e9e9",
"submenu.normalIcon.color": "#8a8a8a",
"submenu.activeIcon.color": "#e9e9e9",

"menu.iconSize.width": "24px",
"menu.iconSize.height": "24px",
"submenu.iconSize.width": "32px",
"submenu.iconSize.height": "32px",

// submenu primary color
"submenu.backgroundColor": "#1e1e1e",
"submenu.partition.color": "#858585",

// submenu labels
"submenu.normalLabel.color": "#858585",
"submenu.normalLabel.fontWeight": "lighter",
"submenu.activeLabel.color": "#fff",
"submenu.activeLabel.fontWeight": "lighter",

// checkbox style
"checkbox.border": "1px solid #ccc",
"checkbox.backgroundColor": "#fff",

// rango style
"range.pointer.color": "#fff",
"range.bar.color": "#666",
"range.subbar.color": "#d1d1d1",

"range.disabledPointer.color": "#414141",
"range.disabledBar.color": "#282828",
"range.disabledSubbar.color": "#414141",

"range.value.color": "#fff",
"range.value.fontWeight": "lighter",
"range.value.fontSize": "11px",
"range.value.border": "1px solid #353535",
"range.value.backgroundColor": "#151515",
"range.title.color": "#fff",
"range.title.fontWeight": "lighter",

// colorpicker style
"colorpicker.button.border": "1px solid #1e1e1e",
"colorpicker.title.color": "#fff",
};
export default {
data() {
return {
instance: null
}
},
mounted() {
this.init()
},
methods: {
init() {
this.instance = new ImageEditor(document.querySelector('#tui-image-editor'), {
includeUI: {
loadImage: {
path: 'https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c1d7a1feb60346449c1a64893888989a~tplv-k3u1fbpfcp-watermark.image',
name: 'image'
},
menu: ['resize', 'crop', 'rotate', 'draw', 'shape', 'icon', 'text', 'filter'], // 底部菜单按钮列表 隐藏镜像flip和遮罩mask
initMenu: 'draw', // 默认打开的菜单项
menuBarPosition: 'bottom', // 菜单所在的位置
locale: locale_zh, // 本地化语言为中文
theme: customTheme // 自定义样式
},
cssMaxWidth: 1000, // canvas 最大宽度
cssMaxHeight: 600 // canvas 最大高度
})
document.getElementsByClassName('tui-image-editor-main')[0].style.top = '45px' // 调整图片显示位置
document.getElementsByClassName(
'tie-btn-reset tui-image-editor-item help'
)[0].style.display = 'none' // 隐藏顶部重置按钮
},
// 保存图片,并上传
save() {
const base64String = this.instance.toDataURL() // base64 文件
const data = window.atob(base64String.split(',')[1])
const ia = new Uint8Array(data.length)
for (let i = 0; i < data.length; i++) {
ia[i] = data.charCodeAt(i)
}
const blob = new Blob([ia], { type: 'image/png' }) // blob 文件
const form = new FormData()
form.append('image', blob)
// upload file
}
}
}
</script>


<style lang="scss" scoped>
.drawing-container {
height: 900px;
position: relative;
.save {
position: absolute;
right: 50px;
top: 15px;
}
}
</style>


总结


以上就是 tui.image-editor 的基本使用方法,相比其他插件,tui.image-editor 的优势是功能强大,简单易上手。


插件固然好用,但本人也发现一个小 bug,当放大图片,用手掌拖动显示位置,再点击重置按钮时,图片很可能就消失不见了。解决办法有两个,一是改源码,在重置之前,先调用 resetZoom 方法,还原缩放比列;二是自己做一个重置按钮,点击之后调用 this.init 方法重新进行渲染。



收起阅读 »

超详细讲解页面加载过程

说一说从输入URL到页面呈现发生了什么?(知识点) ❝ 这个题可以说是面试最常见也是一道可以无限难的题了,一般面试官出这道题就是为了考察你的前端知识的深度与广度。 ❞ 1.浏览器接受URL开启网络请求线程(涉及到:浏览器机制,线程与进程等) 2.开启网络线...
继续阅读 »

说一说从输入URL到页面呈现发生了什么?(知识点)




这个题可以说是面试最常见也是一道可以无限难的题了,一般面试官出这道题就是为了考察你的前端知识的深度与广度。




1.浏览器接受URL开启网络请求线程(涉及到:浏览器机制,线程与进程等)


2.开启网络线程到发出一个完整的http请求(涉及到:DNS解析,TCP/IP请求,5层网络协议等)


3.从服务器接收到请求到对应后台接受到请求(涉及到:负载均衡,安全拦截,后台内部处理等)


4.后台与前台的http交互(涉及到:http头,响应码,报文结构,cookie等)


5.缓存问题(涉及到:http强缓存与协商缓存等)(请看上一篇文章[这些浏览器面试题,看看你能回答几个?](juejin.cn/post/702653…


6.浏览器接受到http数据包后的解析流程(涉及到html词法分析,解析成DOM树,解析CSS生成CSSOM树,合并生成render渲染树。然后layout布局,painting渲染,复合图层合成,GPU绘制,等)


在浏览器地址栏输入URL


当我们在浏览器地址栏输入URL地址后,浏览器会开一个线程来对我们输入的URL进行解析处理。


浏览器中的各个进程及作用:(多进程)



  • 浏览器进程:负责管理标签页的创建销毁以及页面的显示,资源下载等。

  • 第三方插件进程:负责管理第三方插件。

  • GPU进程:负责3D绘制与硬件加速(最多一个)。

  • 渲染进程:负责页面文档解析(HTML,CSS,JS),执行与渲染。(可以有多个)


DNS域名解析


为什么需要DNS域名解析?


因为我们在浏览器中输入的URL通常是一个域名,并不会直接去输入IP地址(纯粹因为域名比IP好记),但我们的计算机并不认识域名,它只知道IP,所以就需要这一步操作将域名解析成IP。


URL组成部分



  • protocol:协议头,比如http,https,ftp等;

  • host:主机域名或者IP地址;

  • port:端口号;

  • path:目录路径;

  • query:查询的参数;

  • hash:#后边的hash值,用来定位某一个位置。


解析过程



  • 首先会查看浏览器DNS缓存,有的话直接使用浏览器缓存

  • 没有的话就查询计算机本地DNS缓存(localhost)

  • 还没有就询问递归式DNS服务器(就是网络提供商,一般这个服务器都会有自己的缓存)

  • 如果依然没有缓存,那就需要通过 根域名服务器 和TLD域名服务器 再到对应的 权威DNS服务器 找记录,并缓存到 递归式服务器,然后 递归服务器 再将记录返回给本地


「⚠️注意:」




DNS解析是非常耗时的,如果页面中需要解析的域名过多,是非常影响页面性能的。考虑使用dns与加载或减少DNS解析进行优化。




发送HTTP请求


拿到了IP地址后,就可以发起HTTP请求了。HTTP请求的本质就是TCP/IP的请求构建。建立连接时需要**「3次握手」进行验证,断开链接也同样需要「4次挥手」**进行验证,保证传输的可靠性


3次握手



  • 第一次握手:客户端发送位码为 SYN = 1(SYN 标志位置位),随机产生初始序列号 Seq = J 的数据包到服务器。服务器由 SYN = 1(置位)知道,客户端要求建立联机。

  • 第二次握手:服务器收到请求后要确认联机信息,向客户端发送确认号Ack = (客户端的Seq +1,J+1),SYN = 1,ACK = 1(SYN,ACK 标志位置位),随机产生的序列号 Seq = K 的数据包。

  • 第三次握手:客户端收到后检查 Ack 是否正确,即第一次发送的 Seq +1(J+1),以及位码ACK是否为1。若正确,客户端会再发送 Ack = (服务器端的Seq+1,K+1),ACK = 1,以及序号Seq为服务器确认号J 的确认包。服务器收到后确认之前发送的 Seq(K+1) 值与 ACK= 1 (ACK置位)则连接建立成功。


3次握手.gif


「直白理解:」


(客户端:hello,你是server么?服务端:hello,我是server,你是client么 客户端:yes,我是client 建立成功之后,接下来就是正式传输数据。)


4次挥手



  • 客户端发送一个FIN Seq = M(FIN置位,序号为M)包,用来关闭客户端到服务器端的数据传送。

  • 服务器端收到这个FIN,它发回一个ACK,确认序号Ack 为收到的序号M+1。

  • 服务器端关闭与客户端的连接,发送一个FIN Seq = N 给客户端。

  • 客户端发回ACK 报文确认,确认序号Ack 为收到的序号N+1。


4次挥手.gif


「直白理解:」


(主动方:我已经关闭了向你那边的主动通道了,只能被动接收了 被动方:收到通道关闭的信息 被动方:那我也告诉你,我这边向你的主动通道也关闭了 主动方:最后收到数据,之后双方无法通信)


五层网络协议


1、应用层(DNS,HTTP):DNS解析成IP并发送http请求;


2、传输层(TCP,UDP):建立TCP连接(3次握手);


3、网络层(IP,ARP):IP寻址;


4、数据链路层(PPP):封装成帧;


5、物理层(利用物理介质传输比特流):物理传输(通过双绞线,电磁波等各种介质)。


「OSI七层框架:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层」


服务器接收请求做出响应


HTTP 请求到达服务器,服务器进行对应的处理。 最后要把数据传给浏览器,也就是返回网络响应。


跟请求部分类似,网络响应具有三个部分:响应行、响应头和响应体。


响应完成之后怎么办?TCP 连接就断开了吗?


不一定。这时候要判断Connection字段, 如果请求头或响应头中包含Connection: Keep-Alive, 表示建立了持久连接,这样TCP连接会一直保持,之后请求统一站点的资源会复用这个连接。否则断开TCP连接, 请求-响应流程结束。


状态码


状态码是由3位数组成,第一个数字定义了响应的类别,且有五种可能取值:



  • 1xx:指示信息–表示请求已接收,继续处理。

  • 2xx:成功–表示请求已被成功接收、理解、接受。

  • 3xx:重定向–要完成请求必须进行更进一步的操作。

  • 4xx:客户端错误–请求有语法错误或请求无法实现。

  • 5xx:服务器端错误–服务器未能实现合法的请求。 平时遇到比较常见的状态码有:200, 204, 301, 302, 304, 400, 401, 403, 404, 422, 500(分别表示什么请自行查找)。


服务器返回相应文件


请求成功后,服务器会返回相应的网页,浏览器接收到响应成功的报文后便开始下载网页,至此,网络通信结束。


浏览器解析渲染页面


浏览器在接收到HTML,CSS,JS文件之后,它是如何将页面渲染在屏幕上的?


render.png


解析HTML构建DOM Tree


浏览器在拿到服务器返回的网页之后,首先会根据顶部定义的DTD类型进行对应的解析,解析过程将被交给内部的GUI渲染线程来处理。


「DTD(Document Type Definition)文档类型定义」


常见的文档类型定义


//HTML5文档定义
<!DOCTYPE html>
//用于XHTML 4.0 的严格型 
<!DOCTYPE HTMLPUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> 
//用于XHTML 4.0 的过渡型 
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> 
//用于XHTML 1.0 的严格型 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 
//用于XHTML 1.0 的过渡型
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

HTML解释器的工作就是将网络或者本地磁盘获取的HTML网页或资源从字节流解释成DOM树🌲结构


HTML解释器.png


通过上图可以清楚的了解这一过程:首先是字节流,经过解码之后是字符流,然后通过词法分析器会被解释成词语(Tokens),之后经过语法分析器构建成节点,最后这些节点被组建成一颗 DOM 树。


对于线程化的解释器,字符流后的整个解释、布局和渲染过程基本会交给一个单独的渲染线程来管理(不是绝对的)。由于 DOM 树只能在渲染线程上创建和访问,所以构建 DOM 树的过程只能在渲染线程中进行。但是,从字符串到词语这个阶段可以交给单独的线程来做,Chrome 浏览器使用的就是这个思想。在解释成词语之后,Webkit 会分批次将结果词语传递回渲染线程。


这个过程中,如果遇到的节点是 JS 代码,就会调用 JS引擎 对 JS代码进行解释执行,此时由于 JS引擎GUI渲染线程 的互斥,GUI渲染线程 就会被挂起,渲染过程停止,如果 JS 代码的运行中对DOM树进行了修改,那么DOM的构建需要从新开始


如果节点需要依赖其他资源,图片/CSS等等,就会调用网络模块的资源加载器来加载它们,它们是异步的,不会阻塞当前DOM树的构建


如果遇到的是 JS 资源URL(没有标记异步),则需要停止当前DOM的构建,直到 JS 的资源加载并被 JS引擎 执行后才继续构建DOM


解析CSS构建CSSOM Tree


CSS解释器会将CSS文件解释成内部表示结构,生成CSS规则树,这个过程也是和DOM解析类似的,CSS 字节转换成字符,接着词法解析与法解析,最后构成 CSS对象模型(CSSOM) 的树结构


构建渲染树(Render Tree)


DOM TreeCSSOM Tree都构建完毕后,接着将它们合并成渲染树(Render Tree)渲染树 只包含渲染网页所需的节点,然后用于计算每个可见元素的布局,并输出给绘制流程,将像素渲染到屏幕上。


渲染(布局,绘制,合成)



  • 计算CSS样式 ;

  • 构建渲染树 ;

  • 布局,主要定位坐标和大小,是否换行,各种position overflow z-index属性 ;

  • 绘制,将图像绘制出来。


这个过程比较复杂,涉及到两个概念: reflow(回流)和repain(重绘)。DOM节点中的各个元素都是以盒模型的形式存在,这些都需要浏览器去计算其位置和大小等,这个过程称为relow;当盒模型的位置,大小以及其他属性,如颜色,字体,等确定下来之后,浏览器便开始绘制内容,这个过程称为repain。页面在首次加载时必然会经历reflow和repain。reflow和repain过程是非常消耗性能的,尤其是在移动设备上,它会破坏用户体验,有时会造成页面卡顿。所以我们应该尽可能少的减少reflow和repain。


这里Reflow和Repaint的概念是有区别的:


(1)Reflow:即回流。一般意味着元素的内容、结构、位置或尺寸发生了变化,需要重新计算样式和渲染树。


(2)Repaint:即重绘。意味着元素发生的改变只是影响了元素的一些外观之类的时候(例如,背景色,边框颜色,文字颜色等),此时只需要应用新样式绘制这个元素就可以了。


回流的成本开销要高于重绘,而且一个节点的回流往往回导致子节点以及同级节点的回流, 所以优化方案中一般都包括,尽量避免回流。


「回流一定导致重绘,但重绘不一定会导致回流」


「合成(composite)」


最后一步合成( composite ),这一步骤浏览器会将各层信息发送给GPU,GPU将各层合成,显示在屏幕上


普通图层和复合图层


可以简单的这样理解,浏览器渲染的图层一般包含两大类:普通图层以及复合图层


首先,普通文档流内可以理解为一个复合图层(这里称为默认复合层,里面不管添加多少元素,其实都是在同一个复合图层中)


其次,absolute布局(fixed也一样),虽然可以脱离普通文档流,但它仍然属于默认复合层


然后,可以通过硬件加速的方式,声明一个新的复合图层,它会单独分配资源 (当然也会脱离普通文档流,这样一来,不管这个复合图层中怎么变化,也不会影响默认复合层里的回流重绘)


可以简单理解下:「GPU中,各个复合图层是单独绘制的,所以互不影响」,这也是为什么某些场景硬件加速效果一级棒


可以Chrome源码调试 -> More Tools -> Rendering -> Layer borders中看到,黄色的就是复合图层信息。



收起阅读 »

VS Code settings.json 10 个高(装)阶(杯)配置!

1. 隐藏活动栏 VS Code 左侧图标列表是“活动栏”,我们可以点击图标跳转到各个模块,我们可以通过配置 workbench.activityBar.visible 来控制活动栏的显示; 如果你想恢复显示,可以自定义快捷键来再次显示这块空间; 如何设置...
继续阅读 »

1. 隐藏活动栏


VS Code 左侧图标列表是“活动栏”,我们可以点击图标跳转到各个模块,我们可以通过配置 workbench.activityBar.visible 来控制活动栏的显示;


image.png


如果你想恢复显示,可以自定义快捷键来再次显示这块空间;


image.png


如何设置快捷键:keybindings


我们可以用 Ctrl+B 来隐藏/显示文件资源管理器,用 Ctrl+Alt+B 来隐藏/显示活动栏;


虽然,你也可以在命令面板 Ctrl+Shift+P 中搜索,不过使用快捷键就更有装杯效果~


活动栏在隐藏状态下,我们也可以通过快捷键跳转到不同的工作空间,比如 Ctrl+Shift+E(跳转到文件资源管理器)、Ctrl+Shift+X(跳转到扩展)、Ctrl+Shift+H(搜索和替换)等


2. AI 编码


GitHub Copilot 是 VS Code 的一个扩展,可在你编写代码时生成片段代码;


由于它是人工智能、机器学习,有可能会产生一些你不喜欢的代码,但是请别仇视它,毕竟 AI 编码是未来趋势!


image.png


处于隐私考虑,建议不要在工作中使用 Copilot,但是可以在个人项目中使用它,有趣又有用,尤其是对于单元测试;


可以在 settings.json 中配置 Copilot;


3. 字体与缩放


这个不多做解释,根据自己的需求进行文字大小及缩放比例的配置;


image.png


当然,你不一定要在 settings.json 中去编写这个配置,也可以在可选项及输入配置窗口进行配置。


4. 无拖拽/删除确认


如果你对自己的编程技能足够自信,或者对 VS Code 的 Ctrl+Z 足够自信,你可以配置取消删除确认;因为拖拽/删除确认有时也会干扰思路~


image.png


image.png


5. 自更新绝对路径


VS Code 的最佳功能之一是它的文件导入很友善,使用绝对路径,例如:@/components/Button../../Button 更让人舒适;


当移动文件重新组织目录时,希望 VS Code 能自动更新文件的路径?你可以配置它们:


image.png


请注意,您需要在 .tsconfig/.jsconfig 文件中配置路径才能使用绝对路径导入。


6. 保存执行


配置过 ESLint 保存修正的应该都知道这个配置。这个非常强大,出了 fixAll,还能 addMissingImports 补充缺少的 Imports,或者其它你想在保存后执行的行为;


image.png


这个配置就像是编程魔法~


7. CSS 格式化


你可能已经在使用 Stylelint 了,如果没有,请在配置中设置它!


image.png


另一个设置是 editor.suggest.insertMode,当设置为“replace”时,意味着——当你选择一个提示并按 Tab 或 Enter 时,将替换整个文本为提示,这非常有用。


8. 开启 Emmet


你可能熟悉 Emmet —— Web 开发人员必备工具包,如果没有,请设置它;虽然它内置于 VS Code,但必须手动配置启用;


image.png


9. Tailwind CSS


Tailwind CSS 是一个功能类优先的 CSS 框架,它集成了诸如 flexpt-4text-center 和 rotate-90 这样的的类,它们能直接在脚本标记语言中组合起来,构建出任何设计。


虽然它目前尚未内置在 VS Code 中,但可作为免费的 VS Code 扩展进行安装使用,还可以配置附加设置增强它的功能!


image.png


10. 单击打开文件


VS Code 默认用户界面,有个奇怪的现象,它需要双击才能从文件资源管理器中打开文件。


单击一下得到的是奇怪的“预览”模式,当你单击下一个文件时,第一个文件就会消失。这就像只有一个标签。


image.png


需要进行这个配置,关闭后,单击将在新选项卡中打开文件。问题解决了~


将配置用 Settings Sync 进行同步,去哪都能个性化、自定义!酷的!


image.png




以上就是本篇分享,你有啥压箱底的 VS Code-settings.json 配置吗?欢迎评论留言,分享交流 (#^.^#)



收起阅读 »

(转载) 爆火的「元宇宙」概念究竟可以为企业带来什么翻天覆地的变化?

“元宇宙”这个概念,最近在全球投资市场突然爆红,近期频繁上热搜的互联网巨头Facebook都改名为Meta(Meta为元宇宙“MetaVerse”的前缀)。承诺让用户在相互连接的虚拟世界中生活、工作和娱乐。那“元宇宙”到底是个什么东西呢?投资市场对它的解释是“...
继续阅读 »


“元宇宙”这个概念,最近在全球投资市场突然爆红,近期频繁上热搜的互联网巨头Facebook都改名为Meta(Meta为元宇宙“MetaVerse”的前缀)。承诺让用户在相互连接的虚拟世界中生活、工作和娱乐。

那“元宇宙”到底是个什么东西呢?投资市场对它的解释是“包涵万物,无所不联”、“所见非所见,所想即所得”,听着就很玄乎是吗?

其实可以把十年前火过的“虚拟增强现实技术”的超级强化版,戴VR眼镜玩游戏,看做是元宇宙的石器时代。

元宇宙(Metaverse)这个词,也不是投资市场凭空捏造的,它源于1992年的科幻小说《雪崩》。小说里有一个虚拟现实世界,人们在里面利用自己的数字化身社交、竞争。这一概念所对应的场景在电影作品中曾多次出现,比如《黑客帝国》《头号玩家》都曾描绘元宇宙场景:人可以在一个完全数字化的虚拟世界中以化身形式生活,获得超越现实的沉浸式体验。

另一个更直观的例子,就是2018年的科幻电影《头号玩家》,电影里的虚拟世界“绿洲”(Oasis)被认为是元宇宙的一个标准。

现在,许多科技公司宣称,当前的互联网已经走到了瓶颈,互联网的下一个阶段就是“元宇宙”,他们要把以前只存在于艺术作品中的“数字平行世界”,搬到现实中来。我们认为,在这样一个时代来临的时候应该准备好,让用户在元宇宙的空间下去实现真正的、超现实的社交体验。

在2021年10月24日程序员节上,蒲公英企服平台发布了Tracup企业元宇宙。这是源自于2016年发布的项目协同管理平台Tracup,通过项目管理协作、实时音视频互动和AR增强现实技术,对数字化企业和组织生产里进行了空间延伸。其核心使命是把传统的项目协同管理的效率大幅度的提升,面向开发者和数字化组织知识工作者的项目协同管理,链接任务、沟通、文件、代码、适配需求、任务、缺陷和迭代管理,针对人员、工作流和OKR进行评估管理,提供敏捷、瀑布、通用任务协同等多种项目模版,实时视频连线、元宇宙场景沉浸沟通,同时满足当下后疫情时代中的不确定,帮助DevOps开发者和知识工作者克服空间阻碍,在办公室、出差、隔离、甚至运动、出行时,可以更加高效地对项目进行管理,对任务进行追踪,同步工作进程、文件和代码,组织有效的会议,与团队、协作伙伴、供应链和客户随时沟通,并将会议和沟通结果同步到工作流当中,帮助企业实现数字世界与现实世界融为一体,在现实增强的企业元宇宙里永续工作、沟通与协作。

面对疫情带来的隔离、出行停滞、交通阻断,原有的企业和组织的工作环境、运行方式显得手足无措、难以从容应对,Tracup项目协同管理通讯平台和元宇宙沉浸场景的Tracup AR眼镜,将对后疫情时代的企业的工作形式和生产力提升带来巨大的影响。


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

收起阅读 »

(转载)元宇宙是什么,离企业应用还有多远?

Tracup使用空间智能与虚拟现实技术,将协同办公等场景构建在所有网络可以延伸到的区域之上。今年以来,元宇宙成为企业与消费者关心的热门话题。脸书、微软、腾讯、阿里、英伟达等互联网与硬件科技公司躬身入局。据虚拟现实研究领域专家介绍,元宇宙作为虚拟世界与现实世界交...
继续阅读 »

Tracup使用空间智能与虚拟现实技术,将协同办公等场景构建在所有网络可以延伸到的区域之上。

今年以来,元宇宙成为企业与消费者关心的热门话题。脸书、微软、腾讯、阿里、英伟达等互联网与硬件科技公司躬身入局。据虚拟现实研究领域专家介绍,元宇宙作为虚拟世界与现实世界交融的所在,蕴含着工作、社交、内容、游戏等场景革新的重大机遇。

1.png

国内头部厂商已率先完成元宇宙布局

早在2020年底,马化腾就在腾讯内部刊物发文表示“一个令人兴奋的机会正在到来,移动互联网十年发展,即将迎来下一波升级,我们称之为全真互联网。”随后,腾讯迅速出手布局元宇宙赛道,投资了如罗布乐思(Roblox)在内的多家元宇宙概念股。

同样在今年四月,内容平台巨头字节跳动也投资了元宇宙概念公司“代码乾坤”,八月又收购了虚拟现实硬件设备厂商Pico,近日又将西瓜、火山视频等产品并入抖音系,其元宇宙阵列初见端倪。

除此以外,网易、莉莉丝等以游戏制作出名的公司也纷纷进行了元宇宙相关的布局。

抢在C端之前,B端元宇宙已实现落地方案

作为深受研发与项目管理者喜爱的项目协同工具——Tracup,早在2019年就意识到一个革命性的科技窗口即将打开,开始调整产品结构并布局硬件设施,经过两年的努力,面向B端的企业元宇宙的“数字基石”已然打造完成,实现了将协同办公、远距离办公、全场景应用的工程级AR智能眼镜与办公场景的适配,并以此为基础构建了各类虚拟应用。

2.png

举个例子:当你出门在外游玩时,可以不再携带相机乃至手机拍照,工程级AR智能眼镜即可帮你实现高清摄像。面对紧急的工作需求,可以通过手势操作迅速处理,轻松实现发文件、语音、视频、手势输入等等操作。通过项目管理协作、实时音视频互动和AR增强现实技术,对数字化企业和组织生产力进行了空间延伸,帮助DevOps开发者和知识工作者克服空间阻碍,在办公室、出差、隔离、甚至运动、出行时,可以更加高效地对项目进行管理。

设计方面,Tracup企业元宇宙AR智能眼镜采用了工业合金架构设计,拥有68克超轻重量,可以折叠成普通眼镜放在口袋里。同时,采用折返式的光学方案,实现了143英寸等效1080P高清画质,拥有75hz的屏幕刷新率,支持2D/3D无缝切换,具备100000:1的屏幕对比度,即使在夜晚也感受不到 AR 内容的底光。此外实现了快速精准匹配近视用户人群的近视度数。

那么,到底什么是元宇宙?

元宇宙概念最早出现在1992年,美国作家尼尔在其科幻小说《雪崩》中描绘了一个平行于现实世界的虚拟数字世界——“元界”,现实世界中的人在“元界”中都有一个虚拟分身,人们通过控制这个虚拟分身来相互竞争以提高地位。

事实上,当前科技界对元宇宙并无权威定义。

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

扎克伯格在解释公司为何改名时,用一段视频直观展示了元宇宙的未来:可以创造一个虚拟的“家”,邀请熟悉的人开展社交,戴上设备就可以进入一个虚拟的工作空间与同事一起工作,甚至可以创造一个虚拟世界……

3.png

不少国际知名咨询企业看好元宇宙的未来,如彭博行业研究报告预计元宇宙将在2024年达到8000亿美元市场规模,普华永道预计元宇宙市场规模在2030年将达到1.5万亿美元。

业内人士建议,在法律层面,总结提炼在网络平台发展过程中的治理经验,加强元宇宙前瞻性立法研究,关注监管审查、数据安全等问题。作为B端元宇宙的探索者和领导者,Tracup项目协同管理通讯平台和元宇宙沉浸场景的Tracup AR眼镜系列服务将确保依法合规,持续为后疫情时代的企业的工作形式和生产力带来新的提升与更为全面的解决方案。

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

收起阅读 »

(转载)最近大火的「元宇宙」究竟是什么

如果要问当下最火的概念是什么,那必然是【元宇宙】。元宇宙到底有多火,对互联网行业有多重要?从 Facebook 创始人兼首席执行官马克·扎克伯格近日的一段采访中可窥知一二。在 The Verge 的专访里,这家世界最大的社交平台掌舵者表示:希望在未来用 5 年...
继续阅读 »

如果要问当下最火的概念是什么,那必然是【元宇宙】。

元宇宙到底有多火,对互联网行业有多重要?从 Facebook 创始人兼首席执行官马克·扎克伯格近日的一段采访中可窥知一二。在 The Verge 的专访里,这家世界最大的社交平台掌舵者表示:希望在未来用 5 年左右的时间,将 Facebook 打造为一家元宇宙公司,并且,为了迎接元宇宙时代的到来,Facebook也在近日将公司的名字改为Meta。元宇宙概念的火爆还体现在,今年的 ChinaJoy 上有关元宇宙的发言屡见报端、连芯片巨头英伟达也忍不住“蹭热点”,等等。

那么问题来了,元宇宙到底是什么?它对我们现在的行业又会产生哪些影响?

一、什么是元宇宙

元宇宙的英语是 Metaverse,Meta 表示“超越”、“元”, verse 表示“宇宙 universe”。元宇宙这个概念最早出现在 1992 年尼尔·斯蒂芬森的科幻小说《雪崩》当中,小说描绘了一个平行于现实世界的虚拟数字世界,在这个虚拟的数字世界,人们用数字化身来控制并相互竞争以提高自己的地位。

在这里插入图片描述 虽然大部分人看元宇宙的文章知道了出处,但是真正是看过原著的人,我估计应该没几个人,对于书中想表达的内涵估计也没几个懂。

相比书记的枯燥无味,2018 年斯皮尔伯格导演的科幻电影《头号玩家》被认为是目前最符合《雪崩》中描述的“元宇宙”形态,在电影中,男主角带上 VR 头盔后,瞬间就能进入自己设计的另一个极其逼真的虚拟游戏世界——“绿洲”(Oasis)。 在这里插入图片描述 在电影《头号玩家》的场景中,人们可以随时随地切换身份,自由穿梭于物理世界和数字世界,在虚拟空间和时间节点所构成的“元宇宙”中学习、工作、交友、购物、旅游等。元宇宙,这个建立在区块链之上的虚拟世界,去中心化平台让玩家享有所有权和自治权。

维基百科对元宇宙的描述是:通过虚拟增强的物理现实,呈现收敛性和物理持久性特征的,基于未来互联网,具有链接感知和共享特征的 3D 虚拟空间。

不过,相比维基百科的定义,我认为朱嘉明教授对元宇宙的描述则更加具体:【元宇宙】是吸纳了信息革命(5G/6G)、互联网革命(web3.0)、人工智能革命,以及 VR、AR、MR,特别是游戏引擎在内的虚拟现实技术革命的成果,向人类展现出构建与传统物理世界平行的全息数字世界的可能性;引发了信息科学、量子科学,数学和生命科学的互动,改变科学范式;推动了传统的哲学、社会学甚至人文科学体系的突破;囊括了所有的数字技术,包括区块链技术成就;丰富了数字经济转型模式,融合 DeFi、IPFS、NFT 等数字金融成果。

二、元宇宙的属性

如果说元宇宙目前还存在于小说和电影中,那么在今年 3 月被称作元宇宙第一股的 Roblox 成功在纽交所上市,则似乎意味着这个虚拟世界正在走向现实。正如大家理解的那样,元宇宙的根基就是建立在游戏这个体系上的,所以不出意外,元宇宙最先落地也是跟游戏相关。

既然元宇宙在游戏里有体现,不妨让我们看看在游戏中元宇宙的八个关键特征,即Identity (身份)、Friends(朋友)、Immersive(沉浸感)、Low Friction(低延迟)、Variety(多样性)、Anywhere(随地)、Economy(经济)、Civility(文明)。

在这里插入图片描述

属性解释如下:

  • 身份:拥有一个虚拟身份,无论与现实身份有没有相关性。
  • 朋友:在元宇宙当中拥有朋友,可以社交,无论在现实中是否认识。
  • 沉浸感:能够沉浸在元宇宙的体验当中,忽略其他的一切。
  • 低延迟:元宇宙中的一切都是同步发生的,没有异步性或延迟性。
  • 多元化:元宇宙提供多种丰富内容,包括玩法、道具、美术素材等。
  • 随地:可以使用任何设备登录元宇宙,随时随地沉浸其中。
  • 经济系统:与任何复杂的大型游戏一样,元宇宙应该有自己的经济系统。
  • 文明:元宇宙应该是一种虚拟的文明。

三、元宇宙的价值链

元宇宙概念为何能在今年爆发,除了资本的追逐外,还因为元宇宙有一套属于自己的价值链,并吸引互联网公司们进入这个赛道,以自己的方式和理解去塑造、定义元宇宙。

虽然,对元宇宙概念目前还很模糊,但不妨碍元宇宙成为一个好的故事。Roblox3 月份上市后,其市值达到 400 亿美元,相比 1 年前 40 亿美元的估值暴涨了 10 倍。App Annie 发布的全球热门游戏收入排名显示,7 月 Roblox 继续蝉联冠军宝座。在国内,号称要打造全年龄段元宇宙世界的 MeteApp 公司,在 Roblox 上市后拿到了 SIG 海纳亚洲资本领投的 1 亿美元 C 轮融资。字节跳动对游戏引擎研发商、“中国版 Roblox”代码乾坤进行了近 1 亿人民币战略投资。

下面我们来看看元宇宙价值链,元宇宙的价值链主要是寻求能够实现的科技体验。因此,从这一价值链出发,元宇宙的价值链包括七个层面:体验(Experience);发现(Discovery);创作者经济(Creator economy);空间计算(Spatial Computing);去中心化(Decentralizition);人机交互(Human Interface);基础设施(Infrastructure)。 在这里插入图片描述

  • 内容和体验:在互联网中,数字化内容占据主要位置。在元宇宙中,数字化行为的比例会大幅度提高,从而使得用户可以尝试各种实时的体验。目前,这一层公司中的业务更多的停留在游戏层面,这是因为当前的硬件还处于比较早期阶段,如VR设备还并没有大规模普及。当硬件设备逐渐成熟和普及后,大量的公司会将现在的各种线下行为数字化,提供丰富的体验,这将包括但不局限于:旅游、教育、体育竞技、演唱会等等。
  • 发现与链接:在元宇宙中,内容和体验的数量是前所未有的,并且会以指数级的方式增长。这就意味着,对于每个人而言,都存在一个问题:如何发现那些有价值的,感兴趣的内容和体验。有能力去解决这个问题的公司,往往会成为内容和体验的包装/分发平台。
  • 硬件:相比于互联网和移动互联网,元宇宙很重要的一个特征是沉浸式体验,这意味着我们需要新一代信息交互设备。而目前最能达到这一条件的设备就是AR/VR智能眼镜,以及后面的计算平台。值得一提的是Facebook旗下的Oculus Quest2,或许能够成为元宇宙发展史上的一个重要产品。

在这里插入图片描述

  • 操作系统:除了AR/VR本身的硬件之外,依然需要一套完整的XR操作系统。目前,大部分VR硬件设备的系统是基于安卓系统,但这很可能并不是最终的答案。XR操作系统将会和手机操作系统有很大的区别,这将体现在交互方式、应用模式、内容分发、硬件形态和体验感等多个方面。
  • 基础设施:单一技术革命,并不能带来时代变革。同样,下一代互联网也需要充足的基础设施,比如我们耳熟能详的VR/AR、5G、AI 、去中心化等技术。

综上,元宇宙的核心价值在于,它将成为一个拥有极致沉浸体验、丰富内容生态、超时空的社交体系、虚实交互的经济系统,能映射现实人类社会文明的超大型数字社区。

四、元宇宙对行业的影响

从元宇宙的价值以及目前的技术情况来看,元宇宙对目前行业的影响体现在泛娱乐行业,特别是游戏行业有望成为元宇宙概念下最早落地的场景。

目前,市场上已经出现一系列基于游戏内核的沉浸式场景体验。比如,美国著名歌手 Travis Scott 在游戏《堡垒之夜》中举办虚拟演唱会,全球 1230 万游戏玩家成为虚拟演唱会观众;加州大学伯克利分校在《Minecraft》重现校园,毕业生以虚拟形象线上场景参加毕业典礼;顶级 AI 学术会议 ACAI 在任天堂《动物森友会》上举行 20 年研讨会,演讲者在游戏中播放 PPT 发表讲话等都是落地的场景。

而在线游戏创作社区 Roblox 因为现象级的内容创作生态带来的游戏自由度和用户活跃度,成为现阶段公认的元宇宙雏形。随着市场对元宇宙认识的加深,游戏之于元宇宙更大的意义在于提供展现方式,是元宇宙搭建虚拟世界的底层逻辑。与此同时,元宇宙概念的火热也吸引了无数游戏厂商的入局。

而另一个容易落地的场景便是社交领域。Facebook(现已改为Meta) CEO 扎克伯格日前表示,Facebook 已经组建了专门研发元宇宙的团队,并表示未来五年要从社交公司变成元宇宙公司。事实上 ,虚拟社交也在改变人们的社交方式,成为未来社交发展的新方向。

而在消费领域,随着元宇宙的到来,用户的消费体验或将迎来新的一波交互体验的升级。目前,新氧已经实现为用户提供 AR 检测脸型的服务,通过手机扫描脸部推算出适合每位用户的妆容发型护肤品等。得物 App 的 AR 虚拟试鞋功能允许用户只需要挑选自己喜欢的鞋型和颜色并 AR 试穿,看到鞋子上脚的效果。在 AR、VR、可穿戴设备、触觉传感等技术的带动下,更加沉浸式的消费或将成为常态。它不局限于购买衣服、鞋子等基本消费,AR 房屋装修、远程看房、甚至模拟旅游景点都将成为可能。

可以看到,元宇宙离不开的便是 AR、VR可穿戴虚拟设备,关于 AR、VR可穿戴虚拟设备的现状,大家可以参考聊聊这个本不存在的 “元宇宙”




收起阅读 »

(转载)初探元宇宙—元宇宙的八大要素

初探元宇宙—元宇宙的八大要素元宇宙的出现也是离不开人工智能技术成熟、以后在元宇宙我们就能以虚拟身份交到 AI 的朋友。在我们开始聊如何将人工智能技术应用到元宇宙中前,我们先了解了解什么是元宇宙。宇宙是我们未来生活的世界,什么是元宇宙。元宇宙是随着信息化的发展,...
继续阅读 »

初探元宇宙—元宇宙的八大要素

元宇宙的出现也是离不开人工智能技术成熟、以后在元宇宙我们就能以虚拟身份交到 AI 的朋友。在我们开始聊如何将人工智能技术应用到元宇宙中前,我们先了解了解什么是元宇宙。

宇宙是我们未来生活的世界,什么是元宇宙。元宇宙是随着信息化的发展,形成一个虚拟世界,这里虚拟世界和我们通过游戏或者电影所了解到虚拟世界有所不同。因为这个虚拟世界内部也会进一步发展,从而反过来影响我们的世界,最终形成一个虚拟和现实交叉共生的新的世界的形态。听起来还是感觉像玩游戏。

roblox_003.jpeg

那么到底什么样虚拟世界可以称为元宇宙,在元宇宙探索的前辈 Roblox 公司给出关于元宇宙的定义。一个元宇宙产品应该具备 8 要素,分别是身份、朋友、沉浸感、低延迟、多元化、随时随地、经济系统和文明。

身份(identity)

虚拟世界中每一个人都拥有自己虚拟身份,与现实身份无关。

朋友(friends)

可以跨空间进行社交,朋友可以真人也可可能是 AI 朋友

沉浸感(immersiveness)

可以沉浸在元宇宙的体验当中,VR/AR 设备提供沉浸体验

低延迟(Low Friction)

元宇宙中的一切都是同步发生的,没有异步性或延迟性,体验完美。通过云平台降低各地服务器之间的延迟。

多元化(variety)

提供丰富、差异化的内容,虚拟世界提供超越现实世界的自由和多元化。

随时随地(anywhere)

随时随地可以登录元宇宙,不受时空的限制。

经济系统(economy)

与现实产业经济系统一样,元宇宙也有自己的经济系统。虽然现在游戏中经济体系过于简单,难于称之为经济系统,不过区块链技术为提供体系的基础。虚拟世界可以使用虚拟货币进行交易,虚拟货币也可以现实货币进行兑换。

文明(civility)

独特的虚拟文明,数字文明,在目前中游戏中还没有形成文明,最多也就是文化,虚拟世界会形成文明社会。

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

收起阅读 »

iOS 面试题 八股文 1.6

一、面试题 1、说说你认识的Swift是什么? Swift是苹果于2014年WWDC(苹果开发者大会)发布的新开发语言,可与Objective-C共同运行于MAC OS和iOS平台,用于搭建基于苹果平台的应用程序。 2、举例说明Swift里面有哪些...
继续阅读 »

一、面试题


1、说说你认识的Swift是什么?

Swift是苹果于2014年WWDC(苹果开发者大会)发布的新开发语言,可与Objective-C共同运行于MAC OS和iOS平台,用于搭建基于苹果平台的应用程序。


2、举例说明Swift里面有哪些是 Objective-C中没有的?

Swift引入了在Objective-C中没有的一些高级数据类型,例如tuples(元组),可以使你创建和传递一组数值。
wift还引入了可选项类型(Optionals),用于处理变量值不存在的情况。可选项的意思有两种:一是变量是存在的,
例如等于X,二是变量值根本不存在。Optionals类似于Objective-C中指向nil的指针,但是适用于所有的数据类型,而非仅仅局限于类,Optionals 相比于Objective-C中nil指针更加安全和简明,并且也是Swift诸多最强大功能的核心。


3、NSArray与NSSet的区别?

NSArray内存中存储地址连续,而NSSet不连续
NSSet效率高,内部使用hash查找;NSArray查找需要遍历
NSSet通过anyObject访问元素,NSArray通过下标访问


4、Swift比Objective-C有什么优势?

Swift全面优于Objective-C语言,性能是Objective-C的1.3倍,上手更加容易。


5、NSHashTable与NSMapTable?

NSHashTable是NSSet的通用版本,对元素弱引用,可变类型;可以在访问成员时copy
NSMapTable是NSDictionary的通用版本,对元素弱引用,可变类型;
可以在访问成员时copy
(注:NSHashTable与NSSet的区别:NSHashTable可以通过option设置元素弱引用/copyin,只有可变类 型。但是添加对象的时候NSHashTable耗费时间是NSSet的两倍。
NSMapTable与NSDictionary的区别:同上)


6、Swift 是一门安全语言吗?

Swift是一门类型安全的语言,Optionals就是代表。Swift能帮助你在类型安全的环境下工作,如果你的代码中需要使用String类型,Swift的安全机制能阻止你错误的将Int值传递过来,这使你在开发阶段就能及时发现并修正问题。


7、属性关键字assign、retain、weak、copy

assign:用于基本数据类型和结构体。如果修饰对象的话,当销毁时,属性值不会自动置nil,可能造成野指针。
weak:对象引用计数为0时,属性值也会自动置nil
retain:强引用类型,ARC下相当于strong,但block不能用retain修饰,因为等同于assign不安全。
strong:强引用类型,修饰block时相当于copy。


8、weak属性如何自动置nil的?

Runtime会对weak属性进行内存布局,构建hash表:以weak属性对象内存地址为key,weak属性值(weak自身地址)为value。当对象引用计数为0 dealloc时,会将weak属性值自动置nil。


9、Swift 是一门安全语言吗?

Swift是一门类型安全的语言,Optionals就是代表。Swift能帮助你在类型安全的环境下工作,如果你的代码中需要使用String类型,Swift的安全机制能阻止你错误的将Int值传递过来,这使你在开发阶段就能及时发现并修正问题。


10、内存泄露问题?

主要集中在循环引用问题中,如block、NSTime、perform selector引用计数问题。


11、Block的循环引用、内部修改外部变量、三种block

block强引用self,self强引用block
内部修改外部变量:block不允许修改外部变量的值,这里的外部变量指的是栈中指针的内存地址。
__block的作用是只要观察到变量被block使用,就将外部变量在栈中的内存地址放到堆中。
三种block:
NSGlobalBlack(全局)、
NSStackBlock(栈block)、
NSMallocBlock(堆block)


12、KVO底层实现原理?手动触发KVO?swift如何实现KVO?

KVO原理:当观察一个对象时,runtime会动态创建继承自该对象的类,并重写被观察对象的setter方法,重写的setter方法会负责在调用原setter方法前后通知所有观察对象值得更改,最后会把该对象的isa指针指向这个创建的子类,对象就变成子类的实例。
如何手动触发KVO:在setter方法里,手动实现NSObject两个方法:willChangeValueForKey、didChangeValueForKey
swift的kvo:继承自NSObject的类,或者直接willset/didset实现。


13、categroy为什么不能添加属性?怎么实现添加?与Extension的区别?category覆盖原类方法?多个category调用顺序

Runtime初始化时categroy的内存布局已经确定,没有ivar,所以默认不能添加属性。
使用runtime的关联对象,并重写setter和getter方法。
Extenstion编译期创建,可以添加成员变量ivar,一般用作隐藏类的信息。必须要有类的源码才可以添加,如NSString就不能创建Extension。
category方法会在runtime初始化的时候copy到原来前面,调用分类方法的时候直接返回,不再调用原类。如何保持原类也调用(https://www.jianshu.com/p/40e28c9f9da5)。
多个category的调用顺序按照:Build Phases ->Complie Source 中的编译顺序。


14、load方法和initialize方法的异同。——主要说一下执行时间,各自用途,没实现子类的方法会不会调用父类的?

load initialize 调用时机 app启动后,runtime初始化的时候 第一个方法调用前调用 调用顺序 父类->本类->分类 父类->本类(如果有分类直接调用分类,本类不会调用) 没实现子类的方法会不会调用父类的 否 是 是否沿用父类实现 否 是

见图 1

15、对 runtime 的理解。——主要是方法调用时如何查找缓存,如何找到方法,找不到方法时怎么转发,对象的内存布局

OC中向对象发送消息时,runtime会根据对象的isa指针找到对象所属的类,然后在该类的方法列表和父类的方法列表中寻找方法执行。如果在最顶层父类中没找到方法执行,就会进行消息转发:Method resoution(实现方法)、fast forwarding(转发给其他对象)、normal forwarding(完整消息转发。可以转发给多个对象)

16、runtime 中,SEL和IMP的区别?

每个类对象都有一个方法列表,方法列表存储方法名、方法实现、参数类型,SEL是方法名(编号),IMP指向方法实现的首地址


17、autoreleasepool的原理和使用场景?

若干个autoreleasepoolpage组成的双向链表的栈结构,objc_autoreleasepoolpush、objc_autoreleasepoolpop、objc_autorelease
使用场景:多次创建临时变量导致内存上涨时,需要延迟释放
autoreleasepoolpage的内存结构:4k存储大小

见图 2


18、Autorelase对象什么时候释放?

在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop。


19、Runloop与线程的关系?Runloop的mode? Runloop的作用?内部机制?

每一个线程都有一个runloop,主线程的runloop默认启动。
mode:主要用来指定事件在运行时循环的优先级
作用:保持程序的持续运行、随时处理各种事件、节省cpu资源(没事件休息释放资源)、渲染屏幕UI


20、iOS中使用的锁、死锁的发生与避免

@synchronized、信号量、NSLock等
死锁:多个线程同时访问同一资源,造成循环等待。GCD使用异步线程、并行队列


21、NSOperation和GCD的区别

GCD底层使用C语言编写高效、NSOperation是对GCD的面向对象的封装。对于特殊需求,如取消任务、设置任务优先级、任务状态监听,NSOperation使用起来更加方便。
NSOperation可以设置依赖关系,而GCD只能通过dispatch_barrier_async实现
NSOperation可以通过KVO观察当前operation执行状态(执行/取消)
NSOperation可以设置自身优先级(queuePriority)。GCD只能设置队列优先级 (DISPATCH_QUEUE_PRIORITY_DEFAULT),无法在执行的block中设置优先级
NSOperation可以自定义operation如NSInvationOperation/NSBlockOperation,而GCD执行任务可以自定义封装但没有那么高的代码复用度
GCD高效,NSOperation开销相对高


22、App启动优化策略?main函数执行前后怎么优化

启动时间 = pre-main耗时+main耗时
pre-main阶段优化:
删除无用代码
抽象重复代码
+load方法做的事情延迟到initialize中,或者+load的事情不宜花费太多时间
减少不必要的framework,或者优化已有framework

Main阶段优化
didFinishLauchingwithOptions里代码延后执行
首次启动渲染的页面优化


23、Swift 支持面向过程编程吗?

它采用了 Objective-C 的命名参数以及动态对象模型,可以无缝对接到现有的 Cocoa 框架,并且可以兼容 Objective-C 代码,支持面向过程编程和面向对象编程



24、Swift 是一门安全语言吗?

Swift是一门类型安全的语言,Optionals就是代表。Swift能帮助你在类型安全的环境下工作,如果你的代码中需要使用String类型,Swift的安全机制能阻止你错误的将Int值传递过来,这使你在开发阶段就能及时发现并修正问题。


25、Swift中如何定义变量和常量?

使用let来声明常量,使用var来声明变量


26、oc与js交互

拦截url
JavaScriptCore(只适用于UIWebView)
WKScriptMessageHandler(只适用于WKWebView)
WebViewJavaScriptBridge(第三方框架)


27、Swift的内存管理是怎样的?

Swift 使用自动引用计数(Automatic Reference Counting, ARC)来简化内存管理


28、struct、Class的区别

class可以继承,struct不可以
class是引用类型,struct是值类型
struct在function里修改property时需要mutating关键字修饰


29、访问控制关键字(public、open、private、filePrivate、internal)

public与open:public在module内部中,class和func都可以被访问/重载/继承,外部只能访问;而open都可以
private与filePrivate:private修饰class/func,表示只能在当前class源文件/func内部使用,外部不可以被继承和访问;而filePrivate表示只能在当前swift源文件内访问
internal:在整个模块或者app内都可以访问,默认访问级别,可写可不写


30、OC与Swift混编

OC调用swift:import "工程名-swift.h” @objc
swift调用oc:桥接文件
31、用Swift定义一个数组和字典?
let emptyArray = String[]()
let emptyDictionary = Dictionary<String, Float>()
32、try、try?与try!
try:手动捕捉异常
try?:系统帮我们处理,出现异常返回nil;没有异常返回对应的对象
try!:直接告诉系统,该方法没有异常。如果出现异常程序会crash
33、guard与defer
guard用于提前处理错误数据,else退出程序,提高代码可读性
defer延迟执行,回收资源。多个defer反序执行,嵌套defer先执行外层,后执行内层
34、架构&设计模式
MVC设计模式介绍
MVVM介绍、MVC与MVVM的区别?
ReactiveCocoa的热信号与冷信号
缓存架构设计LRU方案
SDWebImage源码,如何实现解码
AFNetWorking源码分析
组件化的实施,中间件的设计
哈希表的实现原理?如何解决冲突
35、数据结构&算法
快速排序、归并排序
二维数组查找(每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数)
二叉树的遍历:判断二叉树的层数
单链表判断环
36、内存泄露问题?
主要集中在循环引用问题中,如block、NSTime、perform selector引用计数问题。
37、crash防护?
unrecognized selector crash
KVO crash
NSNotification crash
NSTimer crash
Container crash(数组越界,插nil等)
NSString crash (字符串操作的crash)
Bad Access crash (野指针)
UI not on Main Thread Crash (非主线程刷UI (机制待改善))





收起阅读 »

iOS 面试题 八股文 1.6

如何自定义下标获取 实现 subscript 即可, 如extension AnyList { subscript(index: Int) -> T{ return self.list[index] } subsc...
继续阅读 »

如何自定义下标获取


实现 subscript 即可, 如

extension AnyList {
subscript(index: Int) -> T{
return self.list[index]
}
subscript(indexString: String) -> T?{
guard let index = Int(indexString) else {
return nil
}
return self.list[index]
}
}


索引除了数字之外, 其他类型也是可以的


?? 的作用


可选值的默认值, 当可选值为nil 的时候, 会返回后面的值. 如

let someValue = optional1 ?? 0


lazy 的作用


懒加载, 当属性要使用的时候, 才去完成初始化

class LazyClass {
lazy var someLazyValue: Int = {
print("lazy init value")
return 1
}()
var someNormalValue: Int = {
print("normal init value")
return 2
}()
}
let lazyInstance = LazyClass()
print(lazyInstance.someNormalValue)
print(lazyInstance.someLazyValue)
// 打印输出
// normal init value
// 2
// lazy init value
// 1


一个类型表示选项,可以同时表示有几个选项选中(类似 UIViewAnimationOptions ),用什么类型表示


需要实现自 OptionSet, 一般使用 struct 实现. 由于 OptionSet 要求有一个不可失败的init(rawValue:) 构造器, 而 枚举无法做到这一点(枚举的原始值构造器是可失败的, 而且有些组合值, 是没办法用一个枚举值表示的)

struct SomeOption: OptionSet {
let rawValue: Int
static let option1 = SomeOption(rawValue: 1 << 0)
static let option2 = SomeOption(rawValue:1 << 1)
static let option3 = SomeOption(rawValue:1 << 2)
}
let options: SomeOption = [.option1, .option2]


inout 的作用


输入输出参数, 如:

func swap( a: inout Int, b: inout Int) {
let temp = a
a = b
b = temp
}
var a = 1
var b = 2
print(a, b)// 1 2
swap(a: &a, b: &b)
print(a, b)// 2 1


Error 如果要兼容 NSError 需要做什么操作


其实直接转换就可以, 例如 SomeError.someError as NSError 但是这样没有错误码, 描述等等, 如果想和 NSError 一样有这些东西, 只需要实现 LocalizedErrorCustomNSError 协议, 有些方法有默认实现, 可以略过, 如:

enum SomeError: Error, LocalizedError, CustomNSError {
case error1, error2
public var errorDescription: String? {
switch self {
case .error1:
return "error description error1"
case .error2:
return "error description error2"
}
}
var errorCode: Int {
switch self {
case .error1:
return 1
case .error2:
return 2
}
}
public static var errorDomain: String {
return "error domain SomeError"
}
public var errorUserInfo: [String : Any] {
switch self {
case .error1:
return ["info": "error1"]
case .error2:
return ["info": "error2"]
}
}
}
print(SomeError.error1 as NSError)
// Error Domain=error domain SomeError Code=1 "error description error1" UserInfo={info=error1}


下面的代码都用了哪些语法糖


[1, 2, 3].map{ $0 * 2 }

[1, 2, 3] 使用了, Array 实现的ExpressibleByArrayLiteral 协议, 用于接收数组的字面值

map{xxx} 使用了闭包作为作为最后一个参数时, 可以直接写在调用后面, 而且, 如果是唯一参数的话, 圆括号也可以省略

闭包没有声明函数参数, 返回值类型, 数量, 依靠的是闭包类型的自动推断

闭包中语句只有一句时, 自动将这一句的结果作为返回值

0 在没有声明参数列表的时候, 第一个参数名称为0, 后续参数以此类推


什么是高阶函数


一个函数如果可以以某一个函数作为参数, 或者是返回值, 那么这个函数就称之为高阶函数, 如 map, reduce, filter


如何解决引用循环



  1. 转换为值类型, 只有类会存在引用循环, 所以如果能不用类, 是可以解引用循环的,

  2. delegate 使用 weak 属性.

  3. 闭包中, 对有可能发生循环引用的对象, 使用 weak 或者 unowned, 修饰


下面的代码会不会崩溃,说出原因

var mutableArray = [1,2,3]
for _ in mutableArray {
mutableArray.removeLast()
}


不会, 原理不清楚, 就算是把 removeLast(), 换成 removeAll() ,这个循环也会执行三次, 估计是在一开始, for

in 就对 mutableArray 进行了一次值捕获, 而 Array 是一个值类型 , removeLast() 并不能修改捕获的值.


给集合中元素是字符串的类型增加一个扩展方法,应该怎么声明


使用 where 子句, 限制 Element 为 String

extension Array where Element == String {
var isStringElement:Bool {
return true
}
}
["1", "2"].isStringElement
//[1, 2].isStringElement// error


定义静态方法时关键字 static 和 class 有什么区别


static 定义的方法不可以被子类继承, class 则可以

class AnotherClass {
static func staticMethod(){}
class func classMethod(){}
}
class ChildOfAnotherClass: AnotherClass {
override class func classMethod(){}
//override static func staticMethod(){}// error
}


一个 Sequence 的索引是不是一定从 0 开始?


不一定, 两个 for in 并不能保证都是从 0 开始, 且输出结果一致, 官方文档如下



Repeated Access


The Sequence protocol makes no requirement on conforming types regarding

whether they will be destructively consumed by iteration. As a

consequence, don't assume that multiple for-in loops on a sequence

will either resume iteration or restart from the beginning:

for element in sequence {
if ... some condition { break }
}

for element in sequence {
// No defined behavior
}



有些同学还是不太理解, 我写了一个demo 当作参考

class Countdown: Sequence, IteratorProtocol {
var count: Int
init(count: Int) {
self.count = count
}
func next() -> Int? {
if count == 0 {
return nil
} else {
defer { count -= 1 }
return count
}
}
}

var countDown = Countdown(count: 5)
print("begin for in 1")
for c in countDown {
print(c)
}
print("end for in 1")
print("begin for in 2")
for c in countDown {
print(c)
}
print("end for in 2")


最后输出的结果是

begin for in 1
5
4
3
2
1
end for in 1
begin for in 2
end for in 2


很明显, 第二次没有输出任何结果, 原因就是在第二次for in 的时候, 并没有将count 重置.


数组都实现了哪些协议


MutableCollection, 实现了可修改的数组, 如 a[1] = 2

ExpressibleByArrayLiteral, 实现了数组可以从[1, 2, 3] 这种字面值初始化的能力

...


如何自定义模式匹配


这部分不太懂, 贴个链接吧

http://swifter.tips/pattern-match/


autoclosure 的作用


自动闭包, 会自动将某一个表达式封装为闭包. 如

func autoClosureFunction(_ closure: @autoclosure () -> Int) {
closure()
}
autoClosureFunction(1)


详细可参考http://swifter.tips/autoclosure/


编译选项 whole module optmization 优化了什么


编译器可以跨文件优化编译代码, 不局限于一个文件.

http://www.jianshu.com/p/8dbf2bb05a1c


下面代码中 mutating 的作用是什么

struct Person {
var name: String {
mutating get {
return store
}
}
}


让不可变对象无法访问 name 属性


如何让自定义对象支持字面量初始化


有几个协议, 分别是

ExpressibleByArrayLiteral 可以由数组形式初始化

ExpressibleByDictionaryLiteral 可以由字典形式初始化

ExpressibleByNilLiteral 可以由nil 值初始化

ExpressibleByIntegerLiteral 可以由整数值初始化

ExpressibleByFloatLiteral 可以由浮点数初始化

ExpressibleByBooleanLiteral 可以由布尔值初始化

ExpressibleByUnicodeScalarLiteral

ExpressibleByExtendedGraphemeClusterLiteral

ExpressibleByStringLiteral

这三种都是由字符串初始化, 上面两种包含有 Unicode 字符和特殊字符


dynamic framework 和 static framework 的区别是什么



静态库和动态库, 静态库是每一个程序单独打包一份, 而动态库则是多个程序之间共享



链接: https://www.jianshu.com/p/7c7f4b4e4efe

链接:https://www.jianshu.com/p/cc4a737ddc1d

链接:https://www.jianshu.com/p/23d99f434281

收起阅读 »

iOS 面试题 八股文 1.5

defer 使用场景 defer 语句块中的代码, 会在当前作用域结束前调用, 常用场景如异常退出后, 关闭数据库连接func someQuery() -> ([Result], [Result]){ let db = DBOpen("xxx")...
继续阅读 »

defer 使用场景


defer 语句块中的代码, 会在当前作用域结束前调用, 常用场景如异常退出后, 关闭数据库连接

func someQuery() -> ([Result], [Result]){
let db = DBOpen("xxx")
defer {
db.close()
}
guard results1 = db.query("query1") else {
return nil
}
guard results2 = db.query("query2") else {
return nil
}
return (results1, results2)
}


需要注意的是, 如果有多个 defer, 那么后加入的先执行

func someDeferFunction() {
defer {
print("\(#function)-end-1-1")
print("\(#function)-end-1-2")
}
defer {
print("\(#function)-end-2-1")
print("\(#function)-end-2-2")
}
if true {
defer {
print("if defer")
}
print("if end")
}
print("function end")
}
someDeferFunction()
// 输出
// if end
// if defer
// function end
// someDeferFunction()-end-2-1
// someDeferFunction()-end-2-2
// someDeferFunction()-end-1-1
// someDeferFunction()-end-1-2


String 与 NSString 的关系与区别


NSString 与 String 之间可以随意转换,

let someString = "123"
let someNSString = NSString(string: "n123")
let strintToNSString = someString as NSString
let nsstringToString = someNSString as String


String 是结构体, 值类型, NSString 是类, 引用类型.

通常, 没必要使用 NSString 类, 除非你要使用一些特有方法, 例如使用 pathExtension 属性


怎么获取一个 String 的长度


不考虑编码, 只是想知道字符的数量, 用characters.count

"hello".characters.count // 5
"你好".characters.count // 2
"こんにちは".characters.count // 5


如果想知道在某个编码下占多少字节, 可以用

"hello".lengthOfBytes(using: .ascii) // 5
"hello".lengthOfBytes(using: .unicode) // 10
"你好".lengthOfBytes(using: .unicode) // 4
"你好".lengthOfBytes(using: .utf8) // 6
"こんにちは".lengthOfBytes(using: .unicode) // 10
"こんにちは".lengthOfBytes(using: .utf8) // 15


如何截取 String 的某段字符串


swift 中, 有三个取子串函数,

substring:to , substring:from, substring:with.

let simpleString = "Hello, world"
simpleString.substring(to: simpleString.index(simpleString.startIndex, offsetBy: 5))
// hello
simpleString.substring(from: simpleString.index(simpleString.endIndex, offsetBy: -5))
// world
simpleString.substring(with: simpleString.index(simpleString.startIndex, offsetBy: 5) ..< simpleString.index(simpleString.endIndex, offsetBy: -5))
// ,


使用起来略微麻烦, 具体用法可以参考我的另一篇文章http://www.jianshu.com/p/b3231f9406e9


throws 和 rethrows 的用法与作用


throws 用在函数上, 表示这个函数会抛出错误.

有两种情况会抛出错误, 一种是直接使用 throw 抛出, 另一种是调用其他抛出异常的函数时, 直接使用 try xx 没有处理异常.

enum DivideError: Error {
case EqualZeroError;
}
func divide(_ a: Double, _ b: Double) throws -> Double {
guard b != Double(0) else {
throw DivideError.EqualZeroError
}
return a / b
}
func split(pieces: Int) throws -> Double {
return try divide(1, Double(pieces))
}


rethrows 与 throws 类似, 不过只适用于参数中有函数, 且函数会抛出异常的情况, rethrows 可以用 throws 替换, 反过来不行

func processNumber(a: Double, b: Double, function: (Double, Double) throws -> Double) rethrows -> Double {
return try function(a, b)
}


try? 和 try!是什么意思


这两个都用于处理可抛出异常的函数, 使用这两个关键字可以不用写 do catch.

区别在于, try? 在用于处理可抛出异常函数时, 如果函数抛出异常, 则返回 nil, 否则返回函数返回值的可选值, 如:

print(try? divide(2, 1))
// Optional(2.0)
print(try? divide(2, 0))
// nil


而 try! 则在函数抛出异常的时候崩溃, 否则则返会函数返回值, 相当于(try? xxx)!, 如:

print(try! divide(2, 1))
// 2.0
print(try! divide(2, 0))
// 崩溃


associatedtype 的作用


简单来说就是 protocol 使用的泛型

例如定义一个列表协议

protocol ListProtcol {
associatedtype Element
func push(_ element:Element)
func pop(_ element:Element) -> Element?
}


实现协议的时候, 可以使用 typealias 指定为特定的类型, 也可以自动推断, 如

class IntList: ListProtcol {
typealias Element = Int // 使用 typealias 指定为 Int
var list = [Element]()
func push(_ element: Element) {
self.list.append(element)
}
func pop(_ element: Element) -> Element? {
return self.list.popLast()
}
}
class DoubleList: ListProtcol {
var list = [Double]()
func push(_ element: Double) {// 自动推断
self.list.append(element)
}
func pop(_ element: Double) -> Double? {
return self.list.popLast()
}
}


使用泛型也可以

class AnyList<T>: ListProtcol {
var list = [T]()
func push(_ element: T) {
self.list.append(element)
}
func pop(_ element: T) -> T? {
return self.list.popLast()
}
}


可以使用 where 字句限定 Element 类型, 如:

extension ListProtcol where Element == Int {
func isInt() ->Bool {
return true
}
}


什么时候使用 final


final 用于限制继承和重写. 如果只是需要在某一个属性前加一个 final.

如果需要限制整个类无法被继承, 那么可以在类名之前加一个final


public 和 open 的区别


这两个都用于在模块中声明需要对外界暴露的函数, 区别在于, public 修饰的类, 在模块外无法继承, 而 open 则可以任意继承, 公开度来说, public < open


声明一个只有一个参数没有返回值闭包的别名


没有返回值也就是返回值为 Void

typealias SomeClosuerType = (String) -> (Void)
let someClosuer: SomeClosuerType = { (name: String) in
print("hello,", name)
}
someClosuer("world")
// hello, world


Self 的使用场景


Self 通常在协议中使用, 用来表示实现者或者实现者的子类类型.

例如, 定义一个复制的协议

protocol CopyProtocol {
func copy() -> Self
}


如果是结构体去实现, 要将Self 换为具体的类型

struct SomeStruct: CopyProtocol {
let value: Int
func copySelf() -> SomeStruct {
return SomeStruct(value: self.value)
}
}


如果是类去实现, 则有点复杂, 需要有一个 required 初始化方法, 具体可以看这里 http://swifter.tips/use-self/

class SomeCopyableClass: CopyProtocol {
func copySelf() -> Self {
return type(of: self).init()
}
required init(){}
}


dynamic 的作用


由于 swift 是一个静态语言, 所以没有 Objective-C 中的消息发送这些动态机制, dynamic 的作用就是让 swift 代码也能有 Objective-C 中的动态机制, 常用的地方就是 KVO 了, 如果要监控一个属性, 则必须要标记为 dynamic, 可以参考我的文章http://www.jianshu.com/p/ae26100b9edf


什么时候使用 @objc


@objc 用途是为了在 Objective-C 和 Swift 混编的时候, 能够正常调用 Swift 代码. 可以用于修饰类, 协议, 方法, 属性.

常用的地方是在定义 delegate 协议中, 会将协议中的部分方法声明为可选方法, 需要用到@objc

@objc protocol OptionalProtocol {
@objc optional func optionalFunc()
func normalFunc()
}
class OptionProtocolClass: OptionalProtocol {
func normalFunc() {
}
}
let someOptionalDelegate: OptionalProtocol = OptionProtocolClass()
someOptionalDelegate.optionalFunc?()


Optional(可选型) 是用什么实现的


Optional 是一个泛型枚举

大致定义如下:

enum Optional<Wrapped> {
case none
case some(Wrapped)
}


除了使用 let someValue: Int? = nil 之外, 还可以使用let optional1: Optional<Int> = nil 来定义


收起阅读 »

iOS 面试题 八股文 1.4

励志背下所有的八股文class 和 struct 的区别 class 为类, struct 为结构体, 类是引用类型, 结构体为值类型, 结构体不可以继承 不通过继承,代码复用(共享)的方式有哪些 扩展, 全局函数 Set 独有的方法有哪些?// 定义一个 s...
继续阅读 »

励志背下所有的八股文

class 和 struct 的区别


class 为类, struct 为结构体, 类是引用类型, 结构体为值类型, 结构体不可以继承


不通过继承,代码复用(共享)的方式有哪些


扩展, 全局函数


Set 独有的方法有哪些?

// 定义一个 set
let setA: Set<Int> = [1, 2, 3, 4, 4]// {1, 2, 3, 4}, 顺序可能不一致, 同一个元素只有一个值
let setB: Set<Int> = [1, 3, 5, 7, 9]// {1, 3, 5, 7, 9}
// 取并集 A | B
let setUnion = setA.union(setB)// {1, 2, 3, 4, 5, 7, 9}
// 取交集 A & B
let setIntersect = setA.intersection(setB)// {1, 3}
// 取差集 A - B
let setRevers = setA.subtracting(setB) // {2, 4}
// 取对称差集, A XOR B = A - B | B - A
let setXor = setA.symmetricDifference(setB) //{2, 4, 5, 7, 9}


实现一个 min 函数,返回两个元素较小的元素

func myMin<T: Comparable>(_ a: T, _ b: T) -> T {
return a < b ? a : b
}
myMin(1, 2)


map、filter、reduce 的作用


map 用于映射, 可以将一个列表转换为另一个列表

[1, 2, 3].map{"\($0)"}// 数字数组转换为字符串数组
["1", "2", "3"]


filter 用于过滤, 可以筛选出想要的元素

[1, 2, 3].filter{$0 % 2 == 0} // 筛选偶数
// [2]


reduce 合并

[1, 2, 3].reduce(""){$0 + "\($1)"}// 转换为字符串并拼接
// "123"


组合示例

(0 ..< 10).filter{$0 % 2 == 0}.map{"\($0)"}.reduce(""){$0 + $1}
// 02468


map 与 flatmap 的区别


flatmap 有两个实现函数实现,

public func flatMap<ElementOfResult>(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult]

这个方法, 中间的函数返回值为一个可选值, 而 flatmap 会丢掉那些返回值为 nil 的值

例如

["1", "@", "2", "3", "a"].flatMap{Int($0)}
// [1, 2, 3]
["1", "@", "2", "3", "a"].map{Int($0) ?? -1}
//[Optional(1), nil, Optional(2), Optional(3), nil]


另一个实现

public func flatMap<SegmentOfResult>(_ transform: (Element) throws -> SegmentOfResult) rethrows -> [SegmentOfResult.Iterator.Element] where SegmentOfResult : Sequence

中间的函数, 返回值为一个数组, 而这个 flapmap 返回的对象则是一个与自己元素类型相同的数组

func someFunc(_ array:[Int]) -> [Int] {
return array
}
[[1], [2, 3], [4, 5, 6]].map(someFunc)
// [[1], [2, 3], [4, 5, 6]]
[[1], [2, 3], [4, 5, 6]].flatMap(someFunc)
// [1, 2, 3, 4, 5, 6]


其实这个实现, 相当于是在使用 map 之后, 再将各个数组拼起来一样的

[[1], [2, 3], [4, 5, 6]].map(someFunc).reduce([Int]()) {$0 + $1}
// [1, 2, 3, 4, 5, 6]


什么是 copy on write时候


写时复制, 指的是 swift 中的值类型, 并不会在一开始赋值的时候就去复制, 只有在需要修改的时候, 才去复制.

这里有详细的说明

http://www.jianshu.com/p/7e8ba0659646


如何获取当前代码的函数名和行号


#file 用于获取当前文件文件名

#line 用于获取当前行号

#column 用于获取当前列编号

#function 用于获取当前函数名

以上这些都是特殊的字面量, 多用于调试输出日志

具体可以看这里 apple 文档

https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Expressions.html

这里有中文翻译

http://wiki.jikexueyuan.com/project/swift/chapter3/04_Expressions.html


如何声明一个只能被类 conform 的 protocol


声明协议的时候, 加一个 class 即可

protocol SomeClassProtocl: class {
func someFunction()
}


guard 使用场景


guard 和 if 类似, 不同的是, guard 总是有一个 else 语句, 如果表达式是假或者值绑定失败的时候, 会执行 else 语句, 且在 else 语句中一定要停止函数调用

例如

guard 1 + 1 == 2 else {
fatalError("something wrong")
}


常用使用场景为, 用户登录的时候, 验证用户是否有输入用户名密码等

guard let userName = self.userNameTextField.text,
let password = self.passwordTextField.text else {
return
}


收起阅读 »

n皇后问题

在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。 典型的回溯法问题 思路: 尝试性的放置 ,从第一行开始,接着在下一行放置,(这里的好处就是不需要考虑行了,只需要考虑列和对角线) 一直纠...
继续阅读 »

在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。


典型的回溯法问题


思路:


尝试性的放置 ,从第一行开始,接着在下一行放置,(这里的好处就是不需要考虑行了,只需要考虑列和对角线)

一直纠结第一行的问题,代码中直接传参数为0,一直好奇如何控制第一行的列的变化,后来将0自己模拟走了一遍才明白。

注意判断的是否符合规则的公式:(列==列)(abs(列-列)==abs(行-行))

具体细节见注释(仔细阅读,一定能看懂,)

#include<iostream>
#include <math.h>
#define N 8
using namespace std;

int num=0;//用来记录总的放置个数
int cur[8];//此全局变量是用来记录第i行放在得第j列,其中下标为i,值为j
int check(int n){//传进来行
for(int i=0;i<n;i++){
if(cur[i]==cur[n]||abs(n-i)==abs(cur[n]-cur[i])){//判断当前放置的位置是否与之前的放置位置是否在同一列或同斜列
return 0;
}
}
return 1;
}
void putQueen(int n){
if(n==N){//如果找到了最后一行的下一行,那么就可以将次数+1了(就是之前把所有的行已经放完了,数组下标从0开始的勿忘)
num++;
}else{
for(int j=0;j<N;j++){//列的位置从0往最后放置
cur[n]=j;//记录下当前行的当前列
if(check(n)){//判断当前放置的行列是否合适
putQueen(n+1);//开始进行下一行的放置
}
}
}

}
int main(){
putQueen(0);
cout<<num;
return 0;
}

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

还在用Swagger?试试这款零注解侵入的API文档生成工具,跟Postman绝配!

前后端接口联调需要API文档,我们经常会使用工具来生成。之前经常使用Swagger来生成,最近发现一款好用的API文档生成工具smart-doc, 它有着很多Swagger不具备的特点,推荐给大家。 SpringBoot实战电商项目mall(50k+star...
继续阅读 »

前后端接口联调需要API文档,我们经常会使用工具来生成。之前经常使用Swagger来生成,最近发现一款好用的API文档生成工具smart-doc, 它有着很多Swagger不具备的特点,推荐给大家。



SpringBoot实战电商项目mall(50k+star)地址:github.com/macrozheng/…


聊聊Swagger


在我们使用Swagger的时候,经常会需要用到它的注解,比如@Api@ApiOperation这些,Swagger通过它们来生成API文档。比如下面的代码:



Swagger对代码的入侵性比较强,有时候代码注释和注解中的内容有点重复了。有没有什么工具能实现零注解入侵,直接根据代码注释生成API文档呢?smart-doc恰好是这种工具!


smart-doc简介


smart-doc是一款API文档生成工具,无需多余操作,只要你规范地写好代码注释,就能生成API文档。同时能直接生成Postman调试文件,一键导入Postman即可调试,非常好用!


smart-doc具有如下优点:



生成API文档



接下来我们把smart-doc集成到SpringBoot项目中,体验一下它的API文档生成功能。




  • 首先我们需要在项目中添加smart-doc的Maven插件,可以发现smart-doc就是个插件,连依赖都不用添加,真正零入侵啊;


<plugin>
<groupId>com.github.shalousun</groupId>
<artifactId>smart-doc-maven-plugin</artifactId>
<version>2.2.8</version>
<configuration>
<!--指定smart-doc使用的配置文件路径-->
<configFile>./src/main/resources/smart-doc.json</configFile>
<!--指定项目名称-->
<projectName>mall-tiny-smart-doc</projectName>
</configuration>
</plugin>


  • 接下来在项目的resources目录下,添加配置文件smart-doc.json,属性说明直接参考注释即可;


{
"serverUrl": "http://localhost:8088", //指定后端服务访问地址
"outPath": "src/main/resources/static/doc", //指定文档的输出路径,生成到项目静态文件目录下,随项目启动可以查看
"isStrict": false, //是否开启严格模式
"allInOne": true, //是否将文档合并到一个文件中
"createDebugPage": false, //是否创建可以测试的html页面
"packageFilters": "com.macro.mall.tiny.controller.*", //controller包过滤
"style":"xt256", //基于highlight.js的代码高设置
"projectName": "mall-tiny-smart-doc", //配置自己的项目名称
"showAuthor":false, //是否显示接口作者名称
"allInOneDocFileName":"index.html" //自定义设置输出文档名称
}


  • 打开IDEA的Maven面板,双击smart-doc插件的smart-doc:html按钮,即可生成API文档;




  • 此时我们可以发现,在项目的static/doc目录下已经生成如下文件;




  • 运行项目,访问生成的API接口文档,发现文档非常详细,包括了请求参数和响应结果的各种说明,访问地址:http://localhost:8088/doc/index.html




  • 我们回过来看下实体类的代码,可以发现我们只是规范地添加了字段注释,生成文档的时候就自动有了;


public class PmsBrand implements Serializable {
/**
* ID
*/
private Long id;

/**
* 名称
* @required
*/
private String name;

/**
* 首字母
* @since 1.0
*/
private String firstLetter;

/**
* 排序
*/
private Integer sort;

/**
* 是否为品牌制造商(0,1)
*/
private Integer factoryStatus;

/**
* 显示状态(0,1)
* @ignore
*/
private Integer showStatus;

/**
* 产品数量
*/
private Integer productCount;

/**
* 产品评论数量
*/
private Integer productCommentCount;

/**
* 品牌logo
*/
private String logo;

/**
* 专区大图
*/
private String bigPic;

/**
* 品牌故事
*/
private String brandStory;
//省略getter、setter方法
}


  • 再来看下Controller中代码,我们同样规范地在方法上添加了注释,生成API文档的时候也自动有了;


/**
* 商品品牌管理
*/
@Controller
@RequestMapping("/brand")
public class PmsBrandController {
@Autowired
private PmsBrandService brandService;

/**
* 分页查询品牌列表
*
* @param pageNum 页码
* @param pageSize 分页大小
*/
@RequestMapping(value = "/list", method = RequestMethod.GET)
@ResponseBody
@PreAuthorize("hasRole('ADMIN')")
public CommonResult<CommonPage<PmsBrand>> listBrand(@RequestParam(value = "pageNum", defaultValue = "1")
Integer pageNum,
@RequestParam(value = "pageSize", defaultValue = "3")
Integer pageSize) {
List<PmsBrand> brandList = brandService.listBrand(pageNum, pageSize);
return CommonResult.success(CommonPage.restPage(brandList));
}
}


  • 当然smart-doc还提供了自定义注释tag,用于增强文档功能;

    • @ignore:生成文档时是否要过滤该属性;

    • @required:用于修饰接口请求参数是否必须;

    • @since:用于修饰接口中属性添加的版本号。



  • 为了写出优雅的API文档接口,我们经常会对返回结果进行统一封装,smart-doc也支持这样的设置,在smart-doc.json中添加如下配置即可;


{
"responseBodyAdvice":{ //统一返回结果设置
"className":"com.macro.mall.tiny.common.api.CommonResult" //对应封装类
}
}


  • 我们也经常会用枚举类型来封装状态码,在smart-doc.json中添加如下配置即可;


{
"errorCodeDictionaries": [{ //错误码列表设置
"title": "title",
"enumClassName": "com.macro.mall.tiny.common.api.ResultCode", //错误码枚举类
"codeField": "code", //错误码对应字段
"descField": "message" //错误码描述对应字段
}]
}


  • 配置成功后,即可在API文档中生成错误码列表




  • 有时候我们也会想给某些接口添加自定义请求头,比如给一些需要登录的接口添加Authorization头,在smart-doc.json中添加如下配置即可;


{
"requestHeaders": [{ //请求头设置
"name": "Authorization", //请求头名称
"type": "string", //请求头类型
"desc": "token请求头的值", //请求头描述
"value":"token请求头的值", //请求头的值
"required": false, //是否必须
"since": "-", //添加版本
"pathPatterns": "/brand/**", //哪些路径需要添加请求头
"excludePathPatterns":"/admin/login" //哪些路径不需要添加请求头
}]
}


  • 配置成功后,在接口文档中即可查看到自定义请求头信息了。



使用Postman测试接口



我们使用Swagger生成文档时候,是可以直接在上面测试接口的,而smart-doc的接口测试能力真的很弱,这也许是它拥抱Postman的原因吧,毕竟Postman是非常好用的接口测试工具,下面我们来结合Postman使用下!




  • smart-doc内置了Postman的json生成插件,可以一键生成并导入到Postman中去,双击smart-doc:postman按钮即可生成;




  • 此时将在项目的static/doc目录下生成postman.json文件;




  • postman.json文件直接导入到Postman中即可使用;




  • 导入成功后,所有接口都将在Postman中显示,这下我们可以愉快地测试接口了!



总结


smart-doc确实是一款好用的API文档生成工具,尤其是它零注解侵入的特点。虽然它的接口测试能力有所不足,但是可以一键生成JSON文件并导入到Postman中去,使用起来也是非常方便的!


参考资料


官方文档:gitee.com/smart-doc-t…


项目源码地址


github.com/macrozheng/…


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

你的列表很卡?这4个优化能让你的列表丝般顺滑

前言 列表 ListView 是应用中最为常见得组件,而列表往往也会承载很多元素,当元素多,尤其是那种图片文件比较大的场合,就可能会导致列表卡顿,严重的时候可能导致应用崩溃。本篇来介绍如何优化列表。 优化点1:使用 builder构建列表 当你的列表元素是动态...
继续阅读 »

前言


列表 ListView 是应用中最为常见得组件,而列表往往也会承载很多元素,当元素多,尤其是那种图片文件比较大的场合,就可能会导致列表卡顿,严重的时候可能导致应用崩溃。本篇来介绍如何优化列表。


优化点1:使用 builder构建列表


当你的列表元素是动态增长的时候(比如上拉加载更多),请不要直接用children 的方式,一直往children 的数组增加组件,那样会很糟糕。


//糟糕的用法
ListView(
children: [
item1,
item2,
item3,
...
],
)

//正确的用法
ListView.builder(
itemBuilder: (context, index) => ListItem(),
itemCount: itemCount,
)

对于 ListView.builder 是按需构建列表元素,也就是只有那些可见得元素才会调用itemBuilder 构建元素,这样对于大列表而言性能开销自然会小很多。



Creates a scrollable, linear array of widgets that are created on demand.
This constructor is appropriate for list views with a large (or infinite) number of children because the builder is called only for those children that are actually visible.



优化点2:禁用 addAutomaticKeepAlives 和 addRepaintBoundaries 特性


这两个属性都是为了优化滚动过程中的用户体验的。
addAutomaticKeepAlives 特性默认是 true,意思是在列表元素不可见后可以保持元素的状态,从而在再次出现在屏幕的时候能够快速构建。这其实是一个拿空间换时间的方法,会造成一定程度得内存开销。可以设置为 false 关闭这一特性。缺点是滑动过快的时候可能会出现短暂的白屏(实际会很少发生)。


addRepaintBoundaries 是将列表元素使用一个重绘边界(Repaint Boundary)包裹,从而使得滚动的时候可以避免重绘。而如果列表很容易绘制(列表元素布局比较简单的情况下)的时候,可以关闭这个特性来提高滚动的流畅度。


addAutomaticKeepAlives: false,
addRepaintBoundaries: false,

优化点3:尽可能将列表元素中不变的组件使用 const 修饰


使用 const 相当于将元素缓存起来实现共用,若列表元素某些部分一直保持不变,那么可以使用 const 修饰。


return Padding(
child: Row(
children: [
const ListImage(),
const SizedBox(
width: 5.0,
),
Text('第$index 个元素'),
],
),
padding: EdgeInsets.all(10.0),
);

优化点4:使用 itemExtent 确定列表元素滚动方向的尺寸


对于很多列表,我们在滚动方向上的尺寸是提前可以根据 UI设计稿知道的,如果能够知道的话,那么使用 itemExtent 属性制定列表元素在滚动方向的尺寸,可以提升性能。这是因为,如果不指定的话,在滚动过程中,会需要推算每个元素在滚动方向的尺寸从而消耗计算资源。


itemExtent: 120,

优化实例


下面是一开始未改造的列表,嗯,可以认为是垃圾代码


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

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

class _LargeListViewState extends State<LargeListView> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('大列表'),
brightness: Brightness.dark,
),
body: ListView(
children: List.generate(
1000,
(index) => Padding(
padding: EdgeInsets.all(10.0),
child: Row(
children: [
Image.network(
'https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7869eac08a7d4177b600dc7d64998204~tplv-k3u1fbpfcp-watermark.jpeg',
width: 200,
),
const SizedBox(
width: 5.0,
),
Text('第$index 个元素'),
],
),
),
),
),
);
}
}

当然,实际不会是用 List.generate 来生成列表元素,**但是也不要用一个 List<Widget> 列表对象一直往里面加列表元素,然后把这个列表作为 ListView 的 **children
改造后的代码如下所示,因为将列表元素拆分得更细,代码量是多一些,但是性能上会好很多。


import 'package:flutter/material.dart';

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

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

class _LargeListViewState extends State<LargeListView> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('大列表'),
brightness: Brightness.dark,
),
body: ListView.builder(
itemBuilder: (context, index) => ListItem(
index: index,
),
itemCount: 1000,
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,
itemExtent: 120.0,
),
);
}
}

class ListItem extends StatelessWidget {
final int index;
ListItem({Key? key, required this.index}) : super(key: key);

@override
Widget build(BuildContext context) {
return Padding(
child: Row(
children: [
const ListImage(),
const SizedBox(
width: 5.0,
),
Text('第$index 个元素'),
],
),
padding: EdgeInsets.all(10.0),
);
}
}

class ListImage extends StatelessWidget {
const ListImage({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Image.network(
'https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7869eac08a7d4177b600dc7d64998204~tplv-k3u1fbpfcp-watermark.jpeg',
width: 200,
);
}
}

总结


本篇介绍了 Flutter ListView 的4个优化要点,非常实用哦!实际上,这些要点都可以从官网的文档里找出对应得说明。因此,如果遇到了性能问题,除了搜索引擎外,也建议多看看官方的文档。另外一个,对于列表图片,有时候也需要前后端配合,比如目前的手机都是号称1亿像素的,如果上传的时候直接上传原图,那么加载如此大的图片肯定是非常消耗资源的。对于这种情况,建议是生成列表缩略图(可能需要针对不同屏幕尺寸生成不同的缩略图,比如掘金的文章头图,就分了几种分辨率)。


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

Handler 源码分析

一、ThreadLocal是什么ThreadLocal 是线程的局部变量, 是每一个线程所单独持有的,其他线程不能对其进行访问,ThreadLocal可以让每个线程拥有一个属于自己的变量的副本,不会和其他线程的变量副本冲突,实现了线程的数据隔离。每个线程都有一...
继续阅读 »


一、ThreadLocal是什么

ThreadLocal 是线程的局部变量, 是每一个线程所单独持有的,其他线程不能对其进行访问,ThreadLocal可以让每个线程拥有一个属于自己的变量的副本,不会和其他线程的变量副本冲突,实现了线程的数据隔离。每个线程都有一个ThreadLocalMap类型的threadLocals变量,ThreadLocalMap是一个自定义哈希映射,仅用于维护线程本地变量值。ThreadLocalMap是ThreadLocal的内部类,主要有一个Entry数组,Entry的key为ThreadLocal,value为ThreadLocal对应的值。

也就是说ThreadLocal本身并不真正存储线程的变量值,它只是一个工具,用来维护Thread内部的Map,帮助存和取。注意上图的虚线,它代表一个弱引用类型,而弱引用的生命周期只能存活到下次GC前。

二、ThreadLocal是什么ThreadLocal为什么会内存泄漏

ThreadLocal在ThreadLocalMap中是以一个弱引用身份被Entry中的Key引用的,因此如果ThreadLocal没有外部强引用来引用它,那么ThreadLocal会在下次JVM垃圾收集时被回收。这个时候就会出现Entry中Key已经被回收,出现一个null Key的情况,外部读取ThreadLocalMap中的元素是无法通过null Key来找到Value的。因此如果当前线程的生命周期很长,一直存在,那么其内部的ThreadLocalMap对象也一直生存下来,这些null key就存在一条强引用链的关系一直存在:Thread --> ThreadLocalMap-->Entry-->Value,这条强引用链会导致Entry不会回收,Value也不会回收,但Entry中的Key却已经被回收的情况,造成内存泄漏。

但是JVM团队已经考虑到这样的情况,并做了一些措施来保证ThreadLocal尽量不会内存泄漏:在ThreadLocal的get()、set()、remove()方法调用的时候会清除掉线程ThreadLocalMap中所有Entry中Key为null的Value,并将整个Entry设置为null,利于下次内存回收。

三、Handler机制

我们主要看一下Looper源码, Looper 在程序启动的时候系统就已经帮我们创建好了

在main方法中系统调用了 Looper.prepareMainLooper();来创建主线程的Looper以及MessageQueue,并通过Looper.loop()来开启主线程的消息循环。来看看Looper.prepareMainLooper()是怎么创建出这两个对象的

//系统实例化 Handler
public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
}

可以看到,在这个方法中调用了 prepare(false);方法和 myLooper();方法,那么再进入prepare()

private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}// 往当前线程的私有变量里添加 Looper
sThreadLocal.set(new Looper(quitAllowed));
}

在这里可以看出,sThreadLocal对象保存了一个Looper对象,首先判断是否已经存在Looper对象了,以防止被调用两次。sThreadLocal对象是ThreadLocal类型,因此保证了每个线程中只有一个Looper对象。

// Looper 在实例化的时候也实例化了一个消息队列同时还持有了当前线程的引用
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}

//然后我们从发送消息查看源码
public final boolean sendMessage(Message msg){
return sendMessageDelayed(msg, 0);
}

------经过几个方法的调用进入下面的方法

public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
// mQueue 在 Handler 实例化的时候就从当前线程中取出消息队列并赋值了
MessageQueue queue = mQueue;
if (queue == null) {
RuntimeException e = new RuntimeException(this + "sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
return false;
}
return enqueueMessage(queue, msg, uptimeMillis);
}

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
// 重点在这里,把当前 Handler 的引用赋值给 msg 的 target
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}

------进入消息队列的源码

boolean enqueueMessage(Message msg, long when) {
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
if (msg.isInUse()) {
throw new IllegalStateException(msg + " This message is already in use.");
}

synchronized (this) {
if (mQuitting) {
IllegalStateException e = new IllegalStateException(msg.target + " sending message to a Handler on a dead thread");
msg.recycle();
return false;
}
msg.markInUse();
msg.when = when;
Message p = mMessages;
boolean needWake;
// p == null 代表前面没有消息, when 是延迟消息的时间值
if (p == null || when == 0 || when < p.when) {
// New head, wake up the event queue if blocked.
//当前消息的 next 是 p 引用,形成一个单链表结构,如果是第一个消息的话,p 为空
msg.next = p;
// 赋值消息到轮询器
mMessages = msg;
needWake = mBlocked;
} else {
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
// 发送了第一个消息后
mMessages 就不为空了
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
msg.next = p; // invariant: p == prev.next
prev.next = msg;
}
// We can assume mPtr != 0 because mQuitting is false.
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
收起阅读 »

Android基础-LRU缓存策略

前言缓存策略在Android开发是比较常见的,尤其是在图片使用业务场景中缓存策略发挥重要作用。对于移动应用常用UI就是图片组件而且图片显示资源常常来自网络,因此为了更快的图片加载和减省流量就需要使用缓存策略实现一个完备的图片加载框架。图片加载库的基本逻辑如下:...
继续阅读 »

前言

缓存策略在Android开发是比较常见的,尤其是在图片使用业务场景中缓存策略发挥重要作用。对于移动应用常用UI就是图片组件而且图片显示资源常常来自网络,因此为了更快的图片加载和减省流量就需要使用缓存策略实现一个完备的图片加载框架。

图片加载库的基本逻辑如下:优先从内存中找图片资源;然后从本地资源找图片;最后两者都没有的情况下才从网络中请求图片资源。

请求图片加载
是否有内存缓存
直接加载内存图片资源
是否有本地缓存
加载本地图片资源
请求加载网络图片资源
图片加载完成

内存缓存策略

关于内存缓存策略,在Android原生代码中有LruCache类。LruCache它的核心缓存策略算法是LRU(Least Recently Used)当缓存超出设置最大值时,会优先删除近期使用最少的缓存对象。当然本地存储缓存也能参考LRU策略,两者相结合就能实现一套如上图展示的完善基础资源缓存策略。

LruCache

LruCache是个泛型类,内部主要采用LinkedHashMap<K, V>存储形式缓存对象。提供put和get操作,当缓存超出最大存储值时会移除使用较少的缓存对象,put新的缓存对象。另外还支持remove方法,主动移除缓存对象释放更多缓存存储值。

  • get
public final V get(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V mapValue;
synchronized (this) {
mapValue = map.get(key);
if (mapValue != null) {
hitCount++;
return mapValue;
}
missCount++;
}
V createdValue = create(key);
if (createdValue == null) {
return null;
}
synchronized (this) {
createCount++;
mapValue = map.put(key, createdValue);

if (mapValue != null) {
// There was a conflict so undo that last put
map.put(key, mapValue);
} else {
size += safeSizeOf(key, createdValue);
}
}

if (mapValue != null) {
entryRemoved(false, key, createdValue, mapValue);
return mapValue;
} else {
trimToSize(maxSize);
return createdValue;
}
}
  • put
public final V put(K key, V value) {
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}

V previous;
synchronized (this) {
putCount++;
size += safeSizeOf(key, value);
previous = map.put(key, value);
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}

if (previous != null) {
entryRemoved(false, key, previous, value);
}

trimToSize(maxSize);
return previous;
}
  • remove
public final V remove(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}

V previous;
synchronized (this) {
previous = map.remove(key);
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}

if (previous != null) {
entryRemoved(false, key, previous, null);
}

return previous;
}

通过put和remove方法会发现不管是添加还是删除都会执行safeSizeOf方法,在safeSizeOf中需要开发者自行实现sizeOf方法计算缓存大小累加到缓存池当中。另外put方法还多了trimToSize方法用来check缓存池是否超出最大缓存值。

收起阅读 »

提高app的响应能力-布局优化

提高app的响应能力-布局优化在刚开始开发安卓的时候,应用无响应(ANR)是很常见的,随着安卓手机的性能和编程能力的提高,现在不会遇到这样低级别的错误了,这篇文档分享如何提高应用程序的响应能力。前言应用响应能力意为用户操作时的速度,也就是让使用者感觉到轻、快、...
继续阅读 »

提高app的响应能力-布局优化

在刚开始开发安卓的时候,应用无响应(ANR)是很常见的,随着安卓手机的性能和编程能力的提高,现在不会遇到这样低级别的错误了,这篇文档分享如何提高应用程序的响应能力。

前言

应用响应能力意为用户操作时的速度,也就是让使用者感觉到轻、快、流畅才行。虽然现在的安卓设备一年比一年强劲,但我们在开发中要避免这些“慢”操作,打造让用户感觉到流畅的应用。

帧率控制

肉眼无法感知超过60FPS的动画

虽然有些证明人眼的感知极限是高于60FPS的,但60FPS的帧率已经完全满足。
那么每一帧的切换就是1000/60 等于16毫秒,手机原本为了保持视觉的流畅度,它的屏幕刷新频率是60hz,所以我们在开发中也应该注意这个时间,处理的间隔应当小于这个时间。

布局优化

GPU过度绘制

在开发者选项中有个一个很好用的功能叫“GPU过度绘制”,它的作用是可视化的显示出过度绘制的区域,那么什么叫过度绘制的区域呢。比如我们组件View是从上到下分布的,最顶部的View如果重合下面的View颜色,就叫做过度绘制。

它通过颜色来表示过度绘制的等级。

1.png

来看看下面的颜色级

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

<RelativeLayout
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_centerInParent="true"
android:background="#cfcfcf"
android:padding="10dp">

<RelativeLayout
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_centerInParent="true"
android:background="#cfcfcf"
android:padding="10dp">

<RelativeLayout
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_centerInParent="true"
android:background="#cfcfcf"
android:padding="10dp" />
</RelativeLayout>
</RelativeLayout>
</RelativeLayout>

Screenshot_2021-11-07-20-12-40-86_2a27335eaa331505125090a61677c0b2.jpg

颜色越浅越好,虽然现在的设备对视图的要求并不高,但为了追求极致,我们在开发中还是应该注意这方面的问题。

GPU 呈现模式分析

开发者选项中-GPU呈现模式分析 这个功能开启之后,可以分析GPU的渲染速度,它对每个应用会单独显示一个图形。

Screenshot_2021-11-07-20-27-26-10_2a27335eaa331505125090a61677c0b2.jpg

注意水平方向的每一个竖条代表一帧,绿色线条代表16毫秒,开发者尽量做到没帧在该竖条以下。但由于复杂的业务和动画,我们经常会超出这个值,所以我们作为参考,在闲暇之余往这个方向去追求。


收起阅读 »

SharedFlowBus:30行代码实现消息总线你确定不看吗

前言最近看到很多关于livedata和flow的文章,大家都在学那我肯定不能落后。便去学习一番,偶得SharedFlowBus(卷死你们)。那么正式开始前我们先大概了解下 StateFlow 和 SharedFlowStateFl...
继续阅读 »

前言

最近看到很多关于livedata和flow的文章,大家都在学那我肯定不能落后。便去学习一番,偶得SharedFlowBus(卷死你们)。

那么正式开始前我们先大概了解下 StateFlow 和 SharedFlow

StateFlow

StateFlow 是一个状态容器式可观察数据流,可以向其收集器发出当前状态更新和新状态更新。还可通过其value属性读取当前状态值。

在 Android 中,StateFlow 非常适合需要让可变状态保持可观察的类。

与使用 flow 构建器构建的冷数据流不同,StateFlow 是热数据流:从此类数据流收集数据不会触发任何提供方代码。StateFlow 始终处于活跃状态并存于内存中,而且只有在垃圾回收根中未涉及对它的其他引用时,它才符合垃圾回收条件。

当新使用方开始从数据流中收集数据时,它将接收信息流中的最近一个状态及任何后续状态。您可在 LiveData 等其他可观察类中找到此操作行为。

SharedFlow

SharedFlow 是 StateFlow 的可配置性极高的泛化数据流。您可以使用 SharedFlow 将 tick 信息发送到应用的其余部分,以便让所有内容定期同时刷新。除了获取最新资讯之外,您可能还想要使用用户最喜欢的主题集刷新用户信息部分。

class MainViewModel :  ViewModel() {

private val _sharedFlow = MutableSharedFlow<Int>(0, 1, BufferOverflow.DROP_OLDEST)
val sharedFlow: SharedFlow<Int> = _sharedFlow

init {
viewModelScope.launch {
for (i in 0..10) {
sharedFlow.tryEmit(i)
}
}
}

}

class MainFragment : Fragment() {

private val viewModel: MainViewModel by viewModels()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

CoroutineScope(Dispatchers.Main).launch {
viewModel.sharedFlow.collect {
println(it)
}
}
}

}

您可通过以下方式自定义 SharedFlow 行为:

  • 通过 replay,您可以针对新订阅者重新发送多个之前已发出的值。
  • 通过 onBufferOverflow,您可以指定相关政策来处理缓冲区中已存满要发送的数据项的情况。默认值为 BufferOverflow.SUSPEND,这会使调用方挂起。其他选项包括 DROP_LATEST 或 DROP_OLDEST

MutableSharedFlow 还具有 subscriptionCount 属性,其中包含处于活跃状态的收集器的数量,以便您相应地优化业务逻辑。MutableSharedFlow 还包含一个 resetReplayCache 函数,供您在不想重放已向数据流发送的最新信息的情况下使用。

没错,以上信息摘自 Android Developers ,我真是太能水了,干脆改行写小说得了哈哈哈。

SharedFlowBus的使用

// 发送消息
SharedFlowBus.with(objectKey: Class<T>).tryEmit(value: T)
// 发送粘性消息
SharedFlowBus.withSticky(objectKey: Class<T>).tryEmit(value: T)

// 订阅消息
SharedFlowBus.on(objectKey: Class<T>).observe(owner){ it ->
println(it)
}
// 订阅粘性消息
SharedFlowBus.onSticky(objectKey: Class<T>).observe(owner){ it ->
println(it)
}

通过上面的使用方法可以看出 SharedFlowBus 的优点

  • 使用者不用显示调用反注册方法。
  • 感知生命周期,防止内存泄漏。
  • 实时数据刷新。

SharedFlowBus的实现

object SharedFlowBus {

private var events = ConcurrentHashMap<Any, MutableSharedFlow<Any>>()
private var stickyEvents = ConcurrentHashMap<Any, MutableSharedFlow<Any>>()

fun <T> with(objectKey: Class<T>): MutableSharedFlow<T> {
if (!events.containsKey(objectKey)) {
events[objectKey] = MutableSharedFlow(0, 1, BufferOverflow.DROP_OLDEST)
}
return events[objectKey] as MutableSharedFlow<T>
}

fun <T> withSticky(objectKey: Class<T>): MutableSharedFlow<T> {
if (!stickyEvents.containsKey(objectKey)) {
stickyEvents[objectKey] = MutableSharedFlow(1, 1, BufferOverflow.DROP_OLDEST)
}
return stickyEvents[objectKey] as MutableSharedFlow<T>
}

fun <T> on(objectKey: Class<T>): LiveData<T> {
return with(objectKey).asLiveData()
}

fun <T> onSticky(objectKey: Class<T>): LiveData<T> {
return withSticky(objectKey).asLiveData()
}

}

源码说明

以上就是 SharedFlowBus 的源码,可以直接拷贝到项目中使用。

收起阅读 »

iOS 面试题 八股文 1.3

82.找错题 试题1: void test1() { char string[10]; char* str1 = "0123456789"; strcpy( string, str1 ); } 试题2: void test2() { char string[1...
继续阅读 »


82.找错题
试题1:
void test1()
{
char string[10];
char* str1 = "0123456789";
strcpy( string, str1 );
}
试题2:
void test2()
{
char string[10], str1[10];
int i;
for(i=0; i<10; i++)
{
str1 = 'a';
}
strcpy( string, str1 );
}
试题3:
void test3(char* str1)
{
char string[10];
if( strlen( str1 ) <= 10 )
{
strcpy( string, str1 );
}
}
解答:
试题1字符串str1需要11个字节才能存放下(包括末尾的’\0’),而string只有10个字节的空间,strcpy会导致数组越界;
对试题2,如果面试者指出字符数组str1不能在数组内结束可以给3分;如果面试者指出strcpy(string, str1)调用使得从str1起复制到string内存起所复制的字节数具有不确定性可以给7分,在此基础上指出库函数strcpy工作方式的给10分;
对试题3,if(strlen(str1) <= 10)应改为if(strlen(str1) < 10),因为strlen的结果未统计’\0’所占用的1个字节。
剖析:
考查对基本功的掌握:
(1)字符串以’\0’结尾;
(2)对数组越界把握的敏感度;
(3)库函数strcpy的工作方式,如果编写一个标准strcpy函数的总分值为10,下面给出几个不同得分的答案:
2分
void strcpy( char *strDest, char *strSrc )
{
 while( (*strDest++ = * strSrc++) != ‘\0’ );
}
4分
void strcpy( char *strDest, const char *strSrc ) 
//将源字符串加const,表明其为输入参数,加2分
{
 while( (*strDest++ = * strSrc++) != ‘\0’ );
}
7分
void strcpy(char *strDest, const char *strSrc) 
{
//对源地址和目的地址加非0断言,加3分
assert( (strDest != NULL) && (strSrc != NULL) );
while( (*strDest++ = * strSrc++) != ‘\0’ );
}
10分
//为了实现链式操作,将目的地址返回,加3分!
char * strcpy( char *strDest, const char *strSrc ) 
{
assert( (strDest != NULL) && (strSrc != NULL) );
char *address = strDest; 
while( (*strDest++ = * strSrc++) != ‘\0’ ); 
return address;
}
从2分到10分的几个答案我们可以清楚的看到,小小的strcpy竟然暗藏着这么多玄机,真不是盖的!需要多么扎实的基本功才能写一个完美的strcpy啊!
(4)对strlen的掌握,它没有包括字符串末尾的'\0'。
读者看了不同分值的strcpy版本,应该也可以写出一个10分的strlen函数了,完美的版本为: int strlen( const char *str ) //输入参数const
{
assert( strt != NULL ); //断言字符串地址非0
int len;
while( (*str++) != '\0' ) 

len++; 

return len;
}
试题4:
void GetMemory( char *p )
{
p = (char *) malloc( 100 );
}
void Test( void ) 
{
char *str = NULL;
GetMemory( str ); 
strcpy( str, "hello world" );
printf( str );
}
试题5:
char *GetMemory( void )

char p[] = "hello world"; 
return p; 
}
void Test( void )

char *str = NULL; 
str = GetMemory(); 
printf( str ); 
}
试题6:
void GetMemory( char **p, int num )
{
*p = (char *) malloc( num );
}
void Test( void )
{
char *str = NULL;
GetMemory( &str, 100 );
strcpy( str, "hello" ); 
printf( str ); 
}
试题7:
void Test( void )
{
char *str = (char *) malloc( 100 );
strcpy( str, "hello" );
free( str ); 
... //省略的其它语句
}
解答:
试题4传入中GetMemory( char *p )函数的形参为字符串指针,在函数内部修改形参并不能真正的改变传入形参的值,执行完
char *str = NULL;
GetMemory( str ); 
后的str仍然为NULL;
试题5中
char p[] = "hello world"; 
return p; 
的p[]数组为函数内的局部自动变量,在函数返回后,内存已经被释放。这是许多程序员常犯的错误,其根源在于不理解变量的生存期。
试题6的GetMemory避免了试题4的问题,传入GetMemory的参数为字符串指针的指针,但是在GetMemory中执行申请内存及赋值语句
*p = (char *) malloc( num );
后未判断内存是否申请成功,应加上:
if ( *p == NULL )
{
...//进行申请内存失败处理
}
试题7存在与试题6同样的问题,在执行
char *str = (char *) malloc(100);
后未进行内存是否申请成功的判断;另外,在free(str)后未置str为空,导致可能变成一个“野”指针,应加上:
str = NULL;
试题6的Test函数中也未对malloc的内存进行释放。
剖析:
试题4~7考查面试者对内存操作的理解程度,基本功扎实的面试者一般都能正确的回答其中50~60的错误。但是要完全解答正确,却也绝非易事。
对内存操作的考查主要集中在:
(1)指针的理解;
(2)变量的生存期及作用范围;
(3)良好的动态内存申请和释放习惯。
再看看下面的一段程序有什么错误:
swap( int* p1,int* p2 )
{
int *p;
*p = *p1;
*p1 = *p2;
*p2 = *p;
}
在swap函数中,p是一个“野”指针,有可能指向系统区,导致程序运行的崩溃。在VC++中DEBUG运行时提示错误“Access Violation”。该程序应该改为:
swap( int* p1,int* p2 )
{
int p;
p = *p1;
*p1 = *p2;
*p2 = p;
}[img=12,12]file:///D:/鱼鱼软件/鱼鱼多媒体***本/temp/{56068A28-3D3B-4D8B-9F82-AC1C3E9B128C}_arc_d[1].gif[/img] 3.内功题
试题1:分别给出BOOL,int,float,指针变量 与“零值”比较的 if 语句(假设变量名为var)
解答:
BOOL型变量:if(!var)
int型变量: if(var==0)
float型变量:
const float EPSINON = 0.00001;
if ((x >= - EPSINON) && (x <= EPSINON)
指针变量:  if(var==NULL)
剖析:
考查对0值判断的“内功”,BOOL型变量的0判断完全可以写成if(var==0),而int型变量也可以写成if(!var),指针变量的判断也可以写成if(!var),上述写法虽然程序都能正确运行,但是未能清晰地表达程序的意思。 
一般的,如果想让if判断一个变量的“真”、“假”,应直接使用if(var)、if(!var),表明其为“逻辑”判断;如果用if判断一个数值型变量(short、int、long等),应该用if(var==0),表明是与0进行“数值”上的比较;而判断指针则适宜用if(var==NULL),这是一种很好的编程习惯。
浮点型变量并不精确,所以不可将float变量用“==”或“!=”与数字比较,应该设法转化成“>=”或“<=”形式。如果写成if (x == 0.0),则判为错,得0分。
试题2:以下为Windows NT下的32位C++程序,请计算sizeof的值
void Func ( char str[100] )
{
sizeof( str ) = ?
}
void *p = malloc( 100 );
sizeof ( p ) = ?
解答:
sizeof( str ) = 4
sizeof ( p ) = 4
剖析:
Func ( char str[100] )函数中数组名作为函数形参时,在函数体内,数组名失去了本身的内涵,仅仅只是一个指针;在失去其内涵的同时,它还失去了其常量特性,可以作自增、自减等操作,可以被修改。
数组名的本质如下:
(1)数组名指代一种数据结构,这种数据结构就是数组;
例如:
char str[10];
cout << sizeof(str) << endl;
输出结果为10,str指代数据结构char[10]。
(2)数组名可以转换为指向其指代实体的指针,而且是一个指针常量,不能作自增、自减等操作,不能被修改;
char str[10]; 
str++; //编译出错,提示str不是左值
(3)数组名作为函数形参时,沦为普通指针。
Windows NT 32位平台下,指针的长度(占用内存的大小)为4字节,故sizeof( str ) 、sizeof ( p ) 都为4。
试题3:写一个“标准”宏MIN,这个宏输入两个参数并返回较小的一个。另外,当你写下面的代码时会发生什么事?
least = MIN(*p++, b);
解答:
#define MIN(A,B) ((A) <= (B) ? (A) : (B))
MIN(*p++, b)会产生宏的副作用
剖析:
这个面试题主要考查面试者对宏定义的使用,宏定义可以实现类似于函数的功能,但是它终归不是函数,而宏定义中括弧中的“参数”也不是真的参数,在宏展开的时候对“参数”进行的是一对一的替换。
程序员对宏定义的使用要非常小心,特别要注意两个问题:
(1)谨慎地将宏定义中的“参数”和整个宏用用括弧括起来。所以,严格地讲,下述解答:
#define MIN(A,B) (A) <= (B) ? (A) : (B)
#define MIN(A,B) (A <= B ? A : B )
都应判0分;
(2)防止宏的副作用。
宏定义#define MIN(A,B) ((A) <= (B) ? (A) : (B))对MIN(*p++, b)的作用结果是:
((*p++) <= (b) ? (*p++) : (*p++))
这个表达式会产生副作用,指针p会作三次++自增操作。
除此之外,另一个应该判0分的解答是:
#define MIN(A,B) ((A) <= (B) ? (A) : (B)); 
这个解答在宏定义的后面加“;”,显示编写者对宏的概念模糊不清,只能被无情地判0分并被面试官淘汰。
试题4:为什么标准头文件都有类似以下的结构? 
#ifndef __INCvxWorksh
#define __INCvxWorksh 
#ifdef __cplusplus
extern "C" {
#endif 
/*...*/ 
#ifdef __cplusplus
}
#endif 
#endif /* __INCvxWorksh */
解答:
头文件中的编译宏
#ifndef __INCvxWorksh
#define __INCvxWorksh
#endif 
的作用是防止被重复引用。
作为一种面向对象的语言,C++支持函数重载,而过程式语言C则不支持。函数被C++编译后在symbol库中的名字与C语言的不同。例如,假设某个函数的原型为: 
void foo(int x, int y);
该函数被C编译器编译后在symbol库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字。_foo_int_int这样的名字包含了函数名和函数参数数量及类型信息,C++就是考这种机制来实现函数重载的。
为了实现C和C++的混合编程,C++提供了C连接交换指定符号extern "C"来解决名字匹配问题,函数声明前加上extern "C"后,则编译器就会按照C语言的方式将该函数编译为_foo,这样C语言中就可以调用C++的函数了。[img=12,12]file:///D:/鱼鱼软件/鱼鱼多媒体***本/temp/{C74A38C4-432E-4799-B54D-73E2CD3C5206}_arc_d[1].gif[/img] 
试题5:编写一个函数,作用是把一个char组成的字符串循环右移n个。比如原来是“abcdefghi”如果n=2,移位后应该是“hiabcdefgh” 
函数头是这样的:
//pStr是指向以'\0'结尾的字符串的指针
//steps是要求移动的n
void LoopMove ( char * pStr, int steps )
{
//请填充...
}
解答:
正确解答1:
void LoopMove ( char *pStr, int steps )
{
int n = strlen( pStr ) - steps;
char tmp[MAX_LEN]; 
strcpy ( tmp, pStr + n ); 
strcpy ( tmp + steps, pStr); 
*( tmp + strlen ( pStr ) ) = '\0';
strcpy( pStr, tmp );
}
正确解答2:
void LoopMove ( char *pStr, int steps )
{
int n = strlen( pStr ) - steps;
char tmp[MAX_LEN]; 
memcpy( tmp, pStr + n, steps ); 
memcpy(pStr + steps, pStr, n ); 
memcpy(pStr, tmp, steps ); 
}
剖析:
这个试题主要考查面试者对标准库函数的熟练程度,在需要的时候引用库函数可以很大程度上简化程序编写的工作量。
最频繁被使用的库函数包括:
(1) strcpy
(2) memcpy
(3) memset

收起阅读 »

iOS 面试题 八股文 1.2

12 怎样防止指针的越界使用问题?    必须让指针指向一个有效的内存地址,  1 防止数组越界  2 防止向一块内存中拷贝过多的内容  3 防止使用空...
继续阅读 »


12 怎样防止指针的越界使用问题? 

  必须让指针指向一个有效的内存地址, 

1 防止数组越界 

2 防止向一块内存中拷贝过多的内容 

3 防止使用空指针 

4 防止改变const修改的指针 

5 防止改变指向静态存储区的内容 

6 防止两次释放一个指针 

7 防止使用野指针. 

 

 

13 指针的类型转换? 

指针转换通常是指针类型和void * 类型之前进行强制转换,从而与期望或返回void指针的函数进行正确的交接. 

63static有什么用途?(请至少说明两种)
            1.限制变量的作用域
            2.设置变量的存储域
            7. 引用与指针有什么区别?
            1) 引用必须被初始化,指针不必。
            2) 引用初始化以后不能被改变,指针可以改变所指的对象。
            2) 不存在指向空值的引用,但是存在指向空值的指针。
            8. 描述实时系统的基本特性
            在特定时间内完成特定的任务,实时性与可靠性

64全局变量和局部变量在内存中是否有区别?如果有,是什么区别?
            全局变量储存在静态数据库,局部变量在堆栈
            10. 什么是平衡二叉树
            左右子树都是平衡二叉树且左右子树的深度差值的绝对值不大于1

65堆栈溢出一般是由什么原因导致的?
            没有回收垃圾资源
            12. 什么函数不能声明为虚函数?
            constructor
            13. 冒泡排序算法的时间复杂度是什么?
            O(n^2)
            14. 写出float x 与“零值”比较的if语句。
            if(x>0.000001&&x<-0.000001)
            16. Internet采用哪种网络协议?该协议的主要层次结构?
            tcp/ip 应用层/传输层/网络层/数据链路层/物理层
            17. Internet物理地址和IP地址转换采用什么协议?
            ARP (Address Resolution Protocol)(地址解析協議)
            18.IP地址的编码分为哪俩部分?
            IP地址由两部分组成,网络号和主机号。不过是要和“子网掩码”按位与上之后才能区
            分哪些是网络位哪些是主机位。
            2.用户输入M,N值,从1至N开始顺序循环数数,每数到M输出该数值,直至全部输出。写
            出C程序。
            循环链表,用取余操作做
            3.不能做switch()的参数类型是:
            switch的参数不能为实型。
            華為
            1、局部变量能否和全局变量重名?
            答:能,局部会屏蔽全局。要用全局变量,需要使用"::"
            局部变量可以与全局变量同名,在函数内引用这个变量时,会用到同名的局部变量,而
            不会用到全局变量。对于有些编译器而言,在同一个函数内可以定义多个同名的局部变
            量,比如在两个循环体内都定义一个同名的局部变量,而那个局部变量的作用域就在那
            个循环体内
            2、如何引用一个已经定义过的全局变量?
            答:extern
            可以用引用头文件的方式,也可以用extern关键字,如果用引用头文件方式来引用某个
            在头文件中声明的全局变理,假定你将那个变写错了,那么在编译期间会报错,如果你
            用extern方式引用时,假定你犯了同样的错误,那么在编译期间不会报错,而在连接期
            间报错
            3、全局变量可不可以定义在可被多个.C文件包含的头文件中?为什么?
            答:可以,在不同的C文件中以static形式来声明同名全局变量。
            可以在不同的C文件中声明同名的全局变量,前提是其中只能有一个C文件中对此变量赋
            初值,此时连接不会出错
            4、语句for( ;1 ;)有什么问题?它是什么意思?
            答:和while(1)相同。
            5、do……while和while……do有什么区别?
            答:前一个循环一遍再判断,后一个判断以后再循环

661.IP Phone的原理是什么?
            IPV6
            2.TCP/IP通信建立的过程怎样,端口有什么作用?
            三次握手,确定是哪个应用程序使用该协议
            3.1号信令和7号信令有什么区别,我国某前广泛使用的是那一种?
            4.列举5种以上的电话新业务?
            微软亚洲技术中心的面试题!!!
            1.进程和线程的差别。
            线程是指进程内的一个执行单元,也是进程内的可调度实体.
            与进程的区别:
            (1)调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位
            (2)并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行
            (3)拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属
            于进程的资源.
            (4)系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开
            销明显大于创建或撤消线程时的开销。
            2.测试方法
            人工测试:个人复查、抽查和会审
            机器测试:黑盒测试和白盒测试
            2.Heap与stack的差别。
            Heap是堆,stack是栈。
            Stack的空间由操作系统自动分配/释放,Heap上的空间手动分配/释放。
            Stack空间有限,Heap是很大的自由存储区
            C中的malloc函数分配的内存空间即在堆上,C++中对应的是new操作符。
            程序在编译期对变量和函数分配内存都在栈上进行,且程序运行过程中函数调用时参数的
            传递也在栈上进行
            3.Windows下的内存是如何管理的?
            4.介绍.Net和.Net的安全性。
            5.客户端如何访问.Net组件实现Web Service?
            6.C/C++编译器中虚表是如何完成的?
            7.谈谈COM的线程模型。然后讨论进程内/外组件的差别。
            8.谈谈IA32下的分页机制
            小页(4K)两级分页模式,大页(4M)一级
            9.给两个变量,如何找出一个带环单链表中是什么地方出现环的?
            一个递增一,一个递增二,他们指向同一个接点时就是环出现的地方
            10.在IA32中一共有多少种办法从用户态跳到内核态?
            通过调用门,从ring3到ring0,中断从ring3到ring0,进入vm86等等
            11.如果只想让程序有一个实例运行,不能运行两个。像winamp一样,只能开一个窗
            口,怎样实现?
            用内存映射或全局原子(互斥变量)、查找窗口句柄..
            FindWindow,互斥,写标志到文件或注册表,共享内存。

67如何截取键盘的响应,让所有的‘a’变成‘b’?

            键盘钩子SetWindowsHookEx
            13.Apartment在COM中有什么用?为什么要引入?
            14.存储过程是什么?有什么用?有什么优点?
            我的理解就是一堆sql的集合,可以建立非常复杂的查询,编译运行,所以运行一次后,
            以后再运行速度比单独执行SQL快很多
            15.Template有什么特点?什么时候用?
            16.谈谈Windows DNA结构的特点和优点。
            网络编程中设计并发服务器,使用多进程与多线程,请问有什么区别?
            1,进程:子进程是父进程的复制品。子进程获得父进程数据空间、堆和栈的复制品。
            2,线程:相对与进程而言,线程是一个更加接近与执行体的概念,它可以与同进程的其
            他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。
            两者都可以提高程序的并发度,提高程序运行效率和响应时间。
            线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源管理和保护;而进程
            正相反。同时,线程适合于在SMP机器上运行,而进程则可以跨机器迁移。
            思科

收起阅读 »

iOS 面试题 八股文 1.1

54多线程 多线程编程是防止主线程堵塞,增加运行效率等等的最佳方法。而原始的多线程方法存在很多的毛病,包括线程锁死等。在Cocoa中,Apple提供了NSOperation这个类,提供了一个优秀的多线程编程方法。 本次介绍NSOperation的子集,简易...
继续阅读 »


54多线程

多线程编程是防止主线程堵塞,增加运行效率等等的最佳方法。而原始的多线程方法存在很多的毛病,包括线程锁死等。在Cocoa中,Apple提供了NSOperation这个类,提供了一个优秀的多线程编程方法。

本次介绍NSOperation的子集,简易方法的NSInvocationOperation:

 

一个NSOperationQueue 操作队列,就相当于一个线程管理器,而非一个线程。因为你可以设置这个线程管理器内可以并行运行的的线程数量等等

55oc语法里的@perpoerty不用写@synzhesize了,自动填充了。并且的_name;

写方法时候不用提前声明。llvm 全局方法便利。

枚举类型。enum hello:Integer{  } 冒号后面直接可以跟类型,以前是:

enum hello{} 后面在指定为Integer .

桥接。ARC 自动release retain 的时候 CFString CFArray . Core Fountion. 加上桥接_brige  才能区分CFString 和NSString 而现在自动区分了,叫固定桥接。

 

下拉刷新封装好了。

UICollectionViewController. 可以把表格分成多列。

 

Social Framework(社交集成)

UIActivityViewController来询问用户的社交行为

 

缓存:就是存放在临时文件里,比如新浪微博请求的数据,和图片,下次请求看这里有没有值。

56Singleton(单例模式),也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。 

代码如下: 

static ClassA *classA = nil;//静态的该类的实例 

+ (ClassA *)sharedManager 

{ 

@synchronized(self) { 

if (!classA) { 

classA = [[super allocWithZone:NULL]init]; 

return classA; 

} 

+ (id)allocWithZone:(NSZone *)zone { 

return [[self sharedManager] retain]; 

- (id)copyWithZone:(NSZone *)zone { 

return self; 

- (id)retain { 

return self; 

- (NSUIntger)retainCount { 

return NSUIntgerMax; 

- (oneway void)release { 

- (id)autorelease { 

return self; 

-(void)dealloc{ 

57请写一个C函数,若处理器是Big_endian的,则返回0;若是Little_endian的,则返回1 int checkCPU( ) {   

     {           

       union w      

            {        

                     int a;      

                     char b;         

             } c;             

            c.a = 1;    

        return  (c.b ==1);      

  } 

剖析:嵌入式系统开发者应该对Little-endian和Big-endian模式非常了解。采用Little-endian模式的CPU对操作数的存放方式是从低字节到高字节, Big-endian  模式的CPU对操作数的存放方式是从高字节到低字节。在弄清楚这个之前要弄清楚这个问题:字节从右到坐为从高到低! 假设从地址0x4000开始存放: 0x12345678,是也个32位四个字节的数据,最高字节是0x12,最低字节是0x78:在Little-endian模式CPU内存中的存放方式为: (高字节在高地址,低字节在低地址) 

内存地址0x4000 0x4001 0x4002 0x4003 

存放内容 0x78 0x56 0x34 0x12 

大端机则相反。 

 

有的处理器系统采用了小端方式进行数据存放,如Intel的奔腾。有的处理器系统采用了大端方式进行数据存放,如IBM半导体和Freescale的PowerPC处理器。不仅对于处理器,一些外设的设计中也存在着使用大端或者小端进行数据存放的选择。因此在一个处理器系统中,有可能存在大端和小端模式同时存在的现象。这一现象为系统的软硬件设计带来了不小的麻烦,这要求系统设计工程师,必须深入理解大端和小端模式的差别。大端与小端模式的差别体现在一个处理器的寄存器,指令集,系统总线等各个层次中。   联合体union的存放顺序是所有成员都从低地址开始存放的。以上是网上的原文。让我们看看在ARM处理器上union是如何存储的呢?   地址A ---------------- |A     |A+1   |A+2   |A+3    |int a; |      |         |         |          -------------------- |A     |char b; |      | ---------                                                                            如果是小端如何存储c.a的呢?  

                                         地址A ----------- 

------------------- |A    |A+1   |A+2    |A+3 | int a; 

|0x01 |0x00   |0x00   |0x00 | ------------------------------------- |A    |char b; |     | ---------                                  

                                如果是大端如何存储c.a的呢?   

  地址A --------------------- 

--------- |A      |A+1    |A+2     |A+3     |int a; |0x00   |0x00   |0x00    |0x01    | ------------------------------------------ |A      |char b; |       | ---------                                                                                                                                                        现在知道为什么c.b==0的话是大端,c.b==1的话就是小端了吧。

58

堆和栈上的指针 

指针所指向的这块内存是在哪里分配的,在堆上称为堆上的指针,在栈上为栈上的指针. 

在堆上的指针,可以保存在全局数据结构中,供不同函数使用访问同一块内存. 

在栈上的指针,在函数退出后,该内存即不可访问. 

59什么是指针的释放? 

具体来说包括两个概念. 

1 释放该指针指向的内存,只有堆上的内存才需要我们手工释放,栈上不需要. 

2 将该指针重定向为NULL. 

60数据结构中的指针? 

其实就是指向一块内存的地址,通过指针传递,可实现复杂的内存访问. 

7 函数指针? 

指向一块函数的入口地址. 

 

8 指针作为函数的参数? 

比如指向一个复杂数据结构的指针作为函数变量 

这种方法避免整个复杂数据类型内存的压栈出栈操作,提高效率. 

注意:指针本身不可变,但指针指向的数据结构可以改变. 

 

9 指向指针的指针? 

指针指向的变量是一个指针,即具体内容为一个指针的值,是一个地址. 

此时指针指向的变量长度也是4位. 

61指针与地址的区别? 

区别: 

1指针意味着已经有一个指针变量存在,他的值是一个地址,指针变量本身也存放在一个长度为四个字节的地址当中,而地址概念本身并不代表有任何变量存在. 

2 指针的值,如果没有限制,通常是可以变化的,也可以指向另外一个地址. 

   地址表示内存空间的一个位置点,他是用来赋给指针的,地址本身是没有大小概念,指针指向变量的大小,取决于地址后面存放的变量类型. 

62指针与数组名的关系? 





































































































  其值都是一个地址,但前者是可以移动的,后者是不可变的. 

收起阅读 »

iOS HTTP协议详解

HTTP是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。目前在WWW中使用的是HTTP/1.0的第六版,HTTP/1.1的规范化工作正在进行之中。  http(超文本传输协议)是一个基于请求与响应模式的、无状态...
继续阅读 »


HTTP是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。目前在WWW中使用的是HTTP/1.0的第六版,HTTP/1.1的规范化工作正在进行之中。

 http(超文本传输协议)是一个基于请求与响应模式的、无状态的、应用层的协议,常基于TCP的连接方式,HTTP1.1版本中给出一种持续连接的机制,绝大多数的Web开发,都是构建在HTTP协议之上的Web应用。
HTTP协议的主要特点可概括如下:
1.支持客户/服务器模式。
2.简单快速:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有GET、HEAD、POST。每种方法规定了客户与服务器联系的类型不同。由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快。
3.灵活:HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记。
4.无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
5.无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。

48URL

HTTP URL (URL是一种特殊类型的URI是他的子类,包含了用于查找某个资源的足够的信息)的格式如下:
http://host[":"port][abs_path ]
http表示要通过HTTP协议来定位网络资源;host表示合法的Internet主机域名或者IP地址;port指定一个端口号,为空则使用缺省端口80;abs_path指定请求资源的URI;如果URL中没有给出abs_path,那么当它作为请求URI时,必须以“/”的形式给出,通常这个工作浏览器自动帮我们完成。

49TCP/UDP区别联系

TCP---传输控制协议,提供的是面向连接、可靠的字节流服务。当客户和服务器彼此交换数据前,必须先在双方之间建立一个TCP连接,之后才能传输数据。TCP提供超时重发,丢弃重复数据,检验数据,流量控制等功能,保证数据能从一端传到另一端。 

UDP---用户数据报协议,是一个简单的面向数据报的运输层协议。UDP不提供可靠性,它只是把应用程序传给IP层的数据报发送出去,但是并不能保证它们能到达目的地。由于UDP在传输数据报前不用在客户和服务器之间建立一个连接,且没有超时重发等机制,故而传输速度很快 

TCP(Transmission Control Protocol,传输控制协议)是基于连接的协议,也就是说,在正式收发数据前,必须和对方建立可靠的连接。一个TCP连接必须要经过三次“对话”才能建立起来,我们来看看这三次对话的简单过程:1.主机A向主机B发出连接请求数据包;2.主机B向主机A发送同意连接和要求同步(同步就是两台主机一个在发送,一个在接收,协调工作)的数据包;3.主机A再发出一个数据包确认主机B的要求同步:“我现在就发,你接着吧!”,这是第三次对话。三次“对话”的目的是使数据包的发送和接收同步,经过三次“对话”之后,主机A才向主机B正式发送数据。 

UDP(User Data Protocol,用户数据报协议)是与TCP相对应的协议。它是面向非连接的协议,它不与对方建立连接,而是直接就把数据包发送过去!  UDP适用于一次只传送少量数据、对可靠性要求不高的应用环境。 

tcp协议和udp协议的差别 

是否连接面向连接面向非连接 

传输可靠性可靠不可靠 

应用场合传输大量数据少量数据 

速度慢快

50 socket 连接和 http 连接的区别

简单说,你浏览的网页(网址以http://开头)都是http协议传输到你的浏览器的, 而http是基于socket之上的。socket是一套完成tcp,udp协议的接口。

HTTP协议:简单对象访问协议,对应于应用层  ,HTTP协议是基于TCP连接的

tcp协议:    对应于传输层

ip协议:     对应于网络层 
TCP/IP是传输层协议,主要解决数据如何在网络中传输;而HTTP是应用层协议,主要解决如何包装数据。

Socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket,我们才能使用TCP/IP协议。

http连接:http连接就是所谓的短连接,即客户端向服务器端发送一次请求,服务器端响应后连接即会断掉;

socket连接:socket连接就是所谓的长连接,理论上客户端和服务器端一旦建立起连接将不会主动断掉;但是由于各种环境因素可能会是连接断开,比如说:服务器端或客户端主机down了,网络故障,或者两者之间长时间没有数据传输,网络防火墙可能会断开该连接以释放网络资源。所以当一个socket连接中没有数据的传输,那么为了维持连接需要发送心跳消息~~具体心跳消息格式是开发者自己定义的

我们已经知道网络中的进程是通过socket来通信的,那什么是socket呢?socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。我的理解就是Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭),这些函数我们在后面进行介绍。我们在传输数据时,可以只使用(传输层)TCP/IP协议,但是那样的话,如果没有应用层,便无法识别数据内容,如果想要使传输的数据有意义,则必须使用到应用层协议,应用层协议有很多,比如HTTP、FTP、TELNET等,也可以自己定义应用层协议。WEB使用HTTP协议作应用层协议,以封装HTTP文本信息,然后使用TCP/IP做传输层协议将它发到网络上。
1)Socket是一个针对TCP和UDP编程的接口,你可以借助它建立TCP连接等等。而TCP和UDP协议属于传输层 。
  而http是个应用层的协议,它实际上也建立在TCP协议之上。 

 (HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。)

 2)Socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket,我们才能使用TCP/IP协议。Socket的出现只是使得程序员更方便地使用TCP/IP协议栈而已,是对TCP/IP协议的抽象,从而形成了我们知道的一些最基本的函数接口。

51 什么是 TCP 连接的三次握手

第一次握手:客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。

握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。理想状态下,TCP连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP 连接都将被一直保持下去。断开连接时服务器和客户端均可以主动发起断开TCP连接的请求,断开过程需要经过“四次握手”(过程就不细写了,就是服务器和客户端交互,最终确定断开)

52 利用 Socket 建立网络连接的步骤

建立Socket连接至少需要一对套接字,其中一个运行于客户端,称为ClientSocket ,另一个运行于服务器端,称为ServerSocket 。

套接字之间的连接过程分为三个步骤:服务器监听,客户端请求,连接确认。

1。服务器监听:服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态,等待客户端的连接请求。

2。客户端请求:指客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。

3。连接确认:当服务器端套接字监听到或者说接收到客户端套接字的连接请求时,就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,双方就正式建立连接。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。

53进程与线程

进程(process)是一块包含了某些资源的内存区域。操作系统利用进程把它的工作划分为一些功能单元。

进程中所包含的一个或多个执行单元称为线程(thread)。进程还拥有一个私有的虚拟地址空间,该空间仅能被它所包含的线程访问。

通常在一个进程中可以包含若干个线程,它们可以利用进程所拥有的资源。

在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位。

由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统内多个程序间并发执行的程度。

简而言之 , 一个程序至少有一个进程 , 一个进程至少有一个线程 .一个程序就是一个进程,而一个程序中的多个任务则被称为线程。

 线程只能归属于一个进程并且它只能访问该进程所拥有的资源。当操作系统创建一个进程后,该进程会自动申请一个名为主线程或首要线程的线程。应用程序(application)是由一个或多个相互协作的进程组成的。

另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位.

线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.
一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行.

收起阅读 »

面试官:你给我说一下线程池里面的几把锁。

你好呀,我是歪歪。 最近有个读者给我说,面试聊到线程池的时候,相谈甚欢,基本都回答上来了,但是其中有一个问题直接把他干懵逼了。 面试官问他:你说一下线程池里面的锁吧。 结果他关于线程池的知识点其实都是在各个博客或者面经里面看到的,没有自己去翻阅过源码,也就根本...
继续阅读 »

你好呀,我是歪歪。


最近有个读者给我说,面试聊到线程池的时候,相谈甚欢,基本都回答上来了,但是其中有一个问题直接把他干懵逼了。


面试官问他:你说一下线程池里面的锁吧。


结果他关于线程池的知识点其实都是在各个博客或者面经里面看到的,没有自己去翻阅过源码,也就根本就没有注意过线程池里面还有锁的存在。


他还给我抱怨:



他这么一说,我也觉得,好像大家聊到线程池的时候,都没有怎么聊到里面用到的锁。


确实是存在感非常低。


要不我就安排一下?



mainLock


其实线程池里面用到锁的地方还是非常的多的。


比如我之前说过,线程池里面有个叫做 workers 的变量,它存放的东西,可以理解为线程池里面的线程。


而这个对象的数据结构是 HashSet。


HashSet 不是一个线程安全的集合类,这你知道吧?


所以,你去看它上面的注释是怎么说的:



当持有 mainLock 这个玩意的时候,才能被访问。


就算我不介绍,你看名字也能感觉的到:如果没有猜测的话,那么 mainLock 应该是一把锁。


到底是不是呢,如果是的话,它又是个什么样子的锁呢?



在源码中 mainLock 这个变量,就在 workers 的正上方:



原来它的真身就是一个 ReentrantLock。


用一个 ReentrantLock 来保护一个 HashSet,完全没毛病。


那么 ReentrantLock 和 workers 到底是怎么打配合的呢?


我们还是拿最关键的 addWorker 方法来说:



用到锁了,那么必然是有什么东西需要被被独占起来的。


你再看看,你加锁独占了某个共享资源,你是想干什么?


绝大部分情况下,肯定是想要改变它,往里面塞东西,对不对?


所以你就按照这个思路分析,addWorker 中被锁包裹起来的这段代码,它到底在独占什么东西?


其实都不用分析了,这里面的共享数据一共就两个。两个都需要进行写入操作,这两共享数据,一个是workers 对象,一个是 largestPoolSize 变量。


workers 我们前面说了,它的数据结构是线程不安全的 HashSet。


largestPoolSize 是个啥玩意,它为什么要被锁起来?



这个字段是用来记录线程池中,曾经出现过的最大线程数。


包括读取这个值的时候也是加了 mianLock 锁的:



其实我个人觉得这个地方用 volatile 修饰一下 largestPoolSize 变量,就可以省去 mainLock 的上锁操作。


同样也是线程安全的。


不知道你是不是也是这样觉得的?


如果你也是这样想的话,不好意思,你想错了。


在线程池里面其他的很多字段都用到了 volatile:



为什么 largestPoolSize 不用呢?


你再看一下前面 getLargestPoolSize 方法获取值的地方。


如果修改为 volatile,不上锁,就少了一个 mainLock.lock() 的操作。


去掉这个操作,就有可能少了一个阻塞等待的操作。


假设 addWorkers 方法还没来得及修改 largestPoolSize 的值,就有线程调用了 getLargestPoolSize 方法。


由于没阻塞,直接获取到的值,只是那一瞬间的 largestPoolSize,不是一定是 addWorker 方法执行完成后的


加上阻塞,程序是能感知到 largestPoolSize 有可能正在发生变化,所以获取到的一定是 addWorker 方法执行完成后的 largestPoolSize。


所以我理解加锁,是为了最大程度上保证这个参数的准确性。


除了前面说的几个地方外,还是有很多 mainLock 使用的地方:



我就不一一介绍了,你得自己去翻一翻,这玩意介绍起来也没啥意思,都是一眼就能瞟明白的代码。


说个有意思的。


你有没有想过这里 Doug Lea 老爷子为什么用了线程不安全的 HashSet,配合 ReentrantLock 来实现线程安全呢?


为什么不直接搞一个线程安全的 Set 集合,比如用这个玩意 Collections.synchronizedSet?


答案其实在前面已经出现过了,只是我没有特意说,大家没有注意到。


就在 mainLock 的注释上写着:



我捡关键的地方给你说一下。


首先看这句:



While we could use a concurrent set of some sort, it turns out to be generally preferable to use a lock.



这句话是个倒装句,应该没啥生词,大家都认识。


其中有个 it turns out to be,可以介绍一下,这是个短语,经常出现在美剧里面的对白。


翻译过来就是四个字“事实证明”。


所以,上面这整句话就是这样的:虽然我们可以使用某种并发安全的 set 集合,但是事实证明,一般来说,使用锁还是比较好的。


接下来老爷子就要解释为什么用锁比较好了。


我翻译上这句话的意思就是我没有乱说,都是有根据的,因为这是老爷子亲自解释的为什么他不用线程安全的 Set 集合。


第一个原因是这样说的:



Among the reasons is that this serializes interruptIdleWorkers, which avoids unnecessary interrupt storms, especially during shutdown. Otherwise exiting threads would concurrently interrupt those that have not yet interrupted.



英文是的,我翻译成中文,加上自己的理解是这样的。


首先第一句里面有个 “serializes interruptIdleWorkers”,这两个单词组合在一起还是有一定的迷惑性的。


serializes 在这里,并不是指我们 Java 中的序列化操作,而是需要翻译为“串行化”。


interruptIdleWorkers,这玩意根本就不是一个单词,这是线程池里面的一个方法:



在这个方法里面进来第一件事就是拿 mainLock 锁,然后尝试去做中断线程的操作。


由于有 mainLock.lock 的存在,所以多个线程调用这个方法,就被 serializes 串行化了起来。


串行化起来的好处是什么呢?


就是后面接着说的:避免了不必要的中断风暴(interrupt storms),尤其是调用 shutdown 方法的时候,避免退出的线程再次中断那些尚未中断的线程。


为什么这里特意提到了 shutdown 方法呢?


因为 shutdown 方法调用了 interruptIdleWorkers:



所以上面啥意思呢?


这个地方就要用一个反证法了。


假设我们使用的是并发安全的 Set 集合,不用 mainLock。


这个时候有 5 个线程都来调用 shutdown 方法,由于没有用 mainLock ,所以没有阻塞,那么每一个线程都会运行 interruptIdleWorkers。


所以,就会出现第一个线程发起了中断,导致 worker ,即线程正在中断中。第二个线程又来发起中断了,于是再次对正在中断中的中断发起中断。


额,有点像是绕口令了。


所以我打算重复一遍:对正在中断中的中断,发起中断。


因此,这里用锁是为了避免中断风暴(interrupt storms)的风险。


并发的时候,只想要有一个线程能发起中断的操作,所以锁是必须要有的。有了锁这个大前提后,反正 Set 集合也会被锁起来,索性就不需要并发安全的 Set 了。


所以我理解,在这里用 mainLock 来实现串行化,同时保证了 Set 集合不会出现并发访问的情况。


只要保证这个这个 Set 操作的时候都是被锁包裹起来的就行,因此,不需要并发安全的 Set 集合。


即注释上写的:Accessed only under mainLock.


记住了,有可能会被考哦。


然后,老爷子说的第二个原因:



It also simplifies some of the associated statistics bookkeeping of largestPoolSize etc.



这句话就是说的关于加锁好维护 largestPoolSize 这个参数,不再贅述了。


哦,对了,这是有个 etc,表示“诸如此类”的意思。


这个 etc 指的就是这个 completedTaskCount 参数,道理是一样的:



另一把锁


除了前面说的 mainLock 外,线程池里面其实还有一把经常被大家忽略的锁。


那就是 Worker 对象。



可以看到 Worker 是继承自 AQS 对象的,它的很多方法也是和锁相关的。



同时它也实现了 Runnable 方法,所以说到底它就是一个被封装起来的线程,用来运行提交到线程池里面的任务,当没有任务的时候就去队列里面 take 或者 poll 等着,命不好的就被回收了。


我们还是看一下它加锁的地方,就在很关键的 runWorker 方法里面:



java.util.concurrent.ThreadPoolExecutor#runWorker




那么问题就来了:


这里是线程池里面的线程,正在执行提交的任务的逻辑的地方,为什么需要加锁呢?


这里为什么又自己搞了一个锁,而不用已有的 ReentrantLock ,即 mainLock 呢?


答案还是写在注释里面:



我知道你看着这么大一段英文瞬间就没有了兴趣。


但是别慌,我带你细嚼慢咽。


第一句话就开门见山的说了:



Class Worker mainly maintains interrupt control state for threads running tasks.



worker 类存在的主要意义就是为了维护线程的中断状态。


维护的线程也不是一般的线程,是 running tasks 的线程,也就是正在运行的线程。


怎么理解这个“维护线程的中断状态”呢?


你去看 Worker 类的 lock 和 tryLock 方法,都各自只有一个地方调用。


lock 方法我们前面说了,在 runWorker 方法里面调用了。


在 tryLock 方法是在这里调用的:



这个方法也是我们的老朋友了,前面刚刚才讲过,是用来中断线程的。


中断的是什么类型的线程呢?



就是正在等待任务的线程,即在这里等着的线程:



java.util.concurrent.ThreadPoolExecutor#getTask




换句话说:正在执行任务的线程是不应该被中断的。


那线程池怎么知道那哪任务是正在执行中的,不应该被中断呢?


我们看一下判断条件:



关键的条件其实就是 w.tryLock() 方法。


所以看一下 tryLock 方法里面的核心逻辑是怎么样的:



核心逻辑就是一个 CAS 操作,把某个状态从 0 更新为 1,如果成功了,就是 tryLock 成功。


“0”、“1” 分别是什么玩意呢?


注释,答案还是在注释里面:



所以,tryLock 中的核心逻辑compareAndSetState(0, 1),就是一个上锁的操作。


如果 tryLock 失败了,会是什么原因呢?


肯定是此时的状态已经是 1 了。


那么状态什么时候变成 1 呢?


一个时机就是执行 lock 方法的时候,它也会调用 tryAcquire 方法。


那 lock 是在什么时候上锁的呢?


runWorker 方法里面,获取到 task,准备执行的时候。


也就是说状态为 1 的 worker 肯定就是正在执行任务的线程,不可以被中断。


另外,状态的初始值被设置为 -1。



我们可以写个简单的代码,验证一下上面的三个状态:



首先我们定义一个线程池,然后调用 prestartAllCoreThreads 方法把所有线程都预热起来,让它们处于等待接收任务的状态。


你说这个时候,三个 worker 的状态分别是什么?



那必须得是 0 ,未上锁的状态。


当然了,你也有可能看到这样的局面:



-1 是从哪里来的呢?


别慌,我等下给你讲,我们先看看 1 在哪呢?


按照之前的分析,我们只需要往线程池里面提交一个任务即可:



这个时候,假如我们调用 shutdown 呢,会发什么?


当然是中断空闲的线程了。


那正在执行任务的这个线程怎么办呢?


因为是个 while 循环,等到任务执行完成后,会再次调用 getTask 方法:



getTask 方法里面会先判断线程池状态,这个时候就能感知到线程池关闭了,返回 null,这个 worker 也就默默的退出了。



好了,前面说了这么多,你只要记住一个大前提:自定义 worker 类的大前提是为了维护中断状态,因为正在执行任务的线程是不应该被中断的。


接着往下看注释:



We implement a simple non-reentrant mutual exclusion lock rather than use ReentrantLock because we do not want worker tasks to be able to reacquire the lock when they invoke pool control methods like setCorePoolSize.



这里解释了为什么老爷子不用 ReentrantLock 而是选择了自己搞一个 worker 类。


因为他想要的是一个不能重入的互斥锁,而 ReentrantLock 是可以重入的。


从前面分析的这个方法也能看出来,是一个非重入的方法:



传进来的参数根本没有使用,代码里面也没有累加的逻辑。


如果你还没反应过来是怎么回事的话,我给你看一下 ReentrantLock 里面的重入逻辑:



你看到了吗,有一个累加的过程。


释放锁的时候,又有一个与之对应的递减的过程,减到 0 就是当前线程释放锁成功:



而上面的累加、递减的逻辑在 worker 类里面通通是没有的。


那么问题又来了:如果是可以重入的,会发生什么呢?


目的还是很前面一样:不想打断正在执行任务的线程。


同时注释里面提到了一个方法:setCorePoolSize。


你说巧不巧,这个方法我之前写线程池动态调整的时候重点讲过呀:



可惜当时主要讲 delta>0 里面的的逻辑去了。


现在我们看一下我框起来的地方。


workerCountOf(ctl.get()) > corePoolSize 为 true 说明什么情况?


说明当前的 worker 的数量是多于我要重新设置的 corePoolSize,需要减少一点。


怎么减少呢?


调用 interruptIdleWorkers 方法。


这个方法我们前面刚刚分析了,我再拿出来一起看一下:



里面有个 tryLock,如果是可以重入的,会发生什么情况?


是不是有可能把正在执行的 worker 给中断了。


这合适吗?



好了,注释上的最后一句话:



Additionally, to suppress interrupts until the thread actually starts running tasks, we initialize lock state to a negative value, and clear it upon start (in runWorker).



这句话就是说为了在线程真正开始运行任务之前,抑制中断。所以把 worker 的状态初始化为负数(-1)。


大家要注意这个:and clear it upon start (in runWorker).


在启动的时候清除 it,这个 it 就是值为负数的状态。


老爷子很贴心,把方法都给你指明了:in runWorker.


所以你去看 runWorker,你就知道为什么这里上来先进行一个 unLock 操作,后面跟着一个 allow interrupts 的注释:



因为在这个地方,worker 的状态可能还是 -1 呢,所以先 unLock,把状态刷到 0 去。


同时也就解释了前面我没有解释的 -1 是哪里来的:



想明白了吗,-1 是哪里来的?


肯定是在启动过程中,执行了 workers.add 方法,但是还没有来得及执行 runWorker 方法的 worker 对象,它们的状态就是 -1。



最后说一句


好了,看到了这里了,点赞安排一个吧。写文章很累的,需要一点正反馈。


给各位读者朋友们磕一个了:



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

还在频繁定义常量?不试试用枚举代替

1、简介 不知道大家有没有在自己项目中看到过类似下面这样的代码: public static void fruitsHandle(String fruits) { switch (fruits) { case "Apple": ...
继续阅读 »

1、简介


不知道大家有没有在自己项目中看到过类似下面这样的代码:


public static void fruitsHandle(String fruits) {

switch (fruits) {
case "Apple":
// TODO
break;
case "Banana":
// TODO
break;
case "Orange":
// TODO
break;
default:
throw new IllegalStateException("Unexpected value: " + fruits);
}

}

出现上面这种情况是非常少的,小萌新一般也不会直接在方法中重复定义字符串进行比较,而会将其定义为常量,或者统一抽取为常量类。所以一般会看到这种代码(小捌经常在项目中看到类似这样的代码,但是小捌不敢吭声😄😄):


private static final String APPLE = "Apple";
private static final String BANANA = "Banana";
private static final String ORANGE = "Orange";

public static void fruitsHandle(String fruits) {

switch (fruits) {
case APPLE:
// TODO
break;
case BANANA:
// TODO
break;
case ORANGE:
// TODO
break;
default:
throw new IllegalStateException("Unexpected value: " + fruits);
}

}

上面这种情况我们在代码中出现的频率非常高;它需要程序员提供一组固定常量,并且这一组固定常量在开发时或者说编译时就知道了具体的成员,这个时候我们就应该使用枚举。


枚举类型(enum type)是指由一组固定常量组成合法值的类型。




2、优势


使用枚举类型,相比直接定义常量能够带来非常多的好处。




2.1 类型安全


分别定义一个简单的肉类枚举和水果枚举


// 肉类枚举
public enum MeetEnums {

BEEF,
PORK,
FISH;

}

// 水果枚举
public enum FruitsEnums {

APPLE,
BANANA,
ORANGE;

}

我们改造上面的代码,修改入参类型即可


public static void fruitsHandle(FruitsEnums fruits) {

switch (fruits) {
case APPLE:
// TODO
break;
case BANANA:
// TODO
break;
case ORANGE:
// TODO
break;
default:
throw new IllegalStateException("Unexpected value: " + fruits);
}

}

可以看到定义枚举类型带来函数类型安全性,如果定义的是常量则无法代理这种效果



2.2 枚举能够提供更多信息


枚举在本质上还是一个类,它能够定义属性和方法,我们可以在枚举类中定义想要的方法、或者通过属性扩展枚举提供的基础信息。


比如我们做web开发时最常见的HttpStatus,在springframework框架中就被定义成了枚举类,它不仅包含了Http响应码,还能包含描述状态。


public enum HttpStatus {

OK(200, "OK"),
NOT_FOUND(404, "Not Found"),
INTERNAL_SERVER_ERROR(500, "Internal Server Error");

private final int value;
private final String reasonPhrase;

private HttpStatus(int value, String reasonPhrase) {
this.value = value;
this.reasonPhrase = reasonPhrase;
}

}

2.3 通过函数提供更多服务


此外HttpStatus它内部还嵌套了Series枚举类,这个类可以协助HttpStatus枚举类,通过statusCode / 100的模判断当前的枚举状态是is1xxInformational、is2xxSuccessful、is3xxRedirection、is4xxClientError、is5xxServerError等等。


public static enum Series {
INFORMATIONAL(1),
SUCCESSFUL(2),
REDIRECTION(3),
CLIENT_ERROR(4),
SERVER_ERROR(5);

private final int value;

private Series(int value) {
this.value = value;
}

public int value() {
return this.value;
}

public static HttpStatus.Series valueOf(HttpStatus status) {
return valueOf(status.value);
}

public static HttpStatus.Series valueOf(int statusCode) {
HttpStatus.Series series = resolve(statusCode);
if (series == null) {
throw new IllegalArgumentException("No matching constant for [" + statusCode + "]");
} else {
return series;
}
}

@Nullable
public static HttpStatus.Series resolve(int statusCode) {
int seriesCode = statusCode / 100;
HttpStatus.Series[] var2 = values();
int var3 = var2.length;

for(int var4 = 0; var4 < var3; ++var4) {
HttpStatus.Series series = var2[var4];
if (series.value == seriesCode) {
return series;
}
}

return null;
}
}

2.4 获取所有定义的类型


所有的枚举类会自动产生一个values()方法,它能返回当前定义枚举类的数组集,因此可以很方便的遍历怎么枚举类定义的所有枚举。比如我们简单改造一下MeetEnums枚举类:


public enum MeetEnums {

BEEF("牛肉"),
PORK("猪肉"),
FISH("鱼肉");

String name;

public String getName() {
return name;
}

MeetEnums(String name) {
this.name = name;
}

public static MeetEnums getMeetEnumsByName(String name) {
MeetEnums[] values = values();
Optional<MeetEnums> optional = Stream.of(values).filter(v -> v.getName().equals(name)).findAny();
return optional.isPresent() ? optional.get() : null;
}

}

总之枚举类相比常量来说有太多的优点,它能使得代码更加整洁美观、安全性强、功能强大。虽然大部分情况下,枚举类的选择是由于常量定义的,但是也并不是任何时候都一定要把常量定义成枚举;具体情况大家就可以自己去斟酌啦!


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

解决 Flutter 嵌套过深,是选择函数还是自定义类组件?

前言 初学 Flutter 的时候,一个很大的感受就是组件嵌套层级很深,写下来的代码找对应的括号都找不到。比如下面这种情况,从最外层的 Scaffold 到最里层的 Image.asset,一共有7层组件嵌套。这还不算多的,最夸张是见过一个表单页面写了10多层...
继续阅读 »

前言


初学 Flutter 的时候,一个很大的感受就是组件嵌套层级很深,写下来的代码找对应的括号都找不到。比如下面这种情况,从最外层的 Scaffold 到最里层的 Image.asset,一共有7层组件嵌套。这还不算多的,最夸张是见过一个表单页面写了10多层,代码的阅读体验非常糟糕,而且如果不小心删除了一个括号要找半天才对应得上。当然,通过 VSCode 彩虹括号(Rainbow Brackets)这个插件能够一定程度上解决括号对称查找得问题,但是代码的可维护性、阅读体验还是很差。自然而然,大家会想到拆分。拆分有两种方式,一种是使用返回Widget 的函数,另一种是使用 StatelessWidget,那这两种该如何选择呢?


image.png


拆分原则


在关于这个问题的讨论上,2年前 StackOverflow 有一个经典的回答:使用函数和使用类来构建可复用得组件有什么区别?,大家可以去看看。其中提到得一个关键因素是 Flutter 框架能够检测组件树的类对象,从而提高复用性。而对于私有的方法来说 Flutter 在更新的时候并不知道该如何处理。


image.png


答主也对比了使用类和函数的优劣势。使用类构建的方式:



  • 支持性能优化,比如使用 const 构造方法,更细颗粒度的刷新;

  • 两个不同的布局切换时,能够正确地销毁对应得资源。这个我们在上篇讲 StatefulWidget 的时候有介绍过。

  • 保证正确的方式进行热重载,而使用函数可能破坏热重载。

  • 在 Widget Inspector 中可以查看得到,从而可以方便我们定位和调试问题。

  • 更友好的错误提示。当组件树出现错误时,框架会给出当前构建得组件名称,而如果使用函数的话则得不到清晰得名词。

  • 可以使用 key 提高性能。

  • 可以使用 context 提供的方法(函数式组件除非显示地传递 context)。


使用函数构建组件唯一的优势就是代码量会更少(这可以通过 functional_widget 插件解决,functional_widget 是一个通过注解将和函数式组件构建方式自动转换为类组件的代码生成插件)。


示例对比


下面我们看一段没有拆分的代码,这个仅仅是示例代码,没有任何实际意义。


class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int _counter = 0;

@override
Widget build(BuildContext context) {
return Row(
children: [
Text('Counter: $_counter'),
Container(
child: Column(
children: [
Text('Hello'),
Row(
children: [
Text('there'),
Text('world!'),
],
),
],
),
),
],
);
}
}

括号有点多,对吧,一眼看过去都懵圈了 —— 这也是很多初次接触 Flutter 的人吐槽地方,可以说让不少人直接放弃了! 最直接的方式就是将部分代码抽离成为一个私有方法,比如像下面这样。


class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int _counter = 0;

Widget _buildNonsenseWidget() {
return Container(
child: Column(
children: [
Text('Hello'),
Row(
children: [
Text('there'),
Text('world!'),
],
),
],
),
);
}

@override
Widget build(BuildContext context) {
return Row(
children: [
Text('Counter: $_counter'),
_buildNonsenseWidget(),
],
);
}
}

将深度嵌套的组件代码单独抽成了一个返回 Widget 的私有方法,看起来确实让代码简洁不少。
那么问题就解决了吗?我们来看一下当状态改变的时候会发生什么。
我们知道,当状态变量_counter改变后,Flutter 会调用 build 方法刷新组件。这会导致 _buildNonsenseWidget 这个方法在刷新的时候每次都会被调用,意味着每次都会创建新的组件来替换旧的组件,即便两个组件没有任何改变。而事实上,我们应该只重建那些变化的组件,从而提高性能。
现在再来看使用类组件的方式,实际上有代码模板的情况下,编写一个 StatelessWidget 非常简单。使用类组件后的代码如下所示。代码确实会比函数的方式多,但是实际上大部分不需要我们手敲。


class _MyStatefulWidgetState extends State<MyStatefulWidget> {
int _counter = 0;

@override
Widget build(BuildContext context) {
return Row(
children: [
Text('Counter: $_counter'),

// The deeply nesting widget is now refactored into a
// stateless const widget. No more needless rebuilding!
const _NonsenseWidget(),
],
);
}
}

class _NonsenseWidget extends StatelessWidget {
const _NonsenseWidget();

@override
Widget build(BuildContext context) {
return Container(
child: Column(
children: [
Text('Hello'),
Row(
children: [
Text('there'),
Text('world!'),
],
),
],
),
);
}
}

这里注意,以为这个_NonsenseWidget 在组件得声明周期不会改变,因此使用了 const 的构造方法。这样在刷新过程中,就不会重新构建了!关于 const 可以参考之前的两篇文章。


关于 StatefulWidget,你不得不知道的原理和要点!


解密 Flutter 的 const 关键字


总结


相比使用函数构建复用的组件代码,请尽可能地使用类组件的方式,而且尽可能地将组件拆分为小一点的单元。这样一方面可以提供精确的刷新,另一方面则是可以将组件复用到其他页面中。如果你不想改变自己得习惯,那么可以考虑使用 functional_widget 这个插件来自动生成类组件。


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

总结 scripts 阻塞 HTML 解析

看了一些类似文章,里面有这样一些表述:解析 HTML,DOM 解析等;我们统一下表述:下载完 HTML 文件后,浏览器会解析 HTML,目的是为了构建 DOM 结构 或 生成 DOM 树。 内联 scripts <html> <head&...
继续阅读 »

看了一些类似文章,里面有这样一些表述:解析 HTML,DOM 解析等;我们统一下表述:下载完 HTML 文件后,浏览器会解析 HTML,目的是为了构建 DOM 结构 或 生成 DOM 树


内联 scripts


<html>
<head></head>
<body>
 <script>
console.log('irene')
 </script>
</body>
</html>

解析 HTML 过程中遇到 内联 scripts 会暂停解析,先执行 scripts,然后继续解析 HTML。


普通外联 scripts


<script src="index.js"></script>

解析 HTML 过程中遇到 普通外联 scripts 会暂停解析,发送请求并执行 scripts,然后继续解析 HTML。如下图所示,绿色表示 HTML 解析;灰色表示 HTML 解析暂停;蓝色表示 scripts 下载;粉色表示 scripts 执行。


image.png


defer scripts


<script defer src="index.js"></script>

解析 HTML 过程中遇到 defer scripts 不会停止解析,scripts 也会并行下载;等整个 HTML 解析完成后按引用 scripts 的顺序执行。defer scripts 在 DOMContentLoaded 事件触发之前执行。defer 属性只能用于外联 scripts。


image.png
浏览器并行下载多个 defer scripts,文件小的 scripts 很可能先下载完,defer 属性除了告诉浏览器不去阻塞 HTML 解析,同时还保证了defer scripts 的相对顺序。即使 small.js 先下载完,它还是得等到 long.js 执行完再去执行。


async scripts


<script async src="index.js"></script>

解析 HTML 过程中遇到 async scripts 不会停止解析,scripts 也会并行下载;scripts 下载完之后开始执行,阻塞 HTML 解析。async scripts 的执行顺序和它的引用顺序不一定相同。async scripts 可能在 DOMContentLoaded 事件触发之前或之后执行。如果 HTML 先解析完 async scripts 才下载完成,此时 DOMContentLoaded 事件已经触发, async scripts 很有可能来不及监听 DOMContentLoaded 事件。async 属性只能用于外联 scripts。


image.png
浏览器并行下载多个 async scripts,文件小的 scripts 很可能先下载完,先下载完就先执行了,它无法保证按 async scripts 的引用顺序执行。


defer VS async


在实践中,defer 用于需要整个 DOM 或其相对执行顺序很重要的 scripts。而 async 则用于独立的 scripts,如计数器或广告,而它们的相对执行顺序并不重要。


dynamic scripts


let script = document.createElement('script');
script.src = "/article/script-async-defer/long.js";
document.body.append(script);

脚本一旦被 append 到文档中就开始下载,动态脚本在默认情况下表现的像 async scripts,即先下载完先执行;可以显示设置 script.async = false,这样 scripts 的执行顺序就会和 defer scripts 表现的一致。


这两篇文章中,文一说 defer scripts 会阻塞 HTML 解析,文二说 defer scripts 不会阻塞 HTML 解析。其实两者的想法是一致的:即 defer scripts 的下载不会阻塞 HTML 解析,且执行是在构建完 DOM 之后;之所以有两种不同的表述是因为文一定义阻塞 HTML 解析的标准:是否在 DOMContentLoaded 之前执行,在之前执行就是阻塞 HTML 解析,否则就是不会;defer scripts 是在构建完 DOM 之后,DOMContentLoaded 之前执行的,所有文一认为 defer scripts 会阻塞 HTML 解析。文二说 defer scripts 不会阻塞 HTML 解析就很好理解了。


作者:小被子
链接:https://juejin.cn/post/7027673904927735822

收起阅读 »

手把手教你封装一个日期格式化的工具函数

最近还是在做那个练习的小项目,做完接收数据并渲染到页面上的时候,发现后端小伙伴又在给我找活干了欸,单纯的渲染这当然是小kiss啦,可这个字段是个什么东西? "createTime" : "2021-01-17T13:32:06.381Z", "lastLogi...
继续阅读 »

最近还是在做那个练习的小项目,做完接收数据并渲染到页面上的时候,发现后端小伙伴又在给我找活干了欸,单纯的渲染这当然是小kiss啦,可这个字段是个什么东西?


"createTime" : "2021-01-17T13:32:06.381Z",
"lastLoginTime" : "2021-01-17T13:32:06.381Z"

直接CV到百度,查出来这一串是一种时间格式,下面放上它的解释:



T表示分隔符,Z表示的是UTC.
UTC:世界标准时间,在标准时间上加上8小时,即东八区时间,也就是北京时间。


另:还有别的时间格式和时间戳,想了解的小伙伴可以百度了解一下哦,免得跟我一样,看到了才想着去百度了解,事先了解一下,没坏处的。



了解完了,现在我应该做的,就是将这个时间变成我们大家看得懂的那种格式,并将它渲染到页面上。


开始上手


JavaScript中,处理日期和时间,当然要用到我们的Date对象,所以我们先来写出这个函数的雏形:


const formateDate = (value)=>{
let date = new Date(value)
}

下面要做的应该是定义日期的格式了,这里我用的是yyyy-MM-dd hh:mm:ss


let fmt = 'yyyy-MM-dd hh:mm:ss'

因为年月日时分秒这里都是两位或者两位以上的,所以在获取的时候我是这样定义的:


const o = {
'y+': date.getFullYear(),
'M+': date.getMonth()+1,
'd+': date.getDate(),
'h+': date.getHours(),
'm+': date.getMinutes(),
's+': date.getSeconds()
}

首先先解释一下getMonth()+1,去查看Date文档就知道,这个函数的返回是0-11,我们正常月份都是1-12,所以加上1,才是正确的月份。


定义了规则之后,我们循环它,应该就可以得到我们想要的结果了吧。


for(let k in o ){
if(new RegExp(`(${k})`).test(fmt)){
const val = o[k] + ''
fmt = fmt.replace(RegExp.$1,val)
}
}

我们继续来解释一下代码,首先fmt.replace是代表我们要做一个替换,RegExp.$1就是获取到上面的值表达式内容,将这个内容,换成val中的值,之所以上面加了一个空字符串,是为了将val变成字符串的形式,以防再出纰漏。


$1.png


我们渲染上去,看看结果如何?


秒未补零.png


日期被我们成功的转化为了,我们能看得懂的东西,但是我们可以看到,秒这里,只有一位,也就是说,在秒只有个位数的情况下,我们应该给予它一个补零的操作。



不光是秒,其他也应该是这个道理哦!



关于补零


补零的话,有两种方式,先来说说笨笨的这种吧:


我们去判断这个字符串的长度,如果是1,我们就加个零,如果不是1,那么就不用加。


var a = '6'
a.length = 1?'0'+a:a // '06'

再来说个略微比这个高级一点的:


我们需要两位,所以直接给字符串补上两个零,再用substr去分割一下字符串,就能得到我们想要的了。


var b = '6'
var result = ('00'+b).substr(b.length) // '06'

那么我们去改一下上面的代码,就得到了下面的函数:


const formateDate = (value)=>{
let date = new Date(value)
let fmt = 'yyyy-MM-dd hh:mm:ss'
const o = {
'y+': date.getFullYear(),
'M+': date.getMonth()+1,
'd+': date.getDate(),
'h+': date.getHours(),
'm+': date.getMinutes(),
's+': date.getSeconds()
}
for(let k in o ){
if(new RegExp(`(${k})`).test(fmt)){
const val = o[k] + ''
fmt = fmt.replace(RegExp.$1,RegExp.$1.length==1?val:('00'+val).substr(val.length))
}
}
return fmt
}

在刷新一下网页,看看我们成功了没!


补零结束.png


成功是成功了,但是我们发现,前面的年竟然被干掉了,他也变成了两位的样子,这可不行啊,我们定义的年份格式可是四位的。


这可咋整.webp


但是别慌,这个只需要把年份单独的去做判断,不与其他2位的格式一起进行操作就能解决啦,所以我们最终的函数是这样的:


const formateDate = (value)=>{
let date = new Date(value)
let fmt = 'yyyy-MM-dd hh:mm:ss'
if(/(y+)/.test(fmt)){
fmt = fmt.replace(RegExp.$1,date.getFullYear())
}
const o = {
'M+': date.getMonth()+1,
'd+': date.getDate(),
'h+': date.getHours(),
'm+': date.getMinutes(),
's+': date.getSeconds()
}
for(let k in o ){
if(new RegExp(`(${k})`).test(fmt)){
const val = o[k] + ''
fmt = fmt.replace(RegExp.$1,RegExp.$1.length==1?val:('00'+val).substr(val.length))
}
}
return fmt
}

看一下结果吧:


image.png


如果再严谨一点,可以再给函数加个参数,传递一个rule,这样方便我们后期进行调整数据格式,在定义格式的时候用||就好了。


let fmt = '传入的新格式' || '默认的格式
收起阅读 »

用CSS告诉你为何大橘为重!!

本期我们通过vite+scss去完成一个橘猫心情变化的创意动画,这里的逻辑我们将不使用任何js代码,仅依靠css来完成,所以,通过本期的动画,你可以到一些css动画和绘制的一些技巧。废话不多说,先康康效果~ 还比较可爱吧。当鼠标(鱼)移入出,橘子闷闷不乐,无...
继续阅读 »

本期我们通过vite+scss去完成一个橘猫心情变化的创意动画,这里的逻辑我们将不使用任何js代码,仅依靠css来完成,所以,通过本期的动画,你可以到一些css动画和绘制的一些技巧。废话不多说,先康康效果~


VID_20211030_184225.gif


还比较可爱吧。当鼠标(鱼)移入出,橘子闷闷不乐,无精打采的。但当鼠标(鱼)移入,橘子一看见最喜欢的鱼立马就开心了,连天气都变好了,对,这只橘子就是这么馋,变成胖橘是有原因的。


好了,我们马上就要进入正文了,我们会从基础搭建,太阳,云,猫的绘制和动画去了解制作这个动画的流程。


正文


1.搭建与结构


yarn add vite sass sass-loader

我们是用vite和sass去完成项目的构建,和样式的书写,所以我们先安装下他们。


<div id="app">
<div class="warrper">
<div class="sun"></div>
<div class="cloud"></div>
<div class="cat">
<div class="eye left"><div class="eye-hide"></div></div>
<div class="eye right"><div class="eye-hide"></div></div>
<div class="nose"></div>
<div class="mouth"></div>
</div>
</div>
</div>

在html我们先写出结构来。div#app作为主界面去填满一屏,而div.warrper就作为主要内容的展示区域也就是那个圆圈。然后,在圆圈里面我们放太阳div.sun,云朵div.cloud,猫div.cat,当然猫里面还有眼睛鼻子嘴巴这些,至于猫的耳朵就用两个伪类做个三角形去实现。


2.变量与界面


$cat:rgb(252, 180, 125);

:root{
--bgColor:rgb(81, 136, 168);
--eyeHideTop:0px;
--cloudLeft:45%;
--mouthRadius:10px 10px 0 0;
}
#app{
width: 100%;
height: 100vh;
position: relative;
display: flex;
justify-content: center;
align-items: center;
background-image: repeating-linear-gradient(0deg, hsla(340,87%,75%,0.2) 0px, hsla(340,87%,75%,0.2) 30px,transparent 30px, transparent 60px),repeating-linear-gradient(90deg, hsla(340,87%,75%,0.2) 0px, hsla(340,87%,75%,0.2) 30px,transparent 30px, transparent 60px),linear-gradient(90deg, rgb(255,255,255),rgb(255,255,255));
}

.warrper{
width: 320px;
height: 320px;
border-radius: 50%;
border: 10px solid white;
position: relative;
overflow: hidden;
background-color: var(--bgColor);
transition: background-color 1s linear;
cursor:url("./assets/fish.png"),default;
&:hover{
--bgColor:rgb(178, 222, 247);
--eyeHideTop:-20px;
--cloudLeft:100%;
--mouthRadius:0 0 10px 10px;
}
}

我们先定义猫的主色调,还有一些要变化的颜色和距离,因为我们移入将通过css3去改变这些属性,来达到某些动画的实现。


我们期望的是,当鼠标移入圆圈后,天空变晴,云朵退散,猫开心充满精神,所以,bgColor:天空颜色,eyeHideTop猫的眼皮y轴距离,cloudLeft云朵x轴偏移距离,mouthRadius猫嘴巴的圆角值。目前来说,当鼠标移入div.warrper后,这些值都会发生变化。另外,我自定义了鼠标图标移入圆圈变成了一条鱼(即cursor:url(图片地址))。这里的hover后的值是我事先算好的,如果大家重新开发别的动画可以一边做一边算。


微信截图_20211030200310.png


3.太阳与云朵


.sun{
width: 50px;
height: 50px;
position: absolute;
background-color: rgb(255, 229, 142);
border:7px solid rgb(253, 215, 91);
border-radius: 50%;
left: 55%;
top: 14%;
box-shadow: 0 0 6px rgb(255, 241, 48);
}

太阳我们就画个圆圈定好位置,然后用box-shadow投影去完成一点发光的效果。


微信截图_20211030200343.png


然后,我们再开始画云朵~


.cloud{
width: 100px;
height: 36px;
background-color: white;
position: absolute;
transition: left .6s linear;
left: var(--cloudLeft);
top: 23%;
border-radius: 36px;
animation: bouncy 2s ease-in-out infinite;
&::before{
content: '';
width: 50px;
height: 50px;
background-color: white;
border-radius: 50%;
position: absolute;
top: -23px;
left: 18px;
}
&::after{
content: '';
width: 26px;
height: 26px;
background-color: white;
border-radius: 50%;
position: absolute;
top: -16px;
left: 56px;
}
}

@keyframes bouncy {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}

云朵很简单,我们就是画一个圆角矩形,然后用两个伪类画一个大圆和小圆叠在一起就非常像云了,另外,我们再加个animation动画,让他时大时小,有动的感觉。


微信截图_20211030200357.png


4.橘猫与动画


.cat{
width: 180px;
height: 160px;
background-color: $cat;
position: absolute;
bottom: -20px;
left: 50%;
margin-left: -90px;
animation: wait 2s ease-in-out infinite;
&::after,
&::before{
content: '';
display: block;
border-style: solid;
border-width: 20px 30px;
position: absolute;
top: -30px;
}
&::after{
right: 0;
border-color: transparent $cat $cat transparent;
}
&::before{
left: 0;
border-color: transparent transparent $cat $cat;
}
.eye{
width: 42px;
height: 42px;
border-radius: 50%;
position: absolute;
top: 30px;
background:white;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
.eye-hide{
height: 20px;
position: absolute;
top: var(--eyeHideTop);
left: -2px;
right:-2px;
background-color: $cat;
transition: top .5s ease-in-out;
z-index: 2;
}
&::before{
content: "";
height: 36px;
width: 36px;
background-color:black;
border-radius: 50%;
}
&::after{
content: "";
width: 24px;
height: 24px;
background-color: white;
border-radius: 50%;
position: absolute;
right: 0px;
top: 0px;
}
&.left{
left: 24px;
}
&.right{
right: 24px;
}
}
.nose{
width: 0;
height: 0;
border-top: 7px solid rgb(248, 226, 226);
border-left: 7px solid transparent;
border-right: 7px solid transparent;
position: absolute;
left: 50%;
margin-left: -7px;
top: 70px;
}
.mouth{
width: 26px;
height: 20px;
background-color: rgb(255, 217, 217);
position: absolute;
top: 85px;
left: 50%;
margin-left: -13px;
border-radius: var(--mouthRadius);
transition: border-radius .2s linear;
overflow: hidden;
&::after,
&::before{
content: "";
position: absolute;
display: block;
top: 0;
border-top: 7px solid white;
border-left: 2px solid transparent;
border-right: 2px solid transparent;
}
&::after{
right: 5px;
}
&::before{
left: 5px;
}
}
}

@keyframes wait{
0% {
bottom: -20px;
}
50% {
bottom: -25px;
}
100% {
bottom: -20px;
}
}

我们可以实现分解出,耳朵(伪类)+ 一双眼睛 + 鼻子 + 嘴(包含两颗尖牙) = 猫。


通过以上代码就不难看出主要都是在使用绝对定位来完成,面部器官的摆放。绝大部分都是css基础代码来实现的。唯一可以注意的点,就是耳朵这个三角形,我们是通过伪类实现,将它不设置宽高,而主是通过border-width+boder-color这个技巧去绘制出三角形的,算是个css小技巧吧,后面的鼻子和嘴巴里的尖牙都是这个小技巧来实现的。


另外,还要说的是那双眼睛,我们用先填充白底再分别用伪类去实现里面的黑底圆和白色小圆,肯定有同学问了为什么不用border是实现白色圆框,就不用浪费一个伪类去完成黑底圆了?因为我们用了overflow: hidden,他多余隐藏的内容是border以下的元素,而border边框可以无损,那么他的伪类能盖不住他的border,这样显得眼皮垂下的圆圈还是很大不自然,所以我们又造了一个伪类去实现他的黑底,让外圆不使用border了。


剩下的就是做一个等待的animation动画给猫,让他上下移动着,来实现不停的呼吸的效果。


微信截图_20211030200539.png


这样一直无精打采的橘猫就完成了。因为在第一部分,我们事先已经把移入后改变的变量算好了,现在把鼠标移入,效果就出现咯~


微信截图_20211030200546.png


结语


讲到这里我们就已经完成了这个动画了,不得不说,看见食物这么激动不愧都叫他胖橘!


这里有我这个动画【I Like Fish】codepen地址可以看到演示和代码,有兴趣的小伙伴可以康康。


本期还是比较侧重基础和动画创意的,主要是新手向,大佬勿喷,经常用css写写动画挺有意思的,不仅可以熟悉基本功,而且会迸发出很多创意来,也是一种锻炼自己的学习方式吧,多练习下,大家一起加油鸭~



收起阅读 »

你需要知道的 19 个 console 实用调试技巧

众所周知,浏览器的开发者工具为我们提供了强大的调试系统,可以用来查看DOM树结构、CSS样式调试、动画调试、JavaScript代码断点调试等。今天我们就来看看console调试的那些实用的调试技巧。 如今,我们项目的开发通常会使用React、Vue等前端框...
继续阅读 »

众所周知,浏览器的开发者工具为我们提供了强大的调试系统,可以用来查看DOM树结构、CSS样式调试、动画调试、JavaScript代码断点调试等。今天我们就来看看console调试的那些实用的调试技巧。


如今,我们项目的开发通常会使用React、Vue等前端框架,前端调试也变得更加有难度,除了使用React Dev Tools,Vue Dev Tools等插件之外,我们使用最多的就是console.log(),当然多数情况下,console.log()就能满足我们的需求,但是当数据变得比较复杂时,console.log()就显得有些单一。其实console对象为我们提供了很多打印的方法,下面是console对象包含的方法(这里使用的是Chrome浏览器,版本为 95.0.4638.54(正式版本) (arm64)):


image.png


console 对象提供了浏览器控制台调试的接口,我们可以从任何全局对象中访问到它,如果你平时只是用console.log()来输出一些变量,那你可能没有用过console那些强大的功能。下面带你用console玩玩花式调试。


一、基本打印


1. console.log()


console.log()就是最基本、最常用的用法了。它可以用在JavaScript代码的任何地方,然后就可以浏览器的控制台中看到打印的信息。其基本使用方法如下:


let name = "CUGGZ";
let age = 18;
console.log(name) // CUGGZ
console.log(`my name is: ${name}`) // CUGGZ
console.log(name, age) // CUGGZ 18
console.log("message:", name, age) // message: CUGGZ 18

除此之外,console.log()还支持下面这种输出方式:


let name = "CUGGZ";
let age = 18;
let height = 180;
console.log('Name: %s, Age: %d', name, age) // Name: CUGGZ, Age: 18
console.log('Age: %d, Height: %d', age, height) // Age: 18, Height: 180

这里将后面的变量赋值给了前面的占位符的位置,他们是一一对应的。这种写法在复杂的输出时,能保证模板和数据分离,结构更加清晰。不过如果是简单的输出,就没必要这样写了。在console.log中,支持的占位符格式如下:



  • 字符串:%s

  • 整数:%d

  • 浮点数:%f

  • 对象:%o或%O

  • CSS样式:%c


可以看到,除了最基本的几种类型之外,它还支持定义CSS样式:


let name = "CUGGZ";
console.log('My Name is %cCUGGZ', 'color: skyblue; font-size: 30px;')

打印结果如下(好像并没有什么卵用):


image.png


这个样式打印可能有用的地方就是打印图片,用来查看图片是否正确:


console.log('%c ','background-image:url("http://iyeslogo.orbrand.com/150902Google/005.gif");background-size:120% 120%;background-repeat:no-repeat;background-position:center center;line-height:60px;padding:30px 120px;');

打印结果如下:


image.png


严格地说,console.log()并不支持打印图片,但是可以使用CSS的背景图来打印图片,不过并不能直接打印,因为是不支持设置图片的宽高属性,所以就需要使用line-heigh和padding来撑开图片,使其可以正常显示出来。


我们可以使用console.log()来打印字符画,就像知乎的这样:


image.png


可以使用字符画在线生成工具,将生成的字符粘贴到console.log()即可。在线工具:mg2txt。我的头像生成效果如下,中间的就是生成的字符:


image.png


除此之外,可以看到,当占位符表示一个对象时,有两种写法:%c或者%C,那它们两个有什么区别呢?当我们指定的对象是普通的object对象时,它们两个是没有区别的,如果是DOM节点,那就有有区别了,来看下面的示例:


image.png


可以看到,使用 %o 打印的是DOM节点的内容,包含其子节点。而%O打印的是该DOM节点的对象属性,可以根据需求来选择性的打印。


2. console.warn()


console.warn() 方法用于在控制台输出警告信息。它的用法和console.log是完全一样的,只是显示的样式不太一样,信息最前面加一个黄色三角,表示警告:


const app = ["facebook", "google", "twitter"];
console.warn(app);

打印样式如下:


image.png


3. console.error()


console.error()可以用于在控制台输出错误信息。它和上面的两个方法的用法是一样的,只是显示样式不一样:


const app = ["facebook", "google", "twitter"];
console.error(app)

image.png


需要注意,console.exception() 是 console.error() 的别名,它们功能是相同的。


当然,console.error()还有一个console.log()不具备的功能,那就是打印函数的调用栈:


function a() {
b();
}
function b() {
console.error("error");
}
function c() {
a();
}
c();

打印结果如下:


image.png


可以看到,这里打印出来了函数函数调用栈的信息:b→a→c。


console对象提供了专门的方法来打印函数的调用栈(console.trace()),这个下面会介绍到。


4. console.info()


console.info()可以用来打印资讯类说明信息,它和console.log()的用法一致,打印出来的效果也是一样的:


image.png


二、打印时间


1. console.time() & console.timeEnd()


如果我们想要获取一段代码的执行时间,就可以使用console对象的console.time() 和console.timeEnd()方法,来看下面的例子:


console.time();

setTimeout(() => {
console.timeEnd();
}, 1000);

// default: 1001.9140625 ms

它们都可以传递一个参数,该参数是一个字符串,用来标记唯一的计时器。如果页面只有一个计时器时,就不需要传这个参数 ,如果有多个计时器,就需要使用这个标签来标记每一个计时器:


console.time("timer1");
console.time("timer2");

setTimeout(() => {
console.timeEnd("timer1");
}, 1000);

setTimeout(() => {
console.timeEnd("timer2");
}, 2000);

// timer1: 1004.666259765625 ms
// timer2: 2004.654052734375 ms

2. console.timeLog()


这里的console.timeLog()上面的console.timeEnd()类似,但是也有一定的差别。他们都需要使用console.time()来启动一个计时器。然后console.timeLog()就是打印计时器当前的时间,而console.timeEnd()是打印计时器,直到结束的时间。下面来看例子:


console.time("timer");

setTimeout(() => {
console.timeLog("timer")
setTimeout(() => {
console.timeLog("timer");
}, 2000);
}, 1000);

// timer: 1002.80224609375 ms
// timer: 3008.044189453125 ms

而使用console.timeEnd()时:


console.time("timer");

setTimeout(() => {
console.timeEnd("timer")
setTimeout(() => {
console.timeLog("timer");
}, 2000);
}, 1000);


打印结果如下:


image.png


可以看到,它会终止当前的计时器,所以里面的timeLog就无法在找到timer计数器了。
所以两者的区别就在于,是否会终止当前的计时。


三、分组打印


1. console.group() & console.groupEnd()


这两个方法用于在控制台创建一个信息分组。 一个完整的信息分组以 console.group() 开始,console.groupEnd() 结束。来看下面的例子:


console.group();
console.log('First Group');
console.group();
console.log('Second Group')
console.groupEnd();
console.groupEnd();

打印结果如下:


image.png


再来看一个复杂点的:


console.group("Alphabet")
console.log("A");
console.log("B");
console.log("C");
console.group("Numbers");
console.log("One");
console.log("Two");
console.groupEnd("Numbers");
console.groupEnd("Alphabet");

打印结果如下:


image.png


可以看到,这些分组是可以嵌套的。当前我们需要调试一大堆调试输出,就可以选择使用分组输出,


2. console.groupCollapsed()


console.groupCollapsed()方法类似于console.group(),它们都需要使用console.groupEnd()来结束分组。不同的是,该方法默认打印的信息是折叠展示的,而group()是默认展开的。来对上面的例子进行改写:


console.groupCollapsed("Alphabet")
console.log("A");
console.log("B");
console.log("C");
console.groupCollapsed("Numbers");
console.log("One");
console.log("Two");
console.groupEnd("Numbers");
console.groupEnd("Alphabet");

其打印结果如下:


image.png


可以看到,和上面方法唯一的不同就是,打印的结果被折叠了,需要手动展开来看。


四、打印计次


1. console.count()


可以使用使用console.count()来获取当前执行的次数。来看下面的例子:


for (i = 0; i < 5; i++) {
console.count();
}

// 输出结果如下
default: 1
default: 2
default: 3
default: 4
default: 5

它也可以传一个参数来进行标记(如果为空,则为默认标签default):


for (i = 0; i < 5; i++) {
console.count("hello");
}

// 输出结果如下
hello: 1
hello: 2
hello: 3
hello: 4
hello: 5

这个方法主要用于一些比较复杂的场景,有时候一个函数被多个地方调用,就可以使用这个方法来确定是否少调用或者重复调用了该方法。


2. console.countReset()


顾名思义,console.countReset()就是重置计算器,它会需要配合上面的console.count()方法使用。它有一个可选的参数label:



  • 如果提供了参数label,此函数会重置与label关联的计数,将count重置为0。

  • 如果省略了参数label,此函数会重置默认的计数器,将count重置为0。


console.count(); 
console.count("a");
console.count("b");
console.count("a");
console.count("a");
console.count();
console.count();

console.countReset();
console.countReset("a");
console.countReset("b");

console.count();
console.count("a");
console.count("b");

打印结果如下:


default:1
a:1
b:1
a:2
a:3
default:2
default:3
default:1
a:1
b:1

五、其他打印


1. console.table()


我们平时使用console.log较多,其实console对象还有很多属性可以使用,比如console.table(),使用它可以方便的打印数组对象的属性,打印结果是一个表格。console.table() 方法有两个参数,第一个参数是需要打印的对象,第二个参数是需要打印的表格的标题,这里就是数组对象的属性值。来看下面的例子:


const users = [ 
{
"first_name":"Harcourt",
"last_name":"Huckerbe",
"gender":"Male",
"city":"Linchen",
"birth_country":"China"
},
{
"first_name":"Allyn",
"last_name":"McEttigen",
"gender":"Male",
"city":"Ambelókipoi",
"birth_country":"Greece"
},
{
"first_name":"Sandor",
"last_name":"Degg",
"gender":"Male",
"city":"Mthatha",
"birth_country":"South Africa"
}
]

console.table(users, ['first_name', 'last_name', 'city']);

打印结果如下:


image.png


通过这种方式,可以更加清晰的看到数组对象中的指定属性。


除此之外,还可以使用console.table()来打印数组元素:


const app = ["facebook", "google", "twitter"];
console.table(app);

打印结果如下:
image.png
通过这种方式,我们可以更清晰的看到数组中的元素。


需要注意,console.table() 只能处理最多1000行,因此它可能不适合所有数据集。但是也能适用于多数场景了。


2. console.clear()


console.clear() 顾名思义就是清除控制台的信息。当清空控制台之后,会打印一句:“Console was clered”:


image.png


当然,我们完全可以使用控制台的清除键清除控制台:


image.png


3. console.assert()


console.assert()方法用于语句断言,当断言为 false时,则在信息到控制台输出错误信息。它的语法如下:


console.assert(expression, message)

它有两个参数:



  • expression: 条件语句,语句会被解析成 Boolean,且为 false 的时候会触发message语句输出;

  • message: 输出语句,可以是任意类型。



该方法会在expression条件语句为false时,就会打印message信息。当在特定情况下才输出语句时,就可以使用console.assert()方法。


比如,当列表元素的子节点数量大于等于100时,打印错误信息:


console.assert(list.childNodes.length < 100, "Node count is > 100");

其输出结果如下图所示:


image.png


4. console.trace()


console.trace()方法可以用于打印当前执行的代码在堆栈中的调用路径。它和上面的console.error()的功一致,不过打印的样式就和console.log()是一样的了。来看下面的例子:


function a() {
b();
}
function b() {
console.trace();
}
function c() {
a();
}
c();

打印结果如下:


image.png


可以看到,这里输出了调用栈的信息:b→a→c,这个堆栈信息是从调用位置开始的。


5. console.dir()


console.dir()方法可以在控制台中显示指定JavaScript对象的属性,并通过类似文件树样式的交互列表显示。它的语法如下:


console.dir(object);

它的参数是一个对象,最终会打印出该对象所有的属性和属性值。


在多数情况下,使用consoledir()和使用console.log()的效果是一样的。但是当打印元素结构时,就会有很大的差异了,console.log()打印的是元素的DOM结构,而console.dir()打印的是元素的属性:


image.png


image.png


6. console.dirxml()


console.dirxml()方法用于显示一个明确的XML/HTML元素的包括所有后代元素的交互树。 如果无法作为一个element被显示,那么会以JavaScript对象的形式作为替代。 它的输出是一个继承的扩展的节点列表,可以让你看到子节点的内容。其语法如下:


console.dirxml(object);

该方法会打印输出XML元素及其后代元素,对于XML和HTML元素调用console.log()和console.dirxml()是等价的。


image.png


7. console.memory


console.memory是console对象的一个属性,而不是一个方法。它可以用来查看当前内存的使用情况,如果使用过多的console.log()会占用较多的内存,导致浏览器出现卡顿情况。


image.png



收起阅读 »

淦,为什么 "???".length !== 3

不知道你是否遇到过这样的疑惑,在做表单校验长度的需求中,发现不同字符 length 可能大小不一。比如标题中的 "𠮷" length 是 2(需要注意📢,这并不是一个中文字!)。 '吉'.length // 1 '𠮷'.length // 2 '❤'.le...
继续阅读 »

不知道你是否遇到过这样的疑惑,在做表单校验长度的需求中,发现不同字符 length 可能大小不一。比如标题中的 "𠮷" length 是 2(需要注意📢,这并不是一个中文字!)。


'吉'.length
// 1

'𠮷'.length
// 2

'❤'.length
// 1

'💩'.length
// 2

要解释这个问题要从 UTF-16 编码说起。


UTF-16


ECMAScript® 2015 规范中可以看到,ECMAScript 字符串使用的是 UTF-16 编码。



定与不定: UTF-16 最小的码元是两个字节,即使第一个字节可能都是 0 也要占位,这是固定的。不定是对于基本平面(BMP)的字符只需要两个字节,表示范围 U+0000 ~ U+FFFF,而对于补充平面则需要占用四个字节 U+010000~U+10FFFF



在上一篇文章中,我们有介绍过 utf-8 的编码细节,了解到 utf-8 编码需要占用 1~4 个字节不等,而使用 utf-16 则需要占用 2 或 4 个字节。来看看 utf-16 是怎么编码的。


UTF-16 的编码逻辑


UTF-16 编码很简单,对于给定一个 Unicode 码点 cp(CodePoint 也就是这个字符在 Unicode 中的唯一编号):



  1. 如果码点小于等于 U+FFFF(也就是基本平面的所有字符),不需要处理,直接使用。

  2. 否则,将拆分为两个部分 ((cp – 65536) / 1024) + 0xD800((cp – 65536) % 1024) + 0xDC00 来存储。



Unicode 标准规定 U+D800...U+DFFF 的值不对应于任何字符,所以可以用来做标记。



举个具体的例子:字符 A 的码点是 U+0041,可以直接用一个码元表示。


'\u0041'
// -> A

A === '\u0041'
// -> true

Javascript 中 \u 表示 Unicode 的转义字符,后面跟着一个十六进制数。


而字符 💩 的码点是 U+1f4a9,处于补充平面的字符,经过 👆 公式计算得到两个码元 55357, 56489 这两个数字用十六进制表示为 d83d, dca9,将这两个编码结果组合成代理对。


'\ud83d\udca9'
// -> '💩'

'💩' === '\ud83d\udca9'
// -> true

由于 Javascript 字符串使用 utf-16 编码,所以可以正确将代理对 \ud83d\udca9 解码得到码点 U+1f4a9


还可以使用 \u + {},大括号中直接跟码点来表示字符。看起来长得不一样,但他们表示的结果是一样的。


'\u0041' === '\u{41}'
// -> true

'\ud83d\udca9' === '\u{1f4a9}'
// -> true


可以打开 Dev Tool 的 console 面板,运行代码验证结果。



所以为什么 length 判断会有问题?


要解答这个问题,可以继续查看 规范,里面提到:在 ECMAScript 操作解释字符串值的地方,每个元素都被解释为单个 UTF-16 代码单元。



Where ECMAScript operations interpret String values, each element is interpreted as a single UTF-16 code unit.



所以像💩 字符实际上占用了两个 UTF-16 的码元,也就是两个元素,所以它的 length 属性就是 2。(这跟一开始 JS 使用 USC-2 编码有关,当初以为 65536 个字符就可以满足所有需求了)


但对于普通用户而言,这就完全没办法理解了,为什么明明只填了一个 '𠮷',程序上却提示占用了两个字符长度,要怎样才能正确识别出 Unicode 字符长度呢?


我在 Antd Form 表单使用的 async-validator 包中可以看到下面这段代码


const spRegexp = /[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]/g;

if (str) {
val = value.replace(spRegexp, '_').length;
}

当需要进行字符串长度的判断时,会将码点范围在补充平面的字符全部替换为下划线,这样长度判断就和实际显示的一致了!!!


ES6 对 Unicode 的支持


length 属性的问题,主要还是最初设计 JS 这门语言的时候,没有考虑到会有这么多字符,认为两个字节就完全可以满足。所以不止是 length,字符串常见的一些操作在 Unicode 支持上也会表现异常


下面的内容将介绍部分存在异常的 API 以及在 ES6 中如何正确处理这些问题。


for vs for of


例如使用 for 循环打印字符串,字符串会按照 JS 理解的每个“元素”遍历,辅助平面的字符将会被识别成两个“元素”,于是出现“乱码”。


var str = '👻yo𠮷'
for (var i = 0; i < str.length; i ++) {
console.log(str[i])
}

// -> �
// -> �
// -> y
// -> o
// -> �
// -> �

而使用 ES6 的 for of 语法就不会。


var str = '👻yo𠮷'
for (const char of str) {
console.log(char)
}

// -> 👻
// -> y
// -> o
// -> 𠮷

展开语法(Spread syntax)


前面提到了使用正则表达式,将辅助平面的字符替换的方式来统计字符长度。使用展开语法也可以得到同样的效果。


[...'💩'].length
// -> 1

slice, split, substr 等等方法也存在同样的问题。


正则表达式 u


ES6 中还针对 Unicode 字符增加了 u 描述符。


/^.$/.test('👻')
// -> false

/^.$/u.test('👻')
// -> true

charCodeAt/codePointAt


对于字符串,我们还常用 charCodeAt 来获取 Code Point,对于 BMP 平面的字符是可以适用的,但是如果字符是辅助平面字符 charCodeAt 返回结果就只会是编码后第一个码元对于的数字。


'羽'.charCodeAt(0)
// -> 32701
'羽'.codePointAt(0)
// -> 32701

'😸'.charCodeAt(0)
// -> 55357
'😸'.codePointAt(0)
// -> 128568

而使用 codePointAt 则可以将字符正确识别,并返回正确的码点。


String.prototype.normalize()


由于 JS 中将字符串理解成一串两个字节的码元序列,判断是否相等是根据序列的值来判断的。所以可能存在一些字符串看起来长得一模一样,但是字符串相等判断结果确是 false


'café' === 'café'
// -> false

上面代码中第一个 café 是有 cafe 加上一个缩进的音标字符\u0301组成的,而第二个 café 则是由一个 caf + é 字符组成的。所以两者虽然看上去一样,但码点不一样,所以 JS 相等判断结果为 false


'cafe\u0301'
// -> 'café'

'cafe\u0301'.length
// -> 5

'café'.length
// -> 4

为了能正确识别这种码点不一样,但是语意一样的字符串判断,ES6 增加了 String.prototype.normalize 方法。


'cafe\u0301'.normalize() === 'café'.normalize()
// -> true

'cafe\u0301'.normalize().length
// -> 4

总结


这篇文章主要是我最近重新学习编码的学习笔记,由于时间仓促 && 水平有限,文章中必定存在大量不准确的描述、甚至错误的内容,如有发现还请善意指出。❤️



收起阅读 »

可折叠式标题栏

CollapsingToolbarLayout顾名思义,这是一个作用在Toolbar上的布局,但是要注意的是CollapsingToolbarLayout不能单独存在,它必须要作为AppBarLayout的直接子布局来使用,而AppBarLayout又必须是C...
继续阅读 »

CollapsingToolbarLayout

顾名思义,这是一个作用在Toolbar上的布局,但是要注意的是CollapsingToolbarLayout不能单独存在,它必须要作为AppBarLayout的直接子布局来使用,而AppBarLayout又必须是CoordinatorLayout(监听子控件的事件,做出合理的响应)的子布局。所以可以得到:

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
>

<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="250dp"
android:id="@+id/appbar">

<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsing_toolbar"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:contentScrim="?attr/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
android:layout_width="match_parent"
android:layout_height="match_parent">

</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

接着为了让标题栏高级一些,在CollapsingToolbarLayout中放一张图片和一个Toolbar

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
>

<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="250dp"
android:id="@+id/appbar">

<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsing_toolbar"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:contentScrim="?attr/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
android:layout_width="match_parent"
android:layout_height="match_parent">

<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
app:layout_collapseMode="parallax"
android:id="@+id/fruit_image_view"/>

<androidx.appcompat.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:id="@+id/toolbar"
app:layout_collapseMode="pin"/>

</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

这里有一个layout_collapseMode属性,表示在折叠过程中的折叠样式,parallax表示在折叠过程中会产生错位偏移,而pin表示会始终不变。

NestedScrollView

标题栏完成之后,就是开始编写水果内容的部分了。这里使用NestedScrollView,这和ScrollView,RecyclerView一样都是可以通过滚动的方式来查看屏幕外的数据。同样的和之前的RecyclerView一样,这里也要指定一个布局行为。 由于NestedScrollView和ScrollView一样,只允许存在一个直接子布局,这里就可以嵌套一个LinearLayout作为它的直接子布局。然后在LinearLayout中放具体的内容就可以了。

    <androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="@string/appbar_scrolling_view_behavior">

<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<androidx.cardview.widget.CardView
android:layout_marginTop="35dp"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:layout_marginBottom="15dp"
app:cardCornerRadius="4dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<TextView
android:id="@+id/fruit_content_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"/>

</androidx.cardview.widget.CardView>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

这里添加了一个布局行为(和之前的RecyclerView一样)。 为了让之前的知识尽可能用到,这里再加一个悬浮按钮。

    <com.google.android.material.floatingactionbutton.FloatingActionButton
android:src="@drawable/ic_comment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_anchor="@id/appbar"
app:layout_anchorGravity="bottom|end"/>

这是目前的效果图在这里插入图片描述接着就是通过逻辑代码将数据给填进去了。

MainActivity→FruitActivity

由于数据是在MainActivity得到的,FruitActivity并不能得到数据,所以需要通过MainActivity将数据传输过去,这里可以用Intent来传输。

        ViewHolder viewHolder=new ViewHolder(view);
viewHolder.cardView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent=new Intent(context,FruitActivity.class);
int position=viewHolder.getAdapterPosition();
Fruit fruit=FruitList.get(position);
intent.putExtra("fruitName",fruit.getName());
intent.putExtra("fruitId",fruit.getId());
context.startActivity(intent);
}
});

这里选择在每次生成ViewHolder的时候就为其中的cardView绑定点击事件,将id和name传递给FruitAcitivity。

FruitActivity进行数据处理。

       ImageView imageView=findViewById(R.id.fruit_image_view);
TextView textView=findViewById(R.id.fruit_content_text);
Toolbar toolbar=findViewById(R.id.toolbar);
//将ActionBar换成toolbar
setSupportActionBar(toolbar);
ActionBar actionBar=getSupportActionBar();
if(actionBar!=null){
actionBar.setDisplayHomeAsUpEnabled(true);
}
CollapsingToolbarLayout collapsingToolbarLayout=findViewById(R.id.collapsing_toolbar);
Intent intent=getIntent();
String name=intent.getStringExtra(fruitName);
int id=intent.getIntExtra(fruitId,0);
//用Glide来加载图片
Glide.with(this).load(id).into(imageView);
textView.setText(ExtraText(name));
//为可折叠标题栏设置标题。
collapsingToolbarLayout.setTitle(name);

充分利用状态栏空间。

为ImageView的父布局和其本身添加fitsSystemWindows属性。

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
>

<com.google.android.material.appbar.AppBarLayout
android:fitsSystemWindows="true"
android:layout_width="match_parent"
android:layout_height="250dp"
android:id="@+id/appbar">

<com.google.android.material.appbar.CollapsingToolbarLayout
android:fitsSystemWindows="true"
android:id="@+id/collapsing_toolbar"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:contentScrim="?attr/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
android:layout_width="match_parent"
android:layout_height="match_parent">

<ImageView
android:fitsSystemWindows="true"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
app:layout_collapseMode="parallax"
android:id="@+id/fruit_image_view"/>

<androidx.appcompat.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:id="@+id/toolbar"
app:layout_collapseMode="pin"/>

</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="@string/appbar_scrolling_view_behavior">

<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<androidx.cardview.widget.CardView
android:layout_marginTop="35dp"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:layout_marginBottom="15dp"
app:cardCornerRadius="4dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<TextView
android:id="@+id/fruit_content_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"/>

</androidx.cardview.widget.CardView>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:src="@drawable/ic_comment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_anchor="@id/appbar"
app:layout_anchorGravity="bottom|end"/>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

为FruitActivity自定义一个主题。

<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="FruitActivityTheme" parent="Theme.ListView">
<item name="android:statusBarColor">
@android:color/transparent
</item>
</style>
</resources>

引入这个主题

 <activity android:name=".FruitActivity" android:theme="@style/FruitActivityTheme">
收起阅读 »

基础巩固——多线程

多线程编程Android沿用了Java的线程模型,一个Android应用在创建时会开启一个线程,常称作主线程,也叫做UI线程,如果有请求网络等耗时操作时,就需要开启子线程去处理。因此,此文对多线程进行梳理总结线程基础1. 进程与线程这两个的区分在我的另一篇文章...
继续阅读 »

多线程编程

Android沿用了Java的线程模型,一个Android应用在创建时会开启一个线程,常称作主线程,也叫做UI线程,如果有请求网络等耗时操作时,就需要开启子线程去处理。因此,此文对多线程进行梳理总结


线程基础

1. 进程与线程

这两个的区分在我的另一篇文章# Android面向面试复习-操作系统+计网篇中已经提及,简单复习一下。进程是操作系统结构的基础,是程序在一个数据集合上运行的过程,是系统进行资源分配和调度的基本单位。进程可以看作程序的实体,也是线程的容器。线程是操作系统调度的最小单元,一个进程中可以创建多个线程。这些线程拥有各自的计数器,堆栈,局部变量等属性,并且能够访问共享的内存变量


2. 线程的状态

Java线程在运行的生命周期可能会处于六种不同的状态,如下

  • New:新创建状态,线程被创建,还没有调用start方法,运行之前还有一些基础工作
  • Runnable:可运行状态,一旦调用start方法,就会处于Runnable状态。处于这个状态的线程可能正在运行,也可能没有,取决于操作系统的调度
  • Blocked:阻塞状态,表示线程被锁阻塞,暂不能活动
  • Waiting:等待状态,线程暂时不活动,并且不运行任何代码,这消耗最少的资源,知道调度器重新激活这个线程
  • Timed Waiting:超时等待,可以在指定的时间自行返回
  • Terminated:终止状态,表示当前线程已经执行完毕,比如run方法正常执行退出,或者因为没有被捕获的异常而终止

3. 创建线程

  • 继承Thread类,重写run方法
  • 实现Runnable接口,重写run方法
  • 实现Callable接口,重写call方法

4. 理解中断

当线程的run方法执行完毕,或者方法里出现没有捕获的异常时,线程就要终止。早期Java版本中有stop方法可以终止线程,现在已经被弃用。现版本用interrupt来中断线程,当一个线程调用interrupt方法时,它的中断标志位将被置为true。线程会时不时的检测这个中断标记位,以判断线程是否应该被中断,要想知道线程是否被置位,可以调用isInterrupted方法查看返回值。还可以调用静态方法interrupted来对中断标志位进行复位。但是如果一个线程被阻塞,就无法检测中断状态。如果一个线程处于阻塞状态,那么线程在检查中断标志位时若发现中断标志位为true,就会在阻塞方法调用处抛出阻塞异常,并且在抛出异常前将线程中断标志位复位,即重新设置为false。需要注意的是被中断的线程不一定会终止,中断线程是为了引起线程的注意,被中断的线程可以决定如何响应中断。如果是比较重要的线程,则不会理会中断。而大部分情况是线程会将中断作为一个终止的请求。另外,不要在底层代码里捕获InterruptedException不做处理,这里介绍两种合适的处理方式

  1. 在catch子句中,调用Thread.currentThread().interrupt()来设置中断状态。因为在抛出异常后中断标志位会复位,让外界通过判断isInterrupted()来决定是终止还是继续下去
void test(){
try{
sleep(50);
}catch(InterruptedException e){
Thread.currentThread().interrupt();
}
}
  1. 更好的做法是直接抛出异常,方便调用者捕获
void test() throw InterruptedException{
sleep(50);
}

5. 安全的终止线程

上一点我们提到了中断,首先用中断来终止线程,如下

public class test{
public static void main(String[] args) throws InterruptedException {
MyRunner runner = new MyRunner();
Thread thread = new Thread(runner, "MyRunner");
thread.start();
TimeUnit.MILLISECONDS.sleep(10);
thread.interrupt();
}

public static class MyRunner implements Runnable{
private long i;

@Override
public void run() {
while (!Thread.currentThread().isInterrupted()){
i++;
System.out.println("i=" + i);
}
System.out.println("stop");
}
}
}

代码里用sleep方法使得main线程沉睡10ms,留给MyRunner足够的时间来感知中断从而结束,还可以采用boolean变量来控制是否需要停止线程,如下

public class test{
public static void main(String[] args) throws InterruptedException {
MyRunner runner = new MyRunner();
Thread thread = new Thread(runner, "MyRunner");
thread.start();
TimeUnit.MILLISECONDS.sleep(10);
runner.cancel();
}

public static class MyRunner implements Runnable{
private long i;
private volatile boolean on = true;
@Override
public void run() {
while (on){
i++;
System.out.println("i=" + i);
}
System.out.println("stop");
}

public void cancel(){
on = false;
}
}
}

结果如下,两段代码是类似的

image.png

此处说明线程执行到了run方法的末尾,即将终止


线程同步

在多线程应用中,两个或者两个以上的线程需要共享对同一个数据的存取,如果两个线程存取相同的对象,并且每一个线程都调用了修改该对象的方法,这种情况被称为竞态条件。此时如果不用同步,是无法保证数据原子性的,所以我们就需要用到锁


1. 重入锁与条件对象

synchronized关键字自动提供了锁以及相关条件。大多数需要显示锁的情况使用synchronized非常方便。但是等我们了解了重入锁和条件对象时,能更好的理解synchronized关键字。重入锁ReentrantLock是Java SE 5.0引入的,就是支持重进入的锁,表示该锁能够支持一个线程对资源的重复加锁。具体结构如下

Lock mLock = new ReentrantLock();
mLock.lock();
try {

}catch (){

}finally {
mLock.unlock();
}

这一结构确保任何时刻只有一个线程进入临界区,临界区就是同一时刻只有一个任务访问的代码区域。一旦一个线程封锁了锁对象,其他线程都无法进入。把解锁操作放到finally区域内是十分必要的,如果因为某些异常,锁资源是必须要释放的,否则其他资源将被永久阻塞。进入临界区时,却发现在某一个条件满足之后它才能执行,这时可以用一个条件对象来管理那些已经获得了一把锁但是却不能做有用工作的线程,条件对象又被称作条件变量。通过下面例子来说明为何需条件对象。假设一个场景需要用支付宝转账,我们先写支付宝类,它的构造方法需传入支付宝账户的数量和每个账户的账户金额。

public class Alipay{
private double[] accounts;
private Lock alipayLock;
public Alipay(int n, double money){
accounts = new double[n];
alipayLock = new ReentrantLock();
for(int i = 0; i < accounts.length; i++){
accounts[i] = money;
}
}
}

接下来实现转账,需要一个from转账方,和to接收方,amount是转账金额,如下

public void transfer(int from, int to, int amount){
alipayLock.lock();
try {
while (accounts[from] < amount){
//wait
}
}catch (){

}finally {
alipayLock.unlock();
}
}

有可能会出现转账方余额不足的情况,如果有其他线程给这个转账方再转足够的钱,就可以转账成功了,但是这个线程已经获取了锁,具有排他性,别的线程无法获取锁来进行存款操作,这时我们就需要引入对象锁。一个锁对象拥有多个相关条件对象,可以用new Condition方法获得一个条件对象,我们得到条件对象后调用await方法,当前线程就被阻塞了并放弃了锁,相关代码如下

public class Alipay{
private double[] accounts;
private Lock alipayLock;
private Condition condition;
public Alipay(int n, double money){
accounts = new double[n];
alipayLock = new ReentrantLock();
condition = alipayLock.newCondition();
for(int i = 0; i < accounts.length; i++){
accounts[i] = money;
}
}

public void transfer(int from, int to, int amount) throws InterruptedException{
alipayLock.lock();
try {
while (accounts[from] < amount){
condition.await();
}
}catch (){

}finally {
alipayLock.unlock();
}
}
}

一旦一个线程调用await方法,就会进入该条件的等待集并处于阻塞状态,直到另一个线程调用了同一个条件的signalAll()方法时为止。当另一个线程转账给我们此前的转账方时,只重复调用singnalAll()方法,就会重新激活因为这一条件而等待的所有线程,代码如下

public void transfer(int from, int to, int amount) throws InterruptedException{
alipayLock.lock();
try {
while (accounts[from] < amount){
condition.await();
}
accounts[from] = accounts[from] - amount;
accounts[to] = accounts[to] + amount;
condition.signalAll();
}catch (){

}finally {
alipayLock.unlock();
}
}

当调用了signalAll时并不是立即激活一个等待线程,它仅仅解除了等待线程的阻塞,以便这些线程能够在当前线程退出同步方法后,通过竞态实现对对象的访问,还有个方法是signal,它则是随机解除某个线程的阻塞。如果该线程仍然不能运行,则再次被阻塞,如果没有其他线程再次调用signal,那么系统就死锁了


2. 同步方法

Lock接口和Condition接口为程序设计提供了高度的锁定控制,然而大多数情况下并不需要那样的控制,并且可以使用一种嵌入到Java语言内部的机制。Java中每一个对象都有一个内部锁,如果一个方法用synchronized关键字修饰,那么对象的锁将保护整个方法,也就是说,要调用该方法,线程必须获得内部的对象锁,如下

public synchronized void method(){
···
}

这段代码等价于

Lock mLock = new ReentrantLock();
public void method(){
mLock.lock();
try{
···
}finally{
mLock.unlock();
}
}

对于上面转账的例子,可以将Alipay的transfer方法声明为synchronized,而不是使用一个显示的锁。内部对象锁只有一个相关条件,wait方法将一个线程添加到等待集中,使用notifyAll或notify方法解除等待线程的阻塞状态。也就是说wait相当于调用condition.await(),notifyAll等价于signalAll,所以前面例子里的transfer方法也可以这么写

public synchronized void transfer(int from, int to, int amount) throws InterruptedException{
while (accounts[from] < amount){
wait();
}
accounts[from] = accounts[from] - amount;
accounts[to] = accounts[to] + amount;
notifyAll();
}

在此可以看到,使用sychronized关键字来编码要简练很多,由该锁来管理那些试图进入synchronized方法的线程,由该锁中的条件来管理那些调用wait的线程


3. 同步代码块

除了调用同步方法来获得锁,还可以通过使用同步代码块,如下

synchronized(obj){
···
}

其获得了obj的锁,obj是一个对象,我们用同步代码块进行改写上面的例子

public class Alipay{
private double[] accounts;
private Object lock = new Object();
private Condition condition;
public Alipay(int n, double money){
accounts = new double[n];
for(int i = 0; i < accounts.length; i++){
accounts[i] = money;
}
}

public synchronized void transfer(int from, int to, int amount) throws InterruptedException{
synchronized (lock){
accounts[from] = accounts[from] - amount;
accounts[to] = accounts[to] + amount;
}
}
}

在这里创建了一个名为lock的Object类,为的是使用Object类所持有的锁。同步代码块是非常脆弱的,通常不推荐使用,一般实现同步最好用Java的并发包下的集合类,比如阻塞队列。如果同步方法适合自己的程序,尽量使用同步方法,这样可以减少编写代码的数量,减少出错的概率,如果特别需要使用Lock/Condition结构提供的独有特性时,才使用Lock/Condition


4. volatile

有时,仅仅为了读写一个或者两个实例域就使用同步的话,显得开销过大,而volatile关键字为实例域的同步访问提供了免锁机制。如果声明一个域为volatile的话,那么编译器和虚拟机知道该域是可能被另一个线程并发更新的。当一个共享变量被volatile关键字修饰后,就具备了两个含义,一个含义是线程修改了变量的值时,变量的新值对于其他线程是立即可见的。另一个含义是禁止使用指令重排序,分为编译期重排序和运行时重排序。先来看一段代码,假设线程1先执行,2后执行,如下

//线程1
boolean stop = false;
while(!stop){
//doSomething
}

//线程2
stop = true;

这是一个线程中断的代码,但是这段代码不一定会将线程中断,虽说无法中断线程这个情况出现的概率很小,但是一旦发生便是死循环。因为每个线程都有私有的工作内存,因此线程1运行时会拷贝一份stop的值放入私有工作内存中,当线程2更改了stop的变量值并返回后,线程2突然需要做其他操作,这时就无法将更改的stop变量写入主存中,这样线程1就不知道线程2对stop变量进行了更改,因此线程1会一直执行下去。当stop用volatile修饰,线程2修改stop值时,会强制将修改的值立刻写入主存,这样使得线程1的工作内存中的stop变量缓存无效,这样线程1在此读取变量stop的值时就会去主存读取

volatile不保证原子性

另外volatile不保证原子性,可看如下代码演示

class Test {
public volatile int inc = 0;
public void increase() {
inc++;
}

public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(() -> {
for(int j=0;j<1000;j++)
test.increase();
}).start();
}
//保证前面的线程都执行完
while(Thread.activeCount()>1)
Thread.yield();
System.out.println(test.inc);
}
}

这段代码每次运行的结果都不一致,因为自增操作是不具备原子性的。自增操作里包含了读取原始值、加1、写入工作内存这三个子操作,也就是说这三个子操作可能被割裂执行。

volatile保证有序性

volatile关键字能禁止指令重排序,因此能保证有序性。禁止指令重排序有两层含义,其一是指代码运行到volatile变量操作时,在其前面的操作已经全部执行完毕,并且结果会对后面的操作可见,在其后面的操作还未执行。其二是进行指令优化时,在volatile变量之前的语句不能在volatile变量之后执行

正确使用volatile关键字

synchronized关键字可防止多个线程同时执行一段代码,但是这会很影响程序的执行效率。volatile关键字在有些时候会优于synchronized关键字。但是要注意volatile关键字时无法替代synchronized关键字的,因为其无法保证原子性,通常来说,使用volatile关键字需要具备以下两个条件:

  1. 对变量的写操作不依赖于当前值
  2. 该变量没有包含在具有其他变量的不变式中

关于第一点,就是上面提到的自增自减操作。关于第二点,举个例子,包含一个不变式:下界总是小于或等于上界,代码如下

public class NumberRange {
private volatile int lower, upper;
public int getLower() { return lower; }
public int getUpper() { return upper; }
public void setLower(int value) {
if (value > upper)
throw new IllegalArgumentException(...);
lower = value;
}
public void setUpper(int value) {
if (value < lower)
throw new IllegalArgumentException(...);
upper = value;
}
}

这种方式定义的upper和lower并不能充分实现类的线程安全,如果两个线程在同一时间使用不一致的值执行setLower和setUpper的话,就会使范围处于不一致的状态。例如,如果初始状态是(0,5),同一时间内,两个线程分别调用setLower(4)和setUpper(3),虽然这两个交叉存入的值是不符合条件的,但是这两个线程都会通过用于保护不变式的检查,使得最后范围是(4,3),显然是不对的


收起阅读 »

使用DialogFragment代替Dialog

使用DialogFragment代替Dialog是这样,用了很久的一个Dialog工具类,结果今天发现了一个bug,尝试着搜索发现大家都已经用DialogFragment了,官方也推荐这么做,猛然醒悟原来自己已经过时这么久了。现在就来试试吧。DialogFra...
继续阅读 »

使用DialogFragment代替Dialog

是这样,用了很久的一个Dialog工具类,结果今天发现了一个bug,尝试着搜索发现大家都已经用DialogFragment了,官方也推荐这么做,猛然醒悟原来自己已经过时这么久了。现在就来试试吧。

DialogFragment是什么

DialogFragment从它的源码得知,它继承了Fragment,其实是一个比较特殊的Fragment。那么它相对于普通的Dialog有什么不同,谷歌又为什么推荐我们使用它呢,它相对于普通的Dialog有什么优点呢。

使用过它之后用自己的感受描述:

  • 它的生命周期很清晰,方便写复杂的逻辑
  • 它于Activity的生命周期是绑定的,Activity消失,DialogFragment也会消失。
  • 它可以很简单的控制弹窗的布局。

总结就是dialogfragment能更好的管理dialog的展示与消失,以及在屏幕旋转时的一些状态保存问题。

DialogFragment的踩坑

即使它有很多的优点,但使用不当时,仍然会有很多坑。
我遇到了很多奇奇怪怪的问题。 比如

  • Fragment already added 异常
  • 快速的显示消失,无法消失的异常

当然除此之外我们还可能有以下需求:

  • 设置对话框的大小
  • 设置弹出对话框时背景灰色或者透明

下面我们就来一一实现。

如何实现DialogFragment

重点来关注两个方法。

  • onCreateDialog 新建一个Dialog即可使用
  • onCreateView 自定义一个Dialog界面

onCreateDialog

Screenshot_2021-11-04-22-04-55-27_2a27335eaa331505125090a61677c0b2.jpg

做一个简单的对话框

public class ConfirmDialog extends DialogFragment {
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return super.onCreateView(inflater, container, savedInstanceState);
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog dialog = new AlertDialog.Builder(getActivity())
.setTitle("提示")
.setMessage("确认要退出吗")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {

}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {

}
}).create();
return dialog;
}
}

显示它

ConfirmDialog dialog = new ConfirmDialog();
dialog.show(getSupportFragmentManager(), "dialogTag");

onCreateView

使用自定义视图做一个加载框。
这里有一个非常重要的地方,我出现了 Fragment already added 的问题,意思就是重复添加了,那么为什么会出现重复添加呢,因为我最初的代码是使用isAddedisVisibility进行判断,但是当快速执行的时候,这两个方法并不准确。

正确的做法有两种。

  • 添加事务时先进行移除
beginTransaction().remove(this).commit()
  • 使用变量进行判断,不能使用isAdded
private boolean isShowFragment = false;

@Override
public void show(@NonNull FragmentManager manager, @Nullable String tag) {
// 解决bug:Android java.lang.IllegalStateException: Fragment already added
if (this.isShowFragment) {
return;
}
this.isShowFragment = true;
super.show(manager, tag);
}

@Override
public void dismiss() {
super.dismiss();
this.isShowFragment = false;
}

// 避免有些手机兼容性问题,isShowFragment未变成false而导致无法二次打开
@Override
public void onDestroy() {
super.onDestroy();
this.isShowFragment = false;
}

最后直接放代码,封装好的Loading框

LoadingDialog 对话框 可以看到代码中对bug的处理:在每个add事务前增加一个remove事务,防止连续的add。

public class LoadingDialog extends DialogFragment
implements DialogInterface.OnKeyListener {
/**
* 加载框提示信息 设置默认
*/
private final String hintMsg = "加载中...";

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStyle(DialogFragment.STYLE_NO_TITLE, R.style.MyDialog);
}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
Dialog dialog = getDialog();
// 设置背景透明
if (dialog.getWindow() != null)
dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
// 去掉标题
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
dialog.setCanceledOnTouchOutside(false);
View loadingView = inflater.inflate(R.layout.dialog_loading, container);
TextView hintTextView = loadingView.findViewById(R.id.tv_ios_loading_dialog_hint);
hintTextView.setText(hintMsg);
//不响应返回键
dialog.setOnKeyListener(this);
return loadingView;
}

@Override
public void show(FragmentManager manager, String tag) {
try {
//在每个add事务前增加一个remove事务,防止连续的add
manager.beginTransaction().remove(this).commit();
super.show(manager, tag);
} catch (Exception e) {
//同一实例使用不同的tag会异常,这里捕获一下
e.printStackTrace();
}
}

@Override
public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
// return keyCode == KeyEvent.KEYCODE_BACK;
// 允许按back键取消Loading
return false;
}

}

代理管理类

public class GlobalDialogManager {

private LoadingDialog mLoadingDialog;

private GlobalDialogManager() {
init();
}

public static GlobalDialogManager getInstance() {
return SingletonHolder.INSTANCE;
}

private static class SingletonHolder {
private static final GlobalDialogManager INSTANCE = new GlobalDialogManager();
}

public void init() {
if (mLoadingDialog == null) {
mLoadingDialog = new LoadingDialog();
}
}

/**
* 展示加载框
*/
public synchronized void show(FragmentManager manager) {
if (manager != null && mLoadingDialog != null) {
mLoadingDialog.show(manager, "tag");
}
}

/**
* 隐藏加载框
*/
public synchronized void dismiss(FragmentManager manager) {
if (mLoadingDialog != null && !manager.isDestroyed()) {
mLoadingDialog.dismissAllowingStateLoss();
}
}
}

使用它

if (getContext() != null)
GlobalDialogManager.getInstance().show(((Activity) getContext()).getFragmentManager());

if (getContext() != null)
GlobalDialogManager.getInstance().dismiss(((Activity) getContext()).getFragmentManager());

这里判断getContext()很有必要,避免Activity消失了,getContext为空的bug。

背景不变暗设置一个style属性行啦。

<item name="android:backgroundDimEnabled">false</item><!--activity不变暗-->

收起阅读 »

栈的实现

一、栈 💦 栈的概念及结构 栈:一种特殊的线性表,其只允许在固定的一端插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。 栈中的数据元素遵守后进先出LIFO (Last In First Out) 的原则;同时对于栈来说,一种入栈顺序对...
继续阅读 »

一、栈


💦 栈的概念及结构


栈:一种特殊的线性表,其只允许在固定的一端插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。
栈中的数据元素遵守后进先出LIFO (Last In First Out) 的原则;同时对于栈来说,一种入栈顺序对应多种出栈顺序

栈有两个经典的操作


1️⃣ 压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。


2️⃣ 出栈:栈的删除操作叫做出栈。出数据也在栈顶 。


在这里插入图片描述


💦 栈的实现


这里对于栈的实现我们既可以选择数组也可以和选择链表两者的效率都差不多,但是还是建议使用数组
在这里插入图片描述


1.初始化

函数原型


在这里插入图片描述


函数实现


void StackInit(ST* ps)
{
assert(ps);
//初始化
ps->a = NULL;
ps->top = 0;
ps->capacicy = 0;
}

2.插入

函数原型


在这里插入图片描述


函数实现


void StackPush(ST* ps, STDatatype x)
{
assert(ps);
//检查空间,满了就增容
if (ps->top == ps->capacicy)
{
//第一次开辟空间容量为4,其它次容量为当前容量*2
int newcapacity = ps->capacicy == 0 ? 4 : ps->capacicy * 2;
//第一次开辟空间,a指向空,realloc的效果同malloc
STDatatype* tmp = realloc(ps->a, sizeof(STDatatype) * newcapacity);
//检查realloc
//realloc失败
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
//realloc成功
ps->a = tmp;
ps->capacicy = newcapacity;
}
//插入数据
ps->a[ps->top] = x;
ps->top++;
}

3.判空

函数原型


在这里插入图片描述


函数实现


bool StackEmpty(ST* ps)
{
assert(ps);
//等于0是真,否则为假
return ps->top == 0;
}

4.删除

函数原型


在这里插入图片描述


函数实现


void StackPop(ST* ps)
{
assert(ps);
//删除的话得保证指向的空间不为空
assert(!StackEmpty(ps));
//删除
--ps->top;
}

5.长度

函数原型


在这里插入图片描述


函数实现


int StackSize(ST* ps)
{
assert(ps);
//此时的top就是长度
return ps->top;
}

6.栈顶

函数原型


在这里插入图片描述


函数实现


STDatatype StackTop(ST* ps)
{
assert(ps);
//找栈顶的话得保证指向的空间不为空
assert(!StackEmpty(ps));
//此时的top-1就是栈顶数据
return ps->a[ps->top - 1];
}

7.销毁

函数原型


在这里插入图片描述


函数实现


void StackDestory(ST* ps)
{
assert(ps);
//a为真代表它指向动态开辟的空间
if (ps->a)
{
free(ps->a);
}
ps->a = NULL;
ps->top = 0;
ps->capacicy = 0;
}

💦 完整代码


这里需要三个文件


1️⃣ Static.h,用于函数的声明


2️⃣ Static.c,用于函数的定义


3️⃣ Test.c,用于测试函数




🧿 Stack.h

#pragma once

//头
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>

//结构体
typedef int STDatatype;
typedef struct Stack
{
STDatatype* a; //指向动态开辟的空间
int top; //栈顶
int capacicy; //容量
}ST;

//函数
//注意链表和顺序表我们写Print,但是栈不写,因为如果栈可以Print的话,就不符合后进先出了
//初始化
void StackInit(ST* ps);
//插入
void StackPush(ST* ps, STDatatype x);
//判空
bool StackEmpty(ST* ps);
//删除
void StackPop(ST* ps);
//长度
int StackSize(ST* ps);
//栈顶
STDatatype StackTop(ST* ps);
//销毁
void StackDestory(ST* ps);

🧿 Stack.c

#include"Stack.h"

void StackInit(ST* ps)
{
assert(ps);
//初始化
ps->a = NULL;
ps->top = 0;
ps->capacicy = 0;
}
void StackPush(ST* ps, STDatatype x)
{
assert(ps);
//检查空间,满了就增容
if (ps->top == ps->capacicy)
{
//第一次开辟空间容量为4,其它次容量为当前容量*2
int newcapacity = ps->capacicy == 0 ? 4 : ps->capacicy * 2;
//第一次开辟空间,a指向空,realloc的效果同malloc
STDatatype* tmp = realloc(ps->a, sizeof(STDatatype) * newcapacity);
//检查realloc
//realloc失败
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
//realloc成功
ps->a = tmp;
ps->capacicy = newcapacity;
}
//插入数据
ps->a[ps->top] = x;
ps->top++;
}
bool StackEmpty(ST* ps)
{
assert(ps);
//等于0是真,否则为假
return ps->top == 0;
}
void StackPop(ST* ps)
{
assert(ps);
//删除的话得保证指向的空间不为空
assert(!StackEmpty(ps));
//删除
--ps->top;
}
int StackSize(ST* ps)
{
assert(ps);
//此时的top就是长度
return ps->top;
}
STDatatype StackTop(ST* ps)
{
assert(ps);
//找栈顶的话得保证指向的空间不为空
assert(!StackEmpty(ps));
//此时的top-1就是栈顶数据
return ps->a[ps->top - 1];
}
void StackDestory(ST* ps)
{
assert(ps);
//a为真代表它指向动态开辟的空间
if (ps->a)
{
free(ps->a);
}
ps->a = NULL;
ps->top = 0;
ps->capacicy = 0;
}

🧿 Test.c

#include"Stack.h"

int main()
{
ST st;
//初始化
StackInit(&st);
//插入+删除
StackPush(&st, 1);
StackPush(&st, 2);
StackPush(&st, 3);
StackPush(&st, 4);
StackPush(&st, 5);
StackPop(&st);
StackPop(&st);
//长度
StackSize(&st);
//栈顶
StackTop(&st);
//销毁
StackDestory(&st);
return 0;
}

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

一篇文章了解Java之网络编程

一、网络基础知识 网络编程的目的就是指直接或间接地通过网络协议与其他计算机进行通讯。 计算机网络形式多样,内容繁杂。网络上的计算机要互相通信,必须遵循一定的协议。目前使用最广泛的网络协议是Internet上所使用的TCP/IP协议。 IP地址:具有全球唯一性,...
继续阅读 »

一、网络基础知识


网络编程的目的就是指直接或间接地通过网络协议与其他计算机进行通讯。


计算机网络形式多样,内容繁杂。网络上的计算机要互相通信,必须遵循一定的协议。目前使用最广泛的网络协议是Internet上所使用的TCP/IP协议。


IP地址:具有全球唯一性,相对于internet,IP为逻辑地址。


IP地址分类:


1. A类地址


A类地址第1字节为网络地址,其它3个字节为主机地址。另外第1个字节的最高位固定为0。


A类地址范围:1.0.0.1到126.155.255.254。


A类地址中的私有地址和保留地址:


10.0.0.0到10.255.255.255是私有地址(所谓的私有地址就是在互联网上不使用,而被用在局域网络中的地址)。


127.0.0.0到127.255.255.255是保留地址,用做循环测试用的。


2. B类地址


B类地址第1字节和第2字节为网络地址,其它2个字节为主机地址。另外第1个字节的前两位固定为10。


B类地址范围:128.0.0.1到191.255.255.254。


B类地址的私有地址和保留地址


172.16.0.0到172.31.255.255是私有地址


169.254.0.0到169.254.255.255是保留地址。如果你的IP地址是自动获取IP地址,而你在网络上又没有找到可用的DHCP服务器,这时你将会从169.254.0.0到169.254.255.255中临得获得一个IP地址。


3. C类地址


C类地址第1字节、第2字节和第3个字节为网络地址,第4个个字节为主机地址。另外第1个字节的前三位固定为110。


C类地址范围:192.0.0.1到223.255.255.254。


C类地址中的私有地址:


192.168.0.0到192.168.255.255是私有地址。


4. D类地址


D类地址不分网络地址和主机地址,它的第1个字节的前四位固定为1110。


D类地址范围:224.0.0.1到239.255.255.254


Mac地址:每个网卡专用地址,也是唯一的。


端口(port):OS中可以有65536(2^16)个端口,进程通过端口交换数据。连线的时候需要输入IP也需要输入端口信息。


计算机通信实际上的主机之间的进程通信,进程的通信就需要在端口进行联系。


192.168.0.23:21


协议:为了进行网络中的数据交换(通信)而建立的规则、标准或约定。


不同层的协议是完全不同的。


网络层:寻址、路由(指如何到达地址的过程)


传输层:端口连接


TCP模型:应用层/传输层/网络层/网络接口


端口是一种抽象的软件结构,与协议相关:TCP23端口和UDT23端口为两个不同的概念。


端口应该用1024以上的端口,以下的端口都已经设定功能。


TCP/IP模型


Application


(FTP,HTTP,TELNET,POP3,SMPT)


Transport


(TCP,UDP)


Network


(IP,ICMP,ARP,RARP)


Link


(Device driver,….)


注:


IP:寻址和路由


ARP(Address Resolution Protocol)地址解析协议:将IP地址转换成Mac地址


RARP(Reflect Address Resolution Protocol)反相地址解析协议:与上相反


ICMP(Internet Control Message Protocol)检测链路连接状况。利用此协议的工具:ping , traceroute


二、TCP Socket


TCP是Tranfer Control Protocol的简称,是一种面向连接的保证可靠传输的协议。通过TCP协议传输,得到的是一个顺序的无差错的数据流。发送方和接收方的成对的两个socket之间必须建立连接,以便在TCP协议的基础上进行通信,当一个socket(通常都是server socket)等待建立连接时,另一个socket可以要求进行连接,一旦这两个socket连接起来,它们就可以进行双向数据传输,双方都可以进行发送或接收操作。


   1) 服务器分配一个端口号,服务器使用accept()方法等待客户端的信号,信号一到打开socket连接,从socket中取得OutputStream和InputStream。


   2) 客户端提供主机地址和端口号使用socket端口建立连接,得到OutputStream和InputStream。


TCP/IP的传输层协议


1、 建立TCP服务器端


创建一个TCP服务器端程序的步骤:


    1). 创建一个ServerSocket


    2). 从ServerSocket接受客户连接请求


    3). 创建一个服务线程处理新的连接


    4). 在服务线程中,从socket中获得I/O流


    5). 对I/O流进行读写操作,完成与客户的交互


    6). 关闭I/O流


    7). 关闭Socket


ServerSocket server = new ServerSocket(post)


Socket connection = server.accept();


ObjectInputStream put=new ObjectInputStream(connection.getInputStream());


ObjectOutputStreamo put=newObjectOutputStream(connection.getOutputStream());  


处理输入和输出流;


关闭流和socket。


2、 建立TCP客户端


创建一个TCP客户端程序的步骤:


1).创建Socket


    2). 获得I/O流


    3). 对I/O流进行读写操作


    4). 关闭I/O流


    5). 关闭Socket


Socket connection = new Socket(127.0.0.1, 7777);

ObjectInputStream input=new ObjectInputStream(connection.getInputStream());

ObjectOutputStream utput=new ObjectOutputStream(connection.getOutputStream());


处理输入和输出流;


关闭流和socket。


三、 建立UDP连接


UDP是User Datagram Protocol的简称,是一种无连接的协议,每个数据报都是一个独立的信息,包括完整的源地址或目的地址,它在网络上以任何可能的路径传往目的地,因此能否到达目的地,到达目的地的时间以及内容的正确性都是不能被保证的。


         比较:TCP在网络通信上有极强的生命力,例如远程连接(Telnet)和文件传输(FTP)都需要不定长度的数据被可靠地传输;既然有了保证可靠传输的TCP协议,为什么还要非可靠传输的UDP协议呢?主要的原因有两个。一是可靠的传输是要付出代价的,对数据内容正确性的检验必然占用计算机的处理时间和网络的带宽,因此TCP传输的效率不如UDP高。二是在许多应用中并不需要保证严格的传输可靠性,比如视频会议系统,并不要求音频视频数据绝对的正确,只要保证连贯性就可以了,这种情况下显然使用UDP会更合理一些。


如:http://www.tarena.com.cn:80/teacher/zhu…


协议名://机器名+端口号+文件名


2 . URL类的常见方法


一个URL对象生成后,其属性是不能被改变的,但是我们可以通过类URL所提供的方法来获取这些属性:


   public String getProtocol() 获取该URL的协议名。


   public String getHost() 获取该URL的主机名。


   public int getPort() 获取该URL的端口号,如果没有设置端口,返回-1。


   public String getFile() 获取该URL的文件名。


   public String getRef() 获取该URL在文件中的相对位置。


   public String getQuery() 获取该URL的查询信息。


   public String getPath() 获取该URL的路径


   public String getAuthority() 获取该URL的权限信息


   public String getUserInfo() 获得使用者的信息


   public String getRef() 获得该URL的锚



  1. 例子,将tarena网站首页拷贝到本机上。


import java.net.*;

import java.io.*;

import java.util.*;



public class TestURL{



         public static void main(String[] arg){



                   System.out.println("http://www.tarena.com.cn:80/index.htm===>");

                   //System.out.println(getWebContent());

                   writeWebFile(getWebContent());

             }

   

             public static String getWebContent(){

   

                       URL url = null;

                       HttpURLConnection uc = null;

                       BufferedReader br = null;

                       final int buffLen = 2048;

                       byte[] buff = new byte[buffLen];

                       String message = "";

                       String tmp = "";

                       int len = -1;



                       String urlStr = "http://www.tarena.com.cn:80/index.htm";

                      

                       try{

                             url = new URL(urlStr);

                             //连接到web资源

                             System.out.println("before openConnection ====>"+new Date());

                             uc = (HttpURLConnection)url.openConnection();

                             System.out.println("end openConnection ====>"+new Date());

                             br = new BufferedReader( new InputStreamReader(uc.getInputStream()));

                             System.out.println("end getINputStream() ====>"+new Date());



                             while( ( tmp = br.readLine())!=null){

                                    message += tmp;

                             }

                             System.out.println("end set message ====>"+new Date());



                       }catch(Exception e){e.printStackTrace();System.exit(1);}

                       finally{



                             if(br!=null){

                                    try{

                                           br.close();

                                    }catch(Exception ioe){ioe.printStackTrace();}

                             }

                    }



                    return  message;

    }

   

    public static void writeWebFile(String content){

   

             FileWriter fw = null;

             try{

                       fw = new FileWriter("index.htm");

                       fw.write(content,0,content.length());

             }catch(Exception e){

                       e.printStackTrace();

             }finally{

                       if(fw!=null){

                                try{

                                         fw.close();

                                }catch(Exception e){}

                       }

             }

            

    }

}


四、UDP socket


这种信息传输方式相当于传真,信息打包,在接受端准备纸。


特点:




  1. 基于UDP无连接协议




  2. 不保证消息的可靠传输




  3. 它们由Java技术中的DatagramSocket和DatagramPacket类支持




DatagramSocket(邮递员):对应数据报的Socket概念,不需要创建两个socket,不可使用输入输出流。


DatagramPacket(信件):数据包,是UDP下进行传输数据的单位,数据存放在字节数组中,其中包括了目标地址和端口以及传送的信息(所以不用建立点对点的连接)。


DatagramPacket的分类:


用于接收:DatagramPacket(byte[] buf,int length)


                      DatagramPacket(byte[] buf,int offset,int length)


用于发送:DatagramPacket(byte[] buf,int length, InetAddress address,int port )


                      DatagramPacket(byte[] buf,int offset,int length,InetAddress address,int port)


注:InetAddress类网址用于封装IP地址


没有构造方法,通过


InetAddress.getByAddress(byte[] addr):InetAddress


InetAddress.getByName(String host):InetAddress


等。


1、建立UDP 发送端


创建一个UDP的发送方的程序的步骤:


    1). 创建一个DatagramPacket,其中包含发送的数据和接收方的IP地址和端口


号。


    2). 创建一个DatagramSocket,其中包含了发送方的IP地址和端口号。


    3). 发送数据


    4). 关闭DatagramSocket


byte[] buf = new byte[1024];

DatagramSocket datagramSocket = new DatagramSocket(13);// set port

DatagramPacket intputPacket = new DatagramPacket (buf,buf.length);

datagramSocket.receive(inputPacket);

DatagramPacket  outputPacket = new DatagramPacket (buf,buf.length,

inetAddress,port);

datagramSocket.send(outputPacket);


没建立流所以不用断开。


2、 建立UDP 接受端


创建一个UDP的接收方的程序的步骤:


    1). 创建一个DatagramPacket,用于存储发送方发送的数据及发送方的IP地址和端口号。


    2). 创建一个DatagramSocket,其中指定了接收方的IP地址和端口号。


    3). 接收数据


    4). 关闭DatagramSocket


byte[] buf = new byte[1024];

DatagramSocket datagramSocket = new DatagramSocket();//不用设端口,因为发送的包中端口

DatagramPacket outputPacket=new DatagramPacket(

Buf, buf.length, serverAddress, serverPort);

DatagramPacket inputPacket=new DatagramPacket(buf, buf.length);

datagramSocket.receive(inputPacket);


URL类:可直接送出或读入网络上的数据。


Socket类:可以想象为两个不同主机的程序通过网络的通信信道。


Datagram类:它把数据的目的记录放在数据包中,然后直接放到网络上。


InetAddress类:用来存储internet地址的类(ip地址,域名)。



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

熬夜再战Android-Button实现selector选择器

前提 这是小空熬夜写的Android新手向系列,欢迎品尝。 selector是按钮最常用的功能,对美化控件的作用很大。 上节我们说了selector和shape联合使用,但偏向shape的介绍,今天主要说selector。 👉实践过程 我们先按照上一节的sha...
继续阅读 »

前提


这是小空熬夜写的Android新手向系列,欢迎品尝。


selector是按钮最常用的功能,对美化控件的作用很大。


上节我们说了selector和shape联合使用,但偏向shape的介绍,今天主要说selector。


👉实践过程


我们先按照上一节的shape方式创建两个shape背景

btn_selector_shape1.xml


<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <!-- 圆角 -->
    <corners android:radius="5dp" />
    <!--填充颜色-->
    <solid android:color="#00ff00" />
</shape>

btn_selector_shape2.xml


<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <!--圆角-->
    <corners android:radius="5dp" />
    <!--填充颜色-->
    <solid android:color="#0000ff" />
</shape>

接着我们在【res-drawable】右键创建个Drawable Resource File ,弹出框写文件名创建文件,设置默认【Root element】为selector。


btn_selector0.xml


<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/btn_selector_shape1" android:state_pressed="true" />
    <item android:drawable="@drawable/btn_selector_shape2" android:state_window_focused="false" />
</selector>

布局中引用


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white"
    tools:context=".TextActivity">
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="160dp"
        android:background="@drawable/btn_selector0"
        android:text="按下变色"
        android:textColor="@color/white" />
</RelativeLayout>

我们运行下看看


image.png


image.png


但是


我们回忆下,刚才是不是创建了三个文件,按钮少的情况下还好,自定义的按钮一多,这么多文件非常不容易管理,所以我们要用另外一种写法,将所有内容放到一个文件中。


我们在刚才的btn.selector0.xml中修改:


<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <!--这是第一种方式,利用drwable引用文件-->
    <!--<item android:drawable="@drawable/btn_selector_shape1" android:state_pressed="true" />-->
    <!--<item android:drawable="@drawable/btn_selector_shape2" android:state_pressed="false" />-->
    <!--第二种方式如下-->
    <item android:state_pressed="false">
        <shape android:shape="rectangle">
            <!-- 圆角 -->
            <corners android:radius="5dp" />
            <!--填充颜色为白色-->
            <solid android:color="#0000ff" />
        </shape>
    </item>
    <!--单击时是一个带圆角,白色背景,绿色边框的矩形-->
    <item android:state_pressed="true">
        <shape android:shape="rectangle">
            <!--圆角-->
            <corners android:radius="5dp" />
            <!--填充颜色为白色-->
            <solid android:color="#00ff00" />
        </shape>
    </item>
</selector>

我们运行起来看看,哎,效果很正确啊


Selector的属性不止这两个哦:



  • state_focused 布尔值,是否获得焦点

  • state_window_focused 布尔值,是否获得窗口焦点

  • state_enabled 布尔值,控件是否可用

  • state_checkable 布尔值,控件可否被勾选

  • state_checked 布尔值,控件是否被勾选

  • state_selected 布尔值,控件是否被选择,针对有滚轮的情况

  • state_pressed 布尔值,控件是否被按下

  • state_active 布尔值,控件是否处于活动状态

  • state_singlestate_firststate_middle很少使用,知道就行。

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

View 事件分发机制详解

View事件传递过程先从Activity-->Window-->View。public boolean dispatchTouchEvent(MotionEvent ev) { boolean consume = false; ...
继续阅读 »

View事件传递过程先从Activity-->Window-->View。

public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
//1
if (onInterceptTouchEvent(ev)) {
//2
consume = onTouchEvent(ev);
} else {
//3
consume = child.dispatchTouchEvent(ev);
}
return consume;
}

这个都是事件分发的伪代码,

  • 注释1调用自己ViewGroup的onInterceptTouchEvent方法是否需要拦截此事件
  • 拦截事件调用注释2此View自己消耗调用方法onTouchEvent方法
  • 注释1不拦截这调用注释3子View的dispatchTouchEvent方法

接着我们继续分Activity的dispatchTouchEvent方法,代码如下:

public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
//1
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//2
return onTouchEvent(ev);
}
  • 注释1 可以看出,Activity把事件传递给Window的superDispatchTouchEvent方法让Window处理事件,可看出如果Window不消费事件
  • 注释2 Activity的onTouchEvent方法。我们继续看Window是如何实现的,Window是个抽象类,具体实现者是PhoneWindow

源码如下。

//1
private DecorView mDecor;
public boolean superDispatchTouchEvent(MotionEvent event) {
//2
return mDecor.superDispatchTouchEvent(event);
}
  • 注释1 mDecor是DecorView类的引用,DecorView继承FrameLayout
  • 注释2 调用ViewGroup的superDispatchTouchEvent方法

接着我们继续分析superDispatchTouchEvent源码,其最终调用了ViewGroup的dispatchTouchEvent方法,代码如下:

 public boolean dispatchTouchEvent(MotionEvent ev) {
//1
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {//2
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//3
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
return handled;
}
  • 注释1处当事件ACTION_DOWN时进行初始化(mFirstTouchTarget、mGroupFlags重置),事件是由ACTION_DOWN开始以ACTION_UP结束,这样做的目的在于每次事件不会因为之前设置受影响
  • 注释2处 mFirstTouchTarget(后面细讲)表示子元素是否处理事件,如果处理则不为空。如果ViewGroup拦截事件,在ACTION_MOVE和ACTION_UP状态时都不会调用onInterceptTouchEvent方法且intercept为true,ViewGroup消费这些事件
  • 注释3处 子View通过ViewGroup的requestDisallowInterceptTouchEvent方法告知父容器不要拦截事件,让子View处理。 接着继续分析dispatchTouchEvent方法代码如下:
    @Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//1
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
//2
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
//3
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
//4
newTouchTarget = addTouchTarget(child, idBitsToAssign);
break;
}
}
return handled;
}
  • 注释1处 倒叙遍历ViewGroup子元素,最上面一层最先遍历
  • 注释2处 判断子View是否接收事件,触摸点是否在子View上
  • 注释3处 调用了dispatchTransformedTouchEvent方法,如果返回值为true,表示有子View消费事件
  • 注释4处 调用addTouchTarget方法具体源码如下
    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
//1
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
//2
target.next = mFirstTouchTarget;
//3
mFirstTouchTarget = target;
return target;
}

把需要消费事件的View包装成TouchTarget类的单链表结构。

  • 注释1处 从缓存池拿TouchTarget对象
  • 注释2处 TouchTarget类的target对象作为头指针,target的next指向mFirstTouchTarget引用
  • 注释3处 将target对象赋值给mFirstTouchTarget即成为单链表的表头

mFirstTouchTarget: 存储本轮(ACTION_DOWN到ACTION_UP)需要消耗事件的子View的单链表,也是单链表表头。
我们继续分析ViewGroup类dispatchTouchEvent方法最后一部分代码如下:

    public boolean dispatchTouchEvent(MotionEvent ev) {
//1
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget target = mFirstTouchTarget;
//2
while (target != null) {
final TouchTarget next = target.next;
//3
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
//4
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
}
predecessor = target;
target = next;
}
}
return handled;
}

根据mFirstTouchTarget链表头指针,判断事件交给谁来处理。

  • 注释1处 没有子View来处理事件,则交由自己处理
  • 注释2处 while循环遍历单链表
  • 注释3处 判断是否已经消费此事件
  • 注释4处 事件分发给子View的dispatchTouchEvent方法

接着我们继续分析这个神秘的方法dispatchTransformedTouchEvent方法源码如下:

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
if (child == null) {
//1
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
//2
handled = child.dispatchTouchEvent(transformedEvent);
}
return handled;
  • 注释1处 如果ViewGroup没有子元素则调用父类View的dispatchTouchEvent方法,把事件交给自己处理
  • 注释2处 调用子元素的dispatchTouchEvent方法,实现事件的分发

继续看View的dispatchTouchEvent方法源码实现:

public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
if (onFilterTouchEventForSecurity(event)) {
ListenerInfo li = mListenerInfo;
//1
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//2
if (!result && onTouchEvent(event)) {
result = true;
}
}
return result;
}
  • 注释1处 判断mOnTouchListener不为空则调用onTouch方法,则不会调用onTouchEvent方法,说明onTouch方法调用的优先级高于它
  • 注释2处调用onTouchEvent方法

继续分析onTouchEvent方法源码如下:

 public boolean onTouchEvent(MotionEvent event) {
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
break;
}
return true;
}
return false;
}

只要View是CLICKABLE和LONG_CLICKABLE这个状态,都会返回true表示要消费。接着我们看看点击操作,代码如下:

public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
//1
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}

我们从注释1处看出,如果设置了点击事件这个回调onClick方法,说明onTouchEvent方法优先级高于onClick方法。

以上都是所有分析View事件分发机制


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

收起阅读 »

学习笔记-Retrofit源码解析

挖掘Retrofit:2.8.0源码。介绍Retrofit如何完成对OkHttp的封装,以及Retrofit如何支持的协程。1. BuilderRetrofit通过Retrofit.Builder创建,主要是配置各种工厂Factory。val retrofit...
继续阅读 »

挖掘Retrofit:2.8.0源码。介绍Retrofit如何完成对OkHttp的封装,以及Retrofit如何支持的协程。

1. Builder

Retrofit通过Retrofit.Builder创建,主要是配置各种工厂Factory

val retrofit = Retrofit.Builder()
.baseUrl("this is baseUrl")
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build()

Builder.builder()主要有五个步骤:

  1. 获取Platform对象,Platform是平台适配器,主要为了跨平台使用,类如安卓平台实现就是Android
  2. 初始化CallFactroyCallFactroy的作用是生产realCall,网络请求由他发出。默认值是OkHttpOKHttpClient
  3. 初始化Executor,默认通过Platform获取。
  4. 初始化CallAdapter.FactroyCallAdapter.Factroy生产CallAdapterCallAdapterrealCall的适配器,通过对realCall的包装,实现Executorjava8Futruerxjava等调度方法。存在多个,顺序是自定义配置-->默认配置。默认配置通过Platform获得。
  5. 初始化Converter.FactoryConverter.Factory生产ConverterConverter是数据转换器,将返回的数据转换为需要的数据,一般转换为我们要用的对象。存在多个,顺序是内置配置-->自定义配置-->默认配置。默认配置通过Platform获得。
class Builder{

Builder(Platform platform) {
this.platform = platform;
}

public Builder() {
//step 0
this(Platform.get());
}

public Retrofit build() {
if (baseUrl == null) {
throw new IllegalStateException("Base URL required.");
}

// step1
okhttp3.Call.Factory callFactory = this.callFactory;
if (callFactory == null) {
callFactory = new OkHttpClient();
}

//step2
Executor callbackExecutor = this.callbackExecutor;
if (callbackExecutor == null) {
callbackExecutor = platform.defaultCallbackExecutor();
}

//step3
List<CallAdapter.Factory> callAdapterFactories = new ArrayList<>(this.callAdapterFactories);
callAdapterFactories.addAll(platform.defaultCallAdapterFactories(callbackExecutor));

//step4
List<Converter.Factory> converterFactories = new ArrayList<>(
1 + this.converterFactories.size() + platform.defaultConverterFactoriesSize());
converterFactories.add(new BuiltInConverters());
converterFactories.addAll(this.converterFactories);
converterFactories.addAll(platform.defaultConverterFactories());

return new Retrofit(callFactory, baseUrl, unmodifiableList(converterFactories),
unmodifiableList(callAdapterFactories), callbackExecutor, validateEagerly);
}
}

到这里,Retrofit对象创建完成,一个大致的结构如下图:

retrofit结构.png

2. 创建API对象

Retrofit通过定义网络请求的接口设置请求参数和返回类型,通过调用retrofit.create()创建这个接口的对象,调用这个对象的方法生成最终的网络请求。

interface MyService {
@GET("/user")
fun getUser(): Observable<Response<User>>
}

val myServiceClass: MyService = retrofit.create(MyService::class.java)

进入到create方法,可以看到是直接调用Proxy.newProxyInstance()方法创建出对象。这是标准库提供的动态代理机制,在运行时创建接口的实例对象。

Proxy.newProxyInstance()方法有三个参数:

  • classLoader: 类加载器

  • interfaces:需要实现的接口

  • InvocationHandler:代理方法

public <T> T create(final Class<T> service) {
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new InvocationHandler() {
private final Platform platform = Platform.get();
private final Object[] emptyArgs = new Object[0];

@Override public @Nullable Object invoke(Object proxy, Method method,
@Nullable Object[] args) throws Throwable {
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
if (platform.isDefaultMethod(method)) {
return platform.invokeDefaultMethod(method, service, proxy, args);
}
return loadServiceMethod(method).invoke(args != null ? args : emptyArgs);
}
});
}

为了更好的理解动态代理,可以当做动态代理自动帮我们生成一个接口的实现类。将所有的方法都通过handler.invoke()代理,如下面的代码所示。只不过动态代理是运行时生成的这个类,并且是直接生成了字节码。

interface MyService {
@GET("/user")
fun getUser(): Observable<Response<User>>

@GET("/name")
fun getName(userId: String): Observable<Response<String>>
}

//自动生成的代码示例
class SuspendServiceProxy implements SuspendService {
InvocationHandler handler;

@NonNull
@Override
public Observable<Response<User>> getUser() {
return handler.invoke(
this,
SuspendService.class.getMethod("getUser", String.class),
new Object[]{}
);
}

@NonNull
@Override
public Observable<Response<String>> getName(@NonNull String userId) {
return handler.invoke(
this,
SuspendService.class.getMethod("getName", String.class),
new Object[]{userId}
);
}
}

3. 创建请求对象

创建好API对象之后,就可以调用它的方法创建请求对象。

val observable = myServiceClass.getUser()

根据前面可以知道,这个方法代理给了InvocationHandler,在这个方法首先判断这个方法对象是不是实体对象,如果是的话就直接调用就行。

如果不是一个对象,就把调用再代理给ServiceMethod。首先调用loadServiceMethod()创建ServiceMethod对象,然后调用invoke()方法得到返回值。

public @Nullable Object invoke(Object proxy, Method method, @Nullable Object[] args) throws Throwable {
// 判断这个方法对象是不是实体对象,如果是的话就直接调用
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
// 判断这个方法对象是不是实体对象,如果是的话就直接调用
if (platform.isDefaultMethod(method)) {
return platform.invokeDefaultMethod(method, service, proxy, args);
}
return loadServiceMethod(method).invoke(args != null ? args : emptyArgs);
}

3.1. 创建ServiceMethod对象

ServiceMethodRetrofit内部的自定义的代理方法,实际的逻辑是交给它处理。

abstract class ServiceMethod<T> {
abstract @Nullable T invoke(Object[] args);
}

loadServiceMethod()方法获取ServiceMethod对象。

方法对象Method作为Key缓存ServiceMethod对象在Retrofit中,因为创建ServiceMethod是一个耗时过程,所以弄成单例模式。

如果拿不到缓存,就调用ServiceMethod.parseAnnotations()创建一个ServiceMethod对象。

private final Map<Method, ServiceMethod<?>> serviceMethodCache = new ConcurrentHashMap<>();


ServiceMethod<?> loadServiceMethod(Method method) {
ServiceMethod<?> result = serviceMethodCache.get(method);
if (result != null) return result;

synchronized (serviceMethodCache) {
result = serviceMethodCache.get(method);
if (result == null) {
result = ServiceMethod.parseAnnotations(this, method);
serviceMethodCache.put(method, result);
}
}
return result;
}

ServiceMethod.parseAnnotations()中只做了一件事,创建RequestFactory然后将创建ServiceMethod的工作又交给了子类HttpServiceMethod处理。

RequestFactory是接口的参数配置,通过解析接口的注解,返回值,入参及其注解等获得这些参数。

之后将解析完成的数据传递给HttpServiceMethed,由它继续创建ServiceMethod

abstract class ServiceMethod<T> {
static <T> ServiceMethod<T> parseAnnotations(Retrofit retrofit, Method method) {
RequestFactory requestFactory = RequestFactory.parseAnnotations(retrofit, method);
...
return HttpServiceMethod.parseAnnotations(retrofit, method, requestFactory);
}

abstract @Nullable T invoke(Object[] args);
}

3.1.1. 创建RequstFactory

RequestFactory内部也是一个Builder模式。主要做了两件事:

  1. 遍历接口注解,初始化配置参数,这里读取的是POSTGET等注释。
  2. 遍历入参及其注解,将入参转换为ParameterHandler对象,将每个参数的设置配置的逻辑代理给了它处理。
final class RequestFactory {
static RequestFactory parseAnnotations(Retrofit retrofit, Method method) {
return new RequestFactory.Builder(retrofit, method).build();
}


}
}

3.1.2.2. OkHttpCall

最终创建的网络请求对象是OkHttpCall

OkHttpCall实现了Retrofit.Call接口,这个接口与OkHttp.Call基本一致,这里只介绍它的三个方法:

  1. execute()同步发起请求并且返回请求体Response
  2. enqueue()异步发起请求,通过Callback通信,需要注意的是处理的回调也是在异步中调用的。
  3. cancel()取消请求。

查看OkHttpCall的实现,可以发现所有的Call接口方法的具体实现都是代理给了rawCall

cancel()直接代理给rawCall.cancel()

execute()代理给rawCall.execute(),将返回值交给parseResponse()转换了一次。

enqueue()代理给rawCall.enqueue(),多加了一层Callback回调,在成功回调中也是交给parseResponse()转换之后再回调给原始的Callback

也就是说OkHttpCall把所有的逻辑静态代理给了rawCall,这样做的好处是可以在对应的地方做一下额外的处理,也就是获得返回值通过parseResponse()转换数据。

fAndroid平台默认的CallBackExecutorPlatform的实现类Android中,将Runnable抛到MainHandler中,实现回调到主线程。
static final class Android extends Platform {

@Override public Executor defaultCallbackExecutor() {
return MainThreadExecutor();
}

static class MainThreadExecutor implements Executor {
private final Handler handler = new Handler(Looper.getMainLooper());

@Override public void execute(Runnable r) {
handler.post(r);
}
}
}
3.1.2.3.2. RxJava2CallAdapterFactory

RxJava2CallAdapterFactory只在返回类型是Observable之类的时候创建CallAdapter

public final class RxJava2CallAdapterFactory extends CallAdapter.Factory {

@Override
public @Nullable CallAdapter<?, ?> get(
Type returnType, Annotation[] annotations, Retrofit retrofit) {
Class<?> rawType = getRawType(returnType);

if (rawType != Observable.class) { //省略了其他类型
return null;
}

boolean isResult = false;
boolean isBody = false;
Type responseType;


Type observableType = getParameterUpperBound(0, (ParameterizedType) returnType);
Class<?> rawObservableType = getRawType(observableType);
if (rawObservableType == Response.class) {
responseType = getParameterUpperBound(0, (ParameterizedType) observableType);
}
...
return new RxJava2CallAdapter(...);
}
}

RxJava2CallAdapter中的adapt()Call封装成observable返回。

final class RxJava2CallAdapter<R> implements CallAdapter<R, Object> {

@Override public Object adapt(Call<R> call) {
Observable<Response<R>> observable = new CallExecuteObservable<>(call);
...
return RxJavaPlugins.onAssembly(observable);
}
}

最后进到CallExecuteObservable,在启动的时候调用call.execute()并将结果抛给观察者。

final class CallExecuteObservable<T> extends Observable<Response<T>> {
private final Call<T> originalCall;

CallExecuteObservable(Call<T> originalCall) {
this.originalCall = originalCall;
}

@Override protected void subscribeActual(Observer<? super Response<T>> observer) {
Call<T> call = originalCall.clone();
try {
...
Response<T> response = call.execute();
observer.onNext(response);
}
}
}

3.2. 回顾

  1. Api接口实例动态代理HttpServiceMethod
  2. 网络请求的真实执行者是OkHttpClient创建的RealCall
  3. Api接口的调用到RealCall直接有两层静态代理 OkHttpCallCallAdapter
  4. OkHttpCall代理了RealCall,额外调用Converter做序列化处理。
  5. CallAdapter代理了OkHttpCall,可以在这里做扩展,将Call转换为实际的返回类型。

4. 协程实现

在看Retrofit如何实现协程之前,先梳理一下协程的基本概念。

当有一个延迟任务,后续的逻辑又需要等待这个任务执行完成返回数据,能继续执行,为了不阻塞线程,一般就需要就需要通过线程调度和传递回调来通信。在使用了协程之后,却只需要像同步代码那样书写,就可以完成这些操作。

但这并不是什么黑魔法,并不是用了协程之后不需要线程调度和传递回调,而是将这些繁琐的事进行复杂的封装并且为我们自动生成。将回调封装成Continuation,将线程的调度封装成调度器。

Continuation ,调用它的 resume 或者 resumeWithException 来返回结果或者抛出异常,跟我们所说的回调一模一样。

调度器的本质是一个协程拦截器,它拦截的对象就是Continuation,进而在其中实现回调的调度。调度器一般使用现成的,类如Dispatchers.Main,如果去挖它的源码,你会发现到了最后,还是使用的handler.post(),也跟我们所说的线程调度一模一样。

而前面有讲到Retrofit的实现很多时候需要依据返回类型做不同的处理,所以就需要了解协程是如何自动生成的回调代码和如何传递回调。写一个简单的协程接口,看一下转换后的Java代码,以及尝试在Java代码中调用协程接口。

可以看到返回值String被封装成了Continuation<String>作为入参传递,思考一下回调不也是这样实现的。

真实的返回值成了Object(用于状态机状态切换)。

//Kotlin代码
interface SuspendService {
suspend fun C(c1: Long): String
}

//字节码转化的Java代码
public interface SuspendService {
@Nullable
Object C(long var1, @NotNull Continuation var3);
}

//尝试在Java中调用suspend方法
class MMM {
SuspendService service;

public static void main(String[] args) {
MMM mmm = new MMM();
mmm.service.C(1L, new Continuation<String>() {
@NonNull
@Override
public CoroutineContext getContext() {
return null;
}

@Override
public void resumeWith(@NonNull Object o) {

}
});
}
}

接着回到动态代理那部分,因为suspend生成的代码会多加一个回调参数Continuation,那么动态代理的时候这个参数就会传入到代理的handler中。

Continuation的创建和使用十分的繁琐,最好的处理方法应该是把它再丢进一个kotlinsuspend方法中,让编译器去处理这些东西,而这个也就是Retrofit实现协程的原理。

interface SuspendService {
@GET("/user")
suspend fun getUser(): Response<User>

@GET("/name")
suspend fun getName(userId: String): Response<String>
}

//动态代理生成字节码示例
class SuspendServiceProxy implements SuspendService {
InvocationHandler handler;

@Nullable
@Override
public Object getUser(@NonNull Continuation<? super Response<User>> $completion) {
return handler.invoke(
this,
SuspendService.class.getMethod("getUser", Continuation.class),
new Object[]{$completion}
);
}

@Nullable
@Override
public Object getName(@NonNull String userId, @NonNull Continuation<? super Response<String>> $completion) {
return handler.invoke(
this,
SuspendService.class.getMethod("getName", String.class, Continuation.class),
new Object[]{userId, $completion}
);
}
}

接着再回到HttpServiceMethod,看看刚才被省略的代码。

在这里面会判断是不是suspend方法,判断的逻辑在RequestFactory中,判断的方法就是判断参数有没有Continuation对象,感兴趣可以去RequestFactory源码瞅瞅。

现在如果是suspend方法,会直接自定义一个类型adapterType,它的实际类型是Call,泛型是实际的返回类型(Response<T>里面的T)。之后将他作为返回类型去创建CallAdapter,而这里实际创建的就是DefaultCallAdapterFactoryExecutorCallbackCall。最后创建SuspendForResponse对象返回。

abstract class HttpServiceMethod<ResponseT, ReturnT> extends ServiceMethod<ReturnT> {

static <ResponseT, ReturnT> retrofit2.HttpServiceMethod<ResponseT, ReturnT> parseAnnotations(
Retrofit retrofit, Method method, RequestFactory requestFactory) {

/**
* 通过判断参数是不是Continuation,标志函数是不是suspend
* 在requestFactory内部处理
*/

boolean isKotlinSuspendFunction = requestFactory.isKotlinSuspendFunction;
...
Annotation[] annotations = method.getAnnotations();
Type adapterType;
if (isKotlinSuspendFunction) {
Type[] parameterTypes = method.getGenericParameterTypes();
Type responseType = Utils.getParameterLowerBound(0,
(ParameterizedType) parameterTypes[parameterTypes.length - 1]);

if (getRawType(responseType) == Response.class && responseType instanceof ParameterizedType) {
// Unwrap the actual body type from Response<T>.
responseType = Utils.getParameterUpperBound(0, (ParameterizedType) responseType);
continuationWantsResponse = true;
}
/**
* 自己新建一个返回类型,将实际的返回例行包装给Call
*/

adapterType = new Utils.ParameterizedTypeImpl(null, Call.class, responseType);
} else {
adapterType = method.getGenericReturnType();
}

CallAdapter<ResponseT, ReturnT> callAdapter =
createCallAdapter(retrofit, method, adapterType, annotations);
Type responseType = callAdapter.responseType();

Converter<ResponseBody, ResponseT> responseConverter =
createResponseConverter(retrofit, method, responseType);

okhttp3.Call.Factory callFactory = retrofit.callFactory;
if (!isKotlinSuspendFunction) {
return new CallAdapted<>(requestFactory, callFactory, responseConverter, callAdapter);
} else {
return (HttpServiceMethod<ResponseT, ReturnT>) new SuspendForResponse<>(requestFactory,
callFactory, responseConverter, (CallAdapter<ResponseT, Call<ResponseT>>) callAdapter);
}
}
}

SuspendForResponse中,adapt()的返回类型对应到了suspend的返回类型Object。并且其中的逻辑就是解析出CallContinuation对象,然后有调用KotlinExtensions.awaitResponse(),就如之前说的,它是一个suspend方法,在代理中不处理Continuation,而是交给编译器去处理。

static final class SuspendForResponse<ResponseT> extends HttpServiceMethod<ResponseT, Object> {
private final CallAdapter<ResponseT, Call<ResponseT>> callAdapter;

SuspendForResponse(RequestFactory requestFactory, okhttp3.Call.Factory callFactory,
Converter<ResponseBody, ResponseT> responseConverter,
CallAdapter<ResponseT, Call<ResponseT>> callAdapter) {
super(requestFactory, callFactory, responseConverter);
this.callAdapter = callAdapter;
}

@Override
protected Object adapt(Call<ResponseT> call, Object[] args) {
call = callAdapter.adapt(call);

Continuation<Response<ResponseT>> continuation =
(Continuation<Response<ResponseT>>) args[args.length - 1];
...
return KotlinExtensions.awaitResponse(call, continuation);
}
}

KotlinExtensions.awaitResponse()Call的扩展函数,扩展函数的实现是通过静态方法传入this的方法实现的,所以前面传入awaitResponse()的参数有两个,分别是Call对象和Continuation对象。

KotlinExtensions.awaitResponse()的主体是suspendCancellableCoroutine方法,suspendCancellableCoroutine运行在协程当中并且帮我们获取到当前协程的 CancellableContinuation 实例,CancellableContinuation是一个可取消的Continuation。通过调用它的 invokeOnCancellation 方法可以设置一个取消事件的回调,一旦这个回调被调用,那么意味着调用所在的协程被取消了,这时候我们也要相应的做出取消的响应,也就是把OkHttp发出去的请求给取消掉。这段建议多读几遍。

之后调用Call.enqueue()发送网络请求,在Callback中调用CancellableContinuation的 resume 或者 resumeWithException 来返回结果或者抛出异常。

这里的Callback也是经过了callAdapterOkHttpCall处理,乏了。

suspend fun <T> Call<T>.awaitResponse(): Response<T> {
return suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
cancel()
}
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
continuation.resume(response)
}

override fun onFailure(call: Call<T>, t: Throwable) {
continuation.resumeWithException(t)
}
})
}
}

//扩展函数示例
fun <T> Call<T>.awaitResponse(){
toString()
}

//扩展函数示例转换为Java代码
public static final void awaitResponse(@NotNull Call $this$awaitResponse) {
$this$awaitResponse.toString();
}

收起阅读 »

大厂Android高频问题:谈谈Activity的启动模式?

前言Activity可以说是Android开发面试高频的一道问题,但总有小伙伴在回答这道问题总不能让面试满意, 在这你就要搞清楚面试问你对Activity启动模式时,他最想听到的和其实想问的应该是哪些?下面我们通过以下几点来剖析这道问题!启动模式是什...
继续阅读 »

前言

Activity可以说是Android开发面试高频的一道问题,但总有小伙伴在回答这道问题总不能让面试满意, 在这你就要搞清楚面试问你对Activity启动模式时,他最想听到的和其实想问的应该是哪些?下面我们通过以下几点来剖析这道问题!

  1. 启动模式是什么?
  2. 启动模式如何设置?
  3. Activity的启动模式区别?
  4. 应用场景以及哪些注意的点?

1.activity堆栈流程以及四种启动模式

一个应用由多个Activity构成,多个Activity构成了任务,系统以栈方式进行管理任务(也就是管理多个Activity),管理方式为“先进后出”。

默认情况下,当用户点击App图标后,启动应用,这时会创建一个任务栈,并且将MAIN Activity压入栈中,作为栈底Activity。之后每启动一个Activity,就会将这个Activity压入栈中,显示处于栈顶的Activity。当用户点击“返回”键后,处于栈顶的Activity进行出栈销毁。

Android提供四种Activity的启动模式来进行入栈操作。

standard:

默认值,启动Activity都会重新创建一个Activity的实例进行入栈。此时Activity可能存在多个实例。

image.png

singleTop:

当Activity处于栈顶时,再启动此Activity,不会重新创建实例入栈,而是会使用已存在的实例。

image.png

singleTask:

与singleTop模式相似,只不过singleTop模式是只是针对栈顶的元素,而singleTask模式下,如果task栈内存在目标Activity实例,则:

  1. 将task内的对应Activity实例之上的所有Activity弹出栈。
  2. 将对应Activity置于栈顶,获得焦点。

image.png

singleInstance:

这是我们最后的一种启动模式,也是我们最恶心的一种模式:在该模式下,我们会为目标Activity分配一个新的affinity,并创建一个新的Task栈,将目标Activity放入新的Task,并让目标Activity获得焦点。新的Task有且只有这一个Activity实例。

如果已经创建过目标Activity实例,则不会创建新的Task,而是将以前创建过的Activity唤醒(对应Task设为Foreground状态)

image.png

2.启动模式如何设置?

AndroidMainfest.xml文件设置

设置的lanuchMode属性。可设置四个值: standard、singleTop、singleTask、singleInstance。若不设置默认为standard。

<activity 
android:name=".activity.MainActivity"
android:launchMode="standard"/>

Intent跳转标记Flag

FLAG_ACTIVITY_SINGLE_TOP 等价于 singleTop。位于栈顶的Activity会重用实例,调用onNewIntent函数接收intent。

Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
startActivity(intent);

FLAG_ACTIVITY_SINGLE_NEW_TASK,启动新的TASK,这个新的TASK取决于xml中设置的TaskAffinity(亲和性)属性。

首先去寻找是否存在相同亲和性的任务,如果存在,那么直接将这个Activity加入到这个任务中。若不存在,则新建一个任务来加入Activity。

FLAG_ACTIVITY_CLEAR_TOP,会将位于此Activity上方的Activity进行出栈销毁。

// singleTask的行为可使用代码表示为
Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);

两者区别:

  • xml设置为静态的
  • intent标记是动态的。intent标记Flag的优先级更高一些。所以当标记Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP后,尽管Activity为默认的standard模式,也同样会使用存在的实例,调用onNewIntent。

3.亲和性和多个任务并存

亲和性是指Activity设置在AndroidMainfest.xml中的taskAffinity属性。相同亲和性的Activity在同一个任务中,默认使用application的taskAffinity,也就是package name。

并不是设置taskAffinity就一定起作用,起作用是有条件的:

  • 同时设置了launchMode属性为singleTask。
  • Intent跳转时使用FLAG_ACTIVITY_NEW_TASK。
  • 同时设置allowTaskReparenting属性为true。
  • allowTaskReparenting可以使此Activity从启动任务中转移到该taskAffinity的任务中。此时需要发生Task reset(回到Home之后再进入app)才能看出效果。

不同亲和性意味着不同的任务,也就是同一个app中可以存在不同的任务,前台显示的任务的栈顶Activity为用户可见的Activity。当启动一个新的任务时,新的任务会覆盖当前任务。并且回退时,一个任务中Activity全部出栈,会将后台的任务调出,直到最后一任务的最后一个Activity出栈,app结束,回到Home。

例如: 一个应用有Main、A、B、C四个Activity,C的lanuchMode为singleTask,并且taskAffinity设置为.c,其他都为默认,那么按照启动顺序: Main->A->B->C

此时存在两个task:

  • 默认: Main->A->B (后台)
  • .c: C (前台)

由C启动A,那么此时task为:

  • 默认: Main->A->B->A (前台)

  • .c: C (后台)

按回退键:

  • 出栈顺序为: A、B、A、Main、C

4.应用场景以及需要避免的坑

  1. 新闻客户端的推送,点击打开新闻详情页,此时新闻详情页应该设置singleTop,避免用户在新闻详情页打开推送通知,使得回退出现两次详情页。

  2. 利用singleTask的特性,可以使得应用完全退出。

    • 注意:闪屏页+主页+其他的应用,可以设置主页为singleTask,因为闪屏页展示完就finish掉,栈底存在主页,用户点击回退键可以直接关闭应用。

    • 坑:避免启动MAIN的MainActivity设置为singleTask,这样当用户点击HOME,再重新启动应用时,将始终展示MainActivity,并且此时MainActivity走onNewIntent方法。

  3. singleInstance使用比较少,系统应用比如打电话可使用singleInstance。

  4. singleTop、singleTask、singleInstance在使用已存在的Activity实例时,都将走onNewIntent方法。

看完三件事❤️

如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

  1. 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  2. 关注公众号 『 小新聊Android 』,不定期分享原创知识
  3. 同时可以期待后续文章ing🚀

收起阅读 »

当我们讨论Android重建机制时在讨论什么?

前言Android应用有一个常常被忽略的问题,但问题出现时你又不得不面对。比如Activity横竖屏转换时Fragment重影应用长时间处于后台,并用户重新切到前台时,Activity显示异样或者需要等待一段时间才能显示内容这类问题都与Activity的恢复重...
继续阅读 »

前言

Android应用有一个常常被忽略的问题,但问题出现时你又不得不面对。比如

  • Activity横竖屏转换时Fragment重影

  • 应用长时间处于后台,并用户重新切到前台时,Activity显示异样或者需要等待一段时间才能显示内容

这类问题都与Activity的恢复重建机制相关,如果你想知道怎么解决这类问题,以及Activity恢复重建机制内部原理。这篇文或许能够帮到你。

1) 什么时候会重建

并不是任何Activity的销毁行为都会触发Activity数据的保存**。只有销毁行为是被系统发起的并且今后有可能恢复的时候才会触发**。

1.1)不会触发重建机制

  • 按返回按键。比如,A Actvivity启动B Activity,BActivity中返回不会调用BActivity的OnSaveInstanceState()方法
  • 最近使用屏幕中滑动关闭 Activity。
  • 从设置中【停止】应用
  • 完成某种“完成”操作,开发者通过调用Activity #finish()方法完成

1.2)有可能会触发重建机制的

触发恢复重建机制就是两大类

  • 系统回收内存时kill掉。
  • 横竖屏切换和语言切换等配置发生变化时kill重建。(可以通过 Activity #isChangingConfigurations()方法判断是否为配置更改发生的)。

当由系统发起而非人为手动关闭Activity的时候,Activity有可能在未来的某个时机恢复重建。Android系统提供了两套机制,用以保存和恢复界面状态。 这两套机制我个人分别给其取名为 Save-Restore InstanceState机制RetainNonConfiguration机制

2)Save-Restore InstanceState机制

1635401794(1).jpg

  • Save-Restore InstanceState机制的初衷是保存界面的一些瞬时状态,比如ListView滑动的位置、ViewPager的position一些TextView的文本内容。保证用户进入新建Activity的时候能尽快的看到界面状态的恢复。
  • Save-Restore InstanceState是一些比较轻量级的数据,因为保存过程需要经历数据的序列化和反序列化。

对于开发者时机操作层面来说,Save-Restore InstanceState机制的核心就是Activity中 onSaveInstanceState() 、onCreate()和onRestoreInstanceState()这三个回调方法。

2.1) onSaveInstanceState()

#Activity
protected void onSaveInstanceState(@NonNull Bundle outState) {
//A、整个view树中的view相关信息有机会保存到整个bundle中
outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());
outState.putInt(LAST_AUTOFILL_ID, mLastAutofillId);
Parcelable p = mFragments.saveAllState();
if (p != null) {
outState.putParcelable(FRAGMENTS_TAG, p);
}
if (mAutoFillResetNeeded) {
outState.putBoolean(AUTOFILL_RESET_NEEDED, true);
getAutofillManager().onSaveInstanceState(outState);
}
dispatchActivitySaveInstanceState(outState);
}
  • onSaveInstanceState(outState)的被调用的条件:非手动杀死Activity而是系统kill同时可能会在未来重建时。
  • 在Activity被系统kill时会调用onSaveInstanceState(outState)方法,允许开发者把一些今后重建时需要的一些状态数据存储到outState里面;这个方法的的触发时机是在onStop之前 (Android P开始会在onDestory之前执行)。
  • 默认地,代码A处,onSaveInstance方法会通过window依次调用整个view树的各个view的onSaveInstanceState()方法,view树的每个符合条件的view都有机会存储一些状态。需要注意的是:需要保存view状态的view需要有id作为标识存储在Bundle整个数据结构中。也就是说当view没有id的时候是保存不成功的。

2.2)onCreate(Bundle savedInstanceState)方法

被系统销毁又重建的Activity onCreate(Bundle savedInstanceState)回调方法中savedInstanceState的方法参数不为null。可以在这个位置取出被系统杀死之前保存的一些状态信息用来构建Activity。

2.3)onRestoreInstanceState(Bundle savedInstanceState)

  • 如果Activity是被系统重建的,会触发onRestoreInstanceState(savedInstanceState)方法,开发者可以在savedInstanceState中取出之前被系统销毁时存储的数据,用以在新Activity中恢复状态。

  • onRestoreInstanceState调用时机是在onStart()之后被调用

  • 默认地,onRestoreInstanceState方法会通过 mWindow.restoreHierarchyState()方法把之前保存的view状态信息分发出去,用以恢复view的状态。

protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
if (mWindow != null) {
Bundle windowState = savedInstanceState.getBundle(WINDOW_HIERARCHY_TAG);
if (windowState != null) {
//window view树有一次机会恢复销毁之前的状态
mWindow.restoreHierarchyState(windowState);
}
}
}

3) 配置改变发生的重建

image.png

当如横竖屏的切换、语言切换等配置发生改变时也会触发Activity的重建。这种由配置发生改变而导致的Activity重建除了会触发Save-Restore InstanceState机制之外也会触发RetainNonConfigurationInstance机制

RetainNonConfigurationInstance机制的核心是Activity中onRetainNonConfigurationInstance()和 getLastNonConfigurationInstance()这两个回调方法也会回调onRetainNonConfigurationInstance()方法,

3.1) onRetainNonConfigurationInstance()

  • 用于保存配置发生前的数据,这个数据理论上是没有结构和大小限制的甚至可以把旧Activity本身保存其中。

  • 触发时机会在onStop之前

public Object onRetainNonConfigurationInstance() {
return null;
}

3.2)getLastNonConfigurationInstance()

  • 当Activity 中getLastNonConfigurationInstance()方法返回值不为空的时候,说明当前这个Activity是因为配置发生变化重建而来的,可以使用这个返回值做一些Activity状态恢复的操作。

public Object getLastNonConfigurationInstance() {
return mLastNonConfigurationInstances != null
? mLastNonConfigurationInstances.activity : null;
}

4)两种机制有什么不同

  • 数据上:
    • 通过Save-Restore InstanceState方式保存和恢复界面时只是一些简单的瞬时数据。究其原因这个机制是讲保存的数据传递到了系统进程,在恢复的时候又从系统进程传递到应用进程,数据经历序列化和反序列化,而这些操作又是在主线程。
    • RetainInstance方式数据可以传递一些大数据甚至可以传递Activity本身。究其原因这个机制保存数据还是在当前应用进程,不会经历数据的序列化和反序列化。
  • 触发条件上
    • Save-Restore InstanceState只要Activity有可能被系统kill就会调用onSaveInstance()方法,只要Activity被重建就会在onCreate()方法中传入instanceState数据和调用onRestoreInstaceState()方法。
    • RetaineInstance方式只在系统配置发生变化的时候才生效。

5)AndroidX 做了什么

坊间流传Jetpack中Viewmodel会比Activity的生命周期长,是怎么回事?

阅读这个章节之前如果你对ViewModel的创建比较了解读起来可能会省力些

5.1) 横竖屏切换的时候

在androidx.activity:activity包下的ComponentActivity中关键点,

code 5.5.1
static final class NonConfigurationInstances {
Object custom;
ViewModelStore viewModelStore;
}
  • 引入NonConfigurationInstances类,这个类主要有两个属性,分别用来保存自定义数据和viewModelStore.

    code 5.5.2
    public final Object onRetainNonConfigurationInstance() {
    Object custom = onRetainCustomNonConfigurationInstance();
    ViewModelStore viewModelStore = mViewModelStore;
    if (viewModelStore == null) {
    NonConfigurationInstances nc = (NonConfigurationInstances) getLastNonConfigurationInstance();
    if (nc != null) { viewModelStore = nc.viewModelStore; }
    }
    if (viewModelStore == null && custom == null) {
    return null;
    }
    NonConfigurationInstances nci = new NonConfigurationInstances();
    nci.custom = custom;
    nci.viewModelStore = viewModelStore;
    return nci;
    }
  • 重写了onRetainNonConfigurationInstance()方法并把方法设置为了final。onRetainNonConfigurationInstance()方法内部创建NonConfigurationInstances对象nci,把viewModelStore存放到nci,同时收集onRetainCustomNonConfigurationInstance()方法的返回值存在nci里

  • 开发者可重写onRetainCustomNonConfigurationInstance()这个方法返回需要保存的数据。

    code 5.5.3
    public ComponentActivity() {
    ...
    getLifecycle().addObserver(new LifecycleEventObserver() {
    @Override
    public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
    if (event == Lifecycle.Event.ON_DESTROY) {
    mContextAwareHelper.clearAvailableContext()
    if (!isChangingConfigurations()) { //不在更改配置状态
    getViewModelStore().clear(); //1
    }
    }
    }
    });
    getLifecycle().addObserver(new LifecycleEventObserver() {
    @Override
    public void onStateChanged(@NonNull LifecycleOwner source,
    @NonNull Lifecycle.Event event) {
    ensureViewModelStore();
    getLifecycle().removeObserver(this);
    }
    });
    void ensureViewModelStore() { //在NonConfiguration中取出viewModleStore
    if (mViewModelStore == null) {
    NonConfigurationInstances nc =
    (NonConfigurationInstances) getLastNonConfigurationInstance();
    if (nc != null) {
    mViewModelStore = nc.viewModelStore;
    }
    if (mViewModelStore == null) {
    mViewModelStore = new ViewModelStore();
    }
    }
    }
  • viewmodel中横竖屏转换,横竖屏切换等配置发生变化导致的重建时,新Activity中可听过ensureViewModelStore()方法获取从旧Activity传递过来viewmodelstore,这样就实现了横竖屏切换的时候viewmodel不丢失。

  • 另外值得注意的点是,Activity onDestory的时候,会通过**isChangingConfigurations()**方法判断activity是否处于配置变化状态,如果不是就会将viewmodelstores清空掉。

这套方式解决了一个问题当Activity由横竖屏转换等配置原因发生变化导致Activity重建的时候,会将旧Activity的viewModelStore传给新Activity。如果不是由于配置发生变化导致的Activity重建会清除掉viewModelStore

那有什么方式能解决非配置变化导致Activity重建时保存ViewModel的数据呢?

5.2)非配置变化引起的Activity重建对ViewModel的保存

结论先行,因配置变化引起的Activity重建可以将ViewModleStore保存,在新Activity中可以直接获取旧Activity中的ViewModel。而对于非配置变化引起的Activity重建不能直接将ViewModelStore对象传递给新Activity。AndroidX中是将ViewModel的数据保存到Bundle中,给Bundle分配一个Key,这样ViewModel的保存和恢复就可以通过Save-Restore Stated Instance机制实现。

稍微展开下实现细节

5.2.1) ViewModel销毁时数据保存
  • 数据保存还是通过Activity Save-Restore StateInstace机制,Activity发起saveStatedInstance()时通过调用注册到SavedStateRegistry上SavedStateProvider的saveState()方法获取到对应的Bundle当然最终存储到saveStatedInstance(outBundle)的outBundle中。
  • 通常情况下,ViewModel中会有LiveData,SavedStateHandle中也支持LiveData中数据的保存,SavingStateLiveData继承MutableLiveData复写setValue()方法,设置到LiveData的数据都会保存到mRegular中一份这样实现LiveData数据的保存。
public final class SavedStateHandle {
..
final Map<String, Object> mRegular;
final Map<String, SavedStateProvider> mSavedStateProviders = new HashMap<>();
private final Map<String, SavingStateLiveData<?>> mLiveDatas = new HashMap<>();

private final SavedStateProvider mSavedStateProvider = new SavedStateProvider() {
@SuppressWarnings("unchecked")
@NonNull
@Override
public Bundle saveState() {
...
Set<String> keySet = mRegular.keySet();
ArrayList keys = new ArrayList(keySet.size());
ArrayList value = new ArrayList(keys.size());
for (String key : keySet) {
keys.add(key);
value.add(mRegular.get(key));
}
Bundle res = new Bundle();
//把mRegular保存的数据存放到Bundle中返回
res.putParcelableArrayList("keys", keys);
res.putParcelableArrayList("values", value);
return res;
}
}
5.2.2) ViewModel重建时恢复
  • 带恢复功能的Viewmodel是通过SavedStateViewModelFactory创建,当Activity重建时,会在Activity的onCreate(Bundle data)带后旧Activity存的数据,这bundle中可以取出旧ViewModel的SavedStateHandle对象并以此为构造参数构建ViewModel。这样新建ViewModel就有了旧ViewModel的数据,数据是通过SavedStateHandle对象为介质进行传递的,ViewModel中可以使用对应的key恢复ViewModel的基本数据类型和可序列化的数据类型。

  • 所以,在具备保存-恢复数据特性的ViewModle中获取数据时使用SavedStateHandle对象上的 get(@NonNull String key)方法。获取LiveData()时使用 MutableLiveData getLiveData(String key)方法。内部方法实现是通过key在mRegular中获取到对应的值,再用值作为LiveData初始值创建LiveData。

5.2.3)其他
  • ViewModle中也会存在非序列化的数据(继承了Parcelable或Serializable)或者不能被Bundle存储的对象,如果要保存恢复这些数据怎么实现呢? Lifecycle 2.3.0-alpha03 开始允许设置自定义的SavedStateProvider这样我们可以把非序列化的数据转化成可序列化的数据保存到Bundle中,实现非序列化的数据的保存和恢复。

  • ViewModel的数据保存和恢复虽然逻辑相对比较简单,但是里面涉及到的类和细节比较繁杂这个章节只是说明了一下实现的核心思想,如果大家想了解内部更多的实现细节,今后可以另开一篇展开聊。

6) 最后

  • Android销毁重建机制常常会被开发者忽略,进而造成App线上出现非预期问题甚至crash。开发阶段我们可以通过 开发者模式 ->不保留活动选项,尽早的暴露相关的问题。
  • 关于此类问题的解决时至今日,我们已经有比较完备的工具箱,如果比较熟悉这些工具内部的实现原理机制,在使用这些工具的时候会更得心应手。

不足处批评指正,望不吝点赞


收起阅读 »

Room & Kotlin 符号的处理

Jetpack Room 库在 SQLite 上提供了一个抽象层,能够在没有任何样板代码的情况下,提供编译时验证 SQL 查询的能力。它通过处理代码注解和生成 Java 源代码的方式,实现上述行为。 注解处理器非常强大,但它们会增加构建时间。这对于用 Java...
继续阅读 »

Jetpack Room 库在 SQLite 上提供了一个抽象层,能够在没有任何样板代码的情况下,提供编译时验证 SQL 查询的能力。它通过处理代码注解和生成 Java 源代码的方式,实现上述行为。


注解处理器非常强大,但它们会增加构建时间。这对于用 Java 写的代码来说通常是可以接受的,但对于 Kotlin 而言,编译时间消耗会非常明显,这是因为 Kotlin 没有一个内置的注解处理管道。相反,它通过 Kotlin 代码生成了存根 Java 代码来支持注解处理器,然后将其输送到 Java 编译器中进行处理。


由于并不是所有 Kotlin 源代码中的内容都能用 Java 表示,因此有些信息会在这种转换中丢失。同样,Kotlin 是一种多平台语言,但 KAPT 只在面向 Java 字节码的情况下生效。


认识 Kotlin 符号处理


随着注解处理器在 Android 上的广泛使用,KAPT 成为了编译时的性能瓶颈。为了解决这个问题,Google Kotlin 编译器团队开始研究一个替代方案,来为 Kotlin 提供一流的注解处理支持。当这个项目诞生之初,我们非常激动,因为它将帮助 Room 更好地支持 Kotlin。从 Room 2.4 开始,它对 KSP 有了实验性的支持,我们发现编译速度提高了 2 倍,特别是在全量编译的情况下。


本文内容重点不在注解的处理、Room 或者 KSP。而在于重点介绍我们在为 Room 添加 KSP 支持时所面临的挑战和所做的权衡。为了理解本文您并不需要了解 Room 或者 KSP,但必须熟悉注解处理。



注意: 我们在 KSP 发布稳定版之前就开始使用它了。因此,尚不确定之前做的一些决策是否适用于现在。



本篇文章旨在让注解处理器的作者们在为项目添加 KSP 支持前,充分了解需要注意的问题。


Room 工作原理简介


Room 的注解处理分为两个步骤。有一些 "Processor" 类,它们遍历用户的代码,验证并提取必要的信息到 "值对象" 中。这些值对象被送到 "Writer" 类中,这些类将它们转换为代码。和其他诸多的注解处理器一样,Room 非常依赖 Auto-Commonjavax.lang.model 包 (Java 注解处理 API 包) 中频繁引用的类。


为了支持 KSP,我们有三种选择:



  1. 复制 JavaAP 和 KSP 的每个 "Processor" 类,它们会有相同的值对象作为输出,我们可以将其输入到 Writer 中;

  2. 在 KSP/Java AP 之上创建一个抽象层,以便处理器拥有一个基于该抽象层的实现;

  3. 用 KSP 代替 JavaAP,并要求开发者也使用 KSP 来处理 Java 代码。


选项 C 实际上是不可行的,因为它会对 Java 用户造成严重的干扰。随着 Room 使用数量的增加,这种破坏性的改变是不可能的。在 "A" 和 "B" 两者之间,我们决定选择 "B",因为处理器具有相当数量的业务逻辑,将其分解并非易事。


认识 X-Processing


在 JavaAP 和 KSP 上创建一个通用的抽象并非易事。Kotlin 和 Java 可以互操作,但模式却不相同,例如,Kotlin 中特殊类的类型如 Kotlin 的值类或者 Java 中的静态方法。此外,Java 类中有字段和方法,而 Kotlin 中有属性和函数。


我们决定实现 "Room 需要什么",而不是尝试去追求完美的抽象。从字面意思来看,在 Room 中找到导入了 javax.lang.model 的每一个文件,并将其移动到 X-Processing 的抽象中。这样一来,TypeElement 变成了 XTypeElementExecutableElemen 变成了 XExecutableElemen 等等。


遗憾的是,javax.lang.model API 在 Room 中的应用非常广泛。一次性创建所有这些 X 类,会给审阅者带来非常严重的心理负担。因此,我们需要找到一种方法来迭代这一实现。


另一方面,我们需要证明这是可行的。所以我们首先对其做了 原型 设计,一旦验证这是一个合理的选择,我们就用他们自己的测试 逐一重新实现了所有 X 类


关于我说的实现 "Room 需要什么",有一个很好的例子,我们可以在关于类的字段 更改 中看到。当 Room 处理一个类的字段时,它总是对其所有的字段感兴趣,包括父类中的字段。所以我们在创建相应的 X-Processing API 时,添加了获取所有字段的能力。


interface XTypeElement {
fun getAllFieldsIncludingPrivateSupers(): List<XVariableElement>
}

如果我们正在设计一个通用库,这样可能永远不会通过 API 审查。但因为我们的目标只是 Room,并且它已经有一个与 TypeElement 具有相同功能的辅助方法,所以复制它可以减少项目的风险。


一旦我们有了基本的 X-Processing API 和它们的测试方法,下一步就是让 Room 来调用这个抽象。这也是 "实现 Room 所需要的东西" 获得良好回报的地方。Room 在 javax.lang.model API 上已经拥有了用于基本功能的扩展函数/属性 (例如获取 TypeElement 的方法)。我们首先更新了这些扩展,使其看起来与 X-Processing API 类似,然后在 1 CL 中将 Room 迁移到 X-Processing。


改进 API 可用性


保留类似 JavaAP 的 API 并不意味着我们不能改进任何东西。在将 Room 迁移到 X-Processing 之后,我们又实现了一系列的 API 改进。


例如,Room 多次调用 MoreElement/MoreTypes,以便在不同的 javax.lang.model 类型 (例如 MoreElements.asType) 之间进行转换。相关调用通常如下所示:


val element: Element ...
if (MoreElements.isType(element)) {
val typeElement:TypeElement = MoreElements.asType(element)
}

我们把所有的调用放到了 Kotlin contracts 中,这样一来就可以写成:


val element: XElement ...
if (element.isTypeElement()) {
// 编译器识别到元素是一个 XTypeElement
}

另一个很好的例子是在一个 TypeElement 中找寻方法。通常在 JavaAP 中,您需要调用 ElementFilter 类来获取 TypeElement 中的方法。与此相反,我们直接将其设为 XTypeElement 中的一个属性。


// 前
val methods = ElementFilter.methodsIn(typeElement.enclosedElements)
// 后
val methods = typeElement.declaredMethods

最后一个例子,这也可能是我最喜欢的例子之一,就是可分配性。在 JavaAP 中,如果您要检查给定的 TypeMirror 是否可以由另一个 TypeMirror 赋值,则需要调用 Types.isAssignable


val type1: TypeMirror ...
val type2: TypeMirror ...
if (typeUtils.isAssignable(type1, type2)) {
...
}

这段代码真的很难读懂,因为您甚至无法猜到它是否验证了类型 1 可以由类型 2 指定,亦或是完全相反的结果。我们已经有一个扩展函数如下:


fun TypeMirror.isAssignableFrom(
types: Types,
otherType: TypeMirror
): Boolean

在 X-Processing 中,我们能够将其转换为 XType 上的常规函数,如下方所示:


interface XType {
fun isAssignableFrom(other: XType): Boolean
}

为 X-Processing 实现 KSP 后端


这些 X-Processing 接口每个都有自己的测试套件。我们编写它们并非是用来测试 AutoCommon 或者 JavaAP 的,相反,编写它们是为了在有了它们的 KSP 实现时,我们就可以运行测试用例来验证它是否符合 Room 的预期。


由于最初的 X-Processing API 是按照 avax.lang.model 建模,它们并非每次都适用于 KSP,所以我们也改进了这些 API,以便在需要时为 Kotlin 提供更好的支持。


这样产生了一个新问题。现有的 Room 代码库是为了处理 Java 源代码而写的。当应用是由 Kotlin 编写时,Room 只能识别该 Kotlin 在 Java 存根中的样子。我们决定在 X-Processing 的 KSP 实现中保持类似行为。


例如,Kotlin 中的 suspend 函数在编译时生成如下签名:


// kotlin
suspend fun foo(bar:Bar):Baz
// java
Object foo(bar:Bar, Continuation<? extends Baz>)

为保持相同的行为,KSP 中的 XMethodElement 实现为 suspend 方法合成了一个新参数,以及新的返回类型。(KspMethodElement.kt)



注意: 这样做效果很好,因为 Room 生成的是 Java 代码,即使在 KSP 中也是如此。当我们添加对 Kotlin 代码生成的支持时,可能会引起一些变化。



另一个例子与属性有关。Kotlin 属性也可能具有基于其签名的合成 getter/setter (访问器)。由于 Room 期望找到这些访问器作为方法 (参见: KspTypeElement.kt),因此 XTypeElement 实现了这些合成方法。



注意 : 我们已有计划更改 XTypeElement API 以提供属性而非字段,因为这才是 Room 真正想要获取的内容。正如您现在猜到的那样,我们决定 "暂时" 不这样做来减少 Room 的修改。希望有一天我们能够做到这一点,当我们这样做时,XTypeElement 的 JavaAP 实现将会把方法和字段作为属性捆绑在一起。



在为 X-Processing 添加 KSP 实现时,最后一个有趣的问题是 API 耦合。这些处理器的 API 经常相互访问,因此如果不实现 XField / XMethod,就不能在 KSP 中实现 XTypeElement,而 XField / XMethod 本身又引用了 XType 等等。在添加这些 KSP 实现的同时,我们为它们的实现部分写了单独的测试用例。当 KSP 的实现变得更加完整时,我们逐渐通过 KSP 后端启动全部的 X-Processing 测试。


需要注意的是,在此阶段我们只在 X-Processing 项目中运行测试,所以即使我们知道测试的内容没问题,我们也无法保证所有的 Room 测试都能通过 (也称之为单元测试 vs 集成测试)。我们需要通过一种方法来使用 KSP 后端运行所有的 Room 测试,"X-Processing-Testing" 就应运而生。


认识 X-Processing-Testing


注解处理器的编写包含 20% 的处理器代码和 80% 的测试代码。您需要考虑到各种可能的开发者错误,并确保如实报告错误消息。为了编写这些测试,Room 已经提供一个辅助方法如下:


 

runTest 在底层使用了 Google Compile Testing 库,并允许我们简单地对处理器进行单元测试。它合成了一个 Java 注解处理器并在其中调用了处理器提供的 process 方法。


val entitySource : JavaFileObject //示例 @Entity 注释类
val result = runTest(entitySource) { invocation ->
val element = invocation.processingEnv.findElement("Subject")
val entityValueObject = EntityProcessor(...).process(element)
// 断言 entityValueObject
}
// 断言结果是否有误,警告等

糟糕的是,Google Compile Testing 仅支持 Java 源代码。为了测试 Kotlin 我们需要另一个库,幸运的是有 Kotlin Compile Testing,它允许我们编写针对 Kotlin 的测试,而且我们为该库贡献了对 KSP 支持。



注意 : 我们后来用 内部实现 替换了 Kotlin Compile Testing,以简化 AndroidX Repo 中的 Kotlin/KSP 更新。我们还添加了更好的断言 API,这需要我们对 KCT 执行 API 不兼容的修改操作。



作为能让 KSP 运行所有测试的最后一步,我们创建了以下测试 API:


fun runProcessorTest(
sources: List<Source>,
handler: (XTestInvocation) -> Unit
): Unit

这个和原始版本之间的主要区别在于,它同时通过 KSP 和 JavaAP (或 KAPT,取决于来源) 运行测试。因为它多次运行测试且 KSP 和 JavaAP 两者的判断结果不同,因此无法返回单个结果。


因此,我们想到了一个办法:


fun XTestInvocation.assertCompilationResult(
assertion: (XCompilationResultSubject) -> Unit
}

每次编译后,它都会调用结果断言 (如果没有失败提示,则检查编译是否成功)。我们把每个 Room 测试重构为如下所示:


val entitySource : Source //示例 @Entity 注释类
runProcessorTest(listOf(entitySource)) { invocation ->
// 该代码块运行两次,一次使用 JavaAP/KAPT,一次使用 KSP
val element = invocation.processingEnv.findElement("Subject")
val entityValueObject = EntityProcessor(...).process(element)
// 断言 entityValueObject
invocation.assertCompilationResult {
// 结果被断言为是否有 error,warning 等
hasWarningContaining("...")
}
}

接下来的事情就很简单了。将每个 Room 的编译测试迁移到新的 API,一旦发现新的 KSP / X-Processing 错误,就会上报,然后实施临时解决方案;这一动作反复进行。由于 KSP 正在大力开发中,我们确实遇到了很多 bug。每一次我们都会上报 bug,从 Room 源链接到它,然后继续前进 (或者进行修复)。每当 KSP 发布之后,我们都会搜索代码库来找到已修复的问题,删除临时解决方案并启动测试。


一旦编译测试覆盖情况较好,我们在下一步就会使用 KSP 运行 Room 的 集成测试。这些是实际的 Android 测试应用,也会在运行时测试其行为。幸运的是,Android 支持 Gradle 变体,因此使用 KSP 和 KAPT 来运行我们 Kotlin 集成测试 便相当容易。


下一步


将 KSP 支持添加到 Room 只是第一步。现在,我们需要更新 Room 来使用它。例如,Room 中的所有类型检查都忽略了 nullability,因为 javax.lang.modelTypeMirror 并不理解 nullability。因此,当调用您的 Kotlin 代码时,Room 有时会在运行时触发 NullPointerException。有了 KSP,这些检查现在可在 Room 中创建新的 KSP bug (例如 b/193437407)。我们已经添加了一些临时解决方案,但理想情况下,我们仍希望 改进 Room 以正确处理这些情况。


同样,即使我们支持 KSP,Room 仍然只生成 Java 代码。这种限制使我们无法添加对某些 Kotlin 特性的支持,比如 Value Classes。希望在将来,我们还能对生成 Kotlin 代码提供一些支持,以便在 Room 中为 Kotlin 提供一流的支持。接下来,也许更多 :)。


我能在我的项目上使用 X-Processing 吗?


答案是还不能;至少与您使用任何其他 Jetpack 库的方式不同。如前文所述,我们只实现了 Room 需要的部分。编写一个真正的 Jetpack 库有很大的投入,比如文档、API 稳定性、Codelabs 等,我们无法承担这些工作。话虽如此,Dagger 和 Airbnb (ParisDeeplinkDispatch) 都开始用 X-Processing 来支持 KSP (并贡献了他们需要的东西🙏)。也许有一天我们会把它从 Room 中分解出来。从技术层面上讲,您仍然可以像使用 Google Maven 库 一样使用它,但是没有 API 保证可以这样做,因此您绝对应该使用 shade 技术。


总结


我们为 Room 添加了 KSP 支持,这并非易事但绝对值得。如果您在维护注解处理器,请添加对 KSP 的支持,以提供更好的 Kotlin 开发者体验。


特别感谢 Zac SweersEli Hart 审校这篇文章的早期版本,他们同时也是优秀的 KSP 贡献者。


更多资源



欢迎您 点击这里 向我们提交反馈,或分享您喜欢的内容、发现的问题。您的反馈对我们非常重要,感谢您的支持!


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

坏了!面试官问我垃圾回收机制

面试官:我还记得上次你讲到JVM内存结构(运行时数据区域)提到了「堆」,然后你说是分了几块区域嘛 面试官:当时感觉再讲下去那我可能就得加班了 面试官:今天有点空了,继续聊聊「堆」那块吧 候选者:嗯,前面提到了堆分了「新生代」和「老年代」,「新生代」又分为「Ed...
继续阅读 »

面试官:我还记得上次你讲到JVM内存结构(运行时数据区域)提到了「堆」,然后你说是分了几块区域嘛


面试官:当时感觉再讲下去那我可能就得加班了


面试官今天有点空了,继续聊聊「堆」那块吧


候选者:嗯,前面提到了堆分了「新生代」和「老年代」,「新生代」又分为「Eden」和「Survivor」区,「survivor」区又分为「From Survivor」和「To Survivor」区



候选者:说到这里,我就想聊聊Java的垃圾回收机制了


面试官:那你开始你的表演吧


候选者:我们使用Java的时候,会创建很多对象,但我们未曾「手动」将这些对象进行清除


候选者:而如果用C/C++语言的时候,用完是需要自己free(释放)掉的


候选者:那为什么在写Java的时候不用我们自己手动释放”垃圾”呢?原因很简单,JVM帮我们做了(自动回收垃圾)


面试官:嗯…


候选者:我个人对垃圾的定义:只要对象不再被使用了,那我们就认为该对象就是垃圾,对象所占用的空间就可以被回收



面试官那是怎么判断对象不再被使用的呢?


候选者:常用的算法有两个「引用计数法」和「可达性分析法」


候选者:引用计数法思路很简单:当对象被引用则+1,但对象引用失败则-1。当计数器为0时,说明对象不再被引用,可以被可回收


候选者:引用计数法最明显的缺点就是:如果对象存在循环依赖,那就无法定位该对象是否应该被回收(A依赖B,B依赖A)


面试官:嗯…


候选者:另一种就是可达性分析法:它从「GC Roots」开始向下搜索,当对象到「GC Roots」都没有任何引用相连时,说明对象是不可用的,可以被回收



候选者:「GC Roots」是一组必须「活跃」的引用。从「GC Root」出发,程序通过直接引用或者间接引用,能够找到可能正在被使用的对象


面试官还是不太懂,那「GC Roots」一般是什么?你说它是一组活跃的引用,能不能举个例子,太抽象了。


候选者:比如我们上次不是聊到JVM内存结构中的虚拟机栈吗,虚拟机栈里不是有栈帧吗,栈帧不是有局部变量吗?局部变量不就存储着引用嘛。


候选者:那如果栈帧位于虚拟机栈的栈顶,是不是就可以说明这个栈帧是活跃的(换言之,是线程正在被调用的)


候选者:既然是线程正在调用的,那栈帧里的指向「堆」的对象引用,是不是一定是「活跃」的引用?


候选者:所以,当前活跃的栈帧指向堆里的对象引用就可以是「GC Roots」


面试官:嗯…


候选者:当然了,能作为「GC Roots」也不单单只有上面那一小块


候选者:比如类的静态变量引用是「GC Roots」,被「Java本地方法」所引用的对象也是「GC Roots」等等…



候选者:回到理解的重点:「GC Roots」是一组必须「活跃」的「引用」,只要跟「GC Roots」没有直接或者间接引用相连,那就是垃圾


候选者:JVM用的就是「可达性分析算法」来判断对象是否垃圾


面试官:懂了


候选者:垃圾回收的第一步就是「标记」,标记哪些没有被「GC Roots」引用的对象



候选者:标记完之后,我们就可以选择直接「清除」,只要不被「GC Roots」关联的,都可以干掉


候选者:过程非常简单粗暴,但也存在很明显的问题


候选者:直接清除会有「内存碎片」的问题:可能我有10M的空余内存,但程序申请9M内存空间却申请不下来(10M的内存空间是垃圾清除后的,不连续的)



候选者:那解决「内存碎片」的问题也比较简单粗暴,「标记」完,不直接「清除」。


候选者:我把「标记」存活的对象「复制」到另一块空间,复制完了之后,直接把原有的整块空间给干掉!这样就没有内存碎片的问题了


候选者:这种做法缺点又很明显:内存利用率低,得有一块新的区域给我复制(移动)过去


面试官:嗯…


候选者:还有一种「折中」的办法,我未必要有一块「大的完整空间」才能解决内存碎片的问题,我只要能在「当前区域」内进行移动


候选者:把存活的对象移到一边,把垃圾移到一边,那再将垃圾一起删除掉,不就没有内存碎片了嘛


候选者:这种专业的术语就叫做「整理」



候选者:扯了这么久,我们把思维再次回到「堆」中吧


候选者:经过研究表明:大部分对象的生命周期都很短,而只有少部分对象可能会存活很长时间


候选者:又由于「垃圾回收」是会导致「stop the world」(应用停止访问)


候选者:理解「stop the world」应该很简单吧:回收垃圾的时候,程序是有短暂的时间不能正常继续运作啊。不然JVM在回收的时候,用户线程还继续分配修改引用,JVM怎么搞(:


候选者:为了使「stop the world」持续的时间尽可能短以及提高并发式GC所能应付的内存分配速率


候选者:在很多的垃圾收集器上都会在「物理」或者「逻辑」上,把这两类对象进行区分,死得快的对象所占的区域叫做「年轻代」,活得久的对象所占的区域叫做「老年代」



候选者:但也不是所有的「垃圾收集器」都会有,只不过我们现在线上用的可能都是JDK8,JDK8及以下所使用到的垃圾收集器都是有「分代」概念的。


候选者:所以,你可以看到我的「堆」是画了「年轻代」和「老年代」


候选者:要值得注意的是,高版本所使用的垃圾收集器的ZGC是没有分代的概念的(:


候选者:只不过我为了好说明现状,ZGC的话有空我们再聊


面试官:嗯…好吧


候选者:在前面更前面提到了垃圾回收的过程,其实就对应着几种「垃圾回收算法」,分别是:


候选者:标记清除算法、标记复制算法和标记整理算法【「标记」「清除」「复制」「整理」】


候选者:经过上面的铺垫之后,这几种算法应该还是比较好理解的



候选者:「分代」和「垃圾回收算法」都搞明白了之后,我们就可以看下在JDK8生产环境及以下常见的垃圾回收器了


候选者:「年轻代」的垃圾收集器有:Seria、Parallel Scavenge、ParNew


候选者:「老年代」的垃圾收集器有:Serial Old、Parallel Old、CMS


候选者:看着垃圾收集器有很多,其实还是非常好理解的。Serial是单线程的,Parallel是多线程


候选者:这些垃圾收集器实际上就是「实现了」垃圾回收算法(标记复制、标记整理以及标记清除算法)


候选者:CMS是「JDK8之前」是比较新的垃圾收集器,它的特点是能够尽可能减少「stop the world」时间。在垃圾回收时让用户线程和 GC 线程能够并发执行!



候选者:又可以发现的是,「年轻代」的垃圾收集器使用的都是「标记复制算法」


候选者:所以在「堆内存」划分中,将年轻代划分出Survivor区(Survivor From 和Survivor To),目的就是为了有一块完整的内存空间供垃圾回收器进行拷贝(移动)


候选者:而新的对象则放入Eden区


候选者:我下面重新画下「堆内存」的图,因为它们的大小是有默认的比例的



候选者:图我已经画好了,应该就不用我再说明了


面试官我还想问问,就是,新创建的对象一般是在「新生代」嘛,那在什么时候会到「老年代」中呢?


候选者:嗯,我认为简单可以分为两种情况:


候选者:1. 如果对象太大了,就会直接进入老年代(对象创建时就很大 || Survivor区没办法存下该对象)


候选者:2. 如果对象太老了,那就会晋升至老年代(每发生一次Minor GC ,存活的对象年龄+1,达到默认值15则晋升老年代 || 动态对象年龄判定 可以进入老年代)



面试官既然你又提到了Minor GC,那Minor GC 什么时候会触发呢?


候选者:当Eden区空间不足时,就会触发Minor GC


面试官:Minor GC 在我的理解就是「年轻代」的GC,你前面又提到了「GC Roots」嘛


面试官那在「年轻代」GC的时候,从GC Roots出发,那不也会扫描到「老年代」的对象吗?那那那..不就相当于全堆扫描吗?


候选者:这JVM里也有解决办法的。


候选者:HotSpot 虚拟机「老的GC」(G1以下)是要求整个GC堆在连续的地址空间上。


候选者:所以会有一条分界线(一侧是老年代,另一侧是年轻代),所以可以通过「地址」就可以判断对象在哪个分代上


候选者:当做Minor GC的时候,从GC Roots出发,如果发现「老年代」的对象,那就不往下走了(Minor GC对老年代的区域毫无兴趣)



面试官但又有个问题,那如果「年轻代」的对象被「老年代」引用了呢?(老年代对象持有年轻代对象的引用),那时候肯定是不能回收掉「年轻代」的对象的


候选者:HotSpot虚拟机下 有「card table」(卡表)来避免全局扫描「老年代」对象


候选者:「堆内存」的每一小块区域形成「卡页」,卡表实际上就是卡页的集合。当判断一个卡页中有存在对象的跨代引用时,将这个页标记为「脏页」


候选者:那知道了「卡表」之后,就很好办了。每次Minor GC 的时候只需要去「卡表」找到「脏页」,找到后加入至GC Root,而不用去遍历整个「老年代」的对象了。



面试官:嗯嗯嗯,还可以的啊,要不继续聊聊CMS?


候选者:这面试快一个小时了吧,我图也画了这么多了。下次?下次吧?有点儿累了


本文总结



  • 什么是垃圾:只要对象不再被使用,那即是垃圾

  • 如何判断为垃圾:可达性分析算法和引用计算算法,JVM使用的是可达性分析算法

  • 什么是GC Roots:GC Roots是一组必须活跃的引用,跟GC Roots无关联的引用即是垃圾,可被回收

  • 常见的垃圾回收算法:标记清除、标记复制、标记整理

  • 为什么需要分代:大部分对象都死得早,只有少部分对象会存活很长时间。在堆内存上都会在物理或逻辑上进行分代,为了使「stop the world」持续的时间尽可能短以及提高并发式GC所能应付的内存分配速率。

  • Minor GC:当Eden区满了则触发,从GC Roots往下遍历,年轻代GC不关心老年代对象

  • 什么是card table【卡表】:空间换时间(类似bitmap),能够避免扫描老年代的所有对应进而顺利进行Minor GC (案例:老年代对象持有年轻代对象引用)

  • 堆内存占比:年轻代占堆内存1/3,老年代占堆内存2/3。Eden区占年轻代8/10,Survivor区占年轻代2/10(其中From 和To 各站1/10)

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

flutter 数字键盘、自定义键盘

有些特殊的场景 会遇到使用数字键盘的特殊场景,例如输入金额、数量 number_keypan.dart =》文件内容如下: import 'package:flutter/material.dart'; /// <summary> /// ...
继续阅读 »

有些特殊的场景 会遇到使用数字键盘的特殊场景,例如输入金额、数量


image.png


number_keypan.dart =》文件内容如下:


import 'package:flutter/material.dart';

/// <summary>
/// todo: 数字键盘
/// author:zwb
/// dateTime:2021/7/19 10:25
/// filePath:lib/widgets/number_keypan.dart
/// desc: 示例
/// <summary>
// OverlayEntry overlayEntry;
// TextEditingController controller = TextEditingController();
//
// numberKeypan(
// initialization: (v){
// /// 初始化
// overlayEntry = v;
// /// 唤起键盘
// openKeypan(context: context);
// },
// onDel: (){
// delCursor(textEditingController: controller);
// },
// onTap: (v){
// /// 更新输入框的值
// controller.text += v;
// /// 保持光标
// lastCursor(textEditingController: controller);
// },
// );
OverlayEntry overlayEntry;
NumberKeypan({@required Function(OverlayEntry) initialization,@required Function(String) onTap,Function onCommit,Function onDel,}){
overlayEntry = OverlayEntry(builder: (context) {
List<String> list = ['1','2','3','4','5','6','7','8','9','','0','删除'];
return new Positioned(
bottom: 0,
child: new Material(
child: new Container(
width: MediaQuery.of(context).size.width,
alignment: Alignment.center,
color: Colors.grey[200],
child: Row(
children: [
Expanded(
child: Wrap(
alignment: WrapAlignment.spaceBetween,
children: List.generate(list.length, (index) {
return Material(
color: Colors.white,
child: Ink(
child: InkWell(
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[200],width: 0.25),
),
alignment: Alignment.center,
height: 50,
width: (MediaQuery.of(context).size.width) / 3,
child: Text("${list[index]}",style: TextStyle(fontSize: 18,fontWeight: FontWeight.bold),),
),
onTap: index == 11 ? onDel : (){
if(list[index] != "" && list[index] !="删除"){
onTap(list[index]);
}
},
),
color: Colors.white,
),
);
}),
),
),
// Column(
// children: [
// SizedBox(
// width: 60,
// height: 50 * 1.5,
// child: MaterialButton(
// onPressed: onDel ?? (){},
// child: Text("删除",style: TextStyle(color: Colors.black,fontWeight: FontWeight.bold)),
// color: Colors.grey[100],
// elevation: 0,
// padding: EdgeInsets.all(0),),
// ),
// SizedBox(
// width: 60,
// height: 50 * 2.5,
// child: MaterialButton(
// onPressed: (){
// disKeypan();
// if(onCommit != null ) onCommit();
// },
// child: Text("确认",style: TextStyle(color: Colors.white,fontWeight: FontWeight.bold),),
// color: Colors.blue,
// elevation: 0,
// padding: EdgeInsets.all(0),
// ),
// ),
// ],
// ),
],
),
),
));
});
initialization(overlayEntry);
}

/// <summary>
/// todo: 保持光标在最后
/// author: zwb
/// date: 2021/7/19 11:43
/// param: 参数
/// return: void
/// <summary>
///
lastCursor({@required TextEditingController textEditingController}){
/// 保持光标在最后
final length = textEditingController.text.length;
textEditingController.selection = TextSelection(baseOffset:length , extentOffset:length);
}

/// <summary>
/// todo: 自定义键盘的删除事件
/// author: zwb
/// date: 2021/7/19 11:45
/// param: 参数
/// return: void
/// <summary>
///
delCursor({@required TextEditingController textEditingController}){
if(textEditingController != null && textEditingController.value.text != "") textEditingController.text = textEditingController.text.substring(0,textEditingController.text.length - 1);
}

/// <summary>
/// todo: 打开键盘
/// author: zwb
/// date: 2021/7/19 12:04
/// param: 参数
/// return: void
/// <summary>
///
openKeypan({BuildContext context}){
Overlay.of(context).insert(overlayEntry);
}

/// <summary>
/// todo: 销毁键盘
/// author: zwb
/// date: 2021/7/19 12:03
/// param: 参数
/// return: void
/// <summary>
///
disKeypan(){
if(overlayEntry!=null) overlayEntry.remove();
}

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