我们在app中对崩溃、卡顿、内存问题进行监控。一旦监控到问题,我们就需要记录下来,但是,很多问题的定位仅靠问题发生的那一刹那记录的信息是不够的,我们需要记录app的全量日志来获取更多的信息。

一,使用NSLog获取全量日志,通过CocoaLumberjack第三方库获取系统日志

对NSLog进行重定向采用Hook方式,因为NSLog时C的函数,使用fishHook实现重定向,具体实现如下:

static void (&orig_nslog)(NSString *format, ...);
 
void redirect_nslog(NSString *format, ...) {
    // 可以在这里先进行自己的处理
    
    // 继续执行原 NSLog
    va_list va;
    va_start(va, format);
    NSLogv(format, va);
    va_end(va);
}
 
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        struct rebinding nslog_rebinding = {"NSLog",redirect_nslog,(void*)&orig_nslog};
 
        NSLog(@"try redirect nslog %@,%d",@"is that ok?");
    }
    return

    可以看到,我在上面这段代码中,利用了fishhook 对方法的符号地址进行了重新绑定,从而

只要是NSL og的调用就都会转向redirect_ nslog 方法调用。

     在redirect_ nslog 方法中,你可以先进行自己的处理,比如将日志的输出重新输出到自己的持

久化存储系统里,接着调用NSLog也会调用的NSL _ogv方法进行原NSLog方法的调用。当

然了,你也可以使用fishhook提供的原方法调用方式orig_ _nslog, 进行原NSLog方法的调

用。上面代码里也已经声明了类orig_ nslog, 直接调用即可。

     NSL og最后写文件时的句柄是STDERR,我先前跟你说了苹果对于NSL og的定义是记录错

误的信息,STDERR的全称是standard error,系统错误日志都会通过STDERR句柄来记

录,所以NSLog最终将错误日志进行写操作的时候也会使用STDERR句柄,而dup2函数是

专门进行文件重定向的,那么也就有了另一个不使用fishhook还可以捕获NSLog日志的方

法。你可以使用dup2重定向STDERR句柄,使得重定向的位置可以由你来控制,关键代码

如下:

int fd = open(path, (O_RDWR | O_CREAT), 0644);
dup2(fd, STDERR_FILENO);

path 就是你自定义的重定向输出的文件地址。

 

二,自己创建日志文件,定期上传,获取日志信息

第三方库 https://github.com/CocoaLumberjack/CocoaLumberjack 具体查看github,现在主要说说自己创建日志文件

1。创建log类

class Log {
//创建成单利,便于全局调用
    static var shareInstance = Log()
    var writeFileQueue: DispatchQueue
    //log文件的存储路径   
    var logFile: Path {
        get {
            let now = Date()
            let fileName = "ErrorLog_\(now.year)_\(now.month)_\(now.day).txt"
            
            if !Path.cacheDir["Logs"].exists {
                _ = Path.cacheDir["Logs"].mkdir()
            }
            
            if !Path.cacheDir["Logs"][fileName].exists {
                _ = Path.cacheDir["Logs"][fileName].touch()
                
                let write = DispatchWorkItem(qos: .background, flags: .barrier) {
                    let file = FileHandle(forUpdatingAtPath: Path.cacheDir["Logs"][fileName].asString)
                    file?.seekToEndOfFile()
                    file?.write(self.getDeviceInfo().data(using: String.Encoding.utf8)!)
                }
                writeFileQueue.async(execute: write)
                
                //删除30天以前的Log文件
                if let files = Path.cacheDir["Logs"].contents {
                    let sortedFiles = files.sorted { (p1, p2) -> Bool in
                        guard let attribute1 = p1.attributes else { return false }
                        guard let attribute2 = p2.attributes else { return false }
                        if let date1 = attribute1[FileAttributeKey.creationDate] as? Date, let date2 = attribute2[FileAttributeKey.creationDate] as? Date {
                            return date1 < date2
                        } else {
                            return false
                        }
                    }
                    
                    if sortedFiles.count > 30 {
                        _ = sortedFiles.first!.remove()
                    }
                }
                
            }
            return Path.cacheDir["Logs"][fileName]
        }
    }
    
    fileprivate init() {
        writeFileQueue = DispatchQueue(label: "写日志线程", qos: DispatchQoS.default, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil)
        let _ = logFile
    }
    //添加日志的全局方法
    func log(message: String, toCloudKit: Bool = false) {
        let now = Date()
        let m = convertToVisiable(str: message)
        let string = "\(now.string()) : \(m)\n"
        let write = DispatchWorkItem(qos: .background, flags: .barrier) {
            let file = FileHandle(forUpdatingAtPath: self.logFile.asString)
            file?.seekToEndOfFile()
            file?.write(string.data(using: String.Encoding.utf8)!)
        }
        
        writeFileQueue.async(execute: write)
    }
    //获取当前日志的方法
    func readLog() -> String? {
//展示日志信息,添加一些项目需要的信息
        var debugStr = "BaseURL: \(BaseUrl)"
        if let registerID = PalauDefaults.registerid.value {
             debugStr += "\nRegisterID: \(registerID);"
        }
       //log文件中的内容
         if let readStr = logFile.readString() {
            debugStr += "\n \(readStr)"
        }
        return debugStr
    }
}

 

2.在全局添加日志

Log.shareInstance.log(message: “login”)

 

3.查看当前日志(今天的)展示在textview上

