APP開發(fā)中性能問題無疑是很重要的一點(diǎn),有幾項(xiàng)指標(biāo)可以看出APP的性能是否存在問題褐捻,內(nèi)存使用量狸驳,F(xiàn)PS再层,以及CPU使用率。在開發(fā)階段這些數(shù)據(jù)的測(cè)試很容易秽五,有一些在Xcode編譯項(xiàng)目時(shí)就會(huì)有顯示孽查,而剩下的則可以使用Instruments來進(jìn)行監(jiān)控√勾可以說在線下的監(jiān)控Instruments為我們考慮到了方方面面盲再,但是光是線下監(jiān)控是完全不夠的,因?yàn)锳PP上線后運(yùn)行的環(huán)境是十分復(fù)雜的瓣铣,我們?cè)跍y(cè)試時(shí)不可能模擬的面面俱到答朋,因此就需要針對(duì)線上的性能問題進(jìn)行監(jiān)控。
內(nèi)存使用量
內(nèi)存使用是很重要的一點(diǎn)棠笑,如果我們的內(nèi)存使用量過大梦碗,APP就會(huì)被系統(tǒng)殺掉,給用戶的表現(xiàn)就是閃退,這是很嚴(yán)重的一個(gè)問題洪规,而我們的APP如果被系統(tǒng)強(qiáng)殺會(huì)產(chǎn)生一個(gè)叫jetsam
的日志印屁,這個(gè)日志可以通過手機(jī)中設(shè)置 -> 隱私 -> 分析中看到相關(guān)日志。
現(xiàn)代的進(jìn)程在虛擬內(nèi)存中的運(yùn)行是以分頁(yè)形式存在的斩例,這樣做可以節(jié)省內(nèi)存空間雄人,因?yàn)锳PP在運(yùn)行的時(shí)候只有一部分會(huì)映射到虛擬內(nèi)存中,而不是整個(gè)APP都會(huì)被加載到虛擬內(nèi)存上念赶,只有使用到的部分才會(huì)被映射础钠,而jetsam日志就是以頁(yè)數(shù)為單位來衡量一個(gè)APP使用的內(nèi)存是否超過限制。
"rpages" : 89600,
"reason" : "per-process-limit",
像這樣晶乔,表明我們使用了89600個(gè)內(nèi)存頁(yè)珍坊,超出了單進(jìn)程的內(nèi)存限制,如果可以知道一頁(yè)的大小就可以知道系統(tǒng)對(duì)單個(gè)進(jìn)程的內(nèi)存限制是多少正罢。注意這個(gè)限制不是固定的,而是系統(tǒng)根據(jù)當(dāng)前內(nèi)存情況來決定的驻民。
可以看到一頁(yè)的大小是16384翻具,這樣就可以計(jì)算出當(dāng)前 App 的內(nèi)存限制值:pageSize * rpages / 1024 /1024 =16384 * 89600 / 1024 / 1024 得到的值是 1400 MB,即 1.4G回还。
iOS系統(tǒng)會(huì)使用一個(gè)優(yōu)先級(jí)最高的線程vm_pressure_monitor
來監(jiān)控系統(tǒng)內(nèi)存壓力的情況裆泳,并通過一個(gè)堆棧來維護(hù)所有的APP進(jìn)程,如果發(fā)現(xiàn)某個(gè)進(jìn)程的內(nèi)存快要超出限制了就會(huì)發(fā)出通知柠硕,內(nèi)存有壓力的APP就會(huì)執(zhí)行代理工禾,也就是熟悉的didReceiveMemoryWarning
,在這里面可以寫一個(gè)釋放內(nèi)存的方法蝗柔,這是最后的機(jī)會(huì)去避免APP被強(qiáng)殺闻葵。
不過很遺憾APP在上限后我們是無法去獲取jetsam
日志的,這屬于每個(gè)用戶的隱私癣丧,我們的權(quán)限是無法獲得的槽畔,但是iOS還為我們提供了其他的方法去獲取內(nèi)存的使用情況,以供我們?cè)贏PP內(nèi)存收到警告時(shí)查看當(dāng)前內(nèi)存的使用情況胁编。
我們可以寫一個(gè)文件導(dǎo)入如下頭文件
#import <mach/mach.h>
從頭文件的名稱就可以看出這個(gè)是系統(tǒng)級(jí)的方法厢钧,iOS系統(tǒng)提供了一個(gè)函數(shù)task_info
可以獲得當(dāng)前進(jìn)程的使用情況
struct mach_task_basic_info info;
mach_msg_type_number_t size = sizeof(info);
kern_return_t kl = task_info(mach_task_self(), MACH_TASK_BASIC_INFO, (task_info_t)&info, &size);
float used_mem = info.resident_size;
NSLog(@"使用了 %f MB 內(nèi)存", used_mem / 1024.0f / 1024.0f)
MACH_TASK_BASIC_INFO
這個(gè)結(jié)構(gòu)體中定義了一系列和內(nèi)存有關(guān)的變量
struct mach_task_basic_info {
mach_vm_size_t virtual_size; /* virtual memory size (bytes) */
mach_vm_size_t resident_size; /* resident memory size (bytes) */
mach_vm_size_t resident_size_max; /* maximum resident memory size (bytes) */
time_value_t user_time; /* total user run time for
* terminated threads */
time_value_t system_time; /* total system run time for
* terminated threads */
policy_t policy; /* default policy for new threads */
integer_t suspend_count; /* suspend count for task */
};
但是這樣測(cè)出來和Xcode中實(shí)際顯示的出入較大,后來蘋果的開發(fā)者大會(huì)說task_vm_info
結(jié)構(gòu)體中的phys_footprint
才是真正的物理內(nèi)存使用量
struct task_vm_info {
mach_vm_size_t virtual_size; // 虛擬內(nèi)存大小
integer_t region_count; // 內(nèi)存區(qū)域的數(shù)量
integer_t page_size;
mach_vm_size_t resident_size; // 駐留內(nèi)存大小
mach_vm_size_t resident_size_peak; // 駐留內(nèi)存峰值
...
/* added for rev1 */
mach_vm_size_t phys_footprint; // 物理內(nèi)存
...
由此就可以寫一個(gè)簡(jiǎn)單的內(nèi)存使用量的檢測(cè)方法
- (float)getMemoryUse{
//TASK_VM_INFO中存儲(chǔ)物理內(nèi)存使用信息
int64_t memoryUsageInByte = 0;
task_vm_info_data_t vmInfo;
mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
kern_return_t kernReturn = task_info(mach_task_self(), MACH_TASK_BASIC_INFO, (task_info_t)&info, &size);
if (kernReturn != KERN_SUCCESS) { return NSNotFound; }
memoryUsageInByte = (int64_t) vmInfo.phys_footprint;
return memoryUsageInByte/1024.0/1024.0;
}
為了測(cè)試我寫了一個(gè)方法嬉橙,每隔一秒生成1000個(gè)對(duì)象裝入類中的可變數(shù)組早直,保證其不會(huì)被釋放,測(cè)試內(nèi)存使用量市框。
- (void)startMonitor{
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//創(chuàng)建一個(gè)定時(shí)器(dispatch_source_t本質(zhì)上還是一個(gè)OC對(duì)象)
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//設(shè)置定時(shí)器的各種屬性
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0*NSEC_PER_SEC));
uint64_t interval = (uint64_t)(1.0*NSEC_PER_SEC);
dispatch_source_set_timer(self.timer, start, interval, 0);
//設(shè)置回調(diào)
__weak typeof(self) weakSelf = self;
dispatch_source_set_event_handler(self.timer, ^{
//定時(shí)器需要執(zhí)行的操作
self->usedMemory = [[BZMemoryMonitor shareInstance] getMemoryUse];
[weakSelf increaseMemory];
dispatch_async(dispatch_get_main_queue(), ^(void){
//Run UI Updates
weakSelf.useLabel.text = [NSString stringWithFormat:@"使用內(nèi)存:%f",self->usedMemory];
});
});
//啟動(dòng)定時(shí)器(默認(rèn)是暫停)
dispatch_resume(self.timer);
}
- (void)increaseMemory{
for (int i = 0; i < 1000; i++) {
NSObject *obj = [[NSObject alloc] init];
[self.array addObject:obj];
}
}
工具使用效果如下:
和Xcode中顯示的內(nèi)存使用基本一致霞扬。
FPS監(jiān)控
提到FPS監(jiān)控很多人可能都會(huì)知道使用CADisplayLink
,什么是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幀)或者3,也就是一秒鐘20次硼啤,等等议经。
CADisplayLink可以以屏幕刷新的頻率調(diào)用指定selector,而且iOS系統(tǒng)中正常的屏幕刷新率為60Hz(60次每秒)谴返,那只要在這個(gè)方法里面統(tǒng)計(jì)每秒這個(gè)方法執(zhí)行的次數(shù)煞肾,通過次數(shù)/時(shí)間就可以得出當(dāng)前屏幕的刷新率了。
由此可以寫出一個(gè)FPS監(jiān)控的工具:
- (void)setupDisplayLink{
// 初始化CADisplayLink
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(fpsCount:)];
// 把CADisplayLink對(duì)象加入runloop
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
// 方法執(zhí)行幀率和屏幕刷新率保持一致
- (void)fpsCount:(CADisplayLink *)displayLink {
if (_lastTimestamp == 0) {
_lastTimestamp = self.displayLink.timestamp;
} else {
_performTimes++;
// 開始渲染時(shí)間與上次渲染時(shí)間差值
NSTimeInterval useTime = self.displayLink.timestamp - _lastTimestamp;
if (useTime < 1) return;
_lastTimestamp = self.displayLink.timestamp;
// fps 計(jì)算
float fps = _performTimes / useTime;
NSLog(@"%f",fps);
_performTimes = 0;
}
}
但是我發(fā)現(xiàn)這種方式與Instrument檢測(cè)出來的差距有些大嗓袱,我的demo中滑動(dòng)已經(jīng)十分卡頓籍救,但是這個(gè)工具依舊顯示fps在57左右,后來參考這篇文章才發(fā)現(xiàn)其原因渠抹。
引用文章內(nèi)容:
iOS中每一幀畫面的生成是一個(gè)復(fù)雜的過程蝙昙,但簡(jiǎn)單來說需要經(jīng)過以下步驟:
1、系統(tǒng)根據(jù)你的代碼梧却,設(shè)置布局各個(gè)元素的位置(frame奇颠、AutoLayout)、屬性(顏色篮幢、透明度大刊、陰影等)。
2三椿、CPU對(duì)需要提前繪制的元素缺菌、圖形使用Core Graphics進(jìn)行繪制。
3搜锰、CPU將一切需要繪制到屏幕上的內(nèi)容(包括解壓后的圖片)打包發(fā)送到GPU
4伴郁、GPU對(duì)內(nèi)容進(jìn)行計(jì)算繪制,顯示到屏幕上蛋叼。
我使用的demo是一個(gè)很大的CollectionView焊傅,然后為cell添加了圓角以及陰影剂陡,并且使用了大圖片,所以我和這篇文章中出現(xiàn)的現(xiàn)象一致狐胎。
1鸭栖、滑動(dòng)列表時(shí)(即使是慢速滑動(dòng)),GPU都需要計(jì)算圖像握巢、文本的動(dòng)態(tài)陰影的位置和形狀來進(jìn)行陰影的繪制晕鹊,此時(shí)GPU將成性能瓶頸,能明顯觀察到FPS的下降暴浦。
2溅话、快速滑動(dòng)列表時(shí)Cell每次在顯示前都需要通過imageWithContentsOfFile從硬盤加載圖片并解壓,此時(shí)文件的IO歌焦,圖片的解壓讓CPU也遇到性能瓶頸飞几,使主線程無法流暢執(zhí)行,讓FPS雪上加霜独撇。
原因:
CADisplayLink運(yùn)行在主線程RunLoop之中屑墨,RunLoop中所管理的任務(wù)的調(diào)度時(shí)機(jī)受任務(wù)所處的RunLoopMode和CPU的繁忙程度所影響。
在第二個(gè)原因中受文件IO券勺、解壓圖片的影響绪钥,RunLoop 自然無法保證CADisplayLink被調(diào)用的次數(shù)達(dá)到每秒60次,這里的調(diào)用頻率正是我們的FPS指示器中所顯示FPS关炼。
而在第一個(gè)原因中主要瓶頸在于GPU,即使RunLoop能保持每秒60次調(diào)用CADisplayLink匣吊,也無法說明此時(shí)的屏幕刷新率能達(dá)到60FPS(Core Animation通過與OpenGl打交道控制GPU進(jìn)行屏幕繪制)儒拂,也正因?yàn)檫@樣FPS指示器顯示55+的FPS,但I(xiàn)nstrument中的Core Animation FPS 卻很低色鸳。
總結(jié)來說社痛,我的fps一直保持在很高,只是說明runloop保持了CADisplayLink的高頻率調(diào)用命雀,但是并不能說明屏幕的刷新率也很高蒜哀。這種方法在一些特殊場(chǎng)景下的檢測(cè)并不準(zhǔn)確,所以這種方法我不是很推薦吏砂,可以使用微信開源的matrix來進(jìn)行fps的監(jiān)控撵儿。
CPU使用率檢測(cè)
一個(gè)進(jìn)程運(yùn)行的基本單位就是線程,因此一個(gè)進(jìn)程中所有線程的使用率加起來就是CPU的使用率狐血,iOS系統(tǒng)為我們提供了這些方法淀歇,還是需要導(dǎo)入#import <mach/mach.h>
頭文件,首先thread_basic_info
為我們提供了單個(gè)線程的各種屬性匈织,其中一項(xiàng)就是cpu_usage
浪默。
struct thread_basic_info {
time_value_t user_time; /* user run time */
time_value_t system_time; /* system run time */
integer_t cpu_usage; /* scaled cpu usage percentage */
policy_t policy; /* scheduling policy in effect */
integer_t run_state; /* run state (see below) */
integer_t flags; /* various flags (see below) */
integer_t suspend_count; /* suspend count for thread */
integer_t sleep_time; /* number of seconds that thread
* has been sleeping */
};
task_threads
這個(gè)函數(shù)又為我們提供了獲取當(dāng)前所有線程的方法牡直,接下來的事情就很簡(jiǎn)單了。
- (integer_t)cpuUsage {
thread_act_array_t threads; //int 組成的數(shù)組比如 thread[1] = 5635
mach_msg_type_number_t threadCount = 0; //mach_msg_type_number_t 是 int 類型
const task_t thisTask = mach_task_self();
//根據(jù)當(dāng)前 task 獲取所有線程
kern_return_t kr = task_threads(thisTask, &threads, &threadCount);
if (kr != KERN_SUCCESS) {
return 0;
}
integer_t cpuUsage = 0;
// 遍歷所有線程
for (int i = 0; i < threadCount; i++) {
thread_info_data_t threadInfo;
thread_basic_info_t threadBaseInfo;
mach_msg_type_number_t threadInfoCount = THREAD_INFO_MAX;
if (thread_info((thread_act_t)threads[i], THREAD_BASIC_INFO, (thread_info_t)threadInfo, &threadInfoCount) == KERN_SUCCESS) {
// 獲取 CPU 使用率
threadBaseInfo = (thread_basic_info_t)threadInfo;
if (!(threadBaseInfo->flags & TH_FLAGS_IDLE)) {
cpuUsage += threadBaseInfo->cpu_usage;
}
}
}
assert(vm_deallocate(mach_task_self(), (vm_address_t)threads, threadCount * sizeof(thread_t)) == KERN_SUCCESS);
return cpuUsage;
}
這樣就可以獲取CPU的使用率了纳决。
個(gè)人推薦如果是想簡(jiǎn)單的進(jìn)行一個(gè)性能方面的線上監(jiān)控碰逸,使用這些簡(jiǎn)單的小方法就夠了,如果是想對(duì)APP進(jìn)行一個(gè)全量的內(nèi)存監(jiān)控阔加,那么可以使用微信的Matrix饵史。