相信每个客户端的app都有日志框架, 基本做法是屏蔽 NSLog
, 自定义一个可以改变格式的Log, 并且用#if DEBUG
宏来控制开关. 程序上线之后没有客户端日志查询, 失去一个解决线上问题的途径. 如果打开Log开关又很影响程序效率,还会不断增加程序占用的存储空间.
有时候是这么解决的, 当用户反应一个不容易找到问题的bug, 此时程序员可能会单独为这个用户打出一个带Log的包, 让用户装上帮助debug, 这样一是繁琐, 二是用户不一定乐意配合.
所以需要程序员能随时改变线上用户的log开关, 做法很简单, 由后台控制每一位用户的log开关.
下面是Log类, 一个开关属性, 一个自定义Log格式输出方法, 一个Log写入文件方法. DEBUG的时候直接输出到控制台, release且Log开关开的时候写入文件
- Log.h
#define NSLog(...)
#define PKLog(frmt,...) [Log logWithLine:__LINE__ method:[NSString stringWithFormat:@"%s", __FUNCTION__] time:[NSDate date] format:[NSString stringWithFormat:frmt, ## __VA_ARGS__]]
@interface Log : NSObject
+ (void)setFileLogOnOrOff:(BOOL)on;
+ (void)logWithLine:(NSUInteger)line
method:(NSString *)methodName
time:(NSDate *)timeStr
format:(NSString *)format;
@end
- Log.m
@implementation Log
static BOOL _fileLogOnOrOff;
+ (void)setFileLogOnOrOff:(BOOL)on {
_fileLogOnOrOff = on;
}
+ (void)logWithLine:(NSUInteger)line
method:(NSString *)methodName
time:(NSDate *)timeStr
format:(NSString *)format {
#if DEBUG
// 日志时间格式化, 不用看
NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
NSInteger unitFlags = NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitWeekday |
NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond | NSCalendarUnitNanosecond;
NSDateComponents *comps = [calendar components:unitFlags fromDate:[NSDate date]];
NSString *time = [NSString stringWithFormat:@"%ld/%ld,%ld:%ld:%ld:%@", (long)comps.month, (long)comps.day, (long)comps.hour, (long)comps.minute, (long)comps.second, [[NSString stringWithFormat:@"%ld", (long)comps.nanosecond] substringToIndex:2]];
// debug 直接输出
fprintf(stderr,"[%s]%s %tu行: ● %s.\n", [time UTF8String],[methodName UTF8String],line,[format UTF8String]);
#else
// release && 文件Log开 写入文件
if (_fileLogOnOrOff) {
// 日志时间格式化, 不用看
NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
NSInteger unitFlags = NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay | NSCalendarUnitWeekday |
NSCalendarUnitHour | NSCalendarUnitMinute | NSCalendarUnitSecond | NSCalendarUnitNanosecond;
NSDateComponents *comps = [calendar components:unitFlags fromDate:[NSDate date]];
NSString *time = [NSString stringWithFormat:@"%ld/%ld,%ld:%ld:%ld:%@", (long)comps.month, (long)comps.day, (long)comps.hour, (long)comps.minute, (long)comps.second, [[NSString stringWithFormat:@"%ld", (long)comps.nanosecond] substringToIndex:2]];
// 写入文件
NSString *logStr = [NSString stringWithFormat:@"[%@]%@ %tu行: ● %@.\n", time, methodName,line,format];
[self writeLogWithString:logStr];
}
#endif
}
+ (void)writeLogWithString:(NSString *)content
{
NSString *filePath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"my_log.text"];
NSError *error = nil;
NSFileManager *fileManager = [NSFileManager defaultManager];
if(![fileManager fileExistsAtPath:filePath]) //如果不存在
{
[content writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:&error];
if (error) {
PKLog(@"文件写入失败 errorInfo: %@", error.domain);
}
}
NSFileHandle *fileHandle = [NSFileHandle fileHandleForUpdatingAtPath:filePath];
[fileHandle seekToEndOfFile];
NSData* stringData = [content dataUsingEncoding:NSUTF8StringEncoding];
[fileHandle writeData:stringData]; // 追加
[fileHandle synchronizeFile];
[fileHandle closeFile];
}
@end
请求接口控制Log开关
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 可以多处放这一段代码, 放到此处是因为进后台的操作较少, 不会在请求期间漏掉log信息
[self requestFileLogOnOrOffWithUseId:88088 complete:^(BOOL offOrOn) {
// 既是DEBUG用, 此处也可做成同步, 使用dispatch_semaphore CGD信号量即可, 一般不需要这么极端.
[Log setFileLogOnOrOff:offOrOn];
}];
return YES;
}
触发Log
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
PKLog(@"调用了 touchesBegan 方法");
// 写一个数组越界的crash配合后面的异常捕获
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self crash];
});
}
- (void)crash {
NSArray *array = @[@1,@2];
NSNumber *num = array[3];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
PKLog(@"调用了 touchesEnded 方法");
}
crash日志的捕获, 可以在程序启动调用[self initHandler]初始化异常捕获系统
- (void)initHandler {
struct sigaction newSignalAction;
memset(&newSignalAction, 0,sizeof(newSignalAction));
newSignalAction.sa_handler = &signalHandler;
sigaction(SIGABRT, &newSignalAction, NULL);
sigaction(SIGILL, &newSignalAction, NULL);
sigaction(SIGSEGV, &newSignalAction, NULL);
sigaction(SIGFPE, &newSignalAction, NULL);
sigaction(SIGBUS, &newSignalAction, NULL);
sigaction(SIGPIPE, &newSignalAction, NULL);
//异常时调用的函数
NSSetUncaughtExceptionHandler(&handleExceptions);
}
void signalHandler(int sig) {
// 打印crash信号信息
PKLog(@"signal = %d", sig);
}
void handleExceptions(NSException *exception) {
PKLog(@"exception = %@",exception);
// 打印堆栈信息
PKLog(@"callStackSymbols = %@",[exception callStackSymbols]);
}
将日志文件上传服务器, 可以是自己的服务器, 也可以用三方的, 这里用的阿里云的
- (void)applicationDidEnterBackground:(UIApplication *)application {
// 上传阿里云
[self uploadAliyunComplete:^(NSError *error, NSString *filePath) {
if(!error) {
// 此处根据需要上传成功后删除日志文件
[self deleteWithPath:filePath];
}
}];
}
按照步骤操作完了以后, 登录阿里云下载日志文件, 可以看到目录, 这个目录根据阿里云sdk自己设置
log内容
得到有用信息后, 服务器关闭此用户的log开关即可
整个步骤不是很复杂, 但很能解决问题.