如何 NSLog 进入文件

Posted

技术标签:

【中文标题】如何 NSLog 进入文件【英文标题】:How to NSLog into a file 【发布时间】:2011-11-08 10:46:41 【问题描述】:

是否可以将每个NSLog 不仅写入控制台,还写入文件?我想准备这个而不将NSLog 替换为someExternalFunctionForLogging

替换所有NSLog 将是真正的问题。也许有可能从控制台解析数据或捕获消息?

【问题讨论】:

您可以使用 #define 将 NSLog 替换为另一个函数调用。 我试图按照下面的最佳答案进行操作,但这只会弄乱我的项目,并在 NSObjCRuntime.h 和整个 NSobject 等中引发大量解析问题。 【参考方案1】:

选项 1:使用 ASL

NSLog 将日志输出到 ASL(Apple 的 syslog 版本)和控制台,这意味着当您使用 iPhone 模拟器时,它已经在写入您 Mac 中的文件。如果您想阅读它,请打开应用程序 Console.app,然后在过滤器字段中输入应用程序的名称。要在您的 iPhone 设备上执行相同操作,您需要使用 ASL API 并进行一些编码。

选项 2:写入文件

假设您在模拟器上运行并且不想使用 Console.app。您可以使用 freopen 将错误流重定向到您喜欢的文件:freopen([path cStringUsingEncoding:NSASCIIStringEncoding], "a+", stderr); 有关详细信息,请参阅此explanation and sample project。

或者您可以使用宏使用自定义函数覆盖 NSLog。例如,将此类添加到您的项目中:

// file Log.h
#define NSLog(args...) _Log(@"DEBUG ", __FILE__,__LINE__,__PRETTY_FUNCTION__,args);
@interface Log : NSObject
void _Log(NSString *prefix, const char *file, int lineNumber, const char *funcName, NSString *format,...);
@end

// file Log.m
#import "Log.h"
@implementation Log
void _Log(NSString *prefix, const char *file, int lineNumber, const char *funcName, NSString *format,...) 
    va_list ap;
    va_start (ap, format);
    format = [format stringByAppendingString:@"\n"];
    NSString *msg = [[NSString alloc] initWithFormat:[NSString stringWithFormat:@"%@",format] arguments:ap];   
    va_end (ap);
    fprintf(stderr,"%s%50s:%3d - %s",[prefix UTF8String], funcName, lineNumber, [msg UTF8String]);
    [msg release];

@end

并在项目范围内导入它,将以下内容添加到您的<application>-Prefix.pch

#import "Log.h"

现在对 NSLog 的每次调用都将替换为您的自定义函数,而无需触及您现有的代码。但是,上面的功能只是打印到控制台。添加文件输出,在_Log上面添加这个函数:

void append(NSString *msg)
    // get path to Documents/somefile.txt
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString *path = [documentsDirectory stringByAppendingPathComponent:@"logfile.txt"];
    // create if needed
    if (![[NSFileManager defaultManager] fileExistsAtPath:path])
        fprintf(stderr,"Creating file at %s",[path UTF8String]);
        [[NSData data] writeToFile:path atomically:YES];
     
    // append
    NSFileHandle *handle = [NSFileHandle fileHandleForWritingAtPath:path];
    [handle truncateFileAtOffset:[handle seekToEndOfFile]];
    [handle writeData:[msg dataUsingEncoding:NSUTF8StringEncoding]];
    [handle closeFile];

并在 _Log 函数中的 fprintf 下面添加这一行:

append(msg);

文件写入也适用于您的 iPhone 设备,但该文件将在其中的目录中创建,您将无法访问,除非您添加代码将其发送回您的 Mac,或将其显示在查看您的应用程序内部,或使用 iTunes 添加文档目录。

【讨论】:

您可以使用管理器获取文本文件:选择“设备” - #您的设备# - “应用程序”。选择您的应用程序。您可以在下面的“沙盒中的数据文件”树中看到您的文件(以及其他文件)。点击“下载”。您现在在您的 Mac 上拥有该文件,并且可以右键单击“显示包内容”以浏览到您的文本文件。 我们可以在 NSLog 上创建一个类别吗? 如何在 NSLog 的类上创建一个类别?如果是,您能否建议 NSLog 函数存在的类名是什么?我尝试使用“目标 c 类别文件创建模板”创建 NSObjCRuntime 类别,但它不允许我下一步。请建议我如何覆盖 NSLog ?谢谢。 @PrasadG NSLog 不属于任何类。它是一个 C 函数。要覆盖它,您必须像上面的 #define 一样重新定义它。 当我尝试使用这种方式记录 [对象描述] 时,系统会出错。我不知道为什么。【参考方案2】:

有一种更容易的方法。这是将 NSLog 输出重定向到应用程序的 Documents 文件夹中的文件的方法。当您想在开发工作室之外测试您的应用程序时,这可能很有用,从您的 Mac 上拔下。

ObjC:

- (void)redirectLogToDocuments 

     NSArray *allPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
     NSString *documentsDirectory = [allPaths objectAtIndex:0];
     NSString *pathForLog = [documentsDirectory stringByAppendingPathComponent:@"yourFile.txt"];

     freopen([pathForLog cStringUsingEncoding:NSASCIIStringEncoding],"a+",stderr);

斯威夫特:

// 1. Window > Devices and Simulators
// 2. Select the device
// 3. Select your app and click gear icon
// 4. Download container
// 5. Right click and "view contents"
// 6. Find "yourfile.log" under Downloads
//
// redirectLogToDocuments()

func redirectLogToDocuments() 
  let allPaths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
  let documentsDirectory = allPaths.first!
  let pathForLog = "\(documentsDirectory)/yourfile.log"
  freopen(pathForLog.cString(using: String.Encoding.ascii)!, "a+", stdout)

执行此方法后,NSLog (ObjC) 或 print (Swift) 生成的所有输出都将转发到指定文件。要打开保存的文件Organizer,请浏览应用程序的文件并将Application Data 保存在文件系统的某个位置,而不是简单地浏览到Documents 文件夹。

【讨论】:

当我在运行应用程序后使用它时,我得到了多个日志文件,为什么会变成这样?我们不能只将它用于单个文件吗? freopen(pathForLog.cStringUsingEncoding(NSASCIIStringEncoding)!, "a+", stderr) 。我们需要 stdout 而不是 stderr 以将所有日志重定向到文件。使用 stderr,只有错误日志会进入文件。 我认为 NSLog 只写入标准错误。 有人可以指导一下,如何清除这个文件,一旦它被复制到剪贴板,以便下次启动应用程序重新开始。 我看到的一件事是,如果您在 Swift 中混合使用 print 和 NSLog,那么它们不会以正确的顺序登录到文件中(打印似乎要晚得多完全)。我的偏好通常是使用 NSLog 来获取时间戳信息。【参考方案3】:

我找到了解决问题的最简单方法:Logging to a file on the iPhone。无需更改任何 NSLog 代码或更改记录器本身,只需将这 4 行添加到您的 didFinishLaunchingWithOptions 并确保在您的构建设置中实时发布不会激活此功能(我为此添加了 LOG2FILE 标志)。

#ifdef LOG2FILE
 #if TARGET_IPHONE_SIMULATOR == 0
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString *logPath = [documentsDirectory stringByAppendingPathComponent:@"console.log"];
    freopen([logPath cStringUsingEncoding:NSASCIIStringEncoding],"a+",stderr);
 #endif
#endif

【讨论】:

奇怪的是,几周后发布与此代码几乎完全相同的代码的答案得到了所有的赞成... 从 Xcode 运行时,更容易登录到 Mac 磁盘上的文件。只需登录"~/console.log"。然后在终端中输入tail -f ~/console.log 以获得不断更新的输出。【参考方案4】:

将 JaakL 的答案翻译成 Swift,以防万一其他人也需要它

在您的应用程序中的某处运行此代码,从那一刻起,它将所有 NSLog() 输出存储到文档目录中的文件中。

let docDirectory: NSString = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true)[0] as NSString
let logpath = docDirectory.stringByAppendingPathComponent("YourFileName.txt")
freopen(logpath.cStringUsingEncoding(NSASCIIStringEncoding)!, "a+", stderr)

补充:如何使用 Xcode 查找日志文件: 您可以简单地从 Xcode 访问日志:Windows > 设备 > 选择您的应用程序 > InfoWheelButton > 下载容器。 使用查找器查看文件:在文件上单击鼠标右键 > 显示包内容 > appdata > 文档 > 文件就在那里

【讨论】:

这不会在 swift 中记录 println() 调用 我们如何在全局范围内捕获 println() 调用? 相同的三行,不同的文件名和标准输出,而不是 freopen 中的标准错误。【参考方案5】:

Swift 4 版本

let docDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let logpathe = docDirectory.appendingPathComponent("Logerr.txt")
freopen(logpathe.path.cString(using: .ascii)!, "a+", stderr)
let logpatho = docDirectory.appendingPathComponent("Logout.txt")
freopen(logpatho.path.cString(using: .ascii)!, "a+", stdout)

Swift print() 的输出将在 stdout

【讨论】:

【参考方案6】:

好的!首先,我要感谢 Evan-Mulawski。 这是我的解决方案,也许对某人有帮助:

在 AppDelegate 我添加函数:

void logThis(NSString* Msg, ...)
   
    NSArray* findingMachine = [Msg componentsSeparatedByString:@"%"];
    NSString* outputString = [NSString stringWithString:[findingMachine objectAtIndex:0]];
    va_list argptr;
    va_start(argptr, Msg);

    for(int i = 1; i < [findingMachine count]; i++) 
        if ([[findingMachine objectAtIndex:i] hasPrefix:@"i"]||[[findingMachine objectAtIndex:i] hasPrefix:@"d"]) 
            int argument = va_arg(argptr, int); /* next Arg */
            outputString = [outputString stringByAppendingFormat:@"%i", argument];      
            NSRange range;
            range.location = 0;
            range.length = 1;
            NSString* tmpStr = [[findingMachine objectAtIndex:i] stringByReplacingCharactersInRange:range withString:@""];
            outputString = [outputString stringByAppendingString:tmpStr];
        
        else if ([[findingMachine objectAtIndex:i] hasPrefix:@"@"]) 
            id argument = va_arg(argptr, id);
            // add argument and next patr of message    
            outputString = [outputString stringByAppendingFormat:@"%@", argument];
            NSRange range;
            range.location = 0;
            range.length = 1;
            NSString* tmpStr = [[findingMachine objectAtIndex:i] stringByReplacingCharactersInRange:range withString:@""];
            outputString = [outputString stringByAppendingString:tmpStr];
        
        else if ([[findingMachine objectAtIndex:i] hasPrefix:@"."]) 
            double argument = va_arg(argptr, double);       
            // add argument and next patr of message    
            outputString = [outputString stringByAppendingFormat:@"%f", argument];
            NSRange range;
            range.location = 0;
            range.length = 3;
            NSString* tmpStr = [[findingMachine objectAtIndex:i] stringByReplacingCharactersInRange:range withString:@""];
            outputString = [outputString stringByAppendingString:tmpStr];
        
        else if ([[findingMachine objectAtIndex:i] hasPrefix:@"f"]) 
            double argument = va_arg(argptr, double);       
            // add argument and next patr of message    
            outputString = [outputString stringByAppendingFormat:@"%f", argument];
            NSRange range;
            range.location = 0;
            range.length = 1;
            NSString* tmpStr = [[findingMachine objectAtIndex:i] stringByReplacingCharactersInRange:range withString:@""];
            outputString = [outputString stringByAppendingString:tmpStr];
        
        else 
            outputString = [outputString stringByAppendingString:@"%"];
            outputString = [outputString stringByAppendingString:[findingMachine objectAtIndex:i]];
        
    
    va_end(argptr);
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES);
    NSString *  filePath = [[paths objectAtIndex:0]stringByAppendingPathComponent:@"logFile.txt"];
    NSError* theError = nil;
    NSString * fileString = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:&theError];
    if (theError != nil||[fileString length]==0) 
        fileString = [NSString stringWithString:@""];
    
    fileString = [fileString stringByAppendingFormat:@"\n%@",outputString];
    if(![fileString writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:&theError])
    
            NSLog(@"Loging problem");
    

    NSLog(@"%@",outputString);

