iOS底層原理-界面優化

界面優化無非就是解決卡頓問,優化界面流暢度,如下就經過先分析卡頓的緣由,而後再介紹具體的優化方案,來分析如何作界面優化微信

  • 界面渲染流程

    具體流程能夠參考圖片渲染初探這裏就大概講一下圖片渲染的流程,大致上能夠分爲三個階段就是 CPU處理階段 GPU處理階段和視頻控制器顯示階段。
    1. CPU主要是計算出須要渲染的模型數據
    2. GPU主要是根據 CPU提供的渲染模型數據渲染圖片而後存到幀緩衝區
    3. 視頻控制器衝幀緩衝區中讀取數據最後成像
    大體流程圖解以下:
    16d81697920a87d3.png
    蘋果爲了解決圖片撕裂的問題使用了 VSync + 雙緩衝區的形式,就是顯示器顯示完成一幀的渲染的時候會向 發送一個垂直信號 VSync,收到這個這個垂直信號以後顯示器開始讀取另一個幀緩衝區中的數據而 App接到垂直信號以後開始新一幀的渲染。
  • 卡頓原理

    經過上文張的界面渲染流程知道,在圖一幀渲染完成以後會發送一個垂直信號此時開始讀取另一個幀緩衝區中的數據,加入此時 CPUGPU的工做尚未完成,也就是另一個幀緩衝區仍是加鎖狀態沒有數據的時候,此時顯示器顯示的仍是上一幀的圖像那麼這種狀況就會一直等待下一幀繪製完成而後視頻控制器再讀取另一個幀緩衝區中的數據而後成像,中間這個等待的過程就形成了掉幀,也就是會卡頓。
    卡頓圖解以下:
    16d8169791d4b86e.png 這種狀況隨會形成卡頓
  • 卡頓檢測

    1. FPS監控
      蘋果的iPhone推薦的刷新率是60Hz,也就是每秒中刷新屏幕60次,也就是每秒中有60幀渲染完成,差很少每幀渲染的時間是1000/60 = 16.67毫秒整個界面會比較流暢,通常刷新率低於45Hz的就會出現明顯的卡頓現象。這裏能夠經過YYFPSLabel來實現FPS的監控,該原理主要是依靠 CADisplayLink來實現的,經過CADisplayLink來監聽每次屏幕刷新並獲取屏幕刷新的時間,而後使用次數(也就是1)除以每次刷新的時間間隔獲得FPS,具體源碼以下:
      #import "YYFPSLabel.h"
        #import "YYKit.h"
      
        #define kSize CGSizeMake(55, 20)
      
        @implementation YYFPSLabel {
            CADisplayLink *_link;
            NSUInteger _count;
            NSTimeInterval _lastTime;
            UIFont *_font;
            UIFont *_subFont;
      
            NSTimeInterval _llll;
        }
      
        - (instancetype)initWithFrame:(CGRect)frame {
            if (frame.size.width == 0 && frame.size.height == 0) {
                frame.size = kSize;
            }
            self = [super initWithFrame:frame];
      
            self.layer.cornerRadius = 5;
            self.clipsToBounds = YES;
            self.textAlignment = NSTextAlignmentCenter;
            self.userInteractionEnabled = NO;
            self.backgroundColor = [UIColor colorWithWhite:0.000 alpha:0.700];
      
            _font = [UIFont fontWithName:@"Menlo" size:14];
            if (_font) {
                _subFont = [UIFont fontWithName:@"Menlo" size:4];
            } else {
                _font = [UIFont fontWithName:@"Courier" size:14];
                _subFont = [UIFont fontWithName:@"Courier" size:4];
            }
      
            //YYWeakProxy 這裏使用了虛擬類來解決強引用問題
            _link = [CADisplayLink displayLinkWithTarget:[YYWeakProxy proxyWithTarget:self] selector:@selector(tick:)];
            [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
            return self;
        }
      
        - (void)dealloc {
            [_link invalidate];
        }
      
        - (CGSize)sizeThatFits:(CGSize)size {
            return kSize;
        }
      
        - (void)tick:(CADisplayLink *)link {
            if (_lastTime == 0) {
                _lastTime = link.timestamp;
                NSLog(@"sdf");
                return;
            }
      
            //次數
            _count++;
            //時間
            NSTimeInterval delta = link.timestamp - _lastTime;
            if (delta < 1) return;
            _lastTime = link.timestamp;
            float fps = _count / delta;
            _count = 0;
      
            CGFloat progress = fps / 60.0;
            UIColor *color = [UIColor colorWithHue:0.27 * (progress - 0.2) saturation:1 brightness:0.9 alpha:1];
      
            NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%d FPS",(int)round(fps)]];
            [text setColor:color range:NSMakeRange(0, text.length - 3)];
            [text setColor:[UIColor whiteColor] range:NSMakeRange(text.length - 3, 3)];
            text.font = _font;
            [text setFont:_subFont range:NSMakeRange(text.length - 4, 1)];
      
            self.attributedText = text;
        }
      
        @end
      複製代碼
      FPS只用在開發階段的輔助性的數值,由於他會頻繁喚醒 runloop若是 runloop在閒置的狀態被 CADisplayLink喚醒則會消耗性能。
    2. 經過RunLoop檢測卡頓
      經過監聽主線程 Runloop一次循環的時間來判斷是否卡頓,這裏須要配合使用 GCD的信號量來實現,設置初始化信號量爲0,而後開一個子線程等待信號量的觸發,也是就是在子線程的方法裏面調用 dispatch_semaphore_wait方法設置等待時間是1秒,而後主線程的 RunloopObserver回調方法中發送信號也就是調用 dispatch_semaphore_signal方法,此時時間能夠置爲0了,若是是等待時間超時則看此時的 Runloop的狀態是不是 kCFRunLoopBeforeSources或者是 kCFRunLoopAfterWaiting,若是在這兩個狀態下兩秒則說明有卡頓,詳細代碼以下:(代碼中也有相關的註釋)
      #import "LGBlockMonitor.h"
      
        @interface LGBlockMonitor (){
            CFRunLoopActivity activity;
        }
      
        @property (nonatomic, strong) dispatch_semaphore_t semaphore;
        @property (nonatomic, assign) NSUInteger timeoutCount;
      
        @end
      
        @implementation LGBlockMonitor
      
        + (instancetype)sharedInstance {
            static id instance = nil;
            static dispatch_once_t onceToken;
      
            dispatch_once(&onceToken, ^{
                instance = [[self alloc] init];
            });
            return instance;
        }
      
        - (void)start{
            [self registerObserver];
            [self startMonitor];
        }
      
        static void CallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
        {
            LGBlockMonitor *monitor = (__bridge LGBlockMonitor *)info;
            monitor->activity = activity;
            // 發送信號
            dispatch_semaphore_t semaphore = monitor->_semaphore;
            dispatch_semaphore_signal(semaphore);
        }
      
        - (void)registerObserver{
            CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
            //NSIntegerMax : 優先級最小
            CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                                                    kCFRunLoopAllActivities,
                                                                    YES,
                                                                    NSIntegerMax,
                                                                    &CallBack,
                                                                    &context);
            CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
        }
      
        - (void)startMonitor{
            // 建立信號c
            _semaphore = dispatch_semaphore_create(0);
            // 在子線程監控時長
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                while (YES)
                {
                    // 超時時間是 1 秒,沒有等到信號量,st 就不等於 0, RunLoop 全部的任務
                    // 沒有接收到信號底層會先對信號量進行減減操做,此時信號量就變成負數
                    // 因此開始進入等到,等達到了等待時間尚未收到信號則進行加加操做復原信號量
                    // 執行進入等待的方法dispatch_semaphore_wait會返回非0的數
                    // 收到信號的時候此時信號量是1  底層是減減操做,此時恰好等於0 因此直接返回0
                    long st = dispatch_semaphore_wait(self->_semaphore, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC));
                    if (st != 0)
                    {
                        if (self->activity == kCFRunLoopBeforeSources || self->activity == kCFRunLoopAfterWaiting)
                        {
                            //若是一直處於處理source0或者接受mach_port的狀態則說明runloop的此次循環尚未完成
                            if (++self->_timeoutCount < 2){
                                NSLog(@"timeoutCount==%lu",(unsigned long)self->_timeoutCount);
                                continue;
                            }
                            // 若是超過兩秒則說明卡頓了
                            // 一秒左右的衡量尺度 很大可能性連續來 避免大規模打印!
                            NSLog(@"檢測到超過兩次連續卡頓");
                        }
                    }
                    self->_timeoutCount = 0;
                }
            });
        }
      
      
      
        @end
      複製代碼
    3. 微信matrix
      此方案也是藉助 runloop實現的大致流程和方案三相同,不過微信加入了堆棧分析,可以定位到耗時的方法調用堆棧,因此須要準確的分析卡頓緣由能夠藉助微信matrix來分析卡頓。固然也能夠在方案2中使用 PLCrashReporter這個開源的第三方庫來獲取堆棧信息
    4. 滴滴DoraemonKit
      實現方案大概就是在子線程中一直 ping主線程,在主線程卡頓的狀況下,會出現斷在的無響應的表現,進而檢測卡頓
  • 優化方案

    上文中分析卡頓的緣由咱們知道主要就是在 CPUGPU階段佔用時間太長致使了掉幀卡頓,因此界面優化主要工做就是給 CPUGPU減負
    • 預排版
      預排版主要是對 CPU進行減負。
      假設如今又個 TableView其中須要根據每一個 cell的內容來定 cell的高度。咱們知道 TableView有重用機制,若是複用池中有數據,即將滑入屏內的 cell就會使用複用池內的 cell,作到節省資源,可是仍是要根據新數據的內容來計算 cell的高度,從新佈局新 cell中內容的佈局 ,這樣反覆滑動 TableView相同的 cell就會反覆計算其 frame,這樣也給 CPU帶來了負擔。若是在獲得數據建立模型的時候就把 cell frame算出,TableView返回模型中的 frame這樣的話一樣的一條 cell就算來回反覆滑動 TableView,計算 frame這個操做也就僅僅只會執行一次,因此也就作到了減負的功能,以下圖:一個 cell的組成須要 modal找到數據,也須要 layout找到這個 cell如何佈局: 未命名文件(41).png
    • 預解碼 & 預渲染
      圖片的渲染流程,在 CPU階段拿到圖片的頂點數據和紋理以後會進行解碼生產位圖,而後傳遞到 GPU進行渲染主要流程圖以下 image.png 若是圖片不少很大的狀況下解碼工做就會佔用主線程 RunLoop致使其餘工做沒法執行好比滑動,這樣就會形成卡頓現象,因此這裏就能夠將解碼的工做放到異步線程中不佔用主線程,可能有人會想只要將圖片加載放到異步線程中在異步線程中生成一個 UIImage或者是 CGImage而後再主線程中設置給 UIImageView,此時能夠寫段代碼使用 instrumentsTime Profiler查看一下堆棧信息 image.png 發現圖片的編解碼仍是在主線程。 針對這種問題常見的作法是在子線程中先將圖片繪製到CGBitmapContext,而後從Bitmap 直接建立圖片,例如SDWebImage三方框架中對圖片編解碼的處理。這就是Image的預解碼,代碼以下:
      dispatch_async(queue, ^{
           CGImageRef cgImage = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:self]]].CGImage;
           CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(cgImage) & kCGBitmapAlphaInfoMask;
      
           BOOL hasAlpha = NO;
           if (alphaInfo == kCGImageAlphaPremultipliedLast ||
               alphaInfo == kCGImageAlphaPremultipliedFirst ||
               alphaInfo == kCGImageAlphaLast ||
               alphaInfo == kCGImageAlphaFirst) {
               hasAlpha = YES;
           }
      
           CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
           bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
      
           size_t width = CGImageGetWidth(cgImage);
           size_t height = CGImageGetHeight(cgImage);
      
           CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, CGColorSpaceCreateDeviceRGB(), bitmapInfo);
           CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage);
           cgImage = CGBitmapContextCreateImage(context);
      
           UIImage * image = [[UIImage imageWithCGImage:cgImage] cornerRadius:width * 0.5];
           CGContextRelease(context);
           CGImageRelease(cgImage);
           completion(image);
       });
      複製代碼
    • 按需加載
      顧名思義須要顯示的加載出來,不須要顯示的加載,例如 TableView中的圖片滑動的時候不加載,在滑動中止的時候加載(可使用Runloop,圖片繪製設置 defaultModal就行)
    • 異步渲染
      再說異步渲染以前先了解一下 UIViewCALayer的關係:
      1. UIView是基於 UIKit框架的,可以接受點擊事件,處理用戶的觸摸事件,並管理子視圖
      2. CALayer是基於 CoreAnimation,而CoreAnimation是基於QuartzCode的。因此CALayer只負責顯示,不能處理用戶的觸摸事件
      3. UIView是直接繼承 UIResponder的,CALayer是繼承 NSObject
      4. UIVIew 的主要職責是負責接收並響應事件;而 CALayer 的主要職責是負責顯示 UIUIView 依賴於 CALayer 得以顯示
      總結:UIView主要負責時間處理,CALayer主要是視圖顯示 異步渲染的原理其實也就是在子線程將全部的視圖繪製成一張位圖,而後回到主線程賦值給 layercontents,例如 Graver框架的異步渲染流程以下:
      image.png 核心源碼以下:
      if (drawingFinished && targetDrawingCount == layer.drawingCount)
        {
            CGImageRef CGImage = context ? CGBitmapContextCreateImage(context) : NULL;
            {
                // 讓 UIImage 進行內存管理
                // 最終生成的位圖  
                UIImage *image = CGImage ? [UIImage imageWithCGImage:CGImage] : nil;
                void (^finishBlock)(void) = ^{
                    // 因爲block可能在下一runloop執行,再進行一次檢查
                    if (targetDrawingCount != layer.drawingCount)
                    {
                        failedBlock();
                        return;
                    }
                    //主線程中賦值完成顯示
                    layer.contents = (id)image.CGImage;
                    // ...
                }
                if (drawInBackground) dispatch_async(dispatch_get_main_queue(), finishBlock);
                else finishBlock();
            }
      
            // 一些清理工做: release CGImageRef, Image context ending
        }
      
      複製代碼
      最終效果圖以下:
      image.png 也可使用 YYAsyncLayer
    • 其餘
      1. 減小圖層的層級
      2. 減小離屏渲染
      3. 圖片顯示的話圖片的大小設置(不要太大)
      4. 少使用addViewcell動態添加view
      5. 儘可能避免使用透明view,由於使用透明view,會致使在GPU中計算像素時,會將透明view下層圖層的像素也計算進來,即顏色混合處理(當有兩個圖層的時候一個是半透明一個是不透明若是半透明的層級更高的話此時就會觸發顏色混合,底層的混合並非僅僅的將兩個圖層疊加而是會將兩股顏色混合計算出新的色值顯示在屏幕中)
相關文章
相關標籤/搜索