我该如何在iOS上进行base64编码?

235

我想进行 base64 编码和解码,但在 iPhone 的 SDK 中找不到任何支持。如何在有或没有库的情况下进行 base64 编码和解码?


@GregBernhardt 的链接已失效。 - Cœur
19个回答

116

这是一个很好的使用Objective-C 分类的案例。

对于Base64编码:

#import <Foundation/NSString.h>

@interface NSString (NSStringAdditions)

+ (NSString *) base64StringFromData:(NSData *)data length:(int)length;

@end

-------------------------------------------

#import "NSStringAdditions.h"

static char base64EncodingTable[64] = {
  'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
  'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
  'g', 'h', 'i', 'j', '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', '+', '/'
};

@implementation NSString (NSStringAdditions)

+ (NSString *) base64StringFromData: (NSData *)data length: (int)length {
  unsigned long ixtext, lentext;
  long ctremaining;
  unsigned char input[3], output[4];
  short i, charsonline = 0, ctcopy;
  const unsigned char *raw;
  NSMutableString *result;

  lentext = [data length]; 
  if (lentext < 1)
    return @"";
  result = [NSMutableString stringWithCapacity: lentext];
  raw = [data bytes];
  ixtext = 0; 

  while (true) {
    ctremaining = lentext - ixtext;
    if (ctremaining <= 0) 
       break;        
    for (i = 0; i < 3; i++) { 
       unsigned long ix = ixtext + i;
       if (ix < lentext)
          input[i] = raw[ix];
       else
  input[i] = 0;
  }
  output[0] = (input[0] & 0xFC) >> 2;
  output[1] = ((input[0] & 0x03) << 4) | ((input[1] & 0xF0) >> 4);
  output[2] = ((input[1] & 0x0F) << 2) | ((input[2] & 0xC0) >> 6);
  output[3] = input[2] & 0x3F;
  ctcopy = 4;
  switch (ctremaining) {
    case 1: 
      ctcopy = 2; 
      break;
    case 2: 
      ctcopy = 3; 
      break;
  }

  for (i = 0; i < ctcopy; i++)
     [result appendString: [NSString stringWithFormat: @"%c", base64EncodingTable[output[i]]]];

  for (i = ctcopy; i < 4; i++)
     [result appendString: @"="];

  ixtext += 3;
  charsonline += 4;

  if ((length > 0) && (charsonline >= length))
    charsonline = 0;
  }     
  return result;
}

@end

Base64解码:

#import <Foundation/Foundation.h>

@class NSString;

@interface NSData (NSDataAdditions)

+ (NSData *) base64DataFromString:(NSString *)string;

@end

-------------------------------------------

#import "NSDataAdditions.h"

@implementation NSData (NSDataAdditions)

+ (NSData *)base64DataFromString: (NSString *)string
{
    unsigned long ixtext, lentext;
    unsigned char ch, inbuf[4], outbuf[3];
    short i, ixinbuf;
    Boolean flignore, flendtext = false;
    const unsigned char *tempcstring;
    NSMutableData *theData;

    if (string == nil)
    {
        return [NSData data];
    }

    ixtext = 0;

    tempcstring = (const unsigned char *)[string UTF8String];

    lentext = [string length];

    theData = [NSMutableData dataWithCapacity: lentext];

    ixinbuf = 0;

    while (true)
    {
        if (ixtext >= lentext)
        {
            break;
        }

        ch = tempcstring [ixtext++];

        flignore = false;

        if ((ch >= 'A') && (ch <= 'Z'))
        {
            ch = ch - 'A';
        }
        else if ((ch >= 'a') && (ch <= 'z'))
        {
            ch = ch - 'a' + 26;
        }
        else if ((ch >= '0') && (ch <= '9'))
        {
            ch = ch - '0' + 52;
        }
        else if (ch == '+')
        {
            ch = 62;
        }
        else if (ch == '=')
        {
            flendtext = true;
        }
        else if (ch == '/')
        {
            ch = 63;
        }
        else
        {
            flignore = true; 
        }

        if (!flignore)
        {
            short ctcharsinbuf = 3;
            Boolean flbreak = false;

            if (flendtext)
            {
                if (ixinbuf == 0)
                {
                    break;
                }

                if ((ixinbuf == 1) || (ixinbuf == 2))
                {
                    ctcharsinbuf = 1;
                }
                else
                {
                    ctcharsinbuf = 2;
                }

                ixinbuf = 3;

                flbreak = true;
            }

            inbuf [ixinbuf++] = ch;

            if (ixinbuf == 4)
            {
                ixinbuf = 0;

                outbuf[0] = (inbuf[0] << 2) | ((inbuf[1] & 0x30) >> 4);
                outbuf[1] = ((inbuf[1] & 0x0F) << 4) | ((inbuf[2] & 0x3C) >> 2);
                outbuf[2] = ((inbuf[2] & 0x03) << 6) | (inbuf[3] & 0x3F);

                for (i = 0; i < ctcharsinbuf; i++)
                {
                    [theData appendBytes: &outbuf[i] length: 1];
                }
            }

            if (flbreak)
            {
                break;
            }
        }
    }

    return theData;
}

    @end

