├── .gitignore ├── LICENSE ├── README.md ├── TimeProfiler.png ├── TimeProfiler ├── Core │ ├── TPCallTrace.c │ ├── TPCallTrace.h │ ├── TPMainVC.h │ ├── TPMainVC.m │ ├── TPModel.h │ ├── TPModel.m │ ├── TPRecordCell.h │ ├── TPRecordCell.m │ ├── TPRecordHierarchyModel.h │ ├── TPRecordHierarchyModel.m │ ├── TPRecordModel.h │ ├── TPRecordModel.m │ ├── TimeProfilerVC.h │ ├── TimeProfilerVC.m │ ├── UIWindow+CallRecordShake.h │ ├── UIWindow+CallRecordShake.m │ └── hookObjcMsgSend-arm64.s ├── TimeProfiler.h ├── TimeProfiler.m └── fishhook │ ├── fishhook.c │ └── fishhook.h └── tp.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | # CocoaPods 32 | # 33 | # We recommend against adding the Pods directory to your .gitignore. However 34 | # you should judge for yourself, the pros and cons are mentioned at: 35 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 36 | # 37 | # Pods/ 38 | 39 | # Carthage 40 | # 41 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 42 | # Carthage/Checkouts 43 | 44 | Carthage/Build 45 | 46 | # fastlane 47 | # 48 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 49 | # screenshots whenever they are needed. 50 | # For more information about the recommended setup visit: 51 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 52 | 53 | fastlane/report.xml 54 | fastlane/Preview.html 55 | fastlane/screenshots/**/*.png 56 | fastlane/test_output 57 | 58 | # Code Injection 59 | # 60 | # After new code Injection tools there's a generated folder /iOSInjectionProject 61 | # https://github.com/johnno1962/injectionforxcode 62 | 63 | iOSInjectionProject/ 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 maniac 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TimeProfiler 2 | Recording all OC methods in the main thread takes time 3 | 4 | > 大家对TimeProfiler有什么建议或者需求或遇到crash等所有事情,强烈欢迎到[Issues](https://github.com/maniackk/TimeProfiler/issues)去留言。 5 | 6 | 7 | 8 | # 更新计划 9 | **目前已经支持显示调用堆栈。** 10 | **支持super函数的统计** 11 | **重大更新:仿照os_signpost,用户在一个功能的开始调用TPStartTrace,在结束地方调用TPStopTrace。具体用法见下面👇** 12 | ## 1.1版本:增加耗时方法排序功能和耗时方法中调用次数排序功能(已做) 13 | 14 | ![pic](https://wukaikai.tech/images/tuchuang/tpjj.png) 15 | 16 | ## 1.2版本:优化代码质量和性能问题(未做) 17 | ## 1.3版本:增加打印卡顿时候,所有线程堆栈 (未做) 18 | 19 | 20 | # 特性 21 | 1. 记录所有在主线程运行的OC方法的耗时情况 22 | 2. 支持设置记录的最大深度和最小耗时 23 | 3. 显示调用堆栈 24 | 4. 支持super函数的统计 25 | 5. 支持在程序中多次监控,类似os_signpost 26 | 27 | # 支持机型 28 | iPhone5s及更新真机(arm64) 29 | 30 | # 用法 31 | ## 启动监控 32 | 33 | ``` 34 | //一个函数耗时统计 35 | - (void)viewDidLoad { 36 | [[TimeProfiler shareInstance] TPStartTrace:"大卡页的viewDidLoad函数"]; 37 | ... 38 | [[TimeProfiler shareInstance] TPStopTrace]; 39 | } 40 | 41 | //一个页面/功能点的耗时统计 42 | [[TimeProfiler shareInstance] TPStartTrace:"详情页"]; 43 | /* 中间可以跨多个函数或模块*/ 44 | [[TimeProfiler shareInstance] TPStopTrace]; 45 | 46 | 47 | /** 48 | 开始/停止统计,需成对使用 49 | 可以用来测多个功能点的耗时问题 50 | */ 51 | - (void)TPStartTrace:(char *)featureName; 52 | - (void)TPStopTrace; 53 | 54 | - (void)TPSetMaxDepth:(int)depth; //默认3;不调用的话,默认是3 55 | - (void)TPSetCostMinTime:(uint64_t)time; //单位为us,1ms = 1000us;不调用的话,默认是1000us 56 | - (void)TPSetFilterClass:(NSArray *)classArr; //需要过滤的类,不调用此方法,默认为TimeProfilerVC、TPRecordHierarchyModel、 TPRecordCell、TPRecordModel等TimeProfiler本身类(不统计过滤的类) 57 | ``` 58 | 59 | 把TimeProfiler文件夹放入项目中,run App后,摇一摇App,就可以看到主线程运行的OC方法的耗时情况 60 | 61 | # 原理介绍 62 | [博客](https://juejin.im/post/5d146490f265da1bc37f2065) 63 | 64 | -------------------------------------------------------------------------------- /TimeProfiler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maniackk/TimeProfiler/cec23b0a1aaaf52f8fac198d2540b9d7d3afd971/TimeProfiler.png -------------------------------------------------------------------------------- /TimeProfiler/Core/TPCallTrace.c: -------------------------------------------------------------------------------- 1 | // 2 | // TPCallTrace.c 3 | // VideoIphone 4 | // 5 | // Created by 吴凯凯 on 2019/6/13. 6 | // Copyright © 2019 吴凯凯. All rights reserved. 7 | // 8 | 9 | #include "TPCallTrace.h" 10 | 11 | #ifndef __arm64__ 12 | // 模拟器 或者 iPhone5及更老iPhone设备,不是使用arm64 13 | 14 | void startTrace(char *featureName) { 15 | printf("====模拟器或者iPhone5及更老iPhone设备,不能hook objc_msgSend===="); 16 | }; 17 | 18 | void stopTrace() { 19 | }; 20 | 21 | TPMainThreadCallRecord *getMainThreadCallRecord(void) 22 | { 23 | return NULL; 24 | } 25 | void setMaxDepth(int depth){}; 26 | void setCostMinTime(uint64_t time){}; 27 | 28 | #else 29 | // iPhone5s及更新设备 30 | 31 | //#include 32 | #include 33 | #include 34 | #include 35 | #include "fishhook.h" 36 | #include 37 | #include 38 | 39 | 40 | typedef struct { 41 | Class cls; 42 | SEL sel; 43 | uint64_t time; 44 | } MethodRecord; 45 | 46 | typedef struct { 47 | MethodRecord *stack; 48 | int allocLength; 49 | int index; 50 | } MainThreadMethodStack; 51 | 52 | typedef struct { 53 | int allocLength; 54 | int index; 55 | uintptr_t *lr_stack; 56 | } LRStack; 57 | 58 | void (*orgin_objc_msgSend)(void); 59 | void (*orgin_objc_msgSendSuper2)(void); 60 | static pthread_key_t threadKeyLR; 61 | static MainThreadMethodStack *mainThreadStack = NULL; 62 | static TPMainThreadCallRecord *mainThreadCallRecord = NULL; 63 | static bool CallRecordEnable = YES; 64 | static int maxDepth = 3; 65 | static int ignoreCallNum = 0; 66 | static uint64_t costMinTime = 1000; 67 | 68 | static inline uint64_t getVirtualCallTime() 69 | { 70 | struct timeval now; 71 | gettimeofday(&now, NULL); 72 | uint64_t time = (now.tv_sec % 1000) * 1000000 + now.tv_usec; 73 | return time; 74 | } 75 | 76 | static inline void pushCallRecord(Class cls, SEL sel) 77 | { 78 | if (mainThreadStack->index >= maxDepth) { 79 | ignoreCallNum++; 80 | return; 81 | } 82 | if (mainThreadStack) { 83 | uint64_t time = getVirtualCallTime(); 84 | if (++mainThreadStack->index >= mainThreadStack->allocLength) { 85 | mainThreadStack->allocLength += 128; 86 | mainThreadStack->stack = (MethodRecord *)realloc(mainThreadStack->stack, mainThreadStack->allocLength * sizeof(MethodRecord)); 87 | } 88 | MethodRecord *record = &mainThreadStack->stack[mainThreadStack->index]; 89 | record->cls = cls; 90 | record->sel = sel; 91 | record->time = time; 92 | } 93 | } 94 | 95 | static inline void popCallRecord(BOOL is_objc_msgSendSuper) 96 | { 97 | if (ignoreCallNum > 0) { 98 | ignoreCallNum--; 99 | return; 100 | } 101 | if (mainThreadStack && mainThreadStack->index >= 0) { 102 | //todo: stack空间缩小算法 103 | uint64_t time = getVirtualCallTime(); 104 | MethodRecord *record = &mainThreadStack->stack[mainThreadStack->index]; 105 | uint64_t costTime = time - record->time; 106 | int depth = mainThreadStack->index--; 107 | if (costTime >= costMinTime) { 108 | if (++mainThreadCallRecord->index >= mainThreadCallRecord->allocLength) { 109 | mainThreadCallRecord->allocLength += 128; 110 | mainThreadCallRecord->record = realloc(mainThreadCallRecord->record, mainThreadCallRecord->allocLength * sizeof(TPCallRecord)); 111 | } 112 | TPCallRecord* callRecord = &mainThreadCallRecord->record[mainThreadCallRecord->index]; 113 | callRecord->cls = record->cls; 114 | callRecord->depth = depth; 115 | callRecord->costTime = costTime; 116 | callRecord->sel = record->sel; 117 | callRecord->is_objc_msgSendSuper = is_objc_msgSendSuper; 118 | } 119 | } 120 | } 121 | 122 | static inline void setLRRegisterValue(uintptr_t lr) 123 | { 124 | LRStack *lrStack = pthread_getspecific(threadKeyLR); 125 | if (!lrStack) { 126 | lrStack = (LRStack *)malloc(sizeof(LRStack)); 127 | lrStack->allocLength = 128; 128 | lrStack->lr_stack = (uintptr_t *)malloc(lrStack->allocLength * sizeof(uintptr_t)); 129 | lrStack->index = -1; 130 | pthread_setspecific(threadKeyLR, lrStack); 131 | } 132 | if (++lrStack->index >= lrStack->allocLength) { 133 | lrStack->allocLength += 128; 134 | lrStack->lr_stack = (uintptr_t *)realloc(lrStack->lr_stack, lrStack->allocLength *sizeof(uintptr_t)); 135 | } 136 | lrStack->lr_stack[lrStack->index] = lr; 137 | } 138 | 139 | static inline uintptr_t getLRRegisterValue() 140 | { 141 | LRStack *lrStack = pthread_getspecific(threadKeyLR); 142 | uintptr_t lr = lrStack->lr_stack[lrStack->index--]; 143 | return lr; 144 | } 145 | 146 | void hook_objc_msgSend_before(id self, SEL sel, uintptr_t lr) 147 | { 148 | if (CallRecordEnable && pthread_main_np()) { 149 | pushCallRecord(object_getClass(self), sel); 150 | } 151 | 152 | setLRRegisterValue(lr); 153 | } 154 | 155 | uintptr_t hook_objc_msgSend_after(BOOL is_objc_msgSendSuper) 156 | { 157 | if (CallRecordEnable && pthread_main_np()) { 158 | popCallRecord(is_objc_msgSendSuper); 159 | } 160 | 161 | return getLRRegisterValue(); 162 | } 163 | 164 | void threadCleanLRStack(void *ptr) 165 | { 166 | if (ptr != NULL) { 167 | LRStack *lrStack = (LRStack *)ptr; 168 | if (lrStack->lr_stack) { 169 | free(lrStack->lr_stack); 170 | } 171 | free(lrStack); 172 | } 173 | } 174 | 175 | void initData(char *featureName) 176 | { 177 | if (!mainThreadCallRecord) { 178 | mainThreadCallRecord = (TPMainThreadCallRecord *)malloc(sizeof(TPMainThreadCallRecord)); 179 | mainThreadCallRecord->allocLength = 128; 180 | mainThreadCallRecord->record = (TPCallRecord *)malloc(mainThreadCallRecord->allocLength * sizeof(TPCallRecord)); 181 | mainThreadCallRecord->index = -1; 182 | mainThreadCallRecord->featureName = featureName; 183 | } 184 | 185 | if (!mainThreadStack) { 186 | mainThreadStack = (MainThreadMethodStack *)malloc(sizeof(MainThreadMethodStack)); 187 | mainThreadStack->allocLength = 128; 188 | mainThreadStack->stack = (MethodRecord *)malloc(mainThreadStack->allocLength * sizeof(MethodRecord)); 189 | mainThreadStack->index = -1; 190 | } 191 | } 192 | 193 | extern void hook_msgSend(void); 194 | extern void hook_msgSendSuper2(void); 195 | 196 | void startTrace(char *featureName) { 197 | initData(featureName); 198 | CallRecordEnable = YES; 199 | static dispatch_once_t onceToken; 200 | dispatch_once(&onceToken, ^{ 201 | pthread_key_create(&threadKeyLR, threadCleanLRStack); 202 | struct rebinding rebindObjc_msgSend; 203 | rebindObjc_msgSend.name = "objc_msgSend"; 204 | rebindObjc_msgSend.replacement = hook_msgSend; 205 | rebindObjc_msgSend.replaced = (void *)&orgin_objc_msgSend; 206 | struct rebinding rebindObjc_msgSendSuper2; 207 | rebindObjc_msgSendSuper2.name = "objc_msgSendSuper2"; 208 | rebindObjc_msgSendSuper2.replacement = hook_msgSendSuper2; 209 | rebindObjc_msgSendSuper2.replaced = (void *)&orgin_objc_msgSendSuper2; 210 | struct rebinding rebs[2] = {rebindObjc_msgSend, rebindObjc_msgSendSuper2}; 211 | rebind_symbols(rebs, 2); 212 | }); 213 | }; 214 | 215 | void stopTrace() { 216 | CallRecordEnable = NO; 217 | if (mainThreadStack) { 218 | free(mainThreadStack->stack); 219 | free(mainThreadStack); 220 | mainThreadStack = NULL; 221 | ignoreCallNum = 0; 222 | } 223 | }; 224 | 225 | TPMainThreadCallRecord *getMainThreadCallRecord(void) 226 | { 227 | TPMainThreadCallRecord *retValue = mainThreadCallRecord; 228 | mainThreadCallRecord = NULL; 229 | return retValue; 230 | } 231 | 232 | void setMaxDepth(int depth) 233 | { 234 | maxDepth = depth; 235 | } 236 | 237 | void setCostMinTime(uint64_t time) 238 | { 239 | costMinTime = time; 240 | } 241 | 242 | 243 | #endif 244 | 245 | -------------------------------------------------------------------------------- /TimeProfiler/Core/TPCallTrace.h: -------------------------------------------------------------------------------- 1 | // 2 | // TPCallTrace.h 3 | // VideoIphone 4 | // 5 | // Created by 吴凯凯 on 2019/6/13. 6 | // Copyright © 2019 吴凯凯. All rights reserved. 7 | // 8 | 9 | #ifndef TPCallTrace_h 10 | #define TPCallTrace_h 11 | 12 | #include 13 | #include 14 | 15 | 16 | typedef struct { 17 | Class cls; 18 | SEL sel; 19 | uint64_t costTime; //单位:纳秒(百万分之一秒) 20 | int depth; 21 | BOOL is_objc_msgSendSuper; 22 | } TPCallRecord; 23 | 24 | typedef struct { 25 | TPCallRecord *record; 26 | int allocLength; 27 | int index; 28 | char *featureName; 29 | } TPMainThreadCallRecord; 30 | 31 | void startTrace(char *featureName); 32 | void stopTrace(void); 33 | TPMainThreadCallRecord *getMainThreadCallRecord(void); 34 | void setMaxDepth(int depth); 35 | void setCostMinTime(uint64_t time); 36 | 37 | #endif /* TPCallTrace_h */ 38 | -------------------------------------------------------------------------------- /TimeProfiler/Core/TPMainVC.h: -------------------------------------------------------------------------------- 1 | // 2 | // TPMainVC.h 3 | // KKMagicHook 4 | // 5 | // Created by 吴凯凯 on 2020/4/2. 6 | // Copyright © 2020 吴凯凯. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface TPMainVC : UIViewController 14 | 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /TimeProfiler/Core/TPMainVC.m: -------------------------------------------------------------------------------- 1 | // 2 | // TPMainVC.m 3 | // KKMagicHook 4 | // 5 | // Created by 吴凯凯 on 2020/4/2. 6 | // Copyright © 2020 吴凯凯. All rights reserved. 7 | // 8 | 9 | #import "TPMainVC.h" 10 | #import "TimeProfiler.h" 11 | #import "TimeProfilerVC.h" 12 | #import "TPModel.h" 13 | 14 | @interface TPMainVC () 15 | 16 | @property (nonatomic, strong)UITableView *tableview; 17 | @property (nonatomic, strong)UIButton *closeBtn; 18 | @property (nonatomic, copy)NSArray *modelData; 19 | @property (nonatomic, strong)UILabel *titleLabel; 20 | 21 | @end 22 | 23 | @implementation TPMainVC 24 | 25 | - (void)viewDidLoad { 26 | [super viewDidLoad]; 27 | self.view.backgroundColor = [UIColor whiteColor]; 28 | [self.view addSubview:self.closeBtn]; 29 | [self.view addSubview:self.tableview]; 30 | [self.view addSubview:self.titleLabel]; 31 | // Do any additional setup after loading the view. 32 | [self reloadTableview]; 33 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reloadTableview) name:TPTimeProfilerProcessedDataNotification object:nil]; 34 | } 35 | 36 | - (void)reloadTableview 37 | { 38 | _modelData = [TimeProfiler shareInstance].modelArr; 39 | [self.tableview reloadData]; 40 | } 41 | 42 | - (void)dealloc 43 | { 44 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 45 | } 46 | 47 | #pragma mark - UITableViewDelegate 48 | 49 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath 50 | { 51 | if (indexPath.row < _modelData.count) { 52 | TPModel *model = _modelData[indexPath.row]; 53 | TimeProfilerVC *vc = [[TimeProfilerVC alloc] initWithModel:model]; 54 | vc.modalPresentationStyle = UIModalPresentationFullScreen; 55 | [self presentViewController:vc animated:YES completion:nil]; 56 | } 57 | } 58 | 59 | #pragma mark - UITableViewDataSource 60 | 61 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView 62 | { 63 | return 1; 64 | } 65 | 66 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 67 | { 68 | return _modelData.count; 69 | } 70 | 71 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 72 | { 73 | static NSString *cellid = @"TPMainTableViewCellID"; 74 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellid]; 75 | if (!cell) { 76 | cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellid]; 77 | cell.selectionStyle = UITableViewCellSelectionStyleNone; 78 | UIView *line = [[UIView alloc] initWithFrame:CGRectMake(0, 36, [UIScreen mainScreen].bounds.size.width, 1)]; 79 | line.backgroundColor = [UIColor grayColor]; 80 | [cell.contentView addSubview:line]; 81 | cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; 82 | } 83 | TPModel *model = _modelData[indexPath.row]; 84 | cell.textLabel.text = model.featureName; 85 | return cell; 86 | } 87 | 88 | - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section 89 | { 90 | return @"Thread: main-thread"; 91 | } 92 | 93 | #pragma mark - private 94 | 95 | - (void)clickCloseBtn:(UIButton *)btn 96 | { 97 | [self dismissViewControllerAnimated:YES completion:nil]; 98 | } 99 | 100 | - (UIButton *)getTPBtnWithFrame:(CGRect)rect title:(NSString *)title sel:(SEL)sel 101 | { 102 | UIButton *btn = [[UIButton alloc] initWithFrame:rect]; 103 | btn.layer.cornerRadius = 2; 104 | btn.layer.borderWidth = 1; 105 | btn.layer.borderColor = [UIColor blackColor].CGColor; 106 | [btn setTitle:title forState:UIControlStateNormal]; 107 | [btn setBackgroundImage:[self imageWithColor:[UIColor colorWithRed:127/255.0 green:179/255.0 blue:219/255.0 alpha:1]] forState:UIControlStateSelected]; 108 | btn.titleLabel.font = [UIFont systemFontOfSize:10]; 109 | [btn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; 110 | [btn addTarget:self action:sel forControlEvents:UIControlEventTouchUpInside]; 111 | return btn; 112 | } 113 | 114 | - (UIImage *)imageWithColor:(UIColor *)color{ 115 | CGRect rect = CGRectMake(0.0f, 0.0f, 1.0f, 1.0f); 116 | UIGraphicsBeginImageContext(rect.size); 117 | CGContextRef context = UIGraphicsGetCurrentContext(); 118 | CGContextSetFillColorWithColor(context, [color CGColor]); 119 | CGContextFillRect(context, rect); 120 | UIImage *theImage = UIGraphicsGetImageFromCurrentImageContext(); 121 | UIGraphicsEndImageContext(); 122 | return theImage; 123 | } 124 | 125 | #pragma mark - get&set method 126 | 127 | - (UITableView *)tableview 128 | { 129 | if (!_tableview) { 130 | _tableview = [[UITableView alloc] initWithFrame:CGRectMake(0, 100, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height-100) style:UITableViewStylePlain]; 131 | _tableview.bounces = NO; 132 | _tableview.dataSource = self; 133 | _tableview.delegate = self; 134 | _tableview.rowHeight = 38; 135 | _tableview.sectionHeaderHeight = 50; 136 | _tableview.separatorStyle = UITableViewCellSeparatorStyleNone; 137 | } 138 | return _tableview; 139 | } 140 | 141 | - (UIButton *)closeBtn 142 | { 143 | if (!_closeBtn) { 144 | _closeBtn = [self getTPBtnWithFrame:CGRectMake([UIScreen mainScreen].bounds.size.width-50, 65, 40, 30) title:@"关闭" sel:@selector(clickCloseBtn:)]; 145 | } 146 | return _closeBtn; 147 | } 148 | 149 | - (UILabel *)titleLabel 150 | { 151 | if (!_titleLabel) { 152 | _titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(60, 55, [UIScreen mainScreen].bounds.size.width-120, 50)]; 153 | _titleLabel.textAlignment = NSTextAlignmentCenter; 154 | _titleLabel.font = [UIFont boldSystemFontOfSize:25]; 155 | _titleLabel.text = @"TimeProfiler"; 156 | } 157 | return _titleLabel; 158 | } 159 | 160 | @end 161 | -------------------------------------------------------------------------------- /TimeProfiler/Core/TPModel.h: -------------------------------------------------------------------------------- 1 | // 2 | // TPModel.h 3 | // KKMagicHook 4 | // 5 | // Created by 吴凯凯 on 2020/4/2. 6 | // Copyright © 2020 吴凯凯. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface TPModel : NSObject 14 | 15 | @property (nonatomic, copy)NSString *featureName; 16 | @property (nonatomic, copy)NSArray *sequentialMethodRecord; 17 | @property (nonatomic, copy)NSArray *costTimeSortMethodRecord; 18 | @property (nonatomic, copy)NSArray *callCountSortMethodRecord; 19 | 20 | @end 21 | 22 | NS_ASSUME_NONNULL_END 23 | -------------------------------------------------------------------------------- /TimeProfiler/Core/TPModel.m: -------------------------------------------------------------------------------- 1 | // 2 | // TPModel.m 3 | // KKMagicHook 4 | // 5 | // Created by 吴凯凯 on 2020/4/2. 6 | // Copyright © 2020 吴凯凯. All rights reserved. 7 | // 8 | 9 | #import "TPModel.h" 10 | 11 | @implementation TPModel 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /TimeProfiler/Core/TPRecordCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // TPRecordCell.h 3 | // VideoIphone 4 | // 5 | // Created by 吴凯凯 on 2019/6/24. 6 | // Copyright © 2019 吴凯凯. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @class TPRecordModel; 14 | @class TPRecordCell; 15 | 16 | @protocol TPRecordCellDelegate 17 | 18 | - (void)recordCell:(TPRecordCell *)cell clickExpandWithSection:(NSInteger)section; 19 | 20 | @end 21 | 22 | @interface TPRecordCell : UITableViewCell 23 | 24 | @property (nonatomic, weak)id delegate; 25 | 26 | - (void)bindRecordModel:(TPRecordModel *)model isHiddenExpandBtn:(BOOL)isHidden isExpand:(BOOL)isExpand section:(NSInteger)section isCallCountType:(BOOL)isCallCountType; 27 | 28 | @end 29 | 30 | NS_ASSUME_NONNULL_END 31 | -------------------------------------------------------------------------------- /TimeProfiler/Core/TPRecordCell.m: -------------------------------------------------------------------------------- 1 | // 2 | // TPRecordCell.m 3 | // VideoIphone 4 | // 5 | // Created by 吴凯凯 on 2019/6/24. 6 | // Copyright © 2019 吴凯凯. All rights reserved. 7 | // 8 | 9 | #import "TPRecordCell.h" 10 | #import "TPRecordModel.h" 11 | 12 | #define kDepthLabelWidth 30 13 | #define kTimeLabelWidth 70 14 | #define kMethodLabelWidth 500 15 | 16 | @interface TPRecordCell() 17 | { 18 | NSInteger _section; 19 | } 20 | 21 | @property (nonatomic, strong)UILabel *depthLabel; 22 | @property (nonatomic, strong)UILabel *timeLabel; 23 | @property (nonatomic, strong)UILabel *methodLabel; 24 | @property (nonatomic, strong)UIButton *expandBtn; 25 | @property (nonatomic, strong)UILabel *callCountLabel; 26 | 27 | @end 28 | 29 | @implementation TPRecordCell 30 | 31 | - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier 32 | { 33 | self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; 34 | if (self) { 35 | self.selectionStyle = UITableViewCellSelectionStyleNone; 36 | [self.contentView addSubview:self.depthLabel]; 37 | [self.contentView addSubview:[self LineView:CGRectMake(kDepthLabelWidth+2, 0, 2, 18)]]; 38 | [self.contentView addSubview:self.timeLabel]; 39 | [self.contentView addSubview:[self LineView:CGRectMake(CGRectGetMaxX(self.timeLabel.frame)+2, 0, 2, 18)]]; 40 | [self.contentView addSubview:self.expandBtn]; 41 | [self.contentView addSubview:self.callCountLabel]; 42 | [self.contentView addSubview:[self LineView:CGRectMake(CGRectGetMaxX(self.callCountLabel.frame)+2, 0, 2, 18)]]; 43 | [self.contentView addSubview:self.methodLabel]; 44 | } 45 | return self; 46 | } 47 | 48 | - (UIView *)LineView:(CGRect)rect 49 | { 50 | UIView *line = [[UIView alloc] initWithFrame:rect]; 51 | line.backgroundColor = [UIColor grayColor]; 52 | return line; 53 | } 54 | 55 | - (void)bindRecordModel:(TPRecordModel *)model isHiddenExpandBtn:(BOOL)isHidden isExpand:(BOOL)isExpand section:(NSInteger)section isCallCountType:(BOOL)isCallCountType 56 | { 57 | _section = section; 58 | self.expandBtn.hidden = isHidden; 59 | if (!self.expandBtn.hidden) { 60 | self.expandBtn.selected = isExpand; 61 | } 62 | self.depthLabel.text = [NSString stringWithFormat:@"%d", model.depth]; 63 | self.timeLabel.text = [NSString stringWithFormat:@"%lgms", model.costTime/1000.0]; 64 | self.callCountLabel.hidden = !isCallCountType; 65 | if (isCallCountType) { 66 | self.callCountLabel.text = [NSString stringWithFormat:@"%d", model.callCount]; 67 | } 68 | 69 | NSMutableString *methodStr = [NSMutableString string]; 70 | if (model.depth>0 && !isCallCountType) { 71 | [methodStr appendString:[[NSString string] stringByPaddingToLength:model.depth withString:@"  " startingAtIndex:0]]; 72 | } 73 | if (class_isMetaClass(model.cls)) { 74 | [methodStr appendString:@"+"]; 75 | } 76 | else 77 | { 78 | [methodStr appendString:@"-"]; 79 | } 80 | if (model.is_objc_msgSendSuper) { 81 | [methodStr appendString:[NSString stringWithFormat:@"[(super)%@ %@]", NSStringFromClass(model.cls), NSStringFromSelector(model.sel)]]; 82 | } 83 | else 84 | { 85 | [methodStr appendString:[NSString stringWithFormat:@"[%@ %@]", NSStringFromClass(model.cls), NSStringFromSelector(model.sel)]]; 86 | } 87 | 88 | self.methodLabel.text = methodStr; 89 | } 90 | 91 | - (void)clickExpandBtn:(UIButton *)btn 92 | { 93 | if ([self.delegate respondsToSelector:@selector(recordCell:clickExpandWithSection:)]) { 94 | [self.delegate recordCell:self clickExpandWithSection:_section]; 95 | } 96 | } 97 | 98 | #pragma mark - get method 99 | 100 | - (UILabel *)depthLabel 101 | { 102 | if (!_depthLabel) { 103 | _depthLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, kDepthLabelWidth, 18)]; 104 | _depthLabel.textAlignment = NSTextAlignmentCenter; 105 | _depthLabel.font = [UIFont systemFontOfSize:12]; 106 | } 107 | return _depthLabel; 108 | } 109 | 110 | - (UILabel *)timeLabel 111 | { 112 | if (!_timeLabel) { 113 | 114 | _timeLabel = [[UILabel alloc] initWithFrame:CGRectMake(kDepthLabelWidth+6, 0, kTimeLabelWidth, 18)]; 115 | _timeLabel.textAlignment = NSTextAlignmentRight; 116 | _timeLabel.font = [UIFont systemFontOfSize:12]; 117 | } 118 | return _timeLabel; 119 | } 120 | 121 | - (UILabel *)methodLabel 122 | { 123 | if (!_methodLabel) { 124 | _methodLabel = [[UILabel alloc] initWithFrame:CGRectMake(kDepthLabelWidth+kTimeLabelWidth+18+26, 0, kMethodLabelWidth, 18)]; 125 | _methodLabel.font = [UIFont systemFontOfSize:12]; 126 | } 127 | return _methodLabel; 128 | } 129 | 130 | - (UIButton *)expandBtn 131 | { 132 | if (!_expandBtn) { 133 | _expandBtn = [[UIButton alloc] initWithFrame:CGRectMake(kDepthLabelWidth+kTimeLabelWidth+16, 0, 18, 18)]; 134 | [_expandBtn setBackgroundImage:[UIImage imageNamed:@"TPNOExpandIcon"] forState:UIControlStateNormal]; 135 | [_expandBtn setBackgroundImage:[UIImage imageNamed:@"TPExpandIcon"] forState:UIControlStateSelected]; 136 | [_expandBtn addTarget:self action:@selector(clickExpandBtn:) forControlEvents:UIControlEventTouchUpInside]; 137 | _expandBtn.hidden = YES; 138 | } 139 | return _expandBtn; 140 | } 141 | 142 | - (UILabel *)callCountLabel 143 | { 144 | if (!_callCountLabel) { 145 | _callCountLabel = [[UILabel alloc] initWithFrame:CGRectMake(kDepthLabelWidth+kTimeLabelWidth+12, 0, 26, 18)]; 146 | _callCountLabel.textAlignment = NSTextAlignmentCenter; 147 | _callCountLabel.font = [UIFont systemFontOfSize:12]; 148 | _callCountLabel.hidden = YES; 149 | } 150 | return _callCountLabel; 151 | } 152 | 153 | @end 154 | -------------------------------------------------------------------------------- /TimeProfiler/Core/TPRecordHierarchyModel.h: -------------------------------------------------------------------------------- 1 | // 2 | // TPRecordHierarchyModel.h 3 | // VideoIphone 4 | // 5 | // Created by 吴凯凯 on 2019/6/29. 6 | // Copyright © 2019 吴凯凯. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "TPRecordModel.h" 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @interface TPRecordHierarchyModel : NSObject 15 | 16 | @property (nonatomic, strong)TPRecordModel *rootMethod; 17 | @property (nonatomic, copy)NSArray *subMethods; 18 | @property (nonatomic, assign)BOOL isExpand; //是否展开所有的子函数 19 | 20 | - (instancetype)initWithRecordModelArr:(NSArray *)recordModelArr; 21 | - (TPRecordModel *)getRecordModel:(NSInteger)index; 22 | 23 | @end 24 | 25 | NS_ASSUME_NONNULL_END 26 | -------------------------------------------------------------------------------- /TimeProfiler/Core/TPRecordHierarchyModel.m: -------------------------------------------------------------------------------- 1 | // 2 | // TPRecordHierarchyModel.m 3 | // VideoIphone 4 | // 5 | // Created by 吴凯凯 on 2019/6/29. 6 | // Copyright © 2019 吴凯凯. All rights reserved. 7 | // 8 | 9 | #import "TPRecordHierarchyModel.h" 10 | 11 | @implementation TPRecordHierarchyModel 12 | 13 | - (instancetype)initWithRecordModelArr:(NSArray *)recordModelArr 14 | { 15 | self = [super init]; 16 | if (self) { 17 | if ([recordModelArr isKindOfClass:NSArray.class] && recordModelArr.count > 0) { 18 | self.rootMethod = recordModelArr[0]; 19 | self.isExpand = YES; 20 | if (recordModelArr.count > 1) { 21 | self.subMethods = [recordModelArr subarrayWithRange:NSMakeRange(1, recordModelArr.count-1)]; 22 | } 23 | } 24 | } 25 | return self; 26 | } 27 | 28 | - (TPRecordModel *)getRecordModel:(NSInteger)index 29 | { 30 | if (index==0) { 31 | return self.rootMethod; 32 | } 33 | return self.subMethods[index-1]; 34 | } 35 | 36 | - (id)copyWithZone:(NSZone *)zone 37 | { 38 | TPRecordHierarchyModel *model = [[[self class] allocWithZone:zone] init]; 39 | model.rootMethod = self.rootMethod; 40 | model.subMethods = self.subMethods; 41 | model.isExpand = self.isExpand; 42 | return model; 43 | } 44 | 45 | @end 46 | -------------------------------------------------------------------------------- /TimeProfiler/Core/TPRecordModel.h: -------------------------------------------------------------------------------- 1 | // 2 | // TPRecordModel.h 3 | // VideoIphone 4 | // 5 | // Created by 吴凯凯 on 2019/6/24. 6 | // Copyright © 2019 吴凯凯. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @interface TPRecordModel : NSObject 15 | 16 | @property (nonatomic, strong)Class cls; 17 | @property (nonatomic)SEL sel; 18 | @property (nonatomic, assign)BOOL is_objc_msgSendSuper; 19 | @property (nonatomic, assign)uint64_t costTime; //单位:纳秒(百万分之一秒) 20 | @property (nonatomic, assign)int depth; 21 | 22 | // 辅助堆栈排序 23 | @property (nonatomic, assign)int total; 24 | @property (nonatomic)BOOL isUsed; 25 | 26 | //call 次数 27 | @property (nonatomic, assign)int callCount; 28 | 29 | - (instancetype)initWithCls:(Class)cls sel:(SEL)sel time:(uint64_t)costTime depth:(int)depth total:(int)total is_objc_msgSendSuper:(BOOL)is_objc_msgSendSuper; 30 | 31 | - (BOOL)isEqualRecordModel:(TPRecordModel *)model; 32 | 33 | @end 34 | 35 | NS_ASSUME_NONNULL_END 36 | -------------------------------------------------------------------------------- /TimeProfiler/Core/TPRecordModel.m: -------------------------------------------------------------------------------- 1 | // 2 | // TPRecordModel.m 3 | // VideoIphone 4 | // 5 | // Created by 吴凯凯 on 2019/6/24. 6 | // Copyright © 2019 吴凯凯. All rights reserved. 7 | // 8 | 9 | #import "TPRecordModel.h" 10 | 11 | @implementation TPRecordModel 12 | 13 | - (instancetype)initWithCls:(Class)cls sel:(SEL)sel time:(uint64_t)costTime depth:(int)depth total:(int)total is_objc_msgSendSuper:(BOOL)is_objc_msgSendSuper 14 | { 15 | self = [super init]; 16 | if (self) { 17 | self.callCount = 0; 18 | self.cls = cls; 19 | self.sel = sel; 20 | self.costTime = costTime; 21 | self.depth = depth; 22 | self.total = total; 23 | self.isUsed = NO; 24 | self.is_objc_msgSendSuper = is_objc_msgSendSuper; 25 | } 26 | return self; 27 | } 28 | 29 | - (id)copyWithZone:(NSZone *)zone 30 | { 31 | TPRecordModel *model = [[[self class] allocWithZone:zone] init]; 32 | model.cls = self.cls; 33 | model.sel = self.sel; 34 | model.costTime = self.costTime; 35 | model.depth = self.depth; 36 | model.total = self.total; 37 | model.isUsed = self.isUsed; 38 | model.callCount = self.callCount; 39 | model.is_objc_msgSendSuper = self.is_objc_msgSendSuper; 40 | return model; 41 | } 42 | 43 | - (BOOL)isEqualRecordModel:(TPRecordModel *)model 44 | { 45 | if ([self.cls isEqual:model.cls] && self.sel==model.sel) { 46 | return YES; 47 | } 48 | return NO; 49 | } 50 | 51 | @end 52 | -------------------------------------------------------------------------------- /TimeProfiler/Core/TimeProfilerVC.h: -------------------------------------------------------------------------------- 1 | // 2 | // TimeProfilerVC.h 3 | // VideoIphone 4 | // 5 | // Created by 吴凯凯 on 2019/6/13. 6 | // Copyright © 2019 吴凯凯. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @class TPModel; 14 | 15 | @interface TimeProfilerVC : UIViewController 16 | 17 | - (instancetype)initWithModel:(TPModel *)model; 18 | 19 | @end 20 | 21 | NS_ASSUME_NONNULL_END 22 | -------------------------------------------------------------------------------- /TimeProfiler/Core/TimeProfilerVC.m: -------------------------------------------------------------------------------- 1 | // 2 | // TimeProfilerVC.m 3 | // VideoIphone 4 | // 5 | // Created by 吴凯凯 on 2019/6/13. 6 | // Copyright © 2019 吴凯凯. All rights reserved. 7 | // 8 | 9 | #import "TimeProfilerVC.h" 10 | #import "TPCallTrace.h" 11 | #import "TPRecordCell.h" 12 | #import "TPRecordModel.h" 13 | #import "TPRecordHierarchyModel.h" 14 | #import "TimeProfiler.h" 15 | #import "TPModel.h" 16 | #import 17 | 18 | typedef NS_ENUM(NSInteger, TPTableType) { 19 | tableTypeSequential, 20 | tableTypecostTime, 21 | tableTypeCallCount, 22 | }; 23 | 24 | static CGFloat TPScrollWidth = 600; 25 | static CGFloat TPHeaderHight = 140; 26 | 27 | #define IS_SHOW_DEBUG_INFO_IN_CONSOLE 0 28 | 29 | @interface TimeProfilerVC () 30 | 31 | @property (nonatomic, strong)UIButton *RecordBtn; 32 | @property (nonatomic, strong)UIButton *costTimeSortBtn; 33 | @property (nonatomic, strong)UIButton *callCountSortBtn; 34 | @property (nonatomic, strong)UIButton *popVCBtn; 35 | @property (nonatomic, strong)UILabel *titleLabel; 36 | @property (nonatomic, strong)UITableView *tpTableView; 37 | @property (nonatomic, strong)UILabel *tableHeaderViewLabel; 38 | @property (nonatomic, strong)UIScrollView *tpScrollView; 39 | @property (nonatomic, copy)NSArray *sequentialMethodRecord; 40 | @property (nonatomic, copy)NSArray *costTimeSortMethodRecord; 41 | @property (nonatomic, copy)NSArray *callCountSortMethodRecord; 42 | @property (nonatomic, assign)TPTableType tpTableType; 43 | 44 | @end 45 | 46 | @implementation TimeProfilerVC 47 | 48 | - (instancetype)initWithModel:(TPModel *)model 49 | { 50 | self = [super init]; 51 | if (self) { 52 | self.sequentialMethodRecord = model.sequentialMethodRecord; 53 | self.costTimeSortMethodRecord = model.costTimeSortMethodRecord; 54 | self.callCountSortMethodRecord = model.callCountSortMethodRecord; 55 | self.titleLabel.text = model.featureName; 56 | } 57 | return self; 58 | } 59 | 60 | - (void)viewDidLoad { 61 | [super viewDidLoad]; 62 | self.view.backgroundColor = [UIColor whiteColor]; 63 | [self.view addSubview:self.titleLabel]; 64 | [self.view addSubview:self.RecordBtn]; 65 | [self.view addSubview:self.costTimeSortBtn]; 66 | [self.view addSubview:self.callCountSortBtn]; 67 | [self.view addSubview:self.popVCBtn]; 68 | [self.view addSubview:self.tpScrollView]; 69 | [self.tpScrollView addSubview:self.tableHeaderViewLabel]; 70 | [self.tpScrollView addSubview:self.tpTableView]; 71 | // Do any additional setup after loading the view. 72 | [self clickRecordBtn]; 73 | } 74 | 75 | - (void)clickPopVCBtn:(UIButton *)btn 76 | { 77 | [self dismissViewControllerAnimated:YES completion:nil]; 78 | } 79 | 80 | #pragma mark - TPRecordCellDelegate 81 | 82 | - (void)recordCell:(TPRecordCell *)cell clickExpandWithSection:(NSInteger)section 83 | { 84 | NSIndexSet *indexSet; 85 | TPRecordHierarchyModel *model; 86 | switch (self.tpTableType) { 87 | case tableTypeSequential: 88 | model = self.sequentialMethodRecord[section]; 89 | break; 90 | case tableTypecostTime: 91 | model = self.costTimeSortMethodRecord[section]; 92 | break; 93 | 94 | default: 95 | break; 96 | } 97 | model.isExpand = !model.isExpand; 98 | indexSet=[[NSIndexSet alloc] initWithIndex:section]; 99 | [self.tpTableView reloadSections:indexSet withRowAnimation:UITableViewRowAnimationAutomatic]; 100 | } 101 | 102 | #pragma mark - UITableViewDataSource 103 | 104 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView 105 | { 106 | if (self.tpTableType == tableTypeSequential) { 107 | return self.sequentialMethodRecord.count; 108 | } 109 | else if (self.tpTableType == tableTypecostTime) 110 | { 111 | return self.costTimeSortMethodRecord.count; 112 | } 113 | return 1; 114 | } 115 | 116 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 117 | { 118 | if (self.tpTableType == tableTypeSequential) { 119 | TPRecordHierarchyModel *model = self.sequentialMethodRecord[section]; 120 | if (model.isExpand && [model.subMethods isKindOfClass:NSArray.class]) { 121 | return model.subMethods.count+1; 122 | } 123 | } 124 | else if (self.tpTableType == tableTypecostTime) 125 | { 126 | TPRecordHierarchyModel *model = self.costTimeSortMethodRecord[section]; 127 | if (model.isExpand && [model.subMethods isKindOfClass:NSArray.class]) { 128 | return model.subMethods.count+1; 129 | } 130 | } 131 | else 132 | { 133 | return self.callCountSortMethodRecord.count; 134 | } 135 | return 1; 136 | } 137 | 138 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 139 | { 140 | static NSString *TPRecordCell_reuseIdentifier = @"TPRecordCell_reuseIdentifier"; 141 | TPRecordCell *cell = [tableView dequeueReusableCellWithIdentifier:TPRecordCell_reuseIdentifier]; 142 | if (!cell) { 143 | cell = [[TPRecordCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:TPRecordCell_reuseIdentifier]; 144 | } 145 | TPRecordHierarchyModel *model; 146 | TPRecordModel *recordModel; 147 | BOOL isShowExpandBtn; 148 | switch (self.tpTableType) { 149 | case tableTypeSequential: 150 | model = self.sequentialMethodRecord[indexPath.section]; 151 | recordModel = [model getRecordModel:indexPath.row]; 152 | isShowExpandBtn = indexPath.row == 0 && [model.subMethods isKindOfClass:NSArray.class] && model.subMethods.count > 0; 153 | cell.delegate = self; 154 | [cell bindRecordModel:recordModel isHiddenExpandBtn:!isShowExpandBtn isExpand:model.isExpand section:indexPath.section isCallCountType:NO]; 155 | break; 156 | case tableTypecostTime: 157 | model = self.costTimeSortMethodRecord[indexPath.section]; 158 | recordModel = [model getRecordModel:indexPath.row]; 159 | isShowExpandBtn = indexPath.row == 0 && [model.subMethods isKindOfClass:NSArray.class] && model.subMethods.count > 0; 160 | cell.delegate = self; 161 | [cell bindRecordModel:recordModel isHiddenExpandBtn:!isShowExpandBtn isExpand:model.isExpand section:indexPath.section isCallCountType:NO]; 162 | break; 163 | case tableTypeCallCount: 164 | recordModel = self.callCountSortMethodRecord[indexPath.row]; 165 | [cell bindRecordModel:recordModel isHiddenExpandBtn:YES isExpand:YES section:indexPath.section isCallCountType:YES]; 166 | break; 167 | 168 | default: 169 | break; 170 | } 171 | return cell; 172 | } 173 | 174 | #pragma mark - Btn click method 175 | 176 | - (void)clickRecordBtn 177 | { 178 | self.costTimeSortBtn.selected = NO; 179 | self.callCountSortBtn.selected = NO; 180 | if (!self.RecordBtn.selected) { 181 | self.RecordBtn.selected = YES; 182 | self.tpTableType = tableTypeSequential; 183 | [self.tpTableView reloadData]; 184 | } 185 | } 186 | 187 | - (void)clickCostTimeSortBtn 188 | { 189 | self.RecordBtn.selected = NO; 190 | self.callCountSortBtn.selected = NO; 191 | if (!self.costTimeSortBtn.selected) { 192 | self.costTimeSortBtn.selected = YES; 193 | self.tpTableType = tableTypecostTime; 194 | [self.tpTableView reloadData]; 195 | } 196 | } 197 | 198 | - (void)clickCallCountSortBtn 199 | { 200 | self.costTimeSortBtn.selected = NO; 201 | self.RecordBtn.selected = NO; 202 | if (!self.callCountSortBtn.selected) { 203 | self.callCountSortBtn.selected = YES; 204 | self.tpTableType = tableTypeCallCount; 205 | [self.tpTableView reloadData]; 206 | } 207 | } 208 | 209 | 210 | #pragma mark - get&set method 211 | 212 | - (UIScrollView *)tpScrollView 213 | { 214 | if (!_tpScrollView) { 215 | _tpScrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, TPHeaderHight, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height-TPHeaderHight)]; 216 | _tpScrollView.showsHorizontalScrollIndicator = YES; 217 | _tpScrollView.alwaysBounceHorizontal = YES; 218 | _tpScrollView.contentSize = CGSizeMake(TPScrollWidth, 0); 219 | } 220 | return _tpScrollView; 221 | } 222 | 223 | - (UITableView *)tpTableView 224 | { 225 | if (!_tpTableView) { 226 | _tpTableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 30, TPScrollWidth, [UIScreen mainScreen].bounds.size.height-TPHeaderHight-30) style:UITableViewStylePlain]; 227 | _tpTableView.bounces = NO; 228 | _tpTableView.dataSource = self; 229 | _tpTableView.rowHeight = 18; 230 | _tpTableView.separatorStyle = UITableViewCellSeparatorStyleNone; 231 | } 232 | return _tpTableView; 233 | } 234 | 235 | - (UIButton *)getTPBtnWithFrame:(CGRect)rect title:(NSString *)title sel:(SEL)sel 236 | { 237 | UIButton *btn = [[UIButton alloc] initWithFrame:rect]; 238 | btn.layer.cornerRadius = 2; 239 | btn.layer.borderWidth = 1; 240 | btn.layer.borderColor = [UIColor blackColor].CGColor; 241 | [btn setTitle:title forState:UIControlStateNormal]; 242 | [btn setBackgroundImage:[self imageWithColor:[UIColor colorWithRed:127/255.0 green:179/255.0 blue:219/255.0 alpha:1]] forState:UIControlStateSelected]; 243 | btn.titleLabel.font = [UIFont systemFontOfSize:10]; 244 | [btn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; 245 | [btn addTarget:self action:sel forControlEvents:UIControlEventTouchUpInside]; 246 | return btn; 247 | } 248 | 249 | - (UIImage *)imageWithColor:(UIColor *)color{ 250 | CGRect rect = CGRectMake(0.0f, 0.0f, 1.0f, 1.0f); 251 | UIGraphicsBeginImageContext(rect.size); 252 | CGContextRef context = UIGraphicsGetCurrentContext(); 253 | CGContextSetFillColorWithColor(context, [color CGColor]); 254 | CGContextFillRect(context, rect); 255 | UIImage *theImage = UIGraphicsGetImageFromCurrentImageContext(); 256 | UIGraphicsEndImageContext(); 257 | return theImage; 258 | } 259 | 260 | - (UIButton *)RecordBtn 261 | { 262 | if (!_RecordBtn) { 263 | _RecordBtn = [self getTPBtnWithFrame:CGRectMake(5, 105, 60, 30) title:@"调用时间" sel:@selector(clickRecordBtn)]; 264 | } 265 | return _RecordBtn; 266 | } 267 | 268 | - (UIButton *)costTimeSortBtn 269 | { 270 | if (!_costTimeSortBtn) { 271 | _costTimeSortBtn = [self getTPBtnWithFrame:CGRectMake(70, 105, 60, 30) title:@"最耗时" sel:@selector(clickCostTimeSortBtn)]; 272 | } 273 | return _costTimeSortBtn; 274 | } 275 | 276 | - (UIButton *)callCountSortBtn 277 | { 278 | if (!_callCountSortBtn) { 279 | _callCountSortBtn = [self getTPBtnWithFrame:CGRectMake(135, 105, 60, 30) title:@"调用次数" sel:@selector(clickCallCountSortBtn)]; 280 | } 281 | return _callCountSortBtn; 282 | } 283 | 284 | - (UIButton *)popVCBtn 285 | { 286 | if (!_popVCBtn) { 287 | _popVCBtn = [self getTPBtnWithFrame:CGRectMake([UIScreen mainScreen].bounds.size.width-50, 105, 40, 30) title:@"返回" sel:@selector(clickPopVCBtn:)]; 288 | } 289 | return _popVCBtn; 290 | } 291 | 292 | - (UILabel *)titleLabel 293 | { 294 | if (!_titleLabel) { 295 | _titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 50, [UIScreen mainScreen].bounds.size.width, 50)]; 296 | _titleLabel.textAlignment = NSTextAlignmentCenter; 297 | _titleLabel.font = [UIFont boldSystemFontOfSize:25]; 298 | } 299 | return _titleLabel; 300 | } 301 | 302 | - (UILabel *)tableHeaderViewLabel 303 | { 304 | if (!_tableHeaderViewLabel) { 305 | _tableHeaderViewLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, TPScrollWidth, 30)]; 306 | _tableHeaderViewLabel.font = [UIFont systemFontOfSize:15]; 307 | _tableHeaderViewLabel.backgroundColor = [UIColor colorWithRed:219.0/255 green:219.0/255 blue:219.0/255 alpha:1]; 308 | } 309 | return _tableHeaderViewLabel; 310 | } 311 | 312 | - (void)setTpTableType:(TPTableType)tpTableType 313 | { 314 | if (_tpTableType!=tpTableType) { 315 | if (tpTableType==tableTypeCallCount) { 316 | self.tableHeaderViewLabel.text = @"深度 耗时 次数 方法名"; 317 | } 318 | else 319 | { 320 | self.tableHeaderViewLabel.text = @"深度 耗时 方法名"; 321 | } 322 | _tpTableType = tpTableType; 323 | } 324 | } 325 | 326 | @end 327 | -------------------------------------------------------------------------------- /TimeProfiler/Core/UIWindow+CallRecordShake.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIWindow+CallRecordShake.h 3 | // VideoIphone 4 | // 5 | // Created by 吴凯凯 on 2019/6/18. 6 | // Copyright © 2019 吴凯凯. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface UIWindow (CallRecordShake) 14 | 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /TimeProfiler/Core/UIWindow+CallRecordShake.m: -------------------------------------------------------------------------------- 1 | // 2 | // UIWindow+CallRecordShake.m 3 | // VideoIphone 4 | // 5 | // Created by 吴凯凯 on 2019/6/18. 6 | // Copyright © 2019 吴凯凯. All rights reserved. 7 | // 8 | 9 | #import "UIWindow+CallRecordShake.h" 10 | #import "TPMainVC.h" 11 | 12 | @implementation UIWindow (CallRecordShake) 13 | 14 | - (BOOL)canBecomeFirstResponder { 15 | return YES; 16 | } 17 | 18 | 19 | - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event 20 | { 21 | } 22 | 23 | - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event 24 | { 25 | TPMainVC *vc = [[TPMainVC alloc] init]; 26 | vc.modalPresentationStyle = UIModalPresentationFullScreen; 27 | [self.rootViewController presentViewController:vc animated:YES completion:nil]; 28 | } 29 | 30 | - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event 31 | { 32 | } 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /TimeProfiler/Core/hookObjcMsgSend-arm64.s: -------------------------------------------------------------------------------- 1 | // 2 | // hookObjcMsgSend-arm64.s 3 | // staticHook 4 | // 5 | // Created by 吴凯凯 on 2020/3/19. 6 | // Copyright © 2020 吴凯凯. All rights reserved. 7 | // 8 | 9 | #ifdef __arm64__ 10 | #include 11 | 12 | 13 | .macro ENTRY /* name */ 14 | .text 15 | .align 5 16 | .private_extern $0 17 | $0: 18 | .endmacro 19 | 20 | .macro END_ENTRY /* name */ 21 | LExit$0: 22 | .endmacro 23 | 24 | //由于显示调用堆栈(复制栈帧)有一定性能消耗,可自行评估。1表示显示调用堆栈;0表示不显示调用堆栈 25 | #define SUPPORT_SHOW_CALL_STACK 1 26 | 27 | .macro BACKUP_REGISTERS 28 | stp q6, q7, [sp, #-0x20]! 29 | stp q4, q5, [sp, #-0x20]! 30 | stp q2, q3, [sp, #-0x20]! 31 | stp q0, q1, [sp, #-0x20]! 32 | stp x6, x7, [sp, #-0x10]! 33 | stp x4, x5, [sp, #-0x10]! 34 | stp x2, x3, [sp, #-0x10]! 35 | stp x0, x1, [sp, #-0x10]! 36 | str x8, [sp, #-0x10]! 37 | .endmacro 38 | 39 | .macro RESTORE_REGISTERS 40 | ldr x8, [sp], #0x10 41 | ldp x0, x1, [sp], #0x10 42 | ldp x2, x3, [sp], #0x10 43 | ldp x4, x5, [sp], #0x10 44 | ldp x6, x7, [sp], #0x10 45 | ldp q0, q1, [sp], #0x20 46 | ldp q2, q3, [sp], #0x20 47 | ldp q4, q5, [sp], #0x20 48 | ldp q6, q7, [sp], #0x20 49 | .endmacro 50 | 51 | .macro CALL_HOOK_BEFORE 52 | BACKUP_REGISTERS 53 | mov x2, lr 54 | bl _hook_objc_msgSend_before 55 | RESTORE_REGISTERS 56 | .endmacro 57 | 58 | .macro CALL_HOOK_SUPER_BEFORE 59 | BACKUP_REGISTERS 60 | ldp x0, x16, [x0] 61 | mov x2, lr 62 | bl _hook_objc_msgSend_before 63 | RESTORE_REGISTERS 64 | .endmacro 65 | 66 | .macro CALL_HOOK_AFTER 67 | BACKUP_REGISTERS 68 | mov x0, #0x0 69 | bl _hook_objc_msgSend_after 70 | mov lr, x0 71 | RESTORE_REGISTERS 72 | .endmacro 73 | 74 | .macro CALL_HOOK_SUPER_AFTER 75 | BACKUP_REGISTERS 76 | mov x0, #0x1 77 | bl _hook_objc_msgSend_after 78 | mov lr, x0 79 | RESTORE_REGISTERS 80 | .endmacro 81 | 82 | .macro CALL_ORIGIN_OBJC_MSGSEND 83 | adrp x17, _orgin_objc_msgSend@PAGE 84 | ldr x17, [x17, _orgin_objc_msgSend@PAGEOFF] 85 | blr x17 86 | .endmacro 87 | 88 | .macro CALL_ORIGIN_OBJC_MSGSENDSUPER2 89 | adrp x17, _orgin_objc_msgSendSuper2@PAGE 90 | ldr x17, [x17, _orgin_objc_msgSendSuper2@PAGEOFF] 91 | blr x17 92 | .endmacro 93 | 94 | .macro COPY_STACK_FRAME 95 | #if SUPPORT_SHOW_CALL_STACK 96 | stp x29, x30, [sp, #-0x10] 97 | mov x17, sp 98 | sub x17, fp, x17 99 | sub fp, sp, #0x10 100 | sub sp, fp, x17 101 | stp x0, x1, [sp, #-0x10] 102 | stp x2, x3, [sp, #-0x20] 103 | mov x0, sp 104 | add x1, sp, x17 105 | add x1, x1, #0x10 106 | mov x3, #0x0 107 | cmp x3, x17 108 | b.eq #0x18 109 | ldr x2, [x1, x3] 110 | str x2, [x0, x3] 111 | add x3, x3, #0x8 112 | cmp x3, x17 113 | b.lt #-0x10 114 | ldp x0, x1, [sp, #-0x10] 115 | ldp x2, x3, [sp, #-0x20] 116 | #endif 117 | .endmacro 118 | 119 | .macro FREE_STACK_FRAME 120 | #if SUPPORT_SHOW_CALL_STACK 121 | mov sp, fp 122 | add sp, sp, #0x10 123 | ldr fp, [fp] 124 | #endif 125 | .endmacro 126 | 127 | # todo: 目前是全量复制栈帧,但是其实只需要复制参数传递用到的栈,利用函数签名等手段,去判断需要复制的栈帧大小 128 | ENTRY _hook_msgSend 129 | COPY_STACK_FRAME 130 | CALL_HOOK_BEFORE 131 | CALL_ORIGIN_OBJC_MSGSEND 132 | CALL_HOOK_AFTER 133 | FREE_STACK_FRAME 134 | ret 135 | END_ENTRY _hook_msgSend 136 | 137 | ENTRY _hook_msgSendSuper2 138 | COPY_STACK_FRAME 139 | CALL_HOOK_SUPER_BEFORE 140 | CALL_ORIGIN_OBJC_MSGSENDSUPER2 141 | CALL_HOOK_SUPER_AFTER 142 | FREE_STACK_FRAME 143 | ret 144 | END_ENTRY _hook_msgSendSuper2 145 | 146 | #endif 147 | -------------------------------------------------------------------------------- /TimeProfiler/TimeProfiler.h: -------------------------------------------------------------------------------- 1 | // 2 | // TimeProfiler.h 3 | // KKMagicHook 4 | // 5 | // Created by 吴凯凯 on 2020/4/2. 6 | // Copyright © 2020 吴凯凯. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | FOUNDATION_EXPORT NSNotificationName _Nonnull const TPTimeProfilerProcessedDataNotification; 14 | 15 | @class TPModel; 16 | 17 | @interface TimeProfiler : NSObject 18 | 19 | @property (nonatomic, copy, readonly)NSArray *ignoreClassArr; 20 | @property (nonatomic, strong, readonly)NSMutableArray *modelArr; 21 | 22 | + (instancetype)shareInstance; 23 | 24 | /** 25 | 开始/停止统计,需成对使用 26 | 可以用来测多个功能点的耗时问题 27 | */ 28 | - (void)TPStartTrace:(char *)featureName; 29 | - (void)TPStopTrace; 30 | 31 | - (void)TPSetMaxDepth:(int)depth; 32 | - (void)TPSetCostMinTime:(uint64_t)time; //单位为us,1ms = 1000us 33 | - (void)TPSetFilterClass:(NSArray *)classArr; //需要过滤的类,不调用此方法,默认为TimeProfilerVC、TPRecordHierarchyModel、 TPRecordCell、TPRecordModel等TimeProfiler本身类(不统计过滤的类) 34 | 35 | @end 36 | 37 | NS_ASSUME_NONNULL_END 38 | -------------------------------------------------------------------------------- /TimeProfiler/TimeProfiler.m: -------------------------------------------------------------------------------- 1 | // 2 | // TimeProfiler.m 3 | // KKMagicHook 4 | // 5 | // Created by 吴凯凯 on 2020/4/2. 6 | // Copyright © 2020 吴凯凯. All rights reserved. 7 | // 8 | 9 | #import "TimeProfiler.h" 10 | #import "TPCallTrace.h" 11 | #import "TPRecordModel.h" 12 | #import "TPRecordHierarchyModel.h" 13 | #import "TPModel.h" 14 | 15 | #define IS_SHOW_DEBUG_INFO_IN_CONSOLE 0 16 | 17 | NSNotificationName const TPTimeProfilerProcessedDataNotification = @"TPTimeProfilerProcessedDataNotification"; 18 | 19 | @interface TimeProfiler() 20 | { 21 | NSArray *_defaultIgnoreClass; 22 | NSArray *_ignoreClassArr; 23 | } 24 | 25 | @property (nonatomic, copy, readwrite)NSArray *ignoreClassArr; 26 | @property (nonatomic, strong, readwrite)NSMutableArray *modelArr; 27 | 28 | @end 29 | 30 | @implementation TimeProfiler 31 | 32 | + (instancetype)shareInstance 33 | { 34 | static TimeProfiler *instance; 35 | static dispatch_once_t onceToken; 36 | dispatch_once(&onceToken, ^{ 37 | instance = [[TimeProfiler alloc] init]; 38 | }); 39 | return instance; 40 | } 41 | 42 | - (instancetype)init 43 | { 44 | self = [super init]; 45 | if (self) { 46 | _defaultIgnoreClass = @[NSClassFromString(@"TPModel"), NSClassFromString(@"TPMainVC"), NSClassFromString(@"TimeProfiler"), NSClassFromString(@"TimeProfilerVC"), NSClassFromString(@"TPRecordHierarchyModel"), NSClassFromString(@"TPRecordCell"), NSClassFromString(@"TPRecordModel")]; 47 | } 48 | return self; 49 | } 50 | 51 | - (void)TPStartTrace:(char *)featureName 52 | { 53 | startTrace(featureName); 54 | } 55 | 56 | - (void)TPStopTrace 57 | { 58 | [self stopAndGetCallRecord]; 59 | } 60 | 61 | - (void)TPSetMaxDepth:(int)depth 62 | { 63 | setMaxDepth(depth); 64 | } 65 | 66 | - (void)TPSetCostMinTime:(uint64_t)time 67 | { 68 | setCostMinTime(time); 69 | } 70 | 71 | - (void)TPSetFilterClass:(NSArray *)classArr 72 | { 73 | self.ignoreClassArr = classArr; 74 | } 75 | 76 | - (NSUInteger)findStartDepthIndex:(NSUInteger)start arr:(NSArray *)arr 77 | { 78 | NSUInteger index = start; 79 | if (arr.count > index) { 80 | TPRecordModel *model = arr[index]; 81 | int minDepth = model.depth; 82 | int minTotal = model.total; 83 | for (NSUInteger i = index+1; i < arr.count; i++) { 84 | TPRecordModel *tmp = arr[i]; 85 | if (tmp.depth < minDepth || (tmp.depth == minDepth && tmp.total < minTotal)) { 86 | minDepth = tmp.depth; 87 | minTotal = tmp.total; 88 | index = i; 89 | } 90 | } 91 | } 92 | return index; 93 | } 94 | 95 | - (NSArray *)recursive_getRecord:(NSMutableArray *)arr 96 | { 97 | if ([arr isKindOfClass:NSArray.class] && arr.count > 0) { 98 | BOOL isValid = YES; 99 | NSMutableArray *recordArr = [NSMutableArray array]; 100 | NSMutableArray *splitArr = [NSMutableArray array]; 101 | NSUInteger index = [self findStartDepthIndex:0 arr:arr]; 102 | if (index > 0) { 103 | [splitArr addObject:[NSMutableArray array]]; 104 | for (int i = 0; i < index; i++) { 105 | [[splitArr lastObject] addObject:arr[i]]; 106 | } 107 | } 108 | TPRecordModel *model = arr[index]; 109 | [recordArr addObject:model]; 110 | [arr removeObjectAtIndex:index]; 111 | int startDepth = model.depth; 112 | int startTotal = model.total; 113 | for (NSUInteger i = index; i < arr.count; ) { 114 | model = arr[i]; 115 | if (model.total == startTotal && model.depth-1==startDepth) { 116 | [recordArr addObject:model]; 117 | [arr removeObjectAtIndex:i]; 118 | startDepth++; 119 | isValid = YES; 120 | } 121 | else 122 | { 123 | if (isValid) { 124 | isValid = NO; 125 | [splitArr addObject:[NSMutableArray array]]; 126 | } 127 | [[splitArr lastObject] addObject:model]; 128 | i++; 129 | } 130 | 131 | } 132 | 133 | for (NSUInteger i = splitArr.count; i > 0; i--) { 134 | NSMutableArray *sArr = splitArr[i-1]; 135 | [recordArr addObjectsFromArray:[self recursive_getRecord:sArr]]; 136 | } 137 | return recordArr; 138 | } 139 | return @[]; 140 | } 141 | 142 | - (void)setRecordDic:(NSMutableArray *)arr record:(TPCallRecord *)record 143 | { 144 | if ([arr isKindOfClass:NSMutableArray.class] && record) { 145 | int total=1; 146 | for (NSUInteger i = 0; i < arr.count; i++) 147 | { 148 | TPRecordModel *model = arr[i]; 149 | if (model.depth == record->depth) { 150 | total = model.total+1; 151 | break; 152 | } 153 | } 154 | 155 | TPRecordModel *model = [[TPRecordModel alloc] initWithCls:record->cls sel:record->sel time:record->costTime depth:record->depth total:total is_objc_msgSendSuper:record->is_objc_msgSendSuper]; 156 | [arr insertObject:model atIndex:0]; 157 | } 158 | } 159 | 160 | 161 | - (void)stopAndGetCallRecord 162 | { 163 | stopTrace(); 164 | TPMainThreadCallRecord *mainThreadCallRecord = getMainThreadCallRecord(); 165 | if (mainThreadCallRecord==NULL) { 166 | NSLog(@"====================================="); 167 | NSLog(@"====================================="); 168 | NSLog(@"函数TPStartTrace跟TPStopTrace需要成对调用"); 169 | NSLog(@"具体用法请看:https://github.com/maniackk/TimeProfiler"); 170 | NSLog(@"====================================="); 171 | NSLog(@"====================================="); 172 | return; 173 | } 174 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ 175 | #if IS_SHOW_DEBUG_INFO_IN_CONSOLE 176 | NSMutableString *textM = [[NSMutableString alloc] init]; 177 | #endif 178 | NSMutableArray *allMethodRecord = [NSMutableArray array]; 179 | int i = 0, j; 180 | while (i <= mainThreadCallRecord->index) { 181 | NSMutableArray *methodRecord = [NSMutableArray array]; 182 | for (j = i; j <= mainThreadCallRecord->index;j++) 183 | { 184 | TPCallRecord *callRecord = &mainThreadCallRecord->record[j]; 185 | #if IS_SHOW_DEBUG_INFO_IN_CONSOLE 186 | NSString *str = [self debug_getMethodCallStr:callRecord]; 187 | [textM appendString:str]; 188 | [textM appendString:@"\r"]; 189 | #endif 190 | [self setRecordDic:methodRecord record:callRecord]; 191 | if (callRecord->depth==0 || j==mainThreadCallRecord->index) 192 | { 193 | NSArray *recordModelArr = [self recursive_getRecord:methodRecord]; 194 | recordModelArr = [self filterClass:recordModelArr]; 195 | if (recordModelArr.count > 0) { 196 | TPRecordHierarchyModel *model = [[TPRecordHierarchyModel alloc] initWithRecordModelArr:recordModelArr]; 197 | [allMethodRecord addObject:model]; 198 | } 199 | //退出循环 200 | break; 201 | } 202 | } 203 | 204 | i = j+1; 205 | } 206 | 207 | TPModel *model = [[TPModel alloc] init]; 208 | model.sequentialMethodRecord = [[NSArray alloc] initWithArray:allMethodRecord copyItems:YES]; 209 | model.costTimeSortMethodRecord = [self sortCostTimeRecord:[[NSArray alloc] initWithArray:allMethodRecord copyItems:YES]]; 210 | model.callCountSortMethodRecord = [self sortCallCountRecord:[[NSArray alloc] initWithArray:allMethodRecord copyItems:YES]]; 211 | char *featureName = mainThreadCallRecord->featureName; 212 | if (featureName) { 213 | model.featureName = [NSString stringWithUTF8String:featureName]; 214 | } 215 | if (!model.featureName) { 216 | model.featureName = @"调用TPStartTrace:函数,需要传name"; 217 | } 218 | dispatch_async(dispatch_get_main_queue(), ^{ 219 | [self.modelArr addObject:model]; 220 | [[NSNotificationCenter defaultCenter] postNotificationName:TPTimeProfilerProcessedDataNotification object:nil]; 221 | }); 222 | if (mainThreadCallRecord) { 223 | free(mainThreadCallRecord->record); 224 | free(mainThreadCallRecord); 225 | } 226 | #if IS_SHOW_DEBUG_INFO_IN_CONSOLE 227 | [self debug_printMethodRecord:textM]; 228 | #endif 229 | }); 230 | } 231 | 232 | - (NSArray *)filterClass:(NSArray *)recordModelArr 233 | { 234 | NSArray *ignoreClassArr = [TimeProfiler shareInstance].ignoreClassArr; 235 | NSMutableArray *result = [NSMutableArray array]; 236 | if ([recordModelArr isKindOfClass:NSArray.class]) { 237 | int depth = 0; 238 | BOOL isIgnore = FALSE; 239 | for (TPRecordModel *model in recordModelArr) { 240 | if (isIgnore) { 241 | if (depth >= model.depth) { 242 | isIgnore = [ignoreClassArr containsObject:model.cls]; 243 | depth = model.depth; 244 | } 245 | } 246 | else 247 | { 248 | isIgnore = [ignoreClassArr containsObject:model.cls]; 249 | depth = model.depth; 250 | } 251 | if (!isIgnore) { 252 | [result addObject:model]; 253 | } 254 | } 255 | } 256 | return result; 257 | } 258 | 259 | #if IS_SHOW_DEBUG_INFO_IN_CONSOLE 260 | 261 | - (void)debug_printMethodRecord:(NSString *)text 262 | { 263 | //记录的顺序是方法完成时间 264 | NSLog(@"=========printMethodRecord==Start================"); 265 | NSLog(@"%@", text); 266 | NSLog(@"=========printMethodRecord==End================"); 267 | } 268 | 269 | - (NSString *)debug_getMethodCallStr:(TPCallRecord *)callRecord 270 | { 271 | NSMutableString *str = [[NSMutableString alloc] init]; 272 | double ms = callRecord->costTime/1000.0; 273 | [str appendString:[NSString stringWithFormat:@" %d | %lgms | ", callRecord->depth, ms]]; 274 | if (callRecord->depth>0) { 275 | [str appendString:[[NSString string] stringByPaddingToLength:callRecord->depth withString:@" " startingAtIndex:0]]; 276 | } 277 | if (class_isMetaClass(callRecord->cls)) 278 | { 279 | [str appendString:@"+"]; 280 | } 281 | else 282 | { 283 | [str appendString:@"-"]; 284 | } 285 | if (callRecord->is_objc_msgSendSuper) { 286 | [str appendString:[NSString stringWithFormat:@"[(super)%@  %@]", NSStringFromClass(callRecord->cls), NSStringFromSelector(callRecord->sel)]]; 287 | } 288 | else 289 | { 290 | [str appendString:[NSString stringWithFormat:@"[%@  %@]", NSStringFromClass(callRecord->cls), NSStringFromSelector(callRecord->sel)]]; 291 | } 292 | return str.copy; 293 | } 294 | 295 | #endif 296 | 297 | - (NSArray *)sortCostTimeRecord:(NSArray *)arr 298 | { 299 | NSArray *sortArr = [arr sortedArrayUsingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) { 300 | TPRecordHierarchyModel *model1 = (TPRecordHierarchyModel *)obj1; 301 | TPRecordHierarchyModel *model2 = (TPRecordHierarchyModel *)obj2; 302 | if (model1.rootMethod.costTime > model2.rootMethod.costTime) { 303 | return NSOrderedAscending; 304 | } 305 | return NSOrderedDescending; 306 | }]; 307 | for (TPRecordHierarchyModel *model in sortArr) { 308 | model.isExpand = NO; 309 | } 310 | return sortArr; 311 | } 312 | 313 | - (void)arrAddRecord:(TPRecordModel *)model arr:(NSMutableArray *)arr 314 | { 315 | for (int i = 0; i < arr.count; i++) { 316 | TPRecordModel *temp = arr[i]; 317 | if ([temp isEqualRecordModel:model]) { 318 | temp.callCount++; 319 | return; 320 | } 321 | } 322 | model.callCount = 1; 323 | [arr addObject:model]; 324 | } 325 | 326 | - (NSArray *)sortCallCountRecord:(NSArray *)arr 327 | { 328 | NSMutableArray *arrM = [NSMutableArray array]; 329 | for (TPRecordHierarchyModel *model in arr) { 330 | [self arrAddRecord:model.rootMethod arr:arrM]; 331 | if ([model.subMethods isKindOfClass:NSArray.class]) { 332 | for (TPRecordModel *recoreModel in model.subMethods) { 333 | [self arrAddRecord:recoreModel arr:arrM]; 334 | } 335 | } 336 | } 337 | 338 | NSArray *sortArr = [arrM sortedArrayUsingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2) { 339 | TPRecordModel *model1 = (TPRecordModel *)obj1; 340 | TPRecordModel *model2 = (TPRecordModel *)obj2; 341 | if (model1.callCount > model2.callCount) { 342 | return NSOrderedAscending; 343 | } 344 | return NSOrderedDescending; 345 | }]; 346 | return sortArr; 347 | } 348 | 349 | #pragma mark - get&set method 350 | 351 | - (NSArray *)ignoreClassArr 352 | { 353 | if (!_ignoreClassArr) { 354 | _ignoreClassArr = _defaultIgnoreClass; 355 | } 356 | return _ignoreClassArr; 357 | } 358 | 359 | - (void)setIgnoreClassArr:(NSArray *)ignoreClassArr 360 | { 361 | if (ignoreClassArr.count > 0) { 362 | NSMutableArray *arrM = [NSMutableArray arrayWithArray:_defaultIgnoreClass]; 363 | [arrM addObjectsFromArray:ignoreClassArr]; 364 | _ignoreClassArr = arrM.copy; 365 | } 366 | } 367 | 368 | - (NSMutableArray *)modelArr 369 | { 370 | if (!_modelArr) { 371 | _modelArr = [NSMutableArray array]; 372 | } 373 | return _modelArr; 374 | } 375 | 376 | @end 377 | -------------------------------------------------------------------------------- /TimeProfiler/fishhook/fishhook.c: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, Facebook, Inc. 2 | // All rights reserved. 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are met: 5 | // * Redistributions of source code must retain the above copyright notice, 6 | // this list of conditions and the following disclaimer. 7 | // * Redistributions in binary form must reproduce the above copyright notice, 8 | // this list of conditions and the following disclaimer in the documentation 9 | // and/or other materials provided with the distribution. 10 | // * Neither the name Facebook nor the names of its contributors may be used to 11 | // endorse or promote products derived from this software without specific 12 | // prior written permission. 13 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 14 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 17 | // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 20 | // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 21 | // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 22 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | 24 | #include "fishhook.h" 25 | 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | 39 | #ifdef __LP64__ 40 | typedef struct mach_header_64 mach_header_t; 41 | typedef struct segment_command_64 segment_command_t; 42 | typedef struct section_64 section_t; 43 | typedef struct nlist_64 nlist_t; 44 | #define LC_SEGMENT_ARCH_DEPENDENT LC_SEGMENT_64 45 | #else 46 | typedef struct mach_header mach_header_t; 47 | typedef struct segment_command segment_command_t; 48 | typedef struct section section_t; 49 | typedef struct nlist nlist_t; 50 | #define LC_SEGMENT_ARCH_DEPENDENT LC_SEGMENT 51 | #endif 52 | 53 | #ifndef SEG_DATA_CONST 54 | #define SEG_DATA_CONST "__DATA_CONST" 55 | #endif 56 | 57 | struct rebindings_entry { 58 | struct rebinding *rebindings; 59 | size_t rebindings_nel; 60 | struct rebindings_entry *next; 61 | }; 62 | 63 | static struct rebindings_entry *_rebindings_head; 64 | 65 | static int prepend_rebindings(struct rebindings_entry **rebindings_head, 66 | struct rebinding rebindings[], 67 | size_t nel) { 68 | struct rebindings_entry *new_entry = (struct rebindings_entry *) malloc(sizeof(struct rebindings_entry)); 69 | if (!new_entry) { 70 | return -1; 71 | } 72 | new_entry->rebindings = (struct rebinding *) malloc(sizeof(struct rebinding) * nel); 73 | if (!new_entry->rebindings) { 74 | free(new_entry); 75 | return -1; 76 | } 77 | memcpy(new_entry->rebindings, rebindings, sizeof(struct rebinding) * nel); 78 | new_entry->rebindings_nel = nel; 79 | new_entry->next = *rebindings_head; 80 | *rebindings_head = new_entry; 81 | return 0; 82 | } 83 | 84 | static vm_prot_t get_protection(void *sectionStart) { 85 | mach_port_t task = mach_task_self(); 86 | vm_size_t size = 0; 87 | vm_address_t address = (vm_address_t)sectionStart; 88 | memory_object_name_t object; 89 | #if __LP64__ 90 | mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64; 91 | vm_region_basic_info_data_64_t info; 92 | kern_return_t info_ret = vm_region_64( 93 | task, &address, &size, VM_REGION_BASIC_INFO_64, (vm_region_info_64_t)&info, &count, &object); 94 | #else 95 | mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT; 96 | vm_region_basic_info_data_t info; 97 | kern_return_t info_ret = vm_region(task, &address, &size, VM_REGION_BASIC_INFO, (vm_region_info_t)&info, &count, &object); 98 | #endif 99 | if (info_ret == KERN_SUCCESS) { 100 | return info.protection; 101 | } else { 102 | return VM_PROT_READ; 103 | } 104 | } 105 | static void perform_rebinding_with_section(struct rebindings_entry *rebindings, 106 | section_t *section, 107 | intptr_t slide, 108 | nlist_t *symtab, 109 | char *strtab, 110 | uint32_t *indirect_symtab) { 111 | const bool isDataConst = strcmp(section->segname, SEG_DATA_CONST) == 0; 112 | uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1; 113 | void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr); 114 | vm_prot_t oldProtection = VM_PROT_READ; 115 | if (isDataConst) { 116 | oldProtection = get_protection(rebindings); 117 | mprotect(indirect_symbol_bindings, section->size, PROT_READ | PROT_WRITE); 118 | } 119 | for (uint i = 0; i < section->size / sizeof(void *); i++) { 120 | uint32_t symtab_index = indirect_symbol_indices[i]; 121 | if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL || 122 | symtab_index == (INDIRECT_SYMBOL_LOCAL | INDIRECT_SYMBOL_ABS)) { 123 | continue; 124 | } 125 | uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx; 126 | char *symbol_name = strtab + strtab_offset; 127 | bool symbol_name_longer_than_1 = symbol_name[0] && symbol_name[1]; 128 | struct rebindings_entry *cur = rebindings; 129 | while (cur) { 130 | for (uint j = 0; j < cur->rebindings_nel; j++) { 131 | if (symbol_name_longer_than_1 && 132 | strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) { 133 | if (cur->rebindings[j].replaced != NULL && 134 | indirect_symbol_bindings[i] != cur->rebindings[j].replacement) { 135 | *(cur->rebindings[j].replaced) = indirect_symbol_bindings[i]; 136 | } 137 | indirect_symbol_bindings[i] = cur->rebindings[j].replacement; 138 | goto symbol_loop; 139 | } 140 | } 141 | cur = cur->next; 142 | } 143 | symbol_loop:; 144 | } 145 | if (isDataConst) { 146 | int protection = 0; 147 | if (oldProtection & VM_PROT_READ) { 148 | protection |= PROT_READ; 149 | } 150 | if (oldProtection & VM_PROT_WRITE) { 151 | protection |= PROT_WRITE; 152 | } 153 | if (oldProtection & VM_PROT_EXECUTE) { 154 | protection |= PROT_EXEC; 155 | } 156 | mprotect(indirect_symbol_bindings, section->size, protection); 157 | } 158 | } 159 | 160 | static void rebind_symbols_for_image(struct rebindings_entry *rebindings, 161 | const struct mach_header *header, 162 | intptr_t slide) { 163 | Dl_info info; 164 | if (dladdr(header, &info) == 0) { 165 | return; 166 | } 167 | 168 | segment_command_t *cur_seg_cmd; 169 | segment_command_t *linkedit_segment = NULL; 170 | struct symtab_command* symtab_cmd = NULL; 171 | struct dysymtab_command* dysymtab_cmd = NULL; 172 | 173 | uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t); 174 | for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) { 175 | cur_seg_cmd = (segment_command_t *)cur; 176 | if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) { 177 | if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) { 178 | linkedit_segment = cur_seg_cmd; 179 | } 180 | } else if (cur_seg_cmd->cmd == LC_SYMTAB) { 181 | symtab_cmd = (struct symtab_command*)cur_seg_cmd; 182 | } else if (cur_seg_cmd->cmd == LC_DYSYMTAB) { 183 | dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd; 184 | } 185 | } 186 | 187 | if (!symtab_cmd || !dysymtab_cmd || !linkedit_segment || 188 | !dysymtab_cmd->nindirectsyms) { 189 | return; 190 | } 191 | 192 | // Find base symbol/string table addresses 193 | uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff; 194 | nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff); 195 | char *strtab = (char *)(linkedit_base + symtab_cmd->stroff); 196 | 197 | // Get indirect symbol table (array of uint32_t indices into symbol table) 198 | uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff); 199 | 200 | cur = (uintptr_t)header + sizeof(mach_header_t); 201 | for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) { 202 | cur_seg_cmd = (segment_command_t *)cur; 203 | if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) { 204 | if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 && 205 | strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) != 0) { 206 | continue; 207 | } 208 | for (uint j = 0; j < cur_seg_cmd->nsects; j++) { 209 | section_t *sect = 210 | (section_t *)(cur + sizeof(segment_command_t)) + j; 211 | if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) { 212 | perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab); 213 | } 214 | if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) { 215 | perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab); 216 | } 217 | } 218 | } 219 | } 220 | } 221 | 222 | static void _rebind_symbols_for_image(const struct mach_header *header, 223 | intptr_t slide) { 224 | rebind_symbols_for_image(_rebindings_head, header, slide); 225 | } 226 | 227 | int rebind_symbols_image(void *header, 228 | intptr_t slide, 229 | struct rebinding rebindings[], 230 | size_t rebindings_nel) { 231 | struct rebindings_entry *rebindings_head = NULL; 232 | int retval = prepend_rebindings(&rebindings_head, rebindings, rebindings_nel); 233 | rebind_symbols_for_image(rebindings_head, (const struct mach_header *) header, slide); 234 | if (rebindings_head) { 235 | free(rebindings_head->rebindings); 236 | } 237 | free(rebindings_head); 238 | return retval; 239 | } 240 | 241 | int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) { 242 | int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel); 243 | if (retval < 0) { 244 | return retval; 245 | } 246 | // If this was the first call, register callback for image additions (which is also invoked for 247 | // existing images, otherwise, just run on existing images 248 | if (!_rebindings_head->next) { 249 | _dyld_register_func_for_add_image(_rebind_symbols_for_image); 250 | } else { 251 | uint32_t c = _dyld_image_count(); 252 | for (uint32_t i = 0; i < c; i++) { 253 | _rebind_symbols_for_image(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i)); 254 | } 255 | } 256 | return retval; 257 | } 258 | -------------------------------------------------------------------------------- /TimeProfiler/fishhook/fishhook.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, Facebook, Inc. 2 | // All rights reserved. 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are met: 5 | // * Redistributions of source code must retain the above copyright notice, 6 | // this list of conditions and the following disclaimer. 7 | // * Redistributions in binary form must reproduce the above copyright notice, 8 | // this list of conditions and the following disclaimer in the documentation 9 | // and/or other materials provided with the distribution. 10 | // * Neither the name Facebook nor the names of its contributors may be used to 11 | // endorse or promote products derived from this software without specific 12 | // prior written permission. 13 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 14 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 17 | // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 20 | // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 21 | // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 22 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | 24 | #ifndef fishhook_h 25 | #define fishhook_h 26 | 27 | #include 28 | #include 29 | 30 | #if !defined(FISHHOOK_EXPORT) 31 | #define FISHHOOK_VISIBILITY __attribute__((visibility("hidden"))) 32 | #else 33 | #define FISHHOOK_VISIBILITY __attribute__((visibility("default"))) 34 | #endif 35 | 36 | #ifdef __cplusplus 37 | extern "C" { 38 | #endif //__cplusplus 39 | 40 | /* 41 | * A structure representing a particular intended rebinding from a symbol 42 | * name to its replacement 43 | */ 44 | struct rebinding { 45 | const char *name; 46 | void *replacement; 47 | void **replaced; 48 | }; 49 | 50 | /* 51 | * For each rebinding in rebindings, rebinds references to external, indirect 52 | * symbols with the specified name to instead point at replacement for each 53 | * image in the calling process as well as for all future images that are loaded 54 | * by the process. If rebind_functions is called more than once, the symbols to 55 | * rebind are added to the existing list of rebindings, and if a given symbol 56 | * is rebound more than once, the later rebinding will take precedence. 57 | */ 58 | FISHHOOK_VISIBILITY 59 | int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel); 60 | 61 | /* 62 | * Rebinds as above, but only in the specified image. The header should point 63 | * to the mach-o header, the slide should be the slide offset. Others as above. 64 | */ 65 | FISHHOOK_VISIBILITY 66 | int rebind_symbols_image(void *header, 67 | intptr_t slide, 68 | struct rebinding rebindings[], 69 | size_t rebindings_nel); 70 | 71 | #ifdef __cplusplus 72 | } 73 | #endif //__cplusplus 74 | 75 | #endif //fishhook_h 76 | 77 | -------------------------------------------------------------------------------- /tp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maniackk/TimeProfiler/cec23b0a1aaaf52f8fac198d2540b9d7d3afd971/tp.jpg --------------------------------------------------------------------------------