然后,使用“全部替换”NSLog -> logThis。 此代码适用于我的应用程序。可以根据不同的需求进行扩展。


感谢您的帮助。

【讨论】:

【参考方案7】:

这是我使用的并且效果很好:

http://parmanoir.com/Redirecting_NSLog_to_a_file

希望对你有帮助。

为了内容,我就放在这里

- (BOOL)redirectNSLog  
     // Create log file 
     [@"" writeToFile:@"/NSLog.txt" atomically:YES encoding:NSUTF8StringEncoding error:nil]; 
     id fileHandle = [NSFileHandle fileHandleForWritingAtPath:@"/NSLog.txt"]; 
     if (!fileHandle) return NSLog(@"Opening log failed"), NO; 
     [fileHandle retain];  

     // Redirect stderr 
     int err = dup2([fileHandle fileDescriptor], STDERR_FILENO); 
     if (!err) return NSLog(@"Couldn't redirect stderr"), NO;  return YES; 

【讨论】:

【参考方案8】:

Swift 2.0:

将这些添加到 Appdelegate didFinishLaunchWithOptions。

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool 
    var paths: Array = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
    let documentsDirectory: String = paths[0]
    let logPath: String = documentsDirectory.stringByAppendingString("/console.log")

    if (isatty(STDERR_FILENO) == 0)
    
        freopen(logPath, "a+", stderr)
        freopen(logPath, "a+", stdin)
        freopen(logPath, "a+", stdout)
    
    print(logPath)

    return true

访问console.log:

在Xcode Log Area打印日志路径后,选择路径,右键,在Finder中选择Services-Reaveal,打开文件console.log

【讨论】:

你为什么要检查isatty(STDERR_FILENO) == 0【参考方案9】:

我根据 Alvin George 的答案做了一些工作。

为了控制日志文件的大小,我实现了(快速而肮脏的)“10 代日志文件”解决方案,并添加了一个函数以便稍后删除它们

应用程序每次启动时,都会生成一个索引为“0”的新日志文件。现有文件将使用比以前更高的索引重命名。索引“10”将被删除。

因此,每次启动都会为您提供一个新的日志文件,最多 10 代

