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

易翔本尊發表於2018-11-22

一、前言

iOS研發助手DoraemonKit技術實現(一)中介紹了幾個常用工具集的技術實現,大家如果有疑問的話,可以在文章中進行留言,也希望大家接入試用,或者加入到DoraemonKit交流群一起交流。

效能問題極大程度的會影響到使用者的體驗,對於我們開發者和測試同學要隨時隨地保證我們app的質量,避免不好的體驗帶來使用者的流失。本篇文章我們來講一下,效能監控的幾款工具的技術實現。主要包括,幀率監控、CPU監控、記憶體監控、流量監控、卡頓監控和自定義監控這幾個功能。

有人說幀率、CPU和記憶體這些資訊我們都可以在Xcode中的Instruments工具進行聯調的時候可以檢視,為什麼還要在客戶端中列印出來呢?

  1. 第一、很多測試同學比較關注App質量,但是他們卻沒有Xcode執行環境,他們對於質量資料無法很有效的檢視。
  2. 第二、App端實時的檢視App的質量資料,不依賴IDE,方便快捷直觀。
  3. 第三、實時採集效能資料,為後期結合測試平臺產生效能資料包表提供資料來源。

二、技術實現

3.1:幀率展示

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

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次每秒),那隻要在這個方法裡面統計每秒這個方法執行的次數,通過次數/時間就可以得出當前螢幕的重新整理率了。

大致實現思路如下:

- (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指示器詳解

所有程式碼請參考:DorameonKit/Core/Plugin/FPS

3.2:CPU展示

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

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:記憶體展示

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

裝置記憶體和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很多個類和方法。阿里有篇文件化介紹了他們流量監控的方案,就是採用這種,下面這張圖我擷取過來的,看一下,還是比較複雜的。


    iOS研發助手DoraemonKit技術實現(二)
  • 方案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:自定義監控

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

以上所有的操作都是針對於單個指標,無法提供一套全面的監控資料,自定義監控可以選擇你需要監控的資料,目前包括幀率、CPU使用率、記憶體使用量和流量監控,這些監控沒有波形圖進行顯示,均在後臺進行監控,測試完畢,會把這些資料上傳到我們後臺進行分析。

因為目前後臺是基於我們內部平臺上開發的,暫時不提供開源。不過後續的話,我們也會考慮將後臺的功能的功能對外提供,請大家拭目以待。對於開源版本的話,目前效能測試的結果儲存在沙盒Library/Caches/DoraemonPerformance中,使用者可以使用沙盒瀏覽器功能匯出來之後自己進行分析。

所有程式碼請參考:DorameonKit/Core/Plugin/AllTest

三、總結

寫這篇文章主要是為了能夠讓大家對於DorameonKit進行快速的瞭解,大家如果有什麼好的想法,或者發現我們的這個專案有bug,歡迎大家去github上提Issues或者直接Pull requests,我們會第一時間處理,也可以加入我們的qq交流群進行交流,也希望我們這個工具集合能在大家的一起努力下,繼續做大做好。

如果大家覺得我們這個專案還可以的話,點上一顆star吧。

DoraemonKit專案地址:github.com/didi/Doraem…

四、參考文章

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

iOS-Monitor-Platform

正確地獲取 iOS 應用佔用的記憶體

五、交流群

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


相關文章