5
如果Obj-C和C很相似的话,你应该能够这样做: static char base64EncodingTable[64] = "ABCDE[等等]789+/"; - Artelius
3
我发现为什么我只得到了4个字符......在while()循环中的return之前需要有一个}。我想编辑它,但看起来我做不到。 - Larry Hipp
3
不是分析器的错误。请注意,代码还尝试访问超出该数组范围的inbuf [3]。这段代码很糟糕。 - Mike Weller
1
长度值代表什么? - MegaManX
4
从iOS7开始,苹果公司公开了其本地的Base64编码方法。请参见下面Rob的回答以了解如何在保持向后兼容性的同时使用它。 - Code Commander
显示剩余5条评论

100
一份非常快速的实现,它是从PHP Core库移植(并进行改进/改造)到本机的Objective-C代码中的QSUtilities LibraryQSStrings Class中提供。我进行了一个快速的基准测试:一个5.3MB的图像(JPEG)文件需要<50ms来编码,大约需要140ms来解码。
整个库的代码(包括Base64方法)都可以在GitHub上找到。
或者,如果你只想要Base64方法的代码本身,那么我在这里发布了它:
首先,你需要映射表:
static const char _base64EncodingTable[64] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
static const short _base64DecodingTable[256] = {
    -2, -2, -2, -2, -2, -2, -2, -2, -2, -1, -1, -2, -1, -1, -2, -2,
    -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
    -1, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, 62, -2, -2, -2, 63,
    52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -2, -2, -2, -2, -2, -2,
    -2,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14,
    15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -2, -2, -2, -2, -2,
    -2, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
    41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -2, -2, -2, -2, -2,
    -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
    -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
    -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
    -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
    -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
    -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
    -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
    -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2
};

编码:

+ (NSString *)encodeBase64WithString:(NSString *)strData {
    return [QSStrings encodeBase64WithData:[strData dataUsingEncoding:NSUTF8StringEncoding]];
}

+ (NSString *)encodeBase64WithData:(NSData *)objData {
    const unsigned char * objRawData = [objData bytes];
    char * objPointer;
    char * strResult;

    // Get the Raw Data length and ensure we actually have data
    int intLength = [objData length];
    if (intLength == 0) return nil;

    // Setup the String-based Result placeholder and pointer within that placeholder
    strResult = (char *)calloc((((intLength + 2) / 3) * 4) + 1, sizeof(char));
    objPointer = strResult;

    // Iterate through everything
    while (intLength > 2) { // keep going until we have less than 24 bits
        *objPointer++ = _base64EncodingTable[objRawData[0] >> 2];
        *objPointer++ = _base64EncodingTable[((objRawData[0] & 0x03) << 4) + (objRawData[1] >> 4)];
        *objPointer++ = _base64EncodingTable[((objRawData[1] & 0x0f) << 2) + (objRawData[2] >> 6)];
        *objPointer++ = _base64EncodingTable[objRawData[2] & 0x3f];

        // we just handled 3 octets (24 bits) of data
        objRawData += 3;
        intLength -= 3; 
    }

    // now deal with the tail end of things
    if (intLength != 0) {
        *objPointer++ = _base64EncodingTable[objRawData[0] >> 2];
        if (intLength > 1) {
            *objPointer++ = _base64EncodingTable[((objRawData[0] & 0x03) << 4) + (objRawData[1] >> 4)];
            *objPointer++ = _base64EncodingTable[(objRawData[1] & 0x0f) << 2];
            *objPointer++ = '=';
        } else {
            *objPointer++ = _base64EncodingTable[(objRawData[0] & 0x03) << 4];
            *objPointer++ = '=';
            *objPointer++ = '=';
        }
    }

    // Terminate the string-based result
    *objPointer = '\0';

    // Create result NSString object
    NSString *base64String = [NSString stringWithCString:strResult encoding:NSASCIIStringEncoding];

    // Free memory
    free(strResult);

    return base64String;
}

