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

经营你的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应用日志(二):异常日志 

言归正传。开发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: @"\r\n" ];
    }

    // 写日志,级别为ERROR
    writeCinLog( __FUNCTION__, CinLogLevelError, @"[ Uncaught Exception ]\r\nName: %@, Reason: %@\r\n[ Fe Symbols Start ]\r\n%@[ 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线程发生未捕获异常,都可以先引导用户发送日志,再把进程崩溃掉。

转载于:https://www.cnblogs.com/codingking/archive/2013/01/17/2865061.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/550019.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java代码情书_程序员们的爱情表白书

下面看看我们程序员是如何用自己的语言说出爱你。就算闷呆&#xff0c;也要闷呆得很性感。java程序员的情书我能抽象出整个世界&#xff0e;&#xff0e;&#xff0e;但是我不能抽象出你&#xff0e;&#xff0e;&#xff0e;因为你在我心中是那么的具体&#xff0e;&#xff0…

motorola 企业移动解决方案

&#xff08;一&#xff09;moto数据采集解决方案 数据采集和查询&#xff0c;可随时随地记录并传输各种形式的数据 安全便捷接入企业数据库&#xff0c;实现数据前后台无缝整合 终端小巧&#xff0c;方便携带&#xff1b;快速、低成本的实现高效移动远程管理 &#xff08;二&a…

使用乱序标签来控制HTML的输出效果

在HTML的元素中&#xff0c;有一个比较特殊的元素form。我们用它来收集表单数据并提交给服务器&#xff0c;并且理论上说来它是没有任何的UI被呈现的。当然如果我们在body元素后紧跟一个form&#xff0c;这样一来似乎看不出来有什么UI呈现的问题&#xff0c;可是当form存在于别…

杭电2019多校第一场,Problem I,String 2019

题目描述 Tom has a string containing only lowercase letters. He wants to choose a subsequence of the string whose length is k and lexicographical order is the smallest. Its simple and he solved it with ease.But Jerry, who likes to play with Tom, tells him …

2024年最新Python爬虫入门『最强教程』新鲜出炉!

近年来&#xff0c;大数据成为业界与学术界最火热的话题之一&#xff0c;数据已经成为每个公司极为重要的资产。互联网大量的公开数据为个人和公司提供了以往想象不到的可以获取的数据量。而掌握网络爬虫技术可以帮助你获取这些有用的公开数据集。 爬虫能干什么呢&#xff1f;一…

使用LUA构建页面

Android&#xff0c;LUA&#xff0c;CoronaCard 使用CoronaCard引擎构建页面&#xff0c;只需要嵌入CoronaView&#xff0c;就可以构建灵活多样的页面。 微游手柄游戏厅 4.0版本&#xff0c;全面改版&#xff0c;使用LUA语言&#xff0c;通过CoronaCard引擎构建。 一 界面效果&…

C#实现Singleton (转载)

您要在 C# 中构建应用程序。您需要只有一个实例的类&#xff0c;并且需要提供一个用于访问实例的全局访问点。您希望确保您的解决方案高效&#xff0c;并且能够利用 Microsoft? .NET 公共语言运行库功能。您可能还希望确保解决方案是线程安全的。 实现策略 尽管 Singleton 是一…

JQuery VS JS DOM

JQ断断续续的也用了一阵子了.对我来说确实是个小小的突破。之前的javascript 简直是 一塌糊涂。。糊涂到 连:document.getElementById("") 这个语句都不会写。哈哈这也算是选择JQ的一个理由吧&#xff0c;因为JQ 只需要$("#ID") $(".Class") 就可…

java设计模式组合模式详解_《JAVA设计模式》之组合模式(Composite)

在阎宏博士的《JAVA与模式》一书中开头是这样描述合成(Composite)模式的&#xff1a;html合成模式属于对象的结构模式&#xff0c;有时又叫作“部分——总体”模式。合成模式将对象组织到树结构中&#xff0c;能够用来描述总体与部分的关系。合成模式可使客户端将单纯元素与复合…

XAF Excel数据导入模块使用说明与源码

我实现了XAF项目中Excel数据的导入&#xff0c;使用Devexpress 新出的spreadsheet控件&#xff0c;可能也不新了吧:D 好&#xff0c;先看一下效果图&#xff1a;下图是Web版本的。 下面是win版&#xff1a; 功能说明&#xff1a; 支持从Excel任意版本导入数据&#xff0c;可以使…

ASP注入漏洞基础教程(二)

进 阶 篇 在入门篇&#xff0c;我们学会了&#xff33;&#xff31;&#xff2c;注入的判断方法&#xff0c;但真正要拿到网站的保密内容&#xff0c;是远远不够的。接下来&#xff0c;我们就继续学习如何从数据库中获取想要获得的内容&#xff0c;首先&#xff0c;我们先看看&…

linux 内存清理/释放命令

1.清理前内存使用情况 free -m2.开始清理 echo 1 > /proc/sys/vm/drop_caches3.清理后内存使用情况 free -m4.完成!查看内存条数命令&#xff1a; dmidecode | grep -A16 "Memory Device$" # sync# echo 1 > /proc/sys/vm/drop_caches echo 2 > /proc/sys…

dotnet程序优化心得(三)

&#xff08;4&#xff09;继续优化――用空间换取时间 现在对每一个字符&#xff0c;都要用get_Item(object key)方法过一遍&#xff0c;可这个乖乖方法那么长&#xff0c;肯定太耗时间了&#xff0c;能不能用更简单的手段呢&#xff1f;改Hashtable&#xff1f;哇&#xff0c…

【Mininet】Mininet使用源码安装

实验参考&#xff1a; Mininet使用源码安装 实验步骤&#xff1a; 1. 更新软件&#xff08;用#sudo apt-get update与#sudo apt-get upgrade&#xff09;。 2. 从github上获取mininet源码&#xff08;#git clone git://github.com/mininet/mininet&#xff09;。 3. 获取完后&a…

【Mininet】Mininet可视化应用

实验参考&#xff1a; Mininet可视化应用 实验步骤&#xff1a; 1. 用命令启动mininet可视化界面&#xff08;#cd mininet/mininet/example #./miniedit.py&#xff09;&#xff0c;同时开启另一终端打开Opendaylight。 2. 用鼠标选择左侧的对应的网络组件&#xff0c;然后在空…

【Mininet】Mininet命令延伸实验扩展

实验参考&#xff1a; Mininet命令延伸实验扩展 实验步骤&#xff1a; 1.用命令实现 #sudo mn --topo minimal #最小的网络拓扑&#xff0c;一个交换机下挂两个主机。 #sudo mn --topo linear,4 #每个交换机连接一个主机&#xff0c;交换机间相连接。本例&#xff1a;4个主机&a…

java非检查性异常有哪些_Java异常处理-检查性异常、非检查性异常、Error

一、Java异常处理详解异常.png目录&#xff1a;1.java中异常的分类1.1 异常(Exception)1.1.1 运行时异常(RuntimeException)可以不需要捕获1.1.2 编译异常(IOException)编译器会提示要捕获&#xff0c;如果不进行捕获则编译器会报错1.2 错误(Error)3.java处理异常机制4.throw和…

【Mininet】基于Mininet测量路径的损耗率

实验参考&#xff1a; 基于Mininet测量路径的损耗率 SDN常用控制器安装部署之POX篇 实验步骤&#xff1a; 1. 在装有mininet的虚拟机中新建文件mymininet.py并编辑以下内容&#xff0c;这里要注意一点&#xff0c;文中的dp0与dp1须填POX安装的虚拟机的地址&#xff0c;由于本次…

【Mininet】Mininet设置带宽之简单性能测试

实验参考&#xff1a; Mininet设置带宽之简单性能测试 实验步骤&#xff1a; 1. 进入mininet/custom目录下&#xff0c;通过vi mymininet1.py创建脚本并添加内容&#xff08;本实验通过python脚本自定义拓扑&#xff0c;创建包含一个交换机、四个主机的网络拓扑&#xff09;&am…

java 配置tocat_Tomcat安装配置及Eclipse配置详解

整个安装过程我们先学习安装jdk和配置然后是安装tomcat和配置&#xff0c;最后我们学习安装eclipse和配置以及web程序的使用和发布举例1. 安装jdk和配置(1)下载jdk安装包&#xff1a;(2)按照步骤点击下一步进行jdk软件的安装。(3)配置javajdk的环境变量**配置环境变量包括java_…