背景
开发过程中,为了便于调试,会输出很多打印日志,而只有当电脑连着真机进行调试的时候,Xcode控制台才会有日志输出。这也就意味着如果未处于调试状态时,是看不到Xcode控制台的日志输出的,那么如果还想看到日志输出,那么这个功能就尤为重要了。
实现方案的调研与思考
iOS 开发语言有Objective-C和Swift,经销商项目中均有使用。Objective-C中的打印方法为NSLog,Swift中打印方法为print。这就意味着如果要实现此功能,就需要处理两个方法。
NSLog
NSLog是一个全局的C函数,函数声明如下:
FOUNDATION_EXPORT void NSLog(NSString *format, ...) NS_FORMAT_FUNCTION(1,2) NS_NO_TAIL_CALL;
这意味着用swizzle方法交换是行不通的。
经过调研,捕获NSLog日志有三种方式
1、iOS 10以前可以通过ASL接口来获取
有成熟的第三方库CocoaLumberjack。但是 需要使用他们公开的API,这于我们来说侵入性太大,完全没有必要。
2、通过fishhook库hook NSLog方法重定向NSLog函数
fishhook是Facebook提供的一个动态修改链接Mach-O文件的工具,能够hook C函数。
3、使用dup2函数和STDERR句柄重定向NSLog函数
NSLog最后重定向的句柄是STDERR,NSLog输出的日志内容,最终都通过STDERR句柄来记录,而dup2函数式专门进行文件重定向的;可以使用dup2重定向STDERR句柄,将内容重定向指定的位置。但是重定向之后,控制台无法正常输出
经考虑后选择更容易驾驭的第二种方案。
print是Swift的全局方法,函数声明如下:
func print(_ items: Any..., separator: String = " ", terminator: String = "\n")
Swift语言具备重载特性,我们可以在项目中重载该print方法。这样在项目中调用print方法后,会直接调用已重载后的print方法。但需保证Swift的print方法重载类在项目目录下,不能在组件下。
代码实现
Objective-C中NSLog的方法重定向(选用优化后的fishhook方案)
核心代码如下:
//函数指针,用来保存原始的函数的地址
static void(*old_nslog)(NSString *format, ...);
//新的NSLog
void myNSLog(NSString *format, ...){
va_list vl;
va_start(vl, format);
NSString* str = [[NSString alloc] initWithFormat:format arguments:vl];
va_end(vl);
[[DoraemonNSLogManager sharedInstance] addNSLog:str];
//再调用原来的nslog
//old_nslog(str);
old_nslog(@"%@",str);
}
@implementation DoraemonNSLogManager
+ (instancetype)sharedInstance {
static id instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
// 开启日志监听
- (void)startNSLogMonitor{
doraemon_rebind_symbols((struct doraemon_rebinding[1]){"NSLog", (void *)myNSLog, (void **)&old_nslog},1);
}
// 关闭日志监听
- (void)stopNSLogMonitor{
doraemon_rebind_symbols((struct doraemon_rebinding[1]){"NSLog", (void *)old_nslog, NULL},1);
}
// 日志写入缓存
- (void)addNSLog:(NSString *)log{
DoraemonNSLogModel *model = [[DoraemonNSLogModel alloc] init];
model.content = log;
model.timeInterval = [[NSDate date] timeIntervalSince1970];
if (!_dataArray) {
_dataArray = [[NSMutableArray alloc] init];
}
[_dataArray addObject:model];
}
Swift的print方法捕获(方法重载方案)
核心代码如下:
// 重载print方法
public func print(_ items: Any..., separator: String = " ", terminator: String = "\n") {
#if DEBUG
// 调用原系统的打印方法 保持控制台的正常输出
Swift.print(items, separator: separator, terminator: terminator)
// 添加swift日志打印
let isOn = DoraemonCacheManager.sharedInstance().nsLogSwitch()
guard isOn else { return }
let str = items.map { "\($0)" }.joined(separator: separator)
// 日志写入缓存
DoraemonNSLogManager.sharedInstance().addNSLog(str)
#endif
}
捕获这两个方法后,即可将输出的日志,写入到缓存中,然后再呈现到UI页面上,即可实现在设备上实时查看日志的功能。
如何使用
该方案无侵入,仅需要植入对应的代码即可。
在app的小工具下即可使用日志捕获功能。