- 一种简单的方式可对运行时对象方法耗时的检测。
- 一种简单的手段可对APP性能瓶颈的直观的定位。
- 独立的,自闭合Development pod配置环境以及面向切面的思想(AOP)达到了监控与业务的解耦合。
- 支持对方法耗时数据的接收和上传,方便对数据的深度分析挖掘。
- 方便的脚本支持,一键安装、一键清除。
可CocoaPods直接引入
pod 'LJBenchmark'
但是我们更加推荐使用脚本方式接入, 下载github仓库路径下的 /LJBenchmark/LJBenchmark/Classes/Workspace/benchmark 此文件为脚本文件。 脚本拖入您的工作仓库,也就是Podfile文件同级目录,然后,cd 到该目录下,命令行执行:
sh benchmark connect [file/dir/podspec_s_name]
安装完成以后,执行 Command+R运行项目,即可达到对 [file/dir/podspec_s_name] 描述的业务中函数方法耗时的实时监控。
如果您遇到,sed相关的错误,比如:
sed: -e expression #1, char 1: unknown command: `_’
sed: can not read …
不要担心,这是Mac的shell和Linux shell差异造成的,只需要更新下设置即可,或者直接下载使用我们gnu版本的shell脚本。
可参考链接:
https://www.sunshines.cc/tech/2018/10/26/sed-on-mac/
在设计LJBenchmark过程中我们对于耗时数据是直接放到RunLoop空闲状态下来处理的。
OBJC_EXPORT NSString * const kLJBenchmarkLogNotification;
此时我们会发送上述的通知,使用时可自行接收该通知,进行耗时数据展示的自定义。
sh benchmark clear [file/dir/podspec_s_name]
这时候脚本会将1.1中的接入的业务监控相关的设置从你的开发环境移除。
更多使用细节可执行如下命令行
sh benchmark --help
-
LJAspects可参考:https://github.com/steipete/Aspects
-
LJClassInfo 可参考:https://github.com/ibireme/YYModel
-
NSObject (LJBenchmark):采用MethodSwizzling技术手段hook了所有iOS对象的-init方法。
-
LJBenchmarkTaskManager:主要是对对象方法执行耗时信息进行内存的缓存和管理。
-
LJBenchmark:主要是对检测工具的一些配置设置相关。
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
ljbm_swizzleInstanceMethod(NSObject.class, @selector(init), @selector(ljbm_init_benchmark_hook));
});
}
- (instancetype)ljbm_init_benchmark_hook {
if (ljbm_benchmarkAvailable && ljbm_whiteList && [ljbm_whiteList containsObject:NSStringFromClass(self.class)])
{
[self ljbm_hookAllSels];
}
return [self ljbm_init_benchmark_hook];
}
在实际开发过程中属性方法的getter和Setter会被频繁的调用,多为系统自动编译生成, 所以在 - ljbm_hookAllSels 方法中使用 LJClassInfo 在运行时取得了该对象的对象方法列表和属性getter和setter方法集合的差集。
- (void)ljbm_hookAllSels {
Class cls = self.class;
/*
由于开发习惯导致重写getter && setter方法
导致getter方法调用比较频繁,为不比较交换
所以从hook方法列表中剔除
*/
NSMutableSet<NSString *> *propertySelNamesSet = nil;
unsigned int propertyCount = 0;
objc_property_t *properties = class_copyPropertyList(cls, &propertyCount);
if (properties) {
propertySelNamesSet = [NSMutableSet set];
for (unsigned int i = 0; i < propertyCount; i++) {
LJClassPropertyInfo *info = [[LJClassPropertyInfo alloc] initWithProperty:properties[i]];
if (info.setter != NULL) {
[propertySelNamesSet addObject:NSStringFromSelector(info.setter)];
}
if (info.getter != NULL) {
[propertySelNamesSet addObject:NSStringFromSelector(info.getter)];
}
}
free(properties);
}
unsigned int methodCount = 0;
Method *methods = class_copyMethodList(cls, &methodCount);
if (methods) {
for (unsigned int i = 0; i < methodCount; i++) {
Method method = methods[i];
LJClassMethodInfo *info = [[LJClassMethodInfo alloc] initWithMethod:method];
/*
!apsects not support struct return type hook
*/
if ([info.returnTypeEncoding hasPrefix:@"{"] && [info.returnTypeEncoding hasSuffix:@"}"]) {
continue;
}
SEL _sel = method_getName(method);
if (![self isSelInBlackList:_sel]) {
if (!propertySelNamesSet || ![propertySelNamesSet containsObject:NSStringFromSelector(_sel)]) {
[self ljbm_hookWithSel:_sel];
}
}
}
free(methods);
}
}
并在 - ljbm_hookWithSel 使用 LJAspects 的对象方法进行了统一的hook
- (id<LJAspectToken>)ljaspect_hookSelector:(SEL)selector
withOptions:(LJAspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
我们在方法执行前 LJAspectPositionBefore 和方法执行后 LJAspectPositionAfter 将方法执行的相关信息, 通过 LJBenchmarkTaskManager这个类进行内存的缓存和管理,在主线程runloop空闲的时候将信息输出。
- (void)ljbm_hookWithSel:(SEL)_sel {
NSString *selName = NSStringFromSelector(_sel);
NSString *clsName = NSStringFromClass(self.class);
NSString *desc = [NSString stringWithFormat:@"-[%@ %@]", clsName, selName];
NSMutableDictionary *passedDic = [NSMutableDictionary dictionaryWithCapacity:6];
[passedDic setObject:desc forKey:kLJBenchmarkLogDescKey];
[passedDic setObject:clsName forKey:kLJBenchmarkLogClsKey];
[passedDic setObject:selName forKey:kLJBenchmarkLogSelKey];
[passedDic setObject:@"0" forKey:kLJBenchmarkLogDurationKey];
__block CFTimeInterval startTime = 0;
__block struct timeval t0, t1;
[self ljaspect_hookSelector:_sel
withOptions:LJAspectPositionBefore
usingBlock:^(id<LJAspectInfo> info){
gettimeofday(&t0, NULL);
startTime = (double)(t0.tv_sec) * 1e3 + (double)(t0.tv_usec) * 1e-3;
config_add_task_queue(^{
[passedDic setObject:[NSNumber numberWithDouble:startTime] forKey:kLJBenchmarkLogStartKey];
});
}
error:NULL];
[self ljaspect_hookSelector:_sel
withOptions:LJAspectPositionAfter
usingBlock:^(id<LJAspectInfo> info){
gettimeofday(&t1, NULL);
double endTime = (double)(t1.tv_sec) * 1e3 + (double)(t1.tv_usec) * 1e-3;
config_add_task_queue(^{
[passedDic setObject:[NSNumber numberWithDouble:endTime] forKey:kLJBenchmarkLogEndKey];
[passedDic setObject:[NSNumber numberWithDouble:ljbm_filtLogTime] forKey:kLJBenchmarkLogFiltTimeKey];
[[LJBenchmarkTaskManager sharedInstance] addTimeDic:[passedDic mutableCopy]];
});
}
error:NULL];
}
- LJBMUserCustomConfiguration:一些用户想自定义的设置可在此进行
+ (void)userCustomConfiguration {
// 设置时长0.01ms内的耗时都监听
[LJBenchmark filtLogTimeGreaterThan:0.001];
[LJBenchmark addHookClassName:@"LJTestAsynchronous"];
[LJBenchmark addHookClassName:@"LJViewController"];
}
+ (void)load {
[self userCustomConfiguration];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(benchmarkTaskNotification:) name:kLJBenchmarkLogNotification object:nil];
}
+ (void)benchmarkTaskNotification:(NSNotification *)notification {
NSLog(@"%@", notification.object);
}
- 目录结构看LJBenchmark:
用户的自定义配置和对数据的接收以及输出完全是在 Development Pods 下单独的 LJBenchmark 项目下自我闭合的管理的,用户的所有使用过程中,不会存在着对业务代码的直接侵入。
-help:帮助脚本
-pod:pod相关的自动化管理脚本
-list:所有插件的查询脚本
-connect:自动链接业务并配置监控的脚本
-clear:自动清理业务监控配置的脚本
上述脚本均以插件挂载在 benchmark 脚本下作为operation被执行。
脚本的统一执行格式为
sh benchmark [operation] [source] [options]
operation:目前暂定义为help、pod、list、connect、clear。
source 通常为输入源,比如文件夹、文件或者podsepc名称代表的仓库等。
options 通常为可选项,由 operation 的行为定义的可选传入参数。
- 为什么推荐使用脚本 benchmark 接入?
这时候脚本会自动帮你布置初始化环境,分析你的业务版本信息和所有的类信息,并加载到日志中。 这对后继深度的进行版本性能优化对比分析的场景可能是有用的。
- log打印日志查看分析
假设我们发现某处页面打开时候加载比较缓慢或者卡顿,这时候我们的工具就能派上用场了。 下面是一个发现页面加载时长的场景的简化示例:
优化前我们发现 - viewdidload 该方法加载耗时比较严重。那么从调用栈的角度往上查找,会发现有取缓存数据的一个函数
耗时比较多。我们很直观的就发现了问题的所在,即在主线程中读取缓存然后渲染视图,并不是一个很好的选择。
改用异步线程读取缓存优化以后,实际真机体验取得了良好的效果。再用 LJBenchmark
观察页面加载数据:
不难看出,LJBenchmark对于函数耗时的分析,将有助于卡顿和延迟问题的快速定位和解决。