深入理解Base64的用法和原理

Base64 是一种编码方法,用于将二进制数据转换成64个可打印的ASCII字符的序列。这种编码方式广泛应用于在文本格式中存储二进制数据,例如在 URL、文件名、或在 XML 和 JSON 文档中。Java 中的 Base64 编码和解码通常可以通过 java.util.Base64 类实现,这个类在 Java 8 及以后的版本中提供。

以下是V 哥整理的 Java 中 java.util.Base64 类的核心组件,V 哥将重点介绍这些组件的原理和用法:

  1. 编码器(Encoder)

    • getEncoder():返回一个 Base64.Encoder 实例,用于将数据编码为 Base64 格式。
  2. 解码器(Decoder)

    • getDecoder():返回一个 Base64.Decoder 实例,用于将 Base64 编码的数据解码回原始格式。
  3. 编码表(Encoding Table)

    • Base64 编码器使用一个静态的编码表来将字节映射到 Base64 字符。
  4. 解码表(Decoding Table)

    • Base64 解码器使用一个静态的解码表来将 Base64 字符映射回字节。
  5. 编码模式(Encoding Mode)

    • BASE64:标准的 Base64 编码模式。
    • URL_SAFE:URL 和文件名安全的 Base64 编码模式,使用 -_ 代替标准模式中的 +/
  6. 行长度和填充

    • Base64 编码输出通常每76个字符会有一个换行符,但这可以通过 Encoder 的配置来改变。

示例代码:

import java.util.Base64;
import java.nio.charset.StandardCharsets;

public class Base64Example {
    public static void main(String[] args) {
        String original = "Hello, VG!";
        byte[] encoded = Base64.getEncoder().encode(original.getBytes(StandardCharsets.UTF_8));
        System.out.println("Encoded: " + new String(encoded, StandardCharsets.UTF_8));

        byte[] decoded = Base64.getDecoder().decode(encoded);
        System.out.println("Decoded: " + new String(decoded, StandardCharsets.UTF_8));
    }
}

在上述代码中,我们使用 Base64.getEncoder().encode() 方法将字符串 “Hello, VG!” 编码为 Base64 格式,然后使用 Base64.getDecoder().decode() 方法将其解码回原始字符串。

Base64 编码和解码的实现通常依赖于这些核心组件,它们提供了灵活的方式来处理不同场景下的编码需求。

小试牛刀后,我们来一起详细看看它们的实现原理。

1. 编码器(Encoder)

在 Java 8 引入的 java.util.Base64 包中,Encoder 类是 Base64 类的一个内部类,用于实现 Base64 编码功能。以下是 Encoder 类实现的详细步骤和原理分析:

1. 初始化编码器

首先,通过 Base64.getEncoder() 获取 Encoder 对象的实例。这个实例包含了编码过程中需要的所有配置,例如是否添加填充字符等。

Base64.Encoder encoder = Base64.getEncoder();

2. 准备编码数据

将需要编码的数据放入字节数组中。这些数据将作为输入传递给编码器。

byte[] dataToEncode = "beijing Hot".getBytes(StandardCharsets.UTF_8);

3. 编码数据

使用 Encoder 实例的 encode 方法对数据进行编码。这个方法会返回一个包含 Base64 编码结果的字节数组。

byte[] encodedData = encoder.encode(dataToEncode);

4. 处理编码结果

编码后的字节数组可以转换为字符串,或者直接写入到输出流中。

String encodedString = new String(encodedData, StandardCharsets.UTF_8);

编码原理

  1. Base64 字符集
    Base64 编码使用一个包含 64 个字符的字符集,包括大写字母 A-Z、a-z、数字 0-9、加号(+)和斜杠(/)。

  2. 3 字节到 4 字符的映射
    每次从输入数据中读取 3 个字节(24 位),然后将这 24 位分割成 4 个 6 位的组。每个 6 位组映射到一个 Base64 字符。

  3. 填充
    如果输入数据的字节数不是 3 的倍数,在编码的最后会添加一个或两个 = 字符作为填充。

  4. 换行符
    在编码过程中,可以根据配置在每 76 个字符后添加换行符,以确保编码后的文本符合 MIME 的要求。

  5. 无填充模式
    使用 encoder.withoutPadding() 可以禁用自动填充,这样编码后的输出就不会包含 = 字符。

