在串口通信过程中,进行解析时经常会遇到字符串、十六进制、文本等之间的相互转换,本文主要对相关的数据转换进行分析。
1,十六进制转换为文本
private fun hexToText(hex: String): String {
if (hex == null) {
throw IllegalArgumentException("Input hex string cannot be null")
}
// 去除十六进制字符串中的所有空格
val hexWithoutSpaces = hex.replace(" ", "")
// 检查去除空格后的字符串长度是否为偶数
if (hexWithoutSpaces.length % 2 != 0) {
throw IllegalArgumentException("Hexadecimal string length must be an even number, but got ${hexWithoutSpaces.length}")
}
// 把字符串转换为字符数组,直接操作字符数组,避免频繁调用substring方法,可以提高性能。
val hexChars = hexWithoutSpaces.toCharArray()
// 初始化一个字节数组,其长度为去除空格后字符串长度的一半。
val byteArray = ByteArray(hexChars.size / 2)
// 逐字节转换:通过 for 循环遍历 byteArray 的每个索引,调用 hexCharToInt //函数将每两个十六进制字符转换为对应的高四位和低四位整数,然后通过位运算组合成一个字节,存储到 byteArray 中。
for (i in byteArray.indices) {
val highNibble = hexCharToInt(hexChars[i * 2], i * 2)
val lowNibble = hexCharToInt(hexChars[i * 2 + 1], i * 2 + 1)
byteArray[i] = ((highNibble shl 4) or lowNibble).toByte()
}
// 将 byteArray 按照 UTF - 8 编码转换为字符串并返回。
return String(byteArray, Charsets.UTF_8)
}
private fun hexCharToInt(hexChar: Char, index: Int): Int {
return when (hexChar) {
// 如果字符在 '0' 到 '9' 之间,将其转换为对应的整数(通过减去字符 '0' 的 ASCII 值)。
in '0'..'9' -> hexChar - '0'
// 如果字符在 'A' 到 'F' 之间,将其转换为对应的整数(通过减去字符 'A' 的 ASCII 值并加上 10)。
in 'A'..'F' -> hexChar - 'A' + 10
// 如果字符在 'a' 到 'f' 之间,将其转换为对应的整数(通过减去字符 'a' 的 ASCII 值并加上 10)。
in 'a'..'f' -> hexChar - 'a' + 10
// 如果字符不在上述范围内,说明它不是一个有效的十六进制字符,抛出 IllegalArgumentException 异常,并给出详细的错误信息,包括该字符和其索引位置。
else -> throw IllegalArgumentException("Non-hexadecimal character '$hexChar' found at index $index")
}
}
// 已知val hex = "E7 AA 97 E5 B8 98 31 00 00 00 00 00 00 00 00 00 00 00",
// 调用上面的代码,即
try {
val text = hexToText(hex).trimEnd('\u0000')
println("转换后的文本是: $text")
} catch (e: IllegalArgumentException) {
println("错误: ${e.message}")
}
// 那么上面的代码有如下输出:
// 转换后的文本是: 窗帘1
2,十六进制转换为字节列表
// 把一个以空格分隔的十六进制字符串转换为一个可变的有符号字节列表。
private fun hexStringToByteList(hexString: String): MutableList<Byte> {
// 借助 split(" ") 方法把输入的十六进制字符串按空格分割成一个字符串列表。例如,输入 "AA 97 E5" 会被分割成 ["AA", "97", "E5"]。
return hexString.split(" ")
// 使用 filter { it.matches(Regex("[0-9A-Fa-f]{2}")) } 筛选出符合 [0-9A-Fa-f]{2} 正则表达式的字符串,
// 也就是长度为 2 且仅包含十六进制字符(0 - 9、A - F、a - f)的字符串。这一步能确保后续处理的都是有效的十六进制对。
.filter {
it.matches(Regex("[0-9A-Fa-f]{2}"))
}
// 利用 map { it.toInt(16).toByte() } 把每个筛选出的十六进制字符串转换为整数(使用 toInt(16) 方法以十六进制解析字符串),
// 再将其转换为有符号字节(Byte 类型)。
.map {
it.toInt(16)
.toByte()
}
// 调用 toMutableList() 把转换后的字节列表转换为可变列表并返回
.toMutableList()
}
// 将一个以空格分隔的十六进制字符串转换为无符号字节数组。
private fun hexStringToUByteArray(hexString: String): UByteArray {
return hexString.split(" ")
.filter {
it.matches(Regex("[0-9A-Fa-f]{2}"))
}
// 通过 map { it.toInt(16).toUByte() } 把每个筛选出的十六进制字符串转换为整数,
// 再将其转换为无符号字节(UByte 类型)。
.map {
it.toInt(16)
.toUByte()
}
// 调用 toUByteArray() 把转换后的无符号字节列表转换为无符号字节数组并返回。
.toUByteArray()
}
上面的代码通过如下调用:
val hex = "E7 AA 97 E5 B8 98 31 00 00 00 00 00 00 00 00 00 00 00"
val list = hexStringToByteList(hex)
println("hexStringToByteList:$list")
val list1 = hexStringToUByteArray(hex)
println("hexStringToUByteArray:${list1.contentToString()}")
得到的输出如下:
hexStringToByteList:[-25, -86, -105, -27, -72, -104, 49, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
hexStringToUByteArray:[231, 170, 151, 229, 184, 152, 49, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
对于十六进制转换为字节数组,还可以使用如下方式:
// 将一个十六进制字符串转换为对应的字节数组
fun hexStringToByteArray(hex: String): ByteArray {
// require 是 Kotlin 中的一个标准库函数,用于对输入参数进行验证。
// hex.length % 2 == 0 检查输入的十六进制字符串的长度是否为偶数。因为每两个十六进制字符对应一个字节,所以十六进制字符串的长度必须为偶数才能正确转换为字节数组。
// 如果输入的字符串长度不是偶数,require 函数会抛出一个 IllegalArgumentException 异常,并附带指定的错误信息 "Hex string must have an even length"。
require(hex.length % 2 == 0) {
"Hex string must have an even length" }
// chunked(size: Int) 是 Kotlin 字符串的一个扩展函数,
// 它将字符串按照size指定的大小的字符为一组进行分割,返回一个包含多个子字符串的列表。
// 例如,对于输入的十六进制字符串 "123456",chunked(2) 会返回 ["12", "34", "56"]。
return hex.chunked(2)
// map 是 Kotlin 集合的一个扩展函数,用于对集合中的每个元素进行转换操作。
.map {
// it.toInt(16) 将每个子字符串(即十六进制表示的字符串)转换为对应的整数。
// toInt(16) 方法的参数 16 表示将字符串按照十六进制进行解析。例如,字符串 "12" 会被转换为整数 18(因为 1 * 16 + 2 = 18)。
it.toInt(16)
// .toByte() 将转换后的整数再转换为字节类型。需要注意的是,如果整数超出了字节类型的范围(-128 到 127),会进行截断处理。
.toByte()
}
// toByteArray() 是 Kotlin 集合的一个扩展函数,它将经过转换后的元素列表转换为字节数组并返回。
.toByteArray()
}
3,字节列表转换为文本
fun byteListToString(byteList: MutableList<Byte>): String {
// 调用了 toByteArray() 方法,将输入的可变字节列表 byteList 转换为一个普通的字节数组 byteArray。
// 之所以要进行这样的转换,是因为 String 类的构造函数通常接受字节数组作为参数,而不是列表。
val byteArray = byteList.toByteArray()
// 使用 String 类的构造函数,将上一步得到的字节数组 byteArray 转换为一个字符串。
//Charsets.UTF_8 明确指定了使用 UTF - 8 编码来解析字节数组。
// UTF - 8 是一种广泛使用的字符编码,它可以表示世界上大多数的字符。
return String(byteArray, Charsets.UTF_8)
// '\u0000' 是一个 Unicode 转义序列,代表的是 ASCII 码中的空字符(Null 字符),其十进制值为 0。
// 在计算机编程里,空字符常常被用作字符串的结束符或者填充字符。
.trimEnd('\u0000')
}
上面的代码通过如下调用:
val hex = "E7 AA 97 E5 B8 98 31 00 00 00 00 00 00 00 00 00 00 00"
val list = hexStringToByteList(hex)
val str = byteListToString(list)
println("byteListToString:$str")
上面代码的输出如下: byteListToString:窗帘1
4,十六进制数据与字节数组之间的转换
// 将十六进制字符串转换为字节数组
fun hexToBytes(hex: String): ByteArray {
// 计算十六进制字符串的长度 len,并创建一个长度为 len / 2 的字节数组 data,因为每两个十六进制字符对应一个字节。
val len = hex.length
val data = ByteArray(len / 2)
var i = 0
// 使用 while 循环遍历十六进制字符串,每次处理两个字符。
while (i < len) {
// Character.digit(hex[i], 16) 用于将当前字符转换为对应的十六进制数值,
// 然后左移 4 位(shl 4),再加上下一个字符转换后的数值,
// 最后将结果转换为字节类型存储到 data 数组中。
data[i / 2] = ((Character.digit(hex[i], 16) shl 4)
+ Character.digit(hex[i + 1], 16)).toByte()
i += 2
}
return data
}
// 将字节数组转换为十六进制字符串
fun bytesToHex(bytes: ByteArray): String {
// 创建一个长度为 bytes.size * 2 的字符数组 hexChars,因为每个字节对应两个十六进制字符。
val hexChars = CharArray(bytes.size * 2)
// 使用 for 循环遍历字节数组,对于每个字节,先将其转换为无符号整数(toInt() and 0xFF),
// 然后将高 4 位和低 4 位分别转换为对应的十六进制字符,存储到 hexChars 数组中。
// bytes.indices 会返回一个包含 bytes 数组所有有效索引的区间
for (j in bytes.indices) {
// bytes[j].toInt():将 bytes 数组中索引为 j 的字节元素转换为 Int 类型。
// 在 Kotlin 中,Byte 类型是有符号的,范围是 -128 到 127,转换为 Int 类型时,符号位会被扩展。
// and 0xFF:通过按位与操作,将转换后的 Int 值的高 24 位清零,只保留低 8 位,从而将字节转换为无符号整数。
val v = bytes[j].toInt() and 0xFF
// v ushr 4:对 v 进行无符号右移 4 位操作,将高 4 位移到低 4 位的位置。
// 例如,若 v 为 0xAB(二进制 10101011),右移 4 位后变为 0x0A(二进制 00001010)。
// hexChars[j * 2]:将取出的十六进制字符存储在 hexChars 数组中,
// 由于每个字节对应两个十六进制字符,所以存储位置为 j * 2。
hexChars[j * 2] = hexArray[v ushr 4]
// v and 0x0F:通过按位与操作,将 v 的高 4 位清零,只保留低 4 位。
// hexArray[v and 0x0F]:根据按位与的结果从 hexArray 中取出对应的十六进制字符.
// hexChars[j * 2 + 1]:将取出的十六进制字符存储在 hexChars 数组中,
// 存储位置为 j * 2 + 1,紧挨着高 4 位对应的字符。
hexChars[j * 2 + 1] = hexArray[v and 0x0F]
}
// 最后将 hexChars 数组转换为字符串并返回。
return String(hexChars)
}
// 存储十六进制字符,用于 bytesToHex 函数中字符转换。
// hexArray 是一个包含十六进制字符 0 - F 的字符数组,
// hexArray[v ushr 4] 会根据右移后的结果从 hexArray 中取出对应的十六进制字符。
// 例如,v ushr 4 为 0x0A 时,会取出 hexArray[10],即字符 A。
private val hexArray = "0123456789ABCDEF".toCharArray()
上面的代码经过如下调用:
val testBytes = byteArrayOf(0x12, 0x34, 0x56, 0x78)
val frame1 = "55AA0009000100000001011B0B"
val frame2 = hexToBytes(frame1)
// contentToString() 是 Array 类的一个扩展函数,用于将数组元素以字符串形式输出,方便查看数组内容。
println("hexToBytes: ${frame2.contentToString()}")
val frame2HexValue = frame2.joinToString("") { byte ->
"%02X".format(byte.toInt() and 0xFF)
}
println("hexToBytes1: $frame2HexValue")
// 字节数组转十六进制字符串
val hexString = bytesToHex(testBytes)
println("Bytes to Hex: $hexString")
上面的代码输出如下:
hexToBytes: [85, -86, 0, 9, 0, 1, 0, 0, 0, 1, 1, 27, 11]
hexToBytes1: 55AA0009000100000001011B0B
Bytes to Hex: 12345678
5, 字节数组和十进制之间的转换
// 将字节数组转换为整数。
// 在比如网络通信中等场景下接收到的数据是以字节数组形式存在的,而某些数据需要以整数形式处理,就可以使用该函数进行转换。
fun bytesToInt(bytes: ByteArray): Int {
// var result = 0,用于存储最终转换得到的整数。
var result = 0
// for (i in bytes.indices) 会遍历字节数组中的每一个字节。
for (i in bytes.indices) {
// result shl 8:将 result 左移 8 位,相当于将之前处理的字节数据整体向左移动一个字节的位置,为新的字节腾出空间。
// bytes[i].toInt() and 0xFF:将当前字节转换为 Int 类型,并通过 and 0xFF 操作确保只保留低 8 位,将其转换为无符号的字节值。
// 因为在 Kotlin 中 Byte 是有符号的,范围是 -128 到 127,转换为 Int 时可能会出现符号扩展问题,通过 and 0xFF 可以避免。
// (result shl 8) or (bytes[i].toInt() and 0xFF):使用按位或操作将左移后的 result 和当前字节的无符号值合并,更新 result 的值。
result = (result shl 8) or (bytes[i].toInt() and 0xFF)
}
// 遍历完所有字节后,返回最终的整数结果。
return result
}
//上面的代码经过如下调用:
val testBytes = byteArrayOf(0x12, 0x34, 0x56, 0x78)
// 字节数组转十进制整数
val intValue = bytesToInt(testBytes)
println("Bytes to Int: $intValue")
上面的代码输出如下:
Bytes to Int: 305419896
下面分析下这个数据的正确性。
已知测试的字节数组为 testBytes = byteArrayOf(0x12, 0x34, 0x56, 0x78),在 bytesToInt 函数中,是按照大端字节序(Big - Endian)来组合这些字节的,也就是字节数组中的第一个字节是整数的最高有效字节,最后一个字节是整数的最低有效字节。
计算过程如下:
第一个字节 0x12 是最高有效字节,在组合成整数时,它会被左移 24 位。
第二个字节 0x34 会被左移 16 位。
第三个字节 0x56 会被左移 8 位。
第四个字节 0x78 作为最低有效字节,不进行左移。
代码并没有将字节数组中的最低位放在整数的最高位,而是遵循了大端字节序。
大端字节序是指数据的高位字节存于低地址,低位字节存于高地址。
在 bytesToInt 函数中,循环从字节数组的第一个字节开始处理,每次将之前处理的结果左移 8 位,然后将当前字节合并进去,这样就保证了字节数组的第一个字节成为整数的最高有效字节,最后一个字节成为整数的最低有效字节。
如果想要使用小端字节序(Little - Endian,即数据的低位字节存于低地址,高位字节存于高地址),可以修改 bytesToInt 函数,从字节数组的最后一个字节开始处理。
fun bytesToIntLittleEndian(bytes: ByteArray): Int {
var result = 0
for (i in bytes.size - 1 downTo 0) {
result = (result shl 8) or (bytes[i].toInt() and 0xFF)
}
return result
}
fun main() {
val testBytes = byteArrayOf(0x12, 0x34, 0x56, 0x78)
val intValueLittleEndian = bytesToIntLittleEndian(testBytes)
println("小端字节序转换结果: $intValueLittleEndian")
}
上面的代码输出如下:
小端字节序转换结果: 2018915346
采用小端和大端存储数据得到不同结果,主要是因为它们对多字节数据(如整数、浮点数等)中各个字节的排列顺序处理方式不同。
大端字节序(Big - Endian)
定义:大端字节序也称为网络字节序,在这种存储方式下,数据的高位字节存于低地址,低位字节存于高地址。也就是说,数据的最高有效字节(Most Significant Byte, MSB)被存放在内存的起始位置,而最低有效字节(Least Significant Byte, LSB)被存放在内存的最后位置。
示例:以十六进制数 0x12345678 为例,在大端字节序的存储方式下,内存中的字节排列如下:
| 内存地址 | 存储内容 |
|---|---|
| 低地址 | 0x12 |
| 0x34 | |
| 0x56 | |
| 高地址 | 0x78 |
小端字节序(Little - Endian)
定义:小端字节序与大端字节序相反,数据的低位字节存于低地址,高位字节存于高地址。即最低有效字节(LSB)被存放在内存的起始位置,而最高有效字节(MSB)被存放在内存的最后位置。
示例:同样以十六进制数 0x12345678 为例,在小端字节序的存储方式下,内存中的字节排列如下:
| 内存地址 | 存储内容 |
|---|---|
| 低地址 | 0x78 |
| 0x56 | |
| 0x34 | |
| 高地址 | 0x12 |
结果不同的原因:
由于大端和小端字节序对字节排列顺序的处理方式不同,在进行数据读取和解释时就会得到不同的结果。
数据读取:当程序从内存中读取多字节数据时,会按照字节序的规则来组合这些字节。例如,在 bytesToInt 函数中,如果按照大端字节序读取字节数组 [0x12, 0x34, 0x56, 0x78],会将 0x12 作为最高有效字节,组合后的整数为 0x12345678,转换为十进制就是 305419896。而如果按照小端字节序读取同样的字节数组,会将 0x78 作为最高有效字节,组合后的整数为 0x78563412,转换为十进制是一个不同的值。
解释差异:不同的字节序会导致对同一组字节数据的解释不同。在网络通信中,如果发送方和接收方使用的字节序不一致,就会出现数据解析错误。例如,发送方使用大端字节序发送数据,而接收方按照小端字节序来解析,就会得到错误的结果。
上面的字节转换为十进制整数,下面分析下十进制整数转换为字节。
// 将整数转换为字节数组,
// 将一个 32 位的整数(Int 类型)转换为一个长度为 4 的字节数组(ByteArray 类型)
fun intToBytes(value: Int): ByteArray {
// 使用 byteArrayOf 函数将得到的 4 个字节组合成一个字节数组并返回。
// 这个字节数组的元素顺序是按照大端字节序(Big - Endian)排列的,即最高有效字节在前,最低有效字节在后。
return byteArrayOf(
// value shr 24:shr 是 Kotlin 中的右移运算符,这里将整数 value 右移 24 位。因为一个 Int 类型是 32 位,右移 24 位后,原来的最高 8 位就移动到了最低 8 位的位置。
// 例如,若 value 为 0x12345678,右移 24 位后得到 0x00000012。
// .toByte():将右移后的结果转换为 Byte 类型。
// 由于 Byte 类型是 8 位,所以只会保留低 8 位,即把 0x00000012 转换为 0x12 这个字节。这个字节就是原整数的最高有效字节(MSB)。
(value shr 24).toByte(),
(value shr 16).toByte(),
(value shr 8).toByte(),
value.toByte()
)
}
fun main() {
val testBytes = byteArrayOf(0x12, 0x34, 0x56, 0x78)
// 字节数组转十进制整数
val intValue = bytesToInt(testBytes)
println("Bytes to Int: $intValue")
// 十进制整数转字节数组
val bytesFromInt = intToBytes(intValue)
println("Int to Bytes: ${bytesFromInt.contentToString()}")
}
上面的代码输出如下:
Bytes to Int: 305419896
Int to Bytes: [18, 52, 86, 120]
6, 如何判断大小端
在不同的编程语言中,检测系统字节序(大端字节序或小端字节序)的方法有所不同,在 Java 和 Kotlin 里,可以借助 java.nio.ByteOrder 类和 java.nio.ByteBuffer 来检测系统字节序。
fun isLittleEndian(): Boolean {
// 借助 java.nio.ByteBuffer.allocate(4) 构建一个容量为 4 字节的字节缓冲区。
val buffer = java.nio.ByteBuffer.allocate(4)
// 运用 buffer.putInt(1) 把整数 1 存入缓冲区。
buffer.putInt(1)
// 利用 buffer.order() 获取当前缓冲区的字节序,
// 并且和 java.nio.ByteOrder.LITTLE_ENDIAN 进行比较。
return buffer.order() == java.nio.ByteOrder.LITTLE_ENDIAN
}
fun main() {
// 若结果为 true,则表示系统使用小端字节序;反之,则使用大端字节序。
if (isLittleEndian()) {
print("系统使用小端字节序\n");
} else {
print("系统使用大端字节序\n");
}
}