iOS SDK快速入门

本文介绍如何快速使用日志服务iOS SDK采集日志数据。

前提条件

已安装iOS SDK。具体操作,请参见安装iOS SDK

快速使用

您可以按照以下方式对SDK进行初始化,并调用addLog方法上报日志。

重要
  • iOS SDK支持初始化多个实例,LogProducerConfig实例与LogProducerClient实例要成对使用。

  • 上报日志到日志服务时需使用阿里云账号或RAM用户的AccessKey,用于鉴权及防篡改。为避免将AccessKey保存在移动端应用中,造成安全风险,推荐您使用移动端日志直传服务配置AccessKey。具体操作,请参见采集-搭建移动端日志直传服务

@interface ProducerExampleController ()
// 建议您全局保存LogProducerConfig实例和LogProducerClient实例。
@property(nonatomic, strong) LogProducerConfig *config;
@property(nonatomic, strong) LogProducerClient *client;
@end

@implementation ProducerExampleController


// callback为可选配置,如果您不需要关注日志的发送成功或失败状态,可以不注册callback。
// 如果需要动态化配置AccessKey,建议设置callback,并在callback被调用时更新AccessKey。
static void _on_log_send_done(const char * config_name, log_producer_result result, size_t log_bytes, size_t compressed_bytes, const char * req_id, const char * message, const unsigned char * raw_buffer, void * userparams) {
    if (result == LOG_PRODUCER_OK) {
        NSString *success = [NSString stringWithFormat:@"send success, config : %s, result : %d, log bytes : %d, compressed bytes : %d, request id : %s", config_name, (result), (int)log_bytes, (int)compressed_bytes, req_id];
        SLSLogV("%@", success);
    } else {
        NSString *fail = [NSString stringWithFormat:@"send fail   , config : %s, result : %d, log bytes : %d, compressed bytes : %d, request id : %s, error message : %s", config_name, (result), (int)log_bytes, (int)compressed_bytes, req_id, message];
        SLSLogV("%@", fail);
    }
}

- (void) initLogProducer {
    // 日志服务的服务接入点。此处必须是以https://或http://开头。
    NSString *endpoint = @"your endpoint";
    NSString *project = @"your project";
    NSString *logstore = @"your logstore";

    _config = [[LogProducerConfig alloc] initWithEndpoint:endpoint
                                                  project:project
                                                 logstore:logstore
    ];

    // 设置日志主题。
    [_config SetTopic:@"example_topic"];
    // 设置tag信息,此tag信息将被附加在每条日志上。
    [_config AddTag:@"example" value:@"example_tag"];
    //是否丢弃过期日志。0表示不丢弃,把日志时间修改为当前时间; 1表示丢弃。默认值为1。
    [_config SetDropDelayLog:1];
    // 是否丢弃鉴权失败的日志,0表示不丢弃,1表示丢弃。默认值为0。
    [_config SetDropUnauthorizedLog:0];    

    // 需要关注日志的发送成功或失败状态时, 第二个参数需要传入一个callback。
    _client = [[LogProducerClient alloc] initWithLogProducerConfig:_config callback:_on_log_send_done];
}

// 请求AccessKey信息。
- (void) requestAccessKey {
    // 推荐您先使用移动端日志直传服务配置AccessKey信息。
    // ...

    //  获取到AccessKey信息后,完成更新。
    [self updateAccessKey:accessKeyId accessKeySecret:accessKeySecret securityToken:securityToken];
}

// 更新AccessKey信息。
- (void) updateAccessKey:(NSString *)accessKeyId accessKeySecret:(NSString *)accessKeySecret securityToken:(NSString *)securityToken {
    
    // 通过STS服务获取的AccessKey会包含securitToken,需要使用以下方式更新。
    if (securityToken.length > 0) {
        if (accessKeyId.length > 0 && accessKeySecret.length > 0) {
            [_config ResetSecurityToken:accessKeyId
                        accessKeySecret:accessKeySecret
                          securityToken:securityToken
            ];
        }
    } else {
        // 不是通过STS服务获取的AccessKey,使用以下方式更新。
        if (accessKeyId.length > 0 && accessKeySecret.length > 0) {
            [_config setAccessKeyId: accessKeyId];
            [_config setAccessKeySecret: accessKeySecret];
        }
    }
}
// 上报日志。
- (void) addLog {
    Log *log = [Log log];
    // 您可以根据实际业务需要调整需上报的字段。
    [log putContent:@"content_key_1" intValue:123456];
    [log putContent:@"content_key_2" floatValue:23.34f];
    [log putContent:@"content_key_3" value:@"中文"];
    
    [_client AddLog:log];
}
@end

高级用法

动态配置参数

