阅读 446

为什么说重写了drawRect:后会增加内存开销

前言

很多博客都会写显示重写了drawRect:会增加额外的内存开销,但很少有写具体原因的,下面我就从源码来解释这个“常识”

UIKit框架是闭源的,但是可以根据微软的winObjc提供的源码来看

详解

首先给drawRect:方法打个断点:

根据已有知识,在Runloop有个系统监听了beforeWaiting状态(还监听了个exit状态不常用),每次到beforeWaiting就会回调__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__,里面调用C++的CA::Transaction::commit()方法把UI变动打包提交给GPU来渲染,我们常用的layoutSubviews:drawRect:只是这个函数实现的一部分(等同于苹果暴露了接口给你写)

看断点第一个用到OC语法的方法是[CALayer _display],我们来看winObjc的源码:

代码有点长,我省略了日志等信息

- (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 任一一个都会增加内存消耗

后言

关于源码中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);
}
复制代码

首先是objc_getClass object_getClass 两个runtime方法的区别:

id objc_getClass(const char *name) 参数是类的名字,返回的是类对象Class

Class object_getClass(id obj) 参数是对象(实例对象 或 类对象),返回的是类对象(如果参数是实例对象)或元类对象(如果参数是类对象)

IMP class_getMethodImplementation(Class cls, SEL name) 返回类对象(或元类对象)是否有对应selector的实现

所以拿这个作为例子object_isMethodFromClass(priv->delegate, @selector(drawRect:), "UIView"),就是说priv->delegate的类对象和UIView的类对象里的drawRect:方法实现是否是同一个,言外之意就是priv->delegate类有没有重写drawRect:方法