克魔助手 - iOS效能檢測平臺

雪奈椰子發表於2024-02-02

前言

眾所周知,如今的使用者變得越來越關心app的體驗,開發者必須關注應用效能所帶來的使用者流失問題。目前危害較大的效能問題主要有:閃退、卡頓、發熱、耗電快、網路劫持等,但是做過iOS開發的人都知道,在開發過程中我們沒有一個很直觀的工具可以實時的知道開發者寫出來的程式碼會不會造成效能問題,雖然Xcode裡提供了耗電量檢測、記憶體洩漏檢測等工具,但是這些工具使用效果並不理想(如Leak無法發現迴圈引用造成的記憶體洩漏)。所以這篇文章主要是介紹一款實時監控app各項效能指標的工具,包括 CPU佔用率、記憶體使用量、記憶體洩漏、FPS、卡頓檢測,並且會分析造成這些效能問題的原因。

CPU

CPU 是移動裝置最重要的組成部分,如果開發者寫的程式碼有問題導致CPU負載過高,會導致app使用過程中發生卡頓,同時也可能導致手機發熱發燙,耗電過快,嚴重影響使用者體驗。 如果想避免CPU負載過高可以透過檢測app的CPU使用率,然後可以發現導致CPU過高的程式碼,並根據具體情況最佳化。那該如何檢測CPU使用率呢?大學期間學過計算機的應該都上過作業系統這門課,學過的都知道執行緒CPU是排程和分配的基本單位,而應用作為程式執行時,包含了多個不同的執行緒,這樣如果我們能知道app裡所有執行緒佔用 CPU 的情況,也就能知道整個app的 CPU 佔用率。幸運的是我們在 Mach 層中 thread_basic_info 結構體發現了我們想要的東西, thread_basic_info 結構體定義如下:

CPU記憶體監控

克魔助手提供了分析記憶體佔用、檢視 CPU 實時活動資料以及追蹤特定應用程式的功能,讓開發者可以更好地瞭解應用程式的執行情況。 以下是一些示例截圖:

克魔助手 - iOS效能檢測平臺

克魔助手 - iOS效能檢測平臺

克魔助手 - iOS效能檢測平臺

同樣,克魔助手還提供了記憶體、GPU 效能監控、網路監控等功能,開發者可以檢視實時資料活動和追蹤應用程式的特定功能。 如下:

記憶體監控

以下是記憶體監控的示例截圖:

克魔助手 - iOS效能檢測平臺

Memory

實體記憶體(RAM)與 CPU 一樣都是系統中最稀少的資源,也是最有可能產生競爭的資源,應用記憶體與效能直接相關 - 通常是以犧牲別的應用為代價。 不像 PC 端,iOS 沒有交換空間作為備選資源,這就使得記憶體資源尤為重要。

App佔用的記憶體

獲取app記憶體的API同樣可以在 Mach層找到, mach_task_basic_info 結構體儲存了 Mach task 的記憶體使用資訊,其中 resident_size 就是應用使用的實體記憶體大小, virtual_size 是虛擬記憶體大小。

#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 */ };

最後得到獲取當前 App Memory 的使用情況:

- (CGFloat)usedMemory { task_basic_info_data_t taskInfo; mach_msg_type_number_t infoCount = TASK_BASIC_INFO_COUNT; kern_return_t kernReturn = task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)&taskInfo, &infoCount); if (kernReturn != KERN_SUCCESS) { return NSNotFound; } CGFloat value = (CGFloat)(taskInfo.resident_size / 1024.0 / 1024.0); return value; }

裝置已使用的記憶體

+ (CGFloat)deviceUsedMemory { size_t length = 0; int mib[6] = {0}; int pagesize = 0; mib[0] = CTL_HW; mib[1] = HW_PAGESIZE; length = sizeof(pagesize); if (sysctl(mib, 2, &pagesize, &length, NULL, 0) < 0) { return 0; } mach_msg_type_number_t count = HOST_VM_INFO_COUNT; vm_statistics_data_t vmstat; if (host_statistics(mach_host_self(), HOST_VM_INFO, (host_info_t)&vmstat, &count) != KERN_SUCCESS) { return 0; } int wireMem = vmstat.wire_count * pagesize; int activeMem = vmstat.active_count * pagesize; return (CGFloat)(wireMem + activeMem) / 1024.0 / 1024.0; }

裝置可用的記憶體

