Graver源碼閱讀

前言

Graver 是一款高效的 UI 渲染框架稳吮,它以更低的資源消耗來構(gòu)建十分流暢的 UI 界面殉摔。Graver 獨(dú)創(chuàng)性的采用了基于繪制的視覺元素分解方式來構(gòu)建界面凄贩,得益于此淑玫,該框架能讓 UI 渲染過程變得更加簡(jiǎn)單、靈活捆毫。目前,該框架已經(jīng)在美團(tuán) App 的外賣頻道冲甘、獨(dú)立外賣 App 核心業(yè)務(wù)場(chǎng)景的大多數(shù)業(yè)務(wù)中進(jìn)行了應(yīng)用绩卤,同時(shí)感謝美團(tuán)團(tuán)隊(duì)開源了該優(yōu)秀的框架。

背景

項(xiàng)目中雖然卡片列表FPS江醇、CPU濒憋、Memory 等方面的各項(xiàng)指標(biāo)還算過的去,但是在涉及大量圖表的頁面中還是存在滑動(dòng)不流暢的情況陶夜,正好美團(tuán)技術(shù)團(tuán)隊(duì)開源了Graver項(xiàng)目凛驮,在閱讀了Graver源碼后,雖然沒法拿來直接在項(xiàng)目中使用去替換掉頁面的元素条辟,但是根據(jù)思路修改了自定義一些圖表的繪制(大量的折線圖黔夭,餅圖,及復(fù)雜元素層級(jí))羽嫡,在經(jīng)過一段時(shí)間將項(xiàng)目中部分模塊修改為異步繪制后本姥,大幅度提高了前端用戶的體驗(yàn)感覺。

開始

該片文章只要對(duì)WMGCanvasView杭棵、WMGAsyncDrawView兩個(gè)基礎(chǔ)類的源碼進(jìn)行每一步的詳解來總結(jié)Graver異步繪制的思想婚惫,在開始之前先說兩個(gè)地方

Graver 渲染原理

繪制原理.png

這也是一個(gè)最典型的卡片列表頁的一個(gè)流程大部分是沒有繪制隊(duì)列這一個(gè)隊(duì)列也就是將預(yù)排版好的數(shù)據(jù)直接在主隊(duì)列中完成繪制,而Graver在繪制隊(duì)列中將預(yù)排版的數(shù)據(jù)直接將位圖畫好傳回給主隊(duì)列直接進(jìn)行展示,不需要在有層級(jí)的一層層疊加先舷,也就是大幅度減少了層級(jí)過多有帶來的卡頓艰管。

CALayer 繪制的大概流程

當(dāng) CALayer 需要繪制 UI 的時(shí)候,會(huì)查看 layer 的代理是否實(shí)現(xiàn)了 - (void)displayLayer:(CALayer *)layer; 方法蒋川,如果實(shí)現(xiàn)了蛙婴,就進(jìn)入用戶自定義繪制流程
如果沒有實(shí)現(xiàn)則進(jìn)入系統(tǒng)繪制流程
系統(tǒng)繪制流程,就會(huì)查看 layer 的代理 - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx; 尔破, 并且調(diào)用 UIView 的 - (void)drawRect:(CGRect)rect 方法街图。
Graver實(shí)現(xiàn)了displayLayer的代理直接進(jìn)行自定義的繪制

WMGCanvasView

WMGCanvasView是Graver 提供的一個(gè)做基礎(chǔ)的畫布View類作用與UIImageView基本相同,因?yàn)楣δ鼙容^單一所以可以先通過該類看下基本的異步繪制流程懒构。

在WMGCanvasView.h中直接將操作layer的屬性暴露出來方便使用

@property (nonatomic, assign) CGFloat cornerRadius;
@property (nonatomic, assign) CGFloat borderWidth;
@property (nonatomic, strong) UIColor *borderColor;
@property (nonatomic, strong) UIColor *shadowColor;
@property (nonatomic, assign) UIOffset shadowOffset;
@property (nonatomic, assign) CGFloat shadowBlur;
@property (nonatomic, strong) UIImage *backgroundImage;