iOS SDK支持动态化配置ProjectName、Logstore、Endpoint、AccessKey等参数。其中Endpoint的获取方式,请参见服务接入点。AccessKey的获取方式,请参见访问密钥

  • 动态化配置Endpoint、ProjectName、Logstore。

    // 支持独立配置或一起配置Endpoint、ProjectName、Logstore。
    // 更新Endpoint。
    [_config setEndpoint:@"your new-endpoint"];
    // 更新ProjectName。
    [_config setProject:@"your new-project"];
    // 更新Logstore。
    [_config setLogstore:@"your new-logstore"];
  • 动态配置AccessKey。

    动态配置AccessKey时,一般建议与callback结合使用。

    // 如果您在初始化LogProducerClient时已经完成了callback的初始化,以下代码可忽略。
    static void _on_log_send_done(const char * config_name, log_producer_result result, size_t log_bytes, size_t compressed_bytes, const char * req_id, const char * message, const unsigned char * raw_buffer, void * userparams) {
        if (LOG_PRODUCER_SEND_UNAUTHORIZED == result || LOG_PRODUCER_PARAMETERS_INVALID) {
            [selfClzz requestAccessKey]; // selfClzz为对当前类的持有。
        }
    }
    
    // 需要关注日志的发送成功或失败状态时, 第二个参数需要传入一个callback。
    _client = [[LogProducerClient alloc] initWithLogProducerConfig:_config callback:_on_log_send_done];
    
    // 请求AccessKey信息。
    - (void) requestAccessKey {
        // 推荐您先使用移动端日志直传服务配置AccessKey信息。
        // ...
    
        //  获取到AccessKey信息后,完成更新。
        [self updateAccessKey:accessKeyId accessKeySecret:accessKeySecret securityToken:securityToken];
    }
    
    // 更新AccessKey信息。
    - (void) updateAccessKey:(NSString *)accessKeyId accessKeySecret:(NSString *)accessKeySecret securityToken:(NSString *)securityToken {
        
        // 通过STS服务获取的AccessKey会包含securitToken,需要使用以下方式更新。
        if (securityToken.length > 0) {
            if (accessKeyId.length > 0 && accessKeySecret.length > 0) {
                [_config ResetSecurityToken:accessKeyId
                            accessKeySecret:accessKeySecret
                              securityToken:securityToken
                ];
            }
        } else {
            // 不是通过STS服务获取的AccessKey,使用以下方式更新。
            if (accessKeyId.length > 0 && accessKeySecret.length > 0) {
                [_config setAccessKeyId: accessKeyId];
                [_config setAccessKeySecret: accessKeySecret];
            }
        }
    }
  • 动态化配置 source、topic、tag。

    重要

    source、topic、tag无法针对某类日志进行设置。一旦设置后,所有未成功发送到日志服务的日志,都可能会更新。如果您需要通过source、topic、tag来跟踪具体类别的日志,可能会导致与您的业务预期不相符合。建议您在生成Log时新增字段来标识对应的类别信息。

    // 设置日志主题。
    [_config SetTopic:@"your new-topic"];
    // 设置日志来源。
    [_config SetSource:@"your new-source"];
    // 设置tag信息,此tag信息会附加在每条日志上。
    [_config AddTag:@"test" value:@"your new-tag"];

断点续传

iOS SDK支持断点续传。开启断点续传后,每次通过addLog方法上传成功的日志都会先保存在本地binlog文件中,只有日志发送成功后才会删除本地数据,确保日志上传实现At Least Once。

您可以在SDK初始化时加入以下代码,实现断点续传。

重要
  • 初始化多个LogProducerConfig实例时,LogProducerConfig类的setPersistentFilePath方法需要传入不同的值。

  • 如果您的App存在多进程且开启了断点续传功能,您应只在主进程初始化SDK。如果子进程也有采集数据的需求,您需要确保SetPersistentFilePath方法传入的文件路径的唯一性,否则可能会导致日志数据错乱、丢失等问题。

  • 使用时应注意多线程导致的LogProducerConfig重复初始化问题。

- (void) initLogProducer {
    //  1表示开启断点续传功能,0表示关闭。默认值为0。
    [_config SetPersistent:1];
    
    // 持久化的文件名,需要保证文件所在的文件夹已创建。
    NSArray  *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *Path = [[paths lastObject] stringByAppendingString:@"/log.dat"];
    [_config SetPersistentFilePath:Path];
    // 持久化文件滚动个数,建议设置成10。
    [_config SetPersistentMaxFileCount:10];
    // 每个持久化文件的大小,单位为Byte,格式为N*1024*1024。建议N的取值范围为1~10。
    [_config SetPersistentMaxFileSize:N*1024*1024];
    // 本地最多缓存的日志数量,不建议超过1048576,默认为65536。
    [_config SetPersistentMaxLogCount:65536];
}

配置参数说明

所有的配置参数由LogProducerConfig类提供,详见下表:

参数

数据类型

说明

SetTopic

String

设置topic字段的值。默认值为空字符串。

AddTag

String

设置tag,格式为tag:xxxx。默认值为空字符串。

SetSource

String

设置source字段的值。默认值为iOS。

SetPacketLogBytes

Int

每个缓存的日志包大小上限。超过上限后,日志会被立即发送。

取值范围为1~5242880,默认值为1024 * 1024,单位为字节。

SetPacketLogCount

Int

每个缓存的日志包中包含日志数量的最大值。超过上限后日志会被立即发送。

取值范围为1~4096,默认值为1024。

SetPacketTimeout

Int

被缓存日志的发送超时时间,如果缓存超时,日志会被立即发送。

默认值为3000,单位为毫秒。

SetMaxBufferLimit

Int

单个Producer Client实例可以使用的内存的上限,超出缓存时add_log接口会立即返回失败。