+ (CGFloat)deviceAvailableMemory { vm_statistics64_data_t vmStats; mach_msg_type_number_t infoCount = HOST_VM_INFO_COUNT; kern_return_t kernReturn = host_statistics(mach_host_self(), HOST_VM_INFO, (host_info_t)&vmStats, &infoCount); if (kernReturn != KERN_SUCCESS) { return NSNotFound; } return (CGFloat)(vm_page_size * (vmStats.free_count + vmStats.inactive_count) / 1024.0 / 1024.0); }

FPS

FPS即螢幕每秒的重新整理率,範圍在0-60之間,60。FPS是測量用於儲存、顯示動態影片的資訊數量,每秒鐘幀數愈多,所顯示的動作就會愈流暢,優秀的app都要保證FPS 在 55-60 之間,這樣才會給使用者流暢的感覺,反之,使用者則會感覺到卡頓。 對於FPS的計算網上爭議頗多,這邊使用的和 YYKit 中的 YYFPSLabel 原理一樣,系統提供了 CADisplayLink 這個 API,該API在螢幕每次繪製的時候都會回撥,透過接收 CADisplayLink 的回撥,計算每秒鐘收到的回撥次數得到螢幕每秒的重新整理次數,從而得到 FPS,具體程式碼如下:

首先,為了使用克魔助手檢查iOS遊戲的幀率,需要在電腦上安裝愛思助手或者iTunes驅動。隨後,透過USB連線蘋果手機到電腦,並啟動克魔助手。

克魔助手 - iOS效能檢測平臺

裝置選擇

第二步,在工具左側選單欄中,開啟“裝置”視窗。視窗中會顯示你連線的所有蘋果裝置,選擇要查詢的裝置,然後選中裝置。

克魔助手 - iOS效能檢測平臺

檢視FPS和fps監測

第三步,點選“開始監聽”,在右側的“FPS曲線”檢視當前應用程式的幀率。另外,在克魔助手中可以看到每秒影像

更新多少次,以及總影像更新次數,從而瞭解遊戲效能如何。

克魔助手 - iOS效能檢測平臺

克魔助手 - iOS效能檢測平臺

CPU佔比和應用追蹤及特定APP資料

第四步,檢視幀率是保證遊戲順暢性的重要因素,所以應用程式開發人員將會經常使用克魔助手檢查蘋果手機玩遊戲的幀率。當然,遊戲還有很多需要最佳化的地方,可以提高遊戲整體效能,所以克魔助手不僅提供了監控幀率,還提供了可以分析記憶體佔用,檢視CPU實時活動資料,以及追蹤特定app時等功能,讓開發者可以更好地瞭解遊戲執行情況。

克魔助手 - iOS效能檢測平臺

克魔助手 - iOS效能檢測平臺

克魔助手 - iOS效能檢測平臺

透過克魔助手的幫助,開發者可以方便快捷地檢查蘋果手機玩遊戲的幀率。瞭解幀率和遊戲效能對增加遊戲使用者量,增強使用者體驗和遊戲的質量都非常重要,所以在開發過程中不要忘記檢查蘋果手機的幀率。

值得注意的是基於 CADisplayLink 實現的 FPS 在生產場景中只有指導意義,不能代表真實的 FPS,因為基於 CADisplayLink 實現的 FPS 無法完全檢測出當前 Core Animation 的效能情況,它只能檢測出當前 RunLoop 的幀率。

Freezing

為什麼會出現卡頓

從一個畫素到最後真正顯示在螢幕上,iPhone 究竟在這個過程中做了些什麼?想要了解背後的運作流程,首先需要了解螢幕顯示的原理。iOS 上完成圖形的顯示實際上是 CPU、GPU 和顯示器協同工作的結果,具體來說,CPU 負責計算顯示內容,包括檢視的建立、佈局計算、圖片解碼、文字繪製等,CPU 完成計算後會將計算內容提交給 GPU,GPU 進行變換、合成、渲染後將渲染結果提交到幀緩衝區,當下一次垂直同步訊號(簡稱 V-Sync)到來時,最後顯示到螢幕上

上文中提到 V-Sync 是什麼,以及為什麼要在 iPhone 的顯示流程引入它呢?在 iPhone 中使用的是雙緩衝機制,即上圖中的 FrameBuffer 有兩個緩衝區,雙緩衝區的引入是為了提升顯示效率,但是與此同時,他引入了一個新的問題,當影片控制器還未讀取完成時,比如螢幕內容剛顯示一半時,GPU 將新的一幀內容提交到幀緩衝區並把兩個緩衝區進行交換後,影片控制器就會把新的一幀資料的下半段顯示到螢幕上,造成畫面撕裂現象,V-Sync 就是為了解決畫面撕裂問題,開啟 V-Sync 後,GPU 會在顯示器發出 V-Sync 訊號後,去進行新幀的渲染和緩衝區的更新。

