質(zhì)量監(jiān)控-資源使用

前言

應(yīng)用性能的衡量標(biāo)準(zhǔn)有很多,從用戶的角度來看,卡頓是最明顯的表現(xiàn),但這不意味看起來不卡頓的應(yīng)用就不存在性能問題。從開發(fā)角度來看,衡量一段代碼或者說算法的標(biāo)準(zhǔn)包括空間復(fù)雜度和時(shí)間復(fù)雜度,分別對應(yīng)內(nèi)存和CPU兩種重要的計(jì)算機(jī)硬件。只有外在與內(nèi)在都做沒問題,才能說應(yīng)用的性能做好了。因此,一套應(yīng)用性能監(jiān)控系統(tǒng)對開發(fā)者的幫助是巨大的,它能幫助你找到應(yīng)用的性能瓶頸。

CPU

線程是程序運(yùn)行的最小單位,換句話來說就是:我們的應(yīng)用其實(shí)是由多個(gè)運(yùn)行在CPU上面的線程組合而成的。要想知道應(yīng)用占用了CPU多少資源,其實(shí)就是獲取應(yīng)用所有線程占用CPU的使用量。結(jié)構(gòu)體thread_basic_info封裝了單個(gè)線程的基本信息:

struct thread_basic_info {
    time_value_t    user_time;      /* user run time */
    time_value_t    system_time;    /* system run time */
    integer_t       cpu_usage;      /* scaled cpu usage percentage */
    policy_t        policy;         /* scheduling policy in effect */
    integer_t       run_state;      /* run state (see below) */
    integer_t       flags;          /* various flags (see below) */
    integer_t       suspend_count;  /* suspend count for thread */
    integer_t       sleep_time;     /* number of seconds that thread
                                   has been sleeping */
};

問題在于如何獲取這些信息。iOS的操作系統(tǒng)是基于Darwin內(nèi)核實(shí)現(xiàn)的,這個(gè)內(nèi)核提供了task_threads接口讓我們獲取所有的線程列表以及接口thread_info來獲取單個(gè)線程的信息:

kern_return_t task_threads
(
    task_inspect_t target_task,
    thread_act_array_t *act_list,
    mach_msg_type_number_t *act_listCnt
);

kern_return_t thread_info
(
    thread_inspect_t target_act,
    thread_flavor_t flavor,
    thread_info_t thread_info_out,
    mach_msg_type_number_t *thread_info_outCnt
);

第一個(gè)函數(shù)的target_task傳入進(jìn)程標(biāo)記,這里使用mach_task_self()獲取當(dāng)前進(jìn)程,后面兩個(gè)傳入兩個(gè)指針分別返回線程列表和線程個(gè)數(shù),第二個(gè)函數(shù)的flavor通過傳入不同的宏定義獲取不同的線程信息,這里使用THREAD_BASIC_INFO。此外,參數(shù)存在多種類型,實(shí)際上大多數(shù)都是mach_port_t類型的別名:

因此可以得到下面的代碼來獲取應(yīng)用對應(yīng)的CPU占用信息。宏定義TH_USAGE_SCALE返回CPU處理總頻率:

- (double)currentUsage {
    double usageRatio = 0;
    thread_info_data_t thinfo;
    thread_act_array_t threads;
    thread_basic_info_t basic_info_t;
    mach_msg_type_number_t count = 0;
    mach_msg_type_number_t thread_info_count = THREAD_INFO_MAX;

    if (task_threads(mach_task_self(), &threads, &count) == KERN_SUCCESS) {
        for (int idx = 0; idx < count; idx++) {
            if (thread_info(threads[idx], THREAD_BASIC_INFO, (thread_info_t)thinfo, &thread_info_count) == KERN_SUCCESS) {
                basic_info_t = (thread_basic_info_t)thinfo;
                if (!(basic_info_t->flags & TH_FLAGS_IDLE)) {
                    usageRatio += basic_info_t->cpu_usage / (double)TH_USAGE_SCALE;
                }
            }
        }
        assert(vm_deallocate(mach_task_self(), (vm_address_t)threads, count * sizeof(thread_t)) == KERN_SUCCESS);
    }
    return usageRatio * 100.;
}

