經營你的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__,能將代碼文件名和行號也加入到日志中,就看有沒有必要了。

稍后繼續寫





免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM