mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-17 19:09:56 +00:00
ASDisplayNode and several subclasses had previously cleared memory-heavy objects like the backing store and text layout manager when the node's view or layer is removed from a visible heirarchy. This works great in any system that uses a "working range", where exiting the range removes the node from the hierarchy and reclaiming memory at that time is important. However, for standard UIViewController patterns (unused in Paper), this behavior causes highly undesirable thrashing (leading to visible flashes & wasteful re-rendering of content). After this change, node subclasses should implement -reclaimMemory if they need to perform any other cleanup besides backing store destruction when they leave a working range or other scenario where memory reduction is valuable. To trigger this behavior, calling code should use -recursivelyReclaimMemory. r=nadi
339 lines
10 KiB
Plaintext
339 lines
10 KiB
Plaintext
/* Copyright (c) 2014-present, Facebook, Inc.
|
|
* All rights reserved.
|
|
*
|
|
* This source code is licensed under the BSD-style license found in the
|
|
* LICENSE file in the root directory of this source tree. An additional grant
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
*/
|
|
|
|
#import "ASImageNode.h"
|
|
|
|
#import <AsyncDisplayKit/_ASDisplayLayer.h>
|
|
#import <AsyncDisplayKit/_ASCoreAnimationExtras.h>
|
|
#import <AsyncDisplayKit/ASAssert.h>
|
|
#import <AsyncDisplayKit/ASDisplayNode+Subclasses.h>
|
|
|
|
#import "ASImageNode+CGExtras.h"
|
|
|
|
@interface _ASImageNodeDrawParameters : NSObject
|
|
|
|
@property (nonatomic, assign, readonly) BOOL cropEnabled;
|
|
@property (nonatomic, assign) BOOL opaque;
|
|
@property (nonatomic, retain) UIImage *image;
|
|
@property (nonatomic, assign) CGRect bounds;
|
|
@property (nonatomic, assign) CGFloat contentsScale;
|
|
@property (nonatomic, assign) ASImageNodeTint tint;
|
|
@property (nonatomic, retain) UIColor *backgroundColor;
|
|
@property (nonatomic, assign) UIViewContentMode contentMode;
|
|
@property (nonatomic, assign) CGRect cropRect;
|
|
@property (nonatomic, copy) asimagenode_modification_block_t imageModificationBlock;
|
|
|
|
@end
|
|
|
|
// TODO: eliminate explicit parameters with a set of keys copied from the node
|
|
@implementation _ASImageNodeDrawParameters
|
|
|
|
- (id)initWithCrop:(BOOL)cropEnabled opaque:(BOOL)opaque image:(UIImage *)image bounds:(CGRect)bounds contentsScale:(CGFloat)contentsScale backgroundColor:(UIColor *)backgroundColor tint:(ASImageNodeTint)tint contentMode:(UIViewContentMode)contentMode cropRect:(CGRect)cropRect imageModificationBlock:(asimagenode_modification_block_t)imageModificationBlock
|
|
{
|
|
self = [self init];
|
|
if (!self) return nil;
|
|
|
|
_cropEnabled = cropEnabled;
|
|
_opaque = opaque;
|
|
_image = image;
|
|
_bounds = bounds;
|
|
_contentsScale = contentsScale;
|
|
_backgroundColor = backgroundColor;
|
|
_tint = tint;
|
|
_contentMode = contentMode;
|
|
_cropRect = cropRect;
|
|
_imageModificationBlock = [imageModificationBlock copy];
|
|
|
|
return self;
|
|
}
|
|
|
|
- (NSString *)description
|
|
{
|
|
return [NSString stringWithFormat:@"<%@ : %p image:%@ cropEnabled:%@ opaque:%@ bounds:%@ contentsScale:%.2f backgroundColor:%@ tint:%zd contentMode:%@ cropRect:%@>", [self class], self, self.image, @(self.cropEnabled), @(self.opaque), NSStringFromCGRect(self.bounds), self.contentsScale, self.backgroundColor, self.tint, ASDisplayNodeNSStringFromUIContentMode(self.contentMode), NSStringFromCGRect(self.cropRect)];
|
|
}
|
|
|
|
@end
|
|
|
|
|
|
@implementation ASImageNode
|
|
{
|
|
@private
|
|
UIImage *_image;
|
|
|
|
void (^_displayCompletionBlock)(BOOL canceled);
|
|
ASDN::RecursiveMutex _imageLock;
|
|
|
|
// Cropping.
|
|
BOOL _cropEnabled; // Defaults to YES.
|
|
ASImageNodeTint _tint;
|
|
CGRect _cropRect; // Defaults to CGRectMake(0.5, 0.5, 0, 0)
|
|
CGRect _cropDisplayBounds;
|
|
}
|
|
|
|
@synthesize image = _image;
|
|
|
|
- (id)init
|
|
{
|
|
if (!(self = [super init]))
|
|
return nil;
|
|
|
|
// TODO can this be removed?
|
|
self.contentsScale = [[UIScreen mainScreen] scale];
|
|
self.contentMode = UIViewContentModeScaleAspectFill;
|
|
self.opaque = YES;
|
|
|
|
_cropEnabled = YES;
|
|
_cropRect = CGRectMake(0.5, 0.5, 0, 0);
|
|
_cropDisplayBounds = CGRectNull;
|
|
|
|
return self;
|
|
}
|
|
|
|
- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize
|
|
{
|
|
ASDN::MutexLocker l(_imageLock);
|
|
if (_image)
|
|
return _image.size;
|
|
else
|
|
return CGSizeZero;
|
|
}
|
|
|
|
- (void)setImage:(UIImage *)image
|
|
{
|
|
ASDisplayNodeAssertThreadAffinity(self);
|
|
ASDN::MutexLocker l(_imageLock);
|
|
if (_image != image) {
|
|
_image = image;
|
|
[self invalidateCalculatedSize];
|
|
[self setNeedsDisplay];
|
|
}
|
|
}
|
|
|
|
- (UIImage *)image
|
|
{
|
|
ASDisplayNodeAssertThreadAffinity(self);
|
|
ASDN::MutexLocker l(_imageLock);
|
|
return _image;
|
|
}
|
|
|
|
- (void)setTint:(ASImageNodeTint)tint
|
|
{
|
|
ASDisplayNodeAssertThreadAffinity(self);
|
|
ASDN::MutexLocker l(_imageLock);
|
|
if (_tint != tint) {
|
|
_tint = tint;
|
|
[self setNeedsDisplay];
|
|
}
|
|
}
|
|
|
|
- (ASImageNodeTint)tint
|
|
{
|
|
ASDisplayNodeAssertThreadAffinity(self);
|
|
ASDN::MutexLocker l(_imageLock);
|
|
return _tint;
|
|
}
|
|
|
|
- (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer;
|
|
{
|
|
BOOL hasValidCropBounds = _cropEnabled && !CGRectIsNull(_cropDisplayBounds) && !CGRectIsEmpty(_cropDisplayBounds);
|
|
|
|
return [[_ASImageNodeDrawParameters alloc] initWithCrop:_cropEnabled
|
|
opaque:self.opaque
|
|
image:self.image
|
|
bounds:(hasValidCropBounds ? _cropDisplayBounds : self.bounds)
|
|
contentsScale:self.contentsScaleForDisplay
|
|
backgroundColor:self.backgroundColor
|
|
tint:self.tint
|
|
contentMode:self.contentMode
|
|
cropRect:self.cropRect
|
|
imageModificationBlock:self.imageModificationBlock];
|
|
}
|
|
|
|
+ (UIImage *)displayWithParameters:(_ASImageNodeDrawParameters *)parameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelled
|
|
{
|
|
UIImage *image = parameters.image;
|
|
|
|
if (!image) {
|
|
return nil;
|
|
}
|
|
|
|
ASDisplayNodeAssert(parameters.contentsScale > 0, @"invalid contentsScale at display time");
|
|
|
|
CGRect bounds = parameters.bounds;
|
|
|
|
CGFloat contentsScale = parameters.contentsScale;
|
|
UIViewContentMode contentMode = parameters.contentMode;
|
|
BOOL stretchable = !UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero);
|
|
CGSize imageSize = image.size;
|
|
CGSize imageSizeInPixels = CGSizeMake(imageSize.width * image.scale, imageSize.height * image.scale);
|
|
CGSize boundsSizeInPixels = CGSizeMake(floorf(bounds.size.width * contentsScale), floorf(bounds.size.height * contentsScale));
|
|
|
|
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(image.CGImage);
|
|
BOOL imageHasAlpha = alphaInfo == kCGImageAlphaFirst
|
|
|| alphaInfo == kCGImageAlphaLast
|
|
|| alphaInfo == kCGImageAlphaPremultipliedFirst
|
|
|| alphaInfo == kCGImageAlphaPremultipliedLast;
|
|
|
|
BOOL contentModeSupported = contentMode == UIViewContentModeScaleAspectFill
|
|
|| contentMode == UIViewContentModeScaleAspectFit;
|
|
|
|
CGSize backingSize;
|
|
CGRect imageDrawRect;
|
|
|
|
if (boundsSizeInPixels.width * contentsScale < 1.0f ||
|
|
boundsSizeInPixels.height * contentsScale < 1.0f ||
|
|
imageSizeInPixels.width < 1.0f ||
|
|
imageSizeInPixels.height < 1.0f) {
|
|
return nil;
|
|
}
|
|
|
|
// If we're not supposed to do any cropping, just decode image at original size
|
|
if (!parameters.cropEnabled || !contentModeSupported || stretchable) {
|
|
backingSize = imageSizeInPixels;
|
|
imageDrawRect = (CGRect){.size = backingSize};
|
|
} else {
|
|
ASCroppedImageBackingSizeAndDrawRectInBounds(imageSizeInPixels,
|
|
boundsSizeInPixels,
|
|
contentMode,
|
|
parameters.cropRect,
|
|
&backingSize,
|
|
&imageDrawRect);
|
|
}
|
|
|
|
if (backingSize.width <= 0.0f ||
|
|
backingSize.height <= 0.0f ||
|
|
imageDrawRect.size.width <= 0.0f ||
|
|
imageDrawRect.size.height <= 0.0f) {
|
|
return nil;
|
|
}
|
|
|
|
// Use contentsScale of 1.0 and do the contentsScale handling in boundsSizeInPixels so ASCroppedImageBackingSizeAndDrawRectInBounds
|
|
// will do its rounding on pixel instead of point boundaries
|
|
UIGraphicsBeginImageContextWithOptions(backingSize, !imageHasAlpha, 1.0);
|
|
|
|
CGContextRef context = UIGraphicsGetCurrentContext();
|
|
|
|
[image drawInRect:imageDrawRect];
|
|
|
|
if (parameters.tint == ASImageNodeTintGreyscale) {
|
|
[[UIColor grayColor] setFill];
|
|
CGContextSetBlendMode(context, kCGBlendModeColor);
|
|
CGContextFillRect(context, (CGRect){.size = backingSize});
|
|
}
|
|
|
|
if (isCancelled()) {
|
|
UIGraphicsEndImageContext();
|
|
return nil;
|
|
}
|
|
|
|
UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
|
|
|
|
UIGraphicsEndImageContext();
|
|
|
|
if (parameters.imageModificationBlock != NULL) {
|
|
result = parameters.imageModificationBlock(result);
|
|
}
|
|
|
|
if (stretchable) {
|
|
return [image resizableImageWithCapInsets:image.capInsets resizingMode:image.resizingMode];
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
- (void)displayDidFinish
|
|
{
|
|
[super displayDidFinish];
|
|
|
|
// If we've got a block to perform after displaying, do it.
|
|
if (self.image && _displayCompletionBlock) {
|
|
|
|
// FIXME: _displayCompletionBlock is not protected by lock
|
|
_displayCompletionBlock(NO);
|
|
_displayCompletionBlock = nil;
|
|
}
|
|
}
|
|
|
|
#pragma mark -
|
|
- (void)setNeedsDisplayWithCompletion:(void (^)(BOOL canceled))displayCompletionBlock
|
|
{
|
|
if (self.preventOrCancelDisplay) {
|
|
if (displayCompletionBlock)
|
|
displayCompletionBlock(YES);
|
|
return;
|
|
}
|
|
|
|
// Stash the block and call-site queue. We'll invoke it in -displayDidFinish.
|
|
// FIXME: _displayCompletionBlock not protected by lock
|
|
if (_displayCompletionBlock != displayCompletionBlock) {
|
|
_displayCompletionBlock = [displayCompletionBlock copy];
|
|
}
|
|
|
|
[self setNeedsDisplay];
|
|
}
|
|
|
|
#pragma mark - Cropping
|
|
- (BOOL)isCropEnabled
|
|
{
|
|
ASDisplayNodeAssertThreadAffinity(self);
|
|
return _cropEnabled;
|
|
}
|
|
|
|
- (void)setCropEnabled:(BOOL)cropEnabled
|
|
{
|
|
ASDisplayNodeAssertThreadAffinity(self);
|
|
[self setCropEnabled:cropEnabled recropImmediately:NO inBounds:self.bounds];
|
|
}
|
|
|
|
- (void)setCropEnabled:(BOOL)cropEnabled recropImmediately:(BOOL)recropImmediately inBounds:(CGRect)cropBounds
|
|
{
|
|
ASDisplayNodeAssertThreadAffinity(self);
|
|
if (_cropEnabled == cropEnabled)
|
|
return;
|
|
|
|
_cropEnabled = cropEnabled;
|
|
_cropDisplayBounds = cropBounds;
|
|
|
|
// If we have an image to display, display it, respecting our recrop flag.
|
|
if (self.image)
|
|
{
|
|
if (recropImmediately)
|
|
[self displayImmediately];
|
|
else
|
|
[self setNeedsDisplay];
|
|
}
|
|
}
|
|
|
|
- (CGRect)cropRect
|
|
{
|
|
ASDisplayNodeAssertThreadAffinity(self);
|
|
return _cropRect;
|
|
}
|
|
|
|
- (void)setCropRect:(CGRect)cropRect
|
|
{
|
|
ASDisplayNodeAssertThreadAffinity(self);
|
|
|
|
if (CGRectEqualToRect(_cropRect, cropRect))
|
|
return;
|
|
|
|
_cropRect = cropRect;
|
|
|
|
// TODO: this logic needs to be updated to respect cropRect.
|
|
CGSize boundsSize = self.bounds.size;
|
|
CGSize imageSize = self.image.size;
|
|
|
|
BOOL isCroppingImage = ((boundsSize.width < imageSize.width) || (boundsSize.height < imageSize.height));
|
|
|
|
// Re-display if we need to.
|
|
if (self.nodeLoaded && self.contentMode == UIViewContentModeScaleAspectFill && isCroppingImage)
|
|
[self setNeedsDisplay];
|
|
}
|
|
|
|
@end
|