iOS研發助手DoraemonKit技術實現(二)

1、前言

iOS研發助手DoraemonKit技術實現(一)中介紹了幾個經常使用工具集的技術實現,你們若是有疑問的話,能夠在文章中進行留言,也但願你們接入試用,或者加入到DoraemonKit交流羣一塊兒交流。ios

性能問題極大程度的會影響到用戶的體驗,對於咱們開發者和測試同窗要隨時隨地保證咱們app的質量,避免很差的體驗帶來用戶的流失。本篇文章咱們來說一下,性能監控的幾款工具的技術實現。主要包括,幀率監控、CPU監控、內存監控、流量監控、卡頓監控和自定義監控這幾個功能。git

有人說幀率、CPU和內存這些信息咱們均可以在Xcode中的Instruments工具進行聯調的時候能夠查看,爲何還要在客戶端中打印出來呢?github

  1. 第1、不少測試同窗比較關注App質量,可是他們卻沒有Xcode運行環境,他們對於質量數據沒法頗有效的查看。
  2. 第2、App端實時的查看App的質量數據,不依賴IDE,方便快捷直觀。
  3. 第3、實時採集性能數據,爲後期結合測試平臺產生性能數據報表提供數據來源。

2、技術實現

3.1:幀率展現

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

3.2:CPU展現

CPU是移動設備的運算核心和控制核心,若是咱們的App的使用率長時間處於高消耗的話,咱們的手機會發熱,電量使用加重,致使App產生卡頓,嚴重影響用戶體驗。因此對於CPU使用率進行實時的監控,也有利於及時的把控咱們App的總體質量,阻止不合格的功能上線。函數

對於app使用率的獲取,網上的方案仍是比較統一的。

  1. 使用task_threads函數,獲取當前App行程中全部的線程列表。
  2. 對於第一步中獲取的線程列表進行遍歷,經過thread_info函數獲取每個非閒置線程的cpu使用率,進行相加。
  3. 使用vm_deallocate函數釋放資源。

代碼實現以下:

+ (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

3.3:內存展現

設備內存和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

3.4:流量監控

流量監控1

流量監控2

流量監控3

流量監控4

流量監控5

在線下開發階段,咱們開發要和服務端聯調結果,咱們須要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

3.5:自定義監控

以上全部的操做都是針對於單個指標,沒法提供一套全面的監控數據,自定義監控能夠選擇你須要監控的數據,目前包括幀率、CPU使用率、內存使用量和流量監控,這些監控沒有波形圖進行顯示,均在後臺進行監控,測試完畢,會把這些數據上傳到咱們後臺進行分析。

由於目先後臺是基於咱們內部平臺上開發的,暫時不提供開源。不事後續的話,咱們也會考慮將後臺的功能的功能對外提供,請你們拭目以待。對於開源版本的話,目前性能測試的結果保存在沙盒Library/Caches/DoraemonPerformance中,使用者可使用沙盒瀏覽器功能導出來以後本身進行分析。

全部代碼請參考:DorameonKit/Core/Plugin/AllTest

3、總結

寫這篇文章主要是爲了可以讓你們對於DorameonKit進行快速的瞭解,你們若是有什麼好的想法,或者發現咱們的這個項目有bug,歡迎你們去github上提Issues或者直接Pull requests,咱們會第一時間處理,也能夠加入咱們的qq交流羣進行交流,也但願咱們這個工具集合能在你們的一塊兒努力下,繼續作大作好。

若是你們以爲咱們這個項目還能夠的話,點上一顆star吧。

DoraemonKit項目地址:github.com/didi/Doraem…

4、參考文章

iOS中基於CADisplayLink的FPS指示器詳解

iOS-Monitor-Platform

正確地獲取 iOS 應用佔用的內存

5、交流羣

相關文章
相關標籤/搜索