iOS开发中的卡顿分析

市面上的iOS卡顿分析方案有三种:监控FPS、监控RunLoop、ping主线程。

方案一:监控FPS

一般来说,我们约定60FPS即为流畅。那么反过来,如果App在运行期间出现了掉帧,即可认为出现了卡顿。

监控FPS的方案几乎都是基于CADisplayLink实现的。简单介绍一下CADisplayLink:CADisplayLink是一个和屏幕刷新率保持一致的定时器,一但 CADisplayLink 以特定的模式注册到runloop之后,每当屏幕需要刷新的时候,runloop就会调用CADisplayLink绑定的target上的selector。
可以通过向RunLoop中添加CADisplayLink,根据其回调来计算出当前画面的帧数。

#import "FPSMonitor.h"
#import <UIKit/UIKit.h>

@interface FPSMonitor ()
@property (nonatomic, strong) CADisplayLink* link;
@property (nonatomic, assign) NSInteger count;
@property (nonatomic, assign) NSTimeInterval lastTime;
@end

@implementation FPSMonitor

- (void)beginMonitor {
    _link = [CADisplayLink displayLinkWithTarget:self selector:@selector(fpsInfoCaculate:)];
    [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
    
}

- (void)fpsInfoCaculate:(CADisplayLink *)sender {
    if (_lastTime == 0) {
        _lastTime = sender.timestamp;
        return;
    }
    _count++;
    double deltaTime = sender.timestamp - _lastTime;
    if (deltaTime >= 1) {
        NSInteger FPS = _count / deltaTime;
        _lastTime = sender.timestamp;
        _count = 0;
        NSLog(@"FPS: %li", (NSInteger)ceill(FPS + 0.5));
    }
}

@end

FPS的好处就是直观,小手一划后FPS下降了,说明页面的某处有性能问题。坏处就是只知道这是页面的某处,不能准确定位到具体的堆栈。


方案二:监控RunLoop

首先来介绍下什么是RunLoop。RunLoop是维护其内部事件循环的一个对象,它在程序运行过程中重复的做着一些事情,例如接收消息、处理消息、休眠等等。

所谓的事件循环,就是对事件/消息进行管理,没有消息时,休眠线程以避免资源消耗,从用户态切换到内核态。

有事件/消息需要进行处理时,立即唤醒线程,回到用户态进行处理。

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

UIApplicationMain函数内部会启动主线程的RunLoop,使得iOS程序持续运行。

iOS系统中有两套API来使用RunLoop,NSRunLoop(CFRunLoopRef的封装)和CFRunLoopRef。Foundation框架是不开源的,可以通过开源的CoreFoundation来分析RunLoop内部实现。

点此下载CoreFoundation

RunLoop对象底层就是一个CFRunLoopRef结构体,内部数据如下:

struct __CFRunLoop {
    pthread_t _pthread;               // 与RunLoop一一对应的线程
    CFMutableSetRef _commonModes;     // 存储着NSString(mode名称)的集合
    CFMutableSetRef _commonModeItems; // 存储着被标记为commonMode的Source0/Source1/Timer/Observer
    CFRunLoopModeRef _currentMode;    // RunLoop当前的运行模式
    CFMutableSetRef _modes;           // 存储着RunLoop所有的 Mode(CFRunLoopModeRef)模式
        // 其他属性略 
};
struct __CFRunLoopMode {
    CFStringRef _name;            // mode 类型,如:NSDefaultRunLoopMode
    CFMutableSetRef _sources0;    // 事件源 sources0
    CFMutableSetRef _sources1;    // 事件源 sources1
    CFMutableArrayRef _observers; // 观察者
    CFMutableArrayRef _timers;    // 定时器
        // 其他属性略
};

Source0被添加到RunLoop上时并不会主动唤醒线程,需要手动去唤醒。Source0负责对触摸事件的处理以及performSeletor:onThread:

Source1具备唤醒线程的能力,使用的是基于Port的线程间通信。Source1负责捕获系统事件,并将事件交由Source0处理。

struct __CFRunLoopSource {
    CFRuntimeBase _base;
    uint32_t _bits;
    pthread_mutex_t _lock;
    CFIndex _order;         /* immutable */
    CFMutableBagRef _runLoops;
    union {
                CFRunLoopSourceContext version0;      // 表示 sources0
        CFRunLoopSourceContext1 version1;     // 表示 sources1
    } _context;
};

__CFRunLoopTimer和NSTimer是免费桥接toll-free bridged的。
performSelector:WithObject:afterDelay:方法会创建timer并添加到RunLoop中。

struct __CFRunLoopTimer {
    CFRuntimeBase _base;
    uint16_t _bits;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;
    CFMutableSetRef _rlModes;
    CFAbsoluteTime _nextFireDate;
    CFTimeInterval _interval;       /* immutable */
    CFTimeInterval _tolerance;          /* mutable */
    uint64_t _fireTSR;          /* TSR units */
    CFIndex _order;         /* immutable */
    CFRunLoopTimerCallBack _callout;    /* immutable */
    CFRunLoopTimerContext _context; /* immutable, except invalidation */
};

RunLoopObserver用于监听RunLoop的六种状态。CFRunLoopObserver中的_activities用于保存RunLoop的活动状态,当状态发生改变时,通过回调函数_callout函数通知所有observer。

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),          // 即将进入 RunLoop
    kCFRunLoopBeforeTimers = (1UL << 1),   // 即将处理 Timers
    kCFRunLoopBeforeSources = (1UL << 2),  // 即将处理 Sources
    kCFRunLoopBeforeWaiting = (1UL << 5),  // 即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),   // 刚从休眠中唤醒
    kCFRunLoopExit = (1UL << 7),           // 即将退出 RunLoop
    kCFRunLoopAllActivities = 0x0FFFFFFFU  // 以上所有状态
};
struct __CFRunLoopObserver {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;
    CFIndex _rlCount;
    CFOptionFlags _activities;      /* immutable */
    CFIndex _order;         /* immutable */
    CFRunLoopObserverCallBack _callout; /* immutable */
    CFRunLoopObserverContext _context;  /* immutable, except invalidation */
};

