iOS SDK quick start

更新时间:
复制 MD 格式

This guide shows you how to quickly use the Simple Log Service SDK for iOS to collect log data.

Prerequisites

You have installed the iOS SDK. For details, see Install the iOS SDK.

Quick start

Initialize the SDK and call the addLog method to send logs.

Important
  • The iOS SDK supports initializing multiple instances. The LogProducerConfig and LogProducerClient instances must be used in pairs.

  • When you send logs to Simple Log Service, you must use the AccessKey pair of an Alibaba Cloud account or a RAM user to authenticate requests and prevent tampering. To prevent security risks from exposed credentials, do not hard-code the AccessKey pair in your mobile application. Instead, we recommend using the direct log transfer service for mobile devices to configure the AccessKey pair. For more information, see Build a service to upload logs from mobile devices to Simple Log Service.

@interface ProducerExampleController ()
// We recommend keeping the LogProducerConfig and LogProducerClient instances as global properties.
@property(nonatomic, strong) LogProducerConfig *config;
@property(nonatomic, strong) LogProducerClient *client;
@end

@implementation ProducerExampleController


// The callback is optional. Use it to track the log sending status.
// To configure the AccessKey pair dynamically, set a callback to update it when invoked.
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 {
    // The service endpoint. The value must begin with https:// or http://.
    NSString *endpoint = @"";
    NSString *project = @"";
    NSString *logstore = @"";

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

    // Set a log topic.
    [_config SetTopic:@"example_topic"];
    // Add a global tag, which is attached to all logs.
    [_config AddTag:@"example" value:@"example_tag"];
    // Specifies whether to drop expired logs. 0: Keep the logs and update their timestamps to the current time. 1 (Default): Drop the logs.
    [_config SetDropDelayLog:1];
    // Specifies whether to drop logs that fail authentication. 0 (Default): Keep the logs. 1: Drop the logs.
    [_config SetDropUnauthorizedLog:0];    

    // To monitor the log sending status, pass a callback.
    _client = [[LogProducerClient alloc] initWithLogProducerConfig:_config callback:_on_log_send_done];
}

// Request the AccessKey pair.
- (void) requestAccessKey {
    // We recommend using the direct log transfer service for mobile devices to configure the AccessKey pair.
    // ...

    // After obtaining the AccessKey pair, call this method to update it.
    [self updateAccessKey:accessKeyId accessKeySecret:accessKeySecret securityToken:securityToken];
}

// Update the AccessKey pair.
- (void) updateAccessKey:(NSString *)accessKeyId accessKeySecret:(NSString *)accessKeySecret securityToken:(NSString *)securityToken {
    
    // For an AccessKey pair from Security Token Service (STS) that includes a security token, use this method to update the credentials.
    if (securityToken.length > 0) {
        if (accessKeyId.length > 0 && accessKeySecret.length > 0) {
            [_config ResetSecurityToken:accessKeyId
                        accessKeySecret:accessKeySecret
                          securityToken:securityToken
            ];
        }
    } else {
        // For an AccessKey pair that does not use a security token, use this method.
        if (accessKeyId.length > 0 && accessKeySecret.length > 0) {
            [_config setAccessKeyId: accessKeyId];
            [_config setAccessKeySecret: accessKeySecret];
        }
    }
}
// Send a log.
- (void) addLog {
    Log *log = [Log log];
    // Customize the log fields based on your business requirements.
    [log putContent:@"content_key_1" intValue:123456];
    [log putContent:@"content_key_2" floatValue:23.34f];
    [log putContent:@"content_key_3" value:@"Chinese characters"];
    
    [_client AddLog:log];
}
@end

Advanced usage

Dynamically configure parameters

The iOS SDK allows you to dynamically configure parameters such as the Project, Logstore, endpoint, and AccessKey pair. For more information about obtaining an endpoint, see Endpoints. For more information about obtaining an AccessKey pair, see AccessKey pair.

  • Dynamically configure the endpoint, Project, and Logstore.

    // You can configure the endpoint, Project, and Logstore parameters independently or together.
    // Update the endpoint.
    [_config setEndpoint:@"your new-endpoint"];
    // Update the Project.
    [_config setProject:@"your new-project"];
    // Update the Logstore.
    [_config setLogstore:@"your new-logstore"];
  • Dynamically configure an AccessKey pair.

    When dynamically configuring an AccessKey pair, we recommend using a callback.

    // If you have initialized the callback when you initialize LogProducerClient, you can ignore the following code.
    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 is a reference to the current class instance, captured for use in the C-style callback.
        }
    }
    
    // If you want to check the send status of logs, pass a callback as the second parameter.
    _client = [[LogProducerClient alloc] initWithLogProducerConfig:_config callback:_on_log_send_done];
    
    // Request the AccessKey pair.
    - (void) requestAccessKey {
        // We recommend that you first use the service for direct log transfer from mobile devices to configure the AccessKey pair.
        // ...
    
        // After you obtain the AccessKey pair, update the credentials.
        [self updateAccessKey:accessKeyId accessKeySecret:accessKeySecret securityToken:securityToken];
    }
    
    // Update the AccessKey pair.
    - (void) updateAccessKey:(NSString *)accessKeyId accessKeySecret:(NSString *)accessKeySecret securityToken:(NSString *)securityToken {
        
        // If you obtain an AccessKey pair by using STS, the AccessKey pair contains a security token. In this case, you must update the AccessKey pair in the following way.
        if (securityToken.length > 0) {
            if (accessKeyId.length > 0 && accessKeySecret.length > 0) {
                [_config ResetSecurityToken:accessKeyId
                            accessKeySecret:accessKeySecret
                              securityToken:securityToken
                ];
            }
        } else {
            // If you do not obtain an AccessKey pair by using STS, update the AccessKey pair in the following way.
            if (accessKeyId.length > 0 && accessKeySecret.length > 0) {
                [_config setAccessKeyId: accessKeyId];
                [_config setAccessKeySecret: accessKeySecret];
            }
        }
    }
  • Dynamically configure the log source, log topic, and tag.

    Important

    You cannot configure log source, log topic, and tags for individual logs. These settings are global and apply to all unsent logs, including those in the retry buffer. This global application can cause unexpected behavior if you use these parameters to track specific log categories. To track categories, we recommend adding a custom field to each log instead.

    // Set the log topic.
    [_config SetTopic:@"your new-topic"];
    // Set the log source.
    [_config SetSource:@"your new-source"];
    // Set tags. The tags are attached to each log.
    [_config AddTag:@"test" value:@"your new-tag"];

Resumable upload

The iOS SDK supports resumable upload. When enabled, this feature persists logs submitted with the addLog method to a local binary log (binlog) file. The SDK deletes this local data only after successfully sending the logs, ensuring at-least-once delivery.

To enable resumable upload, add the following code during SDK initialization.

Important
  • When initializing multiple LogProducerConfig instances, you must provide a unique file path to the setPersistentFilePath method for each LogProducerConfig instance.

  • If your app uses multiple processes with resumable upload enabled, initialize the SDK only in the main process. If child processes also need to collect data, ensure the file path provided to the SetPersistentFilePath method is unique for each process. Otherwise, log data may become disordered or lost.

  • Repeatedly initializing LogProducerConfig from multiple threads can cause issues.

- (void) initLogProducer {
    // A value of 1 enables resumable upload, and 0 disables it. The default value is 0.
    [_config SetPersistent:1];
    
    // The name of the persistence file. Ensure that the directory for this file exists.
    NSArray  *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *Path = [[paths lastObject] stringByAppendingString:@"/log.dat"];
    [_config SetPersistentFilePath:Path];
    // The number of persistent files to roll over. We recommend a value of 10.
    [_config SetPersistentMaxFileCount:10];
    // The size of each persistent file in bytes, specified as N*1024*1024. We recommend a value for N from 1 to 10.
    [_config SetPersistentMaxFileSize:N*1024*1024];
    // The maximum number of logs to cache locally. Do not exceed 1,048,576. The default is 65,536.
    [_config SetPersistentMaxLogCount:65536];
}