WMGCanvasView.m中的方法只有6個(gè)

//初始化
- (id)initWithFrame:(CGRect)frame
//背景顏色相關(guān)
- (void)setBackgroundColor:(UIColor *)backgroundColor
- (UIColor *)backgroundColor
//背景圖片
- (void)setBackgroundImage:(UIImage *)backgroundImage
//配置數(shù)據(jù)
- (NSDictionary *)currentDrawingUserInfo
//繪制方法
- (BOOL)drawInRect:(CGRect)rect withContext:(CGContextRef)context asynchronously:(BOOL)asynchronously userInfo:(NSDictionary *)userInfo


初始化方法中有個(gè)drawingPolicy

 self.drawingPolicy = WMGViewDrawingPolicyAsynchronouslyDrawWhenContentsChanged;

drawingPolicy是一個(gè)關(guān)于是否異步繪制的枚舉

typedef NS_ENUM(NSInteger, WMGViewDrawingPolicy)
{
    // 當(dāng) contentsChangedAfterLastAsyncDrawing 為 YES 時(shí)異步繪制
    WMGViewDrawingPolicyAsynchronouslyDrawWhenContentsChanged,
    // 同步繪制
    WMGViewDrawingPolicySynchronouslyDraw,
    // 異步繪制
    WMGViewDrawingPolicyAsynchronouslyDraw,
};

看下繪制- (BOOL)drawInRect:(CGRect)rect withContext:(CGContextRef)context asynchronously:(BOOL)asynchronously userInfo:(NSDictionary *)userInfo方法的具體實(shí)現(xiàn)

- (BOOL)drawInRect:(CGRect)rect withContext:(CGContextRef)context asynchronously:(BOOL)asynchronously userInfo:(NSDictionary *)userInfo
{
    [super drawInRect:rect withContext:context asynchronously:asynchronously userInfo:userInfo];
    //獲取在currentDrawingUserInfo方法中配置的繪制數(shù)據(jù)
    UIColor *backgroundColor = (UIColor *)[userInfo valueForKey:WMGCanvasViewBackgroundColorKey];
    CGFloat borderWidth = [[userInfo valueForKey:WMGCanvasViewBorderWidthKey] floatValue];
    CGFloat cornerRadius = [[userInfo valueForKey:WMGCanvasViewCornerRadiusKey] floatValue];
    UIColor *borderColor = (UIColor *)[userInfo valueForKey:WMGCanvasViewBorderColorKey];
    borderWidth *= [[UIScreen mainScreen] scale];
    
    if(cornerRadius == 0){
        
        if (backgroundColor && backgroundColor != [UIColor clearColor]) {
            //給上下文填充顏色
            CGContextSetFillColorWithColor(context, backgroundColor.CGColor);
            //填充框
            CGContextFillRect(context, rect);
        }
        if(borderWidth > 0){
            //將路徑對(duì)象加入上下文對(duì)象中
            CGContextAddPath(context, [UIBezierPath bezierPathWithRect:rect].CGPath);
        }
        CGContextSetFillColorWithColor(context, [UIColor clearColor].CGColor);
        if(borderWidth > 0){
            //設(shè)置線框的顏色
            CGContextSetStrokeColorWithColor(context, borderColor.CGColor);
            //設(shè)置線的寬度
            CGContextSetLineWidth(context, borderWidth);
            //如果borderwidth大于0繪制路徑
            CGContextDrawPath(context, kCGPathFillStroke);
        }else{
            //如果borderwidth等于0繪制填充
            CGContextDrawPath(context, kCGPathFill);
        }
        /*
         kCGPathStroke:劃線(空心)
         kCGPathFill: 填充(實(shí)心)
         kCGPathFillStroke:即劃線又填充
         */
    }
    else{
        
        CGRect targetRect = CGRectMake(0, 0, rect.size.width , rect.size.height);
        //帶圓角矩形的bezier路徑
        UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:targetRect
                                                   byRoundingCorners:UIRectCornerAllCorners
                                                         cornerRadii:CGSizeMake(cornerRadius, cornerRadius)];
        //使用偶數(shù)奇數(shù)填充規(guī)則
        [path setUsesEvenOddFillRule:YES];
        //就相當(dāng)于剪裁路徑以外的看不見
        [path addClip];
        CGContextAddPath(context, path.CGPath);
        if (backgroundColor && backgroundColor != [UIColor clearColor]) {
            CGContextSetFillColorWithColor(context, backgroundColor.CGColor);
            CGContextFillRect(context, rect);
            CGContextAddPath(context, path.CGPath);
        }
        CGContextSetFillColorWithColor(context, [UIColor clearColor].CGColor);
        if(borderWidth > 0){
            CGContextSetStrokeColorWithColor(context, borderColor.CGColor);
            CGContextSetLineWidth(context, borderWidth);
            CGContextDrawPath(context, kCGPathFillStroke);
        }else{
            CGContextDrawPath(context, kCGPathFill);
        }
    }
    // 陰影設(shè)置
    UIColor *shadowColor = (UIColor *)[userInfo valueForKey:WMGCanvasViewShadowColorKey];
    CGFloat shadowBlur = [[userInfo valueForKey:WMGCanvasViewShadowBlurKey] floatValue];
    UIOffset shadowOffset = [[userInfo valueForKey:WMGCanvasViewShadowOffsetKey] UIOffsetValue];
    if (shadowColor) {
        //設(shè)置陰影顏色
        CGContextSetShadowWithColor(context, CGSizeMake(shadowOffset.horizontal, shadowOffset.vertical), shadowBlur, shadowColor.CGColor);
    }
    //更改當(dāng)前上下文
    UIGraphicsPushContext(context);
    UIImage *image = [userInfo valueForKey:WMGCanvasViewBackgroundImageKey];
    [image drawInRect:rect];
    UIGraphicsPopContext();
    return YES;
}

