爲何說重寫了drawRect:後會增長內存開銷

前言

不少博客都會寫顯示重寫了drawRect:會增長額外的內存開銷,但不多有寫具體緣由的,下面我就從源碼來解釋這個「常識」canvas

UIKit框架是閉源的,可是能夠根據微軟的winObjc提供的源碼來看app

詳解

首先給drawRect:方法打個斷點: 框架

根據已有知識,在Runloop有個系統監聽了beforeWaiting狀態(還監聽了個exit狀態不經常使用),每次到beforeWaiting就會回調__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__,裏面調用C++的CA::Transaction::commit()方法把UI變更打包提交給GPU來渲染,咱們經常使用的layoutSubviews:drawRect:只是這個函數實現的一部分(等同於蘋果暴露了接口給你寫)函數

看斷點第一個用到OC語法的方法是[CALayer _display],咱們來看winObjc的源碼:oop

代碼有點長,我省略了日誌等信息ui

- (void)display {
    if (priv->savedContext != NULL) {
        CGContextRelease(priv->savedContext);
        priv->savedContext = NULL;
    }

    //這裏的contents就是一個layer的back-store
    if (priv->contents == NULL || priv->ownsContents || [self isKindOfClass:[CAShapeLayer class]]) {
        if (priv->contents) {
            if (priv->ownsContents) {
                CGImageRelease(priv->contents);
            }
            priv->contents = NULL;
        }

        // Update content size, even in case of the early out below.
        int widthInPoints = ceilf(priv->bounds.size.width);
        int heightInPoints = ceilf(priv->bounds.size.height);

        int width = (int)(widthInPoints * priv->contentsScale);
        int height = (int)(heightInPoints * priv->contentsScale);
        // 這裏計算back-store的大小,也就是須要的內存塊大小
        priv->contentsSize.width = (float)width;
        priv->contentsSize.height = (float)height;

        // 下面就是決定drawRect: 爲何會額外增長內存消耗的緣由了
       // 先來看object_isMethodFromClass這個方法,這個方法的實現:
       // static BOOL object_isMethodFromClass(id object, SEL selector, const char* className) {
       // return class_getMethodImplementation(objc_getClass(className), selector) ==
       // class_getMethodImplementation(object_getClass(object), selector);
       //}
       // 這個方法本質是判斷object和className對應的類對象裏的selector實現是否是同一個,換句話來講判斷object有沒有重寫selector方法,若是重寫了就返回YES
       // 因此 若是下面三個方法都沒有重寫(`drawRect:` `drawLayer:in:` `displayLayer`),那麼hasDrawingMethod就是false,display方法直接return
        bool hasDrawingMethod = false;
        if (priv->delegate != nil && (!object_isMethodFromClass(priv->delegate, @selector(drawRect:), "UIView") ||
                                      !object_isMethodFromClass(priv->delegate, @selector(drawLayer:inContext:), "UIView") ||
                                      [priv->delegate respondsToSelector:@selector(displayLayer:)])) {
            hasDrawingMethod = true;
        }

        if (!object_isMethodFromClass(self, @selector(drawInContext:), "CALayer")) {
            hasDrawingMethod = true;
        }

        if (!hasDrawingMethod) {
            return;
        }

        // 若是有重寫方法,就進行下面的代碼
        unsigned int tries = 0;
        do {
            // Create the contents
            // 這個方法就是具體的建立內存塊的方法了,也就是增長內存消耗的部分,下面總之就是會調用你實現的那三個方法或之一
            woc::StrongCF<CGContextRef> drawContext{ woc::MakeStrongCF(
                CreateLayerContentsBitmapContext32(width, height, priv->contentsScale)) };
            _CGContextPushBeginDraw(drawContext);

            if (priv->_backgroundColor != nullptr && CGColorGetPattern(priv->_backgroundColor) != nullptr) {
                CGContextSaveGState(drawContext);
                CGContextSetFillColorWithColor(drawContext, priv->_backgroundColor);

                CGRect wholeRect = CGRectMake(0, 0, width, height);
                CGContextFillRect(drawContext, wholeRect);
                CGContextRestoreGState(drawContext);
            }

            // UIKit and CALayer consumers expect the origin to be in the top left.
            // CoreGraphics defaults to the bottom left, so we must flip and translate the canvas.
            CGContextTranslateCTM(drawContext, 0, heightInPoints);
            CGContextScaleCTM(drawContext, 1.0f, -1.0f);
            CGContextTranslateCTM(drawContext, -priv->bounds.origin.x, -priv->bounds.origin.y);

            _CGContextSetShadowProjectionTransform(drawContext, CGAffineTransformMakeScale(1.0, -1.0));

            [self drawInContext:drawContext];

            if (priv->delegate != 0) {
                if ([priv->delegate respondsToSelector:@selector(displayLayer:)]) {
                    [priv->delegate displayLayer:self];
                } else {
                    [priv->delegate drawLayer:self inContext:drawContext];
                }
            }

            _CGContextPopEndDraw(drawContext);

            woc::StrongCF<CFErrorRef> renderError;
            if (CGContextIwGetError(drawContext, &renderError)) {
                switch (CFErrorGetCode(renderError)) {
                    case kCGContextErrorDeviceReset:
                        NSTraceInfo(TAG, @"Hardware device disappeared when rendering %@; retrying.", self);
                        ++tries;
                        continue;
                    default: {
                        FAIL_FAST_MSG("Failed to render <%hs %p>: %hs",
                                      object_getClassName(self),
                                      self,
                                      [[static_cast<NSError*>(renderError.get()) debugDescription] UTF8String]);
                        break;
                    }
                }
            }

            CGImageRef target = _CGBitmapContextGetImage(drawContext);
            priv->ownsContents = TRUE;
            priv->savedContext = CGContextRetain(drawContext);
            priv->contents = CGImageRetain(target);
            break;
        } while (tries < _kCALayerRenderAttempts);

        if (!priv->contents) {
            NSTraceError(TAG, @"Failed to render layer %@", self);
        }
    } else if (priv->contents) {
        priv->contentsSize.width = float(CGImageGetWidth(priv->contents));
        priv->contentsSize.height = float(CGImageGetHeight(priv->contents));
    }
}
複製代碼

總結

根據源碼即可以知道,不光是drawRect:,只要是顯式在自定義類裏寫了drawRect: drawLayer:in: displayLayer 任一一個都會增長內存消耗spa

相關文章
相關標籤/搜索