源码分析

Base64.Encoder 类中,编码过程主要涉及以下几个关键部分:

  • 编码表ENCODE 数组定义了如何将 6 位二进制数映射到 Base64 字符集。
  • 缓冲区encoder 内部维护一个缓冲区,用于存储待编码的字节。
  • 编码方法encode 方法实现具体的编码逻辑,包括从缓冲区读取字节、映射到 Base64 字符、处理剩余字节和填充。

编码步骤

  1. 填充缓冲区:将输入数据写入到 encoder 的内部缓冲区。
  2. 分组:将缓冲区中的字节按每 3 个字节分为一组。
  3. 映射字符:使用 ENCODE 表将每组的 24 位映射到 4 个 Base64 字符。
  4. 处理剩余:如果最后一组不足 3 个字节,使用 = 作为填充,并相应调整映射的字符。
  5. 输出结果:将映射后的字符输出或转换为字符串。

来一个示例代码尝试一下使用

public byte[] encode(byte[] input) {
    // 初始化输出数组
    byte[] output = new byte[...];
    int outputPos = 0;

    for (int i = 0; i < input.length; i += 3) {
        // 读取 3 个字节
        int threeBytes = ((input[i] & 0xFF) << 16) |
                         ((i + 1 < input.length) ? (input[i + 1] & 0xFF) << 8 : 0) |
                         ((i + 2 < input.length) ? (input[i + 2] & 0xFF) : 0);

        // 映射到 4 个 Base64 字符
        for (int j = 0; j < 4; j++) {
            int index = (threeBytes & (0xFF << (8 * (3 - j)))) >> (8 * (3 - j));
            output[outputPos++] = ENCODE[index];
        }
    }

    // 处理填充
    if (neededPadding) {
        output[outputPos++] = '=';
        // 可能还需要第二个 '='
    }

    return Arrays.copyOf(output, outputPos);
}

以上代码演示了 Base64 编码的基本逻辑,实际的 Encoder 类实现可能会包含更多的细节,例如处理换行符、无填充模式等。

2. 解码器(Decoder)

在 Java 8 及以后的版本中,java.util.Base64 包中的 Decoder 类是 Base64 类的一个内部类,用于实现 Base64 解码功能。以下是 Decoder 类实现的详细步骤和原理分析:

1. 初始化解码器

首先,通过 Base64.getDecoder() 获取 Decoder 对象的实例。这个实例包含了解码过程中需要的所有配置。

Base64.Decoder decoder = Base64.getDecoder();

2. 准备解码数据

将需要解码的 Base64 字符串转换为字节数组。这些数据将作为输入传递给解码器。

String base64String = "SGVsbG8sIFdvcmxkIQ==";
byte[] dataToDecode = base64String.getBytes(StandardCharsets.UTF_8);

3. 解码数据

使用 Decoder 实例的 decode 方法对 Base64 字符串进行解码。这个方法会返回一个包含原始数据的字节数组。

byte[] decodedData = decoder.decode(dataToDecode);

4. 处理解码结果

解码后的字节数组可以转换为字符串,或者直接用于其他需要原始数据的场合。

String decodedString = new String(decodedData, StandardCharsets.UTF_8);

解码原理

  1. Base64 字符集
    Base64 解码使用与编码相同的 64 个字符集,包括大写字母 A-Z、a-z、数字 0-9、加号(+)和斜杠(/)。

  2. 4 字符到 3 字节的映射
    每次从 Base64 编码的数据中读取 4 个字符(24 位),然后将这 24 位分割成 3 个 8 位的组。每个 8 位的组映射回原始的字节。

  3. 处理填充
    如果编码时使用了填充字符(=),解码时需要识别并忽略这些字符。

  4. 异常处理
    如果输入数据包含非法字符或格式不正确,解码过程将抛出 IllegalArgumentException

源码分析

Base64.Decoder 类中,解码过程主要涉及以下几个关键部分:

  • 解码表DECODE 数组定义了 Base64 字符到 6 位二进制数的映射。
  • 缓冲区decoder 内部维护一个缓冲区,用于存储待解码的 Base64 字符。
  • 解码方法decode 方法实现具体的解码逻辑,包括从输入读取字符、映射回字节、处理填充和非法字符。