简单过一下RunLoop的源码。

void CFRunLoopRun(void) {   /* DOES CALLOUT */
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

简单来看RunLoop是个 do..while循环,下面来看看循环中具体干了哪些事情。

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
    __CFRunLoopLock(rl);
    //根据modeName来查找本次运行的mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    // 如果没找到mode 或者 mode里没有任何的事件,就此停止,不再循环
    if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
             Boolean did = false;
             if (currentMode) __CFRunLoopModeUnlock(currentMode);
             __CFRunLoopUnlock(rl);
             return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
    }
    CFRunLoopModeRef previousMode = rl->_currentMode;
    rl->_currentMode = currentMode;
    int32_t result = kCFRunLoopRunFinished;
    // 通知 observers 即将进入RunLoop
    if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
  // RunLoop具体要做的事情
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
  // 通知 observers 即将退出RunLoop
    if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

        __CFRunLoopModeUnlock(currentMode);
        __CFRunLoopPopPerRunData(rl, previousPerRun);
    rl->_currentMode = previousMode;
    __CFRunLoopUnlock(rl);
    return result;
}

从上面可以看到RunLoop除了通知observers即将进入/退出外,其他具体要做的事情都写在了__CFRunLoopRun中。

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    uint64_t startTSR = mach_absolute_time();

    // 状态判断
    if (__CFRunLoopIsStopped(rl)) {
        __CFRunLoopUnsetStopped(rl);
    return kCFRunLoopRunStopped;
    } else if (rlm->_stopped) {
    rlm->_stopped = false;
    return kCFRunLoopRunStopped;
    }
  // 初始化timeout_timer代码 略
  
    int32_t retVal = 0;
  
    do {
          __CFPortSet waitSet = rlm->_portSet;
        __CFRunLoopUnsetIgnoreWakeUps(rl);
                // 通知 observers 即将处理Timer
        if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        // 通知 observers 即将处理Sources
        if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
                // 处理主队列异步的block
          __CFRunLoopDoBlocks(rl, rlm);
                // 处理Source0
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        if (sourceHandledThisLoop) {
            // 处理block
            __CFRunLoopDoBlocks(rl, rlm);
    }

        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);

        didDispatchPortLastTime = false;
        
        if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
          // 判断有无Source1
            if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
              // 有Source1就跳转到handle_msg
                goto handle_msg;
            }
        }
  // 通知 observers 即将进入休眠
    if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        __CFRunLoopSetSleeping(rl);
        __CFPortSetInsert(dispatchPort, waitSet);
          __CFRunLoopModeUnlock(rlm);
          __CFRunLoopUnlock(rl);

        CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent();

        if (kCFUseCollectableAllocator) {
            memset(msg_buffer, 0, sizeof(msg_buffer));
        }
      
        msg = (mach_msg_header_t *)msg_buffer;
      // 休眠,等待消息来唤醒线程
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);

        __CFRunLoopLock(rl);
        __CFRunLoopModeLock(rlm);

        rl->_sleepTime += (poll ? 0.0 : (CFAbsoluteTimeGetCurrent() - sleepStart));

        __CFPortSetRemove(dispatchPort, waitSet);
        
        __CFRunLoopSetIgnoreWakeUps(rl);

    __CFRunLoopUnsetSleeping(rl);
      //通知 observers RunLoop刚从休眠中唤醒
    if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting))  __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
 // 跳转标志 handle_msg
        handle_msg:;
        __CFRunLoopSetIgnoreWakeUps(rl);

        if (MACH_PORT_NULL == livePort) {
            CFRUNLOOP_WAKEUP_FOR_NOTHING();
            // handle nothing
        } else if (livePort == rl->_wakeUpPort) {
            CFRUNLOOP_WAKEUP_FOR_WAKEUP();
        }