默认值为64 * 1024 * 1024。

SetPersistent

Int

是否开启断点续传功能。

  • 1:开启。

  • 0(默认值):关闭。

SetPersistentFilePath

String

持久化的文件名,需保证文件所在的文件夹已创建。配置多个LogProducerConfig实例时,需确保唯一性。

默认值为空。

SetPersistentForceFlush

Int

是否开启每次AddLog强制刷新功能。

  • 1:开启。开启后对性能会有影响,建议谨慎开启。

  • 0(默认值):关闭。

在高可靠性场景下建议开启。

SetPersistentMaxFileCount

Int

持久化文件滚动个数,建议取值范围为1~10,默认值为0。

SetPersistentMaxFileSize

Int

每个持久化文件的大小,单位为Byte,格式为N*1024*1024。建议N的取值范围为1~10。

SetPersistentMaxLogCount

Int

本地最多缓存的日志数量,不建议超过1048576,默认为65536。

SetConnectTimeoutSec

IntInt

网络连接超时时间。默认值为10,单位为秒。

SetSendTimeoutSec

Int

日志发送超时时间。默认值为15,单位为秒。

SetDestroyFlusherWaitSec

Int

flusher线程销毁最大等待时间。默认值为1,单位为秒。

SetDestroySenderWaitSec

Int

sender线程池销毁最大等待时间。默认值为1,单位为秒。

SetCompressType

Int

数据上传时的压缩类型。

  • 0:不压缩。

  • 1(默认值):LZ4压缩。

SetNtpTimeOffset

Int

设备时间与标准时间的差值,值为标准时间-设备时间。一般这种差值是由于用户客户端设备时间不同步场景。默认值为0,单位为秒。

SetMaxLogDelayTime

Int

日志时间与本机时间的差值。超过该差值后,SDK会根据setDropDelayLog选项进行处理。单位为秒,默认值为7243600,即7天。

SetDropDelayLog

Int

是否丢弃超过setMaxLogDelayTime的过期日志。

  • 0:不丢弃,把日志时间修改为当前时间。

  • 1(默认值):丢弃。

SetDropUnauthorizedLog

Int

是否丢弃鉴权失败的日志。

  • 0(默认值):不丢弃。

  • 1:丢弃。

错误码

全部的错误码定义在log_producer_result,详细说明如下表所示。

错误码

数值

说明

解决方案

LOG_PRODUCER_OK

0

成功。

不涉及。

LOG_PRODUCER_INVALID

1

SDK已销毁或无效。

  1. 检查是否正确初始化SDK。

  2. 检查是否调用了destroy()方法。

LOG_PRODUCER_WRITE_ERROR

2

数据写入错误,可能原因是Project写入流量已达上限。

调整Project写入流量上限。具体操作,请参见调整资源配额

LOG_PRODUCER_DROP_ERROR

3

磁盘或内存缓存已满,日志无法写入。

调整maxBufferLimit、persistentMaxLogCount、persistentMaxFileSize参数值后重试。

LOG_PRODUCER_SEND_NETWORK_ERROR

4

网络错误。

检查Endpoint、Project、Logstore的配置情况。

LOG_PRODUCER_SEND_QUOTA_ERROR

5

Project写入流量已达上限。

调整Project写入流量上限。具体操作,请参见调整资源配额

LOG_PRODUCER_SEND_UNAUTHORIZED

6

AccessKey过期、无效或AccessKey权限策略配置不正确。

检查AccessKey。

RAM用户需具备操作日志服务资源的权限。具体操作,请参见RAM用户授权

LOG_PRODUCER_SEND_SERVER_ERROR

7

服务错误

提请工单联系技术支持。

LOG_PRODUCER_SEND_DISCARD_ERROR

8

数据被丢弃,一般是设备时间与服务器时间不同步导致

SDK会自动重新发送。

LOG_PRODUCER_SEND_TIME_ERROR

9

与服务器时间不同步。

SDK会自动修复该问题。

LOG_PRODUCER_SEND_EXIT_BUFFERED

10

SDK销毁时缓存数据还没有发出。

建议开启断点续传功能,以避免数据丢失。

LOG_PRODUCER_PARAMETERS_INVALID

11

SDK初始化参数错误

检查AccessKey、Endpoint、Project、Logstore等参数配置。

LOG_PRODUCER_PERSISTENT_ERROR

99

缓存数据写入磁盘失败

1、检查缓存文件路径配置是否正确。

2、检查缓存文件是否写满。

3、检查系统磁盘空间是否充足。

常见问题

为什么会存在重复日志?

iOS SDK发送日志的过程是异步的,受网络状态影响,日志可能会发送失败并重新发送。由于SDK只在接口返回200状态码时才认为发送成功,因此日志会存在一定的重复率。建议您通过SQL查询分析语句对数据进行去重。