Configuration parameters

All configuration parameters are provided by the LogProducerConfig class. The following table describes these parameters.

Parameter

Type

Description

SetTopic

String

Specifies the value for the topic field. The default value is an empty string.

AddTag

String

Specifies a tag for a log in the tag:xxxx format. The default value is an empty string.

SetSource

String

Specifies the value for the source field. The default value is iOS.

SetPacketLogBytes

Int

Specifies the maximum size of a log packet in the buffer. Packets exceeding this limit are sent immediately.

Value range: 1 to 5,242,880 bytes. Default: 1,048,576 bytes (1 MiB).

SetPacketLogCount

Int

Specifies the maximum number of logs per packet. If this limit is exceeded, the log packet is sent immediately.

Value range: 1 to 4,096. Default: 1,024.

SetPacketTimeout

Int

Specifies the maximum time a log packet can remain in the buffer. The packet is sent immediately when this timeout is reached.

Default: 3,000 milliseconds.

SetMaxBufferLimit

Int

Specifies the maximum buffer size in bytes for a producer instance. If this limit is exceeded, calls to add_log fail immediately.

Default: 67,108,864 (64 MiB).

SetPersistent

Int

Enables or disables the resumable upload feature.

  • 1: Enabled.

  • 0 (default): Disabled.

SetPersistentFilePath

String

Specifies the file path for data persistence. The directory must exist. Each LogProducerConfig instance requires a unique file path.

The default value is an empty string.

SetPersistentForceFlush

Int

Specifies whether to force a flush to the persistence file after every AddLog call.

  • 1: Enabled. Enabling this option may impact performance and should be used with caution.

  • 0 (default): Disabled.

We recommend this option for high-reliability scenarios.

SetPersistentMaxFileCount

Int

Specifies the maximum number of persistence files for rotation. A value of 0 disables rotation and uses a single file. Recommended range: 1 to 10. Default: 0.

SetPersistentMaxFileSize

Int

Specifies the maximum size of each persistence file, in bytes. The recommended value is between 1,048,576 bytes (1 MiB) and 10,485,760 bytes (10 MiB).

SetPersistentMaxLogCount

Int

Specifies the maximum number of logs that can be stored locally for persistence. The value should not exceed 1,048,576. Default: 65,536.

SetConnectTimeoutSec

Int

Specifies the network connection timeout, in seconds. The default value is 10.

SetSendTimeoutSec

Int

Specifies the timeout for sending a log packet, in seconds. The default value is 15.

SetDestroyFlusherWaitSec

Int

Specifies the maximum wait time in seconds for the flusher thread to be destroyed. The default value is 1.

SetDestroySenderWaitSec

Int

Specifies the maximum wait time in seconds for the sender thread pool to be destroyed. The default value is 1.

SetCompressType

Int

Specifies the data compression type for uploads.

  • 0: No compression.

  • 1 (default): LZ4 compression.

SetNtpTimeOffset

Int

Specifies the offset between the device time and standard NTP time, in seconds. This is calculated as Standard time - Device time and is used to correct discrepancies from unsynchronized client clocks. The default value is 0.

SetMaxLogDelayTime

Int

Specifies the maximum allowed time difference between a log's timestamp and the device's local time, in seconds. If this difference is exceeded, the log is processed based on the SetDropDelayLog setting. The default value is 604,800 (7 24 3600), which represents 7 days.

SetDropDelayLog

Int

Specifies whether to drop logs that exceed the SetMaxLogDelayTime limit.

  • 0: Do not drop. The log's timestamp is updated to the current time.

  • 1 (default): Drop the log.

SetDropUnauthorizedLog

Int

