<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          iOS 底層原理:界面優(yōu)化

          共 16509字,需瀏覽 34分鐘

           ·

          2021-09-05 20:38

          ????關(guān)注后回復(fù) “進(jìn)群” ,拉你進(jìn)程序員交流群????


          界面優(yōu)化無(wú)非就是解決卡頓問(wèn),優(yōu)化界面流暢度,以下就通過(guò)先分析卡頓的原因,然后再介紹具體的優(yōu)化方案,來(lái)分析如何做界面優(yōu)化

          • 界面渲染流程

            具體流程可以參考圖片渲染初探[1]這里就大概講一下圖片渲染的流程,大體上可以分為三個(gè)階段就是 CPU處理階段 GPU處理階段和視頻控制器顯示階段。

            大致流程圖解如下:

            蘋果為了解決圖片撕裂的問(wèn)題使用了 VSync + 雙緩沖區(qū)的形式,就是顯示器顯示完成一幀的渲染的時(shí)候會(huì)向 發(fā)送一個(gè)垂直信號(hào) VSync,收到這個(gè)這個(gè)垂直信號(hào)之后顯示器開(kāi)始讀取另外一個(gè)幀緩沖區(qū)中的數(shù)據(jù)而 App接到垂直信號(hào)之后開(kāi)始新一幀的渲染。

            1. CPU主要是計(jì)算出需要渲染的模型數(shù)據(jù)
            2. GPU主要是根據(jù) CPU提供的渲染模型數(shù)據(jù)渲染圖片然后存到幀緩沖區(qū)
            3. 視頻控制器沖幀緩沖區(qū)中讀取數(shù)據(jù)最后成像
          • 卡頓原理

            通過(guò)上文張的界面渲染流程知道,在圖一幀渲染完成之后會(huì)發(fā)送一個(gè)垂直信號(hào)此時(shí)開(kāi)始讀取另外一個(gè)幀緩沖區(qū)中的數(shù)據(jù),加入此時(shí) CPUGPU的工作還沒(méi)有完成,也就是另外一個(gè)幀緩沖區(qū)還是加鎖狀態(tài)沒(méi)有數(shù)據(jù)的時(shí)候,此時(shí)顯示器顯示的還是上一幀的圖像那么這種情況就會(huì)一直等待下一幀繪制完成然后視頻控制器再讀取另外一個(gè)幀緩沖區(qū)中的數(shù)據(jù)然后成像,中間這個(gè)等待的過(guò)程就造成了掉幀,也就是會(huì)卡頓。
            卡頓圖解如下:
            這種情況隨會(huì)造成卡頓

          • 卡頓檢測(cè)

          1. FPS監(jiān)控
          蘋果的iPhone推薦的刷新率是60Hz,也就是每秒中刷新屏幕60次,也就是每秒中有60幀渲染完成,差不多每幀渲染的時(shí)間是1000/60 = 16.67毫秒整個(gè)界面會(huì)比較流暢,一般刷新率低于45Hz的就會(huì)出現(xiàn)明顯的卡頓現(xiàn)象。這里可以通過(guò)YYFPSLabel來(lái)實(shí)現(xiàn)FPS的監(jiān)控,該原理主要是依靠 CADisplayLink來(lái)實(shí)現(xiàn)的,通過(guò)CADisplayLink來(lái)監(jiān)聽(tīng)每次屏幕刷新并獲取屏幕刷新的時(shí)間,然后使用次數(shù)(也就是1)除以每次刷新的時(shí)間間隔得到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 這里使用了虛擬類來(lái)解決強(qiáng)引用問(wèn)題
            _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;
            }

            //次數(shù)
            _count++;
            //時(shí)間
            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只用在開(kāi)發(fā)階段的輔助性的數(shù)值,因?yàn)樗麜?huì)頻繁喚醒 runloop如果 runloop在閑置的狀態(tài)被 CADisplayLink喚醒則會(huì)消耗性能。

          1. 通過(guò)RunLoop檢測(cè)卡頓

          通過(guò)監(jiān)聽(tīng)主線程 Runloop一次循環(huán)的時(shí)間來(lái)判斷是否卡頓,這里需要配合使用 GCD的信號(hào)量來(lái)實(shí)現(xiàn),設(shè)置初始化信號(hào)量為0,然后開(kāi)一個(gè)子線程等待信號(hào)量的觸發(fā),也是就是在子線程的方法里面調(diào)用 dispatch_semaphore_wait方法設(shè)置等待時(shí)間是1秒,然后主線程的 RunloopObserver回調(diào)方法中發(fā)送信號(hào)也就是調(diào)用 dispatch_semaphore_signal方法,此時(shí)時(shí)間可以置為0了,如果是等待時(shí)間超時(shí)則看此時(shí)的 Runloop的狀態(tài)是否是 kCFRunLoopBeforeSources或者是 kCFRunLoopAfterWaiting,如果在這兩個(gè)狀態(tài)下兩秒則說(shuō)明有卡頓,詳細(xì)代碼如下:(代碼中也有相關(guān)的注釋)

          #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;
            // 發(fā)送信號(hào)
            dispatch_semaphore_t semaphore = monitor->_semaphore;
            dispatch_semaphore_signal(semaphore);
          }

          - (void)registerObserver{
            CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
            //NSIntegerMax : 優(yōu)先級(jí)最小
            CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                                                    kCFRunLoopAllActivities,
                                                                    YES,
                                                                    NSIntegerMax,
                                                                    &CallBack,
                                                                    &context);
            CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
          }

          - (void)startMonitor{
            // 創(chuàng)建信號(hào)c
            _semaphore = dispatch_semaphore_create(0);
            // 在子線程監(jiān)控時(shí)長(zhǎng)
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                while (YES)
                {
                    // 超時(shí)時(shí)間是 1 秒,沒(méi)有等到信號(hào)量,st 就不等于 0, RunLoop 所有的任務(wù)
                    // 沒(méi)有接收到信號(hào)底層會(huì)先對(duì)信號(hào)量進(jìn)行減減操作,此時(shí)信號(hào)量就變成負(fù)數(shù)
                    // 所以開(kāi)始進(jìn)入等到,等達(dá)到了等待時(shí)間還沒(méi)有收到信號(hào)則進(jìn)行加加操作復(fù)原信號(hào)量
                    // 執(zhí)行進(jìn)入等待的方法dispatch_semaphore_wait會(huì)返回非0的數(shù)
                    // 收到信號(hào)的時(shí)候此時(shí)信號(hào)量是1  底層是減減操作,此時(shí)剛好等于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的狀態(tài)則說(shuō)明runloop的這次循環(huán)還沒(méi)有完成
                            if (++self->_timeoutCount < 2){
                                NSLog(@"timeoutCount==%lu",(unsigned long)self->_timeoutCount);
                                continue;
                            }
                            // 如果超過(guò)兩秒則說(shuō)明卡頓了
                            // 一秒左右的衡量尺度 很大可能性連續(xù)來(lái) 避免大規(guī)模打印!
                            NSLog(@"檢測(cè)到超過(guò)兩次連續(xù)卡頓");
                        }
                    }
                    self->_timeoutCount = 0;
                }
            });
          }



          @end
          1. 微信matrix

          此方案也是借助 runloop實(shí)現(xiàn)的大體流程和方案三相同,不過(guò)微信加入了堆棧分析,能夠定位到耗時(shí)的方法調(diào)用堆棧,所以需要準(zhǔn)確的分析卡頓原因可以借助微信matrix來(lái)分析卡頓。當(dāng)然也可以在方案2中使用 PLCrashReporter這個(gè)開(kāi)源的第三方庫(kù)來(lái)獲取堆棧信息

          1. 滴滴DoraemonKit

          實(shí)現(xiàn)方案大概就是在子線程中一直 ping主線程,在主線程卡頓的情況下,會(huì)出現(xiàn)斷在的無(wú)響應(yīng)的表現(xiàn),進(jìn)而檢測(cè)卡頓

          • 優(yōu)化方案

            上文中分析卡頓的原因我們知道主要就是在 CPUGPU階段占用時(shí)間太長(zhǎng)導(dǎo)致了掉幀卡頓,所以界面優(yōu)化主要工作就是給 CPUGPU減負(fù)

            • 預(yù)排版

              預(yù)排版主要是對(duì) CPU進(jìn)行減負(fù)。
              假設(shè)現(xiàn)在又個(gè) TableView其中需要根據(jù)每個(gè) cell的內(nèi)容來(lái)定 cell的高度。我們知道 TableView有重用機(jī)制,如果復(fù)用池中有數(shù)據(jù),即將滑入屏內(nèi)的 cell就會(huì)使用復(fù)用池內(nèi)的 cell,做到節(jié)省資源,但是還是要根據(jù)新數(shù)據(jù)的內(nèi)容來(lái)計(jì)算 cell的高度,重新布局新 cell中內(nèi)容的布局 ,這樣反復(fù)滑動(dòng) TableView相同的 cell就會(huì)反復(fù)計(jì)算其 frame,這樣也給 CPU帶來(lái)了負(fù)擔(dān)。如果在得到數(shù)據(jù)創(chuàng)建模型的時(shí)候就把 cell frame算出,TableView返回模型中的 frame這樣的話同樣的一條 cell就算來(lái)回反復(fù)滑動(dòng) TableView,計(jì)算 frame這個(gè)操作也就僅僅只會(huì)執(zhí)行一次,所以也就做到了減負(fù)的功能,如下圖:一個(gè) cell的組成需要 modal找到數(shù)據(jù),也需要 layout找到這個(gè) cell如何布局:

            • 預(yù)解碼 & 預(yù)渲染

              圖片的渲染流程,在 CPU階段拿到圖片的頂點(diǎn)數(shù)據(jù)和紋理之后會(huì)進(jìn)行解碼生產(chǎn)位圖,然后傳遞到 GPU進(jìn)行渲染主要流程圖如下 如果圖片很多很大的情況下解碼工作就會(huì)占用主線程 RunLoop導(dǎo)致其他工作無(wú)法執(zhí)行比如滑動(dòng),這樣就會(huì)造成卡頓現(xiàn)象,所以這里就可以將解碼的工作放到異步線程中不占用主線程,可能有人會(huì)想只要將圖片加載放到異步線程中在異步線程中生成一個(gè) UIImage或者是 CGImage然后再主線程中設(shè)置給 UIImageView,此時(shí)可以寫段代碼使用 instrumentsTime Profiler查看一下堆棧信息 發(fā)現(xiàn)圖片的編解碼還是在主線程。針對(duì)這種問(wèn)題常見(jiàn)的做法是在子線程中先將圖片繪制到CGBitmapContext,然后從Bitmap 直接創(chuàng)建圖片,例如SDWebImage三方框架中對(duì)圖片編解碼的處理。這就是Image的預(yù)解碼,代碼如下:

              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);
              });
            • 按需加載

              顧名思義需要顯示的加載出來(lái),不需要顯示的加載,例如 TableView中的圖片滑動(dòng)的時(shí)候不加載,在滑動(dòng)停止的時(shí)候加載(可以使用Runloop,圖片繪制設(shè)置 defaultModal就行)

            • 異步渲染

              再說(shuō)異步渲染之前先了解一下 UIViewCALayer的關(guān)系:

            1. UIView是基于 UIKit框架的,能夠接受點(diǎn)擊事件,處理用戶的觸摸事件,并管理子視圖
            2. CALayer是基于 CoreAnimation,而CoreAnimation是基于QuartzCode的。所以CALayer只負(fù)責(zé)顯示,不能處理用戶的觸摸事件
            3. UIView是直接繼承 UIResponder的,CALayer是繼承 NSObject
            4. UIVIew 的主要職責(zé)是負(fù)責(zé)接收并響應(yīng)事件;而 CALayer 的主要職責(zé)是負(fù)責(zé)顯示 UIUIView 依賴于 CALayer 得以顯示

            總結(jié):UIView主要負(fù)責(zé)時(shí)間處理,CALayer主要是視圖顯示 異步渲染的原理其實(shí)也就是在子線程將所有的視圖繪制成一張位圖,然后回到主線程賦值給 layercontents,例如 Graver框架的異步渲染流程如下:
            核心源碼如下:

            if (drawingFinished && targetDrawingCount == layer.drawingCount)
            {
              CGImageRef CGImage = context ? CGBitmapContextCreateImage(context) : NULL;
              {
                  // 讓 UIImage 進(jìn)行內(nèi)存管理
                  // 最終生成的位圖  
                  UIImage *image = CGImage ? [UIImage imageWithCGImage:CGImage] : nil;
                  void (^finishBlock)(void) = ^{
                      // 由于block可能在下一runloop執(zhí)行,再進(jìn)行一次檢查
                      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
            }


            最終效果圖如下:
            也可以使用 YYAsyncLayer

            • 其他
            1. 減少圖層的層級(jí)
            2. 減少離屏渲染
            3. 圖片顯示的話圖片的大小設(shè)置(不要太大)
            4. 少使用addViewcell動(dòng)態(tài)添加view
            5. 盡量避免使用透明view,因?yàn)槭褂猛该?code style="font-size: 14px;font-family: 'Operator Mono', Consolas, Monaco, Menlo, monospace;word-break: break-all;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(233, 105, 0);background: rgb(248, 248, 248);">view,會(huì)導(dǎo)致在GPU中計(jì)算像素時(shí),會(huì)將透明view下層圖層的像素也計(jì)算進(jìn)來(lái),即顏色混合處理(當(dāng)有兩個(gè)圖層的時(shí)候一個(gè)是半透明一個(gè)是不透明如果半透明的層級(jí)更高的話此時(shí)就會(huì)觸發(fā)顏色混合,底層的混合并不是僅僅的將兩個(gè)圖層疊加而是會(huì)將兩股顏色混合計(jì)算出新的色值顯示在屏幕中)

          參考資料

          [1]

          https://juejin.cn/post/6847902220403343374: https://juejin.cn/post/6847902220403343374

          轉(zhuǎn)自:掘金 Potato_土豆

          https://juejin.cn/post/6977666830114816030

          -End-

          最近有一些小伙伴,讓我?guī)兔φ乙恍?nbsp;面試題 資料,于是我翻遍了收藏的 5T 資料后,匯總整理出來(lái),可以說(shuō)是程序員面試必備!所有資料都整理到網(wǎng)盤了,歡迎下載!

          點(diǎn)擊??卡片,關(guān)注后回復(fù)【面試題】即可獲取

          在看點(diǎn)這里好文分享給更多人↓↓

          瀏覽 44
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  超碰操网 | 成人手机看片 | 欧美性掹交xxx | 精品无码免费一区二区三区 | 色婷婷在线无码精品秘 人口传媒 |