#if USE_MK_TIMER_TOO
      // 被Timer唤醒
        else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
            CFRUNLOOP_WAKEUP_FOR_TIMER();
          //处理Timer
            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                // Re-arm the next timer
                __CFArmNextTimerInMode(rlm, rl);
            }
        }
#endif
        // 被GCD唤醒
        else if (livePort == dispatchPort) {
            CFRUNLOOP_WAKEUP_FOR_DISPATCH();
            __CFRunLoopModeUnlock(rlm);
            __CFRunLoopUnlock(rl);
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);
          // 处理GCD
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);
            __CFRunLoopLock(rl);
            __CFRunLoopModeLock(rlm);
            sourceHandledThisLoop = true;
            didDispatchPortLastTime = true;
        } else {
          // 被Source1唤醒
            CFRUNLOOP_WAKEUP_FOR_SOURCE();
            voucher_t previousVoucher = _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, (void *)voucherCopy, os_release);
            CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
            _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, previousVoucher, os_release);
            
        } 
    // 处理Block    
    __CFRunLoopDoBlocks(rl, rlm);
        
    // 处理返回值
    if (sourceHandledThisLoop && stopAfterHandle) {
     // 进入loop时参数标记为处理完事件就返回
        retVal = kCFRunLoopRunHandledSource;
  } else if (timeout_context->termTSR < mach_absolute_time()) {
     // 超出传入参数标记的超时时间
            retVal = kCFRunLoopRunTimedOut;
    } else if (__CFRunLoopIsStopped(rl)) {
     // 被外部调用者强行停止
            __CFRunLoopUnsetStopped(rl);
        retVal = kCFRunLoopRunStopped;
    } else if (rlm->_stopped) {
     // 自动停止
        rlm->_stopped = false;
        retVal = kCFRunLoopRunStopped;
    } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
     // mode为空,没有source0、source1、timer、observers
        retVal = kCFRunLoopRunFinished;
    }
        
    } while (0 == retVal);

    if (timeout_timer) {
        dispatch_source_cancel(timeout_timer);
        dispatch_release(timeout_timer);
    } else {
        free(timeout_context);
    }

    return retVal;
}

整体流程如下图所示。

事件循环机制

根据这张图可以看出:RunLoop在BeforeSources和AfterWaiting后会进行任务的处理。可以在此时阻塞监控线程并设置超时时间,若超时后RunLoop的状态仍为RunLoop在BeforeSources或AfterWaiting,表明此时RunLoop仍然在处理任务,主线程发生了卡顿。

- (void)beginMonitor {
    self.dispatchSemaphore = dispatch_semaphore_create(0);
    // 第一个监控,监控是否处于 运行状态
    CFRunLoopObserverContext context = {0, (__bridge void *) self, NULL, NULL, NULL};
    self.runLoopBeginObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                                        kCFRunLoopAllActivities,
                                                        YES,
                                                        LONG_MIN,
                                                        &myRunLoopBeginCallback,
                                                        &context);
    //  第二个监控,监控是否处于 睡眠状态
    self.runLoopEndObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                                      kCFRunLoopAllActivities,
                                                      YES,
                                                      LONG_MAX,
                                                      &myRunLoopEndCallback,
                                                      &context);
    CFRunLoopAddObserver(CFRunLoopGetMain(), self.runLoopBeginObserver, kCFRunLoopCommonModes);
    CFRunLoopAddObserver(CFRunLoopGetMain(), self.runLoopEndObserver, kCFRunLoopCommonModes);
    
    // 创建子线程监控
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        //子线程开启一个持续的loop用来进行监控
        while (YES) {
            long semaphoreWait = dispatch_semaphore_wait(self.dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 17 * NSEC_PER_MSEC));
            if (semaphoreWait != 0) {
                if (!self.runLoopBeginObserver || !self.runLoopEndObserver) {
                    self.timeoutCount = 0;
                    self.dispatchSemaphore = 0;
                    self.runLoopBeginActivity = 0;
                    self.runLoopEndActivity = 0;
                    return;
                }
                // 两个runloop的状态,BeforeSources和AfterWaiting这两个状态区间时间能够检测到是否卡顿
                if ((self.runLoopBeginActivity == kCFRunLoopBeforeSources || self.runLoopBeginActivity == kCFRunLoopAfterWaiting) ||
                    (self.runLoopEndActivity == kCFRunLoopBeforeSources || self.runLoopEndActivity == kCFRunLoopAfterWaiting)) {
                    // 出现三次出结果
                    if (++self.timeoutCount < 2) {
                        continue;
                    }
                    NSLog(@"调试:监测到卡顿");
                } // end activity
            }// end semaphore wait
            self.timeoutCount = 0;
        }// end while
    });
}