Specifies whether to drop logs that fail authentication.

  • 0 (default): Do not drop.

  • 1: Drop the log.

Error codes

All error codes are defined in log_producer_result. The following table describes each error code and its solution.

Error code

Value

Description

Solution

LOG_PRODUCER_OK

0

Success.

N/A.

LOG_PRODUCER_INVALID

1

The SDK has been destroyed or is invalid.

  1. Ensure the SDK is initialized correctly.

  2. Check if the destroy() method was called.

LOG_PRODUCER_WRITE_ERROR

2

Failed to write data. This error can occur if the write traffic for the Project has reached its limit.

Increase the write traffic limit for the Project. For more information, see Adjust resource quotas.

LOG_PRODUCER_DROP_ERROR

3

The disk or memory cache is full.

Increase the values of the maxBufferLimit, persistentMaxLogCount, and persistentMaxFileSize parameters, and then retry.

LOG_PRODUCER_SEND_NETWORK_ERROR

4

A network error occurred.

Verify the Endpoint, Project, and Logstore configurations.

LOG_PRODUCER_SEND_QUOTA_ERROR

5

The write traffic quota for the Project has been exceeded.

Increase the write traffic limit for the Project. For more information, see Adjust resource quotas.

LOG_PRODUCER_SEND_UNAUTHORIZED

6

The AccessKey is expired, invalid, or has an incorrectly configured access policy.

Verify the AccessKey.

The RAM user must have permissions to manage Simple Log Service resources. For more information, see Grant permissions to a RAM user.

LOG_PRODUCER_SEND_SERVER_ERROR

7

A server-side error occurred.

Contact technical support.

LOG_PRODUCER_SEND_DISCARD_ERROR

8

Data was discarded, usually due to clock skew between the device and the server.

The SDK automatically retries the request.

LOG_PRODUCER_SEND_TIME_ERROR

9

The device clock is not synchronized with the server time.

The SDK automatically corrects this issue.

LOG_PRODUCER_SEND_EXIT_BUFFERED

10

The SDK was destroyed before all cached data could be sent.

To prevent data loss, enable the resumable upload feature.

LOG_PRODUCER_PARAMETERS_INVALID

11

Invalid SDK initialization parameters.

Verify parameters such as AccessKey, Endpoint, Project, and Logstore.

LOG_PRODUCER_PERSISTENT_ERROR

99

Failed to write cached data to the disk.

1. Ensure the cache file path is correct.

2. Check if the cache file is full.

3. Ensure sufficient disk space is available.

FAQ

Why do duplicate logs occur?

The iOS SDK sends logs asynchronously. Network issues can cause transmission failures and retries. The SDK confirms delivery only upon receiving a 200 status code. This process can lead to duplicate logs. Use SQL queries to deduplicate data during analysis.

If you observe a high rate of duplicate logs, check for issues in the SDK initialization. Common causes and their solutions include:

  • Incorrect resumable upload configuration

    To resolve this, verify that the file path passed to the SetPersistentFilePath method is globally unique.

  • Repeated SDK initialization

    • Repeated SDK initialization is often caused by failing to use the singleton pattern correctly. Initialize the SDK as shown in the following example.

      // 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 {
          // Replace with your initialization code.
          _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
    • Using multiple processes can also cause repeated initialization. Initialize the SDK only in the main process. If you must initialize the SDK across different processes, ensure uniqueness by setting a different value for SetPersistentFilePath in each process.

  • Optimizing the configuration for weak network environments

    If your application operates in a weak network environment, optimize the SDK configuration as shown below.

    // Initialize the SDK.
    - (void) initProducer() {
        // Adjust the HTTP connection and send timeouts to help reduce the rate of duplicate logs.
        // You can adjust the timeout values as needed.
        [_config SetConnectTimeoutSec:20];
        [_config SetSendTimeoutSec:20];
      
        // Other initialization parameters.
        // ...
    }

Handling log loss

Log reporting is an asynchronous process. If the application closes before reporting completes, log loss can occur. We recommend enabling the resumable upload feature. For more information, see Resumable upload.