該子類重寫的方法在displayLayer中調(diào)用

  • -(NSDictionary *)currentDrawingUserInfo
  • -(BOOL)drawInRect:(CGRect)rect withContext:(CGContextRef)context asynchronously:(BOOL)asynchronously userInfo:(NSDictionary *)userInfo
    這兩個(gè)方法均是重寫父類方法餐济,下面看下在WMGAsyncDrawView如何進(jìn)行異步繪制的

WMGAsyncDrawView

在WMGAsyncDrawView當(dāng)中先將layer指定為自己自定義的layer

+ (Class)layerClass
{
    return [WMGAsyncDrawLayer class];
}
/**
 * 子類可以重寫,并在此方法中進(jìn)行繪制胆剧,請(qǐng)勿直接調(diào)用此方法
 *
 * @param rect 進(jìn)行繪制的區(qū)域絮姆,目前只可能是 self.bounds
 * @param context 繪制到的context,目前在調(diào)用時(shí)此context都會(huì)在系統(tǒng)context堆棧棧頂
 * @param asynchronously 當(dāng)前是否是異步繪制
 * @param userInfo 由currentDrawingUserInfo傳入的字典秩霍,供繪制傳參使用
 *
 * @return 繪制是否已執(zhí)行完成篙悯。若為 NO,繪制的內(nèi)容不會(huì)被顯示
 */
- (BOOL)drawInRect:(CGRect)rect withContext:(CGContextRef)context asynchronously:(BOOL)asynchronously userInfo:(NSDictionary *)userInfo;

在前文說過當(dāng) CALayer 需要繪制 UI 的時(shí)候铃绒,會(huì)查看 layer 的代理是否實(shí)現(xiàn)了 - (void)displayLayer:(CALayer *)layer; 方法鸽照,如果實(shí)現(xiàn)了,就進(jìn)入用戶自定義繪制流程颠悬,在WMGAsyncDrawView中實(shí)現(xiàn)了該方法:

