Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generalize the main thread ivar deallocation system #959

Merged
merged 2 commits into from
Jun 7, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions AsyncDisplayKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,8 @@
CCB1F95C1EFB6350009C7475 /* ASSignpost.h in Headers */ = {isa = PBXBuildFile; fileRef = CCB1F95B1EFB6316009C7475 /* ASSignpost.h */; };
CCB2F34D1D63CCC6004E6DE9 /* ASDisplayNodeSnapshotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CCB2F34C1D63CCC6004E6DE9 /* ASDisplayNodeSnapshotTests.m */; };
CCBBBF5D1EB161760069AA91 /* ASRangeManagingNode.h in Headers */ = {isa = PBXBuildFile; fileRef = CCBBBF5C1EB161760069AA91 /* ASRangeManagingNode.h */; settings = {ATTRIBUTES = (Public, ); }; };
CCBDDD0520C62A2D00CBA922 /* ASMainThreadDeallocation.h in Headers */ = {isa = PBXBuildFile; fileRef = CCBDDD0320C62A2D00CBA922 /* ASMainThreadDeallocation.h */; settings = {ATTRIBUTES = (Public, ); }; };
CCBDDD0620C62A2D00CBA922 /* ASMainThreadDeallocation.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCBDDD0420C62A2D00CBA922 /* ASMainThreadDeallocation.mm */; };
CCCCCCD51EC3EF060087FE10 /* ASTextDebugOption.h in Headers */ = {isa = PBXBuildFile; fileRef = CCCCCCC31EC3EF060087FE10 /* ASTextDebugOption.h */; };
CCCCCCD61EC3EF060087FE10 /* ASTextDebugOption.m in Sources */ = {isa = PBXBuildFile; fileRef = CCCCCCC41EC3EF060087FE10 /* ASTextDebugOption.m */; };
CCCCCCD71EC3EF060087FE10 /* ASTextInput.h in Headers */ = {isa = PBXBuildFile; fileRef = CCCCCCC51EC3EF060087FE10 /* ASTextInput.h */; };
Expand Down Expand Up @@ -907,6 +909,8 @@
CCBBBF5C1EB161760069AA91 /* ASRangeManagingNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASRangeManagingNode.h; sourceTree = "<group>"; };
CCBD05DE1E4147B000D18509 /* ASIGListAdapterBasedDataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASIGListAdapterBasedDataSource.m; sourceTree = "<group>"; };
CCBD05DF1E4147B000D18509 /* ASIGListAdapterBasedDataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASIGListAdapterBasedDataSource.h; sourceTree = "<group>"; };
CCBDDD0320C62A2D00CBA922 /* ASMainThreadDeallocation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASMainThreadDeallocation.h; sourceTree = "<group>"; };
CCBDDD0420C62A2D00CBA922 /* ASMainThreadDeallocation.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASMainThreadDeallocation.mm; sourceTree = "<group>"; };
CCCCCCC31EC3EF060087FE10 /* ASTextDebugOption.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTextDebugOption.h; sourceTree = "<group>"; };
CCCCCCC41EC3EF060087FE10 /* ASTextDebugOption.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTextDebugOption.m; sourceTree = "<group>"; };
CCCCCCC51EC3EF060087FE10 /* ASTextInput.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTextInput.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1165,6 +1169,8 @@
058D09DE195D050800B7D73C /* ASImageNode.mm */,
68355B2E1CB5799E001D4E68 /* ASImageNode+AnimatedImage.mm */,
CCF1FF5D20C4785000AAD8FC /* ASLocking.h */,
CCBDDD0320C62A2D00CBA922 /* ASMainThreadDeallocation.h */,
CCBDDD0420C62A2D00CBA922 /* ASMainThreadDeallocation.mm */,
92DD2FE11BF4B97E0074C9DD /* ASMapNode.h */,
92DD2FE21BF4B97E0074C9DD /* ASMapNode.mm */,
0516FA3E1A1563D200B4EBED /* ASMultiplexImageNode.h */,
Expand Down Expand Up @@ -1822,6 +1828,7 @@
693A1DCA1ECC944E00D0C9D2 /* IGListAdapter+AsyncDisplayKit.h in Headers */,
E5E2D72E1EA780C4005C24C6 /* ASCollectionGalleryLayoutDelegate.h in Headers */,
E58E9E461E941D74004CFC59 /* ASCollectionLayoutDelegate.h in Headers */,
CCBDDD0520C62A2D00CBA922 /* ASMainThreadDeallocation.h in Headers */,
CCAA0B7F206ADBF30057B336 /* ASRecursiveUnfairLock.h in Headers */,
E5E281741E71C833006B67C2 /* ASCollectionLayoutState.h in Headers */,
E5B077FF1E69F4EB00C24B5B /* ASElementMap.h in Headers */,
Expand Down Expand Up @@ -2377,6 +2384,7 @@
B35061F91B010EFD0018CF92 /* ASControlNode.mm in Sources */,
8021EC1F1D2B00B100799119 /* UIImage+ASConvenience.m in Sources */,
CCAA0B80206ADBF30057B336 /* ASRecursiveUnfairLock.m in Sources */,
CCBDDD0620C62A2D00CBA922 /* ASMainThreadDeallocation.mm in Sources */,
B35062181B010EFD0018CF92 /* ASDataController.mm in Sources */,
CCB1F95A1EFB60A5009C7475 /* ASLog.m in Sources */,
767E7F8E1C90191D0066C000 /* AsyncDisplayKit+Debug.m in Sources */,
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- Add snapshot test for astextnode2. [Max Wang](https://github.com/wsdwsd0829) [#935](https://github.com/TextureGroup/Texture/pull/935)
- Internal housekeeping on the async transaction (rendering) system. [Adlai Holler](https://github.com/Adlai-Holler)
- Add new protocol `ASLocking` that extends `NSLocking` with `tryLock`, and allows taking multiple locks safely. [Adlai Holler](https://github.com/Adlai-Holler)
- Make the main thread ivar deallocation system available to other classes. Plus a little optimization. See `ASMainThreadDeallocation.h`. [Adlai Holler](https://github.com/Adlai-Holler) [#959](https://github.com/TextureGroup/Texture/pull/959)

## 2.7
- Fix pager node for interface coalescing. [Max Wang](https://github.com/wsdwsd0829) [#877](https://github.com/TextureGroup/Texture/pull/877)
Expand Down
115 changes: 2 additions & 113 deletions Source/ASDisplayNode.mm
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
#import <AsyncDisplayKit/ASLayoutSpec.h>
#import <AsyncDisplayKit/ASLayoutSpecPrivate.h>
#import <AsyncDisplayKit/ASLog.h>
#import <AsyncDisplayKit/ASMainThreadDeallocation.h>
#import <AsyncDisplayKit/ASRunLoopQueue.h>
#import <AsyncDisplayKit/ASSignpost.h>
#import <AsyncDisplayKit/ASTraitCollection.h>
Expand Down Expand Up @@ -428,124 +429,12 @@ - (void)dealloc
for (ASDisplayNode *subnode in _subnodes)
[subnode _setSupernode:nil];

// Trampoline any UIKit ivars' deallocation to main
if (ASDisplayNodeThreadIsMain() == NO) {
[self _scheduleIvarsForMainDeallocation];
}
[self scheduleIvarsForMainThreadDeallocation];

// TODO: Remove this? If supernode isn't already nil, this method isn't dealloc-safe anyway.
[self _setSupernode:nil];
}

- (void)_scheduleIvarsForMainDeallocation
{
NSValue *ivarsObj = [[self class] _ivarsThatMayNeedMainDeallocation];

// Unwrap the ivar array
unsigned int count = 0;
// Will be unused if assertions are disabled.
__unused int scanResult = sscanf(ivarsObj.objCType, "[%u^{objc_ivar}]", &count);
ASDisplayNodeAssert(scanResult == 1, @"Unexpected type in NSValue: %s", ivarsObj.objCType);
Ivar ivars[count];
[ivarsObj getValue:ivars];

for (Ivar ivar : ivars) {
id value = object_getIvar(self, ivar);
if (value == nil) {
continue;
}

if (ASClassRequiresMainThreadDeallocation(object_getClass(value))) {
as_log_debug(ASMainThreadDeallocationLog(), "%@: Trampolining ivar '%s' value %@ for main deallocation.", self, ivar_getName(ivar), value);

// Before scheduling the ivar for main thread deallocation we have clear out the ivar, otherwise we can run
// into a race condition where the main queue is drained earlier than this node is deallocated and the ivar
// is still deallocated on a background thread
object_setIvar(self, ivar, nil);

ASPerformMainThreadDeallocation(&value);
} else {
as_log_debug(ASMainThreadDeallocationLog(), "%@: Not trampolining ivar '%s' value %@.", self, ivar_getName(ivar), value);
}
}
}

/**
* Returns an NSValue-wrapped array of all the ivars in this class or its superclasses
* up through ASDisplayNode, that we expect may need to be deallocated on main.
*
* This method caches its results.
*
* Result is of type NSValue<[Ivar]>
*/
+ (NSValue * _Nonnull)_ivarsThatMayNeedMainDeallocation NS_RETURNS_RETAINED
{
static NSCache<Class, NSValue *> *ivarsCache;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
ivarsCache = [[NSCache alloc] init];
});

NSValue *result = [ivarsCache objectForKey:self];
if (result != nil) {
return result;
}

// Cache miss.
unsigned int resultCount = 0;
static const int kMaxDealloc2MainIvarsPerClassTree = 64;
Ivar resultIvars[kMaxDealloc2MainIvarsPerClassTree];

// Get superclass results first.
Class c = class_getSuperclass(self);
if (c != [NSObject class]) {
NSValue *ivarsObj = [c _ivarsThatMayNeedMainDeallocation];
// Unwrap the ivar array and append it to our working array
unsigned int count = 0;
// Will be unused if assertions are disabled.
__unused int scanResult = sscanf(ivarsObj.objCType, "[%u^{objc_ivar}]", &count);
ASDisplayNodeAssert(scanResult == 1, @"Unexpected type in NSValue: %s", ivarsObj.objCType);
ASDisplayNodeCAssert(resultCount + count < kMaxDealloc2MainIvarsPerClassTree, @"More than %d dealloc2main ivars are not supported. Count: %d", kMaxDealloc2MainIvarsPerClassTree, resultCount + count);
[ivarsObj getValue:resultIvars + resultCount];
resultCount += count;
}

// Now gather ivars from this particular class.
unsigned int allMyIvarsCount;
Ivar *allMyIvars = class_copyIvarList(self, &allMyIvarsCount);

for (NSUInteger i = 0; i < allMyIvarsCount; i++) {
Ivar ivar = allMyIvars[i];
const char *type = ivar_getTypeEncoding(ivar);

if (type != NULL && strcmp(type, @encode(id)) == 0) {
// If it's `id` we have to include it just in case.
resultIvars[resultCount] = ivar;
resultCount += 1;
as_log_debug(ASMainThreadDeallocationLog(), "%@: Marking ivar '%s' for possible main deallocation due to type id", self, ivar_getName(ivar));
} else {
// If it's an ivar with a static type, check the type.
Class c = ASGetClassFromType(type);
if (ASClassRequiresMainThreadDeallocation(c)) {
resultIvars[resultCount] = ivar;
resultCount += 1;
as_log_debug(ASMainThreadDeallocationLog(), "%@: Marking ivar '%s' for main deallocation due to class %@", self, ivar_getName(ivar), c);
} else {
as_log_debug(ASMainThreadDeallocationLog(), "%@: Skipping ivar '%s' for main deallocation.", self, ivar_getName(ivar));
}
}
}
free(allMyIvars);

// Encode the type (array of Ivars) into a string and wrap it in an NSValue
char arrayType[32];
snprintf(arrayType, 32, "[%u^{objc_ivar}]", resultCount);
result = [NSValue valueWithBytes:resultIvars objCType:arrayType];

[ivarsCache setObject:result forKey:self];
return result;
}

#pragma mark - Loading

- (BOOL)_locked_shouldLoadViewOrLayer
Expand Down
3 changes: 3 additions & 0 deletions Source/ASDisplayNodeExtras.mm
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ extern void ASPerformMainThreadDeallocation(id _Nullable __strong * _Nonnull obj
});

if (objectPtr != NULL && *objectPtr != nil) {
// TODO: If ASRunLoopQueue supported an "unsafe_unretained" mode, we could
// transfer the caller's +1 into it and save the retain/release pair.

// Lock queue while enqueuing and releasing, so that there's no risk
// that the queue will release before we get a chance to release.
[queue lock];
Expand Down
47 changes: 47 additions & 0 deletions Source/ASMainThreadDeallocation.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// ASMainThreadDeallocation.h
// Texture
//
// Copyright (c) 2018-present, Pinterest, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface NSObject (ASMainThreadIvarTeardown)

/**
* Call this from -dealloc to schedule this instance's
* ivars for main thread deallocation as needed.
*
* This method includes a check for whether it's on the main thread,
* and it will do nothing in that case.
*/
- (void)scheduleIvarsForMainThreadDeallocation;

@end

@interface NSObject (ASNeedsMainThreadDeallocation)

/**
* Override this property to indicate that instances of this
* class need to be deallocated on the main thread.
* You do not access this property yourself.
*
* The NSObject implementation returns NO if the class name has
* a prefix UI, AV, or CA. This property is also overridden to
* return fixed values for other common classes, such as UIImage,
* UIGestureRecognizer, and UIResponder.
*/
@property (class, readonly) BOOL needsMainThreadDeallocation;

@end


NS_ASSUME_NONNULL_END
Loading