如果日志重复率较高,您需要先排查下SDK初始化是否存在问题。主要原因和解决方案如下:

  • 断点续传配置错误

    针对断点续传配置错误,您需要确认下SetPersistentFilePath方法传入的文件路径是否全局唯一。

  • SDK重复初始化

    • 造成SDK重复初始化的常见原因是单个实例写法错误,或者没有使用单例模式封装SDK的初始化,建议您参考以下方式完成SDK初始化。

      // AliyunLogHelper.h
      @interface AliyunLogHelper : NSObject
      + (instancetype)sharedInstance;
      - (void) addLog:(Log *)log;
      @end
      
      
      // AliyunLogHelper.m
      @interface AliyunLogHelper ()
      @property(nonatomic, strong) LogProducerConfig *config;
      @property(nonatomic, strong) LogProducerClient *client;
      @end
      
      @implementation AliyunLogHelper
      
      + (instancetype)sharedInstance {
          static AliyunLogHelper *sharedInstance = nil;
          static dispatch_once_t onceToken;
          dispatch_once(&onceToken, ^{
              sharedInstance = [[self alloc] init];
          });
          return sharedInstance;
      }
      
      - (instancetype)init {
          self = [super init];
          if (self) {
              [self initLogProducer];
          }
          return self;
      }
      
      - (void) initLogProducer {
          // 以下代码替换为您的初始化代码。
          _config = [[LogProducerConfig alloc] initWithEndpoint:@""
                                                        project:@""
                                                       logstore:@""
                                                    accessKeyID:@""
                                                accessKeySecret:@""
                                                  securityToken:@""
          ];
      
          _client = [[LogProducerClient alloc] initWithLogProducerConfig:_config callback:_on_log_send_done];
      }
      
      - (void) addLog:(Log *)log {
          if (nil == log) {
              return;
          }
      
          [_client AddLog:log];
      }
      
      @end
    • 另外一个造成SDK重复初始化的原因是多进程。建议您只在主进程中初始化SDK,或者在不同进程中初始化SDK时,设置SetPersistentFilePath为不同的值,确保唯一性。

  • 弱网环境配置优化

    如果您的应用是在弱网环境下使用,建议您参考如下示例优化SDK配置参数。

    // 初始化SDK。
    - (void) initProducer() {
        // 调整HTTP链接和发送超时时间,有利于减少日志重复率。
        // 您可以根据实际情况调整具体的超时时间。
        [_config SetConnectTimeoutSec:20];
        [_config SetSendTimeoutSec:20];
      
        // 其他初始化参数。
        // ...
    }

日志缺失,如何处理?

日志上报的过程是异步的,如果在上报日志前App被关闭,则日志有可能无法被上报,造成日志丢失。建议您开启断点续传功能。具体操作,请参见断点续传

日志上报延时,如何处理?

SDK发送日志的过程是异步的,受网络环境以及应用使用场景的影响,日志可能不会立即上报。如果只有个别设备出现日志上报延时,这种情况是正常的,否则请您根据如下错误码进行排查。

错误码

说明

LOG_PRODUCER_SEND_NETWORK_ERROR

检查Endpoint、Project、Logstore的配置是否正确。

LOG_PRODUCER_SEND_UNAUTHORIZED

检查AccessKey是否过期、有效或AccessKey权限策略配置是否正确。

LOG_PRODUCER_SEND_QUOTA_ERROR

调整Project写入流量已达上限。具体操作,请参见调整资源配额

iOS SDK是否支持DNS预解析和缓存策略?

以下提供iOS SDK结合HTTPDNS SDK实现DNS预解析和缓存策略的示例。更多信息,请参见HTTPS+SNI场景接入方案