實(shí)踐

由于硬件爆發(fā)式的性能增長,使用舊設(shè)備進(jìn)行性能監(jiān)控來優(yōu)化代碼的可行性更高。另外,不要害怕CPU的使用率過高。越高的占用率代表對CPU的有效使用越高,即便出現(xiàn)超出100%的使用率也不要害怕,需要結(jié)合FPS來觀察是否對使用造成了影響。

拿筆者最近的一次優(yōu)化來說,機(jī)型iPhone5c,性能瓶頸出現(xiàn)在啟動應(yīng)用之后第一次進(jìn)入城市選擇列表后出現(xiàn)的卡頓。通過監(jiān)控CPU發(fā)現(xiàn)在進(jìn)行的時(shí)候達(dá)到了187%左右的使用率,幀數(shù)下降到27幀左右。最初的偽代碼如下:

static BOOL kmc_should_update_cities = YES;

- (void)fetchCities {
    if (kmc_should_update_cities) {
        [self fetchCitiesFromRemoteServer: ^(NSDictionary * allCities) {
            [self updateCities: allCities];
            [allCities writeToFile: [self localPath] atomically: YES];
        }];
    } else {
        [self fetchCitiesFromLocal: ^(NSDictionary * allCities) {
            [self updateCities: allCities];
        }];
    }
}

上述的代碼對于數(shù)據(jù)的處理基本都放在子線程中處理,主線程實(shí)際上處理的工作并不多,但是同樣引發(fā)了卡頓。筆者的猜測是:CPU對所執(zhí)行的線程一視同仁,并不會因?yàn)橹骶€程的關(guān)系就額外分配更多的處理時(shí)間。因此當(dāng)子線程的任務(wù)處理超出了某個(gè)闕值時(shí),主線程照樣會受到影響,因此將耗時(shí)任務(wù)放到子線程處理來避免主線程卡頓是存在前提的。那么可以比較輕松的找到代碼中CPU消耗最嚴(yán)重的地方:文件寫入

多線程陷阱一文中我提到過并發(fā)存在的缺陷,這種缺陷同上面的代碼是一致的。數(shù)據(jù)在寫入本地的時(shí)候會占據(jù)CPU資源,直到寫入操作完成,如果此時(shí)其他核心上同樣處理著大量任務(wù),應(yīng)用基本逃不開卡頓出現(xiàn)。筆者的解決方案是使用將數(shù)據(jù)分片寫入本地,由于NSStream自身的設(shè)計(jì),可以保證寫入操作的流暢性:

case NSStreamEventHasSpaceAvailable: {
    LXDDispatchQueueAsyncBlockInUtility(^{
        uint8_t * writeBytes = (uint8_t *)_writeData.bytes;
        writeBytes += _currentOffset;
        NSUInteger dataLength = _writeData.length;
            
        NSUInteger writeLength = (dataLength - _currentOffset > kMaxBufferLength) ? kMaxBufferLength : (dataLength - _currentOffset);
        uint8_t buffer[writeLength];
        (void)memcpy(buffer, writeBytes, writeLength);
        writeLength = [self.outputStream write: buffer maxLength: writeLength];
        _currentOffset += writeLength;
    });
} break;

替換文件寫入的操作時(shí)候,再次進(jìn)入幀數(shù)已經(jīng)上升到了40左右了,但是仍然會在進(jìn)行之后發(fā)生短暫的卡頓。進(jìn)一步猜測原因是:在異步實(shí)現(xiàn)流寫入的操作時(shí),同樣異步進(jìn)行數(shù)據(jù)處理。多個(gè)線程與主線程搶占CPU資源,因此將數(shù)據(jù)處理的操作進(jìn)一步延后,放到寫入操作完成之后:

case NSStreamEventOpenCompleted: {
    [self fetchCitiesFromLocal: ^(NSDictionary * allCities) {
        [self updateCities: allCities];
    }];
} break;

完成這一步之后,除了頁面跳轉(zhuǎn)時(shí)幀數(shù)會降到40左右,跳轉(zhuǎn)完成之后基本保持在52以上的幀數(shù)。此外,其他的優(yōu)化方案還包括UI展示時(shí)對數(shù)據(jù)的懶加載等,不過上面二個(gè)處理更符合CPU相關(guān)優(yōu)化的概念,因此其他的就不再多說。

內(nèi)存

進(jìn)程的內(nèi)存使用信息同樣放在了另一個(gè)結(jié)構(gòu)體mach_task_basic_info中,存儲了包括多種內(nèi)存使用信息:

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

對應(yīng)的獲取函數(shù)名為task_info,傳入進(jìn)程名、獲取的信息類型、信息存儲結(jié)構(gòu)體以及數(shù)量變量:

kern_return_t task_info
(
    task_name_t target_task,
    task_flavor_t flavor,
    task_info_t task_info_out,
    mach_msg_type_number_t *task_info_outCnt
);

由于mach_task_basic_info中的內(nèi)存使用bytes作為單位,在顯示之前我們還需要進(jìn)行一層轉(zhuǎn)換。另外為了方便實(shí)際使用中的換算,筆者使用結(jié)構(gòu)體來存儲內(nèi)存相關(guān)信息:

#ifndef NBYTE_PER_MB
#define NBYTE_PER_MB (1024 * 1024)
#endif

typedef struct LXDApplicationMemoryUsage
{
    double usage;   ///< 已用內(nèi)存(MB)
    double total;   ///< 總內(nèi)存(MB)
    double ratio;   ///< 占用比率
} LXDApplicationMemoryUsage;

獲取內(nèi)存占用量的代碼如下:

- (LXDApplicationMemoryUsage)currentUsage {
    struct mach_task_basic_info info;
    mach_msg_type_number_t count = sizeof(info) / sizeof(integer_t);
    if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO, (task_info_t)&info, &count) == KERN_SUCCESS) {
        return (LXDApplicationMemoryUsage){
            .usage = info.resident_size / NBYTE_PER_MB,
            .total = [NSProcessInfo processInfo].physicalMemory / NBYTE_PER_MB,
            .ratio = info.virtual_size / [NSProcessInfo processInfo].physicalMemory,
        };
    }
    return (LXDApplicationMemoryUsage){ 0 };
}

展示

內(nèi)存和CPU的監(jiān)控并不像其他設(shè)備信息一樣,能做更多有趣的事情。實(shí)際上,這兩者的獲取是一段枯燥又固定的代碼,因此并沒有太多可說的。對于這兩者的信息,基本上是開發(fā)階段展示出來觀察性能的。因此設(shè)置一個(gè)良好的查詢周期以及展示是這個(gè)過程中相對好玩的地方。筆者最終監(jiān)控的效果如下:

不知道什么原因?qū)е铝?code>task_info獲取到的內(nèi)存信息總是比Xcode自身展示的要多20M左右,因此使用的時(shí)候自行扣去這一部分再做衡量。為了保證展示器總能顯示在頂部,筆者創(chuàng)建了一個(gè)UIWindow的單例,通過設(shè)置windowLevel的值為CGFLOAT_MAX來保證顯示在最頂層,并且重寫了一部分方法保證不被修改:

- (instancetype)initWithFrame: (CGRect)frame {
    if (self = [super initWithFrame: frame]) {
        [super setUserInteractionEnabled: NO];
        [super setWindowLevel: CGFLOAT_MAX];
    
        self.rootViewController = [UIViewController new];
        [self makeKeyAndVisible];
    }
    return self;
}

