iOS開發(fā)中的卡頓分析

市面上的iOS卡頓分析方案有三種:監(jiān)控FPS养葵、監(jiān)控RunLoop心剥、ping主線程邦尊。

方案一:監(jiān)控FPS

一般來說硼控,我們約定60FPS即為流暢。那么反過來胳赌,如果App在運行期間出現(xiàn)了掉幀牢撼,即可認為出現(xiàn)了卡頓。

監(jiān)控FPS的方案幾乎都是基于CADisplayLink實現(xiàn)的疑苫。簡單介紹一下CADisplayLink:CADisplayLink是一個和屏幕刷新率保持一致的定時器熏版,一但 CADisplayLink 以特定的模式注冊到runloop之后,每當屏幕需要刷新的時候捍掺,runloop就會調(diào)用CADisplayLink綁定的target上的selector撼短。
可以通過向RunLoop中添加CADisplayLink,根據(jù)其回調(diào)來計算出當前畫面的幀數(shù)挺勿。

#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下降了,說明頁面的某處有性能問題不瓶。壞處就是只知道這是頁面的某處禾嫉,不能準確定位到具體的堆棧。


方案二:監(jiān)控RunLoop

首先來介紹下什么是RunLoop蚊丐。RunLoop是維護其內(nèi)部事件循環(huán)的一個對象熙参,它在程序運行過程中重復的做著一些事情,例如接收消息麦备、處理消息孽椰、休眠等等。

所謂的事件循環(huán)凛篙,就是對事件/消息進行管理黍匾,沒有消息時,休眠線程以避免資源消耗呛梆,從用戶態(tài)切換到內(nèi)核態(tài)锐涯。

有事件/消息需要進行處理時,立即喚醒線程削彬,回到用戶態(tài)進行處理全庸。

#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函數(shù)內(nèi)部會啟動主線程的RunLoop,使得iOS程序持續(xù)運行融痛。

iOS系統(tǒng)中有兩套API來使用RunLoop壶笼,NSRunLoop(CFRunLoopRef的封裝)和CFRunLoopRef。Foundation框架是不開源的雁刷,可以通過開源的CoreFoundation來分析RunLoop內(nèi)部實現(xiàn)覆劈。

點此下載CoreFoundation

RunLoop對象底層就是一個CFRunLoopRef結構體,內(nèi)部數(shù)據(jù)如下:

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負責捕獲系統(tǒng)事件白筹,并將事件交由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:方法會創(chuàng)建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用于監(jiān)聽RunLoop的六種狀態(tài)系馆。CFRunLoopObserver中的_activities用于保存RunLoop的活動狀態(tài),當狀態(tài)發(fā)生改變時顽照,通過回調(diào)函數(shù)_callout函數(shù)通知所有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  // 以上所有狀態(tài)
};
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循環(huán)代兵,下面來看看循環(huán)中具體干了哪些事情尼酿。

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
    __CFRunLoopLock(rl);
    //根據(jù)modeName來查找本次運行的mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    // 如果沒找到mode 或者 mode里沒有任何的事件,就此停止奢人,不再循環(huán)
    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();

    // 狀態(tài)判斷
    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時參數(shù)標記為處理完事件就返回
        retVal = kCFRunLoopRunHandledSource;
  } else if (timeout_context->termTSR < mach_absolute_time()) {
     // 超出傳入?yún)?shù)標記的超時時間
            retVal = kCFRunLoopRunTimedOut;
    } else if (__CFRunLoopIsStopped(rl)) {
     // 被外部調(diào)用者強行停止
            __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;
}

整體流程如下圖所示拷淘。

事件循環(huán)機制

根據(jù)這張圖可以看出:RunLoop在BeforeSources和AfterWaiting后會進行任務的處理各墨。可以在此時阻塞監(jiān)控線程并設置超時時間启涯,若超時后RunLoop的狀態(tài)仍為RunLoop在BeforeSources或AfterWaiting贬堵,表明此時RunLoop仍然在處理任務,主線程發(fā)生了卡頓结洼。

