APP的性能監(jiān)控包括: CPU 占用率纤垂、 內(nèi)存使用情況花竞、網(wǎng)絡(luò)狀況監(jiān)控虏两、啟動(dòng)時(shí)閃退荣瑟、卡頓征炼、FPS、使用時(shí)崩潰吼鱼、耗電量監(jiān)控蓬豁、流量監(jiān)控等等。
文中所有代碼都已同步到github中菇肃,有興趣的可以clone下來一起探討下地粪。
1 . CPU 占用率
CPU作為手機(jī)的中央處理器,可以說是手機(jī)最關(guān)鍵的組成部分琐谤,所有應(yīng)用程序都需要它來調(diào)度運(yùn)行蟆技,資源有限。所以當(dāng)我們的APP因設(shè)計(jì)不當(dāng),使 CPU 持續(xù)以高負(fù)載運(yùn)行质礼,將會(huì)出現(xiàn)APP卡頓旺聚、手機(jī)發(fā)熱發(fā)燙、電量消耗過快等等嚴(yán)重影響用戶體驗(yàn)的現(xiàn)象眶蕉。
因此我們對(duì)應(yīng)用在CPU中占用率的監(jiān)控砰粹,將變得尤為重要。那么我們應(yīng)該如何來獲取CPU的占有率呢妻坝?!
我們都知道惊窖,我們的APP在運(yùn)行的時(shí)候刽宪,會(huì)對(duì)應(yīng)一個(gè)Mach Task,而Task下可能有多條線程同時(shí)執(zhí)行任務(wù)界酒,每個(gè)線程都是作為利用CPU的基本單位圣拄。所以我們可以通過獲取當(dāng)前Mach Task下,所有線程占用 CPU 的情況毁欣,來計(jì)算APP的 CPU 占用率庇谆。
在《OS X and iOS Kernel Programming》是這樣描述 Mach task 的:
任務(wù)(task)是一種容器(container)對(duì)象,虛擬內(nèi)存空間和其他資源都是通過這個(gè)容器對(duì)象管理的凭疮,這些資源包括設(shè)備和其他句柄饭耳。嚴(yán)格地說,Mach
的任務(wù)并不是其他操作系統(tǒng)中所謂的進(jìn)程执解,因?yàn)?Mach 作為一個(gè)微內(nèi)核的操作系統(tǒng)寞肖,并沒有提供“進(jìn)程”的邏輯,而只是提供了最基本的實(shí)現(xiàn)衰腌。不過在
BSD 的模型中新蟆,這兩個(gè)概念有1:1的簡單映射,每一個(gè) BSD 進(jìn)程(也就是 OS X 進(jìn)程)都在底層關(guān)聯(lián)了一個(gè) Mach 任務(wù)對(duì)象右蕊。
iOS
是基于 Apple Darwin 內(nèi)核琼稻,由kernel、XNU和Runtime 組成饶囚,而XNU 是Darwin 的內(nèi)核帕翻,它是“X is not
UNIX”的縮寫,是一個(gè)混合內(nèi)核萝风,由 Mach 微內(nèi)核和 BSD 組成熊咽。Mach
內(nèi)核是輕量級(jí)的平臺(tái),只能完成操作系統(tǒng)最基本的職責(zé)闹丐,比如:進(jìn)程和線程横殴、虛擬內(nèi)存管理、任務(wù)調(diào)度、進(jìn)程通信和消息傳遞機(jī)制等衫仑。其他的工作梨与,例如文件操作和設(shè)備訪問,都由
BSD 層實(shí)現(xiàn)文狱。
iOS 的線程技術(shù)與Mac OS X類似粥鞋,也是基于 Mach 線程技術(shù)實(shí)現(xiàn)的,在 Mach 層中thread_basic_info 結(jié)構(gòu)體封裝了單個(gè)線程的基本信息:
structthread_basic_info{
time_value_tuser_time;/*?user?run?time?*/
time_value_tsystem_time;/*?system?run?time?*/
integer_tcpu_usage;/*?scaled?cpu?usage?percentage?*/
policy_tpolicy;/*?scheduling?policy?in?effect?*/
integer_trun_state;/*?run?state?(see?below)?*/
integer_tflags;/*?various?flags?(see?below)?*/
integer_tsuspend_count;/*?suspend?count?for?thread?*/
integer_tsleep_time;/*?number?of?seconds?that?thread??has?been?sleeping?*/
}
一個(gè)Mach Task包含它的線程列表瞄崇。內(nèi)核提供了task_threads API 調(diào)用獲取指定 task 的線程列表呻粹,然后可以通過thread_info API調(diào)用來查詢指定線程的信息,在 thread_act.h 中有相關(guān)定義苏研。
task_threads 將target_task 任務(wù)中的所有線程保存在act_list數(shù)組中等浊,act_listCnt表示線程個(gè)數(shù):
kern_return_ttask_threads
(
task_ttarget_task,
thread_act_array_t*act_list,
mach_msg_type_number_t*act_listCnt
);
thread_info結(jié)構(gòu)如下:
kern_return_tthread_info
(
thread_act_ttarget_act,
thread_flavor_tflavor,//?傳入不同的宏定義獲取不同的線程信息
thread_info_tthread_info_out,//?查詢到的線程信息
mach_msg_type_number_t*thread_info_outCnt//?信息的大小
);
所以我們?nèi)缦聛慝@取CPU的占有率:
#import"LSLCpuUsage.h"
#import
#import
#import
#import
#import
@implementation?LSLCpuUsage
+?(double)getCpuUsage?{
kern_return_tkr;
thread_array_tthreadList;//?保存當(dāng)前Mach?task的線程列表
mach_msg_type_number_tthreadCount;//?保存當(dāng)前Mach?task的線程個(gè)數(shù)
thread_info_data_tthreadInfo;//?保存單個(gè)線程的信息列表
mach_msg_type_number_tthreadInfoCount;//?保存當(dāng)前線程的信息列表大小
thread_basic_info_tthreadBasicInfo;//?線程的基本信息
//?通過“task_threads”API調(diào)用獲取指定?task?的線程列表
//??mach_task_self_,表示獲取當(dāng)前的?Mach?task
kr?=?task_threads(mach_task_self(),?&threadList,?&threadCount);
if(kr?!=?KERN_SUCCESS)?{
return-1;
}
doublecpuUsage?=0;
for(inti?=0;?i?<?threadCount;?i++)?{
threadInfoCount?=?THREAD_INFO_MAX;
//?通過“thread_info”API調(diào)用來查詢指定線程的信息
//??flavor參數(shù)傳的是THREAD_BASIC_INFO摹蘑,使用這個(gè)類型會(huì)返回線程的基本信息筹燕,
//??定義在?thread_basic_info_t?結(jié)構(gòu)體,包含了用戶和系統(tǒng)的運(yùn)行時(shí)間衅鹿、運(yùn)行狀態(tài)和調(diào)度優(yōu)先級(jí)等
kr?=?thread_info(threadList[i],?THREAD_BASIC_INFO,?(thread_info_t)threadInfo,?&threadInfoCount);
if(kr?!=?KERN_SUCCESS)?{
return-1;
}
threadBasicInfo?=?(thread_basic_info_t)threadInfo;
if(!(threadBasicInfo->flags?&?TH_FLAGS_IDLE))?{
cpuUsage?+=?threadBasicInfo->cpu_usage;
}
}
//?回收內(nèi)存撒踪,防止內(nèi)存泄漏
vm_deallocate(mach_task_self(),?(vm_offset_t)threadList,?threadCount?*sizeof(thread_t));
returncpuUsage?/?(double)TH_USAGE_SCALE?*100.0;
}
@end
2. 內(nèi)存
雖然現(xiàn)在的手機(jī)內(nèi)存越來越大,但畢竟是有限的大渤,如果因?yàn)槲覀兊膽?yīng)用設(shè)計(jì)不當(dāng)造成內(nèi)存過高制妄,可能面臨被系統(tǒng)“干掉”的風(fēng)險(xiǎn),這對(duì)用戶來說是毀滅性的體驗(yàn)泵三。
Mach task 的內(nèi)存使用信息存放在mach_task_basic_info結(jié)構(gòu)體中 忍捡,其中resident_size 為應(yīng)用使用的物理內(nèi)存大小,virtual_size為虛擬內(nèi)存大小切黔,在task_info.h中:
#defineMACH_TASK_BASIC_INFO?????20/*?always?64-bit?basic?info?*/
structmach_task_basic_info{
mach_vm_size_tvirtual_size;/*?virtual?memory?size?(bytes)?*/
mach_vm_size_tresident_size;/*?resident?memory?size?(bytes)?*/
mach_vm_size_tresident_size_max;/*?maximum?resident?memory?size?(bytes)?*/
time_value_tuser_time;/*?total?user?run?time?for
terminated?threads?*/
time_value_tsystem_time;/*?total?system?run?time?for
terminated?threads?*/
policy_tpolicy;/*?default?policy?for?new?threads?*/
integer_tsuspend_count;/*?suspend?count?for?task?*/
};
獲取方式是通過task_infoAPI 根據(jù)指定的 flavor 類型砸脊,返回 target_task 的信息,在task.h中:
kern_return_ttask_info
(
task_name_ttarget_task,
task_flavor_tflavor,
task_info_ttask_info_out,
mach_msg_type_number_t*task_info_outCnt
);
筆者嘗試過使用如下方式獲取內(nèi)存情況纬霞,基本和騰訊的GT的相近凌埂,但是和Xcode和Instruments的值有較大差距:
//?獲取當(dāng)前應(yīng)用的內(nèi)存占用情況,和Xcode數(shù)值相差較大
+?(double)getResidentMemory?{
structmach_task_basic_infoinfo;
mach_msg_type_number_tcount?=?MACH_TASK_BASIC_INFO_COUNT;
if(task_info(mach_task_self(),?MACH_TASK_BASIC_INFO,?(task_info_t)&info,?&count)?==?KERN_SUCCESS)?{
returninfo.resident_size?/?(1024*1024);
}else{
return-1.0;
}
}
后來看了一篇博主討論了這個(gè)問題诗芜,說使用phys_footprint才是正解瞳抓,博客地址。親測伏恐,基本和Xcode的數(shù)值相近孩哑。
//?獲取當(dāng)前應(yīng)用的內(nèi)存占用情況,和Xcode數(shù)值相近
+?(double)getMemoryUsage?{
task_vm_info_data_tvmInfo;
mach_msg_type_number_tcount?=?TASK_VM_INFO_COUNT;
if(task_info(mach_task_self(),?TASK_VM_INFO,?(task_info_t)?&vmInfo,?&count)?==?KERN_SUCCESS)?{
return(double)vmInfo.phys_footprint?/?(1024*1024);
}else{
return-1.0;
}
}
博主文中提到:關(guān)于 phys_footprint 的定義可以在 XNU 源碼中翠桦,找到 osfmk/kern/task.c 里對(duì)于 phys_footprint 的注釋横蜒,博主認(rèn)為注釋里提到的公式計(jì)算的應(yīng)該才是應(yīng)用實(shí)際使用的物理內(nèi)存胳蛮。
/*
*?phys_footprint
*???Physical?footprint:?Thisisthe?sumof:
*?????+?(internal?-?alternate_accounting)
*?????+?(internal_compressed?-?alternate_accounting_compressed)
*?????+?iokit_mapped
*?????+?purgeable_nonvolatile
*?????+?purgeable_nonvolatile_compressed
*?????+?page_table
*
*?internal
*???The?task's?anonymous?memory,?which?on?iOS?is?always?resident.
*
*?internal_compressed
*???Amountofthis?task's?internal?memory?which?is?held?by?the?compressor.
*???Such?memoryisno?longer?actually?residentforthe?task?[i.e.,?residentinits?pmap],
*andcould?be?either?decompressed?backintomemory,orpaged?outtostorage,?depending
*onour?implementation.
*
*?iokit_mapped
*???IOKit?mappings:?The?total?sizeofall?IOKit?mappingsinthis?task,?regardlessof
clean/dirtyorinternal/external?state].
*
*?alternate_accounting
*???The?numberofinternal?dirty?pages?which?are?partofIOKit?mappings.Bydefinition,?these?pages
*???are?countedinboth?internal?*and*?iokit_mapped,?so?we?must?subtract?themfromthe?totaltoavoid
*doublecounting.
*/
當(dāng)然我也是贊同這點(diǎn)的>.<。
3. 啟動(dòng)時(shí)間
APP的啟動(dòng)時(shí)間丛晌,直接影響用戶對(duì)你的APP的第一體驗(yàn)和判斷仅炊。如果啟動(dòng)時(shí)間過長,不單單體驗(yàn)直線下降澎蛛,而且可能會(huì)激發(fā)蘋果的watch
dog機(jī)制kill掉你的APP抚垄,那就悲劇了,用戶會(huì)覺得APP怎么一啟動(dòng)就卡死然后崩潰了谋逻,不能用呆馁,然后長按APP點(diǎn)擊刪除鍵。(Xcode在debug模式下是沒有開啟watch
dog的毁兆,所以我們一定要連接真機(jī)測試我們的APP)
在衡量APP的啟動(dòng)時(shí)間之前我們先了解下浙滤,APP的啟動(dòng)流程:
APP的啟動(dòng)可以分為兩個(gè)階段,即main()執(zhí)行之前和main()執(zhí)行之后荧恍〈山校總結(jié)如下:
t(App 總啟動(dòng)時(shí)間) = t1( main()之前的加載時(shí)間 ) + t2( main()之后的加載時(shí)間 )屯吊。
t1 = 系統(tǒng)的 dylib (動(dòng)態(tài)鏈接庫)和 App 可執(zhí)行文件的加載時(shí)間送巡;
t2 =? main()函數(shù)執(zhí)行之后到AppDelegate類中的applicationDidFinishLaunching:withOptions:方法執(zhí)行結(jié)束前這段時(shí)間。
所以我們對(duì)APP啟動(dòng)時(shí)間的獲取和優(yōu)化都是從這兩個(gè)階段著手盒卸,下面先看看main()函數(shù)執(zhí)行之前如何獲取啟動(dòng)時(shí)間骗爆。
衡量main()函數(shù)執(zhí)行之前的耗時(shí)
對(duì)于衡量main()之前也就是time1的耗時(shí),蘋果官方提供了一種方法蔽介,即在真機(jī)調(diào)試的時(shí)候摘投,勾選DYLD_PRINT_STATISTICS選項(xiàng)(如果想獲取更詳細(xì)的信息可以使用DYLD_PRINT_STATISTICS_DETAILS),如下圖:
輸出結(jié)果如下:
Total?pre-maintime:34.22milliseconds?(100.0%)
dylib?loadingtime:14.43milliseconds?(42.1%)
rebase/bindingtime:1.82milliseconds?(5.3%)
ObjC?setuptime:3.89milliseconds?(11.3%)
initializertime:13.99milliseconds?(40.9%)
slowest?intializers?:
libSystem.B.dylib?:2.20milliseconds?(6.4%)
libBacktraceRecording.dylib?:2.90milliseconds?(8.4%)
libMainThreadChecker.dylib?:6.55milliseconds?(19.1%)
libswiftCoreImage.dylib?:0.71milliseconds?(2.0%)
系統(tǒng)級(jí)別的動(dòng)態(tài)鏈接庫虹蓄,因?yàn)樘O果做了優(yōu)化犀呼,所以耗時(shí)并不多,而大多數(shù)時(shí)候薇组,t1的時(shí)間大部分會(huì)消耗在我們自身App中的代碼上和鏈接第三方庫上外臂。
所以我們應(yīng)如何減少main()調(diào)用之前的耗時(shí)呢,我們可以優(yōu)化的點(diǎn)有:
減少不必要的framework律胀,特別是第三方的宋光,因?yàn)閯?dòng)態(tài)鏈接比較耗時(shí);
check framework應(yīng)設(shè)為optional和required炭菌,如果該framework在當(dāng)前App支持的所有iOS系統(tǒng)版本都存在罪佳,那么就設(shè)為required,否則就設(shè)為optional黑低,因?yàn)閛ptional會(huì)有些額外的檢查赘艳;
合并或者刪減一些OC類,關(guān)于清理項(xiàng)目中沒用到的類,可以借助AppCode代碼檢查工具:
刪減一些無用的靜態(tài)變量
刪減沒有被調(diào)用到或者已經(jīng)廢棄的方法
將不必須在+load方法中做的事情延遲到+initialize中
盡量不要用C++虛函數(shù)(創(chuàng)建虛函數(shù)表有開銷)
衡量main()函數(shù)執(zhí)行之后的耗時(shí)
第二階段的耗時(shí)統(tǒng)計(jì)第练,我們認(rèn)為是從main ()執(zhí)行之后到applicationDidFinishLaunching:withOptions:方法最后阔馋,那么我們可以通過打點(diǎn)的方式進(jìn)行統(tǒng)計(jì)。
Objective-C項(xiàng)目因?yàn)橛衜ain文件娇掏,所以我么直接可以通過添加代碼獲扰磺蕖:
//?1.?在?main.m?添加如下代碼:
CFAbsoluteTimeAppStartLaunchTime;
intmain(intargc,char*?argv[])?{
AppStartLaunchTime?=CFAbsoluteTimeGetCurrent();
.....
}
//?2.?在?AppDelegate.m?的開頭聲明
externCFAbsoluteTimeAppStartLaunchTime;
//?3.?最后在AppDelegate.m?的?didFinishLaunchingWithOptions?中添加
dispatch_async(dispatch_get_main_queue(),?^{
NSLog(@"App啟動(dòng)時(shí)間--%f",(CFAbsoluteTimeGetCurrent()-AppStartLaunchTime));
});
大家都知道Swift項(xiàng)目是沒有main文件,官方給了如下解釋:
In
Xcode, Mac templates default to including a “main.swift” file, but for
iOS apps the default for new iOS project templates is to add
@UIApplicationMain to a regular Swift file. This causes the compiler to
synthesize a mainentry point for your iOS app, and eliminates the need
for a “main.swift” file.
也就是說婴梧,通過添加@UIApplicationMain標(biāo)志的方式下梢,幫我們添加了mian函數(shù)了。所以如果是我們需要在mian函數(shù)中做一些其它操作的話塞蹭,需要我們自己來創(chuàng)建main.swift文件孽江,這個(gè)也是蘋果允許的。
1. 刪除AppDelegate類中的 @UIApplicationMain標(biāo)志番电;
2. 自行創(chuàng)建main.swift文件岗屏,并添加程序入口:
importUIKit
var?appStartLaunchTime:CFAbsoluteTime=CFAbsoluteTimeGetCurrent()
UIApplicationMain(
CommandLine.argc,
UnsafeMutableRawPointer(CommandLine.unsafeArgv)
.bindMemory(
to:?UnsafeMutablePointer.self,
capacity:?Int(CommandLine.argc)),
nil,
NSStringFromClass(AppDelegate.self)
)
3. 在AppDelegate的didFinishLaunchingWithOptions :方法最后添加:
//?APP啟動(dòng)時(shí)間耗時(shí),從mian函數(shù)開始到didFinishLaunchingWithOptions方法結(jié)束
DispatchQueue.main.async{
print("APP啟動(dòng)時(shí)間耗時(shí)漱办,從mian函數(shù)開始到didFinishLaunchingWithOptions方法:\(CFAbsoluteTimeGetCurrent()?-?appStartLaunchTime)这刷。")
}
main函數(shù)之后的優(yōu)化:
盡量使用純代碼編寫,減少xib的使用娩井;
啟動(dòng)階段的網(wǎng)絡(luò)請(qǐng)求暇屋,是否都放到異步請(qǐng)求;
一些耗時(shí)的操作是否可以放到后面去執(zhí)行洞辣,或異步執(zhí)行等咐刨。
4. FPS
通過維基百科我們知道,F(xiàn)PS是Frames Per Second 的簡稱縮寫扬霜,意思是每秒傳輸幀數(shù)定鸟,也就是我們常說的“刷新率(單位為Hz)。
FPS是測量用于保存著瓶、顯示動(dòng)態(tài)視頻的信息數(shù)量联予。每秒鐘幀數(shù)愈多,所顯示的畫面就會(huì)愈流暢蟹但,F(xiàn)PS值越低就越卡頓躯泰,所以這個(gè)值在一定程度上可以衡量應(yīng)用在圖像繪制渲染處理時(shí)的性能。一般我們的APP的FPS只要保持在 50-60之間华糖,用戶體驗(yàn)都是比較流暢的麦向。
蘋果手機(jī)屏幕的正常刷新頻率是每秒60次,即可以理解為FPS值為60客叉。我們都知道CADisplayLink是和屏幕刷新頻率保存一致诵竭,所以我們是否可以通過它來監(jiān)控我們的FPS呢话告?!
首先CADisplayLink是什么
CADisplayLink是CoreAnimation提供的另一個(gè)類似于NSTimer的類卵慰,它總是在屏幕完成一次更新之前啟動(dòng)沙郭,它的接口設(shè)計(jì)的和NSTimer很類似,所以它實(shí)際上就是一個(gè)內(nèi)置實(shí)現(xiàn)的替代裳朋,但是和timeInterval以秒為單位不同病线,CADisplayLink有一個(gè)整型的frameInterval屬性,指定了間隔多少幀之后才執(zhí)行鲤嫡。默認(rèn)值是1送挑,意味著每次屏幕更新之前都會(huì)執(zhí)行一次。但是如果動(dòng)畫的代碼執(zhí)行起來超過了六十分之一秒暖眼,你可以指定frameInterval為2惕耕,就是說動(dòng)畫每隔一幀執(zhí)行一次(一秒鐘30幀)。
使用CADisplayLink監(jiān)控界面的FPS值诫肠,參考自YYFPSLabel:
importUIKit
classLSLFPSMonitor:UILabel{
private?var?link:CADisplayLink=CADisplayLink.init()
private?var?count:NSInteger=0
private?var?lastTime:?TimeInterval?=0.0
private?var?fpsColor:UIColor=UIColor.green
public?var?fps:?Double?=0.0
//?MARK:?-?init
override?init(frame:CGRect)?{
var?f?=?frame
iff.size?==CGSize.zero?{
f.size?=CGSize(width:55.0,?height:22.0)
}
super.init(frame:?f)
self.textColor?=UIColor.white
self.textAlignment?=?.center
self.font?=UIFont.init(name:"Menlo",?size:12.0)
self.backgroundColor?=UIColor.black
link?=CADisplayLink.init(target:?LSLWeakProxy(target:self),?selector:#selector(tick))
link.add(to:?RunLoop.current,?forMode:?RunLoopMode.commonModes)
}
deinit?{
link.invalidate()
}
required?init?(coder?aDecoder:NSCoder)?{
fatalError("init(coder:)?has?not?been?implemented")
}
//?MARK:?-?actions
@objc?func?tick(link:CADisplayLink)?{
guard?lastTime?!=0else{
lastTime?=?link.timestamp
return
}
count?+=1
let?delta?=?link.timestamp?-?lastTime
guard?delta?>=1.0else{
return
}
lastTime?=?link.timestamp
fps?=?Double(count)?/?delta
let?fpsText?="\(String.init(format:?"%.3f",?fps))?FPS"
count?=0
let?attrMStr?=NSMutableAttributedString(attributedString:NSAttributedString(string:?fpsText))
iffps?>55.0{
fpsColor?=UIColor.green
}elseif(fps?>=50.0&&?fps?<=55.0)?{
fpsColor?=UIColor.yellow
}else{
fpsColor?=UIColor.red
}
attrMStr.setAttributes([NSAttributedStringKey.foregroundColor:fpsColor],?range:NSMakeRange(0,?attrMStr.length?-3))
attrMStr.setAttributes([NSAttributedStringKey.foregroundColor:UIColor.white],?range:NSMakeRange(attrMStr.length?-3,3))
DispatchQueue.main.async?{
self.attributedText?=?attrMStr
}
}
}
通過CADisplayLink的實(shí)現(xiàn)方式司澎,并真機(jī)測試之后,確實(shí)是可以在很大程度上滿足了監(jiān)控FPS的業(yè)務(wù)需求和為提高用戶體驗(yàn)提供參考栋豫,但是和Instruments的值可能會(huì)有些出入。下面我們來討論下使用CADisplayLink的方式笼才,可能存在的問題漱受。
(1). 和Instruments值對(duì)比有出入络凿,原因如下:
CADisplayLink運(yùn)行在被添加的那個(gè)RunLoop之中(一般是在主線程中)骡送,因此它只能檢測出當(dāng)前RunLoop下的幀率。RunLoop中所管理的任務(wù)的調(diào)度時(shí)機(jī)絮记,受任務(wù)所處的RunLoopMode和CPU的繁忙程度所影響摔踱。所以想要真正定位到準(zhǔn)確的性能問題所在,最好還是通過Instrument來確認(rèn)怨愤。
(2). 使用CADisplayLink可能存在的循環(huán)引用問題派敷。
例如以下寫法:
let?link?=CADisplayLink.init(target:self,?selector:#selector(tick))
let?timer?=?Timer.init(timeInterval:1.0,?target:self,?selector:#selector(tick),?userInfo:?nil,?repeats:?true)
原因:以上兩種用法,都會(huì)對(duì) self 強(qiáng)引用撰洗,此時(shí) timer持有 self篮愉,self 也持有 timer,循環(huán)引用導(dǎo)致頁面 dismiss 時(shí)差导,雙方都無法釋放试躏,造成循環(huán)引用。此時(shí)使用 weak 也不能有效解決:
weakvar?weakSelf?=self
let?link?=CADisplayLink.init(target:?weakSelf,?selector:#selector(tick))
那么我們應(yīng)該怎樣解決這個(gè)問題设褐,有人會(huì)說在deinit(或dealloc)中調(diào)用定時(shí)器的invalidate方法颠蕴,但是這是無效的泣刹,因?yàn)橐呀?jīng)造成循環(huán)引用了,不會(huì)走到這個(gè)方法的犀被。
YYKit作者提供的解決方案是使用 YYWeakProxy椅您,這個(gè)YYWeakProxy不是繼承自NSObject而是繼承NSProxy。
NSProxy
An abstract superclass defining an API for objects that act as stand-ins for other objects or for objects that don’t exist yet.
NSProxy是一個(gè)為對(duì)象定義接口的抽象父類寡键,并且為其它對(duì)象或者一些不存在的對(duì)象扮演了替身角色掀泳。具體的可以看下NSProxy的官方文檔
修改后代碼如下,親測定時(shí)器如愿釋放西轩,LSLWeakProxy的具體實(shí)現(xiàn)代碼已經(jīng)同步到github中开伏。
let?link?=CADisplayLink.init(target:?LSLWeakProxy(target:self),?selector:#selector(tick))
5. 卡頓
在了解卡頓產(chǎn)生的原因之前,先看下屏幕顯示圖像的原理遭商。
屏幕顯示圖像的原理:
現(xiàn)在的手機(jī)設(shè)備基本都是采用雙緩存+垂直同步(即V-Sync)屏幕顯示技術(shù)固灵。
如上圖所示,系統(tǒng)內(nèi)CPU劫流、GPU和顯示器是協(xié)同完成顯示工作的巫玻。其中CPU負(fù)責(zé)計(jì)算顯示的內(nèi)容,例如視圖創(chuàng)建祠汇、布局計(jì)算仍秤、圖片解碼、文本繪制等等可很。隨后CPU將計(jì)算好的內(nèi)容提交給GPU诗力,由GPU進(jìn)行變換、合成我抠、渲染苇本。GPU會(huì)預(yù)先渲染好一幀放入一個(gè)緩沖區(qū)內(nèi),讓視頻控制器讀取菜拓,當(dāng)下一幀渲染好后瓣窄,GPU會(huì)直接將視頻控制器的指針指向第二個(gè)容器(雙緩存原理)。這里纳鼎,GPU會(huì)等待顯示器的VSync(即垂直同步)信號(hào)發(fā)出后俺夕,才進(jìn)行新的一幀渲染和緩沖區(qū)更新(這樣能解決畫面撕裂現(xiàn)象,也增加了畫面流暢度贱鄙,但需要消費(fèi)更多的計(jì)算資源劝贸,也會(huì)帶來部分延遲)。
卡頓的原因:
由上面屏幕顯示的原理逗宁,采用了垂直同步機(jī)制的手機(jī)設(shè)備映九。如果在一個(gè)VSync
時(shí)間內(nèi),CPU 或GPU
沒有完成內(nèi)容提交疙剑,則那一幀就會(huì)被丟棄氯迂,等待下一次機(jī)會(huì)再顯示践叠,而這時(shí)顯示屏?xí)A糁暗膬?nèi)容不變。例如在主線程里添加了阻礙主線程去響應(yīng)點(diǎn)擊嚼蚀、滑動(dòng)事件禁灼、以及阻礙主線程的UI繪制等的代碼,都是造成卡頓的常見原因轿曙。
卡頓監(jiān)控:
卡頓監(jiān)控一般有兩種實(shí)現(xiàn)方案:
(1). 主線程卡頓監(jiān)控弄捕。通過子線程監(jiān)測主線程的runLoop,判斷兩個(gè)狀態(tài)區(qū)域之間的耗時(shí)是否達(dá)到一定閾值导帝。
(2). FPS監(jiān)控守谓。要保持流暢的UI交互,App 刷新率應(yīng)該當(dāng)努力保持在 60fps您单。FPS的監(jiān)控實(shí)現(xiàn)原理斋荞,上面已經(jīng)探討過這里略過。
在使用FPS監(jiān)控性能的實(shí)踐過程中虐秦,發(fā)現(xiàn) FPS 值抖動(dòng)較大平酿,造成偵測卡頓比較困難。為了解決這個(gè)問題悦陋,通過采用檢測主線程每次執(zhí)行消息循環(huán)的時(shí)間蜈彼,當(dāng)這一時(shí)間大于規(guī)定的閾值時(shí),就記為發(fā)生了一次卡頓的方式來監(jiān)控俺驶。
這也是美團(tuán)的移動(dòng)端采用的性能監(jiān)控Hertz 方案幸逆,微信團(tuán)隊(duì)也在實(shí)踐過程中提出來類似的方案--微信讀書 iOS 性能優(yōu)化總結(jié)。
方案的提出暮现,是根據(jù)滾動(dòng)引發(fā)的Sources事件或其它交互事件總是被快速的執(zhí)行完成还绘,然后進(jìn)入到kCFRunLoopBeforeWaiting狀態(tài)下;假如在滾動(dòng)過程中發(fā)生了卡頓現(xiàn)象送矩,那么RunLoop必然會(huì)保持kCFRunLoopAfterWaiting或者kCFRunLoopBeforeSources這兩個(gè)狀態(tài)之一蚕甥。
所以監(jiān)控主線程卡頓的方案一:
開辟一個(gè)子線程哪替,然后實(shí)時(shí)計(jì)算 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting 兩個(gè)狀態(tài)區(qū)域之間的耗時(shí)是否超過某個(gè)閥值栋荸,來斷定主線程的卡頓情況。
但是由于主線程的RunLoop在閑置時(shí)基本處于Before Waiting狀態(tài)凭舶,這就導(dǎo)致了即便沒有發(fā)生任何卡頓晌块,這種檢測方式也總能認(rèn)定主線程處在卡頓狀態(tài)。
為了解決這個(gè)問題寒神(南梔傾寒)給出了自己的解決方案帅霜,Swift的卡頓檢測第三方ANREye匆背。這套卡頓監(jiān)控方案大致思路為:創(chuàng)建一個(gè)子線程進(jìn)行循環(huán)檢測,每次檢測時(shí)設(shè)置標(biāo)記位為YES身冀,然后派發(fā)任務(wù)到主線程中將標(biāo)記位設(shè)置為NO钝尸。接著子線程沉睡超時(shí)闕值時(shí)長括享,判斷標(biāo)志位是否成功設(shè)置成NO,如果沒有說明主線程發(fā)生了卡頓珍促。
結(jié)合這套方案铃辖,當(dāng)主線程處在Before Waiting狀態(tài)的時(shí)候,通過派發(fā)任務(wù)到主線程來設(shè)置標(biāo)記位的方式處理常態(tài)下的卡頓檢測:
#define?lsl_SEMAPHORE_SUCCESS?0
staticBOOLlsl_is_monitoring?=NO;
staticdispatch_semaphore_t?lsl_semaphore;
staticNSTimeIntervallsl_time_out_interval?=0.05;
@implementationLSLAppFluencyMonitor
staticinlinedispatch_queue_t__lsl_fluecy_monitor_queue()?{
staticdispatch_queue_tlsl_fluecy_monitor_queue;
staticdispatch_once_tonce;
dispatch_once(&once,?^{
lsl_fluecy_monitor_queue?=?dispatch_queue_create("com.dream.lsl_monitor_queue",NULL);
});
returnlsl_fluecy_monitor_queue;
}
staticinlinevoid__lsl_monitor_init()?{
staticdispatch_once_tonceToken;
dispatch_once(&onceToken,?^{
lsl_semaphore?=?dispatch_semaphore_create(0);
});
}
#pragma?mark?-?Public
+?(instancetype)monitor?{
return[LSLAppFluencyMonitor?new];
}
-?(void)startMonitoring?{
if(lsl_is_monitoring)?{return;?}
lsl_is_monitoring?=YES;
__lsl_monitor_init();
dispatch_async(__lsl_fluecy_monitor_queue(),?^{
while(lsl_is_monitoring)?{
__blockBOOLtimeOut?=YES;
dispatch_async(dispatch_get_main_queue(),?^{
timeOut?=NO;
dispatch_semaphore_signal(lsl_semaphore);
});
[NSThreadsleepForTimeInterval:?lsl_time_out_interval];
if(timeOut)?{
[LSLBacktraceLogger?lsl_logMain];//?打印主線程調(diào)用棧
//????????????????[LSLBacktraceLogger?lsl_logCurrent];????//?打印當(dāng)前線程的調(diào)用棧
//????????????????[LSLBacktraceLogger?lsl_logAllThread];??//?打印所有線程的調(diào)用棧
}
dispatch_wait(lsl_semaphore,?DISPATCH_TIME_FOREVER);
}
});
}
-?(void)stopMonitoring?{
if(!lsl_is_monitoring)?{return;?}
lsl_is_monitoring?=NO;
}
@end
其中LSLBacktraceLogger是獲取堆棧信息的類猪叙,詳情見代碼Github娇斩。
打印日志如下:
2018-08-1612:36:33.910491+0800AppPerformance[4802:171145]?Backtrace?of?Thread771:
======================================================================================
libsystem_kernel.dylib0x10d089bce__semwait_signal?+10
libsystem_c.dylib0x10ce55d10usleep?+53
AppPerformance0x108b8b478$S14AppPerformance25LSLFPSTableViewControllerC05tableD0_12cellForRowAtSo07UITableD4CellCSo0kD0C_10Foundation9IndexPathVtF?+1144
AppPerformance0x108b8b60b$S14AppPerformance25LSLFPSTableViewControllerC05tableD0_12cellForRowAtSo07UITableD4CellCSo0kD0C_10Foundation9IndexPathVtFTo?+155
UIKitCore0x1135b104f-[_UIFilteredDataSource?tableView:cellForRowAtIndexPath:]?+95
UIKitCore0x1131ed34d-[UITableView_createPreparedCellForGlobalRow:withIndexPath:willDisplay:]?+765
UIKitCore0x1131ed8da-[UITableView_createPreparedCellForGlobalRow:willDisplay:]?+73
UIKitCore0x1131b4b1e-[UITableView_updateVisibleCellsNow:isRecursive:]?+2863
UIKitCore0x1131d57eb-[UITableViewlayoutSubviews]?+165
UIKitCore0x1133921ee-[UIView(CALayerDelegate)?layoutSublayersOfLayer:]?+1501
QuartzCore0x10ab72eb1-[CALayerlayoutSublayers]?+175
QuartzCore0x10ab77d8b_ZN2CA5Layer16layout_if_neededEPNS_11TransactionE?+395
QuartzCore0x10aaf3b45_ZN2CA7Context18commit_transactionEPNS_11TransactionE?+349
QuartzCore0x10ab285b0_ZN2CA11Transaction6commitEv?+576
QuartzCore0x10ab29374_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv?+76
CoreFoundation0x109dc3757__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__?+23
CoreFoundation0x109dbdbde__CFRunLoopDoObservers?+430
CoreFoundation0x109dbe271__CFRunLoopRun?+1537
CoreFoundation0x109dbd931CFRunLoopRunSpecific+625
GraphicsServices0x10f5981b5GSEventRunModal?+62
UIKitCore0x112c812ceUIApplicationMain+140
AppPerformance0x108b8c1f0main?+224
libdyld.dylib0x10cd4dc9dstart?+1
======================================================================================
方案二是結(jié)合CADisplayLink的方式實(shí)現(xiàn)
在檢測FPS值的時(shí)候,我們就詳細(xì)介紹了CADisplayLink的使用方式穴翩,在這里也可以通過FPS值是否連續(xù)低于某個(gè)值開進(jìn)行監(jiān)控犬第。
后續(xù)
關(guān)于更多APP性能監(jiān)控的內(nèi)容,包括網(wǎng)絡(luò)狀況監(jiān)控芒帕、啟動(dòng)時(shí)閃退歉嗓、使用時(shí)崩潰、耗電量監(jiān)控背蟆、流量監(jiān)控等等遥椿,由于篇幅太長,將作為第二篇文中發(fā)出淆储,歡迎交流探討冠场。
文中所提的所以實(shí)例代碼:
作者:青蘋果園
鏈接:http://www.reibang.com/p/95df83780c8f