- (void)displayLayer:(CALayer *)layer
{
    if (!layer) return;
    
    NSAssert([layer isKindOfClass:[WMGAsyncDrawLayer class]], @"WMGAsyncDrawingView can only display WMGAsyncDrawLayer");
    
    if (layer != self.layer) return;
    
    [self _displayLayer:(WMGAsyncDrawLayer *)layer rect:self.bounds drawingStarted:^(BOOL drawInBackground) {
        //異步繪制即將啟動(dòng)
        [self drawingWillStartAsynchronously:drawInBackground];
    } drawingFinished:^(BOOL drawInBackground) {
        //異步繪制完成
        [self drawingDidFinishAsynchronously:drawInBackground success:YES];
    } drawingInterrupted:^(BOOL drawInBackground) {
        //異步繪制失敗
        [self drawingDidFinishAsynchronously:drawInBackground success:NO];
    }];
}

- (void)_displayLayer:(WMGAsyncDrawLayer *)layer
                 rect:(CGRect)rectToDraw
       drawingStarted:(WMGAsyncDrawCallback)startCallback
      drawingFinished:(WMGAsyncDrawCallback)finishCallback
   drawingInterrupted:(WMGAsyncDrawCallback)interruptCallback
{
    //是否允許異步繪制
    BOOL drawInBackground = layer.isAsyncDrawsCurrentContent && ![[self class] globalAsyncDrawingDisabled];
    //增加異步繪制次數(shù)
    [layer increaseDrawingCount];
    //得到繪制次數(shù)
    NSUInteger targetDrawingCount = layer.drawingCount;
    //子類可以重寫矮燎,用于在主線程生成并傳入繪制所需參數(shù)
    NSDictionary *drawingUserInfo = [self currentDrawingUserInfo];
    //這部分blcok用的很多看起來很亂
    //定義一個(gè)drawBlock
    void (^drawBlock)(void) = ^{
        //定義一個(gè)failedBlock用來回掉失敗
        void (^failedBlock)(void) = ^{
            if (interruptCallback)
            {
                interruptCallback(drawInBackground);
            }
        };
        //如果繪制次數(shù)不等于得到的繪制次數(shù)中斷回掉
        if (layer.drawingCount != targetDrawingCount)
        {
            failedBlock();
            return;
        }
        
        //獲得繪制區(qū)域
        CGSize contextSize = layer.bounds.size;
        //上下文大小有效性
        BOOL contextSizeValid = contextSize.width >= 1 && contextSize.height >= 1;
        CGContextRef context = NULL;
        //繪制是否完成
        BOOL drawingFinished = YES;
        
        if (contextSizeValid) {
            //開啟上下文
            UIGraphicsBeginImageContextWithOptions(contextSize, layer.isOpaque, layer.contentsScale);
            //獲取當(dāng)前上下文
            context = UIGraphicsGetCurrentContext();
            
            if (!context) {
                WMGLog(@"may be memory warning");
            }
            //保存上下文
            CGContextSaveGState(context);
            
            if (rectToDraw.origin.x || rectToDraw.origin.y)
            {
                //該方法相當(dāng)于把原來位于 (0, 0) 位置的坐標(biāo)原點(diǎn)平移到 (tx, ty) 點(diǎn)。在平移后的坐標(biāo)系統(tǒng)上繪制圖形時(shí)赔癌,所有坐標(biāo)點(diǎn)的 X 坐標(biāo)都相當(dāng)于增加了 tx诞外,所有點(diǎn)的 Y 坐標(biāo)都相當(dāng)于增加了 ty。
                CGContextTranslateCTM(context, rectToDraw.origin.x, -rectToDraw.origin.y);
            }
            
            if (layer.drawingCount != targetDrawingCount)
            {
                drawingFinished = NO;
            }
            else
            {
                //繪制方法
                drawingFinished = [self drawInRect:rectToDraw withContext:context asynchronously:drawInBackground userInfo:drawingUserInfo];
            }
            
            CGContextRestoreGState(context);
        }
        
        // 所有耗時(shí)的操作都已完成灾票,但僅在繪制過程中未發(fā)生重繪時(shí)峡谊,將結(jié)果顯示出來
        if (drawingFinished && targetDrawingCount == layer.drawingCount)
        {
            //生成圖片
            CGImageRef CGImage = context ? CGBitmapContextCreateImage(context) : NULL;
            {
                // 讓 UIImage 進(jìn)行內(nèi)存管理
                UIImage *image = CGImage ? [UIImage imageWithCGImage:CGImage] : nil;
                //定義finishBlock用來完成回掉
                void (^finishBlock)(void) = ^{
                    // 由于block可能在下一runloop執(zhí)行,再進(jìn)行一次檢查
                    if (targetDrawingCount != layer.drawingCount)
                    {
                        failedBlock();
                        return;
                    }
                    layer.contents = (id)image.CGImage;
                    // 在drawingPolicy 為 WMGViewDrawingPolicyAsynchronouslyDrawWhenContentsChanged 時(shí)使用
                    // 需要異步繪制時(shí)設(shè)置一次 YES刊苍,默認(rèn)為NO
                    [layer setContentsChangedAfterLastAsyncDrawing:NO];
                    //下次AsyncDrawing完成前是否保留當(dāng)前的contents
                    [layer setReserveContentsBeforeNextDrawingComplete:NO];
                    //完成繪制
                    if (finishCallback)
                    {
                        finishCallback(drawInBackground);
                    }
                    
                    // 如果當(dāng)前是異步繪制既们,且設(shè)置了有效fadeDuration,則執(zhí)行動(dòng)畫
                    if (drawInBackground && layer.fadeDuration > 0.0001)
                    {
                        layer.opacity = 0.0;
                        
                        [UIView animateWithDuration:layer.fadeDuration delay:0.0 options:UIViewAnimationOptionAllowUserInteraction animations:^{
                            layer.opacity = 1.0;
                        } completion:NULL];
                    }
                };
                //是否異步完成完成繪制
                if (drawInBackground)
                {
                    dispatch_async(dispatch_get_main_queue(), finishBlock);
                }
                else
                {
                    finishBlock();
                }
            }
            
            if (CGImage) {
                CGImageRelease(CGImage);
            }
        }
        else
        {
            failedBlock();
        }
        
        UIGraphicsEndImageContext();
    };
   
    if (startCallback)
    {
        //異步繪制即將啟動(dòng)
        startCallback(drawInBackground);
    }
    //是否允許異步繪制
    if (drawInBackground)
    {
        // 清空 layer 的顯示
        if (!layer.reserveContentsBeforeNextDrawingComplete)
        {
            layer.contents = nil;
        }
        
        //異步全局隊(duì)列做任務(wù)
        dispatch_async([self drawQueue], drawBlock);
    }
    else
    {
        void (^block)(void) = ^{
            @autoreleasepool {
                drawBlock();
            }
        };
        if ([NSThread isMainThread])
        {
            // 已經(jīng)在主線程班缰,直接執(zhí)行繪制
            block();
        }
        else
        {
            // 不應(yīng)當(dāng)在其他線程贤壁,轉(zhuǎn)到主線程繪制
            dispatch_async(dispatch_get_main_queue(), block);
        }
    }
}

