IOS中判断卡顿的方案总结

2025-05-29 0 104

FPS

FPS (Frames Per Second) 是图像领域中的定义,表示每秒渲染帧数,通常用于衡量画面的流畅度,每秒帧数越多,则表示画面越流畅,60fps 最佳,一般我们的APP的FPS 只要保持在 50-60之间,用户体验都是比较流畅的。

监测FPS也有好几种,这里只说最常用的方案,我最早是在YYFPSLabel中看到的。实现原理实现原理是向主线程的RunLoop的添加一个commonModes的CADisplayLink,每次屏幕刷新的时候都要执行CADisplayLink的方法,所以可以统计1s内屏幕刷新的次数,也就是FPS了,下面贴上我用Swift实现的代码:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92
class WeakProxy: NSObject {

weak var target: NSObjectProtocol?

init(target: NSObjectProtocol) {

self.target = target

super.init()

}

override func responds(to aSelector: Selector!) -> Bool {

return (target?.responds(to: aSelector) ?? false) || super.responds(to: aSelector)

}

override func forwardingTarget(for aSelector: Selector!) -> Any? {

return target

}

}

class FPSLabel: UILabel {

var link:CADisplayLink!

//记录方法执行次数

var count: Int = 0

//记录上次方法执行的时间,通过link.timestamp - _lastTime计算时间间隔

var lastTime: TimeInterval = 0

var _font: UIFont!

var _subFont: UIFont!

fileprivate let defaultSize = CGSize(width: 55,height: 20)

override init(frame: CGRect) {

super.init(frame: frame)

if frame.size.width == 0 && frame.size.height == 0 {

self.frame.size = defaultSize

}

self.layer.cornerRadius = 5

self.clipsToBounds = true

self.textAlignment = NSTextAlignment.center

self.isUserInteractionEnabled = false

self.backgroundColor = UIColor.white.withAlphaComponent(0.7)

_font = UIFont(name: "Menlo", size: 14)

if _font != nil {

_subFont = UIFont(name: "Menlo", size: 4)

}else{

_font = UIFont(name: "Courier", size: 14)

_subFont = UIFont(name: "Courier", size: 4)

}

link = CADisplayLink(target: WeakProxy.init(target: self), selector: #selector(FPSLabel.tick(link:)))

link.add(to: RunLoop.main, forMode: .commonModes)

}

//CADisplayLink 刷新执行的方法

@objc func tick(link: CADisplayLink) {

guard lastTime != 0 else {

lastTime = link.timestamp

return

}

count += 1

let timePassed = link.timestamp - lastTime

//时间大于等于1秒计算一次,也就是FPSLabel刷新的间隔,不希望太频繁刷新

guard timePassed >= 1 else {

return

}

lastTime = link.timestamp

let fps = Double(count) / timePassed

count = 0

let progress = fps / 60.0

let color = UIColor(hue: CGFloat(0.27 * (progress - 0.2)), saturation: 1, brightness: 0.9, alpha: 1)

let text = NSMutableAttributedString(string: "\\(Int(round(fps))) FPS")

text.addAttribute(NSAttributedStringKey.foregroundColor, value: color, range: NSRange(location: 0, length: text.length - 3))

text.addAttribute(NSAttributedStringKey.foregroundColor, value: UIColor.white, range: NSRange(location: text.length - 3, length: 3))

text.addAttribute(NSAttributedStringKey.font, value: _font, range: NSRange(location: 0, length: text.length))

text.addAttribute(NSAttributedStringKey.font, value: _subFont, range: NSRange(location: text.length - 4, length: 1))

self.attributedText = text

}

// 把displaylin从Runloop modes中移除

deinit {

link.invalidate()

}

required init?(coder aDecoder: NSCoder) {

fatalError("init(coder:) has not been implemented")

}

}

RunLoop

其实FPS中CADisplayLink的使用也是基于RunLoop,都依赖main RunLoop。我们来看看

先来看看简版的RunLoop的代码

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34
// 1.进入loop

__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled)

// 2.RunLoop 即将触发 Timer 回调。

