iOS研發助手DoraemonKit技術實現(一)中介紹了幾個經常使用工具集的技術實現,你們若是有疑問的話,能夠在文章中進行留言,也但願你們接入試用,或者加入到DoraemonKit交流羣一塊兒交流。ios
性能問題極大程度的會影響到用戶的體驗,對於咱們開發者和測試同窗要隨時隨地保證咱們app的質量,避免很差的體驗帶來用戶的流失。本篇文章咱們來說一下,性能監控的幾款工具的技術實現。主要包括,幀率監控、CPU監控、內存監控、流量監控、卡頓監控和自定義監控這幾個功能。git
有人說幀率、CPU和內存這些信息咱們均可以在Xcode中的Instruments工具進行聯調的時候能夠查看,爲何還要在客戶端中打印出來呢?github
app的流暢度是最直接影響用戶體驗的,若是咱們app持續卡頓,會嚴重影響咱們app的用戶留存度。因此對於用戶App是否流暢進行監控,可以讓咱們今早的發現咱們app的性能問題。對於App流暢度最直觀最簡單的監控手段就是對咱們App的幀率進行監控。瀏覽器
幀率(FPS)是指畫面每秒傳輸幀數,通俗來說就是指動畫或視頻的畫面數。FPS是測量用於保存、顯示動態視頻的信息數量。每秒鐘幀數愈多,所顯示的動做就會越流暢。對於咱們App開發來講,咱們要保持FPS高於50以上,用戶體驗纔會流暢。服務器
在YYKit Demo工程中有一個工具類叫YYFPSLabel,它是基於CADisplayLink這個類作FPS計算的,CADisplayLink是CoreAnimation提供的另外一個相似於NSTimer的類,它會在屏幕每次刷新回調一次。既然CADisplayLink能夠以屏幕刷新的頻率調用指定selector,並且iOS系統中正常的屏幕刷新率爲60Hz(60次每秒),那隻要在這個方法裏面統計每秒這個方法執行的次數,經過次數/時間就能夠得出當前屏幕的刷新率了。網絡
大體實現思路以下:app
- (void)startRecord{
if (_link) {
_link.paused = NO;
}else{
_link = [CADisplayLink displayLinkWithTarget:self selector:@selector(trigger:)];
[_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
_record = [DoraemonRecordModel instanceWithType:DoraemonRecordTypeFPS];
_record.startTime = [[NSDate date] timeIntervalSince1970];
}
}
- (void)trigger:(CADisplayLink *)link{
if (_lastTime == 0) {
_lastTime = link.timestamp;
return;
}
_count++;
NSTimeInterval delta = link.timestamp - _lastTime;
if (delta < 1) return;
_lastTime = link.timestamp;
CGFloat fps = _count / delta;
_count = 0;
NSInteger intFps = (NSInteger)(fps+0.5);
// 0~60 對應 高度0~200
[self.record addRecordValue:fps time:[[NSDate date] timeIntervalSince1970]];
[_oscillogramView addHeightValue:fps*200./60. andTipValue:[NSString stringWithFormat:@"%zi",intFps]];
}
複製代碼
值得注意的是基於CADisplayLink實現的 FPS 在生產場景中只有指導意義,不能表明真實的 FPS,由於基於CADisplayLink實現的 FPS 沒法徹底檢測出當前 Core Animation 的性能狀況,它只能檢測出當前 RunLoop 的幀率。但要真正定位到準確的性能問題所在,最好仍是經過Instrument來確認。具體緣由能夠參考iOS中基於CADisplayLink的FPS指示器詳解。tcp
全部代碼請參考:DorameonKit/Core/Plugin/FPSide
CPU是移動設備的運算核心和控制核心,若是咱們的App的使用率長時間處於高消耗的話,咱們的手機會發熱,電量使用加重,致使App產生卡頓,嚴重影響用戶體驗。因此對於CPU使用率進行實時的監控,也有利於及時的把控咱們App的總體質量,阻止不合格的功能上線。函數
對於app使用率的獲取,網上的方案仍是比較統一的。
代碼實現以下:
+ (CGFloat)cpuUsageForApp {
kern_return_t kr;
thread_array_t thread_list;
mach_msg_type_number_t thread_count;
thread_info_data_t thinfo;
mach_msg_type_number_t thread_info_count;
thread_basic_info_t basic_info_th;
// get threads in the task
// 獲取當前進程中 線程列表
kr = task_threads(mach_task_self(), &thread_list, &thread_count);
if (kr != KERN_SUCCESS)
return -1;
float tot_cpu = 0;
for (int j = 0; j < thread_count; j++) {
thread_info_count = THREAD_INFO_MAX;
//獲取每個線程信息
kr = thread_info(thread_list[j], THREAD_BASIC_INFO,
(thread_info_t)thinfo, &thread_info_count);
if (kr != KERN_SUCCESS)
return -1;
basic_info_th = (thread_basic_info_t)thinfo;
if (!(basic_info_th->flags & TH_FLAGS_IDLE)) {
// cpu_usage : Scaled cpu usage percentage. The scale factor is TH_USAGE_SCALE.
//宏定義TH_USAGE_SCALE返回CPU處理總頻率:
tot_cpu += basic_info_th->cpu_usage / (float)TH_USAGE_SCALE;
}
} // for each thread
// 注意方法最後要調用 vm_deallocate,防止出現內存泄漏
kr = vm_deallocate(mach_task_self(), (vm_offset_t)thread_list, thread_count * sizeof(thread_t));
assert(kr == KERN_SUCCESS);
return tot_cpu;
}
複製代碼
測試結果基本和Xcode測量出來的cpu使用率是同樣的,仍是比較準確的。
全部代碼請參考:DorameonKit/Core/Plugin/CPU
設備內存和CPU同樣都是系統中最稀少的資源,也是最有可能產生競爭的資源,應用內存跟app的性能直接相關。若是一個app在前臺消耗內存過多,會引發系統強殺,這種現象叫作OOM。表現跟crash同樣,並且這種crash事件沒法被捕獲到的。
獲取app消耗的內存,剛開始使用的是獲取使用的物理內存大小resident_size,網上大部分也是這種方案。
- (NSUInteger)getResidentMemory{
struct mach_task_basic_info info;
mach_msg_type_number_t count = MACH_TASK_BASIC_INFO_COUNT;
int r = task_info(mach_task_self(), MACH_TASK_BASIC_INFO, (task_info_t)& info, & count);
if (r == KERN_SUCCESS)
{
return info.resident_size;
}
else
{
return -1;
}
}
複製代碼
使用這種方式以後方向,會與Xcode自帶統計內存消耗的工具備一些誤差。這個時候,多謝yxjxx同窗提交的PRuse phys_footprint to get instruments memory usage。使用phys_footprint代替resident_size獲取的內存消耗基本與Xcode自帶的統計工具相同。具體緣由能夠參考正確地獲取 iOS 應用佔用的內存。
修改以後,具體實現的主要代碼以下:
//當前app消耗的內存
+ (NSUInteger)useMemoryForApp{
task_vm_info_data_t vmInfo;
mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
kern_return_t kernelReturn = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t) &vmInfo, &count);
if(kernelReturn == KERN_SUCCESS)
{
int64_t memoryUsageInByte = (int64_t) vmInfo.phys_footprint;
return memoryUsageInByte/1024/1024;
}
else
{
return -1;
}
}
//設備總的內存
+ (NSUInteger)totalMemoryForDevice{
return [NSProcessInfo processInfo].physicalMemory/1024/1024;
}
複製代碼
全部代碼請參考:DorameonKit/Core/Plugin/Memory
在線下開發階段,咱們開發要和服務端聯調結果,咱們須要Xcode斷點調試服務器返回的結果是否正確。測試階段,測試同窗會經過Charles設置代理查看結果,這些操做都須要依賴第三方工具才能實現流量監控。能不能有一個工具,可以隨身攜帶,對流量進行監控攔截,可以方便咱們不少。咱們DoraemonKit就作了這件事。
對於流量監控,業界基本有以上幾個方案:
方案1 : 騰訊GT的方案,監控系統的上行流量和下行流量。這樣監控的話,力度太粗了,不能獲得每個app的流量統計,更不能的獲得每個接口的流量和統計,不符合咱們的需求。
方案2 : 浸入業務方本身的網路庫,作流量統計,這種方案能夠作的很是細節,可是不是特別通用。咱們公司內部omega監控平臺就是這麼作的,omega的流量監控代碼是寫在OneNetworking中的。不是特別通用。好比咱們杭州團隊的網路庫是自研的,若是要接入omega的網絡監控功能,就須要在本身的網絡庫中,寫流量統計代碼。
方案3 : hook系統底層網絡庫,這種方式比較通用,可是很是繁瑣,須要hook不少個類和方法。阿里有篇文檔化介紹了他們流量監控的方案,就是採用這種,下面這張圖我截取過來的,看一下,仍是比較複雜的。
方案4 : 也是DoraemonKit採用的方案,使用iOS中一個很是強大的類,叫NSURLProtocol,這個類能夠攔截NSURLConnection、NSUrlSession、UIWebView中全部的網絡請求,獲取每個網絡請求的request和response對象。可是這個類沒法攔截tcp的請求,這個是他的缺點。美團的內部監控工具赫茲就是基於該類進行處理的。之餘這個類具體怎麼使用,因爲時間緣由,我在這裏就不說,我想你們推薦一下個人博客,我有篇文章專門寫了這個類的使用。
下面就是DoraemonKit中NSURLProtocol的具體實現:
@interface DoraemonNSURLProtocol()<NSURLConnectionDelegate,NSURLConnectionDataDelegate>
@property (nonatomic, strong) NSURLConnection *connection;
@property (nonatomic, assign) NSTimeInterval startTime;
@property (nonatomic, strong) NSURLResponse *response;
@property (nonatomic, strong) NSMutableData *data;
@property (nonatomic, strong) NSError *error;
@end
@implementation DoraemonNSURLProtocol
+ (BOOL)canInitWithRequest:(NSURLRequest *)request{
if ([NSURLProtocol propertyForKey:kDoraemonProtocolKey inRequest:request]) {
return NO;
}
if (![DoraemonNetFlowManager shareInstance].canIntercept) {
return NO;
}
if (![request.URL.scheme isEqualToString:@"http"] &&
![request.URL.scheme isEqualToString:@"https"]) {
return NO;
}
//NSLog(@"DoraemonNSURLProtocol == %@",request.URL.absoluteString);
return YES;
}
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request{
//NSLog(@"canonicalRequestForRequest");
NSMutableURLRequest *mutableReqeust = [request mutableCopy];
[NSURLProtocol setProperty:@YES forKey:kDoraemonProtocolKey inRequest:mutableReqeust];
return [mutableReqeust copy];
}
- (void)startLoading{
//NSLog(@"startLoading");
self.connection = [[NSURLConnection alloc] initWithRequest:[[self class] canonicalRequestForRequest:self.request] delegate:self];
[self.connection start];
self.data = [NSMutableData data];
self.startTime = [[NSDate date] timeIntervalSince1970];
}
- (void)stopLoading{
//NSLog(@"stopLoading");
[self.connection cancel];
DoraemonNetFlowHttpModel *httpModel = [DoraemonNetFlowHttpModel dealWithResponseData:self.data response:self.response request:self.request];
if (!self.response) {
httpModel.statusCode = self.error.localizedDescription;
}
httpModel.startTime = self.startTime;
httpModel.endTime = [[NSDate date] timeIntervalSince1970];
httpModel.totalDuration = [NSString stringWithFormat:@"%f",[[NSDate date] timeIntervalSince1970] - self.startTime];
[[DoraemonNetFlowDataSource shareInstance] addHttpModel:httpModel];
}
#pragma mark - NSURLConnectionDelegate
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
[[self client] URLProtocol:self didFailWithError:error];
self.error = error;
}
- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection *)connection {
return YES;
}
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
[[self client] URLProtocol:self didReceiveAuthenticationChallenge:challenge];
}
- (void)connection:(NSURLConnection *)connection didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
[[self client] URLProtocol:self didCancelAuthenticationChallenge:challenge];
}
#pragma mark - NSURLConnectionDataDelegate
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
[[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];
self.response = response;
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
[[self client] URLProtocol:self didLoadData:data];
[self.data appendData:data];
}
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse{
return cachedResponse;
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
[[self client] URLProtocolDidFinishLoading:self];
}
複製代碼
全部代碼請參考:DorameonKit/Core/Plugin/NetFlow
以上全部的操做都是針對於單個指標,沒法提供一套全面的監控數據,自定義監控能夠選擇你須要監控的數據,目前包括幀率、CPU使用率、內存使用量和流量監控,這些監控沒有波形圖進行顯示,均在後臺進行監控,測試完畢,會把這些數據上傳到咱們後臺進行分析。
由於目先後臺是基於咱們內部平臺上開發的,暫時不提供開源。不事後續的話,咱們也會考慮將後臺的功能的功能對外提供,請你們拭目以待。對於開源版本的話,目前性能測試的結果保存在沙盒Library/Caches/DoraemonPerformance中,使用者可使用沙盒瀏覽器功能導出來以後本身進行分析。
全部代碼請參考:DorameonKit/Core/Plugin/AllTest
寫這篇文章主要是爲了可以讓你們對於DorameonKit進行快速的瞭解,你們若是有什麼好的想法,或者發現咱們的這個項目有bug,歡迎你們去github上提Issues或者直接Pull requests,咱們會第一時間處理,也能夠加入咱們的qq交流羣進行交流,也但願咱們這個工具集合能在你們的一塊兒努力下,繼續作大作好。
若是你們以爲咱們這個項目還能夠的話,點上一顆star吧。
DoraemonKit項目地址:github.com/didi/Doraem…