- (void)setWindowLevel: (UIWindowLevel)windowLevel { }
- (void)setBackgroundColor: (UIColor *)backgroundColor { }
- (void)setUserInteractionEnabled: (BOOL)userInteractionEnabled { }

三個(gè)標(biāo)簽欄采用異步繪制的方式保證更新文本的時(shí)候不影響主線程,核心代碼:

CGSize textSize = [attributedText.string boundingRectWithSize: size options: NSStringDrawingUsesLineFragmentOrigin attributes: @{ NSFontAttributeName: self.font } context: nil].size;
textSize.width = ceil(textSize.width);
textSize.height = ceil(textSize.height);
    
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake((size.width - textSize.width) / 2, 5, textSize.width, textSize.height));
CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributedText);
CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, attributedText.length), path, NULL);
CTFrameDraw(frame, context);
    
UIImage * contents = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
CFRelease(frameSetter);
CFRelease(frame);
CFRelease(path);
dispatch_async(dispatch_get_main_queue(), ^{
    self.layer.contents = (id)contents.CGImage;
});

其他

除了監(jiān)控應(yīng)用本身占用的CPU和內(nèi)存資源之外,Darwin提供的接口還允許我們?nèi)ケO(jiān)控整個(gè)設(shè)備本身的內(nèi)存和CPU使用量,筆者分別封裝了額外兩個(gè)類來獲取這些數(shù)據(jù)。最后統(tǒng)一封裝了LXDResourceMonitor類來監(jiān)控這些資源的使用,通過枚舉來控制監(jiān)控內(nèi)容:

typedef NS_ENUM(NSInteger, LXDResourceMonitorType)
{
    LXDResourceMonitorTypeDefault = (1 << 2) | (1 << 3),
    LXDResourceMonitorTypeSystemCpu = 1 << 0,   ///<    監(jiān)控系統(tǒng)CPU使用率,優(yōu)先級低
    LXDResourceMonitorTypeSystemMemory = 1 << 1,    ///<    監(jiān)控系統(tǒng)內(nèi)存使用率,優(yōu)先級低
    LXDResourceMonitorTypeApplicationCpu = 1 << 2,  ///<    監(jiān)控應(yīng)用CPU使用率,優(yōu)先級高
    LXDResourceMonitorTypeApplicationMemoty = 1 << 3,   ///<    監(jiān)控應(yīng)用內(nèi)存使用率,優(yōu)先級高
};

這里使用到了位運(yùn)算的內(nèi)容,相比起其他的手段要更簡潔高效。APM系列至此已經(jīng)完成了大半,當(dāng)然除了網(wǎng)上常用的APM手段之外,筆者還會加入包括RunLoop優(yōu)化運(yùn)用相關(guān)的技術(shù)。

Demo在此

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,466評論 25 708
  • 從三月份找實(shí)習(xí)到現(xiàn)在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發(fā)崗...
    時(shí)芥藍(lán)閱讀 42,372評論 11 349
  • 又來到了一個(gè)老生常談的問題,應(yīng)用層軟件開發(fā)的程序員要不要了解和深入學(xué)習(xí)操作系統(tǒng)呢? 今天就這個(gè)問題開始,來談?wù)劜?..
    tangsl閱讀 4,172評論 0 23
  • 準(zhǔn)確衡量自己,沒必要自卑,活出自己 剛才下火車對吳老師不自信,也沒做錯(cuò)啥。大膽去說,差不多即可。和真老師打電話有點(diǎn)...
    三不主義閱讀 150評論 0 0
  • 今天終于將公眾號注冊成功了,說起注冊公眾號也是一條坎坷之路,去年報(bào)寫手圈訓(xùn)練營自己的作品需要一個(gè)寄存之處,好多同學(xué)...
    山水伊人兒閱讀 191評論 0 0