解码:

+ (NSData *)decodeBase64WithString:(NSString *)strBase64 {
    const char *objPointer = [strBase64 cStringUsingEncoding:NSASCIIStringEncoding];
    size_t intLength = strlen(objPointer);
    int intCurrent;
    int i = 0, j = 0, k;

    unsigned char *objResult = calloc(intLength, sizeof(unsigned char));

    // Run through the whole string, converting as we go
    while ( ((intCurrent = *objPointer++) != '\0') && (intLength-- > 0) ) {
        if (intCurrent == '=') {
            if (*objPointer != '=' && ((i % 4) == 1)) {// || (intLength > 0)) {
                // the padding character is invalid at this point -- so this entire string is invalid
                free(objResult);
                return nil;
            }
            continue;
        }

        intCurrent = _base64DecodingTable[intCurrent];
        if (intCurrent == -1) {
            // we're at a whitespace -- simply skip over
            continue;
        } else if (intCurrent == -2) {
            // we're at an invalid character
            free(objResult);
            return nil;
        }

        switch (i % 4) {
            case 0:
                objResult[j] = intCurrent << 2;
                break;

            case 1:
                objResult[j++] |= intCurrent >> 4;
                objResult[j] = (intCurrent & 0x0f) << 4;
                break;

            case 2:
                objResult[j++] |= intCurrent >>2;
                objResult[j] = (intCurrent & 0x03) << 6;
                break;

            case 3:
                objResult[j++] |= intCurrent;
                break;
        }
        i++;
    }

    // mop things up if we ended on a boundary
    k = j;
    if (intCurrent == '=') {
        switch (i % 4) {
            case 1:
                // Invalid state
                free(objResult);
                return nil;

            case 2:
                k++;
                // flow through
            case 3:
                objResult[k] = 0;
        }
    }

    // Cleanup and setup the return NSData
    NSData * objData = [[[NSData alloc] initWithBytes:objResult length:j] autorelease];
    free(objResult);
    return objData;
}

2
终于有了正确而高效的实现。谢谢。这里的其他代码有些让我感到害怕。 - Mike Weller
4
编码器中分配给 strResult 的内存似乎存在泄漏,只需在最后添加一个 free() 即可(在返回之前但在 NSString stringWithCString 之后)。 - JosephH
2
在你的encodeBase64WithData:方法中,对于调用calloc()的第一个参数,是否需要增加1来考虑你在末尾添加的空终止符('\0')? - erikprice
2
我已经使用这个工具一段时间了,一直表现得非常好,直到最近开始出现一些与内存损坏相关的错误。通过使用 guard malloc,我将问题追溯到了这一行代码: *objPointer = '\0'; 所以如果你在自己的应用程序中使用这段代码,请小心。 - Mattia
2
自iOS7起,苹果公司已经公开了他们的本地base64编码方法。请参见下面Rob的答案,以了解如何在保持向后兼容性的同时使用它。 - Code Commander
显示剩余4条评论

78

当这个问题最初发布时,人们理所当然地引导您使用第三方Base64库,因为缺乏任何本地例程。但iOS 7引入了Base64编码例程(实际上只是公开了iOS从iOS 4开始就有的私有方法)。

因此,您可以使用NSData方法base64EncodedStringWithOptions:NSData创建一个Base64字符串。

NSString *string = [data base64EncodedStringWithOptions:kNilOptions];

您可以使用initWithBase64EncodedString:options:将base-64字符串转换回NSData

NSData *data = [[NSData alloc] initWithBase64EncodedString:string options:kNilOptions]; 

或者,使用Swift:

let string = data.base64EncodedString()

并且

let data = Data(base64Encoded: string)