__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);

// 3.RunLoop 即将触发 Source0 (非port) 回调。

__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);

// 4.RunLoop 触发 Source0 (非port) 回调。

sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle)

// 5.执行被加入的block

__CFRunLoopDoBlocks(runloop, currentMode);

// 6.RunLoop 的线程即将进入休眠(sleep)。

__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);

// 7.调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。

__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort)

// 进入休眠

// 8.RunLoop 的线程刚刚被唤醒了。

__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting

// 9.如果一个 Timer 到时间了,触发这个Timer的回调

__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())

// 10.如果有dispatch到main_queue的block,执行bloc

__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);

// 11.如果一个 Source1 (基于port) 发出事件了,处理这个事件

__CFRunLoopDoSource1(runloop, currentMode, source1, msg);

// 12.RunLoop 即将退出

__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

我们可以看到RunLoop调用方法主要集中在kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting之间,有人可能会问kCFRunLoopAfterWaiting之后也有一些方法调用,为什么不监测呢,我的理解,大部分导致卡顿的的方法是在kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting之间,比如source0主要是处理App内部事件,App自己负责管理(出发),如UIEvent(Touch事件等,GS发起到RunLoop运行再到事件回调到UI)、CFSocketRef。开辟一个子线程,然后实时计算 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting 两个状态区域之间的耗时是否超过某个阀值,来断定主线程的卡顿情况。

这里做法又有点不同,iOS实时卡顿监控3是设置连续5次超时50ms认为卡顿,戴铭在GCDFetchFeed4中设置的是连续3次超时80ms认为卡顿的代码。以下是iOS实时卡顿监控中提供的代码:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54
- (void)start

{

if (observer)

return;

// 信号

semaphore = dispatch_semaphore_create(0);

// 注册RunLoop状态观察

CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};

observer = CFRunLoopObserverCreate(kCFAllocatorDefault,

kCFRunLoopAllActivities,

YES,

0,

&runLoopObserverCallBack,

&context);

CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);

// 在子线程监控时长

dispatch_async(dispatch_get_global_queue(0, 0), ^{

while (YES)

{

long st = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));

if (st != 0)

{

if (!observer)

{

timeoutCount = 0;

semaphore = 0;

activity = 0;

return;

}

if (activity==kCFRunLoopBeforeSources || activity==kCFRunLoopAfterWaiting)

{

if (++timeoutCount < 5)

continue;

PLCrashReporterConfig *config = [[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD

symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll];

PLCrashReporter *crashReporter = [[PLCrashReporter alloc] initWithConfiguration:config];

NSData *data = [crashReporter generateLiveReport];

PLCrashReport *reporter = [[PLCrashReport alloc] initWithData:data error:NULL];

NSString *report = [PLCrashReportTextFormatter stringValueForCrashReport:reporter

withTextFormat:PLCrashReportTextFormatiOS];

NSLog(@"------------\\n%@\\n------------", report);

}

}

timeoutCount = 0;

}

});

}

子线程Ping

但是由于主线程的RunLoop在闲置时基本处于Before Waiting状态,这就导致了即便没有发生任何卡顿,这种检测方式也总能认定主线程处在卡顿状态。这套卡顿监控方案大致思路为:创建一个子线程通过信号量去ping主线程,因为ping的时候主线程肯定是在kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting之间。每次检测时设置标记位为YES,然后派发任务到主线程中将标记位设置为NO。接着子线程沉睡超时阙值时长,判断标志位是否成功设置成NO,如果没有说明主线程发生了卡顿ANREye5中就是使用子线程Ping的方式监测卡顿的。

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29
@interface PingThread : NSThread

......

@end

@implementation PingThread

- (void)main {

[self pingMainThread];

}

- (void)pingMainThread {

while (!self.cancelled) {

@autoreleasepool {

dispatch_async(dispatch_get_main_queue(), ^{

[_lock unlock];

});

CFAbsoluteTime pingTime = CFAbsoluteTimeGetCurrent();

NSArray *callSymbols = [StackBacktrace backtraceMainThread];

[_lock lock];

if (CFAbsoluteTimeGetCurrent() - pingTime >= _threshold) {

......

}

[NSThread sleepForTimeInterval: _interval];

}

}

}