搞清楚了 iPhone 的螢幕顯示原理後,下面來看看在 iPhone 上為什麼會出現卡頓現象,上文已經提及在影像真正在螢幕顯示之前,CPU 和 GPU 需要完成自身的任務,而如果他們完成的時間錯過了下一次 V-Sync 的到來(通常是1000/60=16.67ms),這樣就會出現螢幕還是之前幀的內容,這就是介面卡頓的原因(離屏渲染就是典型的卡頓問題)。不難發現,無論是 CPU 還是 GPU 引起錯過 V-Sync 訊號,都會造成介面卡頓。

那如何檢測卡頓呢?比較常見的思路是:開闢一條單獨的子執行緒,讓這條子執行緒去實時檢測主執行緒的 RunLoop 情況,實時計算 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting 兩個狀態之間的耗時是否超過某個閥值,如果超過閾值即認定主執行緒發生了卡頓。下面是程式碼實現:

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) { MyClass *object = (__bridge MyClass*)info; // 記錄狀態值 object->activity = activity; // 傳送訊號 dispatch_semaphore_t semaphore = moniotr->semaphore; dispatch_semaphore_signal(semaphore); } - (void)registerObserver { CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL}; CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &runLoopObserverCallBack, &context); CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes); // 建立訊號 semaphore = dispatch_semaphore_create(0); // 在子執行緒監控時長 dispatch_async(dispatch_get_global_queue(0, 0), ^{ while (YES) { // 假定連續5次超時50ms認為卡頓(當然也包含了單次超時250ms) long st = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC)); if (st != 0) { if (activity==kCFRunLoopBeforeSources || activity==kCFRunLoopAfterWaiting) { if (++timeoutCount < 5) continue; // 檢測到卡頓,進行卡頓上報 } } timeoutCount = 0; } }); }

當檢測到卡頓後可以進一步收集卡頓現場,如堆疊資訊等,關於收集堆疊資訊這裡就不細說,很多第三方庫都有實現,我之前是使用了專案中已經整合的收集崩潰資訊的三方庫,透過這個庫在收集堆疊資訊。

MemoryLeak

記憶體洩漏也是造成app記憶體過高的主要原因,如果iPhone手機的效能都很強,如果一個app會因為記憶體過高被系統強制殺掉,大部分都是存在記憶體洩漏。記憶體洩漏對於開發和測試而言表現得並不明顯,如果它不洩漏到一定程度是使用者是無法察覺的,但是這也是開發者必須杜絕的一大問題。

查詢記憶體洩漏

對於記憶體洩漏Xcode提供了Leak工具,但是使用過的人都知道Leak無法查出很多洩漏(如迴圈引用),在這裡檢測記憶體洩漏使用的是微信讀書團隊 Mr.佘 提供的工具 MLeakFinder。 這裡大致講一下實現原理,當一個VC(或者View)被pop或者被dismiss 2 秒後還沒有被銷燬則認定該VC(或View)發生了洩漏。那如何知道 2 秒後該物件有沒有被釋放呢,

+ (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self swizzleSEL:@selector(popViewControllerAnimated:) withSEL:@selector(swizzled_popViewControllerAnimated:)]; }); }

透過方法交換將系統的pop方法換掉,然後注入自己的程式碼實現,當呼叫pop時會呼叫 willDealloc 方法,該方法實現如下:

- (BOOL)willDealloc { __weak id weakSelf = self; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ __strong id strongSelf = weakSelf; [strongSelf assertNotDealloc]; }); return YES; }

新增圖片註釋,不超過 140 字(可選)

透過弱引用持有自身,並在 2 秒後呼叫 assertNotDealloc, 如果 2 秒內該物件已釋放這裡的 weakSelf 為nil,也就什麼都不會發生,反之則認為發生了記憶體洩漏,進行下一步操作,如彈出警告等。 這裡只是大致講一下 MLeakFinder 的原理,詳細介紹可以去 詳細瞭解。

查詢迴圈引用

查詢迴圈引用使用的是 Facebook 開源庫 FBRetainCycleDetector ,具體也可以去網上查詢相關資料,這裡就不詳細說。


來自 “ ITPUB部落格 ” ,連結:https://blog.itpub.net/70026554/viewspace-3006132/,如需轉載,請註明出處,否則將追究法律責任。

相關文章