注意:如果SLSendpoint使用的是HTTPS域名,则您必须参考 HTTPS+SNI场景接入方案。
  1. 自定义NSURLProtocol实现。

    #import <Foundation/Foundation.h>
    #import "HttpDnsNSURLProtocolImpl.h"
    #import <arpa/inet.h>
    #import <zlib.h>
    #import <objc/runtime.h>
    
    static NSString *const hasBeenInterceptedCustomLabelKey = @"HttpDnsHttpMessagePropertyKey";
    static NSString *const kAnchorAlreadyAdded = @"AnchorAlreadyAdded";
    
    @interface HttpDnsNSURLProtocolImpl () <NSStreamDelegate>
    
    @property (strong, readwrite, nonatomic) NSMutableURLRequest *curRequest;
    @property (strong, readwrite, nonatomic) NSRunLoop *curRunLoop;
    @property (strong, readwrite, nonatomic) NSInputStream *inputStream;
    @property (nonatomic, assign) BOOL responseIsHandle;
    @property (assign, nonatomic) z_stream gzipStream;
    
    @end
    
    @implementation HttpDnsNSURLProtocolImpl
    
    - (instancetype)initWithRequest:(NSURLRequest *)request cachedResponse:(nullable NSCachedURLResponse *)cachedResponse client:(nullable id <NSURLProtocolClient>)client {
      self = [super initWithRequest:request cachedResponse:cachedResponse client:client];
      if (self) {
        _gzipStream.zalloc = Z_NULL;
        _gzipStream.zfree = Z_NULL;
        if (inflateInit2(&_gzipStream, 16 + MAX_WBITS) != Z_OK) {
          [self.client URLProtocol:self didFailWithError:[[NSError alloc] initWithDomain:@"gzip initialize fail" code:-1 userInfo:nil]];
        }
      }
      return self;
    }
    
    /**
     *  是否拦截处理指定的请求
     *
     *  @param request 指定的请求
     *  @return 返回YES表示要拦截处理,返回NO表示不拦截处理
     */
    + (BOOL)canInitWithRequest:(NSURLRequest *)request {
      if([[request.URL absoluteString] isEqual:@"about:blank"]) {
        return NO;
      }
    
      // 防止无限循环,因为一个请求在被拦截处理过程中,也会发起一个请求,这样又会走到这里,如果不进行处理,就会造成无限循环
      if ([NSURLProtocol propertyForKey:hasBeenInterceptedCustomLabelKey inRequest:request]) {
        return NO;
      }
    
      NSString * url = request.URL.absoluteString;
      NSString * domain = request.URL.host;
    
      // 只有https需要拦截
      if (![url hasPrefix:@"https"]) {
        return NO;
      }
    
      // 需要的话可以做更多判断,如配置一个host数组来限制只有这个数组中的host才需要拦截
    
      // 只有已经替换为ip的请求需要拦截
      if (![self isPlainIpAddress:domain]) {
        return NO;
      }
      return YES;
    }
    
    + (BOOL)isPlainIpAddress:(NSString *)hostStr {
      if (!hostStr) {
        return NO;
      }
    
      // 是否ipv4地址
      const char *utf8 = [hostStr UTF8String];
      int success = 0;
      struct in_addr dst;
      success = inet_pton(AF_INET, utf8, &dst);
      if (success == 1) {
        return YES;
      }
    
      // 是否ipv6地址
      struct in6_addr dst6;
      success = inet_pton(AF_INET6, utf8, &dst6);
      if (success == 1) {
        return YES;
      }
    
      return NO;
    }
    
    // 如果需要对请求进行重定向,添加指定头部等操作,可以在该方法中进行
    + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
      return request;
    }
    
    // 开始加载,在该方法中,加载一个请求
    - (void)startLoading {
      NSMutableURLRequest *request = [self.request mutableCopy];
      // 表示该请求已经被处理,防止无限循环
      [NSURLProtocol setProperty:@(YES) forKey:hasBeenInterceptedCustomLabelKey inRequest:request];
      self.curRequest = [self createNewRequest:request];
      [self startRequest];
    }
    
    - (NSString *)cookieForURL:(NSURL *)URL {
      NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
      NSMutableArray *cookieList = [NSMutableArray array];
      for (NSHTTPCookie *cookie in [cookieStorage cookies]) {
        if (![self p_checkCookie:cookie URL:URL]) {
          continue;
        }
        [cookieList addObject:cookie];
      }
    
      if (cookieList.count > 0) {
        NSDictionary *cookieDic = [NSHTTPCookie requestHeaderFieldsWithCookies:cookieList];
        if ([cookieDic objectForKey:@"Cookie"]) {
          return cookieDic[@"Cookie"];
        }
      }
      return nil;
    }
    
    
    - (BOOL)p_checkCookie:(NSHTTPCookie *)cookie URL:(NSURL *)URL {
      if (cookie.domain.length <= 0 || URL.host.length <= 0) {
        return NO;
      }
      if ([URL.host containsString:cookie.domain]) {
        return YES;
      }
      return NO;
    }
    
    - (NSMutableURLRequest *)createNewRequest:(NSURLRequest*)request {
      NSURL* originUrl = request.URL;
      NSString *cookie = [self cookieForURL:originUrl];
    
      NSMutableURLRequest* mutableRequest = [request copy];
      [mutableRequest setValue:cookie forHTTPHeaderField:@"Cookie"];
    
      return [mutableRequest copy];
    }
    
    /**
     * 取消请求
     */
    - (void)stopLoading {
      if (_inputStream.streamStatus == NSStreamStatusOpen) {
        [self closeStream:_inputStream];
      }
      [self.client URLProtocol:self didFailWithError:[[NSError alloc] initWithDomain:@"stop loading" code:-1 userInfo:nil]];
    }
    
    /**
     * 使用CFHTTPMessage转发请求
     */
    - (void)startRequest {
      // 原请求的header信息
      NSDictionary *headFields = _curRequest.allHTTPHeaderFields;
      CFStringRef url = (__bridge CFStringRef) [_curRequest.URL absoluteString];
      CFURLRef requestURL = CFURLCreateWithString(kCFAllocatorDefault, url, NULL);
    
      // 原请求所使用的方法,GETPOST
      CFStringRef requestMethod = (__bridge_retained CFStringRef) _curRequest.HTTPMethod;
    
      // 根据请求的url、方法、版本创建CFHTTPMessageRef对象
      CFHTTPMessageRef cfrequest = CFHTTPMessageCreateRequest(kCFAllocatorDefault, requestMethod, requestURL, kCFHTTPVersion1_1);
    
      // 添加http post请求所附带的数据
      CFStringRef requestBody = CFSTR("");
      CFDataRef bodyData = CFStringCreateExternalRepresentation(kCFAllocatorDefault, requestBody, kCFStringEncodingUTF8, 0);
    
      if (_curRequest.HTTPBody) {
        bodyData = (__bridge_retained CFDataRef) _curRequest.HTTPBody;
      }  else if (_curRequest.HTTPBodyStream) {
        NSData *data = [self dataWithInputStream:_curRequest.HTTPBodyStream];
        NSString *strBody = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        NSLog(@"originStrBody: %@", strBody);
    
        CFDataRef body = (__bridge_retained CFDataRef) data;
        CFHTTPMessageSetBody(cfrequest, body);
        CFRelease(body);
      } else {
        CFHTTPMessageSetBody(cfrequest, bodyData);
      }
    
      // copy原请求的header信息
      for (NSString* header in headFields) {
        CFStringRef requestHeader = (__bridge CFStringRef) header;
        CFStringRef requestHeaderValue = (__bridge CFStringRef) [headFields valueForKey:header];
        CFHTTPMessageSetHeaderFieldValue(cfrequest, requestHeader, requestHeaderValue);
      }
    
      // 创建CFHTTPMessage对象的输入流
      #pragma clang diagnostic push
      #pragma clang diagnostic ignored "-Wdeprecated-declarations"
      CFReadStreamRef readStream = CFReadStreamCreateForHTTPRequest(kCFAllocatorDefault, cfrequest);
      #pragma clang diagnostic pop
      self.inputStream = (__bridge_transfer NSInputStream *) readStream;
    
      // 设置SNI host信息,关键步骤
      NSString *host = [_curRequest.allHTTPHeaderFields objectForKey:@"host"];
      if (!host) {
        host = _curRequest.URL.host;
      }
    
      [_inputStream setProperty:NSStreamSocketSecurityLevelNegotiatedSSL forKey:NSStreamSocketSecurityLevelKey];
      NSDictionary *sslProperties = [[NSDictionary alloc] initWithObjectsAndKeys: host, (__bridge id) kCFStreamSSLPeerName, nil];
      [_inputStream setProperty:sslProperties forKey:(__bridge_transfer NSString *) kCFStreamPropertySSLSettings];
      [_inputStream setDelegate:self];
    
      if (!_curRunLoop) {
        // 保存当前线程的runloop,这对于重定向的请求很关键
        self.curRunLoop = [NSRunLoop currentRunLoop];
      }
      // 将请求放入当前runloop的事件队列
      [_inputStream scheduleInRunLoop:_curRunLoop forMode:NSRunLoopCommonModes];
      [_inputStream open];
    
      CFRelease(cfrequest);
      CFRelease(requestURL);
      cfrequest = NULL;
      CFRelease(bodyData);
      CFRelease(requestBody);
      CFRelease(requestMethod);
    }
    
    - (NSData*)dataWithInputStream:(NSInputStream*)stream {
      NSMutableData *data = [NSMutableData data];
      [stream open];
      NSInteger result;
      uint8_t buffer[1024];
    
      while ((result = [stream read:buffer maxLength:1024]) != 0) {
        if (result > 0) {
          // buffer contains result bytes of data to be handled
          [data appendBytes:buffer length:result];
        } else if (result < 0) {
          // The stream had an error. You can get an NSError object using [iStream streamError]
          data = nil;
          break;
        }
      }
      [stream close];
      return data;
    }
    
    #pragma mark - NSStreamDelegate
    /**
     * input stream 收到header complete后的回调函数
     */
    - (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode {
      if (eventCode == NSStreamEventHasBytesAvailable) {
        CFReadStreamRef readStream = (__bridge_retained CFReadStreamRef) aStream;
        #pragma clang diagnostic push
        #pragma clang diagnostic ignored "-Wdeprecated-declarations"
        CFHTTPMessageRef message = (CFHTTPMessageRef) CFReadStreamCopyProperty(readStream, kCFStreamPropertyHTTPResponseHeader);
        #pragma clang diagnostic pop
        if (CFHTTPMessageIsHeaderComplete(message)) {
          NSInputStream *inputstream = (NSInputStream *) aStream;
          NSNumber *alreadyAdded = objc_getAssociatedObject(aStream, (__bridge const void *)(kAnchorAlreadyAdded));
          NSDictionary *headDict = (__bridge NSDictionary *) (CFHTTPMessageCopyAllHeaderFields(message));
    
          if (!alreadyAdded || ![alreadyAdded boolValue]) {
            objc_setAssociatedObject(aStream, (__bridge const void *)(kAnchorAlreadyAdded), [NSNumber numberWithBool:YES], OBJC_ASSOCIATION_COPY);
            // 通知client已收到response,只通知一次
    
            CFStringRef httpVersion = CFHTTPMessageCopyVersion(message);
            // 获取响应头部的状态码
            CFIndex statusCode = CFHTTPMessageGetResponseStatusCode(message);
    
            if (!self.responseIsHandle) {
              NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:_curRequest.URL statusCode:statusCode
                                                       HTTPVersion:(__bridge NSString *) httpVersion headerFields:headDict];
              [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
              self.responseIsHandle = YES;
            }
    
            // 验证证书
            SecTrustRef trust = (__bridge SecTrustRef) [aStream propertyForKey:(__bridge NSString *) kCFStreamPropertySSLPeerTrust];
            SecTrustResultType res = kSecTrustResultInvalid;
            NSMutableArray *policies = [NSMutableArray array];
            NSString *domain = [[_curRequest allHTTPHeaderFields] valueForKey:@"host"];
            if (domain) {
              [policies addObject:(__bridge_transfer id) SecPolicyCreateSSL(true, (__bridge CFStringRef) domain)];
            } else {
              [policies addObject:(__bridge_transfer id) SecPolicyCreateBasicX509()];
            }
    
            // 绑定校验策略到服务端的证书上
            SecTrustSetPolicies(trust, (__bridge CFArrayRef) policies);
            if (SecTrustEvaluate(trust, &res) != errSecSuccess) {
              [self closeStream:aStream];
              [self.client URLProtocol:self didFailWithError:[[NSError alloc] initWithDomain:@"can not evaluate the server trust" code:-1 userInfo:nil]];
              return;
            }
            if (res != kSecTrustResultProceed && res != kSecTrustResultUnspecified) {
              // 证书验证不通过,关闭input stream
              [self closeStream:aStream];
              [self.client URLProtocol:self didFailWithError:[[NSError alloc] initWithDomain:@"fail to evaluate the server trust" code:-1 userInfo:nil]];
            } else {
              // 证书校验通过
              if (statusCode >= 300 && statusCode < 400) {
                // 处理重定向错误码
                [self closeStream:aStream];
                [self handleRedirect:message];
              } else {
                NSError *error = nil;
                NSData *data = [self readDataFromInputStream:inputstream headerDict:headDict stream:aStream error:&error];
                if (error) {
                  [self.client URLProtocol:self didFailWithError:error];
                } else {
                  [self.client URLProtocol:self didLoadData:data];
                }
              }
            }
          } else {
            NSError *error = nil;
            NSData *data = [self readDataFromInputStream:inputstream headerDict:headDict stream:aStream error:&error];
            if (error) {
              [self.client URLProtocol:self didFailWithError:error];
            } else {
              [self.client URLProtocol:self didLoadData:data];
            }
          }
          CFRelease((CFReadStreamRef)inputstream);
          CFRelease(message);
        }
      } else if (eventCode == NSStreamEventErrorOccurred) {
        [self closeStream:aStream];
        inflateEnd(&_gzipStream);
        // 通知client发生错误了
        [self.client URLProtocol:self didFailWithError:
             [[NSError alloc] initWithDomain:@"NSStreamEventErrorOccurred" code:-1 userInfo:nil]];
      } else if (eventCode == NSStreamEventEndEncountered) {
        CFReadStreamRef readStream = (__bridge_retained CFReadStreamRef) aStream;
        #pragma clang diagnostic push
        #pragma clang diagnostic ignored "-Wdeprecated-declarations"
        CFHTTPMessageRef message = (CFHTTPMessageRef) CFReadStreamCopyProperty(readStream, kCFStreamPropertyHTTPResponseHeader);
        #pragma clang diagnostic pop
        if (CFHTTPMessageIsHeaderComplete(message)) {
          NSNumber *alreadyAdded = objc_getAssociatedObject(aStream, (__bridge const void *)(kAnchorAlreadyAdded));
          NSDictionary *headDict = (__bridge NSDictionary *) (CFHTTPMessageCopyAllHeaderFields(message));
    
          if (!alreadyAdded || ![alreadyAdded boolValue]) {
            objc_setAssociatedObject(aStream, (__bridge const void *)(kAnchorAlreadyAdded), [NSNumber numberWithBool:YES], OBJC_ASSOCIATION_COPY);
            // 通知client已收到response,只通知一次
    
            if (!self.responseIsHandle) {
              CFStringRef httpVersion = CFHTTPMessageCopyVersion(message);
              // 获取响应头部的状态码
              CFIndex statusCode = CFHTTPMessageGetResponseStatusCode(message);
              NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:_curRequest.URL statusCode:statusCode
                                                       HTTPVersion:(__bridge NSString *) httpVersion headerFields:headDict];
              [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
              self.responseIsHandle = YES;
            }
          }
        }
    
        [self closeStream:_inputStream];
        inflateEnd(&_gzipStream);
        [self.client URLProtocolDidFinishLoading:self];
      }
    }
    
    - (NSData *)readDataFromInputStream:(NSInputStream *)inputStream headerDict:(NSDictionary *)headDict stream:(NSStream *)aStream error:(NSError **)error {
      // 以防responseheader信息不完整
      UInt8 buffer[16 * 1024];
    
      NSInteger length = [inputStream read:buffer maxLength:sizeof(buffer)];
      if (length < 0) {
        *error = [[NSError alloc] initWithDomain:@"inputstream length is invalid"
                      code:-2
                      userInfo:nil];
        [aStream removeFromRunLoop:_curRunLoop forMode:NSRunLoopCommonModes];
        [aStream setDelegate:nil];
        [aStream close];
        return nil;
      }
    
      NSData *data = [[NSData alloc] initWithBytes:buffer length:length];
      if (headDict[@"Content-Encoding"] && [headDict[@"Content-Encoding"] containsString:@"gzip"]) {
        data = [self gzipUncompress:data];
        if (!data) {
          *error = [[NSError alloc] initWithDomain:@"can't read any data"
                          code:-3
                          userInfo:nil];
          return nil;
        }
      }
    
      return data;
    }
    
    - (void)closeStream:(NSStream*)stream {
      [stream removeFromRunLoop:_curRunLoop forMode:NSRunLoopCommonModes];
      [stream setDelegate:nil];
      [stream close];
    }
    
    - (void)handleRedirect:(CFHTTPMessageRef)messageRef {
      // 响应头
      CFDictionaryRef headerFieldsRef = CFHTTPMessageCopyAllHeaderFields(messageRef);
      NSDictionary *headDict = (__bridge_transfer NSDictionary *)headerFieldsRef;
      [self redirect:headDict];
    }
    
    - (void)redirect:(NSDictionary *)headDict {
      // 重定向时如果有cookie需求的话,注意处理
      NSString *location = headDict[@"Location"];
      if (!location)
        location = headDict[@"location"];
      NSURL *url = [[NSURL alloc] initWithString:location];
      _curRequest.URL = url;
      if ([[_curRequest.HTTPMethod lowercaseString] isEqualToString:@"post"]) {
        // 根据RFC文档,当重定向请求为POST请求时,要将其转换为GET请求
        _curRequest.HTTPMethod = @"GET";
        _curRequest.HTTPBody = nil;
      }
      [self startRequest];
    }
    
    - (NSData *)gzipUncompress:(NSData *)gzippedData {
      if ([gzippedData length] == 0) {
        return gzippedData;
      }
    
      unsigned full_length = (unsigned) [gzippedData length];
      unsigned half_length = (unsigned) [gzippedData length] / 2;
    
      NSMutableData *decompressed = [NSMutableData dataWithLength:full_length + half_length];
      BOOL done = NO;
      int status;
      _gzipStream.next_in = (Bytef *)[gzippedData bytes];
      _gzipStream.avail_in = (uInt)[gzippedData length];
      _gzipStream.total_out = 0;
    
      while (_gzipStream.avail_in != 0 && !done) {
        if (_gzipStream.total_out >= [decompressed length]) {
          [decompressed increaseLengthBy:half_length];
        }
    
        _gzipStream.next_out = (Bytef *)[decompressed mutableBytes] + _gzipStream.total_out;
        _gzipStream.avail_out = (uInt)([decompressed length] - _gzipStream.total_out);
    
        status = inflate(&_gzipStream, Z_SYNC_FLUSH);
    
        if (status == Z_STREAM_END) {
          done = YES;
        } else if (status == Z_BUF_ERROR) {
          // 假如Z_BUF_ERROR是由于输出缓冲区不够大引起的,那么应该满足输入缓冲区未处理完,且输出缓冲区已填满
          // 即满足_gzipStream.avail_in != 0 && _gzipStream.avail_out == 0,此时应该继续循环进行扩容
          // 对于取反的条件,说明不是由于输出缓冲区不够大引起的,那么此时应该结束循环,代表出现了error
          if (_gzipStream.avail_in == 0 || _gzipStream.avail_out != 0) {
            return nil;
          }
        } else if (status != Z_OK) {
                return nil;
            }
        }
    
        [decompressed setLength:_gzipStream.total_out];
        return [NSData dataWithData:decompressed];
    }
    
    @end
    
  2. 实现自定义 BeforeSend。

    - (void)setupHttpDNS:(NSString *)accountId {
      // 配置 HttpDNS
      HttpDnsService *httpdns = [[HttpDnsService alloc] initWithAccountID:accountId];
      [httpdns setHTTPSRequestEnabled:YES];
      [httpdns setPersistentCacheIPEnabled:YES];
      [httpdns setReuseExpiredIPEnabled:YES];
      [httpdns setIPv6Enabled:YES];
    
      NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
    
      NSMutableArray *protocolsArray = [NSMutableArray arrayWithArray:configuration.protocolClasses];
      // 设置上文自定义的 NSURLProtocol
      [protocolsArray insertObject:[HttpDnsNSURLProtocolImpl class] atIndex:0];
      [configuration setProtocolClasses:protocolsArray];
      NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:nil delegateQueue:nil];
    
      [SLSURLSession setURLSession:session];
      [SLSURLSession setBeforeSend:^NSMutableURLRequest * _Nonnull(NSMutableURLRequest * _Nonnull request) {
        NSURL *url = request.URL;
        // 这里可以加个判断,仅对需要的url生效
        
        HttpdnsResult *result = [httpdns resolveHostSync:url.host byIpType:HttpdnsQueryIPTypeAuto];
        if (!result) {
          return request;
        }
    
        NSString *ipAddress = nil;
        if (result.hasIpv4Address) {
          ipAddress = result.firstIpv4Address;
        } else if(result.hasIpv6Address) {
          ipAddress = result.firstIpv6Address;
        } else {
          return request;
        }
    
        NSString *requestUrl = url.absoluteString;
        requestUrl = [requestUrl stringByReplacingOccurrencesOfString: url.host withString:ipAddress];
    
        [request setURL:[NSURL URLWithString:requestUrl]];
        [request setValue:url.host forHTTPHeaderField:@"host"];
    
        return request;
      }];
    }
  3. 完成SLS SDK初始化。

    @implementation AliyunSLS
    
    - (void)initSLS {
      // 给SLS SDK设置自定义 HttpDNS
      // !!!注意!!!
      // 该设置会对所有SLS SDK的示例生效
      [self setupHttpDNS:accountId];
      [self initProducer];
    }
    
    - (void)initProducer {
        // 这里正常实现 LogProducerConfig 和 LogProducerClient 的初始化,与之前保持一致即可。
        // ...
    }
    
    @end