谢谢Rob。您能否简要阐述一下您所写的“*...并公开了之前私有的iOS 4方法*”是什么意思? - phi
8
很遗憾这个答案被埋没在所有那些自定义实现之下。这是SO的一个弱点,原问题问出很久后可能出现更适合的解决方案,但这个解决方案现在必须与之前被接受的竞争。 - jakev
这就是为什么赞更近期正确的答案总是很有用的原因 :) - Steve Wilford
为什么这样的答案不在最上面啊 :( ,我花了很多时间处理上面的所有答案 T__T - Mostafa Sultan
@Rob,你觉得把iOS 4和iOS 7的引用删除后再编辑这个内容怎么样?毕竟现在Xcode已经无法针对它们进行开发了。我本来想自己动手做的,但是想到你可能更喜欢这样。:) 如果能包含Swift示例代码就更好了,额外加分! - Abhi Beckert
我将保留iOS参考资料以供历史目的(否则人们会被所有关于如何手动执行的答案所困惑),并且不会使上面的评论失效。但是,我已经简化了我的答案(因为iOS 7之前的条件逻辑不再那么重要;如果有兴趣,人们可以查看之前的修订版本)。 - Rob

33

iOS内置了对Base64编码和解码的支持。如果你查看resolv.h,你会看到两个函数b64_ntopb64_pton。Square SocketRocket库提供了一个合理的示例,展示了如何在Objective-C中使用这些函数。

这些函数已经经过了充分的测试和验证——不像你可能在某些随意的网上帖子里找到的实现方式。别忘了链接libresolv.dylib


3
太棒了!比随意的互联网站好多了!如果有人担心使用这些文档不充分的函数,可以在苹果网站上看到它们的源代码 - Jesse Rusak
1
这个人在这里提供了更多相关背景:http://www.blog.montgomerie.net/ios-hidden-base64-routines - Mike

21

由于这似乎是与base64编码和iphone相关的谷歌搜索排名最高的问题,因此我想分享一下上面的代码片段的经验。

它能够工作,但速度非常慢。在一个随机图像(0.4 mb)上进行基准测试,本地iphone花费了37秒。主要原因可能是所有OOP魔法 - 单个字符的NSString等,在编码完成后才会被自动释放。

这里发布的另一个建议使用openssl库,这也感觉有些过度。

下面的代码只需70毫秒 - 这是500倍的加速。它仅执行base64编码(解码将在我遇到它时执行)。

+ (NSString *) base64StringFromData: (NSData *)data length: (int)length {
int lentext = [data length]; 
if (lentext < 1) return @"";

char *outbuf = malloc(lentext*4/3+4); // add 4 to be sure

if ( !outbuf ) return nil;

const unsigned char *raw = [data bytes];

int inp = 0;
int outp = 0;
int do_now = lentext - (lentext%3);

for ( outp = 0, inp = 0; inp < do_now; inp += 3 )
{
    outbuf[outp++] = base64EncodingTable[(raw[inp] & 0xFC) >> 2];
    outbuf[outp++] = base64EncodingTable[((raw[inp] & 0x03) << 4) | ((raw[inp+1] & 0xF0) >> 4)];
    outbuf[outp++] = base64EncodingTable[((raw[inp+1] & 0x0F) << 2) | ((raw[inp+2] & 0xC0) >> 6)];
    outbuf[outp++] = base64EncodingTable[raw[inp+2] & 0x3F];
}

if ( do_now < lentext )
{
    char tmpbuf[2] = {0,0};
    int left = lentext%3;
    for ( int i=0; i < left; i++ )
    {
        tmpbuf[i] = raw[do_now+i];
    }
    raw = tmpbuf;
    outbuf[outp++] = base64EncodingTable[(raw[inp] & 0xFC) >> 2];
    outbuf[outp++] = base64EncodingTable[((raw[inp] & 0x03) << 4) | ((raw[inp+1] & 0xF0) >> 4)];
    if ( left == 2 ) outbuf[outp++] = base64EncodingTable[((raw[inp+1] & 0x0F) << 2) | ((raw[inp+2] & 0xC0) >> 6)];
}

NSString *ret = [[[NSString alloc] initWithBytes:outbuf length:outp encoding:NSASCIIStringEncoding] autorelease];
free(outbuf);

return ret;
}

我省略了行切割,因为我不需要它,但添加它很简单。

对于那些有兴趣优化的人:目标是尽量减少主循环中发生的事情。因此,处理最后3个字节的所有逻辑都在循环外部处理。