可能不是最优雅的方式,但在过去几周对我来说非常好,因为我需要一些长时间的“关闭 Mac”日志

  // -----------------------------------------------------------------------------------------------------------
  // redirectConsoleToFile()
  //
  // does two things  
  // 1) redirects "stderr", "stdin" and "stdout" to a logfile
  // 2) deals with old/existing files to keep up to 10 generations of the logfiles
  // tested with ios 9.4 and Swift 2.2
  func redirectConsoleToFile() 

    // Instance of a private filemanager
    let myFileManger = NSFileManager.defaultManager()

    // the path of the documnts directory of the app
    let documentDirectory: String = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true).first!

    // maximum number of logfiles
    let maxNumberOfLogFiles: Int = 10

    // look if the max number of files already exist
    var logFilePath : String = documentDirectory.stringByAppendingString("/Console\(maxNumberOfLogFiles).log")
    var FlagOldFileNoProblem: Bool = true
    if myFileManger.fileExistsAtPath(logFilePath) == true 

        // yes, max number of files reached, so delete the oldest one
        do 
            try myFileManger.removeItemAtPath(logFilePath)

         catch let error as NSError 

            // something went wrong
            print("ERROR deleting old logFile \(maxNumberOfLogFiles): \(error.description)")
            FlagOldFileNoProblem = false
        
    

    // test, if there was a problem with the old file
    if FlagOldFileNoProblem == true 

        // loop over all possible filenames
        for i in 0 ..< maxNumberOfLogFiles 

            // look, if an old file exists, if so, rename it with an index higher than before
            logFilePath = documentDirectory.stringByAppendingString("/Console\((maxNumberOfLogFiles - 1) - i).log")
            if myFileManger.fileExistsAtPath(logFilePath) == true 

                // there is an old file
                let logFilePathNew = documentDirectory.stringByAppendingString("/WayAndSeeConsole\(maxNumberOfLogFiles - i).log")
                do 

                    // rename it
                    try myFileManger.moveItemAtPath(logFilePath, toPath: logFilePathNew)

                 catch let error as NSError 

                    // something went wrong
                    print("ERROR renaming logFile: (i = \(i)), \(error.description)")
                    FlagOldFileNoProblem = false
                
            
        
    

    // test, if there was a problem with the old files
    if FlagOldFileNoProblem == true 

        // No problem so far, so try to delete the old file
        logFilePath = documentDirectory.stringByAppendingString("/Console0.log")
        if myFileManger.fileExistsAtPath(logFilePath) == true 

            // yes, it exists, so delete it
            do 
                try myFileManger.removeItemAtPath(logFilePath)

             catch let error as NSError 

                // something went wrong
                print("ERROR deleting old logFile 0: \(error.description)")
            
        
    

    // even if there was a problem with the files so far, we redirect
    logFilePath = documentDirectory.stringByAppendingString("/Console0.log")

    if (isatty(STDIN_FILENO) == 0) 
        freopen(logFilePath, "a+", stderr)
        freopen(logFilePath, "a+", stdin)
        freopen(logFilePath, "a+", stdout)
        displayDebugString(DEBUG_Others, StringToAdd: "stderr, stdin, stdout redirected to \"\(logFilePath)\"")
     else 
        displayDebugString(DEBUG_Others, StringToAdd: "stderr, stdin, stdout NOT redirected, STDIN_FILENO = \(STDIN_FILENO)")
    


// -----------------------------------------------------------------------------------------------------------
// cleanupOldConsoleFiles()
//
// delete all old consolfiles
func cleanupOldConsoleFiles() 

    // Instance of a private filemanager
    let myFileManger = NSFileManager.defaultManager()

    // the path of the documnts directory of the app
    let documentDirectory: String = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true).first!

    // maximum number of logfiles
    let maxNumberOfLogFiles: Int = 10

    // working string
    var logFilePath: String = ""

    // loop over all possible filenames
    for i in 0 ... maxNumberOfLogFiles 

        // look, if an old file exists, if so, rename it with an index higher than before
        logFilePath = documentDirectory.stringByAppendingString("/Console\(i).log")
        if myFileManger.fileExistsAtPath(logFilePath) == true 

            // Yes, file exist, so delete it
            do 
                try myFileManger.removeItemAtPath(logFilePath)
             catch let error as NSError 

                // something went wrong
                print("ERROR deleting old logFile \"\(i)\": \(error.description)")
            
        
    

【讨论】:

以上是关于如何 NSLog 进入文件的主要内容,如果未能解决你的问题,请参考以下文章

Xcode 如何 NSLog 一个 JSON

如何将参数值传递给自定义 NSLOG

如何在发送之前对 JSON 进行 NSLog

如何动态构建 NSLog 的参数?

将 NSLog 保存到本地文件中

如何 NSLog CGRect [重复]