iOS 10 / Xcode 8 上的设备上的 NSLog 似乎被截断了?为什么?

77
为什么在Xcode 8 / iOS 10中,控制台输出显示不完整?

enter image description here


只是出于好奇,两张图片中的“a”字符数量是否相同?字符串的长度究竟有多长? - Hot Licks
'a'的数量不同,确切地说是1022。 - iPeta
我打印的HTTP响应体不完整,上面只是一个例子。 - iPeta
2
尝试使用printf代替NSLog。https://forums.developer.apple.com/message/161367#161367 - TonyMkenu
这让我很烦恼。调试了好几天,我真的以为我们的服务器返回了一个截断的回复。感谢你的问题。 - GeneCode
显示剩余2条评论
8个回答

75

一个临时解决方案,只需要在全局头文件中重新定义所有的 NSLOGprintf

#define NSLog(FORMAT, ...) printf("%s\n", [[NSString stringWithFormat:FORMAT, ##__VA_ARGS__] UTF8String]);

也对我有用 :) - Ankit
2
我的NSLog输出截断了我的序列化JSON NSData。非常烦人。这对我也起作用。我建议您将分号从末尾删除,如果将其粘贴到.m文件的顶部。 - Carl Hine
如果你想要日期,可以这样做:printf("%s %s\n", [[[NSDate date] description] UTF8String], [[NSString stringWithFormat:FORMAT, ##VA_ARGS] UTF8String]) - matt bezark
太棒了!你让我开心一整天! - floki1

55

在iOS 10和Xcode 8中,苹果公司从传统的ASL(Apple System Log)转向了一种名为Unified logging的新日志系统。实际上,NSLog调用委托给新的os_log API。(出处:https://developer.apple.com/reference/os/logging):

重要提示

Unified logging可在iOS 10.0及更高版本、macOS 10.12及更高版本、tvOS 10.0及更高版本和watchOS 3.0及更高版本中使用,取代了ASL(Apple System Logger)和Syslog APIs。历史上,日志消息被写入到磁盘上特定的位置,例如/etc/system.log。统一日志系统将消息存储在内存和数据存储中,而不是写入基于文本的日志文件。

还有:

重要提示

当日志系统存储大于系统最大消息长度的日志消息时,它们会被截断。使用日志命令行工具查看实时活动流时,完整的消息是可见的。但请记住,流式日志数据是一项昂贵的活动。

SDK头文件中显示,格式化变量的“系统最大消息长度”限制为1024个字符,由@Hot_Leaks指出(出处:<os/log.h>):

/*!  
 * @function os_log  
 *   
 * ...  
 *  
 * There is a physical cap of 1024 bytes per log line for dynamic content,  
 * such as %s and %@, that can be written to the persistence store.  
 * All content exceeding the limit will be truncated before it is  
 * written to disk.  
 *
 * ... 
 *
 */  
#define os_log(log, format, ...)    os_log_with_type(log, OS_LOG_TYPE_DEFAULT, format, ##__VA_ARGS__)

由于缓冲区大小限制似乎已经硬编码到 libsystem_trace.dylib 中,我看不到除了打印字符串文字(%@)或将格式化字符串变量分成 < 1024 字符串之外的其他解决方法。

printf 在调试期间可以使用,因为调试器(Xcode)显示进程的输出 / 错误流,但它不会被发送到设备日志本身。这意味着 xfdai 的解决方案不能帮助您使用其他日志应用程序(如 macOS 的 Console 应用程序),或者在非调试应用程序(例如运行在客户设备上的 AppStore 应用程序)上出现问题时。


将 xfdai 的答案扩展到部署应用程序

在部署的应用程序 / 非调试构建中,无法查看 NSLogprintf

唯一的直接将消息打印到设备日志(可使用 Xcode -> 窗口 -> 设备、mac 的 Console 应用程序或第三方实用程序(如 deviceconsole)访问)的方法是调用 os_log API(自 iOS 10 起使用的 ASL 的后继者)。

这是我正在使用的全局头文件,将 NSLog 重定义为在 iOS 10 上调用 _os_log_internal

#ifndef PrefixHeader_pch
#define PrefixHeader_pch

#ifdef __OBJC__
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#endif

#import <os/object.h>
#import <os/activity.h>

/*
 *  System Versioning Preprocessor Macros
 */

#define SYSTEM_VERSION_EQUAL_TO(v)                  ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedSame)
#define SYSTEM_VERSION_GREATER_THAN(v)              ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedDescending)
#define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v)  ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedAscending)
#define SYSTEM_VERSION_LESS_THAN(v)                 ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedAscending)
#define SYSTEM_VERSION_LESS_THAN_OR_EQUAL_TO(v)     ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedDescending)

// os_log is only supported when compiling with Xcode 8.
// Check if iOS version > 10 and the _os_log_internal symbol exists,
// load it dynamically and call it.
// Definitions extracted from #import <os/log.h>

#if OS_OBJECT_SWIFT3
OS_OBJECT_DECL_SWIFT(os_log);
#elif OS_OBJECT_USE_OBJC
OS_OBJECT_DECL(os_log);
#else
typedef struct os_log_s *os_log_t;
#endif /* OS_OBJECT_USE_OBJC */

extern struct os_log_s _os_log_default;

extern __attribute__((weak)) void _os_log_internal(void *dso, os_log_t log, int type, const char *message, ...);

// In iOS 10 NSLog only shows in device log when debugging from Xcode:
#define NSLog(FORMAT, ...) \
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"10.0")) {\
    void(*ptr_os_log_internal)(void *, __strong os_log_t, int, const char *, ...) = _os_log_internal;\
    if (ptr_os_log_internal != NULL) {\
        _Pragma("clang diagnostic push")\
        _Pragma("clang diagnostic error \"-Wformat\"")\
        _os_log_internal(&__dso_handle, OS_OBJECT_GLOBAL_OBJECT(os_log_t, _os_log_default), 0x00, [[NSString stringWithFormat:FORMAT, ##__VA_ARGS__] UTF8String]);\
        _Pragma("clang diagnostic pop")\
    } else {\
        NSLog(FORMAT, ##__VA_ARGS__);\
    }\
} else {\
    NSLog(FORMAT, ##__VA_ARGS__);\
}

#endif /* PrefixHeader_pch */

5
很好的答案。确认它不是一个bug。 - d00dle
1
在我看来,这是一个糟糕的实现。你应该在启动时测试这些“if”语句和变量赋值,将它们的结果设置为全局变量,而不是在每次调用NSLog时都进行测试,并引用它们的宏。 - ishahak
3
我的目的是展示如何使用 os_log API。您可以根据需要编辑代码。 - Elist

10

这是一个仅适用于iOS 10的“功能”。请使用以下替代方法:

printf("%s", [logString UTF8String]);

3
如果我需要打印一个NSDictionary,怎么办?这太疯狂了。 - Deepak Sharma
@DeepakSharma,xfdai提供的解决方案适用于NSDictionaries。 - pir800

4
你可以使用这种方法。将每800个字符拆分一次。或者可以设置。 NSLOG会截断每1000个字符。 如果字符串少于800,将使用简单的NSLog。 这对于Json长字符串很有用,它使用控制台。 printf使用Xcode调试窗口而不是控制台。
    -(void) JSLog:(NSString*)logString{

            int stepLog = 800;
            NSInteger strLen = [@([logString length]) integerValue];
            NSInteger countInt = strLen / stepLog;

            if (strLen > stepLog) {
            for (int i=1; i <= countInt; i++) {
                NSString *character = [logString substringWithRange:NSMakeRange((i*stepLog)-stepLog, stepLog)];
                NSLog(@"%@", character);

            }
            NSString *character = [logString substringWithRange:NSMakeRange((countInt*stepLog), strLen-(countInt*stepLog))];
            NSLog(@"%@", character);
            } else {

            NSLog(@"%@", logString);
            }

    }

如果你需要超出设备控制台日志的1024个字符限制,这会非常有用。谢谢! - AndrewJC

2
在iOS 10中:
  1. printf()在Xcode的控制台中可以使用,但无法在设备的控制台日志中使用。
  2. NSLog在两个地方都被截断。
我现在所做的是将我的NSLog字符串分成几行,并逐行记录每一行。
- (void) logString: (NSString *) string
{
    for (NSString *line in [string componentsSeparatedByCharactersInSet: [NSCharacterSet newlineCharacterSet]])
    {
        NSLog(@"%@", line);
    }
}

这在控制台上运行是可以的,但不易阅读。

1

漂亮的函数和行号

#define NSLog(FORMAT, ...) printf("%s:%d %s\n", __PRETTY_FUNCTION__,__LINE__,[[NSString stringWithFormat:FORMAT, ##__VA_ARGS__] UTF8String])

带有日期。
#define NSLog(FORMAT, ...) printf("%s %s:%d %s\n", [[[NSDate date] description] UTF8String],__PRETTY_FUNCTION__,__LINE__,[[NSString stringWithFormat:FORMAT, ##__VA_ARGS__] UTF8String])

改进 @xfdai 的答案

0
这是我针对同样问题的Swift解决方法。我遇到了每个插值符号1000~1024个字符的限制。
由于我大多数情况下都是在记录json文件时遇到这个问题,所以我决定将字符串分成块,但不是直接分割。我尝试在换行符上进行分割,以保留多个日志之间的整个json字符串。
提供这个解决方法的另一个原因是为了保持logger.debug(_:)调用在原来的位置,这样我就不会失去跳转到实际记录日志的源代码位置的能力。
所以这是我的解决方案:
extension String {
    /// Splits the string into chunks to accommodate a specified maximum length,
    /// considering line breaks as the preferred splitting points.
    ///
    /// - Parameters:
    ///   - maxLength: The maximum length of each output chunk. The default value is 1000.
    /// - Returns: An array of `Substring` chunks.
    ///
    /// - Note: The function iterates through the original string and creates chunks of text
    ///         based on the specified maximum length. If a line break character is found
    ///         within the chunk, the split occurs at the line break. If no line break
    ///         character is present, the function tries to split at the last space character
    ///         before the maxLength. If no space is found, the chunk is split at the
    ///         maxLength. The line break character or space character (if used for
    ///         splitting) is dropped from the output.
    ///
    /// - Complexity: The time complexity is O(n), where n is the number of characters
    ///         in the string. The function iterates through the string once to create the
    ///         chunks.
    public func splittedForLogger(maxLength: Int = 1000) -> [Substring] {
        var chunks: [Substring] = []
        var currentIndex = self.startIndex
        
        while currentIndex < self.endIndex {
            let remainingLength = self.distance(from: currentIndex, to: self.endIndex)
            let chunkLength = min(maxLength, remainingLength)
            let nextIndex = self.index(currentIndex, offsetBy: chunkLength)
            let chunk = self[currentIndex..<nextIndex]
            
            if chunkLength == remainingLength {
                /// Last chunk
                chunks.append(chunk)
                break
            }
            
            /// Attempt to find the last line break character within the chunk
            /// If not found, attempt to find the last space character
            /// If neither line break nor space character is found, split at the maxLength
            let splitIndex = chunk.lastIndex { character in
                CharacterSet.newlines.contains(character.unicodeScalars.first ?? .init(0))
            } ?? chunk.lastIndex { character in
                CharacterSet.whitespaces.contains(character.unicodeScalars.first ?? .init(0))
            } ?? chunk.endIndex
            
            let splitChunk = self[currentIndex..<splitIndex]
            chunks.append(splitChunk)
            currentIndex = splitIndex < chunk.endIndex ? self.index(after: splitIndex) : nextIndex
        }
        
        return chunks
    }
    
    @inlinable public func forEachLoggerChunk(
        maxLength: Int = 1000,
        _ body: (Substring) throws -> Void
    ) rethrows {
        try self
            .splittedForLogger(maxLength: maxLength)
            .forEach(body)
    }
}

现在你可以像这样使用它来记录长字符串。 只需更改你的
logger.debug("\(someLongString)")

转换成这个
someLongString.forEachLoggerChunk { logger.debug("\($0)") }

结果:


0

这并不提供良好的输出,但会在控制台上打印长日志所需的所有必要信息。

func Log(_ logString: String?) {
    if logString?.isEmpty ?? false { return }
    NSLog("%@", logString!)
    Log(String(logString!.dropFirst(1024)))
}

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