iOS監控:資源使用
前言
應用性能的衡量標準有很多,從用戶的角度來看,卡頓是最明顯的表現,但這不意味看起來不卡頓的應用就不存在性能問題。從開發角度來看,衡量一段代碼或者說算法的標準包括空間復雜度和時間復雜度,分別對應內存和 CPU 兩種重要的計算機硬件。只有外在與內在都做沒問題,才能說應用的性能做好了。因此,一套應用性能監控系統對開發者的幫助是巨大的,它能幫助你找到應用的性能瓶頸。
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 的操作系統是基于 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 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 中,存儲了包括多種內存使用信息:
#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; ///
</code></pre>
獲取內存占用量的代碼如下:
- (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 來保證顯示在最頂層,并且重寫了一部分方法保證不被修改:
- (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 { }
</code></pre>
三個標簽欄采用異步繪制的方式保證更新文本的時候不影響主線程,核心代碼:
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 類來監控這些資源的使用,通過枚舉來控制監控內容:
typedef NS_ENUM(NSInteger, LXDResourceMonitorType)
{
LXDResourceMonitorTypeDefault = (1
這里使用到了 位運算 的內容,相比起其他的手段要更簡潔高效。 APM 系列至此已經完成了大半,當然除了網上常用的 APM 手段之外,筆者還會加入包括 RunLoop 優化運用相關的技術。
來自:http://ios.jobbole.com/93186/