- (void)beginMonitor {
    self.dispatchSemaphore = dispatch_semaphore_create(0);
    // 第一個監(jiān)控黎做,監(jiān)控是否處于 運行狀態(tài)
    CFRunLoopObserverContext context = {0, (__bridge void *) self, NULL, NULL, NULL};
    self.runLoopBeginObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                                        kCFRunLoopAllActivities,
                                                        YES,
                                                        LONG_MIN,
                                                        &myRunLoopBeginCallback,
                                                        &context);
    //  第二個監(jiān)控,監(jiān)控是否處于 睡眠狀態(tài)
    self.runLoopEndObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                                      kCFRunLoopAllActivities,
                                                      YES,
                                                      LONG_MAX,
                                                      &myRunLoopEndCallback,
                                                      &context);
    CFRunLoopAddObserver(CFRunLoopGetMain(), self.runLoopBeginObserver, kCFRunLoopCommonModes);
    CFRunLoopAddObserver(CFRunLoopGetMain(), self.runLoopEndObserver, kCFRunLoopCommonModes);
    
    // 創(chuàng)建子線程監(jiān)控
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        //子線程開啟一個持續(xù)的loop用來進行監(jiān)控
        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的狀態(tài)松忍,BeforeSources和AfterWaiting這兩個狀態(tài)區(qū)間時間能夠檢測到是否卡頓
                if ((self.runLoopBeginActivity == kCFRunLoopBeforeSources || self.runLoopBeginActivity == kCFRunLoopAfterWaiting) ||
                    (self.runLoopEndActivity == kCFRunLoopBeforeSources || self.runLoopEndActivity == kCFRunLoopAfterWaiting)) {
                    // 出現(xiàn)三次出結果
                    if (++self.timeoutCount < 2) {
                        continue;
                    }
                    NSLog(@"調(diào)試:監(jiān)測到卡頓");
                } // end activity
            }// end semaphore wait
            self.timeoutCount = 0;
        }// end while
    });
}

// 第一個監(jiān)控蒸殿,監(jiān)控是否處于 運行狀態(tài)
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);
}

//  第二個監(jiān)控,監(jiān)控是否處于 睡眠狀態(tài)
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主線程的核心思想是向主線程發(fā)送一個信號,一定時間內(nèi)收到了主線程的回復宏所,即表示當前主線程流暢運行酥艳。沒有收到主線程的回復,即表示當前主線程在做耗時運算爬骤,發(fā)生了卡頓充石。

目前昆蟲線上使用的就是這套方案。

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)];
        }
    }
}


總結

方案 優(yōu)點 缺點 實現(xiàn)復雜性
FPS 直觀 無法準確定位卡頓堆棧 簡單
RunLoop Observer 能定位卡頓堆棧 不能記錄卡頓時間霞玄,定義卡頓的閾值不好控制 復雜
Ping Main Thread 能定位卡頓堆棧骤铃,能記錄卡頓時間 一直ping主線程,費電 中等
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末溃列,一起剝皮案震驚了整個濱河市劲厌,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌听隐,老刑警劉巖补鼻,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異雅任,居然都是意外死亡风范,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門沪么,熙熙樓的掌柜王于貴愁眉苦臉地迎上來硼婿,“玉大人,你說我怎么就攤上這事禽车】苈” “怎么了?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵殉摔,是天一觀的道長州胳。 經(jīng)常有香客問我,道長逸月,這世上最難降的妖魔是什么栓撞? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮碗硬,結果婚禮上瓤湘,老公的妹妹穿的比我還像新娘。我一直安慰自己恩尾,他們只是感情好弛说,可當我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著特笋,像睡著了一般剃浇。 火紅的嫁衣襯著肌膚如雪巾兆。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天虎囚,我揣著相機與錄音角塑,去河邊找鬼。 笑死淘讥,一個胖子當著我的面吹牛圃伶,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蒲列,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼窒朋,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蝗岖?” 一聲冷哼從身側響起侥猩,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎抵赢,沒想到半個月后欺劳,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡铅鲤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年划提,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片邢享。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡鹏往,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出骇塘,到底是詐尸還是另有隱情伊履,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布款违,位于F島的核電站湾碎,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏奠货。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一座掘、第九天 我趴在偏房一處隱蔽的房頂上張望递惋。 院中可真熱鬧,春花似錦溢陪、人聲如沸萍虽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽杉编。三九已至超全,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間邓馒,已是汗流浹背嘶朱。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留光酣,地道東北人疏遏。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像救军,于是被迫代替她去往敵國和親财异。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 最近在寫APM相關的東西唱遭,所以整理了一下iOS中卡頓監(jiān)測的那些方案戳寸,不了解卡頓的原理的可以看這篇文章iOS 保持界...
    小涼介閱讀 2,449評論 0 18
  • 卡頓原因 圖像的顯示可以簡單理解成先經(jīng)過CPU的計算/排版/編解碼等操作,然后交由GPU去完成渲染放入緩沖中拷泽,當視...
    肥貓記閱讀 3,688評論 0 8
  • 1疫鹊、FPS FPS (Frames Per Second) 是圖像領域中的定義,表示每秒渲染幀數(shù)跌穗,通常用于衡量畫面...
    雷霸龍閱讀 1,373評論 0 13
  • 概要 RunLoop在iOS開發(fā)中的應用范圍并沒有像runtime 那樣廣泛订晌,我們通過CFRuntime的源代碼可...
    _羊羽_閱讀 3,896評論 7 31
  • 參考文章:質(zhì)量監(jiān)控-卡頓檢測圓角卡頓刨根問底iOS App 使用 GCD 導致的卡頓問題 APP出現(xiàn)卡頓不同于一般...
    LUJQ閱讀 6,478評論 2 12