原文作者:suragch.medium.com/
发布时间:2020年12月26日-25分钟阅读
如果你能理解字节,你就能理解任何东西。
前言
我开始研究这个话题是因为我在研究如何在Dart和PostgreSQL数据库服务器之间进行通信。事实证明,这比我所期望的要低级得多。我想我应该写一篇短文来解释一些我所学到的新东西。好吧,写了整整三天之后,那篇短文已经变成了我对一个已经小众的主题所写的最深入的解释之一。虽然文章很长,但我认为你不会觉得无聊,而且很有可能你会学到一两件关于Dart的新东西,即使你已经使用了一段时间。我当然也是这样做的。一如既往,如果你发现任何错误,请让我知道。这是我学习的方式。并尝试自己的代码示例。这是你学习的方式。
本文是最新的Dart 2.10版本。
字节和字节
看过本文的人都知道,一个字节是八位。
00101010
这个8位字节有一个值,在这种情况下,值是42,这只是一个整数。现在将这些知识与所有二进制数据只是一个字节序列的事实相结合,这意味着在Dart中可以将任何二进制数据表示为一个整数列表。
List<int> data = [102, 111, 114, 116, 121, 45, 116, 119, 111, 0];
这些字节可能来自一个文件,一个位图图像,一个mp3录音,一个内存转储,一个网络请求,或者一个字符串的字符代码。它们都是字节。
更高效一点
在Dart中,int类型默认为64位值。这就是八个字节。这里是数字42,这次显示的是64位,供你参考。
0000000000000000000000000000000000000000000000000000000000101010
如果你仔细观察,你可能会发现很多位没有被使用。
一个int可以存储9,223,372,036,854,775,807个值,但一个字节的最大值是255。这绝对是一个用大锤敲开螺母的例子。现在把一个一兆字节的文件想象成一个int的列表。你的问题就大了一百万倍。
这就是Uint8List的作用。这种类型基本上和 List<int>一样,但是对于大的列表,Uint8List 比 List<int>更有效率。Uint8List是一个整数的列表,列表中的值每个只有8位,或者说一个字节。Uint8List的U表示无符号,所以值的范围从0到255。这非常适合表示二进制数据!
附注:二进制中的负数
你有没有想过如何用二进制表示负数?好吧,方法是把最左边的位子变成1表示负数,0表示正数。例如,这些8位有符号的整数都是负数,因为它们都是以1开始的。
11111101
10000001
10101010
另一方面,下面的8位有符号整数都是正数,因为最左边的位是0 。
01111111
00000001
01010101
你可能会认为,如果000001是+1,那么10000001应该是-1。否则你就会有两个0的值。10000000和00000000 解决方法是使用一个叫做 "二的补码 "的系统。如果你有兴趣的话,下面的视频会很好的解释它。
在Dart中,你已经可以使用int类型来表示有符号的值,包括正值和负值。然而,如果你想使用8位有符号整数的列表,你可以使用Int8List类型(注意没有U)。这允许使用-128到127的值。这对于大多数表示字节数据的使用情况来说并不是特别有用,所以我们将坚持使用 Uint8List 中的无符号整数。
将List<int>转换为Uint8List
Uint8List是dart:typed_data的一部分,Dart的核心库之一。要使用它,请添加以下导入。
import 'dart:typed_data';
现在你可以通过使用 fromList 方法将之前的 List<int> 列表转换为 Uint8List。
List<int> data = [102, 111, 114, 116, 121, 45, 116, 119, 111, 0];
Uint8List bytes = Uint8List.fromList(data);
如果List<int>列表中的任何一个值超出了0到255的范围,那么这些值就会被包起来。你可以在下面的例子中看到这一点。
List<int> data = [3, 256, -2, 2348738473];
Uint8List bytes = Uint8List.fromList(data);
print(bytes); // [3, 0, 254, 169]
256超出了255一个,所以变成了0,接下来的数字-2,小于0,所以绕到了最上面两个位置,变成了254。而我不知道2348738473包裹了多少次,但最终还是变成了169。
如果你不想发生这种包装,你可以使用Uint8ClampedList来代替。这将把所有大于255的值箝制为255,所有小于0的值箝制为0。
List<int> data = [3, 256, -2, 2348738473];
Uint8ClampedList bytes = Uint8ClampedList.fromList(data);
print(bytes); // [3, 255, 0, 255]
这次最后三个值都被夹住了。
创建一个Uint8List
在上面的方法中,你通过转换一个List<int>创建了一个Uint8List。如果你只是想从一个字节列表开始呢?你可以通过将列表的长度传入构造函数来实现,就像这样。
final byteList = Uint8List(128);
这将创建一个固定长度的列表,其中所有128个值都是0。 打印byteList,你将看到:
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
随意数一数,确定真的有128个。
修改一个字节列表
如何修改Uint8List的值?就像修改普通List一样。
byteList[2] = 255;
再次打印byteList显示索引2的值已经改变。
[0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
列表的随机访问特性使得在任何索引位置修改字节值都很快捷方便。
可增长的列表
你上面的列表是固定长度的。如果你尝试做这样的事情。
final byteList = Uint8List(128);
byteList.add(42);
你会得到以下异常。
Unsupported operation: Cannot add to a fixed-length list
如果你想做一些类似于从流中收集字节的事情,那就不是很方便。要制作一个可增长的字节列表,你需要一个BytesBuilder。
final bytesBuilder = BytesBuilder();
bytesBuilder.addByte(42);
bytesBuilder.add([0, 5, 255]);
你可以添加单个字节(类型为int)或字节列表(类型为List<int>)。
当你想将builder转换为Uint8List时,可以像这样使用toBytes方法。
Uint8List byteList = bytesBuilder.toBytes();
print(byteList); // [42, 0, 5, 255]
不止一种方式来看待比特
比特和字节只是很多0和1。如果你没有办法解释它们,它们就没有什么意义。
0011111100011101110011011101101110011111100110011011110000010101001110100010101010000111100101101111001111011100000101011001101111010101101000011101100101000111100000100100011111101011111110100111010011001100000000000111111010010010000000001111010001100000
对于Uint8List,我们的解释方式是说每8位是0到255之间的数字。
00111111 00011101 11001101 11011011 10011111 10011001 10111100 00010101 00111010 00101010 10000111 10010110 11110011 11011100 00010101 10011011 11010101 10100001 11011001 01000111 10000010 01000111 11101011 11111010 01110100 11001100 00000000 01111110 10010010 00000000 11110100 01100000
你可以将这些相同的值重新解释为从-128到127的有符号整数。这就是我们在 Int8List 中所做的。
不过,还有其他的方法来看待数据。例如,你可以把数据看成一个16位块的列表,而不是使用8位块。
0011111100011101 1100110111011011 1001111110011001 1011110000010101 0011101000101010 1000011110010110 1111001111011100 0001010110011011 1101010110100001 1101100101000111 1000001001000111 1110101111111010 0111010011001100 0000000001111110 1001001000000000 1111010001100000
dart:typed_data库也有相应的类型。使用Uint16List来处理从0到65,535的无符号整数,或者使用Int16List来处理从-32,768到32,767的有符号整数。
这并没有停止。你也可以将同样的数据解释为32位值的列表。
00111111000111011100110111011011 10011111100110011011110000010101 00111010001010101000011110010110 11110011110111000001010110011011 11010101101000011101100101000111 10000010010001111110101111111010 01110100110011000000000001111110 10010010000000001111010001100000
或64位值。
0011111100011101110011011101101110011111100110011011110000010101
0011101000101010100001111001011011110011110111000001010110011011
1101010110100001110110010100011110000010010001111110101111111010
0111010011001100000000000111111010010010000000001111010001100000
甚至是128位的数值。
00111111000111011100110111011011100111111001100110111100000101010011101000101010100001111001011011110011110111000001010110011011
11010101101000011101100101000111100000100100011111101011111110100111010011001100000000000111111010010010000000001111010001100000
Dart有所有这些类型。
Int32ListUint32ListInt64ListUint64ListInt32x4List(128位)
对于大量的数据,使用你需要的特定列表类型会比使用List<int>更有效。
Dart中的字节视图
Dart用ByteBuffer来备份原始二进制数据。你在上一节中看到的类型都实现了一个叫做TypedData的类,它只是查看ByteBuffer中数据的一种通用方式。这意味着像Uint8List、Int32List和Uint64List这样的类型都只是查看同一数据的不同方式。
我们将在接下来的例子中使用以下四个字节的列表。
00000001 00000000 00000000 10000000
在十进制形式下,这个列表是这样的。
1, 0, 0, 128
首先在Dart中像之前一样创建列表。
Uint8List bytes = Uint8List.fromList([1, 0, 0, 128]);
与任何形式的TypedData一样,你可以通过访问buffer属性来获得对Uint8List底层ByteBuffer的访问。
ByteBuffer byteBuffer = bytes.buffer;
无符号的16位视图
现在你已经有了字节缓冲区,你可以通过使用as...方法来获得不同的字节视图,这次是asUint16List。
Uint16List sixteenBitList = byteBuffer.asUint16List();
在打印sixteenBitList查看内容之前,你认为值会是什么?
觉得你知道答案了吗?好,打印列表。
print(sixteenBitList);
在我的Mac电脑上,结果如下:
[1, 32768]
这就很奇怪了。因为原来的数值是:
00000001 00000000 00000000 10000000
1 0 0 128
我本来以为它们会被组合成16位的块状,像这样。
0000000100000000 0000000010000000
256 128
相反,我们得到了这个。
0000000000000001 1000000000000000
1 32768
保持这种想法。让我们检查一下32位视图。
无符号32位视图
我先从包含十进制值0、1、2、3的字节列表开始。这样我们就可以看到它们的顺序是否相同。为了清楚起见,这里是原始列表的8位二进制形式。
00000000 00000001 00000010 00000011
0 1 2 3
现在运行以下代码。
Uint8List bytes = Uint8List.fromList([0, 1, 2, 3]);
ByteBuffer byteBuffer = bytes.buffer;
Uint32List thirtytwoBitList = byteBuffer.asUint32List();
print(thirtytwoBitList);
这次你在底层缓冲区中的32位视图从原来的Uint8List。print语句显示的值是50462976,在32位二进制中是。
00000011000000100000000100000000
或如果您添加间距(以帮助查看部件)。
00000011 00000010 00000001 00000000
这和原来的顺序完全相反! 发生什么事了?
Endianness
当你只在愉快地构建一个Flutter应用的时候,你通常不需要考虑这种事情,但是当你像我们今天这样低调的时候,你其实是在和机器架构杠上了。
有些机器将一个字节块内的各个字节(不管是2字节块,4字节块,还是8字节块)按正向顺序排列。这被称为大恩典,因为最重要的字节在前。这通常是我们所期望的,因为我们从左到右读取数字,从数字的最大部分开始。
然而,其他机器会以相反的顺序排列一大块字节中的各个字节。这就是所谓的小恩迪安。虽然这看起来很不直观,但实际上在机器架构层面是可行的。当大恩迪安遇到小恩迪安时,问题就来了。这就是我们上面的情况。
在我详谈这个问题之前,你可能喜欢看这个关于endianness的视频。解释的很好(如果你能通过开头的小故事)。
沟通不畅的原因是什么
当我运行这段代码时
Uint8List bytes = Uint8List.fromList([0, 1, 2, 3]);
ByteBuffer byteBuffer = bytes.buffer;
Uint32List thirtytwoBitList = byteBuffer.asUint32List();
print(thirtytwoBitList);
Dart把这四个字节的列表。
00000000 00000001 00000010 00000011
并把它们交给了我的电脑。达特知道000000是第一,00000001是第二,00000010是第三,00000011是最后。我的电脑也知道这一点。
然后Dart要求我的电脑从我的电脑内存中的字节缓冲区中获取32位的字节列表的分块视图。我的电脑很高兴地答应了,并返回了:"0000000011000000100000000100000000"。
00000011000000100000000100000000
好吧,原来我的MacBook有一个小小的恩迪安架构。我的电脑仍然知道000000是第一个,000001是第二个,000010是第三个,000011是最后一个。然而,当print语句在某个地方调用toString方法时,它把这32位解释为大恩迪安格式的单个整数。不能说我怪它。
在Dart中检查endianness
如果你想知道你的系统的endian架构是什么,你可以在Dart中运行以下代码。
print(Endian.host == Endian.little);
如果打印为true,那么你也有一台小恩迪安机器。否则,就是大恩迪安机。不过你的有可能是小恩迪安,因为现在大多数个人电脑都使用小恩迪安。
如果你看一下Endian类的源代码,你会发现它检查主机架构的方式与导致我最初惊讶的原因非常相似。
Class Endian {
...
static final Endian host =
(new ByteData.view(new Uint16List.fromList([1]).buffer))
.getInt8(0) == 1
小
大。
}
它把16位的1表示出来,然后看前8位是否是1,如果是,那就是小恩迪安。这是因为小恩迪安对这16位的顺序是这样的。
00000001 00000000
而大恩迪安机是这样订购的。
00000000 00000001
我在乎什么?
你可能会认为你不需要担心这个问题,因为你不打算如此接近机器硬件。然而,endianness也会在其他情况下出现。基本上任何时候,只要你在处理大于8位块的共享数据,你都需要注意endianness。
例如,如果你看了我上面链接的视频,你就知道不同的文件格式使用不同的endian编码。
- JPEG(大端)
- GIF(小端)
- PNG(大端)
- BMP(小端)
- MPEG-4(大端)
- 网络数据(大端)
- UTF-16文本文件(大端或小端--观看此视频)
所以,即使你知道你正在处理的原始字节数据的endianness,你如何将其传达给Dart,使你不会出现你看到的我之前的误传问题?
继续阅读就会知道。
在Dart中处理endianness
一旦你得到了你的原始数据作为一些像Uint8List这样的TypedData,你就可以使用ByteData类对它进行随机访问读写任务。这样你就不用直接与BytesBuffer进行交互了。ByteData还允许你指定你想要解释数据的endianness。
编写以下几行代码。
Uint8List byteList = Uint8List.fromList([0, 1, 2, 3]);
ByteData byteData = ByteData.sublistView(byteList);
int value = byteData.getUint16(0, Endian.big);
print(value);
这就是它的作用。
- 你从一个整数列表中获取
Uint8List类型的数据。这和你之前看到的一样。 - 然后你用
ByteData来包装这个数据。视图(或 "子列表 "视图)是观察缓冲区中数据的一种方式。它被称为子列表的原因是,你不需要有整个缓冲区的视图,因为缓冲区可能非常大。可以查看缓冲区内较小范围的字节。不过在我们的例子中,byteList的缓冲区只包括这四个字节,所以我们要查看整个缓冲区。 - 之后,你从索引
0开始访问16位(或两个字节),这里的索引是以一个字节为增量的。如果你选择1作为索引,你会得到第二和第三个字节,如果你选择2作为索引,你会得到第三和第四个字节。在这种情况下,任何高于2的索引都会抛出一个错误,因为只有四个字节可用。 - 最后,你还告诉Dart,你想把从索引
0开始的两个字节解释为大恩迪安。这实际上是ByteData的默认值,所以你可以不使用这个参数。
运行这段代码,你会看到输出为1,这是有道理的,因为
00000000 00000001
以大端方式解释,其值为1。
再做一次,将Endian.big替换为Endian.little。现在当你运行这段代码时,你会看到256,因为它把第二个字节(000001)解释为在第一个字节(00000000)之前,所以你得到了
0000000100000000
这不是你想要的数据,所以把代码改回 Endian.big。
在ByteData中设置字节
在上面的部分,你得到的是字节。你也可以在指定endianness的同时设置字节,方法和你得到字节的方法差不多。在你上面写的内容中加入下面一行。
byteData.setInt16(2, 256, Endian.big);
print(byteList);
这里你设置了两个字节(值为256),从缓冲区中的索引二开始。打印一下,你就会看到。
[0, 1, 1, 0]
这有意义吗?之前的缓冲区包含:
[0, 1, 2, 4]
但你把最后两个字节换成了256,在二进制中是:
00000001 00000000
或1和0,当以大恩迪安的方式解释。
好了,关于endianness就到此为止。大多数时候,你可以接受默认的大英译法,而不去想它。
十六进制和二进制
到目前为止,我一直把本文中所有的二进制数都写成10101010这样的字符串,但是在写代码的时候使用这种格式不是很方便,因为Dart对于整数的默认是10基。
int x = 10000000;
那是1000万,不是128。 :(
虽然直接写二进制可能很难,但在Dart中直接写十六进制的值却很容易。只要在十六进制数前加上0x,就像这样。
int x = 0x80; // 10000000 binary
print(x); // 128 decimal
由于二进制和十六进制之间的关系非常直接,这就使得二者之间的转换成为一项简单的任务。如果你经常使用二进制,转换表可能值得记忆。
hex binary
----------
0 0000
1 0001
2 0010
3 0011
4 0100
5 0101
6 0110
7 0111
8 1000
9 1001
A 1010
B 1011
C 1100
D 1101
E 1110
F 1111
字节值将永远是两个十六进制值块(或技术上称为4位块的nibbles,哦,那些早期程序员的幽默)。
下面再举几个例子。如果覆盖二进制,只看十六进制,你能不看就能找出二进制吗?
hex binary
----------------
80 1000 0000
2A 0010 1010
F2F2F2 1111 0010 1111 0010 1111 0010
1F60E 0001 1111 0110 0000 1110
说个小知识,第三个值是 Medium 用来作为代码块背景色的灰色阴影的 RGB(红绿蓝)十六进制值。而最后一个是带着墨镜的笑脸表情😎的Unicode码点。
转换十六进制和二进制字符串
在Dart中,有一个有趣的技巧,你可能不知道,就是你可以在不同的基数之间进行转换,并得到字符串表示的值。
下面是一个例子,将一个十进制数转换成十六进制和二进制的字符串形式。
String hex = 2020.toRadixString(16).padLeft(4, '0');
print(hex); // 07e4
String binary = 2020.toRadixString(2);
print(binary); // 11111100100
注释:"Radix "是指基数。
- Radix只是指基数。
- 第一个例子取十进制数
2020,将其转换为底数-16(即十六进制),并确保长度为4。这使得7e4变成了07e4。 - 第二个例子是将十进制
2020转换成String格式的二进制。
你可以使用int.parse进行另一种方式的转换。
int myInt = int.parse('07e4', radix: 16);
print(myInt); // 2020
myInt = int.parse('11111100100', radix: 2);
print(myInt); // 2020
将Unicode字符串转换为字节,再转换回来。
当我在这里说字节的时候,我只是指数字。Dart中的String类型是一个Unicode数字的列表。Unicode数字被称为码点,可以小到0,大到10FFFF。下面是一个例子。
character Unicode code point (hex)
-------------------------------
H 48
e 65
l 6C
l 6C
o 6F
😎 1F60E
Dart用的码点词是符文。你可以看到它们是这样的。
Runes codePoints = 'Hello😎'.runes;
print(codePoints); // (72, 101, 108, 108, 111, 128526)
那是它们十六进制值的十进制版本。
符文是一个可迭代的int值集合。然而,就像你在文章开头看到的List<int>和Uint8List一样,当英语世界中的大多数字符(除了像😎这样的表情符号)只需要8位时,64位甚至32位的文本存储效率并不高。即使是现存的成千上万的汉字中的大部分也可以用不到16位来编码。
出于这个原因,大多数人使用8位或16位编码系统对Unicode值进行编码。当一个Unicode值太大而无法容纳在8位或16位中时,系统会使用一种特殊的技巧来编码更大的值。8位编码系统称为UTF-8,16位系统称为UTF-16。
UTF-8和UTF-16的编码方式超级有趣,尤其是他们用来编码大的Unicode码点的技巧。如果你对这种事情感兴趣(如果你还在阅读的话,你可能会感兴趣),你一定要看下面的视频。这是我见过的关于这个主题的最好的视频。
Dart中的UTF-16转换
虽然Unicode码点在你查询符文时可以作为一个Iterable,但Dart内部使用UTF-16作为字符串的实际编码。这些16位的值被称为代码单位而不是代码点。
从字符串中获取UTF-16代码单位的转换很简单。
List<int> codeUnits = 'Hello😎'.codeUnits;
print(codeUnits); // [72, 101, 108, 108, 111, 55357, 56846]
笔记。
- 你可能会想,"嘿,一个
int是64个字节,不是16!" 这只是在你做完转换后,它在外部对你的表示方式。int类型应该是通用的,你不应该去考虑它使用了多少字节。在内部,字符串是16位整数的列表。 - 你可以看到,它需要两个UTF-16值来表示😎。
55357和56846,也就是D83D和DE0E的十六进制。这两个数字被称为代用对,如果你看了上面的视频,你就会知道这一切。
Decimal Hex Binary
--------------------------------
55357 D83D 1101100000111101 (high surrogate)
56846 DE0E 1101111000001110 (low surrogate)
String Hex Binary
----------------------------------------
F60E 0000 1111 0110 0000 1110
+ 10000 0001 0000 0000 0000 0000
----------------------------------------
😎 1F60E 0001 1111 0110 0000 1110
由于每个UTF-16代码单元的长度都是一样的,这也使得通过其索引访问任何代码单元变得超级容易。
int index = 0;
int codeUnit = 'Hello😎'.codeUnitAt(index);
print(codeUnit); // 72
从代码单位转换回字符串也很容易。
List<int> codeUnits = 'Hello😎'.codeUnits;
final myString = String.fromCharCodes(codeUnits);
print(myString); // Hello😎
print(String.fromCharCode(72)); // H
尽管这些都很好,但基于UTF-16的字符串操作还是存在一些问题。例如,如果你一次只删除一个代码单元,你很有可能会忘记代用对(和词素簇)。当这种情况发生时,笑笑就不那么笑了。阅读以下文章了解更多关于这个主题的内容。
转换UTF-8字符串
当Dart使用UTF-16编码时,互联网使用UTF-8编码,这意味着当你在网络上传输文本时,你需要将你的Dart字符串转换为UTF-8编码的整数列表。这意味着当你在网络上传输文本时,你需要将你的Dart字符串转换为一个UTF-8编码的整数列表。同样,请记住,这只是一个8位二进制数的列表,特别是一个Uint8List。8位值的好处是,你不需要担心endianness。
如果你看了我上面链接的视频,现在明白了UTF-8编码的工作原理,你可以自己编写编码器和解码器。然而,你不需要这样做,因为Dart的dart:convert库中已经有了它们。
像这样导入库。
import 'dart:convert';
然后你就可以像这样简单的将字符串转换为UTF-8。
Uint8List encoded = utf8.encode('Hello😎');
print(encoded); // [72, 101, 108, 108, 111, 240, 159, 152, 142]
这次Smiley被编码为四个8位值。
Decimal Hex Binary
------------------------
240 F0 11110000
159 9F 10011111
152 98 10011000
142 8E 10001110
String Hex Binary
------------------------
😎 1F60E 00001 1111 0110 0000 1110
我告诉你,你需要看那个视频。😎
从UTF-8转换回String也一样简单。这次使用解码方法。
List<int> list = [72, 101, 108, 108, 111, 240, 159, 152, 142];
String decoded = utf8.decode(list);
print(decoded); // Hello😎
传入一个int值的列表是可以的,但是如果其中任何一个值大于8位,你就会得到一个异常,所以要注意这一点。
布尔逻辑
我假设你知道&&, ||, 和! 布尔类型的逻辑运算符。
// AND
print(true && true); // true
print(true && false); // false
print(false && false); // false
// OR
print(true || true); // true
print(true || false); // true
print(false || false); // false
// NOT
print(!true); // false
print(!false); // true
好吧,这些运算符也有在二进制数上工作的位智等价物。如果你把false看成0,把true看成1,那么你就会得到与位运算符&、|和~类似的结果(加上一个额外的^ XOR运算符)。
// AND
print(1 & 1); // 1
print(1 & 0); // 0
print(0 & 0); // 0
// OR
print(1 | 1); // 1
print(1 | 0); // 1
print(0 | 0); // 0
// XOR
print(1 ^ 1); // 0
print(1 ^ 0); // 1
print(0 ^ 0); // 0
// NOT
print(~1); // -2
print(~0); // -1
嗯,结果几乎是一样的。~ bitwise NOT运算符给出了一些奇怪的数字。我们稍后再来讨论这个问题,尽管你可能已经猜到了原因。
如果你想在我们看到一些Dart的例子之前获得更多的位智操作的背景知识,可以看看这个视频系列。
Bitwise AND运算符&
bitwise &运算符比较每一对比特,只有当输入的两个比特都是1时,才会在结果比特中给出1。
下面是一个例子。
final y = 0x4a; // 01001010
final x = 0x0f; // 00001111
final z = x & y; // 00001010
为什么会有用呢?嗯,有一件事很有用,那就是做一个AND位掩码。比特掩码是一种过滤掉一切的方法,但你要找的信息除外。
你可以在 Flutter TextPainter 源代码中看到它的实际应用。
static bool _isUtf16Surrogate(int value) {
return value & 0xF800 == 0xD800;
}
这个静态方法检查一个UTF-16值是否是代理对。代名词的范围是0xD800-0xDFFF。值0xF800是位掩码。
用boolean的方式看就比较容易理解了。
D800 1101 1000 0000 0000 (min)
DFFF 1101 1111 1111 1111 (max)
F800 1111 1000 0000 0000 (bitmask)
你注意到,任何代用词的前五位都是11011。后面的11位可以是任何东西。所以这就是比特掩码的作用。它使用5个1位来匹配它所寻找的模式,而11个0位来忽略其余的。
记得我们的朋友Smiley 😎是由代用对D83D和DE0E组成的,所以任何一个值都应该返回true。让我们试试第二个,DE0E。
DE0E 1101 1110 0000 1110 (test value)
F800 1111 1000 0000 0000 (bitmask)
-------------------------
D800 1101 1000 0000 0000 (result of & operation)
结果是D800,所以应该可以使比较真实,也就是说是的,DE0E是一个代用值。你可以自己测试一下。
bool _isUtf16Surrogate(int value) {
return value & 0xF800 == 0xD800;
}
print(_isUtf16Surrogate(0xDE0E)); // true
双位OR运算符|
bitwise |运算符比较每一对位,如果输入位中的任何一个(或两个)是1,则结果位中的1。
下面是一个例子。
final y = 0x4a; // 01001010
final x = 0x0f; // 00001111
final z = x | y; // 01001111
为什么这很有用?好吧,你也可以把它作为一个位掩码来 "开启 "某些位,而让其他位不受影响。 例如,颜色通常被装入一个32位的整数中,前8位代表alpha值(透明度),接下来的8位是红色值,接下来的8位是绿色值,最后8位是蓝色值。
所以我们假设你这里有这种半透明的紫色。
如果你想保留这个颜色,但又想让它完全不透明,你可以使用OR位掩码来实现。该颜色的十六进制值是 802E028A。
alpha red green blue
-----------------------------------
80 2E 02 8A
10000000 00101110 00000010 10001010
完全透明是0x00,完全不透明是0xFF。所以你要做一个位掩码,使红、绿、蓝的值保持不变,但使alpha为0xFF。在这种情况下,我们应该期望应用位掩码后的结果为FF2E028A。
10000000 00101110 00000010 10001010 (original)
11111111 00000000 00000000 00000000 (bitmask)
-----------------------------------
11111111 00101110 00000010 10001010 (result of | operation)
由于ORing任何带1的东西都会变成1,这就把所有的alpha值都变成了1。ORing任何带0的值都不会改变,所以其他值保持不变。
在Dart中试试。
final original = 0x802E028A;
final bitmask = 0xFF000000;
final opaque = original | bitmask;
print(opaque.toRadixString(16)); // ff2e028a
知道了!
附注:如果你对十六进制字符串是小写的感到不舒服,你可以随时将其覆盖为大写。
print('ff2e028a'.toUpperCase()); // FF2E028A
位XOR运算符^
bitwise ^运算符比较每一对位,如果输入位不同,则结果位中有1。
下面是一个例子。
final y = 0x4a; // 01001010
final x = 0x0f; // 00001111
final z = x ^ y; // 01000101
为什么会有用呢?在Dart中,你会经常看到^操作符用来创建哈希码。例如,这里有一个Person类。
class Person {
final String name;
final int age;
Person(this.name, this.age);
@override
bool operator ==(Object other) {
return identical(this, other) ||
other is Person &&
runtimeType == other.runtimeType &&
name == other.name &&
age == other.age;
}
@override
int get hashCode => name.hashCode ^ age.hashCode;
}
最后一行的hashCode是有趣的部分。让我们在这里重现一下。
final name = 'Bob';
final age = 97;
final hashCode = name.hashCode ^ age.hashCode;
print(name.hashCode); // 124362681
print(age.hashCode); // 97
print(hashCode); // 124362712
print(name.hashCode.toRadixString(2).padLeft(32, '0'));
print(age.hashCode.toRadixString(2).padLeft(32, '0'));
print(hashCode.toRadixString(2).padLeft(32, '0'));
下面是二进制结果。
00000111011010011001111110111001 (name hash code)
00000000000000000000000001100001 (age hash code)
--------------------------------
00000111011010011001111111011000 (result of ^ operation)
对于哈希码,你希望它们尽可能的分布,这样你就不会发生哈希碰撞。如果你进行&操作,你会得到更多的0,如果你进行|操作,你会得到更多的1。由于使用^运算符可以保持0和1的分布,这使得它成为从其他哈希码中创建新哈希码的良好候选者。
Bitwise NOT运算符 ~
~运算符将位的值反转。之前你看到了这些令人困惑的结果。
// NOT
print(~1); // -2
print(~0); // -1
如果你看二进制值,这就更有意义了。这里是64位的1的表示方法。
0000000000000000000000000000000000000000000000000000000000000001
当你把所有这些位翻转过来(即~1的结果),你就会得到。
1111111111111111111111111111111111111111111111111111111111111110
如果你看过 "二的补数 "的视频,你会记得这就是你用二进制表示数字-2的方法。对于~0也是类似的故事,问题解决了。
顺便说一下,这里有一个笑话给喜欢莎士比亚的人。
2b|~2b
这就是问题所在。
位移<< >>
还有最后一个话题要讲:位移位。您可以使用位向左移位运算符<<,以及位向右移位运算符>>将位移到右边。
你可以在这个视频中了解更多关于位移位的知识。
下面是一个Dart中左移位的例子。
final value = 0x01; // 00000001
print(value << 5); // 00100000
所有的位子都被左移了5,00100000的二进制值是32。一个有趣的事实是,乘以二的简单方法是左移一。
print(7 << 1); // 14 (decimal)
这里是右移。
final value = 0x80; // 10000000
print(value >> 3); // 00010000
这相当于说128右移3是16。
为什么这很重要?好吧,在Dart中你可以看到它的一个地方是从一个打包的整数中提取值。例如,Color类的源代码有以下getter来从一个颜色值中提取红色值。
/// The red channel of this color in an 8 bit value.
int get red => (0x00ff0000 & value) >> 16;
这首先使用AND位掩码只找到ARGB(alpha红绿蓝)值的红色部分,然后将结果移到开头。下面是我们之前的 802E028A 紫色的效果。
alpha red green blue
-----------------------------------
80 2E 02 8A
10000000 00101110 00000010 10001010 (original)
00000000 11111111 00000000 00000000 (bitmask)
-----------------------------------
00000000 00101110 00000000 00000000 (result of &)
-----------------------------------
00000000 00000000 00000000 00101110 (result of >> 16)
而这里是在Dart中。
final purple = 0x802E028A;
final redBitmask = 0x00FF0000;
final masked = purple & redBitmask;
final redPart = masked >> 16;
print(redPart.toRadixString(16)); // 2e
关键点
你成功了! 以下是本文的要点。
-
二进制数据可以用一个整数列表来表示。
-
Uint8List是一个8位整数的无符号列表,在处理大量二进制数据时,它比List<int>更有效。 -
您可以使用
BytesBuilder将二进制数据添加到列表中。 -
字节数据由
BytesBuffer支持,您可以获得底层字节的不同视图。例如,你可以看到8位、16位、32位或64位的字节块,可以是有符号或无符号整数。 -
对于大于8位的字节块,你需要注意字节数,它受底层机器或存储格式的影响。你可以使用
ByteData来指定大恩迪恩或小恩迪恩视图。 -
在数字前加
0x,用十六进制符号来写。 -
Dart字符串是UTF-16值的列表,被称为代码单位。
-
通过使用
dart:convert库将字符串转换为UTF-8。 -
位元逻辑运算符是
&、|、^和~,移位运算符是<<和>>。 -
位元运算符和位元掩码允许你访问和操作单个位元。
继续
值得进一步研究的几个方面是字节流以及如何转换这些流上的数据。例如,当读取或下载一个文件时,你可以得到一个字节流。与其等待整个文件到达后再处理它,不如在得到字节流时就开始将其转换为UTF-8值(或其他值)。 如果有什么不清楚的地方,记得留言或提问。
通过www.DeepL.com/Translator (免费版)翻译