需求
主要考虑到测试员测试app时候发现bug,但是不知道是移动端的bug还是后台数据的bug。如果可以把数据实时的打印到屏幕上方便测试员看到。测试员分析数据即可知道是哪边出现的问题。也方便把测试机给后台伙伴。方便后台伙伴观看数据入参出参,以便更容易发现问题。
效果
1. 日志按钮有拖动功能,防止遮挡屏幕的主要部分。
2. 长按日志按钮,清除文字。
3. 点击日志按钮弹出或者消失文字窗口,方便测试与查看日志的切换。
4. github地址:https://github.com/FreeBaiShun/BSLogWindow
用法
1. pod 'BSLogWindow'
2. 编写代码
#import "BSLogWindow.h"
#define ShowLogWindow 1
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [UIWindow new];
self.window.rootViewController = [ViewController new];
[self.window makeKeyAndVisible];
#if DEBUG && ShowLogWindow
BSLogWindow *logWindow = [BSLogWindow sharedInstance];
[logWindow setHidden:NO];
logWindow.printBlock = ^(NSString *str) {
};
#endif
return YES;
}
代码分析
1. 代码入口
- (instancetype)initWithFrame:(CGRect)frame{
self = [super initWithFrame:CGRectMake(0, 20, BSLogWindowSereenWidth, BSLogWindowSereenHeight/2.0+BSLogWindowNAVTOP)];
if (self) {
if ([UIApplication sharedApplication].keyWindow) {
[[UIApplication sharedApplication].keyWindow addObserver:self forKeyPath:@"rootViewController" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
[self setUpUI];
[self redirectSTD:STDERR_FILENO];
}
}
return self;
}
- 如果window存在就完成UI的初始化以及NSLog的处理
- 添加window的KVO监听rootViewController属性,考虑到app中途改变rootViewController导致视图销毁问题。
2. UI初始化部分
- 首先初始化self自身
- 完成textView初始化
- 接下来利用三方WMDragView初始化viewWindowBtn
- 添加日志按钮的点击事件,以及长按事件
3. NSLog部分的处理
- 编写方法redirectSTD
- 该方法创建了NSPipe,初始化读pipeReadHandle,写pipeFileHandle,再利用dup2函数把fd与pipeFileHandle进行重定向。这样在用NSLog时候就会重定向到pipeFileHandle,就会想NSPipe对象写。之后监听pipeReadHandle读端口即可。
- 这样当我们用NSLog时候其实就会向NSPipe的写端口写数据,这时候我们就会监听到读端口有数据流出。
- (void)redirectSTD:(int)fd{
NSPipe * pipe = [NSPipe pipe];
NSFileHandle *pipeReadHandle = [pipe fileHandleForReading];
int pipeFileHandle = [[pipe fileHandleForWriting] fileDescriptor];
dup2(pipeFileHandle, fd);
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(redirectNotificationHandle:)name:NSFileHandleReadCompletionNotification
object:pipeReadHandle];
[pipeReadHandle readInBackgroundAndNotify];
}
4. 之后在监听到数据后读出数据,并用printf函数把数据打印到控制台(不能用NSLog,会造成死循环)。然后调用[BSLogWindow printLog:str];方法完成界面上textView的文字输出。最后进行文本的输出回调。这个回调可以拦截到NSLog的内容,这些内容可以写在文件里,或者上传到服务器,或者是存到数据库等等。
- (void)redirectNotificationHandle:(NSNotification *)nf{
NSData *data = [[nf userInfo] objectForKey:NSFileHandleNotificationDataItem];
NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
printf("%s", [str UTF8String]);
[BSLogWindow printLog:str];
if(self.printBlock){
self.printBlock(str);
}
[[nf object] readInBackgroundAndNotify];
}
4. 屏幕输出部分
-
printLog方法的编写
-
该方法使用@synchronized (self)进行加锁处理避免了多线程访问self防止数据竞争问题。限制做多打印最新的20条日志信息。
- (void)printLog:(NSString*)newLog { if (newLog.length == 0) { return; } @synchronized (self) { newLog = [NSString stringWithFormat:@"%@\n", newLog]; // add new line BSLogModel* log = [BSLogModel logWithStr:newLog]; // data if (!log) { return; } [self.logsArrM addObject:log]; if (self.logsArrM.count > 20) { [self.logsArrM removeObjectAtIndex:0]; } // view [self refreshLogDisplay]; } }
-
refreshLogDisplay方法的编写
-
这里判断最后两天数据显示为黄色,其他数据显示为白色。这里主要考虑到如果是网络请求,有入参有出参情况。
- (void)refreshLogDisplay { // attributed text NSMutableAttributedString* attributedString = [NSMutableAttributedString new]; [self.logsArrM enumerateObjectsUsingBlock:^(BSLogModel *log, NSUInteger idx, BOOL * _Nonnull stop) { if (log.strLog.length == 0) { return; } NSMutableAttributedString* logString = [[NSMutableAttributedString alloc] initWithString:log.strLog]; UIColor* logColor = (idx == self.logsArrM.count - 1 || idx == self.logsArrM.count - 2) ? [UIColor yellowColor] : [UIColor whiteColor]; // yellow if new, white if more than 0.1 second ago [logString addAttribute:NSForegroundColorAttributeName value:logColor range:NSMakeRange(0, logString.length)]; [attributedString appendAttributedString:logString]; }]; self.textView.attributedText = attributedString; // scroll to bottom if(attributedString.length > 0) { NSRange bottom = NSMakeRange(attributedString.length - 1, 1); [self.textView scrollRangeToVisible:bottom]; } }