if let logString = Log.shareInstance.readLog() {
                let textView = UITextView(frame: CGRect(x: 0, y:0, width: 600, height: 400))
                textView.center = view.center
                view.addSubview(textView)
                textView.text = logString
                if textView.text.count > 0 {
                    let location = textView.text.count - 1
                    let bottom = NSMakeRange(location, 1)
                    textView.scrollRangeToVisible(bottom)
                }
            }

 

4通过通知方式定期上传日志文件

在AppDelegate中上传日志文件到服务器或发送日志文件到相应邮箱

func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
        ) {
            _ = SyncTask.sendLogEmail(email: email)
           
        }
        
  class func sendLogEmail(email: String) -> Promise<Void> {
        var sendEmail = "aldelo@126.com"
        if email.trim().count > 0 {
            sendEmail = email
        }
        
        let syncUrl = PalauDefaults.syncurl.value ?? ""
        
        let url = syncUrl + "/express/email/endofday"
        return firstly {
            uploadDatabase()//上传日志文件到服务器,方法实现在下边
            }.then { fileUrl in
                return Promise<Void> { seal in
                    
                    let storeName = PalauDefaults.storename.value ?? ""
                    let path = Path.temporaryDir["\(storeName)-LogFileAddress.txt"]
                    if path.exists {
                        _ = path.remove()//日志文件已经上传到服务端,删除本地的
                    }
                    let tmpPath = path.asString
                    
                    try? fileUrl.data(using: .utf8, allowLossyConversion: true)?.write(to: URL(fileURLWithPath: tmpPath))
                    
                    
                    Alamofire.upload(multipartFormData: { multipartFormData in
                        multipartFormData.append(URL(fileURLWithPath: tmpPath), withName: "attachments")
                        multipartFormData.append(sendEmail.data(using: .utf8, allowLossyConversion: true)!, withName: "emailaddress")
                    }, usingThreshold: UInt64.init(), to: url, method: .post, headers: ECTicket(), encodingCompletion: { encodingResult in
                        switch encodingResult {
                        case .success(let upload, _, _):
                            upload.responseJSON { response in
                                let json = JSON(response.data as Any)
                                if let errCode = json["err_code"].int , errCode != 0 {
                                    seal.reject(NSError(domain: json["err_msg"].stringValue, code: errCode, userInfo: nil))
                                    return
                                }
                                
                                seal.fulfill(())
                            }
                        case .failure(let encodingError):
                            print(encodingError)
                            seal.reject(encodingError)
                        }
                    })
                }
        }
    }
//上传的方法
class func uploadDatabase() -> Promise<String> {
        return Promise<String> { seal in
            DispatchQueue.global().async {
                let uploadDir = Path.cacheDir["UploadLog"]
                if !uploadDir.exists {
                    _ = uploadDir.mkdir()
                }
                
                var files = [URL]()
                
                let zipFilePath = URL(fileURLWithPath: uploadDir.toString() + “/database.zip”)//压缩文件的名字
                
                if Path.cacheDir["Logs"].exists {
                    if let contents = Path.cacheDir["Logs"].contents {
                        for file in contents {
                            files.append(URL(fileURLWithPath: file.toString()))  //添加每个日志文件路径
                        }
                    }
                }
                do {//压缩所有的日志文件
                    try Zip.zipFiles(paths: files, zipFilePath: zipFilePath, password: nil, progress: { (progress) -> () in
                        print(progress)
                    })
                } catch let error as NSError {
                    seal.reject(error)
                    return
                }
                
                let headers = CUTicket()
                let uploadUrl = BaseUrl + "/express/device/upload/localdata/" + PalauDefaults.storeID.value!
                let deviceGlobalID = PalauDefaults.terminalguid.value!
                let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
                var deviceNumber = ""
                if let dnumber = getDeviceNumber() {
                    deviceNumber = "\(dnumber)"
                }
//以数据流的方式上传 ,默认的是上传数据的大小大于10M的时候采用数据流的方式上传
                Alamofire.upload(multipartFormData: { multipartFormData in
                    multipartFormData.append(deviceGlobalID.data(using: String.Encoding.utf8, allowLossyConversion: false)!, withName :"DeviceGlobalID")
                    multipartFormData.append(deviceNumber.data(using: String.Encoding.utf8, allowLossyConversion: false)!, withName :"DeviceNumber")
                    multipartFormData.append(version.data(using: String.Encoding.utf8, allowLossyConversion: false)!, withName :"DeviceVersion")
                    multipartFormData.append(zipFilePath, withName :"file")
                    multipartFormData.append(PalauDefaults.storeID.value!.data(using: String.Encoding.utf8)!, withName: "storeid")
                }, usingThreshold: UInt64.init(), to: uploadUrl, method: .post, headers: headers, encodingCompletion: { encodingResult in
                    switch encodingResult {
                    case .success(let upload, _, _):
                        upload.responseJSON { response in
                            guard let value = response.result.value else {
                                seal.reject(NSError(domain: "response value is Null", code: 0, userInfo: nil))
                                return
                            }
                            let json = JSON(value)
                            if let err_code = json["err_code"].int {
                                seal.reject(NSError(domain: json["err_msg"].stringValue, code: err_code, userInfo: nil))
                            } else {
                                if let url = json["url"].string {
                                    seal.fulfill(url)
                                } else {
                                    seal.reject(NSError(domain: "response value is Null", code: 0, userInfo: nil))
                                }
                            }
                        }
                    case .failure(let encodingError):
                        seal.reject(encodingError)
                    }
                })
            }
        }

以上时我们的项目中日志的使用具体流程,可以借鉴一下,实现自己的log获取方式