前言
應(yīng)用性能的衡量標(biāo)準(zhǔn)有很多,從用戶的角度來看,卡頓是最明顯的表現(xiàn),但這不意味看起來不卡頓的應(yīng)用就不存在性能問題。從開發(fā)角度來看,衡量一段代碼或者說算法的標(biāo)準(zhǔn)包括空間復(fù)雜度和時(shí)間復(fù)雜度,分別對應(yīng)內(nèi)存和CPU
兩種重要的計(jì)算機(jī)硬件。只有外在與內(nèi)在都做沒問題,才能說應(yīng)用的性能做好了。因此,一套應(yīng)用性能監(jiān)控系統(tǒng)對開發(fā)者的幫助是巨大的,它能幫助你找到應(yīng)用的性能瓶頸。
CPU
線程是程序運(yùn)行的最小單位,換句話來說就是:我們的應(yīng)用其實(shí)是由多個(gè)運(yùn)行在CPU
上面的線程組合而成的。要想知道應(yīng)用占用了CPU
多少資源,其實(shí)就是獲取應(yīng)用所有線程占用CPU
的使用量。結(jié)構(gòu)體thread_basic_info
封裝了單個(gè)線程的基本信息:
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 */
};
問題在于如何獲取這些信息。iOS
的操作系統(tǒng)是基于Darwin
內(nèi)核實(shí)現(xiàn)的,這個(gè)內(nèi)核提供了task_threads
接口讓我們獲取所有的線程列表以及接口thread_info
來獲取單個(gè)線程的信息:
kern_return_t task_threads
(
task_inspect_t target_task,
thread_act_array_t *act_list,
mach_msg_type_number_t *act_listCnt
);
kern_return_t thread_info
(
thread_inspect_t target_act,
thread_flavor_t flavor,
thread_info_t thread_info_out,
mach_msg_type_number_t *thread_info_outCnt
);
第一個(gè)函數(shù)的target_task
傳入進(jìn)程標(biāo)記,這里使用mach_task_self()
獲取當(dāng)前進(jìn)程,后面兩個(gè)傳入兩個(gè)指針分別返回線程列表和線程個(gè)數(shù),第二個(gè)函數(shù)的flavor
通過傳入不同的宏定義獲取不同的線程信息,這里使用THREAD_BASIC_INFO
。此外,參數(shù)存在多種類型,實(shí)際上大多數(shù)都是mach_port_t
類型的別名:
因此可以得到下面的代碼來獲取應(yīng)用對應(yīng)的CPU
占用信息。宏定義TH_USAGE_SCALE
返回CPU
處理總頻率:
- (double)currentUsage {
double usageRatio = 0;
thread_info_data_t thinfo;
thread_act_array_t threads;
thread_basic_info_t basic_info_t;
mach_msg_type_number_t count = 0;
mach_msg_type_number_t thread_info_count = THREAD_INFO_MAX;
if (task_threads(mach_task_self(), &threads, &count) == KERN_SUCCESS) {
for (int idx = 0; idx < count; idx++) {
if (thread_info(threads[idx], THREAD_BASIC_INFO, (thread_info_t)thinfo, &thread_info_count) == KERN_SUCCESS) {
basic_info_t = (thread_basic_info_t)thinfo;
if (!(basic_info_t->flags & TH_FLAGS_IDLE)) {
usageRatio += basic_info_t->cpu_usage / (double)TH_USAGE_SCALE;
}
}
}
assert(vm_deallocate(mach_task_self(), (vm_address_t)threads, count * sizeof(thread_t)) == KERN_SUCCESS);
}
return usageRatio * 100.;
}
實(shí)踐
由于硬件爆發(fā)式的性能增長,使用舊設(shè)備進(jìn)行性能監(jiān)控來優(yōu)化代碼的可行性更高。另外,不要害怕CPU
的使用率過高。越高的占用率代表對CPU
的有效使用越高,即便出現(xiàn)超出100%
的使用率也不要害怕,需要結(jié)合FPS
來觀察是否對使用造成了影響。
拿筆者最近的一次優(yōu)化來說,機(jī)型iPhone5c
,性能瓶頸出現(xiàn)在啟動應(yīng)用之后第一次進(jìn)入城市選擇列表后出現(xiàn)的卡頓。通過監(jiān)控CPU
發(fā)現(xiàn)在進(jìn)行的時(shí)候達(dá)到了187%
左右的使用率,幀數(shù)下降到27
幀左右。最初的偽代碼如下:
static BOOL kmc_should_update_cities = YES;
- (void)fetchCities {
if (kmc_should_update_cities) {
[self fetchCitiesFromRemoteServer: ^(NSDictionary * allCities) {
[self updateCities: allCities];
[allCities writeToFile: [self localPath] atomically: YES];
}];
} else {
[self fetchCitiesFromLocal: ^(NSDictionary * allCities) {
[self updateCities: allCities];
}];
}
}
上述的代碼對于數(shù)據(jù)的處理基本都放在子線程中處理,主線程實(shí)際上處理的工作并不多,但是同樣引發(fā)了卡頓。筆者的猜測是:CPU
對所執(zhí)行的線程一視同仁,并不會因?yàn)橹骶€程的關(guān)系就額外分配更多的處理時(shí)間。因此當(dāng)子線程的任務(wù)處理超出了某個(gè)闕值時(shí),主線程照樣會受到影響,因此將耗時(shí)任務(wù)放到子線程處理來避免主線程卡頓
是存在前提的。那么可以比較輕松的找到代碼中CPU
消耗最嚴(yán)重的地方:文件寫入
在多線程陷阱一文中我提到過并發(fā)存在的缺陷,這種缺陷同上面的代碼是一致的。數(shù)據(jù)在寫入本地的時(shí)候會占據(jù)CPU
資源,直到寫入操作完成,如果此時(shí)其他核心上同樣處理著大量任務(wù),應(yīng)用基本逃不開卡頓出現(xiàn)。筆者的解決方案是使用流
將數(shù)據(jù)分片寫入本地,由于NSStream
自身的設(shè)計(jì),可以保證寫入操作的流暢性:
case NSStreamEventHasSpaceAvailable: {
LXDDispatchQueueAsyncBlockInUtility(^{
uint8_t * writeBytes = (uint8_t *)_writeData.bytes;
writeBytes += _currentOffset;
NSUInteger dataLength = _writeData.length;
NSUInteger writeLength = (dataLength - _currentOffset > kMaxBufferLength) ? kMaxBufferLength : (dataLength - _currentOffset);
uint8_t buffer[writeLength];
(void)memcpy(buffer, writeBytes, writeLength);
writeLength = [self.outputStream write: buffer maxLength: writeLength];
_currentOffset += writeLength;
});
} break;
替換文件寫入的操作時(shí)候,再次進(jìn)入幀數(shù)已經(jīng)上升到了40
左右了,但是仍然會在進(jìn)行之后發(fā)生短暫的卡頓。進(jìn)一步猜測原因是:在異步實(shí)現(xiàn)流寫入的操作時(shí),同樣異步進(jìn)行數(shù)據(jù)處理。多個(gè)線程與主線程搶占CPU
資源,因此將數(shù)據(jù)處理的操作進(jìn)一步延后,放到寫入操作完成之后:
case NSStreamEventOpenCompleted: {
[self fetchCitiesFromLocal: ^(NSDictionary * allCities) {
[self updateCities: allCities];
}];
} break;
完成這一步之后,除了頁面跳轉(zhuǎn)時(shí)幀數(shù)會降到40
左右,跳轉(zhuǎn)完成之后基本保持在52
以上的幀數(shù)。此外,其他的優(yōu)化方案還包括UI
展示時(shí)對數(shù)據(jù)的懶加載等,不過上面二個(gè)處理更符合CPU
相關(guān)優(yōu)化的概念,因此其他的就不再多說。
內(nèi)存
進(jìn)程的內(nèi)存使用信息同樣放在了另一個(gè)結(jié)構(gòu)體mach_task_basic_info
中,存儲了包括多種內(nèi)存使用信息:
#define MACH_TASK_BASIC_INFO 20 /* always 64-bit basic info */
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 */
};
對應(yīng)的獲取函數(shù)名為task_info
,傳入進(jìn)程名、獲取的信息類型、信息存儲結(jié)構(gòu)體以及數(shù)量變量:
kern_return_t task_info
(
task_name_t target_task,
task_flavor_t flavor,
task_info_t task_info_out,
mach_msg_type_number_t *task_info_outCnt
);
由于mach_task_basic_info
中的內(nèi)存使用bytes
作為單位,在顯示之前我們還需要進(jìn)行一層轉(zhuǎn)換。另外為了方便實(shí)際使用中的換算,筆者使用結(jié)構(gòu)體來存儲內(nèi)存相關(guān)信息:
#ifndef NBYTE_PER_MB
#define NBYTE_PER_MB (1024 * 1024)
#endif
typedef struct LXDApplicationMemoryUsage
{
double usage; ///< 已用內(nèi)存(MB)
double total; ///< 總內(nèi)存(MB)
double ratio; ///< 占用比率
} LXDApplicationMemoryUsage;
獲取內(nèi)存占用量的代碼如下:
- (LXDApplicationMemoryUsage)currentUsage {
struct mach_task_basic_info info;
mach_msg_type_number_t count = sizeof(info) / sizeof(integer_t);
if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO, (task_info_t)&info, &count) == KERN_SUCCESS) {
return (LXDApplicationMemoryUsage){
.usage = info.resident_size / NBYTE_PER_MB,
.total = [NSProcessInfo processInfo].physicalMemory / NBYTE_PER_MB,
.ratio = info.virtual_size / [NSProcessInfo processInfo].physicalMemory,
};
}
return (LXDApplicationMemoryUsage){ 0 };
}
展示
內(nèi)存和CPU
的監(jiān)控并不像其他設(shè)備信息一樣,能做更多有趣的事情。實(shí)際上,這兩者的獲取是一段枯燥又固定的代碼,因此并沒有太多可說的。對于這兩者的信息,基本上是開發(fā)階段展示出來觀察性能的。因此設(shè)置一個(gè)良好的查詢周期以及展示是這個(gè)過程中相對好玩的地方。筆者最終監(jiān)控的效果如下:
不知道什么原因?qū)е铝?code>task_info獲取到的內(nèi)存信息總是比Xcode
自身展示的要多20M
左右,因此使用的時(shí)候自行扣去這一部分再做衡量。為了保證展示器總能顯示在頂部,筆者創(chuàng)建了一個(gè)UIWindow
的單例,通過設(shè)置windowLevel
的值為CGFLOAT_MAX
來保證顯示在最頂層,并且重寫了一部分方法保證不被修改:
- (instancetype)initWithFrame: (CGRect)frame {
if (self = [super initWithFrame: frame]) {
[super setUserInteractionEnabled: NO];
[super setWindowLevel: CGFLOAT_MAX];
self.rootViewController = [UIViewController new];
[self makeKeyAndVisible];
}
return self;
}
- (void)setWindowLevel: (UIWindowLevel)windowLevel { }
- (void)setBackgroundColor: (UIColor *)backgroundColor { }
- (void)setUserInteractionEnabled: (BOOL)userInteractionEnabled { }
三個(gè)標(biāo)簽欄采用異步繪制的方式保證更新文本的時(shí)候不影響主線程,核心代碼:
CGSize textSize = [attributedText.string boundingRectWithSize: size options: NSStringDrawingUsesLineFragmentOrigin attributes: @{ NSFontAttributeName: self.font } context: nil].size;
textSize.width = ceil(textSize.width);
textSize.height = ceil(textSize.height);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake((size.width - textSize.width) / 2, 5, textSize.width, textSize.height));
CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributedText);
CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, attributedText.length), path, NULL);
CTFrameDraw(frame, context);
UIImage * contents = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
CFRelease(frameSetter);
CFRelease(frame);
CFRelease(path);
dispatch_async(dispatch_get_main_queue(), ^{
self.layer.contents = (id)contents.CGImage;
});
其他
除了監(jiān)控應(yīng)用本身占用的CPU
和內(nèi)存資源之外,Darwin
提供的接口還允許我們?nèi)ケO(jiān)控整個(gè)設(shè)備本身的內(nèi)存和CPU
使用量,筆者分別封裝了額外兩個(gè)類來獲取這些數(shù)據(jù)。最后統(tǒng)一封裝了LXDResourceMonitor
類來監(jiān)控這些資源的使用,通過枚舉來控制監(jiān)控內(nèi)容:
typedef NS_ENUM(NSInteger, LXDResourceMonitorType)
{
LXDResourceMonitorTypeDefault = (1 << 2) | (1 << 3),
LXDResourceMonitorTypeSystemCpu = 1 << 0, ///< 監(jiān)控系統(tǒng)CPU使用率,優(yōu)先級低
LXDResourceMonitorTypeSystemMemory = 1 << 1, ///< 監(jiān)控系統(tǒng)內(nèi)存使用率,優(yōu)先級低
LXDResourceMonitorTypeApplicationCpu = 1 << 2, ///< 監(jiān)控應(yīng)用CPU使用率,優(yōu)先級高
LXDResourceMonitorTypeApplicationMemoty = 1 << 3, ///< 監(jiān)控應(yīng)用內(nèi)存使用率,優(yōu)先級高
};
這里使用到了位運(yùn)算的內(nèi)容,相比起其他的手段要更簡潔高效。APM
系列至此已經(jīng)完成了大半,當(dāng)然除了網(wǎng)上常用的APM
手段之外,筆者還會加入包括RunLoop
優(yōu)化運(yùn)用相關(guān)的技術(shù)。