日志幾乎是我們每一個(gè)iOS開(kāi)發(fā)者每一天都要打交道的東西饿这,比如運(yùn)行時(shí)想看一下某個(gè)變量的值,那就NSLog()
一下;當(dāng)然手趣,在Swift語(yǔ)言下我們還有另外一種選擇,那就是print()
方法倔韭。都是幫助我們方便調(diào)試與分析的工具。
開(kāi)源社區(qū)也為我們貢獻(xiàn)了很多非常優(yōu)秀的日志框架虹菲,比如OC中大名鼎鼎的CocoaLumberjack
,有興趣的同學(xué)可以移步https://github.com/CocoaLumberjack/CocoaLumberjack
開(kāi)源日志框架是為了我們方便極了日志而開(kāi)發(fā)的掉瞳,比如我們可以調(diào)用CocoaLumberjack的DDLogInfo("Info")
來(lái)記錄一個(gè)Info
類型的日志毕源,用DDLogError("Error")
來(lái)記錄一個(gè)Error
類型的日志。不同的日志記錄框架我們需要調(diào)用的API也不一樣陕习,但是他們都有一個(gè)共同的特點(diǎn)霎褐,就是到最后都會(huì)調(diào)用系統(tǒng)的NSLog()
方法。
因此我們需要做日志全自動(dòng)監(jiān)控的話该镣,可以從NSLog()
方法入手冻璃。這里我們會(huì)分兩方面來(lái)講,因?yàn)樵趇OS上我們不單單可以用NSLog()
來(lái)記錄日志,在Swift語(yǔ)言下還可以用print()
方法來(lái)記錄我們的日志省艳。這一章我們會(huì)先介紹如何選擇和優(yōu)化我們監(jiān)控NSLog()
日志的方案娘纷,然后比較NSLog()
與print()
的不同點(diǎn)。然后再介紹Swift下特有的print()
方法的處理跋炕。
0 前言
該文章摘自筆者撰寫(xiě)的一本iOS技術(shù)書(shū)籍iOS監(jiān)控編程赖晶。
這是一本介紹iOS監(jiān)控編程的書(shū)籍,內(nèi)容涉及日志監(jiān)控,監(jiān)控崩潰枣购,監(jiān)控網(wǎng)絡(luò)嬉探,監(jiān)控卡頓,監(jiān)控硬件棉圈,內(nèi)存泄漏監(jiān)控等方面涩堤。所有的功能都是通過(guò)自行編程實(shí)現(xiàn),而不是通過(guò)使用第三方工具分瘾。每個(gè)章節(jié)記錄了功能的實(shí)現(xiàn)細(xì)節(jié)胎围,以及筆者一路探索過(guò)來(lái)的心路歷程。當(dāng)然德召,筆者后續(xù)依舊會(huì)尋求與探索新的監(jiān)控方向白魂,一旦有所得都會(huì)更新到本書(shū)的章節(jié)中。
這本書(shū)是筆者的開(kāi)源工具GodEye的產(chǎn)出物上岗,GodEye能全自動(dòng)福荸,零代碼入侵,一行代碼接入來(lái)監(jiān)控應(yīng)用的日志肴掷,卡頓敬锐,崩潰,網(wǎng)絡(luò)呆瞻,內(nèi)存泄漏台夺,CPU以及內(nèi)存使用率,幀率等信息痴脾。
1 監(jiān)控NSLog日志
該章節(jié)涉及到的開(kāi)源庫(kù)ASLEye
,大家可以先通過(guò)https://github.com/zixun/ASLEye下載下來(lái)颤介,一遍對(duì)照開(kāi)源代碼一遍閱讀該章節(jié)。
可能你在翻開(kāi)該章節(jié)前已經(jīng)有了自己的方案了赞赖,因?yàn)檫@道題并沒(méi)有唯一解滚朵,我們只是要找出最優(yōu)解。下面讓我們來(lái)逐一分析監(jiān)控NSLog日志的方案前域。
自定義API
可能你和我一樣始绍,第一個(gè)想到的就是模仿CocoaLumberjack
,咱整一套自己的API话侄,讓大家都來(lái)調(diào)咱的API不完事了么,多省事。比如LogEyeInfo("Info")
就是Info
類型的日志年堆,LogEyeError("Error")
就是Error
類型的日志吞杭。
哈哈,咱是省事了变丧,但是接入咱日志監(jiān)控的開(kāi)發(fā)者不干了芽狗,或許他正在和CocoaLumberjack愉快的玩耍,你儂我儂痒蓬,你突然讓我全換了童擎,你他媽的在逗我?
的確攻晒,這樣接入方的開(kāi)發(fā)者的工作量就太大了顾复,而且也和全自動(dòng)記錄日志沾不上邊,不是我們的最佳方案鲁捏,換芯砸!
NSLog宏
相信大家這個(gè)時(shí)候也都想到了NSLog宏的方案。咱可以定義一個(gè)NSLog
的宏给梅,去覆蓋掉系統(tǒng)的NSLog
的方法假丧。在咱的NSLog
的宏里面去調(diào)用我們第一種方案中的自定義API就好。比如
#define NSLog(frmt, ...) AppInfo(frmt, ##__VA_ARGS__)
用這招偷梁換柱的方法动羽,我們就可以在我們的AppInfo
里面撒歡的處理我們的日志啦包帚,用戶一調(diào)我們就知道,用戶一調(diào)我們就知道运吓,好開(kāi)心啊渴邦。趕緊找我們的接入方開(kāi)發(fā)者來(lái)接入。
“我不接!”
“為啥子喲羽德?”
“因?yàn)槲椰F(xiàn)在用的開(kāi)源日志記錄框架也有了一個(gè)NSLog
宏几莽,把你的接入了日志記錄框架的NSLog
宏會(huì)被你覆蓋掉,也有可能你的宏會(huì)被他的宏覆蓋掉宅静,誰(shuí)先加載誰(shuí)被覆蓋章蚣!”
“......(握草泥馬)”
看來(lái),NSLog
宏的方案雖然好姨夹,但是一旦出現(xiàn)另一個(gè)NSLog
宏就會(huì)出現(xiàn)“一山不容二宏”纤垂,“宏和宏不可兼得”的現(xiàn)象。我們不能保證接入方的開(kāi)發(fā)者們不會(huì)接入其他編寫(xiě)了NSLog
宏的第三方庫(kù)磷账,也不能保證接入方的開(kāi)發(fā)者自己不會(huì)寫(xiě)一個(gè)NSLog
宏峭沦。由此可見(jiàn),NSLog
宏的方案雖好逃糟,但是存在易沖突吼鱼,不可靠蓬豁,不可控等問(wèn)題。換菇肃!
尋找新的方案
現(xiàn)在地粪,兩個(gè)方案都被我們排除了,還有啥比這兩個(gè)更好的方案可以用呢琐谤。如果腦海里暫時(shí)沒(méi)有好的方案與想法蟆技,我們就應(yīng)該祭出我們程序員的基本生存技能,問(wèn)度娘斗忌。质礼。。织阳。眶蕉。。不是陈哑,問(wèn)Google妻坝!
首先,我們?nèi)ニ阉饕幌翧pple關(guān)于NSLog
的文檔惊窖。我們會(huì)發(fā)現(xiàn)NSLog
方法中調(diào)用的是NSLogv
方法刽宪。Apple文檔中對(duì)于NSLogv
函數(shù)是這樣寫(xiě)的Logs an error message to the Apple System Log facility
(參見(jiàn)https://developer.apple.com/reference/foundation/1395074-nslogv?language=objc)。
這句話有兩個(gè)信息我們需要重點(diǎn)關(guān)注下界酒,一個(gè)就是error message
,還有一個(gè)就是Apple System Log
,為啥NSLog
是記錄錯(cuò)誤信息的圣拄,而不是日志信息呢?Apple System Log
又是個(gè)什么鬼毁欣?
不急庇谆,我們?cè)偎阉飨?code>CocoaLumberjack的文檔,我們會(huì)發(fā)現(xiàn)CocoaLumberjack
的文檔說(shuō)了這么兩句話:
NSLog does 2 things:
It writes log messages to the Apple System Logging (asl) facility. This allows log messages to show up in Console.app.
It also checks to see if the application's stderr stream is going to a terminal (such as when the application is being run via Xcode). If so it writes the log message to stderr (so that it shows up in the Xcode console).
CocoaLumberjack
的文檔告訴我們凭疮,當(dāng)我們寫(xiě)了一句NSLog
的時(shí)候饭耳,他會(huì)做兩件事情:1.把日志寫(xiě)到Apple System Log
里面。2.把日志展示到Xcode的控制臺(tái)上面执解。
兩個(gè)文檔都提到了Apple System Log
寞肖,那這個(gè)Apple System Log
到底是個(gè)什么鬼。其實(shí)Apple System Log
可以理解為就是一個(gè)我們?cè)O(shè)備的日志數(shù)據(jù)庫(kù)衰腌,這里存放了所有應(yīng)用所有進(jìn)程產(chǎn)生的日志新蟆。也就是說(shuō),不管在哪右蕊,只要你調(diào)用了NSLog
,系統(tǒng)就會(huì)把它寫(xiě)到Apple System Log
中琼稻。
那為啥NSLog
是記錄錯(cuò)誤信息的,而不是日志信息呢饶囚?其實(shí)Apple設(shè)計(jì)NSLog
的時(shí)候就是一個(gè)記錄錯(cuò)誤日志的API帕翻,不然他也不會(huì)把日志寫(xiě)到一個(gè)叫Apple System Log
的數(shù)據(jù)庫(kù)里面鸠补,要知道記錄日志是一個(gè)高頻發(fā)的操作,如果每條都放到數(shù)據(jù)庫(kù)中熊咽,是一件很浪費(fèi)性能的事情莫鸭。所以我覺(jué)得應(yīng)該叫NSLogError
才更合適。
NSLog
耗性能的一個(gè)原因也是因?yàn)樾枰讶罩緮?shù)據(jù)寫(xiě)到Apple System Log
數(shù)據(jù)庫(kù)横殴,所以大伙的App線上Release版本盡量不要寫(xiě)NSLog
,不但耗性能卿拴,而且不安全衫仑。Swift的print
方法就不會(huì)把日志寫(xiě)到數(shù)據(jù)庫(kù)中。當(dāng)然在Swift中并沒(méi)有廢棄掉NSLog
方法堕花,但是Swift對(duì)NSLog
方法做了優(yōu)化文狱,只有在模擬器環(huán)境下才會(huì)將日志寫(xiě)入Apple System Log
。
Apple System Log
OK缘挽,我們找到了我們的方案:Apple System Log
瞄崇。Apple為Apple System Log
提供了一個(gè)framework供大家使用來(lái)對(duì)Apple System Log
的數(shù)據(jù)進(jìn)行操作。他就是asl
,那下面我們就來(lái)試試怎么通過(guò)代碼把Apple System Log
的數(shù)據(jù)撈出來(lái)壕曼。
首先苏研,我們需要把我們的asl
模塊import進(jìn)來(lái):
import asl
然后我們創(chuàng)建一個(gè)專門用來(lái)?yè)艫SL的日志的類ASLEye
,并且做個(gè)Timer
定時(shí)器,定時(shí)拉取ASL的數(shù)據(jù):
public class ASLEye: NSObject {
public func open(with interval:TimeInterval) {
self.timer = Timer.scheduledTimer(timeInterval: interval,target: self,selector: #selector(ASLEye.pollingLogs),userInfo: nil,repeats: true)
}
public func close() {
self.timer?.invalidate()
self.timer = nil
}
private var timer: Timer?
}
好了腮郊,現(xiàn)在就讓我們來(lái)實(shí)現(xiàn)pollingLogs
方法摹蘑,來(lái)拉取我們的數(shù)據(jù)。
@objc private func pollingLogs() {
//TODO 調(diào)用拉取數(shù)據(jù)API轧飞,獲得數(shù)據(jù)數(shù)組
//TODO 判斷數(shù)據(jù)數(shù)組是否為空
//TODO 將數(shù)據(jù)數(shù)組回調(diào)給上層
}
咱先不著急pollingLogs
方法的具體實(shí)現(xiàn)衅鹿。大家可以和我一樣先注釋好方法要做的事情。
這里我們要先介紹下asl
的api的特點(diǎn)过咬。asl
的api類似SQL語(yǔ)句大渤,你可以往查詢語(yǔ)句里面加參數(shù)來(lái)過(guò)濾數(shù)據(jù),比如要查詢當(dāng)前App的日志的話掸绞,需要設(shè)置App的BundleID泵三,當(dāng)然,我們每次啟動(dòng)我們的App的時(shí)候都是不同的進(jìn)程集漾,所以我們還需要設(shè)置當(dāng)前App的進(jìn)程ID切黔。因此我們可以編寫(xiě)一個(gè)initQuery
的方法來(lái)生成我們的查詢語(yǔ)句:
private func initQuery() -> aslmsg {
var query: aslmsg = asl_new(UInt32(ASL_TYPE_QUERY))
//set BundleIdentifier to ASL_KEY_FACILITY
let bundleIdentifier = (Bundle.main.bundleIdentifier! as NSString).utf8String
asl_set_query(query, ASL_KEY_FACILITY, bundleIdentifier, UInt32(ASL_QUERY_OP_EQUAL))
//set pid to ASL_KEY_PID
let pid = NSString(format: "%d", getpid()).cString(using: String.Encoding.utf8.rawValue)
asl_set_query(query, ASL_KEY_PID, pid, UInt32(ASL_QUERY_OP_NUMERIC))
return query
}
然后我們來(lái)編寫(xiě)拉取數(shù)據(jù)API,獲得數(shù)據(jù)數(shù)組的API具篇,這個(gè)API要做的事情就是調(diào)用initQuery()
方法生成我們的查詢語(yǔ)句纬霞,然后解析返回的response,將response解析成一個(gè)字符串的數(shù)組返回給調(diào)動(dòng)著驱显,咱就叫他retrieveLogs()
方法:
private func retrieveLogs() -> [String] {
var logs = [String]()
var query: aslmsg = self.initQuery()
let response: aslresponse? = asl_search(nil, query)
guard response != nil else {
return logs
}
var message = asl_next(response)
while (message != nil) {
let log = self.log(from: message!)
logs.append(log)
message = asl_next(response)
}
asl_free(response)
asl_free(query)
return logs
}
private func parserLog(from message:aslmsg) ->String {
let content = asl_get(message, ASL_KEY_MSG)!;
return String(cString: content, encoding: String.Encoding.utf8)!
}
好诗芜,現(xiàn)在我們讓pollingLogs()
來(lái)調(diào)用我們的retrieveLogs()
方法:
@objc private func pollingLogs() {
let logs = self.retrieveLogs()
if logs.count > 0 {
self.delegate?.aslMonitor?(aslMonitor: self, didMonitorLogs: logs)
}
}
這樣我們的日志就能通過(guò)我們的delegate方法回調(diào)給上層了瞳抓。當(dāng)然在這之前我們需要編寫(xiě)一個(gè)我們這個(gè)delegate的protocol
:
@objc public protocol ASLEyeDelegate: class {
@objc optional func aslEye(aslEye:ASLEye,catchLogs logs:[String])
}
然后在ASLEye
上添加一個(gè)delegate的變量:
public weak var delegate: ASLEyeDelegate?
但是,這個(gè)時(shí)候大家去調(diào)用NSLog會(huì)發(fā)現(xiàn)存在一個(gè)問(wèn)題伏恐。比如我調(diào)用了下NSLog("Hello")
,我們的日志監(jiān)控每次輪詢都會(huì)取到我們的日志數(shù)據(jù)孩哑,我們的delegate會(huì)一直回調(diào),把Hello
給上層翠桦。
會(huì)造成這樣其實(shí)也不難理解横蜒,因?yàn)锳pp System Log是一個(gè)數(shù)據(jù)庫(kù),按照咱現(xiàn)在的查詢語(yǔ)句销凑,每次插敘的時(shí)候當(dāng)然會(huì)把當(dāng)前App當(dāng)前的日志全部返回給我們丛晌,所以,我們還需要設(shè)置一個(gè)'游標(biāo)'斗幼,換句話說(shuō)就是我們需要在我們的查詢語(yǔ)句里加一個(gè)參數(shù)澎蛛,將我們上次查到的最后一個(gè)日志的id給塞進(jìn)去,然后查詢大于這個(gè)ID的日志蜕窿。
OK,首先谋逻,我們來(lái)設(shè)置一個(gè)變量lastMessageID
,用來(lái)記錄最后一次查到的日志ID:
private var lastMessageID: Int32 = 0
然后在我們的initQuery()
方法里最后返回我們的query前將我們的lastMessageID
給塞進(jìn)去:
private func initQuery() -> aslmsg {
var query: aslmsg = asl_new(UInt32(ASL_TYPE_QUERY))
//set BundleIdentifier to ASL_KEY_FACILITY
let bundleIdentifier = (Bundle.main.bundleIdentifier! as NSString).utf8String
asl_set_query(query, ASL_KEY_FACILITY, bundleIdentifier, UInt32(ASL_QUERY_OP_EQUAL))
//set pid to ASL_KEY_PID
let pid = NSString(format: "%d", getpid()).cString(using: String.Encoding.utf8.rawValue)
asl_set_query(query, ASL_KEY_PID, pid, UInt32(ASL_QUERY_OP_NUMERIC))
if self.lastMessageID != 0 {
let m = NSString(format: "%d", self.lastMessageID).utf8String
asl_set_query(query, ASL_KEY_MSG_ID, m, UInt32(ASL_QUERY_OP_GREATER | ASL_QUERY_OP_NUMERIC));
}
return query
}
最后桐经,我們需要在每次解析我們的日志的時(shí)候?qū)⑽覀兊娜罩綢D給記錄下來(lái)放到lastMessageID
中:
private func parserLog(from message:aslmsg) ->String {
let content = asl_get(message, ASL_KEY_MSG)!;
let msg_id = asl_get(message, ASL_KEY_MSG_ID);
let m = atoi(msg_id);
if (m != 0) {
self.lastMessageID = m;
}
return String(cString: content, encoding: String.Encoding.utf8)!
}
OK,這樣毁兆,我們就能夠全自動(dòng)的監(jiān)控我們的日志啦。上層開(kāi)發(fā)者還是按照他們的方式玩就好次询,我們都能拿到他們的日志荧恍。
該章節(jié)涉及到的開(kāi)源庫(kù)ASLEye
有興趣的同學(xué)可以移步https://github.com/zixun/ASLEye查看。
<br />
<br />
更好的方案
通過(guò)ASL來(lái)監(jiān)控NSLog日志雖然能很好的完成自動(dòng)監(jiān)控的功能屯吊,但是畢竟Apple System Log
是一個(gè)數(shù)據(jù)庫(kù)送巡,每次做查詢操作由于會(huì)產(chǎn)生I/0操作,所以會(huì)產(chǎn)生一定的性能消耗盒卸。雖然這點(diǎn)性能消耗在日常應(yīng)用的運(yùn)行時(shí)完全可以忽略不計(jì)骗爆,但是當(dāng)我們?cè)诮y(tǒng)計(jì)應(yīng)用整個(gè)流程的CPU使用率的時(shí)候還是會(huì)造成一定的干擾,那有沒(méi)有更好的方案呢蔽介?
當(dāng)然有摘投。那就是將NSLog
方法給hook掉。不過(guò)當(dāng)我們點(diǎn)開(kāi)NSLog
方法的時(shí)候可以發(fā)現(xiàn)這個(gè)API并不是一個(gè)Objective-C語(yǔ)言的方法虹蓄,而是一個(gè)C語(yǔ)言包裝的API犀呼。C語(yǔ)言的API當(dāng)然不能用我們Runtime的Method Swizzle來(lái)替換,他并不會(huì)走OC的運(yùn)行時(shí)薇组。
不過(guò)方法總比問(wèn)題多外臂,F(xiàn)acebook有一個(gè)名字叫fishhook的開(kāi)源庫(kù),就可以做到將我們的Mach-O
的庫(kù)的符號(hào)表重新綁定律胀,因此應(yīng)該也能將我們的NSLog
重新綁定宋光,重新綁定后就可以做我們的日志監(jiān)控啦貌矿。
有興趣的同學(xué)可以自己實(shí)驗(yàn)下,筆者也會(huì)在今后GodEye
的更新迭代中探索替換該方案來(lái)實(shí)現(xiàn)全自動(dòng)的日志監(jiān)控罪佳。當(dāng)然也會(huì)將實(shí)現(xiàn)原理編寫(xiě)成該書(shū)的一個(gè)章節(jié)提供給大家逛漫。
1 監(jiān)控print日志
前一章節(jié)我們分析了NSLog
日志的監(jiān)控,不過(guò)接觸過(guò)Swift語(yǔ)言的同學(xué)都知道,Swift中大家一般都是用print
來(lái)記錄日志赘艳。當(dāng)然酌毡,Swift并沒(méi)有將NSLog
給廢棄掉。
NSLog In Swift
當(dāng)然蕾管,Swift中我們也可以用NSLog
來(lái)做日志記錄阔馋,但是Swift對(duì)NSLog
做過(guò)優(yōu)化。筆者發(fā)現(xiàn)Swift中調(diào)用NSLog
方法娇掏,它并不會(huì)想Objective-C中調(diào)用NSLog
方法那樣將日志放入Apple System Log
中,模擬器環(huán)境下除外勋眯。也就是說(shuō)我們?cè)谡鎸?shí)環(huán)境下并不能通過(guò)ALS
獲取NSLog
的日志信息婴梧。因此我們需要尋找其他的辦法。
自定義print方法
我們來(lái)看看Swift中print
方法的定義:
public func print(_ items: Any..., separator: String = default, terminator: String = default)
其中separator
的默認(rèn)值是一個(gè)" "(空格字符串)客蹋。terminator
的默認(rèn)值是"\n"(回車符)塞蹭。
我們自定義的print
方法不能完全和Swift的一樣,不然編譯器會(huì)給你一個(gè)報(bào)錯(cuò)Ambiguous use of 'print(_:separator:terminator:)'
,因?yàn)閮蓚€(gè)API一模一樣,編譯器就不知道去鏈接哪個(gè)API了讶坯。
不過(guò)我們可以把默認(rèn)值去掉番电,只需要一個(gè)item參數(shù)就好:
<br />
public func print(_ items: Any...) {
//TODO: Custom Actions
Swift.print(items)
}
這樣,我們只需要在使用print方法的swift文件中辆琅,import我們這個(gè)自定義api的庫(kù)就好了漱办。然后將TODO: Custom Actions
中放入我們的日志監(jiān)控操作即可。
自定義日志框架
自定義print方法固然不錯(cuò)婉烟,但是使用者很容易忘了import我們這個(gè)自定義api的庫(kù)娩井,Swift中也沒(méi)有預(yù)加載的功能,因此使用者很容易忘記做import操作似袁,導(dǎo)致監(jiān)控不到洞辣。而且單獨(dú)一個(gè)print方法無(wú)法優(yōu)雅的區(qū)分日志的類型,比如log,warning,error等昙衅。更不能獲取文件名扬霜,方法名等日志輔助信息。因此我們何嘗不自定義一個(gè)輕量級(jí)的日志框架呢而涉。
該日志框架大家可通過(guò)https://github.com/zixun/Log4G下載著瓶,文中由于篇幅的原因,只會(huì)列出關(guān)鍵代碼婴谱,建議將開(kāi)源源碼下載后對(duì)照著源碼查看蟹但,會(huì)更便于理解躯泰。
我們先來(lái)看一張Log4G
的簡(jiǎn)要設(shè)計(jì)圖:
從圖上我們可以看出,我們對(duì)外有三個(gè)接口华糖,分別是log()
,warning()
和error()
麦向。他們內(nèi)部都會(huì)調(diào)用一個(gè)不公開(kāi)的接口record()
,當(dāng)然在方法中會(huì)自動(dòng)獲取日志的文件(file),行數(shù)(line)客叉,方法名(function)以及線程(thread)诵竭,然后傳遞給record()
方法,record()
方法內(nèi)部所有的操作都在一個(gè)指定的queue中進(jìn)行兼搏,可以防止多線程的問(wèn)題卵慰。record()
會(huì)做兩件事,一個(gè)就是調(diào)用系統(tǒng)的print()
函數(shù)佛呻,還有一個(gè)就是通知Log4g
內(nèi)部維護(hù)的delegate數(shù)組裳朋,告訴日志監(jiān)聽(tīng)者們新的日志信息。
我們先來(lái)看看log()
的實(shí)現(xiàn):
/// record a log type message
///
/// - Parameters:
/// - message: log message
/// - file: file which call the api
/// - line: line number at file which call the api
/// - function: function name which call the api
open class func log(_ message: Any = "",
file: String = #file,
line: Int = #line,
function: String = #function) {
self.shared.record(type: .log,
thread: Thread.current,
message: "\(message)",
file: file,
line: line,
function: function)
}
我們通過(guò)#file
,#line
,#function
可以自動(dòng)獲取調(diào)用API時(shí)候的文件名吓著,行數(shù)以及方法名鲤嫡,不需要調(diào)用者參與。然后通過(guò)Thread.current
拿到當(dāng)前線程绑莺,一并傳遞給record
方法暖眼。這樣調(diào)用者只需要傳遞日志即可,其他一切都是自動(dòng)纺裁。
warning()
和error()
原理一致诫肠,那我們來(lái)看看record()
方法:
/// record message base function
///
/// - Parameters:
/// - type: log type
/// - thread: thread which log the messsage
/// - message: log message
/// - file: file which call the api
/// - line: line number at file which call the api
/// - function: function name which call the api
private func record(type:Log4gType,
thread:Thread,
message: String,
file: String,
line: Int,
function: String) {
self.queue.async {
let model = LogModel(type: type,
thread: thread,
message: message,
file: self.name(of: file),
line: line,
function: function)
print(message)
for delegate in self.delegates {
delegate.delegate?.log4gDidRecord(with: model)
}
}
}
record()
方法內(nèi)部通過(guò)self.queue.async{...}
將所有的操作都同步在一個(gè)統(tǒng)一的線程中,然后調(diào)用系統(tǒng)的print()
方法并生成一個(gè)日志的model欺缘,告訴Log4g
內(nèi)部維護(hù)的delegate數(shù)組監(jiān)聽(tīng)到的新的日志栋豫。這里涉及成delegate數(shù)組可以方便日志監(jiān)聽(tīng)的擴(kuò)展,Log4g
顧名思義浪南,就是Log4GodEye
,就是設(shè)計(jì)給GodEye
使用的日志記錄框架笼才,GodEye
是delegate的一員,但是可能用戶的App其他地方也需要監(jiān)控日志络凿,只要將其加入到delegate即可骡送。
附GodEye
開(kāi)源地址:https://github.com/zixun/GodEye,GodEye
是一個(gè)可以自動(dòng)監(jiān)控日志絮记,崩潰摔踱,卡頓,網(wǎng)絡(luò)怨愤,內(nèi)存泄漏派敷,沙盒文件,CPU,RAM,F(xiàn)PS,流量等信息的工具篮愉,功能豐富腐芍,just like god open his eye。
delegate數(shù)組的坑
在Objective-C中试躏,我們將delegate放入一個(gè)數(shù)組中完全沒(méi)有問(wèn)題猪勇,因?yàn)?code>NSArray不會(huì)強(qiáng)引用放進(jìn)來(lái)的對(duì)象。但是Swift中的Array
是是一個(gè)結(jié)構(gòu)體颠蕴,會(huì)強(qiáng)引用放入的元素泣刹。比如我們的delegate是一個(gè)ViewController,將我們的ViewController放入Array后犀被,只要Array不釋放椅您,我們的ViewController就永遠(yuǎn)不會(huì)釋放,但是ViewController可能已經(jīng)被pop出去了寡键,這就造成了內(nèi)存泄漏掀泳。那怎么辦呢?
只要做一層weak化的包裝就好西轩,就拿Log4g
來(lái)說(shuō)开伏,內(nèi)部維護(hù)了一個(gè)delegate的數(shù)組:
fileprivate var delegates = [WeakLog4GDelegate]()
那WeakLog4GDelegate
是個(gè)什么鬼呢,這個(gè)屬性的申明是fileprivate
的遭商,說(shuō)明用戶是不知道WeakLog4GDelegate
的,我們來(lái)看看WeakLog4GDelegate
:
class WeakLog4GDelegate: NSObject {
weak var delegate : Log4GDelegate?
init (delegate: Log4GDelegate) {
super.init()
self.delegate = delegate
}
}
可以看到捅伤,WeakLog4GDelegate
是Log4GDelegate
的一個(gè)weak化包裝劫流,內(nèi)部有一個(gè)Log4GDelegate
的weak屬性,也通過(guò)Log4GDelegate
來(lái)初始化丛忆。
這樣當(dāng)我們將WeakLog4GDelegate
放入數(shù)組后祠汇,外部的Log4GDelegate就不會(huì)因?yàn)閺?qiáng)制引用而無(wú)法釋放了。當(dāng)然WeakLog4GDelegate
并不是一個(gè)open
或者public
的類熄诡,所以我們還需要包裝一些方法讓使用者添加或刪除外部使用的Log4GDelegate
:
open class func add(delegate:Log4GDelegate) {
let log4g = self.shared
// delete null week delegate
log4g.delegates = log4g.delegates.filter {
return $0.delegate != nil
}
// judge if contains the delegate from parameter
let contains = log4g.delegates.contains {
return $0.delegate?.hash == delegate.hash
}
// if not contains, append it with weak wrapped
if contains == false {
let week = WeakLog4GDelegate(delegate: delegate)
self.shared.delegates.append(week)
}
}
open class func remove(delegate:Log4GDelegate) {
let log4g = self.shared
log4g.delegates = log4g.delegates.filter {
// filter null weak delegate
return $0.delegate != nil
}.filter {
// filter the delegate from parameter
return $0.delegate?.hash != delegate.hash
}
}