iOS中Base64编解码的使用

为什么要进行Base64编码

Base64最早就是用于邮件传输协议中的,原因是邮件传输协议只支持ASCII字符传递,如果要传输二进制文件,如:图片、视频是无法实现的。因此采用Base64将二进制文件内容编码为只包含ASCII字符的内容。

另外在计算机中任何数据都是按ASCII码存储的,而ASCII码的128~255之间的值是不可见字符。而在网络上交换数据时,比如说从A地传到B地,往往要经过多个路由设备,由于不同的设备对字符的处理方式有一些不同,这样那些不可见字符就有可能被处理错误,这是不利于传输的。所以就需要先把数据做一个Base64编码,统统变成可见字符,这样出错的可能性就大降低了。

简而言之就是:Base64主要用于将不可打印的字符转换成可打印字符,或者说将二进制数据编码成ASCII字符。

Base64编解码原理

Base64是一种基于64个可打印字符来表示二进制数据,所以每6个比特为一个单元,对应某个可打印字符。3个字节有24个比特,对应于4个Base64单元,即3个字节可由4个可打印字符来表示。在Base64中的可打印字符包括字母A-Za-z、数字0-9,这样共有62个字符,此外两个可打印符号在不同的系统中而不同。一些如uuencode的其他编码方法,和之后BinHex的版本使用不同的64字符集来代表6个二进制数字,但是不被称为Base64。解码过程就是它的逆过程。

Base64编码之所以称为Base64,是因为其使用64个字符来对任意数据进行编码,同理有Base32Base16编码。标准Base64编码使用的64个字符为:

1485695-5311d9d624394d61.jpg

这64个字符是各种字符编码(比如ASCII编码)所使用字符的子集,并且可打印。

唯一有点特殊的是最后两个字符,因对最后两个字符的选择不同,Base64编码又有很多变种,比如Base64 URL编码。

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

具体例子分析

假设我们要对Hello!进行Base64编码,按照ASCII表,其转换过程如下图所示:

1485695-e572f6b071ef49c0.jpg

可知Hello!Base64编码结果为SGVsbG8h,原始字符串长度为6个字符,编码后长度为8个字符每3个原始字符经Base64编码成4个字符,编码前后长度比4/3

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

Hello!!为例,其转换过程为:

1485695-142384406e84fd12.jpg

注:图表中蓝色背景的二进制0值是额外补充的。

hello!!字符串经过Base64编码的结果为SGVsbG8hIQAA 。这里有可能会存在困惑,同样是叹号(!),前后两个怎么不一样啊?那是因为我们需要满足每3个原始字符编码成4个字符,当不足时添加0,所以最后叹号(!)变成了IQAA

需要注意:最后2个零值只是为了Base64编码而补充的,所以Base64编码结果中的最后两个字符AA实际不带有效信息,所以需要特殊处理,以免解码错误。标准Base64编码通常用 =字符来替换最后的A,即最终的编码结果为SGVsbG8hIQ==。因为= 字符并不在Base64编码索引表中,其意义在于结束符号,在Base64解码时遇到=时即可知道一个Base64编码字符串结束。

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

前面也说了解码是对编码的逆向操作

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

理解Base64或其他类似编码的关键有两点

  • 计算机最终存储和执行的是01二进制序列,这个二进制序列的含义则由解码程序/解释程序决定
  • 很多场景下的数据传输要求数据只能由简单通用的字符组成,比如:HTTP协议要求请求的首行和请求头都必须是ASCII编码

使用场景

  • X.509公钥证书

对证书来说,特别是根证书,一般都是作Base64编码的,因为它要在网上被许多人下载。电子邮件的附件一般也作Base64编码的,因为一个附件数据往往是有不可见字符的。

  • 文本传输

一个XML当中包含另一个XML数据,此时如果将XML数据直接写入显然不合适,将XML进行适当编码存入较为方便,事实上XML当中的字符一般都是可见字符(0-127之间),但是由于中文的存在,可能存在不可见字符,直接将字符打印在外层XML的数据中显然不合理,那么怎么办呢?可以使用Base64进行编码,然后存入XML,解码反之

  • HTTP协议

HTTP协议当中的key value字段,必须进行URLEncode不然出现的等号可能使解析失败,空格也会使HTTP请求解析出现问题,比如:请求行就是以空格来划分的POST /guowuxin/hehe HTTP/1.1

  • 电子邮件(SMTP协议)

