我們在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獲取方式
