简单说两句

objective-c 强大的runtime机制,让开发者不用掌握源码都可以拦截原始方法做一些自己想做的事。

Method Swizzling 不说了。

鉴于开发者常用NSLog、fprintf等方法做日志输出,本文记录如何拦截交换NSLog、fprintf等C方法,并持久化到沙盒目录。

感谢 fishhookCocoaLumberjack

写一个APP内拦截NSLog的宏

这个宏可以将历史源码中的原始NSLog方法做拦截,转移到自定义的方法中,本文基于CocoaLumberjack做日志输出。

#ifdef DEBUG
static const DDLogLevel ddLogLevel = DDLogLevelVerbose;
#else
static const DDLogLevel ddLogLevel = DDLogLevelVerbose ^ DDLogFlagDebug;
#endif
#define NSLog(frmt, ...)   do {DDLogDebug(frmt, ##__VA_ARGS__); } while(0)

这样,APP内大量历史原因沉淀下来的原始NSLog方法都可以路由到DDLog里统一维护了。 建议基于DDLog做一个针对Log的适配封装,本文仅做简单演示,先忽略这层封装。

这就够了吗

这还是不够的,由于项目中引入了一些framework,有些framework内部的日志不方便在APP内输出、查看。

搞起来

写一个通用的拦截交换方法


/**
 拦截、监听C方法
 
 @param orignFunctionName 原始方法名,char字符串
 @param newFunction 本地实现的新的函数
 @param cachedOldFunction 用来保存原始函数的函数
 */
- (void)startCFunctionMonitor:(const char *__nonnull)orignFunctionName withNewFunction:(void* __nonnull)newFunction andCachedOldFunction:(void** __nullable)cachedOldFunction{
    struct rebinding logBind;
    //要拦截的原始函数名称
    logBind.name = orignFunctionName;
    //本地实现的新的函数
    logBind.replacement = newFunction;
    //保存原始函数
    logBind.replaced = cachedOldFunction;
    
    struct rebinding rebs[] = {logBind};
    rebind_symbols(rebs, 1);
}

写一个拦截实现方法

//用来保存原始的NSLog函数的地址
static void(*qg_old_nslog)(NSString *format, ...);

void QGNSLog(NSString *format, ...){
    
    va_list vl;
    va_start(vl, format);
    NSString* str = [[NSString alloc] initWithFormat:format arguments:vl];
    va_end(vl);
    
    NSLog(str);//这个NSLog是上面写的宏,已经不是原始的NSLog了
    
//    qg_old_nslog(@"%@",str);//如果需要,可以让原始方法做输出
}

void  qg_fprintf(FILE * file, const char * format, ...){
    
    va_list vl;
    va_start(vl, format);
    NSString* str = [[NSString alloc] initWithFormat:[NSString stringWithFormat:@"%s",format] arguments:vl];
    va_end(vl);
    
    NSLog(str);//这个NSLog是上面写的宏,已经不是原始的NSLog了

    
}

APP内的配置

基于以上步骤,APP内、无源码的framework等场景都可以拦截到常用的日志输出方法了。

APP内初始化的时候做个简单的配置


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  
    [self configLog];
    
    ....
    return YES;
}

// 配置日志
- (void)configLog {
    //拦截漏网的NSLog方法
    [self startCFunctionMonitor:"NSLog" withNewFunction:QGNSLog andCachedOldFunction:(void**)&qg_old_nslog];
    //拦截fprintf方法
    [self startCFunctionMonitor:"fprintf" withNewFunction:qg_fprintf andCachedOldFunction:NULL];
    
    //输出到控制台
    [DDLog addLogger:[DDASLLogger sharedInstance]]; // ASL = Apple System Logs
    
    /* 将日志存储到本地 */
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
    NSString *baseDir = paths.firstObject;
    NSString *logsDirectory = [baseDir stringByAppendingPathComponent:@"AppLogs"];
    DDLogFileManagerDefault *logFileManager = [[DDLogFileManagerDefault alloc] initWithLogsDirectory:logsDirectory];
    DDFileLogger *fileLogger = [[DDFileLogger alloc] initWithLogFileManager:logFileManager];
    fileLogger.rollingFrequency = 60 * 60 * 24;
    fileLogger.logFileManager.maximumNumberOfLogFiles = 20;
    fileLogger.maximumFileSize = 1024 * 1024 * 2;
    [DDLog addLogger:fileLogger];

}

后续

建议集成 DoraemonKit ,很方便开发者做调试使用。 基于 DoraemonKit 提供的能力,可以快速查看沙盒目录里自己存储的日志了,可以预览、分享(导出)。

自由发挥。