有些文本协议不支持不可见字符的传递,只能用大于32的可见字符来传递信息(协议规定), 这个可参考阮一峰的《MIME笔记》

  • 图片base64编码

前端在实现页面时,对于一些简单图片,通常会选择将图片内容直接内嵌在页面中,避免不必要的外部资源加载,增大页面加载时间,但是图片数据是二进制数据,该怎么嵌入呢?绝大多数现代浏览器都支持一种名为 Data URLs 的特性,允许使用Base64对图片或其他文件的二进制数据进行编码,将其作为文本字符串嵌入网页中。

iOS中的Base64编解码

  • 1、Base64编解码 - OC

通过NSString+Base64分类来实现

#import <Foundation/Foundation.h>

@interface NSString (Base64)

/**
 *  转换为Base64编码
 */
 - (NSString *)base64EncodedString;

 /**
 *  将Base64编码还原
 */
 - (NSString *)base64DecodedString;

 @end

#import "NSString+Base64.h"

@implementation NSString (Base64)

- (NSString *)base64EncodedString;
{
    NSData *data = [self dataUsingEncoding: NSUTF8StringEncoding];
    return [data base64EncodedStringWithOptions:0];
}

- (NSString *)base64DecodedString
{
    NSData *data = [[NSData alloc]initWithBase64EncodedString:self options:0];
    return [[NSString alloc]initWithData:data encoding: NSUTF8StringEncoding];
}

@end

简单使用

#import "ViewController.h"
#import "NSString+Base64.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    NSString *string = @"Hello!";
    NSLog(@"原文 - %@", string);

    NSString *base64String = [string base64EncodedString];
    NSLog(@"Base64编码 - %@", base64String);

    NSString *decodeString = [base64String base64DecodedString];
    NSLog(@"Base64解码 - %@", decodeString);
}

@end

运行结果

原文 - Hello!
Base64编码 - SGVsbG8h
Base64解码 - Hello!
  • Base64编解码 - Swift

扩展String,添加便捷编解码方法

extension String {
    // Base64 encoding a string
    func base64Encoded() -> String? {
        if let data = self.data(using: .utf8) {
            return data.base64EncodedString()
        }
        return nil
    }

    // Base64 decoding a string
    func base64Decoded() -> String? {
        if let data = Data(base64Encoded: self) {
            return String(data: data, encoding: .utf8)
        }
        return nil
    }
}

简单使用

let string = "Hello!"
let base64String = string.base64Encoded()
print("base64String: \(base64String!)")
if let decodeString = base64String?.base64Decoded() {
     print("decodeString: \(decodeString)")
 }

运行结果

base64String: SGVsbG8h
decodeString: Hello!
  • 2、将图片进行Base64编解码
func encodeAndDecodeUIImage() {
    let image = UIImage(named: "pig")!
    let imageData = image.pngData()!

    let base64String = imageData.base64EncodedString(options: .lineLength64Characters)
    print("base64String: \(base64String)")

    if let dataDecoded = Data(base64Encoded: base64String, options: .ignoreUnknownCharacters) {
        let decodedImage = UIImage(data: dataDecoded)
        imageView.image = decodedImage
     }
 }

Convert between UIImage and Base64 string

  • 3、分析Base64编码