// 第一个监控,监控是否处于 运行状态
void myRunLoopBeginCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    RunLoopMonitor2* lagMonitor = (__bridge RunLoopMonitor2 *)info;
    lagMonitor.runLoopBeginActivity = activity;
    dispatch_semaphore_t semaphore = lagMonitor.dispatchSemaphore;
    dispatch_semaphore_signal(semaphore);
}

//  第二个监控,监控是否处于 睡眠状态
void myRunLoopEndCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    RunLoopMonitor2* lagMonitor = (__bridge RunLoopMonitor2 *)info;
    lagMonitor.runLoopEndActivity = activity;
    dispatch_semaphore_t semaphore = lagMonitor.dispatchSemaphore;
    dispatch_semaphore_signal(semaphore);
}

方案三:Ping主线程

Ping主线程的核心思想是向主线程发送一个信号,一定时间内收到了主线程的回复,即表示当前主线程流畅运行。没有收到主线程的回复,即表示当前主线程在做耗时运算,发生了卡顿。

目前昆虫线上使用的就是这套方案。

self.semaphore = dispatch_semaphore_create(0);
- (void)main {
    //判断是否需要上报
    __weak typeof(self) weakSelf = self;
    void (^ verifyReport)(void) = ^() {
        __strong typeof(weakSelf) strongSelf = weakSelf;
        if (strongSelf.reportInfo.length > 0) {
            if (strongSelf.handler) {
                double responseTimeValue = floor([[NSDate date] timeIntervalSince1970] * 1000);
                double duration = responseTimeValue - strongSelf.startTimeValue;
                if (DEBUG) {
                    NSLog(@"卡了%f,堆栈为--%@", duration, strongSelf.reportInfo);
                }
                strongSelf.handler(@{
                    @"title": [InsectUtil dateFormatNow].length > 0 ? [InsectUtil dateFormatNow] : @"",
                    @"duration": [NSString stringWithFormat:@"%.2f",duration],
                    @"content": strongSelf.reportInfo
                                   });
            }
            strongSelf.reportInfo = @"";
        }
    };
    
    while (!self.cancelled) {
        if (_isApplicationInActive) {
            self.mainThreadBlock = YES;
            self.reportInfo = @"";
            self.startTimeValue = floor([[NSDate date] timeIntervalSince1970] * 1000);
            dispatch_async(dispatch_get_main_queue(), ^{
                self.mainThreadBlock = NO;
                dispatch_semaphore_signal(self.semaphore);
            });
            [NSThread sleepForTimeInterval:(self.threshold/1000)];
            if (self.isMainThreadBlock) {
                self.reportInfo = [InsectBacktraceLogger insect_backtraceOfMainThread];
            }
            dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
            //卡顿超时情况;
            verifyReport();
        } else {
            [NSThread sleepForTimeInterval:(self.threshold/1000)];
        }
    }
}


总结

方案 优点 缺点 实现复杂性
FPS 直观 无法准确定位卡顿堆栈 简单
RunLoop Observer 能定位卡顿堆栈 不能记录卡顿时间,定义卡顿的阈值不好控制 复杂
Ping Main Thread 能定位卡顿堆栈,能记录卡顿时间 一直ping主线程,费电 中等
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • 最近在写APM相关的东西,所以整理了一下iOS中卡顿监测的那些方案,不了解卡顿的原理的可以看这篇文章iOS 保持界...
    小凉介阅读 2,417评论 0 18
  • 卡顿原因 图像的显示可以简单理解成先经过CPU的计算/排版/编解码等操作,然后交由GPU去完成渲染放入缓冲中,当视...
    肥猫记阅读 3,669评论 0 8
  • 1、FPS FPS (Frames Per Second) 是图像领域中的定义,表示每秒渲染帧数,通常用于衡量画面...
    雷霸龙阅读 1,364评论 0 13
  • 概要 RunLoop在iOS开发中的应用范围并没有像runtime 那样广泛,我们通过CFRuntime的源代码可...
    _羊羽_阅读 3,892评论 7 31
  • 参考文章:质量监控-卡顿检测圆角卡顿刨根问底iOS App 使用 GCD 导致的卡顿问题 APP出现卡顿不同于一般...
    LUJQ阅读 6,443评论 2 12