iOS监控:资源使用

2019阿里云全部产品优惠券(好东东,强烈推荐)
领取地址 https://promotion.aliyun.com/ntms/yunparter/invite.html

推荐:iOS FPS 监控

[iOS FPS 监控]

前言

应用性能的衡量标准有很多,从用户的角度来看,卡顿是最明显的表现,但这不意味看起来不卡顿的应用就不存在性能问题。从开发角度来看,衡量一段代码或者说算法的标准包括空间复杂度和时间复杂度,分别对应内存和CPU两种重要的计算机硬件。只有外在与内在都做没问题,才能说应用的性能做好了。因此,一套应用性能监控系统对开发者的帮助是巨大的,它能帮助你找到应用的性能瓶颈。

CPU

线程是程序运行的最小单位,换句话来说就是:我们的应用其实是由多个运行在CPU上面的线程组合而成的。要想知道应用占用了CPU多少资源,其实就是获取应用所有线程占用CPU的使用量。结构体thread_basic_info封装了单个线程的基本信息:

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的操作系统是基于Darwin内核实现的,这个内核提供了task_threads接口让我们获取所有的线程列表以及接口thread_info来获取单个线程的信息:

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
);

第一个函数的target_task传入进程标记,这里使用mach_task_self()获取当前进程,后面两个传入两个指针分别返回线程列表和线程个数,第二个函数的flavor通过传入不同的宏定义获取不同的线程信息,这里使用THREAD_BASIC_INFO。此外,参数存在多种类型,实际上大多数都是mach_port_t类型的别名:

因此可以得到下面的代码来获取应用对应的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 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.;
}

内存

进程的内存使用信息同样放在了另一个结构体mach_task_basic_info中,存储了包括多种内存使用信息:

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

对应的获取函数名为task_info,传入进程名、获取的信息类型、信息存储结构体以及数量变量:

推荐:iOS 性能监控方案 Wedjat

[作者:敖志敏 本文为原创文章,转载请注明作者及出处 为什么写这篇文章? 随着移动互联网向纵深发展,用户变得越来越关心应用的体验,开发者必须关注应用性能所带来的用户

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中的内存使用bytes作为单位,在显示之前我们还需要进行一层转换。另外为了方便实际使用中的换算,笔者使用结构体来存储内存相关信息:

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

typedef struct LXDApplicationMemoryUsage
{
    double usage;   ///

获取内存占用量的代码如下:

- (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 };
}

展示

内存和CPU的监控并不像其他设备信息一样,能做更多有趣的事情。实际上,这两者的获取是一段枯燥又固定的代码,因此并没有太多可说的。对于这两者的信息,基本上是开发阶段展示出来观察性能的。因此设置一个良好的查询周期以及展示是这个过程中相对好玩的地方。笔者最终监控的效果如下:

不知道什么原因导致了task_info获取到的内存信息总是比Xcode自身展示的要多20M左右,因此使用的时候自行扣去这一部分再做衡量。为了保证展示器总能显示在顶部,笔者创建了一个UIWindow的单例,通过设置windowLevel的值为CGFLOAT_MAX来保证显示在最顶层,并且重写了一部分方法保证不被修改:

- (instancetype)initWithFrame: (CGRect)frame {
    if (self = [super initWithFrame: frame]) {
        [super setUserInteractionEnabled: NO];
        [super setWindowLevel: CGFLOAT_MAX];

        [[UIApplication sharedApplication].keyWindow addSubview: self];
        [self makeKeyAndVisible];
    }
    return self;
}

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

三个标签栏采用异步绘制的方式保证更新文本的时候不影响主线程,核心代码:

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;
});

其他

除了监控应用本身占用的CPU和内存资源之外,Darwin提供的接口还允许我们去监控整个设备本身的内存和CPU使用量,笔者分别封装了额外两个类来获取这些数据。最后统一封装了LXDResourceMonitor类来监控这些资源的使用,通过枚举来控制监控内容:

typedef NS_ENUM(NSInteger, LXDResourceMonitorType)
{
    LXDResourceMonitorTypeDefault = (1

这里使用到了位运算的内容,相比起其他的手段要更简洁高效。APM系列至此已经完成了大半,当然除了网上常用的APM手段之外,笔者还会加入包括RunLoop优化运用相关的技术。

Demo在此

推荐:iOS监控:DNS劫持

[DNS劫持指在劫持的网络范围内拦截域名解析的请求,分析请求的域名,把审查范围以外的请求放行,否则返回假的IP地址或者什么都不做使请求失去响应,其效果就是对特定的网络

相关推荐