也就是說WMGAsyncDrawView類中開辟了異步隊(duì)列來進(jìn)行繪制,但是繪制的具體過程交由繼承自WMGAsyncDrawView的子類來實(shí)現(xiàn)具體是什么類型的繪制埠忘,然后在WMGAsyncDrawView中拿到子類繪制好的context在將其繪制成位圖在交付出去脾拆。

總結(jié)

通過這兩個(gè)類的思路即可以對(duì)自己項(xiàng)目中的一些無法直接使用Graver的頁面馒索,或不想使用第三方庫的項(xiàng)目來進(jìn)行對(duì)頁面的一次異步繪制的改造。

參考資料:
美團(tuán)開源Graver框架:用“雕刻”詮釋iOS端UI界面的高效渲染

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末名船,一起剝皮案震驚了整個(gè)濱河市绰上,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌渠驼,老刑警劉巖蜈块,帶你破解...
    沈念sama閱讀 222,627評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異迷扇,居然都是意外死亡百揭,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,180評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門蜓席,熙熙樓的掌柜王于貴愁眉苦臉地迎上來器一,“玉大人,你說我怎么就攤上這事厨内∑盹酰” “怎么了?”我有些...
    開封第一講書人閱讀 169,346評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵雏胃,是天一觀的道長(zhǎng)请毛。 經(jīng)常有香客問我,道長(zhǎng)瞭亮,這世上最難降的妖魔是什么方仿? 我笑而不...
    開封第一講書人閱讀 60,097評(píng)論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮街州,結(jié)果婚禮上兼丰,老公的妹妹穿的比我還像新娘。我一直安慰自己唆缴,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,100評(píng)論 6 398
  • 文/花漫 我一把揭開白布黍翎。 她就那樣靜靜地躺著面徽,像睡著了一般。 火紅的嫁衣襯著肌膚如雪匣掸。 梳的紋絲不亂的頭發(fā)上趟紊,一...
    開封第一講書人閱讀 52,696評(píng)論 1 312
  • 那天,我揣著相機(jī)與錄音碰酝,去河邊找鬼霎匈。 笑死,一個(gè)胖子當(dāng)著我的面吹牛送爸,可吹牛的內(nèi)容都是我干的铛嘱。 我是一名探鬼主播暖释,決...
    沈念sama閱讀 41,165評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼墨吓!你這毒婦竟也來了球匕?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,108評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤帖烘,失蹤者是張志新(化名)和其女友劉穎亮曹,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體秘症,經(jīng)...
    沈念sama閱讀 46,646評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡照卦,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,709評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了乡摹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片役耕。...
    茶點(diǎn)故事閱讀 40,861評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖趟卸,靈堂內(nèi)的尸體忽然破棺而出蹄葱,到底是詐尸還是另有隱情,我是刑警寧澤锄列,帶...
    沈念sama閱讀 36,527評(píng)論 5 351
  • 正文 年R本政府宣布图云,位于F島的核電站,受9級(jí)特大地震影響邻邮,放射性物質(zhì)發(fā)生泄漏竣况。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,196評(píng)論 3 336
  • 文/蒙蒙 一筒严、第九天 我趴在偏房一處隱蔽的房頂上張望丹泉。 院中可真熱鬧,春花似錦鸭蛙、人聲如沸摹恨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,698評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽晒哄。三九已至,卻和暖如春肪获,著一層夾襖步出監(jiān)牢的瞬間寝凌,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,804評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工孝赫, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留较木,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,287評(píng)論 3 379
  • 正文 我出身青樓青柄,卻偏偏與公主長(zhǎng)得像伐债,于是被迫代替她去往敵國(guó)和親预侯。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,860評(píng)論 2 361

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

  • 美團(tuán)開源Graver框架:用“雕刻”詮釋iOS端UI界面的高效渲染Graver是一個(gè)App渲染框架泳赋,采用異步渲染的...
    Bel李玉閱讀 1,118評(píng)論 0 1
  • 前言 我們經(jīng)常在面試中雌桑,會(huì)被問及關(guān)于界面優(yōu)化相關(guān)的問題,比如為什么界面會(huì)出現(xiàn)卡頓祖今?如何監(jiān)控卡頓校坑?接著如何解決卡頓?...
    深圳_你要的昵稱閱讀 1,132評(píng)論 1 4
  • UI視圖相關(guān)的知識(shí) UITableview相關(guān)問題 重用機(jī)制重用機(jī)制主要用到了一個(gè)可變數(shù)組visiableCell...
    李白杜甫談戀愛閱讀 385評(píng)論 0 0
  • CALayer通過四個(gè)屬性來確定大小和位置, 分別為:frame千诬、bounds耍目、position、anchorPo...
    一川煙草i蓑衣閱讀 424評(píng)論 0 1
  • UItableView 重用機(jī)制 一般在iOS中徐绑,tableview的重用機(jī)制邪驮,我們?cè)? (UITableView...
    叔簡(jiǎn)閱讀 732評(píng)論 0 3