iOS Java关于Base64编码

最近联调后端和客户端中对二进制图片数据进行Base64编码传输base64字符串,发现编码传输过程中,两边的编码解码方式必须一致,否则会导致图片数据解码后大小改变。

参考《iOS开发探索-Base64编码》得到Base64编码是使用64个字符对任义数据进行编码。编码表如下:

1485695-5311d9d624394d61.jpg

Base64编码本质上是一种将二进制数据转成文本数据的方案。对于非二进制数据,是先将其转换成二进制形式,然后每连续6比特(2的6次方=64)计算其十进制值,根据该值在上面的索引表中找到对应的字符,最终得到一个文本字符串。

字符串 a b c
ASCII编码 97 98 99
二进制表示 01100000 01100001 01100010
按每六位划分 011000 0001110 000101 100010
十进制数 24 22 9 35
base64编码 Y W J j

iOS代码表示

    //原始字符串
    NSString *str = @"abc";
    //转成二进制数据
    NSData *strData = [str dataUsingEncoding:NSUTF8StringEncoding];
    //二进制数据长度 3
    NSLog(@"%ld",strData.length);
    //二进制数据转成字符串
    NSString *string = [[NSString alloc]initWithData:strData encoding:NSUTF8StringEncoding];
    //字符串长度 3
    NSLog(@"%@,%ld",string,string.length);
    //二进制数据转化成base64字符串
    NSString *base64Str = [strData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
    //得到字符串,字符串长度4
    NSLog(@"%@,%ld",base64Str,base64Str.length);
    //base64字符串解码成二进制数据
    NSData *decodedBase64StrData = [[NSData alloc] initWithBase64EncodedString:base64Str options:NSDataBase64DecodingIgnoreUnknownCharacters];
    //二进制数据长度 3
    NSLog(@"decodedBase64StrData : %@,decodedBase64StrData : %ld",decodedBase64StrData,decodedBase64StrData.length);
    //base64解码的二进制数据解码成普通字符串
    NSString *decodeBase64Str = [[NSString alloc]initWithData:decodedBase64StrData encoding:NSUTF8StringEncoding];
    //普通字符串的长度 3
    NSLog(@"%@,%ld",decodeBase64Str,decodeBase64Str.length);
    
//输出
2020-04-21 11:33:09.641930+0800 Base64[41257:1853564] 3
2020-04-21 11:33:09.642039+0800 Base64[41257:1853564] abc,3
2020-04-21 11:33:09.642124+0800 Base64[41257:1853564] YWJj,4
2020-04-21 11:33:09.642215+0800 Base64[41257:1853564] decodedBase64StrData : <616263>,decodedBase64StrData : 3
2020-04-21 11:33:09.642318+0800 Base64[41257:1853564] abc,3

Java代码表示

//原始字符串
        String str = "abc";
        //字符串转二进制数据
        byte[] strBytes = str.getBytes("UTF-8");
        //二进制数据长度 3
        System.out.println("------"+strBytes.length);
        //获取base64字符串加密器
        Base64.Encoder encoder = Base64.getEncoder();
        //二进制数据转base64字符串
        String encodeStr =  encoder.encodeToString(strBytes);
        //base64字符串长度 4
        System.out.println(encodeStr+"----"+encodeStr.length());
        //获取base64字符串加密器
        BASE64Encoder encoder0 = new BASE64Encoder();
        //二进制数据转base64字符串
        String encodeStr0 = strBytes != null ? encoder0.encode(strBytes) : "";
        //二进制数据长度 4
        System.out.println(encodeStr0+"----"+encodeStr0.length());

        //获取base64字符串解码器
        Base64.Decoder decoder = Base64.getDecoder();
        //解码base64字符串
        byte[] base64StrBytes =  decoder.decode(encodeStr);
        //base64字符串解码成二进制数据后的长度 3
        System.out.println(base64StrBytes+"-------------"+base64StrBytes.length);
        //获取base64字符串解码器
        BASE64Decoder decoder0 = new sun.misc.BASE64Decoder();
        //base64字符串转二进制数据
        byte[] base64StrBytes0 = encodeStr != null ? decoder0.decodeBuffer(encodeStr) : null;
        //base64字符串解码后的二进制数据长度 3
        System.out.println(base64StrBytes0+"-------------"+base64StrBytes0.length);
//输出
------3
YWJj----4
YWJj----4
[B@7cef4e59-------------3
[B@64b8f8f4-------------3

但这里需要注意一个点:Base64编码是每3个原始字符编码成4个字符,如果原始字符串长度不能被3整除,那怎么办?使用0值来补充原始字符串。

0.jpg

Hello!! Base64编码的结果为 SGVsbG8hIQAA 。最后2个零值只是为了Base64编码而补充的,在原始字符中并没有对应的字符,那么Base64编码结果中的最后两个字符 AA 实际不带有效信息,所以需要特殊处理,以免解码错误。

标准Base64编码通常用 = 字符来替换最后的 A,即编码结果为 SGVsbG8hIQ==。因为 = 字符并不在Base64编码索引表中,其意义在于结束符号,在Base64解码时遇到 = 时即可知道一个Base64编码字符串结束。

如果Base64编码字符串不会相互拼接再传输,那么最后的 = 也可以省略,解码时如果发现Base64编码字符串长度不能被4整除,则先补充 = 字符,再解码即可。

解码是对编码的逆向操作,但注意一点:对于最后的两个 = 字符,转换成两个 A 字符,再转成对应的两个6比特二进制0值,接着转成原始字符之前,需要将最后的两个6比特二进制0值丢弃,因为它们实际上不携带有效信息。

java自带Base64工具需要把Base64中的换行去掉才能正常使用。

    public void testWithn()throws IOException{
        String base64Str = "PCFET0NUWVBFIGh0bWw+PGh0bWw+CTxoZWFkPgkJPG1ldGEgY2hhcnNldD0iVVRG\n" +
                "LTgiPgkJPG1ldGEgbmFtZT0idmlld3BvcnQiIGNvbnRlbnQ9IndpZHRoPWRldmlj\n" +
                "ZS13aWR0aCxpbml0aWFsLXNjYWxlPTEsbWluaW11bS1zY2FsZT0xLG1heGltdW0t\n" +
                "c2NhbGU9MSx1c2VyLXNjYWxhYmxlPW5vIiAvPgkJPHRpdGxlPjwvdGl0bGU+CQk8\n" +
                "c3R5bGUgdHlwZT0idGV4dC9jc3MiPgkJCWJvZHksCQkJaDEsCQkJaDIsCQkJaDMs\n" +
                "CQkJaDQsCQkJaDUsCQkJaDYs\n";
        String decode = new String(Base64.getDecoder().decode(base64Str.toString().replace("\n","")),"utf-8");
        System.out.println(decode);
    }

否则会报错:
java.lang.IllegalArgumentException: Illegal base64 character a

iOS 二进制数据base64编码方式有四个枚举,若是图片二进制文件转换成base64字符串后,后端需要得到base64字符串后解码为二进制数据,必须要用第四个NSDataBase64EncodingEndLineWithLineFeed,其作用是将生成的Base64字符串以换行结束,则中间不会有多余的换行符,不需要额外处理去掉换行符。否则两端大小会变化,两端数据会不一致。

typedef NS_OPTIONS(NSUInteger, NSDataBase64EncodingOptions) {
    // Use zero or one of the following to control the maximum line length after which a line ending is inserted. No line endings are inserted by default.
    //其作用是将生成的Base64字符串按照64个字符长度进行等分换行。
    NSDataBase64Encoding64CharacterLineLength = 1UL << 0,
    //其作用是将生成的Base64字符串按照76个字符长度进行等分换行。
    NSDataBase64Encoding76CharacterLineLength = 1UL << 1,

    // Use zero or more of the following to specify which kind of line ending is inserted. The default line ending is CR LF.
    //其作用是将生成的Base64字符串以回车结束。
    NSDataBase64EncodingEndLineWithCarriageReturn = 1UL << 4,
    //其作用是将生成的Base64字符串以换行结束。
    NSDataBase64EncodingEndLineWithLineFeed = 1UL << 5,

} API_AVAILABLE(macos(10.9), ios(7.0), watchos(2.0), tvos(9.0));

对一个图片转二进制,分别以四种方式base64字符串编码,写入.txt文件中,得到的文本数据。

//获取图片
        UIImage *image = [UIImage imageNamed:@"landscape.jpg"];
        //压缩为二进制数据
        NSData *imageData = UIImageJPEGRepresentation(image, 1.0f);
        //PNG的图片压缩方式
        NSData *imageData0 = UIImagePNGRepresentation(image);
        //jpeg的压缩方式生成的二进制数据的长度
        NSLog(@"--------%ld----------",imageData.length);
        //png的压缩方式生成的二进制数据的长度
        NSLog(@"--------%ld----------",imageData0.length);
        
        //其作用是将生成的Base64字符串按照64个字符长度进行等分换行
        NSString *base640 = [imageData base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];
        NSLog(@"%ld",base640.length);
        //其作用是将生成的Base64字符串按照76个字符长度进行等分换行。
        NSString *base641 = [imageData base64EncodedStringWithOptions:NSDataBase64Encoding76CharacterLineLength];
        NSLog(@"%ld",base641.length);
        //其作用是将生成的Base64字符串以回车结束。
        NSString *base642 = [imageData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithCarriageReturn];
        NSLog(@"%ld",base642.length);
        //其作用是将生成的Base64字符串以换行结束。
        NSString *base643 = [imageData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
        NSLog(@"%ld",base643.length);
        NSError *error;
        //声明一个指向NSError对象的指针,但是不创建相应的对象
        //实际上只有当错误发生的时候,才会有writeToFile创建相应的NSError对像
        NSArray *strArray = @[base640,base641,base642,base643];
        for (int i=0; i<strArray.count; i++) {
            BOOL success = [strArray[i] writeToFile:[NSString stringWithFormat:@"/Users/cloud/Desktop/base64%d.txt",i] atomically:YES encoding:NSUTF8StringEncoding error:&error];
            //检查返回的布尔值,如果写入文件失败,就查询NSError对象并输出错误描述
            if(success){
                NSLog(@"%@",[NSString stringWithFormat:@"done writing /Users/cloud/Desktop/base64%d.txt",i]);
            }else{
                NSLog(@"%@,%@",[NSString stringWithFormat:@"writing /Users/cloud/Desktop/base64%d.txt failed",i],[error localizedDescription]);
            }
        }
//输出
2020-04-21 17:42:32.710243+0800 Base64[44475:2074596] --------838677----------
2020-04-21 17:42:32.710394+0800 Base64[44475:2074596] --------1472614----------
2020-04-21 17:42:32.716816+0800 Base64[44475:2074596] 1153180
2020-04-21 17:42:32.723992+0800 Base64[44475:2074596] 1147662
2020-04-21 17:42:32.730795+0800 Base64[44475:2074596] 1118236
2020-04-21 17:42:32.737584+0800 Base64[44475:2074596] 1118236
2020-04-21 17:42:32.746791+0800 Base64[44475:2074596] done writing /Users/cloud/Desktop/base640.txt
2020-04-21 17:42:32.753331+0800 Base64[44475:2074596] done writing /Users/cloud/Desktop/base641.txt
2020-04-21 17:42:32.760379+0800 Base64[44475:2074596] done writing /Users/cloud/Desktop/base642.txt
2020-04-21 17:42:32.767148+0800 Base64[44475:2074596] done writing /Users/cloud/Desktop/base643.txt

如下图,NSDataBase64Encoding64CharacterLineLength和NSDataBase64Encoding64CharacterLineLength的base64编码方式有明显的换行符。
通过打印发现NSDataBase64EncodingEndLineWithCarriageReturn和NSDataBase64EncodingEndLineWithLineFeed编码方式得到的Base64字符串长度大小一样。

NSDataBase64Encoding64CharacterLineLength


0.png

NSDataBase64Encoding76CharacterLineLength


1.png

NSDataBase64EncodingEndLineWithCarriageReturn
2.png

NSDataBase64EncodingEndLineWithLineFeed


3.png

将上面四个用四种方式base64编码过的txt文档,在Java工程中读取转二进制文件,看大小的变化。

使用NSDataBase64Encoding64CharacterLineLength或NSDataBase64Encoding76CharacterLineLength的base64字符串,解码需要去除空格,大小才能保持一致,不去除空格会报错。报错信息如下:

java.lang.IllegalArgumentException: Illegal base64 character d at java.util.Base64$Decoder.decode0(Base64.java:714) at java.util.Base64$Decoder.decode(Base64.java:526) at java.util.Base64$Decoder.decode(Base64.java:549)

Java代码

        File base640 = new File("base640.txt");
        FileInputStream fileInputStream = new FileInputStream(base640);
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int length = -1;
        while((length = fileInputStream.read(buffer))!= -1)
        {
            bos.write(buffer,0,length);
        }
        bos.close();
        fileInputStream.close();
        System.out.println(bos.size());
        System.out.println(bos.toByteArray().length);

        String base64Str = bos.toString();
        System.out.println(base64Str.length());

        base64Str = base64Str.toString().replace("\r\n","");
        byte[] bytes = Base64Utils.getStringImage(base64Str);
        System.out.println(bytes.length);
//输出
1153180
1153180
1153180
838677

使用NSDataBase64EncodingEndLineWithCarriageReturn或NSDataBase64EncodingEndLineWithLineFeed的base64字符串则不用取出空格,因为这两中编码方式的base64字符串里面本身没有空格。

    public void testBase64StrToData()throws Exception{
        File base640 = new File("base642.txt");
        FileInputStream fileInputStream = new FileInputStream(base640);
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int length = -1;
        while((length = fileInputStream.read(buffer))!= -1)
        {
            bos.write(buffer,0,length);
        }
        bos.close();
        fileInputStream.close();
        System.out.println(bos.size());
        System.out.println(bos.toByteArray().length);

        String base64Str = bos.toString();
        System.out.println(base64Str.length());

//        base64Str = base64Str.toString().replace("\r\n","");
        byte[] bytes = Base64Utils.getStringImage(base64Str);
        System.out.println(bytes.length);
    }
//输出
1118236
1118236
1118236
838677

读出来的字符串长度(1153180,1147662,1118236,1118236(字节))一致,解码出来的大小和iOS中加密后的大小完全一致,838677个字节。

这里的长度单位是字节(byte),一个字节(byte)代表8位(bit)。参考《字节、字、bit、byte的关系》得到具体。

1字=2字节(1 word = 2 byte)

1字节=8位(1 byte = 8bit)

一个字的字长为16
一个字节的字长是8

bps 是 bits per second 的简称。一般数据机及网络通讯的传输速率都是以「bps」为单位。如56Kbps、100.0Mbps 等等。
Bps即是Byte per second 的简称。而电脑一般都以Bps 显示速度,如1Mbps 大约等同 128 KBps。
bit 电脑记忆体中最小的单位,在二进位电脑系统中,每一bit 可以代表0 或 1 的数位讯号。
Byte一个Byte由8 bits 所组成,可代表一个字元(AZ)、数字(09)、或符号(,.?!%&+-*/),是记忆体储存资料的基本单位,至於每个中文字则须要两Bytes。当记忆体容量过大时,位元组这个单位就不够用,因此就有千位元组的单位KB出现,以下乃个记忆体计算单位之间的相关性:

1 Byte = 8 Bits

1 KB = 1024 Bytes

1 MB = 1024 KB

1 GB = 1024 MB

usb2.0标准接口传输速率。许多人都将“480mbps”误解为480兆/秒。其实,这是错误的,事实上“480mbps”应为“480兆比特/秒”或“480兆位/秒”,它等于“60兆字节/秒”。

这要从bit和byte说起:bit和byte同译为"比特",都是数据量度单位,bit=“比特”或“位”。
byte=字节即1byte=8bits,两者换算是1:8的关系。
mbps=mega bits per second(兆位/秒)是速率单位,所以正确的说法应该是说usb2.0的传输速度是480兆位/秒,即480mbps。
mb=mega bytes(兆比、兆字节)是量单位,1mb/s(兆字节/秒)=8mbps(兆位/秒)。

我们所说的硬盘容量是40gb、80gb、100gb,这里的b指是的byte也就是“字节”。
1 kb = 1024 bytes =2^10 bytes
1 mb = 1024 kb = 2^20 bytes
1 gb = 1024 mb = 2^30 bytes

比如以前所谓的56kb的modem换算过来56kbps除以8也就是7kbyte,所以真正从网上下载文件存在硬盘上的速度也就是每秒7kbyte。
也就是说与传输速度有关的b一般指的是bit。
与容量有关的b一般指的是byte。

最后再说一点: usb2.0 480mbps=60mb/s的传输速率还只是理论值,它还要受到系统环境的制约(cpu、硬盘和内存等),其实际读、取写入硬盘的速度约在11~16mb/s。但这也比usb1.1的12mbps(1.5m/s)快了近10倍。

字节的来由:

最开始计算机只是处理数据运算,也就是0-9,加上运算符号,4bit足够了。举个例子(实际不是这样):用0000表示0,0001表示1,0010表示2,依次类推。
后来加入了字母,程序符号等,8bit也足够了,而这时诞生了ASCII编码的标准,大家就说把8bit表示出来的值叫做字节(byte)吧,于是就有了字节这个单位。
所以1byte等于8bit是计算机发展中的一个约定出来的规则。

汉字:

1 汉字 = 2 byte = 16 bit (这里不是很准确,当编码不同的时候,1个汉字所占的字节数也会有所不同,有些编码是占 2个字节,有些则不是,可能是 3个或者 4个)

Snip20200421_2.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,030评论 5 464
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,198评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 144,995评论 0 327
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,973评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,869评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,766评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,967评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,599评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,886评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,901评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,728评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,504评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,967评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,128评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,445评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,018评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,224评论 2 339

推荐阅读更多精彩内容