@end

以下是我用Swift实现的:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46
public class CatonMonitor {

enum Constants {

static let timeOutInterval: TimeInterval = 0.05

static let queueTitle = "com.roy.PerformanceMonitor.CatonMonitor"

}

private var queue: DispatchQueue = DispatchQueue(label: Constants.queueTitle)

private var isMonitoring = false

private var semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)

public init() {}

public func start() {

guard !isMonitoring else { return }

isMonitoring = true

queue.async {

while self.isMonitoring {

var timeout = true

DispatchQueue.main.async {

timeout = false

self.semaphore.signal()

}

Thread.sleep(forTimeInterval: Constants.timeOutInterval)

if timeout {

let symbols = RCBacktrace.callstack(.main)

for symbol in symbols {

print(symbol.description)

}

}

self.semaphore.wait()

}

}

}

public func stop() {

guard isMonitoring else { return }

isMonitoring = false

}

}

CPU超过了80%

这个是Matrix-iOS 卡顿监控提到的:

我们也认为 CPU 过高也可能导致应用出现卡顿,所以在子线程检查主线程状态的同时,如果检测到 CPU 占用过高,会捕获当前的线程快照保存到文件中。目前微信应用中认为,单核 CPU 的占用超过了 80%,此时的 CPU 占用就过高了。

这种方式一般不能单独拿来作为卡顿监测,但可以像微信Matrix一样配合其他方式一起工作。

戴铭在GCDFetchFeed中如果CPU 的占用超过了 80%也捕获函数调用栈,以下是代码:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31
#define CPUMONITORRATE 80

+ (void)updateCPU {

thread_act_array_t threads;

mach_msg_type_number_t threadCount = 0;

const task_t thisTask = mach_task_self();

kern_return_t kr = task_threads(thisTask, &threads, &threadCount);

if (kr != KERN_SUCCESS) {

return;

}

for (int i = 0; i < threadCount; i++) {

thread_info_data_t threadInfo;

thread_basic_info_t threadBaseInfo;

mach_msg_type_number_t threadInfoCount = THREAD_INFO_MAX;

if (thread_info((thread_act_t)threads[i], THREAD_BASIC_INFO, (thread_info_t)threadInfo, &threadInfoCount) == KERN_SUCCESS) {

threadBaseInfo = (thread_basic_info_t)threadInfo;

if (!(threadBaseInfo->flags & TH_FLAGS_IDLE)) {

integer_t cpuUsage = threadBaseInfo->cpu_usage / 10;

if (cpuUsage > CPUMONITORRATE) {

//cup 消耗大于设置值时打印和记录堆栈

NSString *reStr = smStackOfThread(threads[i]);

SMCallStackModel *model = [[SMCallStackModel alloc] init];

model.stackStr = reStr;

//记录数据库中

[[[SMLagDB shareInstance] increaseWithStackModel:model] subscribeNext:^(id x) {}];

// NSLog(@"CPU useage overload thread stack:\\n%@",reStr);

}

}

}

}

}

卡顿方法的栈信息

当我们得到卡顿的时间点,就要立即拿到卡顿的堆栈,有两种方式一种是遍历栈帧,实现原理我在iOS获取任意线程调用栈7写的挺详细的,同时开源了代码RCBacktrace,另一种方式是通过Signal获取任意线程调用栈,实现原理我在通过Signal handling(信号处理)获取任意线程调用栈写了,代码在backtrace-swift,但这种方式在调试时比较麻烦,建议用第一种方式。

以上就是IOS中判断卡顿的方案总结的详细内容,更多关于IOS卡顿检测的资料请关注快网idc其它相关文章!

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。

快网idc优惠网 建站教程 IOS中判断卡顿的方案总结 https://www.kuaiidc.com/89047.html

相关文章

发表评论
暂无评论