解码步骤

  1. 填充缓冲区:将 Base64 编码的字符串转换为字节数组,并填充到 decoder 的内部缓冲区。
  2. 分组:将缓冲区中的 Base64 字符按每 4 个字符分为一组。
  3. 映射字节:使用 DECODE 表将每组的 24 位映射回 3 个字节。
  4. 处理填充:如果编码时使用了填充,解码时识别 = 字符并相应调整映射的字节。
  5. 输出结果:将映射后的字节输出或转换为原始数据。

还是上示例看用法

public byte[] decode(byte[] input) {
    // 初始化输出数组
    byte[] output = new byte[...];
    int outputPos = 0;

    for (int i = 0; i < input.length; i += 4) {
        // 读取 4 个 Base64 字符
        int fourChars = (DECODE[input[i] & 0xFF] << 18) |
                        (DECODE[input[i + 1] & 0xFF] << 12) |
                        (DECODE[input[i + 2] & 0xFF] << 6) |
                        (DECODE[input[i + 3] & 0xFF]);

        // 映射回 3 个字节
        output[outputPos++] = (fourChars >> 16) & 0xFF;
        if (input[i + 2] != '=') {
            output[outputPos++] = (fourChars >> 8) & 0xFF;
        }
        if (input[i + 3] != '=') {
            output[outputPos++] = fourChars & 0xFF;
        }
    }

    return Arrays.copyOf(output, outputPos);
}

以上的代码演示了 Base64 解码的基本逻辑,实际的 Decoder 类实现可能会包含更多的细节,例如处理非法字符、解码表的初始化等。

通过这种方式,Base64.Decoder 提供了一种灵活且高效的方式来将 Base64 编码的字符串解码回原始的字节数据,适用于多种不同的解码需求。

3. 编码表(Encoding Table)

Base64 编码表是 Base64 编码和解码过程中的核心组件之一。它是一个查找表,用于将 6 位二进制值映射到相应的 Base64 编码字符。以下是 Base64 编码表的源码实现过程步骤和原理分析:

1. 定义 Base64 字符集

Base64 编码使用 64 个可打印的 ASCII 字符来表示数据。这些字符包括大写字母 A-Z(26 个)、小写字母 a-z(26 个,但在标准 Base64 中通常使用大写)、数字 0-9(10 个)、加号 + 和斜杠 /。此外,为了支持 URL 和文件名,还有一个变种使用 - 代替 +_ 代替 /

2. 初始化编码表

在 Java 的 java.util.Base64 类中,编码表通常是通过一个静态初始化的数组来实现的。这个数组的长度为 64,正好对应于 Base64 字符集中的字符数量。

private static final char[] ENCODE = {
    'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
    // ... 省略中间字符
    'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    '+', '/'
};

3. 使用编码表进行编码

在编码过程中,原始数据被读取并转换为字节,然后每三个字节(24 位)被转换为四个 6 位的组。每个 6 位组的值通过查找编码表来找到对应的 Base64 字符。

4. 处理填充

如果原始数据的长度不是 3 的倍数,最后一组可能只有 1 或 2 个字节。在这种情况下,剩余的位使用 = 字符填充。填充字符不通过编码表映射,而是直接添加到输出中。

5. 生成编码字符串

将映射得到的 Base64 字符连接起来,形成最终的编码字符串。

原理解释

  • 6 位映射:由于 Base64 编码将 3 个字节(24 位)转换为 4 个字符(每个字符 6 位),所以每个字符可以表示 6 位二进制数据。ENCODE 数组中的每个索引值对应一个 6 位的二进制数。
  • 字符选择:在编码过程中,通过将字节数据的位操作结果作为索引,从 ENCODE 数组中选择相应的字符。
  • 性能优化:使用查找表可以快速地进行字符映射,避免了复杂的条件判断或循环,从而提高了编码的效率。

示例代码

以下是如何使用编码表进行 Base64 编码的示例代码:

public String encode(byte[] data) {
    StringBuilder encoded = new StringBuilder();
    int i = 0;

    while (i < data.length - 2) { // 处理完整组
        int threeBytes = ((data[i] & 0xFF) << 16) |
                         ((data[i + 1] & 0xFF) << 8) |
                         (data[i + 2] & 0xFF);
        // 将 24 位分为 4 个 6 位组
        for (int j = 0; j < 4; j++) {
            int index = (threeBytes >> (18 - j * 6)) & 0x3F;
            encoded.append(ENCODE[index]);
        }
        i += 3;
    }

    // 处理剩余字节和填充
    if (i < data.length) {
        int remaining = data.length - i;
        int twoBytes = (data[i] & 0xFF) << (8 * (2 - remaining));
        for (int j = 0; j < remaining; j++) {
            int index = (twoBytes >> (16 - j * 8)) & 0xFF;
            encoded.append(ENCODE[index]);
        }
        // 添加填充字符
        while (encoded.length() % 4 != 0) {
            encoded.append('=');
        }
    }

    return encoded.toString();
}

这个伪代码演示了如何使用 Base64 编码表将字节数据编码为 Base64 字符串。实际的 Base64.Encoder 类实现可能会包含更多的细节,例如处理换行符等。

4. 解码表(Decoding Table)

Base64 解码表是 Base64 编码和解码过程中的另一个核心组件。它用于将 Base64 编码的字符映射回相应的 6 位二进制值。以下是 Base64 解码表的源码实现过程步骤和原理分析:

1. 定义 Base64 字符集

与编码表一样,解码表依赖于 Base64 字符集,包括大写字母 A-Z、小写字母 a-z(在标准 Base64 中通常不使用)、数字 0-9、加号 + 和斜杠 /。对于 URL 安全的 Base64,使用 - 代替 +_ 代替 /

2. 初始化解码表

在 Java 的 java.util.Base64 类中,解码表通常是通过一个静态初始化的数组来实现的。这个数组的长度为 128,覆盖了所有可能的 ASCII 字符,因为标准 ASCII 字符集大小为 128 个字符。

private static final int[] DECODE = new int[128];

在静态初始化块中,解码表被填充。每个 Base64 字符都被赋予一个值,从 0 到 63,而非法字符则通常被赋予 -1 或其他表示无效的值。

static {
    for (int i = 0; i < DECODE.length; i++) {
        DECODE[i] = -1; // 初始值设为无效
    }
    // 为 Base64 字符赋值
    for (int i = 'A'; i <= 'Z'; i++) {
        DECODE[i] = i - 'A';
    }
    for (int i = 'a'; i <= 'z'; i++) {
        DECODE[i] = 26 + i - 'a';
    }
    for (int i = '0'; i <= '9'; i++) {
        DECODE[i] = 52 + i - '0';
    }
    DECODE['+'] = 62;
    DECODE['/'] = 63;
    // 对于 URL 安全的 Base64,可以添加以下赋值
    DECODE['-'] = 62;
    DECODE['_'] = 63;
}

3. 使用解码表进行解码

在解码过程中,Base64 编码的字符串被逐个字符读取,每个字符通过解码表转换为其对应的 6 位二进制值。

4. 处理填充

Base64 编码可能以 = 字符结尾,表示原始数据在编码时不足 3 个字节。在解码时,这些填充字符被忽略,不参与解码过程。

5. 生成原始数据

将解码得到的 6 位二进制值重新组合,转换回原始的字节序列。

原理解释

  • 字符到值的映射:解码表提供了从 Base64 字符到其对应的 6 位二进制值的快速映射。这种映射是通过查找表实现的,其中每个可能的字符(128 个 ASCII 字符)都有一个与之对应的整数值。
  • 忽略非法字符:解码表中的非法字符被赋予一个特殊值(如 -1),在解码过程中,这些值被忽略或导致解码失败。
  • 性能优化:使用查找表可以快速地进行字符到值的转换,避免了复杂的条件判断或循环,从而提高了解码的效率。

上示例代码

以下是如何使用解码表进行 Base64 解码的示例代码:

public byte[] decode(String encoded) {
    char[] chars = encoded.toCharArray();
    int[] decodeTable = getDecodeTable(); // 获取初始化的解码表
    ByteArrayOutputStream output = new ByteArrayOutputStream();
    
    for (int i = 0; i < chars.length;) {
        if (chars[i] == '=') { // 处理填充
            break;
        }
        int value = decodeTable[chars[i] & 0xFF];
        if (value == -1) { // 非法字符
            throw new IllegalArgumentException("Illegal character encountered");
        }
        // 将 4 个 Base64 字符转换为 3 个字节
        int threeBytes = (value << 18) |
                         (decodeTable[chars[++i] & 0xFF] << 12) |
                         (decodeTable[chars[++i] & 0xFF] << 6) |
                         decodeTable[chars[++i] & 0xFF];
        output.write((threeBytes >> 16) & 0xFF);
        if (chars[++i] != '=') {
            output.write((threeBytes >> 8) & 0xFF);
        }
        if (chars[++i] != '=') {
            output.write(threeBytes & 0xFF);
        }
        i++; // 跳过最后一个字符,如果它是填充字符
    }
    
    return output.toByteArray();
}

以上代码演示如何使用 Base64 解码表将 Base64 编码的字符串解码为原始字节数组。实际的 Base64.Decoder 类实现可能会包含更多的细节,例如处理不同编码模式等。

5. 编码模式(Encoding Mode)

在 Java 的 java.util.Base64 包中,编码模式(Encoding Mode)决定了 Base64 编码的行为,包括字符集的使用和是否添加换行符。以下是对编码模式的源码实现过程步骤和原理的详细分析:

1. 定义编码模式

Java 的 Base64 类提供了两种编码模式:

  • BASE64:标准的 Base64 编码模式,使用 A-Za-z0-9+/ 字符,并且可以在每 76 个字符后添加换行符。
  • URL_SAFE:URL 安全的 Base64 编码模式,使用 A-Za-z0-9-_ 字符,适用于 URL 和文件名,也不添加换行符。

2. 初始化编码器

编码器可以通过 Base64.getEncoder() 获取,并根据需要选择编码模式。例如,使用 URL 安全模式可以通过 encoder = Base64.getUrlEncoder() 实现。

3. 配置编码器

编码器可以进一步配置以满足特定的编码需求,例如禁用换行符或填充字符。这些配置可以通过编码器的方法链调用来完成。

Base64.Encoder encoder = Base64.getEncoder();
encoder = encoder.withoutPadding(); // 禁用填充

4. 编码数据

使用配置好的编码器对数据进行编码。编码过程会根据编码模式使用不同的字符集,并根据配置决定是否添加换行符。

byte[] encodedData = encoder.encode(originalData);

5. 输出编码结果

编码后的数据可以作为字节数组或转换为字符串进行输出。

String encodedString = new String(encodedData, StandardCharsets.UTF_8);

原理解释

  • 字符集选择:不同的编码模式使用不同的字符集。标准模式使用 +/,而 URL 安全模式使用 -_ 代替,以避免在 URL 中引起歧义。
  • 换行符处理:在标准 Base64 编码中,为了提高可读性,每 76 个字符后可以添加一个换行符。在 URL 安全模式或当禁用换行符时,不添加换行符。
  • 填充处理:当输入数据不是 3 个字节的倍数时,Base64 编码会在结果的末尾添加一个或两个 = 字符作为填充。通过配置编码器,可以禁用这种自动填充。

上示例代码

以下是使用不同编码模式进行 Base64 编码的示例:

import java.util.Base64;

public class Base64EncodingExample {
    public static void main(String[] args) {
        byte[] data = "Some data to encode".getBytes(StandardCharsets.UTF_8);

        // 使用标准 Base64 编码模式
        Base64.Encoder standardEncoder = Base64.getEncoder();
        String standardEncoded = new String(standardEncoder.encode(data));

        // 使用 URL 安全的 Base64 编码模式
        Base64.Encoder urlSafeEncoder = Base64.getUrlEncoder().withoutPadding();
        String urlSafeEncoded = new String(urlSafeEncoder.encode(data));

        System.out.println("Standard Encoded: " + standardEncoded);
        System.out.println("URL Safe Encoded: " + urlSafeEncoded);
    }
}

在这个示例中,我们使用标准 Base64 编码模式和 URL 安全 Base64 编码模式对相同的数据进行编码,并输出编码结果。