Handling log reporting delays

Network conditions and specific application use cases can delay log reporting. If log reporting delays occur on only a few devices, this is typically normal. Otherwise, troubleshoot the issue by using the error codes in the following table.

Error code

Description

LOG_PRODUCER_SEND_NETWORK_ERROR

Verify that the Endpoint, Project, and Logstore are configured correctly.

LOG_PRODUCER_SEND_UNAUTHORIZED

Verify that the access key is valid, has not expired, and that its associated permission policy is configured correctly.

LOG_PRODUCER_SEND_QUOTA_ERROR

The write traffic quota for the Project has been exceeded. For more information, see Adjust resource quotas.

DNS pre-resolution and cache policies

Yes. The following example shows how to use the iOS SDK with the HTTPDNS SDK to implement DNS pre-resolution and cache policies. For more information, see Solutions for HTTPS and SNI scenarios.

Note: If the SLS Endpoint uses an HTTPS domain name, you must refer to Solutions for HTTPS and SNI scenarios.
  1. Implement a custom 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;
    }
    
    /**
     *  Specifies whether to intercept and process the specified request.
     *
     *  @param request The specified request.
     *  @return YES to intercept the request, or NO to pass it through.
     */
    + (BOOL)canInitWithRequest:(NSURLRequest *)request {
      if([[request.URL absoluteString] isEqual:@"about:blank"]) {
        return NO;
      }
    
      // Prevents an infinite loop that can occur if a request is re-intercepted.
      if ([NSURLProtocol propertyForKey:hasBeenInterceptedCustomLabelKey inRequest:request]) {
        return NO;
      }
    
      NSString * url = request.URL.absoluteString;
      NSString * domain = request.URL.host;
    
      // Intercept only HTTPS requests.
      if (![url hasPrefix:@"https"]) {
        return NO;
      }
    
      // You can add more conditions, such as configuring a host array to intercept only the hosts in that array.
    
      // Intercept only requests whose hosts are replaced with IP addresses.
      if (![self isPlainIpAddress:domain]) {
        return NO;
      }
      return YES;
    }
    
    + (BOOL)isPlainIpAddress:(NSString *)hostStr {
      if (!hostStr) {
        return NO;
      }
    
      // Checks whether the address is an IPv4 address.
      const char *utf8 = [hostStr UTF8String];
      int success = 0;
      struct in_addr dst;
      success = inet_pton(AF_INET, utf8, &dst);
      if (success == 1) {
        return YES;
      }
    
      // Checks whether the address is an IPv6 address.
      struct in6_addr dst6;
      success = inet_pton(AF_INET6, utf8, &dst6);
      if (success == 1) {
        return YES;
      }
    
      return NO;
    }
    
    // To redirect the request or add headers, perform the operations in this method.
    + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
      return request;
    }
    
    // Starts to load the request.
    - (void)startLoading {
      NSMutableURLRequest *request = [self.request mutableCopy];
      // Marks the request as processed to prevent infinite loops.
      [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];
    }
    
    /**
     * Cancels the request.
     */
    - (void)stopLoading {
      if (_inputStream.streamStatus == NSStreamStatusOpen) {
        [self closeStream:_inputStream];
      }
      [self.client URLProtocol:self didFailWithError:[[NSError alloc] initWithDomain:@"stop loading" code:-1 userInfo:nil]];
    }
    
    /**
     * Forwards the request using CFHTTPMessage.
     */
    - (void)startRequest {
      // The header information of the original request.
      NSDictionary *headFields = _curRequest.allHTTPHeaderFields;
      CFStringRef url = (__bridge CFStringRef) [_curRequest.URL absoluteString];
      CFURLRef requestURL = CFURLCreateWithString(kCFAllocatorDefault, url, NULL);
    
      // The method of the original request, such as GET or POST.
      CFStringRef requestMethod = (__bridge_retained CFStringRef) _curRequest.HTTPMethod;
    
      // Creates a CFHTTPMessageRef object based on the URL, method, and version of the request.
      CFHTTPMessageRef cfrequest = CFHTTPMessageCreateRequest(kCFAllocatorDefault, requestMethod, requestURL, kCFHTTPVersion1_1);
    
      // Adds the data attached to the HTTP POST request.
      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);
      }
    
      // Copies the header information of the original request.
      for (NSString* header in headFields) {
        CFStringRef requestHeader = (__bridge CFStringRef) header;
        CFStringRef requestHeaderValue = (__bridge CFStringRef) [headFields valueForKey:header];
        CFHTTPMessageSetHeaderFieldValue(cfrequest, requestHeader, requestHeaderValue);
      }
    
      // Creates an input stream for the CFHTTPMessage object.
      #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;
    
      // Sets the Server Name Indication (SNI) host information. This is a key step.
      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) {
        // Saves the runloop of the current thread. This is critical for redirected requests.
        self.curRunLoop = [NSRunLoop currentRunLoop];
      }
      // Schedules the stream on the current 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
    /**
     * Handles stream events.
     */
    - (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);
            // Notifies the client that the response is received. This notification is sent only once.
    
            CFStringRef httpVersion = CFHTTPMessageCopyVersion(message);
            // Obtains the status code from the response header.
            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;
            }
    
            // Verifies the certificate.
            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()];
            }
    
            // Binds the verification policy to the server certificate.
            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) {
              // If the certificate fails verification, close the input stream.
              [self closeStream:aStream];
              [self.client URLProtocol:self didFailWithError:[[NSError alloc] initWithDomain:@"fail to evaluate the server trust" code:-1 userInfo:nil]];
            } else {
              // The certificate is verified.
              if (statusCode >= 300 && statusCode < 400) {
                // Handles the redirection error code.
                [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);
        // Notifies the client that an error occurred.
        [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);
            // Notifies the client that the response is received. This notification is sent only once.
    
            if (!self.responseIsHandle) {
              CFStringRef httpVersion = CFHTTPMessageCopyVersion(message);
              // Obtains the status code from the response header.
              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 {
      // In case the response header is incomplete.
      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 {
      // Response header.
      CFDictionaryRef headerFieldsRef = CFHTTPMessageCopyAllHeaderFields(messageRef);
      NSDictionary *headDict = (__bridge_transfer NSDictionary *)headerFieldsRef;
      [self redirect:headDict];
    }
    
    - (void)redirect:(NSDictionary *)headDict {
      // If cookies are required for redirection, handle them.
      NSString *location = headDict[@"Location"];
      if (!location)
        location = headDict[@"location"];
      NSURL *url = [[NSURL alloc] initWithString:location];
      _curRequest.URL = url;
      if ([[_curRequest.HTTPMethod lowercaseString] isEqualToString:@"post"]) {
        // Per RFCs, a redirected POST request should be converted to a GET request.
        _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) {
          // If Z_BUF_ERROR is caused by an insufficient output buffer, the input buffer is not fully processed and the output buffer is full. In this case, the loop must continue to expand the buffer.
          // The opposite condition indicates that the error is not caused by an insufficient output buffer. In this case, the loop must be terminated, which indicates an 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. Implement a custom BeforeSend.

    - (void)setupHttpDNS:(NSString *)accountId {
      // Configure 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];
      // Set the custom NSURLProtocol defined in the preceding step.
      [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;
        // You can add a condition here to apply this logic only to specific URLs.
        
        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. Complete the SLS SDK initialization.

    @implementation AliyunSLS
    
    - (void)initSLS {
      // Set custom HTTPDNS for the SLS SDK.
      // Note:
      // This setting applies to all SLS SDK instances.
      [self setupHttpDNS:accountId];
      [self initProducer];
    }
    
    - (void)initProducer {
        // Initialize LogProducerConfig and LogProducerClient as usual.
        // ...
    }
    
    @end