此外,尝试在原地处理数据,避免额外的缓冲区复制操作。同时将任何算术运算降到最低。

请注意,用于查找表中条目的位,在进行按位或操作时不会重叠而不需移位。因此,一个主要的改进是使用4个分别为256字节的查找表,消除移位,如下所示:

outbuf[outp++] = base64EncodingTable1[(raw[inp] & 0xFC)];
outbuf[outp++] = base64EncodingTable2[(raw[inp] & 0x03) | (raw[inp+1] & 0xF0)];
outbuf[outp++] = base64EncodingTable3[(raw[inp+1] & 0x0F) | (raw[inp+2] & 0xC0)];
outbuf[outp++] = base64EncodingTable4[raw[inp+2] & 0x3F];
当然,你可以更进一步,但这已经超出了本文的范围。

嗯,我无法让它工作。我观察到的base64编码与我的预期值不同。您是否已经使用RFC 4648中的示例进行了测试?http://tools.ietf.org/html/rfc4648 - Alex Reynolds
3
看不清 base64EncodingTable1、base64EncodingTable2、base64EncodingTable3 和 base64EncodingTable4 引用的是什么? - Jamie Chapman
非常有帮助,但它可以读取超出输入缓冲区末尾的内容。当(left==2)时,raw[inp+2]将超出tmpbuf的末尾一个字节。我认为应该修改这行代码为:如果(left == 2),则outbuf[outp++] = base64EncodingTable[((raw[inp+1] & 0x0F) << 2)]; - John Lemberger
将以下代码行更改为:<code>unsigned char tmpbuf[3] = {0,0,0};</code>,原代码行为<code>char tmpbuf[2] = {0,0};</code>。 - Satya

9
更好的解决方案:
NSData内置了一个函数
[data base64Encoding]; //iOS < 7.0
[data base64EncodedStringWithOptions:NSDataBase64Encoding76CharacterLineLength]; //iOS >= 7.0

我们可以使用“[[UIDevice currentDevice] systemVersion].floatValue”来基于应用程序运行的iOS版本进行操作。 - Nagaraj
2
  1. 这并不能告诉你你链接了哪个SDK,那是运行时检查。
  2. 那直接违反了苹果的指导方针。你应该检查一个功能的可用性,而不是系统版本。
- quellish

9
在mvds的优秀改进中,存在两个问题。将代码更改为以下内容:
raw = tmpbuf;
inp = 0;
outbuf[outp++] = base64EncodingTable[(raw[inp] & 0xFC) >> 2];
outbuf[outp++] = base64EncodingTable[((raw[inp] & 0x03) << 4) | ((raw[inp+1] & 0xF0) >> 4)];
if ( left == 2 ) outbuf[outp++] = base64EncodingTable[((raw[inp+1] & 0x0F) << 2) | ((raw[inp+2] & 0xC0) >> 6)];
else outbuf[outp++] = '=';
outbuf[outp++] = '=';

6
在iOS8及更高版本中,可以使用NSData的- (NSString *)base64EncodedStringWithOptions:(NSDataBase64EncodingOptions)options方法进行base64编码。

6
很高兴大家喜欢它。我必须承认,最终游戏还有一些缺陷。除了正确地将inp设置为0外,您还应该将tmpbuf的大小增加到3,例如:
unsigned char tmpbuf[3] = {0,0,0};

省略raw [inp + 2]的或运算;当该块的raw [inp + 2]!=0时,我们仍然在循环中...

两种方式都可以,为了清晰起见,您可能考虑将最终的表查找块保持与循环中的相同。在我使用的最终版本中,我这样做了。

while ( outp%4 ) outbuf[outp++] = '=';

添加==

很抱歉我没有检查RFC和其他相关内容,应该做得更好!


3
您已经在这里拥有一个帐户,因为您之前的答案实际上是不同的帐户。此外,这应该是对该帐户进行编辑或评论。 - Alastair Pitts
@alastair,您似乎每次在清除cookies后发布回答时都会创建一个“账户”。即使使用相同的电子邮件和IP地址,我也无法连接到我的第一个“账户”,所以我只能将其作为新回答放在那里,非常抱歉。--我刚刚注册了! - mvds
3
可以把这个答案编辑到您之前的答案中,以便有一个明确正确的版本吗?谢谢! - JosephH

3

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接