iOS內(nèi)存優(yōu)化【轉(zhuǎn)載】

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í)例代碼:

Github

作者:青蘋果園

鏈接:http://www.reibang.com/p/95df83780c8f

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市本砰,隨后出現(xiàn)的幾起案子碴裙,更是在濱河造成了極大的恐慌,老刑警劉巖点额,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件舔株,死亡現(xiàn)場離奇詭異,居然都是意外死亡还棱,警方通過查閱死者的電腦和手機(jī)载慈,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來珍手,“玉大人办铡,你說我怎么就攤上這事×找” “怎么了寡具?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長稚补。 經(jīng)常有香客問我童叠,道長,這世上最難降的妖魔是什么课幕? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任厦坛,我火速辦了婚禮五垮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘杜秸。我一直安慰自己拼余,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布亩歹。 她就那樣靜靜地躺著匙监,像睡著了一般。 火紅的嫁衣襯著肌膚如雪小作。 梳的紋絲不亂的頭發(fā)上亭姥,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音顾稀,去河邊找鬼达罗。 笑死,一個(gè)胖子當(dāng)著我的面吹牛静秆,可吹牛的內(nèi)容都是我干的粮揉。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼抚笔,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼扶认!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起殊橙,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤辐宾,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后膨蛮,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體叠纹,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年敞葛,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了誉察。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡惹谐,死狀恐怖持偏,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情豺鼻,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布儒飒,位于F島的核電站,受9級(jí)特大地震影響檩奠,放射性物質(zhì)發(fā)生泄漏蕉扮。R本人自食惡果不足惜在岂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一抽莱、第九天 我趴在偏房一處隱蔽的房頂上張望骄恶。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春析藕,著一層夾襖步出監(jiān)牢的瞬間治泥,已是汗流浹背车摄。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓拦止,卻偏偏與公主長得像顶瞒,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子朋譬,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 作者:敖志敏本文為原創(chuàng)文章狡赐,轉(zhuǎn)載請(qǐng)注明作者及出處 為什么寫這篇文章? 隨著移動(dòng)互聯(lián)網(wǎng)向縱深發(fā)展,用戶變得越來越關(guān)心...
    滬江技術(shù)學(xué)院閱讀 6,117評(píng)論 2 83
  • APP的性能監(jiān)控包括: CPU 占用率、 內(nèi)存使用情況、網(wǎng)絡(luò)狀況監(jiān)控、啟動(dòng)時(shí)閃退、卡頓喇伯、FPS买喧、使用時(shí)崩潰、耗電量...
    青蘋果園閱讀 40,787評(píng)論 3 127
  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時(shí)...
    歐辰_OSR閱讀 29,320評(píng)論 8 265
  • 概述 RunLoop作為iOS中一個(gè)基礎(chǔ)組件和線程有著千絲萬縷的關(guān)系猪杭,同時(shí)也是很多常見技術(shù)的幕后功臣税手。盡管在平時(shí)多...
    陽明先生_x閱讀 1,092評(píng)論 0 17
  • 本來以為明天答辯,然后答辯的PPT還沒做完,壓力最大的就是PPT和答辯了狞谱£粒可是想想答辯之后烁试,除了找工作就只能無所事...
    四橫閱讀 235評(píng)論 0 0