天之道,損有餘而補不足git
原文連接github
Mach Task
,Task下可能有多條線程同時執行任務,每一個線程都是利用CPU的基本單位。要計算CPU 佔用率,就須要得到當前Mach Task
下,全部線程佔用 CPU 的狀況。Mach Task
包含它的線程列表。內核提供了task_threads
API 調用獲取指定 task 的線程列表,而後能夠經過thread_info
API調用來查詢指定線程的信息,kern_return_t task_threads
(
task_t target_task,
thread_act_array_t *act_list,
mach_msg_type_number_t *act_listCnt
);
複製代碼
說明:task_threads
將target_task
任務中的全部線程保存在act_list
數組中,act_listCnt
表示線程個數:web
thread_info
這個API調用來查詢指定線程的信息,thread_info結構以下:kern_return_t thread_info
(
thread_act_t target_act,
thread_flavor_t flavor, // 傳入不一樣的宏定義獲取不一樣的線程信息
thread_info_t thread_info_out, // 查詢到的線程信息
mach_msg_type_number_t *thread_info_outCnt // 信息的大小
);
複製代碼
thread_basic_info
結構體封裝了單個線程的基本信息:struct thread_basic_info {
time_value_t user_time; // 用戶運行時長
time_value_t system_time; // 系統運行時長
integer_t cpu_usage; // CPU 使用率
policy_t policy; // 調度策略
integer_t run_state; // 運行狀態
integer_t flags; // 各類標記
integer_t suspend_count; // 暫停線程的計數
integer_t sleep_time; // 休眠的時間
};
複製代碼
/*
* Scale factor for usage field.
*/
#define TH_USAGE_SCALE 1000
複製代碼
+ (double)getCpuUsage {
kern_return_t kr;
thread_array_t threadList; // 保存當前Mach task的線程列表
mach_msg_type_number_t threadCount; // 保存當前Mach task的線程個數
thread_info_data_t threadInfo; // 保存單個線程的信息列表
mach_msg_type_number_t threadInfoCount; // 保存當前線程的信息列表大小
thread_basic_info_t threadBasicInfo; // 線程的基本信息
// 經過「task_threads」API調用獲取指定 task 的線程列表
// mach_task_self_,表示獲取當前的 Mach task
kr = task_threads(mach_task_self(), &threadList, &threadCount);
if (kr != KERN_SUCCESS) {
return -1;
}
double cpuUsage = 0;
// 遍歷全部線程
for (int i = 0; i < threadCount; i++) {
threadInfoCount = THREAD_INFO_MAX;
// 經過「thread_info」API調用來查詢指定線程的信息
// flavor參數傳的是THREAD_BASIC_INFO,使用這個類型會返回線程的基本信息,
// 定義在 thread_basic_info_t 結構體,包含了用戶和系統的運行時間、運行狀態和調度優先級等
kr = thread_info(threadList[i], THREAD_BASIC_INFO, (thread_info_t)threadInfo, &threadInfoCount);
if (kr != KERN_SUCCESS) {
return -1;
}
threadBasicInfo = (thread_basic_info_t)threadInfo;
if (!(threadBasicInfo->flags & TH_FLAGS_IDLE)) {
cpuUsage += threadBasicInfo->cpu_usage;
}
}
// 回收內存,防止內存泄漏
vm_deallocate(mach_task_self(), (vm_offset_t)threadList, threadCount * sizeof(thread_t));
return cpuUsage / (double)TH_USAGE_SCALE * 100.0;
}
複製代碼
mach_task_basic_info
結構體中 ,其中resident_size
爲駐留內存大小,而phys_footprint表示實際使用的物理內存,iOS 9以後使用phys_footprint來統計App佔用的內存大小(和Xcode和Instruments的值顯示值接近)。struct task_vm_info {
mach_vm_size_t virtual_size; // 虛擬內存大小
integer_t region_count; // 內存區域的數量
integer_t page_size;
mach_vm_size_t resident_size; // 駐留內存大小
mach_vm_size_t resident_size_peak; // 駐留內存峯值
...
/* added for rev1 */
mach_vm_size_t phys_footprint; // 實際使用的物理內存
...
複製代碼
uint64_t qs_getAppMemoryBytes() {
task_vm_info_data_t vmInfo;
mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
kern_return_t result = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t) &vmInfo, &count);
if (result != KERN_SUCCESS)
return 0;
return vmInfo.phys_footprint;
}
複製代碼
FPS
是Frames Per Second
,意思是每秒幀數,也就是咱們常說的「刷新率(單位爲Hz)。FPS低(小於50)表示App不流暢,App須要優化,iOS手機屏幕的正常刷新頻率是每秒60次,即FPS
值爲60。CADisplayLink
是和屏幕刷新頻率保存一致,它是CoreAnimation
提供的另外一個相似於NSTimer
的類,它老是在屏幕完成一次更新以前啓動,CADisplayLink
有一個整型的frameInterval
屬性,指定了間隔多少幀以後才執行。默認值是1,意味着每次屏幕更新以前都會執行一次。- (void)start {
//注意CADisplayLink的處理循環引用問題
self.displayLink = [CADisplayLink displayLinkWithTarget:[YYWeakProxy proxyWithTarget:self] selector:@selector(updateFPSCount:)];
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
// 執行幀率和屏幕刷新率保持一致
- (void)updateFPSCount:(CADisplayLink *)displayLink {
if (self.lastTimeStamp == 0) {
self.lastTimeStamp = self.displayLink.timestamp;
} else {
self.total++;
// 開始渲染時間與上次渲染時間差值
NSTimeInterval useTime = self.displayLink.timestamp - self.lastTimeStamp;
//小於1s當即返回
if (useTime < 1){
return;
}
self.lastTimeStamp = self.displayLink.timestamp;
// fps 計算
NSInteger fps = self.total / useTime;
NSLog(@"self.total = %@,useTime = %@,fps = %@",@(self.total),@(useTime),@(fps));
self.total = 0;
}
}
複製代碼
說明:不少團隊很是相信(甚至迷信)FPS值,認爲FPS值(大於50)就表明不卡頓,這點我是不承認。下面我列舉遇到的2個很是典型的Case。objective-c
drawRect:
方法,在模擬器上頻繁調用setNeedsDisplay來觸發drawRect:
方法,FPS值還穩定在50以上,可是真機上去掉幀很厲害。我認爲這裏犯了兩個錯誤。drawRect:
是利用CPU繪製的,性能並不如GPU繪製,對於頻繁繪製的繪製需求,不該該考慮使用重寫drawRect:
這種方式,推薦CAShapeLayer+UIBezierPath
。//RunLoop的狀態
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即將進入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), // 即將退出Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU // loop 全部狀態改變
};
複製代碼
因爲kCFRunLoopBeforeSources以後
須要處理Source0,kCFRunLoopAfterWaiting以後
須要處理timer、dispatch 到 main_queue 的 block和Source1,因此能夠認爲kCFRunLoopBeforeSources
和kCFRunLoopAfterWaiting
。由於kCFRunLoopBeforeSources
以後和kCFRunLoopAfterWaiting
以後是事情處理的主要時間段。數組
dispatch_semaphore_t信號量機制特性:信號量到達、或者 超時會繼續向下進行,不然等待;若是超時則返回的結果一定不爲0,不然信號量到達結果爲0。緩存
主線程卡頓發生是由於要處理大量的事情。這就意味着主線程在消耗時間在處理繁重的事件,致使信號超時了(dispatch_semaphore_signal不能及時執行),若是此時發現當前的RunLoop的狀態是kCFRunLoopBeforeSources或kCFRunLoopAfterWaiting,就認爲主線程長期停留在這兩個狀態上,此時就斷定卡頓發生。bash
// QSMainThreadMonitor.h
@interface QSMainThreadMonitor : NSObject
+ (instancetype)sharedInstance;
- (void)beginMonitor;
- (void)stopMonitor;
@end
// QSMainThreadMonitor.m
@interface QSMainThreadMonitor()
@property (nonatomic,strong) dispatch_semaphore_t semaphore;
@property (nonatomic,assign) CFRunLoopObserverRef observer;
@property (nonatomic,assign) CFRunLoopActivity runloopActivity;
@property (nonatomic,strong) dispatch_queue_t monitorQueue;
@end
@implementation QSMainThreadMonitor
+ (instancetype)sharedInstance {
static QSMainThreadMonitor *monitor = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
monitor = [[QSMainThreadMonitor alloc]init];
});
return monitor;
}
- (instancetype)init {
self = [super init];
if (self) {
self.monitorQueue = dispatch_queue_create("com.main.thread.monitor.queue", DISPATCH_QUEUE_CONCURRENT);
}
return self;
}
- (void)beginMonitor{
if (self.observer) {
return;
}
__block int timeoutCount = 0;
//建立觀察者並添加到主線程
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL,NULL};
self.observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &runLoopObserverCallBack, &context);
//將self.observer添加到主線程RunLoop的Common模式下觀察
CFRunLoopAddObserver(CFRunLoopGetMain(), self.observer, kCFRunLoopCommonModes);
self.semaphore = dispatch_semaphore_create(0);
dispatch_async(self.monitorQueue, ^{
while (YES) {
long result = dispatch_semaphore_wait(self.semaphore, dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC));
if (result != 0 && self.observer) {
//超時判斷
if (self.runloopActivity == kCFRunLoopBeforeSources || self.runloopActivity == kCFRunLoopAfterWaiting) {
if (++timeoutCount < 1) {
NSLog(@"--timeoutCount--%@",@(timeoutCount));
continue;
}
//出現卡頓、進一步處理
NSLog(@"--timeoutCount 卡頓發生--");
// todo,eg:獲取堆棧信息並上報
}
}else {
timeoutCount = 0;
}
}
});
}
- (void)stopMonitor{
if (!self.observer) {
return;
}
CFRunLoopRemoveObserver(CFRunLoopGetMain(), self.observer, kCFRunLoopCommonModes);
CFRelease(self.observer);
self.observer = NULL;
}
#pragma mark -Private Method
/**
* 觀察者回調函數
*/
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
//每一次監測到Runloop狀態變化調用
QSMainThreadMonitor *monitor = (__bridge QSMainThreadMonitor *)info;
monitor.runloopActivity = activity;
if (monitor.semaphore) {
dispatch_semaphore_signal(monitor.semaphore);
}
}
@end
複製代碼
kCFRunLoopBeforeSources
或kCFRunLoopAfterWaiting
狀態就認爲卡頓。利用UIDevice
提供了獲取設備電池的相關信息,包括當前電池的狀態以及電量。服務器
//開啓電量監控
[UIDevice currentDevice].batteryMonitoringEnabled = YES;
//監聽電量使用狀況
[[NSNotificationCenter defaultCenter] addObserverForName:UIDeviceBatteryLevelDidChangeNotification object:nil queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification *notification) {
// Level has changed
NSLog(@"");
//UIDevice返回的batteryLevel的範圍在0到1之間。
NSUInteger batteryLevel = [UIDevice currentDevice].batteryLevel * 100;
NSLog(@"[Battery Level]: %@", @(batteryLevel));
}];
複製代碼
說明:使用 UIDevice
能夠很是方便獲取到電量,可是經測試發現,在 iOS 8.0 以前,batteryLevel
只能精確到5%,而在 iOS 8.0
以後,精確度能夠達到1%微信
IOKit
, 經過它能夠獲取設備電量信息,精確度達到1%。#import "IOPSKeys.h"
#import "IOPowerSources.h"
-(double) getBatteryLevel{
// 返回電量信息
CFTypeRef blob = IOPSCopyPowerSourcesInfo();
// 返回電量句柄列表數據
CFArrayRef sources = IOPSCopyPowerSourcesList(blob);
CFDictionaryRef pSource = NULL;
const void *psValue;
// 返回數組大小
int numOfSources = CFArrayGetCount(sources);
// 計算大小出錯處理
if (numOfSources == 0) {
NSLog(@"Error in CFArrayGetCount");
return -1.0f;
}
// 計算所剩電量
for (int i=0; i<numOfSources; i++) {
// 返回電源可讀信息的字典
pSource = IOPSGetPowerSourceDescription(blob, CFArrayGetValueAtIndex(sources, i));
if (!pSource) {
NSLog(@"Error in IOPSGetPowerSourceDescription");
return -1.0f;
}
psValue = (CFStringRef) CFDictionaryGetValue(pSource, CFSTR(kIOPSNameKey));
int curCapacity = 0;
int maxCapacity = 0;
double percentage;
psValue = CFDictionaryGetValue(pSource, CFSTR(kIOPSCurrentCapacityKey));
CFNumberGetValue((CFNumberRef)psValue, kCFNumberSInt32Type, &curCapacity);
psValue = CFDictionaryGetValue(pSource, CFSTR(kIOPSMaxCapacityKey));
CFNumberGetValue((CFNumberRef)psValue, kCFNumberSInt32Type, &maxCapacity);
percentage = ((double) curCapacity / (double) maxCapacity * 100.0f);
NSLog(@"curCapacity : %d / maxCapacity: %d , percentage: %.1f ", curCapacity, maxCapacity, percentage);
return percentage;
}
return -1.
}
複製代碼
說明:網絡
CPU使用率高的操做
線程過多 (控制合適的線程數)
定位 (按需使用,下降頻次)
CPU任務繁重 (使用輕量級對象,緩存計算結果,對象複用等)
頻繁網絡請求(避免無效冗餘的網絡請求)
複製代碼
I/O操做頻繁的操做
直接讀寫磁盤文件 (合理利用內存緩存,碎片化的數據在內存中聚合,合適時機寫入磁盤)
複製代碼
對APP的質量指標的監控,是爲了更早地發現問題;發現問題是爲了更好地解決問題。因此監控不是終點,是起點。
在17年時候,在簡書中寫了iOS實錄14:淺談iOS Crash(一)和 iOS實錄15:淺談iOS Crash(二)兩篇文章;時隔兩年以後,書寫此文,是爲了記念過去大半年時候在App質量監控上花的努力。
文章篇幅有限,沒有介紹具體的優化辦法。