iOS监控:资源使用
lilwp345
8年前
<h3>前言</h3> <p>应用性能的衡量标准有很多,从用户的角度来看,卡顿是最明显的表现,但这不意味看起来不卡顿的应用就不存在性能问题。从开发角度来看,衡量一段代码或者说算法的标准包括空间复杂度和时间复杂度,分别对应内存和 CPU 两种重要的计算机硬件。只有外在与内在都做没问题,才能说应用的性能做好了。因此,一套应用性能监控系统对开发者的帮助是巨大的,它能帮助你找到应用的性能瓶颈。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/b8928aade218ade0d945ff842b22c63b.jpg"></p> <h3>CPU</h3> <p>线程是程序运行的最小单位,换句话来说就是:我们的应用其实是由多个运行在 CPU 上面的线程组合而成的。要想知道应用占用了 CPU 多少资源,其实就是获取应用所有线程占用 CPU 的使用量。结构体 thread_basic_info 封装了单个线程的基本信息:</p> <pre> <code class="language-objectivec">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 */ }; </code></pre> <p>问题在于如何获取这些信息。 iOS 的操作系统是基于 Darwin 内核实现的,这个内核提供了 task_threads 接口让我们获取所有的线程列表以及接口 thread_info 来获取单个线程的信息:</p> <pre> <code class="language-objectivec">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 ); </code></pre> <p>第一个函数的 target_task 传入进程标记,这里使用 mach_task_self() 获取当前进程,后面两个传入两个指针分别返回线程列表和线程个数,第二个函数的 flavor 通过传入不同的宏定义获取不同的线程信息,这里使用 THREAD_BASIC_INFO 。此外,参数存在多种类型,实际上大多数都是 mach_port_t 类型的别名:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/c0265d59806d04458fe99abe30ea4883.jpg"></p> <p>因此可以得到下面的代码来获取应用对应的 CPU 占用信息。宏定义 TH_USAGE_SCALE 返回 CPU 处理总频率:</p> <pre> <code class="language-objectivec">- (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.; } </code></pre> <h3>内存</h3> <p>进程的内存使用信息同样放在了另一个结构体 mach_task_basic_info 中,存储了包括多种内存使用信息:</p> <pre> <code class="language-objectivec">#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 */ }; </code></pre> <p>对应的获取函数名为 task_info ,传入进程名、获取的信息类型、信息存储结构体以及数量变量:</p> <pre> <code class="language-objectivec">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 ); </code></pre> <p>由于 mach_task_basic_info 中的内存使用 bytes 作为单位,在显示之前我们还需要进行一层转换。另外为了方便实际使用中的换算,笔者使用结构体来存储内存相关信息:</p> <pre> <code class="language-objectivec">#ifndef NBYTE_PER_MB #define NBYTE_PER_MB (1024 * 1024) #endif typedef struct LXDApplicationMemoryUsage { double usage; /// </code></pre> <p>获取内存占用量的代码如下:</p> <pre> <code class="language-objectivec">- (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 }; } </code></pre> <h3>展示</h3> <p>内存和 CPU 的监控并不像其他设备信息一样,能做更多有趣的事情。实际上,这两者的获取是一段枯燥又固定的代码,因此并没有太多可说的。对于这两者的信息,基本上是开发阶段展示出来观察性能的。因此设置一个良好的查询周期以及展示是这个过程中相对好玩的地方。笔者最终监控的效果如下:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/ac423c0bbf340f41fd651eb20361f3ef.gif"></p> <p>不知道什么原因导致了 task_info 获取到的内存信息总是比 Xcode 自身展示的要多 20M 左右,因此使用的时候自行扣去这一部分再做衡量。为了保证展示器总能显示在顶部,笔者创建了一个 UIWindow 的单例,通过设置 windowLevel 的值为 CGFLOAT_MAX 来保证显示在最顶层,并且重写了一部分方法保证不被修改:</p> <pre> <code class="language-objectivec">- (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 { } </code></pre> <p>三个标签栏采用异步绘制的方式保证更新文本的时候不影响主线程,核心代码:</p> <pre> <code class="language-objectivec">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; }); </code></pre> <h3>其他</h3> <p>除了监控应用本身占用的 CPU 和内存资源之外, Darwin 提供的接口还允许我们去监控整个设备本身的内存和 CPU 使用量,笔者分别封装了额外两个类来获取这些数据。最后统一封装了 LXDResourceMonitor 类来监控这些资源的使用,通过枚举来控制监控内容:</p> <pre> <code class="language-objectivec">typedef NS_ENUM(NSInteger, LXDResourceMonitorType) { LXDResourceMonitorTypeDefault = (1 </code></pre> <p>这里使用到了 位运算 的内容,相比起其他的手段要更简洁高效。 APM 系列至此已经完成了大半,当然除了网上常用的 APM 手段之外,笔者还会加入包括 RunLoop 优化运用相关的技术。</p> <p> </p> <p> </p> <p>来自:http://ios.jobbole.com/93186/</p> <p> </p>