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

Fix crash setting attributed text on multiple threads #1141

Merged
merged 1 commit into from
Oct 9, 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
- Remove display node's reliance on shared_ptr. [Adlai Holler](https://github.com/Adlai-Holler)
- [ASCollectionView] Fix a crash that is caused by clearing a collection view's data while it's still being used. [Huy Nguyen](https://github.com/nguyenhuy) [#1154](https://github.com/TextureGroup/Texture/pull/1154)
- Clean up timing of layout tree flattening/ copying of unflattened tree for Weaver. [Michael Zuccarino](https://github.com/mikezucc) [#1157](https://github.com/TextureGroup/Texture/pull/1157)
- Fix crash setting attributed text on multiple threads [Michael Schneider](https://github.com/maicki)

## 2.7
- Fix pager node for interface coalescing. [Max Wang](https://github.com/wsdwsd0829) [#877](https://github.com/TextureGroup/Texture/pull/877)
Expand Down
5 changes: 5 additions & 0 deletions Source/ASDisplayNode+Layout.mm
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ - (BOOL)implementsLayoutMethod
- (ASLayoutElementStyle *)style
{
ASDN::MutexLocker l(__instanceLock__);
return [self _locked_style];
}

- (ASLayoutElementStyle *)_locked_style
{
if (_style == nil) {
_style = [[ASLayoutElementStyle alloc] init];
}
Expand Down
56 changes: 33 additions & 23 deletions Source/ASTextNode.mm
Original file line number Diff line number Diff line change
Expand Up @@ -440,39 +440,46 @@ - (void)setAttributedText:(NSAttributedString *)attributedText
if (attributedText == nil) {
attributedText = [[NSAttributedString alloc] initWithString:@"" attributes:nil];
}

// Don't hold textLock for too long.

{
ASLockScopeSelf();
if (ASObjectIsEqual(attributedText, _attributedText)) {
return;
}

_attributedText = ASCleanseAttributedStringOfCoreTextAttributes(attributedText);
#if AS_TEXTNODE_RECORD_ATTRIBUTED_STRINGS
[ASTextNode _registerAttributedText:_attributedText];
#endif
NSAttributedString *cleanedAttributedString = ASCleanseAttributedStringOfCoreTextAttributes(attributedText);

// Invalidating the truncation text must be done while we still hold the lock. Because after we release it,
// another thread may set a new truncation text that will then be cleared by this thread, other may draw
// this soon-to-be-invalidated text.
[self _locked_invalidateTruncationText];
Copy link
Member

@nguyenhuy nguyenhuy Sep 30, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, so invalidating the truncation text must be done while we still hold the lock. Because after we release it, another thread may set a new truncation text that will then be cleared by this thread, or may draw this soon-to-be-invalidated text.

Let's leave a comment/explaination here.


NSUInteger length = cleanedAttributedString.length;
if (length > 0) {
// Updating ascender and descender in one transaction while holding the lock.
ASLayoutElementStyle *style = [self _locked_style];
style.ascender = [[self class] ascenderWithAttributedString:cleanedAttributedString];
style.descender = [[attributedText attribute:NSFontAttributeName atIndex:cleanedAttributedString.length - 1 effectiveRange:NULL] descender];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same deal with ascender and descender here. We basically use the lock as a transactional one.

}

// Update attributed text with cleaned attributed string
_attributedText = cleanedAttributedString;
}

// Since truncation text matches style of attributedText, invalidate it now.
[self _invalidateTruncationText];

NSUInteger length = _attributedText.length;
if (length > 0) {
self.style.ascender = [[self class] ascenderWithAttributedString:_attributedText];
self.style.descender = [[_attributedText attribute:NSFontAttributeName atIndex:length - 1 effectiveRange:NULL] descender];
}

// Tell the display node superclasses that the cached layout is incorrect now
[self setNeedsLayout];

// Force display to create renderer with new size and redisplay with new string
[self setNeedsDisplay];


// Accessiblity
self.accessibilityLabel = _attributedText.string;
self.isAccessibilityElement = (length != 0); // We're an accessibility element by default if there is a string.
let currentAttributedText = self.attributedText; // Grab attributed string again in case it changed in the meantime
self.accessibilityLabel = currentAttributedText.string;
self.isAccessibilityElement = (currentAttributedText.length != 0); // We're an accessibility element by default if there is a string.

#if AS_TEXTNODE_RECORD_ATTRIBUTED_STRINGS
[ASTextNode _registerAttributedText:_attributedText];
#endif
}

#pragma mark - Text Layout
Expand Down Expand Up @@ -1166,13 +1173,15 @@ - (void)setTruncationAttributedText:(NSAttributedString *)truncationAttributedTe
{
if (ASLockedSelfCompareAssignCopy(_truncationAttributedText, truncationAttributedText)) {
[self _invalidateTruncationText];
[self setNeedsDisplay];
}
}

- (void)setAdditionalTruncationMessage:(NSAttributedString *)additionalTruncationMessage
{
if (ASLockedSelfCompareAssignCopy(_additionalTruncationMessage, additionalTruncationMessage)) {
[self _invalidateTruncationText];
[self setNeedsDisplay];
}
}

Expand Down Expand Up @@ -1231,12 +1240,13 @@ - (NSUInteger)lineCount

- (void)_invalidateTruncationText
{
{
ASLockScopeSelf();
_composedTruncationText = nil;
}
ASLockScopeSelf();
[self _locked_invalidateTruncationText];
}

[self setNeedsDisplay];
- (void)_locked_invalidateTruncationText
{
_composedTruncationText = nil;
}

/**
Expand Down
16 changes: 11 additions & 5 deletions Source/ASTextNode2.mm
Original file line number Diff line number Diff line change
Expand Up @@ -280,12 +280,13 @@ - (void)setAttributedText:(NSAttributedString *)attributedText
}

// Since truncation text matches style of attributedText, invalidate it now.
[self _invalidateTruncationText];
[self _locked_invalidateTruncationText];

NSUInteger length = attributedText.length;
if (length > 0) {
self.style.ascender = [[self class] ascenderWithAttributedString:attributedText];
self.style.descender = [[attributedText attribute:NSFontAttributeName atIndex:attributedText.length - 1 effectiveRange:NULL] descender];
ASLayoutElementStyle *style = [self _locked_style];
style.ascender = [[self class] ascenderWithAttributedString:attributedText];
style.descender = [[attributedText attribute:NSFontAttributeName atIndex:attributedText.length - 1 effectiveRange:NULL] descender];
}

// Tell the display node superclasses that the cached layout is incorrect now
Expand Down Expand Up @@ -1061,10 +1062,15 @@ - (NSUInteger)lineCount
- (void)_invalidateTruncationText
{
ASLockScopeSelf();
_textContainer.truncationToken = nil;
[self _locked_invalidateTruncationText];
[self setNeedsDisplay];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it actually ok to call -setNeedsDisplay here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I asked that myself too, but I didn't want to change it within this diff. In theory the code in ASTextNode2 should be adjusted to the same as in ASTextNode after this lands.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

}

- (void)_locked_invalidateTruncationText
{
_textContainer.truncationToken = nil;
}

/**
* @return the additional truncation message range within the as-rendered text.
* Must be called from main thread
Expand Down
9 changes: 9 additions & 0 deletions Source/Private/ASDisplayNodeInternal.h
Original file line number Diff line number Diff line change
Expand Up @@ -386,4 +386,13 @@ AS_EXTERN NSString * const ASRenderingEngineDidDisplayNodesScheduledBeforeTimest

@end

@interface ASDisplayNode (ASLayoutElementPrivate)

/**
* Returns the internal style object or creates a new if no exists. Need to be called with lock held.
*/
- (ASLayoutElementStyle *)_locked_style;

@end

NS_ASSUME_NONNULL_END