+(NSString *)base64StringFromData:(NSData *)data
{
    NSString *encoding = nil;
    unsigned char *encodingBytes = NULL;
    @try {
        // 字符集合
        static char encodingTable[64] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
        //
        static NSUInteger paddingTable[] = {0,2,1};
        //                 Table 1: The Base 64 Alphabet
        //        
        //    Value Encoding  Value Encoding  Value Encoding  Value Encoding
        //        0 A            17 R            34 i            51 z
        //        1 B            18 S            35 j            52 0
        //        2 C            19 T            36 k            53 1
        //        3 D            20 U            37 l            54 2
        //        4 E            21 V            38 m            55 3
        //        5 F            22 W            39 n            56 4
        //        6 G            23 X            40 o            57 5
        //        7 H            24 Y            41 p            58 6
        //        8 I            25 Z            42 q            59 7
        //        9 J            26 a            43 r            60 8
        //       10 K            27 b            44 s            61 9
        //       11 L            28 c            45 t            62 +
        //       12 M            29 d            46 u            63 /
        //       13 N            30 e            47 v
        //       14 O            31 f            48 w         (pad) =
        //       15 P            32 g            49 x
        //       16 Q            33 h            50 y

        // 数据大小
        NSUInteger dataLength = [data length];
        NSUInteger encodedBlocks = dataLength / 3;
        if( (encodedBlocks + 1) >= (NSUIntegerMax / 4) ) return nil; // NSUInteger overflow check

        // 需要拼接的字符数,如:数据长度取余3为0,那么表示可以正处,不需要添加额外字符
        NSUInteger padding = paddingTable[dataLength % 3];

        // 如果需要添加字符存在,增加需要编码的数量
        if( padding > 0 ) encodedBlocks++;

        // 总的编码长度,因为根据编码可知,编码前的3字符变为编码后的4字符
        NSUInteger encodedLength = encodedBlocks * 4;

        // 分配内存大小
        encodingBytes = malloc(encodedLength);
        // 指针不为空
        if( encodingBytes != NULL ) {
            // 未编码之前的字节数
            NSUInteger rawBytesToProcess = dataLength;
            // 未编码之前的位置
            NSUInteger rawBaseIndex = 0;
            // 编码的位置
            NSUInteger encodingBaseIndex = 0;
            // 获取字节指针
            unsigned char *rawBytes = (unsigned char *)[data bytes];

            // 编码前的字节
            unsigned char rawByte1, rawByte2, rawByte3;
            // 循环遍历所有字符
            while( rawBytesToProcess >= 3 ) {
                // 获取3个字符
                rawByte1 = rawBytes[rawBaseIndex];
                rawByte2 = rawBytes[rawBaseIndex+1];
                rawByte3 = rawBytes[rawBaseIndex+2];

                // 设置4个字符
                encodingBytes[encodingBaseIndex] = encodingTable[((rawByte1 >> 2) & 0x3F)];
                encodingBytes[encodingBaseIndex+1] = encodingTable[((rawByte1 << 4) & 0x30) | ((rawByte2 >> 4) & 0x0F) ];
                encodingBytes[encodingBaseIndex+2] = encodingTable[((rawByte2 << 2) & 0x3C) | ((rawByte3 >> 6) & 0x03) ];
                encodingBytes[encodingBaseIndex+3] = encodingTable[(rawByte3 & 0x3F)];

                // 修改位置,获取下一个三个字符
                rawBaseIndex += 3;
                // 设置接下来的4个字符
                encodingBaseIndex += 4;
                rawBytesToProcess -= 3;
            }
            rawByte2 = 0;
            switch (dataLength-rawBaseIndex) {
                case 2:
                    rawByte2 = rawBytes[rawBaseIndex+1];
                case 1:
                    rawByte1 = rawBytes[rawBaseIndex];
                    encodingBytes[encodingBaseIndex] = encodingTable[((rawByte1 >> 2) & 0x3F)];
                    encodingBytes[encodingBaseIndex+1] = encodingTable[((rawByte1 << 4) & 0x30) | ((rawByte2 >> 4) & 0x0F) ];
                    encodingBytes[encodingBaseIndex+2] = encodingTable[((rawByte2 << 2) & 0x3C) ];
                    // we can skip rawByte3 since we have a partial block it would always be 0
                    break;
            }
            // compute location from where to begin inserting padding, it may overwrite some bytes from the partial block encoding
            // if their value was 0 (cases 1-2).
            encodingBaseIndex = encodedLength - padding;
            while( padding-- > 0 ) {
                // 添加=补齐4/3
                encodingBytes[encodingBaseIndex++] = '=';
            }
            encoding = [[NSString alloc] initWithBytes:encodingBytes length:encodedLength encoding:NSASCIIStringEncoding];
        }
    }
    @catch (NSException *exception) {
        encoding = nil;
        NSLog(@"WARNING: error occured while tring to encode base 32 data: %@", exception);
    }
    @finally {
        if( encodingBytes != NULL ) {
            free( encodingBytes );
        }
    }
    return encoding;
}

Base64

优秀的加密库

CryptoSwift

最后注意:Base64并不是安全领域的加密算法,其实Base64只能算是一个编码算法,对数据内容进行编码来适合传输

参考

iOS开发探索-Base64编码
为什么要使用base64编码,有哪些情景需求?
Base64

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