Skip to content

Commit

Permalink
Generalize the main thread ivar dealloc stuff (#959)
Browse files Browse the repository at this point in the history
  • Loading branch information
Adlai-Holler committed Jun 7, 2018
1 parent 35d59ac commit f6c5dc3
Show file tree
Hide file tree
Showing 10 changed files with 269 additions and 142 deletions.
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

0 comments on commit f6c5dc3

Please sign in to comment.