前言
應用效能的衡量標準有很多,從使用者的角度來看,卡頓是最明顯的表現,但這不意味看起來不卡頓的應用就不存在效能問題。從開發角度來看,衡量一段程式碼或者說演算法的標準包括空間複雜度和時間複雜度,分別對應記憶體和CPU
兩種重要的計算機硬體。只有外在與內在都做沒問題,才能說應用的效能做好了。因此,一套應用效能監控系統對開發者的幫助是巨大的,它能幫助你找到應用的效能瓶頸。
CPU
執行緒是程式執行的最小單位,換句話來說就是:我們的應用其實是由多個執行在CPU
上面的執行緒組合而成的。要想知道應用佔用了CPU
多少資源,其實就是獲取應用所有執行緒佔用CPU
的使用量。結構體thread_basic_info
封裝了單個執行緒的基本資訊:
1 2 3 4 5 6 7 8 9 10 11 |
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
的作業系統是基於Darwin
核心實現的,這個核心提供了task_threads
介面讓我們獲取所有的執行緒列表以及介面thread_info
來獲取單個執行緒的資訊:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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
處理總頻率:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
- (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 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.; } |
記憶體
程式的記憶體使用資訊同樣放在了另一個結構體mach_task_basic_info
中,儲存了包括多種記憶體使用資訊:
1 2 3 4 5 6 7 8 9 10 11 12 |
#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
,傳入程式名、獲取的資訊型別、資訊儲存結構體以及數量變數:
1 2 3 4 5 6 7 |
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
作為單位,在顯示之前我們還需要進行一層轉換。另外為了方便實際使用中的換算,筆者使用結構體來儲存記憶體相關資訊:
1 2 3 4 5 6 7 |
#ifndef NBYTE_PER_MB #define NBYTE_PER_MB (1024 * 1024) #endif typedef struct LXDApplicationMemoryUsage { double usage; /// |
獲取記憶體佔用量的程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
- (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
的監控並不像其他裝置資訊一樣,能做更多有趣的事情。實際上,這兩者的獲取是一段枯燥又固定的程式碼,因此並沒有太多可說的。對於這兩者的資訊,基本上是開發階段展示出來觀察效能的。因此設定一個良好的查詢週期以及展示是這個過程中相對好玩的地方。筆者最終監控的效果如下:
不知道什麼原因導致了task_info
獲取到的記憶體資訊總是比Xcode
自身展示的要多20M
左右,因此使用的時候自行扣去這一部分再做衡量。為了保證展示器總能顯示在頂部,筆者建立了一個UIWindow
的單例,通過設定windowLevel
的值為CGFLOAT_MAX
來保證顯示在最頂層,並且重寫了一部分方法保證不被修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
- (instancetype)initWithFrame: (CGRect)frame { if (self = [super initWithFrame: frame]) { [super setUserInteractionEnabled: NO]; [super setWindowLevel: CGFLOAT_MAX]; [[UIApplication sharedApplication].keyWindow addSubview: self]; [self makeKeyAndVisible]; } return self; } - (void)setWindowLevel: (UIWindowLevel)windowLevel { } - (void)setBackgroundColor: (UIColor *)backgroundColor { } - (void)setUserInteractionEnabled: (BOOL)userInteractionEnabled { } |
三個標籤欄採用非同步繪製的方式保證更新文字的時候不影響主執行緒,核心程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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; }); |
其他
除了監控應用本身佔用的CPU
和記憶體資源之外,Darwin
提供的介面還允許我們去監控整個裝置本身的記憶體和CPU
使用量,筆者分別封裝了額外兩個類來獲取這些資料。最後統一封裝了LXDResourceMonitor
類來監控這些資源的使用,通過列舉來控制監控內容:
1 2 3 |
typedef NS_ENUM(NSInteger, LXDResourceMonitorType) { LXDResourceMonitorTypeDefault = (1 |
這裡使用到了位運算的內容,相比起其他的手段要更簡潔高效。APM
系列至此已經完成了大半,當然除了網上常用的APM
手段之外,筆者還會加入包括RunLoop
優化運用相關的技術。