前言
應用性能的衡量標準有很多,從用戶的角度來看,卡頓是最明顯的表現,但這不意味看起來不卡頓的應用就不存在性能問題。從開發(fā)角度來看,衡量一段代碼或者說算法的標準包括空間復雜度和時間復雜度,分別對應內存和CPU
兩種重要的計算機硬件。只有外在與內在都做沒問題,才能說應用的性能做好了。因此,一套應用性能監(jiān)控系統(tǒng)對開發(fā)者的幫助是巨大的,它能幫助你找到應用的性能瓶頸。
CPU
線程是程序運行的最小單位,換句話來說就是:我們的應用其實是由多個運行在CPU
上面的線程組合而成的。要想知道應用占用了CPU
多少資源,其實就是獲取應用所有線程占用CPU
的使用量。結構體thread_basic_info
封裝了單個線程的基本信息:
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
內核實現的,這個內核提供了task_threads
接口讓我們獲取所有的線程列表以及接口thread_info
來獲取單個線程的信息:
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
);
第一個函數的target_task
傳入進程標記,這里使用mach_task_self()
獲取當前進程,后面兩個傳入兩個指針分別返回線程列表和線程個數,第二個函數的flavor
通過傳入不同的宏定義獲取不同的線程信息,這里使用THREAD_BASIC_INFO
。此外,參數存在多種類型,實際上大多數都是mach_port_t
類型的別名:
因此可以得到下面的代碼來獲取應用對應的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.;
}
實踐
由于硬件爆發(fā)式的性能增長,使用舊設備進行性能監(jiān)控來優(yōu)化代碼的可行性更高。另外,不要害怕CPU
的使用率過高。越高的占用率代表對CPU
的有效使用越高,即便出現超出100%
的使用率也不要害怕,需要結合FPS
來觀察是否對使用造成了影響。
拿筆者最近的一次優(yōu)化來說,機型iPhone5c
,性能瓶頸出現在啟動應用之后第一次進入城市選擇列表后出現的卡頓。通過監(jiān)控CPU
發(fā)現在進行的時候達到了187%
左右的使用率,幀數下降到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];
}];
}
}
上述的代碼對于數據的處理基本都放在子線程中處理,主線程實際上處理的工作并不多,但是同樣引發(fā)了卡頓。筆者的猜測是:CPU
對所執(zhí)行的線程一視同仁,并不會因為主線程的關系就額外分配更多的處理時間。因此當子線程的任務處理超出了某個闕值時,主線程照樣會受到影響,因此將耗時任務放到子線程處理來避免主線程卡頓
是存在前提的。那么可以比較輕松的找到代碼中CPU
消耗最嚴重的地方:文件寫入
在多線程陷阱一文中我提到過并發(fā)存在的缺陷,這種缺陷同上面的代碼是一致的。數據在寫入本地的時候會占據CPU
資源,直到寫入操作完成,如果此時其他核心上同樣處理著大量任務,應用基本逃不開卡頓出現。筆者的解決方案是使用流
將數據分片寫入本地,由于NSStream
自身的設計,可以保證寫入操作的流暢性:
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;
替換文件寫入的操作時候,再次進入幀數已經上升到了40
左右了,但是仍然會在進行之后發(fā)生短暫的卡頓。進一步猜測原因是:在異步實現流寫入的操作時,同樣異步進行數據處理。多個線程與主線程搶占CPU
資源,因此將數據處理的操作進一步延后,放到寫入操作完成之后:
case NSStreamEventOpenCompleted: {
[self fetchCitiesFromLocal: ^(NSDictionary * allCities) {
[self updateCities: allCities];
}];
} break;
完成這一步之后,除了頁面跳轉時幀數會降到40
左右,跳轉完成之后基本保持在52
以上的幀數。此外,其他的優(yōu)化方案還包括UI
展示時對數據的懶加載等,不過上面二個處理更符合CPU
相關優(yōu)化的概念,因此其他的就不再多說。
內存
進程的內存使用信息同樣放在了另一個結構體mach_task_basic_info
中,存儲了包括多種內存使用信息:
#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 */
};
對應的獲取函數名為task_info
,傳入進程名、獲取的信息類型、信息存儲結構體以及數量變量:
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
中的內存使用bytes
作為單位,在顯示之前我們還需要進行一層轉換。另外為了方便實際使用中的換算,筆者使用結構體來存儲內存相關信息:
#ifndef NBYTE_PER_MB
#define NBYTE_PER_MB (1024 * 1024)
#endif
typedef struct LXDApplicationMemoryUsage
{
double usage; ///< 已用內存(MB)
double total; ///< 總內存(MB)
double ratio; ///< 占用比率
} LXDApplicationMemoryUsage;
獲取內存占用量的代碼如下:
- (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 };
}
展示
內存和CPU
的監(jiān)控并不像其他設備信息一樣,能做更多有趣的事情。實際上,這兩者的獲取是一段枯燥又固定的代碼,因此并沒有太多可說的。對于這兩者的信息,基本上是開發(fā)階段展示出來觀察性能的。因此設置一個良好的查詢周期以及展示是這個過程中相對好玩的地方。筆者最終監(jiān)控的效果如下:
不知道什么原因導致了task_info
獲取到的內存信息總是比Xcode
自身展示的要多20M
左右,因此使用的時候自行扣去這一部分再做衡量。為了保證展示器總能顯示在頂部,筆者創(chuàng)建了一個UIWindow
的單例,通過設置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 { }
三個標簽欄采用異步繪制的方式保證更新文本的時候不影響主線程,核心代碼:
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)控應用本身占用的CPU
和內存資源之外,Darwin
提供的接口還允許我們去監(jiān)控整個設備本身的內存和CPU
使用量,筆者分別封裝了額外兩個類來獲取這些數據。最后統(tǒng)一封裝了LXDResourceMonitor
類來監(jiān)控這些資源的使用,通過枚舉來控制監(jiān)控內容:
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)內存使用率,優(yōu)先級低
LXDResourceMonitorTypeApplicationCpu = 1 << 2, ///< 監(jiān)控應用CPU使用率,優(yōu)先級高
LXDResourceMonitorTypeApplicationMemoty = 1 << 3, ///< 監(jiān)控應用內存使用率,優(yōu)先級高
};
這里使用到了位運算的內容,相比起其他的手段要更簡潔高效。APM
系列至此已經完成了大半,當然除了網上常用的APM
手段之外,筆者還會加入包括RunLoop
優(yōu)化運用相關的技術。