注意事项

  • 编码模式的选择应根据数据的使用场景来决定。例如,当数据将被用于 URL 传输时,应使用 URL 安全模式。
  • 配置编码器时,应考虑到接收端的解码能力,确保编码和解码使用相同的模式和配置。
  • 禁用填充可能会影响某些协议或应用程序的兼容性,因为它们可能期望 Base64 编码的数据以 = 字符结束。

通过这种方式,Base64 类的编码模式提供了灵活性,以适应不同的编码需求和使用场景。

6. 行长度和填充

在 Java 的 java.util.Base64 包中,行长度(line length)和填充(padding)是 Base64 编码过程中的两个可选配置,它们影响编码输出的格式。以下是行长度和填充的源码实现过程步骤和原理分析:

1. 定义行长度和填充的默认行为

在 Base64 编码中,可以设置每行的字符数(行长度),以及是否在编码数据的末尾添加填充字符 =

  • 行长度:默认情况下,每行包含 76 个字符,这是为了确保编码后的文本符合 MIME 的要求。
  • 填充:如果编码的数据不是 3 个字节的倍数,编码后的输出会在末尾添加一个或两个 = 字符作为填充。

2. 配置编码器

编码器可以通过调用 Base64.getEncoder() 获取,并使用 withoutPadding() 方法来配置不使用填充。

Base64.Encoder encoder = Base64.getEncoder().withoutPadding();

3. 实现自定义行分隔

虽然 Java 的 Base64.Encoder 没有直接提供设置行长度的方法,但你可以通过自定义编码逻辑来实现。例如,你可以在编码后的结果上手动插入换行符。

4. 编码数据

使用配置好的编码器对数据进行编码。编码过程会根据是否配置了无填充来决定是否在输出末尾添加 = 字符。

byte[] encodedData = encoder.encode(originalData);

5. 手动处理行分隔

如果你需要自定义行长度,可以在编码后的结果上手动添加换行符。这可以通过遍历编码后的字节数组并每隔一定数量的字符插入一个换行符来实现。

原理解释

  • 行长度:设置行长度的目的是在编码后的文本中添加可读性,使其更适合在文本环境中展示和编辑。每 76 个 Base64 字符后添加一个换行符是 Base64 编码的常见约定。
  • 填充:Base64 编码要求每三个字节的原始数据转换为四个 Base64 字符。如果原始数据的字节数不是 3 的倍数,编码器会在结果末尾添加一个或两个 = 字符来填充。这表明编码的数据不是原始数据的完整表示。
  • 自定义行分隔:在某些情况下,你可能需要不同于默认行长度的编码格式。通过在编码后的字符串中手动插入换行符,可以实现自定义行分隔。

示例代码

以下是如何使用 Base64.Encoder 进行编码,并手动添加自定义行分隔的示例:

import java.util.Base64;
import java.io.ByteArrayOutputStream;

public class Base64CustomLineLength {
    public static void main(String[] args) {
        String original = "Some data to encode";
        byte[] data = original.getBytes(StandardCharsets.UTF_8);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        Base64.Encoder encoder = Base64.getEncoder().withoutPadding();

        try {
            encoder.encode(data, baos);
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        }

        String encoded = baos.toString();
        // 手动添加自定义行分隔,例如每 50 个字符
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < encoded.length(); i += 50) {
            sb.append(encoded, i, Math.min(i + 50, encoded.length()));
            if (i + 50 < encoded.length()) {
                sb.append("\n"); // 添加换行符
            }
        }
        encoded = sb.toString();

        System.out.println("Custom line length encoded: " + encoded);
    }
}

在这个示例中,我们使用 Base64.Encoder 对数据进行编码,并在编码后的结果上手动添加了每 50 个字符的换行符,实现了自定义行长度的效果。

注意事项

  • 自定义行分隔可能会影响编码数据的解码,确保解码时也考虑到了行分隔的处理。
  • 禁用填充可能会影响某些协议或应用程序的兼容性,因为它们可能期望 Base64 编码的数据以 = 字符结束。
  • 在手动添加行分隔时,确保换行符的使用符合目标格式的要求。

最后

以上就是 Base64的核心类库的全部介绍,了解 Base64的用法和原理,向高手靠近一点点。关注威哥爱编程。

  • 33
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

威哥爱编程(马剑威)

你的鼓励是我创作的最大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值