全部產品
Search
文件中心

Simple Log Service:iOS SDK快速入門

更新時間:Jan 06, 2026

本文介紹如何快速使用Log ServiceiOS SDK採集日誌資料。

前提條件

已安裝iOS SDK。具體操作,請參見安裝iOS SDK

快速使用

您可以按照以下方式對SDK進行初始化,並調用addLog方法上報日誌。

重要
  • iOS SDK支援初始化多個執行個體,LogProducerConfig執行個體與LogProducerClient執行個體要成對使用。

  • 上報日誌到Log Service時需使用阿里雲帳號或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 {
    // Log Service的服務存取點。此處必須是以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無法針對某類日誌進行設定。一旦設定後,所有未成功發送到Log Service的日誌,都可能會更新。如果您需要通過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預解析和緩衝策略的樣本。

注意:如果SLS的endpoint使用的是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);
    
      // 原請求所使用的方法,GET或POST
      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 {
      // 以防response的header資訊不完整
      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