• iOS应用日志:开始编写日志组件与异常日志


    应用日志(一):开始编写日志组件

    对于那些做后端开发的工程师来说,看 LOG解Bug应该是理所当然的事,但我接触到的移动应用开发的工程师里面,很多人并没有这个意识,查Bug时总是一遍一遍的试图重现,试图调试,特别是 对一些不太容易重现的Bug经常焦头烂额。而且iOS的异常机制比较复杂,Objective-C的语言驾驭也需要一定的功力,做出来的应用有时候挺容易 产生崩溃闪退。一遍一遍的用XCode取应用崩溃记录、解析符号,通常不胜其烦,有时还对着解析出来的调用栈发呆,因为程序当时的内部状态常常难以看明 白,只能去猜测。

    好了,先从一个自制的日志组件开始吧。我们需要一个专门的后台线程去输出日志,线程根函数如下:
    - ( void ) threadProc
    {
        do
        {
            NSAutoreleasePool* pool = [ [ NSAutoreleasePool alloc ] init ];
            for ( int i = 0; i < 20; i++ )
            {
                [ _signal lock ];
                while ( [ _queue count ] == 0 ) // NSMutableArray* _queue,其它线程将日志加入_queue,日志线程负责输出到文件和控制台
                    [ _signal wait ]; // NSCondition* _signal
                NSArray* items = [ NSArray arrayWithArray: _queue ];
                [ _queue removeAllObjects ];
                [ _signal unlock ];
                if ( [ items count ] > 0 && [ self checkFileCreated ] /* 检查日志文件是否已创建 */ )
                    [ self logToFile: items ]; // 输出到文件以及控制台
            }
           
            // 每20次输出日志执行一次NSAutoreleasePool的release
            // 保证既不太频繁也不太滞后
            [ pool release ];
           
        } while ( YES );
    }
    再上记录日志的入口函数。注意Objective-C作为一门动态语言,要以动态语言的思维去使用,比如习惯去用NSDictionary,而不是自己定义一个数据类。好处很多,后面再说。
    void writeCinLog( const char* function,        // 记录日志所在的函数名称
                     CinLogLevel level,            // 日志级别,Debug、Info、Warn、Error
                     NSString* format,            // 日志内容,格式化字符串
                     ... )                        // 格式化字符串的参数
    {
        CinLoggerManager* manager = instanceOfLoggerManager(); // CinLoggerManager是单件的日志管理器
       
        if ( manager.mLogLevel > level || ! format ) // 先检查当前程序设置的日志输出级别。如果这条日志不需要输出,就不用做字符串格式化
            return;
       
        va_list args;
        va_start( args, format );
        NSString* str = [ [ NSString alloc ] initWithFormat: format arguments: args ];
        va_end( args );
        NSThread* currentThread = [ NSThread currentThread ];
        NSString* threadName = [ currentThread name ];
        NSString* functionName = [ NSString stringWithUTF8String: function ];
        if ( ! threadName )
            threadName = @"";
        if ( ! functionName )
            functionName = @"";
        if ( ! str )
            str = @"";
       
        // NSDictionary中加入所有需要记录到日志中的信息
        NSDictionary* entry = [ [ NSDictionary alloc ] initWithObjectsAndKeys:
                               @"LogEntry", @"Type",
                               str, @"Message",                                                // 日志内容
                               [ NSDate date ], @"Date",                                    // 日志生成时间
                               [ NSNumber numberWithUnsignedInteger: level ], @"Level",        // 本条日志级别
                               threadName, @"ThreadName",                                    // 本条日志所在的线程名称
                               functionName, @"FunctionName",                                // 本条日志所在的函数名称
                               nil ];
        [ str release ];
        [ manager appendLogEntry: entry ];
        [ entry release ];
    }
    appendLogEntry实现如下:
    - ( void ) appendLogEntry: ( NSDictionary* )entry
    {
        [ _signal lock ];
        [ _queue addObject: entry ];
        [ _signal signal ];
        [ _signal unlock ];
    }
    日志文件的管理也是必须考虑的。我现在日志文件的文件名形如:“03月27日 09:57:25 (0).txt”;其中前面是本次程序启动的时间,括号内默认是0。如果同一次的运行进程输出的日志文件超过1M,就创建新文件“03月27日 09:57:25 (1).txt”。这样文件不会太大,也有利于在时间点上与测试报上的Bug对应起来。

    另外为了调用writeCinLog时能将当前所在的函数名传进来,我们需要借助宏,使用__FUNCTION__预定义宏在编译期将函数名转换为字符串
    #define FeLogDebug(format,...)        writeCinLog(__FUNCTION__,CinLogLevelDebug,format,##__VA_ARGS__)
    #define FeLogInfo(format,...)        writeCinLog(__FUNCTION__,CinLogLevelInfo,format,##__VA_ARGS__)
    #define FeLogWarn(format,...)        writeCinLog(__FUNCTION__,CinLogLevelWarning,format,##__VA_ARGS__)
    #define FeLogError(format,...)        writeCinLog(__FUNCTION__,CinLogLevelError,format,##__VA_ARGS__)

    这样,如果在didFinishLaunchingWithOptions函数中写一句日志
    FeLogInfo( @"========= 应用已经启动成功了 =========" );

    输出的日志可能是这样的
    <- 03-27 10:44:59 INFO -> [UI] -[myAppDelegate application:didFinishLaunchingWithOptions:]
    ========= 应用已经启动成功了 =========

    其中前面是时间,INFO是日志级 别,UI是线程名称,myAppDelegate是记录日志的类的名 称,application:didFinishLaunchingWithOptions:是所在的函数名称。还有其它可利用的预定义宏,比如 __FILE__、__LINE__,能将代码文件名和行号也加入到日志中,就看有没有必要了。

     

    应用日志(二):异常日志

    言归正传。开发iOS应用,解决Crash问题始终是一个难题。Crash分为两种,一种是由EXC_BAD_ACCESS引起的,原因是访问了不属于本 进程的内存地址,有可能是访问已被释放的内存;另一种是未被捕获的Objective-C异常(NSException),导致程序向自身发送了 SIGABRT信号而崩溃。其实对于未捕获的Objective-C异常,我们是有办法将它记录下来的,如果日志记录得当,能够解决绝大部分崩溃的问题。 这里对于UI线程与后台线程分别说明。

    先看UI线程。iOS SDK提供了NSSetUncaughtExceptionHandler函数,用法如:
    NSSetUncaughtExceptionHandler( handleRootException );

    这样在UI线程发生未捕获异常后,进程崩溃之前,handleRootException会被执行。这个函数实现如下
    static void handleRootException( NSException* exception )
    {
        NSString* name = [ exception name ];
        NSString* reason = [ exception reason ];
        NSArray* symbols = [ exception callStackSymbols ]; // 异常发生时的调用栈
        NSMutableString* strSymbols = [ [ NSMutableString alloc ] init ]; // 将调用栈拼成输出日志的字符串
        for ( NSString* item in symbols )
        {
            [ strSymbols appendString: item ];
            [ strSymbols appendString: @" " ];
        }

        // 写日志,级别为ERROR
        writeCinLog( __FUNCTION__, CinLogLevelError, @"[ Uncaught Exception ] Name: %@, Reason: %@ [ Fe Symbols Start ] %@[ Fe Symbols End ]", name, reason, strSymbols );
        [ strSymbols release ];
       
        // 这儿必须Hold住当前线程,等待日志线程将日志成功输出,当前线程再继续运行
        blockingFlushLogs( __FUNCTION__ );

        // 写一个文件,记录此时此刻发生了异常。这个挺有用的哦
        NSDictionary* dict = [ NSDictionary dictionaryWithObjectsAndKeys:
                currentCinLogFileName(), @"LogFile",                // 当前日志文件名称
                currentCinLogFileFullPath(), @"LogFileFullPath",    // 当前日志文件全路径
                [ NSDate date ], @"TimeStamp",                        // 异常发生的时刻
                nil ];
        NSString* path = [ NSString stringWithFormat: @"%@/Documents/", NSHomeDirectory() ];
        NSString* lastExceptionLog = [ NSString stringWithFormat: @"%@LastExceptionLog.txt", path ];
        [ dict writeToFile: lastExceptionLog atomically: YES ];
       
    }
    而我们的日志组件必须实现blockingFlushLogs函数,确保进程在日志完全写入文件后再退出。这个实现应该很简单吧。

    当应用下次启动时,我们可以检查,如果有LastExceptionLog.txt,则弹窗引导测试人员将日志发过来。如果iPhone上面配置了 EMail帐户,可以很简单的调用MFMailComposeViewController将日志文件作为附件发送,当然也可以想其它办法。

    记得正式发布的版本要将它条件编译去掉哦。



    其中文件中的最后一条ERROR即为导致崩溃的异常,而从ERROR之前的日志可以看出当前程序的运行情况。ERROR如下:
    <- 03-20 17:21:43 ERROR -> [UI] -[CinUIRunLoopActionManager(Protected) handleRootException:]
    [ Uncaught Exception ]
    Name: NSDestinationInvalidException, Reason: *** -[CinThreadRunLoopActionManager performSelector:onThread:withObject:waitUntilDone:modes:]: target thread exited while waiting for the perform
    [ Fe Symbols Start ]
       CoreFoundation                      0x340c88d7 __exceptionPreprocess + 186
       libobjc.A.dylib                     0x343181e5 objc_exception_throw + 32
       CoreFoundation                      0x340c87b9 +[NSException raise:format:] + 0
       CoreFoundation                      0x340c87db +[NSException raise:format:] + 34
       Foundation                          0x35a12493 -[NSObject(NSThreadPerformAdditions) performSelector:onThread:withObject:waitUntilDone:modes:] + 998
       Foundation                          0x35a3afb5 -[NSObject(NSThreadPerformAdditions) performSelector:onThread:withObject:waitUntilDone:] + 108
       MyiOSapplication                    0x0022b7e9 -[CinThreadRunLoopActionManager(Protected) performAction:] + 144
      UIKit                               0x374b36b5 -[UIViewController _setViewAppearState:isAnimating:] + 144
      UIKit                               0x374b38c1 -[UINavigationController viewWillAppear:] + 288
      UIKit                               0x374b36b5 -[UIViewController _setViewAppearState:isAnimating:] + 144
      UIKit                               0x3750e61b -[UIViewController beginAppearanceTransition:animated:] + 190
      UIKit                               0x3750b415 -[UITabBarController transitionFromViewController:toViewController:transition:shouldSetSelected:] + 184
      UIKit                               0x3750b357 -[UITabBarController transitionFromViewController:toViewController:] + 30
      UIKit                               0x3750ac91 -[UITabBarController _setSelectedViewController:] + 300
      UIKit                               0x3750a9c5 -[UITabBarController setSelectedIndex:] + 240
      MyiOSapplication                    0x0007ef1d +[Utility ResetCurrentTabIndex] + 172
      MyiOSapplication                    0x001a87bd -[UIViewController(statusBar) dismissModalViewControllerAnimatedEx:] + 416
      MyiOSapplication                    0x001793fb -[ImageProcessingViewController save:] + 690
      CoreFoundation                      0x34022435 -[NSObject performSelector:withObject:withObject:] + 52
      UIKit                               0x3748c9eb -[UIApplication sendAction:to:from:forEvent:] + 62
      UIKit                               0x3748c9a7 -[UIApplication sendAction:toTarget:fromSender:forEvent:] + 30
      UIKit                               0x3748c985 -[UIControl sendAction:to:forEvent:] + 44
      UIKit                               0x3748c6f5 -[UIControl(Internal) _sendActionsForEvents:withEvent:] + 492
      UIKit                               0x3748d02d -[UIControl touchesEnded:withEvent:] + 476
      UIKit                               0x3748b50f -[UIWindow _sendTouchesForEvent:] + 318
      UIKit                               0x3748af01 -[UIWindow sendEvent:] + 380
      UIKit                               0x374714ed -[UIApplication sendEvent:] + 356
      UIKit                               0x37470d2d _UIApplicationHandleEvent + 5808
      GraphicsServices                    0x308a3df3 PurpleEventCallback + 882
      CoreFoundation                      0x3409c553 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ + 38
      CoreFoundation                      0x3409c4f5 __CFRunLoopDoSource1 + 140
      CoreFoundation                      0x3409b343 __CFRunLoopRun + 1370
      CoreFoundation                      0x3401e4dd CFRunLoopRunSpecific + 300
      CoreFoundation                      0x3401e3a5 CFRunLoopRunInMode + 104
      GraphicsServices                    0x308a2fcd GSEventRunModal + 156
      UIKit                               0x3749f743 UIApplicationMain + 1090
      MyiOSapplication                    0x000d4ccb main + 174
      MyiOSapplication                    0x000039c8 start + 40
    [ Fe Symbols End ]

    可以看到,即使我们没有编译时生成的符号文件,也能够打印出调用栈上的每个函数的名称,只是没有文件名和行号。

    那么,除了UI线程之外,自己创建的后台线程呢?运行NSRunLoop的后台线程的线程函数应该如下:
    - ( void ) threadProc: ( NSString* )threadName
    {
        NSThread* current = [ NSThread currentThread ];
        [ current setName: threadName ];
        NSAutoreleasePool *pool = [ [ NSAutoreleasePool alloc ] init ];
       
        // 一个没有实际作用的NSTimer,确保NSRunLoop不退出。不知道有没有更好的办法啊
        _dummyTimer = [ [ NSTimer timerWithTimeInterval: 10.0
                                                 target: self
                                               selector: @selector( dummyTimerProc: )
                                               userInfo: nil
                                                repeats: YES ] retain ];
       
        NSRunLoop *r = [ NSRunLoop currentRunLoop ];
        [ r addTimer: _dummyTimer forMode: NSDefaultRunLoopMode ];
        @try {
            // 启动后台线程的NSRunLoop
            [ r run ];
        }
        @catch ( NSException *exception ) {
            [ self handleRootException: exception ];
            // 一旦在线程根上捕捉到未知异常,记录异常后本线程退出
        }
        @finally {
            [ _dummyTimer invalidate ];
            [ _dummyTimer release ];
            [ pool release ];
        }
    }
    后台线程的handleRootException与UI线程基本一致。不过为了测试人员更加方便,其实只要不是UI线程发生未捕获异常,都可以先引导用户发送日志,再把进程崩溃掉。

  • 相关阅读:
    查看系统的所有port的状态
    python技巧26[python的egg包的安装和制作]
    python类库31[进程subprocess与管道pipe]
    [BuildRelease Management]hudson插件
    python类库31[使用xml.etree.ElementTree读写xml]
    开机自动运行VMWare
    python实例26[计算MD5]
    2021年最新大厂php+go面试题集(四)
    Jumpserver开源跳板机系统
    报错:ImportError: libmysqlclient.so.20: cannot open shared object file: No such file or director(亲测可用)
  • 原文地址:https://www.cnblogs.com/er-dai-ma-nong/p/4931039.html
Copyright © 2020-2023  润新知