├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FBRetainCycleDetector.podspec ├── FBRetainCycleDetector.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ ├── FBRetainCycleDetector.xcscheme │ └── FBRetainCycleDetectorTests.xcscheme ├── FBRetainCycleDetector ├── Associations │ ├── FBAssociationManager.h │ ├── FBAssociationManager.mm │ └── Internal │ │ └── FBAssociationManager+Internal.h ├── Detector │ ├── FBNodeEnumerator.h │ ├── FBNodeEnumerator.mm │ ├── FBRetainCycleDetector+Internal.h │ ├── FBRetainCycleDetector.h │ └── FBRetainCycleDetector.mm ├── FBRetainCycleDetector-Info.plist ├── FBRetainCycleUtils.h ├── FBRetainCycleUtils.m ├── Filtering │ ├── FBStandardGraphEdgeFilters.h │ └── FBStandardGraphEdgeFilters.mm ├── Graph │ ├── FBObjectGraphConfiguration.h │ ├── FBObjectGraphConfiguration.m │ ├── FBObjectiveCBlock.h │ ├── FBObjectiveCBlock.m │ ├── FBObjectiveCGraphElement.h │ ├── FBObjectiveCGraphElement.mm │ ├── FBObjectiveCObject.h │ ├── FBObjectiveCObject.m │ ├── Internal │ │ └── FBObjectiveCGraphElement+Internal.h │ └── Specialization │ │ ├── FBObjectiveCNSCFTimer.h │ │ └── FBObjectiveCNSCFTimer.mm └── Layout │ ├── Blocks │ ├── Circle-LICENSE │ ├── FBBlockInterface.h │ ├── FBBlockStrongLayout.h │ ├── FBBlockStrongLayout.m │ ├── FBBlockStrongRelationDetector.h │ └── FBBlockStrongRelationDetector.m │ └── Classes │ ├── FBClassStrongLayout.h │ ├── FBClassStrongLayout.mm │ ├── FBClassStrongLayoutHelpers.h │ ├── FBClassStrongLayoutHelpers.m │ ├── FBClassSwiftHelpers.h │ ├── FBClassSwiftHelpers.mm │ ├── Parser │ ├── BaseType.h │ ├── FBStructEncodingParser.h │ ├── FBStructEncodingParser.mm │ ├── Struct.h │ ├── Struct.mm │ └── Type.h │ ├── Reference │ ├── FBIvarReference.h │ ├── FBIvarReference.m │ ├── FBObjectInStructReference.h │ ├── FBObjectInStructReference.m │ ├── FBObjectReference.h │ ├── FBObjectReferenceWithLayout.h │ ├── FBSwiftReference.h │ └── FBSwiftReference.m │ ├── SwiftIntrospectionKit.swift │ └── SwiftIntrospector.swift ├── FBRetainCycleDetectorTests ├── FBAssociationManagerTests.mm ├── FBBlockRecognizingTests.mm ├── FBBlockStrongLayoutTests.mm ├── FBClassStrongLayoutTests.mm ├── FBGraphEdgeFilterTests.m ├── FBObjectiveCBlockTests.m ├── FBObjectiveCNSCFTimerTests.m ├── FBObjectiveCObjectTests.m ├── FBRCDCollectionTests.m ├── FBRetainCycleDetectorTests-Bridging-Header.h ├── FBRetainCycleDetectorTests-Info.plist ├── FBRetainCycleDetectorTests.mm ├── FBRetainCycleSwiftDetectorTests.swift ├── FBStructEncodingParserTests.mm ├── FBSwiftReferenceTest.swift ├── RCDObjectWrapperTestClass.h └── RCDObjectWrapperTestClass.mm ├── LICENSE ├── README.md ├── build.sh ├── github └── workflows │ └── ci.yml ├── rcd_fishhook ├── LICENSE ├── README.md ├── rcd_fishhook.c └── rcd_fishhook.h └── update_fishhook.sh /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ $default-branch ] 6 | pull_request: 7 | branches: [ $default-branch ] 8 | workflow_dispatch: 9 | branches: [ $default-branch ] 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: macos-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Build 19 | run: ./build.sh 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData 8 | 9 | ## Filesystem 10 | *.DS_Store 11 | 12 | ## Various settings 13 | *.pbxuser 14 | !default.pbxuser 15 | *.mode1v3 16 | !default.mode1v3 17 | *.mode2v3 18 | !default.mode2v3 19 | *.perspectivev3 20 | !default.perspectivev3 21 | xcuserdata 22 | xcuserdata/ 23 | 24 | ## Other 25 | *.xccheckout 26 | *.moved-aside 27 | *.xcuserstate 28 | *.xcscmblueprint 29 | 30 | ## Obj-C/Swift specific 31 | *.hmap 32 | *.ipa 33 | 34 | # CocoaPods 35 | # 36 | # We recommend against adding the Pods directory to your .gitignore. However 37 | # you should judge for yourself, the pros and cons are mentioned at: 38 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 39 | # 40 | Pods/ 41 | 42 | # Carthage 43 | # 44 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 45 | Carthage/Checkouts 46 | 47 | Carthage/Build 48 | .DS_Store 49 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | Facebook has adopted a Code of Conduct that we expect project participants to adhere to. Please [read the full text](https://code.fb.com/codeofconduct) so that you can understand what actions will and will not be tolerated. -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to FBRetainCycleDetector 2 | We want to make contributing to this project as easy and transparent as 3 | possible. 4 | 5 | ## Pull Requests 6 | We actively welcome your pull requests. 7 | 8 | 1. Fork the repo and create your branch from `main`. 9 | 2. If you've added code that should be tested, add tests. 10 | 3. If you've changed APIs, update the documentation. 11 | 4. Ensure the test suite passes. 12 | 6. If you haven't already, complete the Contributor License Agreement ("CLA"). 13 | 14 | ## Contributor License Agreement ("CLA") 15 | In order to accept your pull request, we need you to submit a CLA. You only need 16 | to do this once to work on any of Facebook's open source projects. 17 | 18 | Complete your CLA here: 19 | 20 | ## Issues 21 | We use GitHub issues to track public bugs. Please ensure your description is 22 | clear and has sufficient instructions to be able to reproduce the issue. 23 | 24 | Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe 25 | disclosure of security bugs. In those cases, please go through the process 26 | outlined on that page and do not file a public issue. 27 | 28 | ## Coding Style 29 | * 2 spaces for indentation rather than tabs 30 | 31 | ## License 32 | By contributing to FBRetainCycleDetector, you agree that your contributions will be licensed 33 | under its BSD license. 34 | -------------------------------------------------------------------------------- /FBRetainCycleDetector.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "FBRetainCycleDetector" 3 | s.version = "0.1.4" 4 | s.summary = "Library that helps with detecting retain cycles in iOS apps" 5 | s.homepage = "https://github.com/facebook/FBRetainCycleDetector" 6 | s.license = "BSD" 7 | s.author = { "Grzegorz Pstrucha" => "gricha@fb.com" } 8 | s.platform = :ios, "7.0" 9 | s.source = { 10 | :git => "https://github.com/facebook/FBRetainCycleDetector.git", 11 | :tag => "0.1.4" 12 | } 13 | s.source_files = "FBRetainCycleDetector", "{FBRetainCycleDetector,rcd_fishhook}/**/*.{h,m,mm,c}" 14 | 15 | mrr_files = [ 16 | 'FBRetainCycleDetector/Associations/FBAssociationManager.h', 17 | 'FBRetainCycleDetector/Associations/FBAssociationManager.mm', 18 | 'FBRetainCycleDetector/Layout/Blocks/FBBlockStrongLayout.h', 19 | 'FBRetainCycleDetector/Layout/Blocks/FBBlockStrongLayout.m', 20 | 'FBRetainCycleDetector/Layout/Blocks/FBBlockStrongRelationDetector.h', 21 | 'FBRetainCycleDetector/Layout/Blocks/FBBlockStrongRelationDetector.m', 22 | 'FBRetainCycleDetector/Layout/Classes/FBClassStrongLayoutHelpers.h', 23 | 'FBRetainCycleDetector/Layout/Classes/FBClassStrongLayoutHelpers.m', 24 | ] 25 | 26 | files = Pathname.glob("FBRetainCycleDetector/**/*.{h,m,mm}") 27 | files = files.map {|file| file.to_path} 28 | files = files.reject {|file| mrr_files.include?(file)} 29 | 30 | s.requires_arc = files.sort + [ 31 | 'rcd_fishhook/**/*.{c,h}' 32 | ] 33 | s.public_header_files = [ 34 | 'FBRetainCycleDetector/Detector/FBRetainCycleDetector.h', 35 | 'FBRetainCycleDetector/Associations/FBAssociationManager.h', 36 | 'FBRetainCycleDetector/Graph/FBObjectiveCBlock.h', 37 | 'FBRetainCycleDetector/Graph/FBObjectiveCGraphElement.h', 38 | 'FBRetainCycleDetector/Graph/Specialization/FBObjectiveCNSCFTimer.h', 39 | 'FBRetainCycleDetector/Graph/FBObjectiveCObject.h', 40 | 'FBRetainCycleDetector/Graph/FBObjectGraphConfiguration.h', 41 | 'FBRetainCycleDetector/Filtering/FBStandardGraphEdgeFilters.h', 42 | ] 43 | 44 | s.framework = "Foundation", "CoreGraphics", "UIKit" 45 | s.library = 'c++' 46 | end 47 | -------------------------------------------------------------------------------- /FBRetainCycleDetector.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /FBRetainCycleDetector.xcodeproj/xcshareddata/xcschemes/FBRetainCycleDetector.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /FBRetainCycleDetector.xcodeproj/xcshareddata/xcschemes/FBRetainCycleDetectorTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 14 | 15 | 17 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 39 | 40 | 41 | 42 | 48 | 49 | 51 | 52 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Associations/FBAssociationManager.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import 10 | 11 | /** 12 | FBAssociationManager is a tracker of object associations. For given object it can return all objects that 13 | are being retained by this object with objc_setAssociatedObject & retain policy. 14 | */ 15 | @interface FBAssociationManager : NSObject 16 | 17 | /** 18 | Start tracking associations. It will use fishhook to swizzle C methods: 19 | objc_(set/remove)AssociatedObject and inject some tracker code. 20 | */ 21 | + (void)hook; 22 | 23 | /** 24 | Stop tracking associations, fishhooks. 25 | */ 26 | + (void)unhook; 27 | 28 | /** 29 | For given object return all objects that are retained by it using associated objects. 30 | 31 | @return NSArray of objects associated with given object 32 | */ 33 | + (nullable NSArray *)associationsForObject:(nullable id)object; 34 | 35 | @end 36 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Associations/FBAssociationManager.mm: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #if __has_feature(objc_arc) 10 | #error This file must be compiled with MRR. Use -fno-objc-arc flag. 11 | #endif 12 | 13 | #import 14 | #import 15 | #import 16 | #import 17 | #import 18 | #import 19 | #import 20 | 21 | #import "FBAssociationManager+Internal.h" 22 | 23 | #import "rcd_fishhook.h" 24 | 25 | #if _INTERNAL_RCD_ENABLED 26 | 27 | namespace FB { namespace AssociationManager { 28 | using ObjectAssociationSet = std::unordered_set; 29 | using AssociationMap = std::unordered_map; 30 | 31 | static auto _associationMap = new AssociationMap(); 32 | static auto _associationMutex = new std::mutex; 33 | 34 | static std::mutex *hookMutex(new std::mutex); 35 | static bool hookTaken = false; 36 | 37 | void _threadUnsafeResetAssociationAtKey(id object, void *key) { 38 | auto i = _associationMap->find(object); 39 | 40 | if (i == _associationMap->end()) { 41 | return; 42 | } 43 | i->second->erase(key); 44 | } 45 | 46 | void _threadUnsafeSetStrongAssociation(id object, void *key, id value) { 47 | if (value) { 48 | auto i = _associationMap->find(object); 49 | ObjectAssociationSet *refs; 50 | if (i != _associationMap->end()) { 51 | refs = i->second; 52 | } else { 53 | refs = new ObjectAssociationSet; 54 | (*_associationMap)[object] = refs; 55 | } 56 | refs->insert(key); 57 | } else { 58 | _threadUnsafeResetAssociationAtKey(object, key); 59 | } 60 | } 61 | 62 | void _threadUnsafeRemoveAssociations(id object) { 63 | if (_associationMap->size() == 0 ){ 64 | return; 65 | } 66 | 67 | auto i = _associationMap->find(object); 68 | if (i == _associationMap->end()) { 69 | return; 70 | } 71 | 72 | auto *refs = i->second; 73 | delete refs; 74 | _associationMap->erase(i); 75 | } 76 | 77 | NSArray *associations(id object) { 78 | std::lock_guard l(*_associationMutex); 79 | if (_associationMap->size() == 0 ){ 80 | return nil; 81 | } 82 | 83 | auto i = _associationMap->find(object); 84 | if (i == _associationMap->end()) { 85 | return nil; 86 | } 87 | 88 | auto *refs = i->second; 89 | 90 | NSMutableArray *array = [NSMutableArray array]; 91 | for (auto &key: *refs) { 92 | id value = objc_getAssociatedObject(object, key); 93 | if (value) { 94 | [array addObject:value]; 95 | } 96 | } 97 | 98 | return array; 99 | } 100 | 101 | static void (*fb_orig_objc_setAssociatedObject)(id object, void *key, id value, objc_AssociationPolicy policy); 102 | static void (*fb_orig_objc_removeAssociatedObjects)(id object); 103 | 104 | static void fb_objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy) { 105 | { 106 | std::lock_guard l(*_associationMutex); 107 | // Track strong references only 108 | if (policy == OBJC_ASSOCIATION_RETAIN || 109 | policy == OBJC_ASSOCIATION_RETAIN_NONATOMIC) { 110 | _threadUnsafeSetStrongAssociation(object, key, value); 111 | } else { 112 | // We can change the policy, we need to clear out the key 113 | _threadUnsafeResetAssociationAtKey(object, key); 114 | } 115 | } 116 | 117 | /** 118 | We are doing that behind the lock. Otherwise it could deadlock. 119 | The reason for that is when objc calls up _object_set_associative_reference, when we nil out 120 | a reference for some object, it will also release this value, which could cause it to dealloc. 121 | This is done inside _object_set_associative_reference without lock. Otherwise it would deadlock, 122 | since the object that is released, could also clean up some associated objects. 123 | 124 | If we would keep a lock during that, we would fall for that deadlock. 125 | 126 | Unfortunately this also means the association manager can be not a 100% accurate, since there 127 | can technically be a race condition between setting values on the same object and same key from 128 | different threads. (One thread sets value, other nil, we are missing this value) 129 | */ 130 | fb_orig_objc_setAssociatedObject(object, key, value, policy); 131 | } 132 | 133 | static void fb_objc_removeAssociatedObjects(id object) { 134 | { 135 | std::lock_guard l(*_associationMutex); 136 | _threadUnsafeRemoveAssociations(object); 137 | } 138 | 139 | fb_orig_objc_removeAssociatedObjects(object); 140 | } 141 | 142 | static void cleanUp() { 143 | std::lock_guard l(*_associationMutex); 144 | _associationMap->clear(); 145 | } 146 | 147 | } } 148 | 149 | #endif 150 | 151 | @implementation FBAssociationManager 152 | 153 | + (void)hook 154 | { 155 | #if _INTERNAL_RCD_ENABLED 156 | std::lock_guard l(*FB::AssociationManager::hookMutex); 157 | rcd_rebind_symbols((struct rcd_rebinding[2]){ 158 | { 159 | "objc_setAssociatedObject", 160 | (void *)FB::AssociationManager::fb_objc_setAssociatedObject, 161 | (void **)&FB::AssociationManager::fb_orig_objc_setAssociatedObject 162 | }, 163 | { 164 | "objc_removeAssociatedObjects", 165 | (void *)FB::AssociationManager::fb_objc_removeAssociatedObjects, 166 | (void **)&FB::AssociationManager::fb_orig_objc_removeAssociatedObjects 167 | }}, 2); 168 | FB::AssociationManager::hookTaken = true; 169 | #endif //_INTERNAL_RCD_ENABLED 170 | } 171 | 172 | + (void)unhook 173 | { 174 | #if _INTERNAL_RCD_ENABLED 175 | std::lock_guard l(*FB::AssociationManager::hookMutex); 176 | if (FB::AssociationManager::hookTaken) { 177 | rcd_rebind_symbols((struct rcd_rebinding[2]){ 178 | { 179 | "objc_setAssociatedObject", 180 | (void *)FB::AssociationManager::fb_orig_objc_setAssociatedObject, 181 | }, 182 | { 183 | "objc_removeAssociatedObjects", 184 | (void *)FB::AssociationManager::fb_orig_objc_removeAssociatedObjects, 185 | }}, 2); 186 | FB::AssociationManager::cleanUp(); 187 | } 188 | #endif //_INTERNAL_RCD_ENABLED 189 | } 190 | 191 | + (NSArray *)associationsForObject:(id)object 192 | { 193 | #if _INTERNAL_RCD_ENABLED 194 | return FB::AssociationManager::associations(object); 195 | #else 196 | return nil; 197 | #endif //_INTERNAL_RCD_ENABLED 198 | } 199 | 200 | @end 201 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Associations/Internal/FBAssociationManager+Internal.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import "FBAssociationManager.h" 10 | #import "FBRetainCycleDetector.h" 11 | 12 | #if _INTERNAL_RCD_ENABLED 13 | 14 | namespace FB { namespace AssociationManager { 15 | 16 | void _threadUnsafeResetAssociationAtKey(id object, void *key); 17 | void _threadUnsafeSetStrongAssociation(id object, void *key, id value); 18 | void _threadUnsafeRemoveAssociations(id object); 19 | 20 | NSArray *associations(id object); 21 | 22 | } } 23 | 24 | #endif 25 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Detector/FBNodeEnumerator.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import 10 | 11 | @class FBObjectiveCGraphElement; 12 | 13 | /** 14 | FBNodeEnumerator wraps any object graph element (FBObjectiveCGraphElement) and lets you enumerate over its 15 | retained references 16 | */ 17 | @interface FBNodeEnumerator : NSEnumerator 18 | 19 | /** 20 | Designated initializer 21 | */ 22 | - (nonnull instancetype)initWithObject:(nonnull FBObjectiveCGraphElement *)object; 23 | 24 | - (nullable FBNodeEnumerator *)nextObject; 25 | 26 | @property (nonatomic, strong, readonly, nonnull) FBObjectiveCGraphElement *object; 27 | 28 | @end 29 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Detector/FBNodeEnumerator.mm: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import "FBNodeEnumerator.h" 10 | 11 | #import "FBObjectiveCGraphElement.h" 12 | 13 | @implementation FBNodeEnumerator 14 | { 15 | NSSet *_retainedObjectsSnapshot; 16 | NSEnumerator *_enumerator; 17 | } 18 | 19 | - (instancetype)initWithObject:(FBObjectiveCGraphElement *)object 20 | { 21 | if (self = [super init]) { 22 | _object = object; 23 | } 24 | 25 | return self; 26 | } 27 | 28 | - (FBNodeEnumerator *)nextObject 29 | { 30 | if (!_object) { 31 | return nil; 32 | } else if (!_retainedObjectsSnapshot) { 33 | _retainedObjectsSnapshot = [_object allRetainedObjects]; 34 | _enumerator = [_retainedObjectsSnapshot objectEnumerator]; 35 | } 36 | 37 | FBObjectiveCGraphElement *next = [_enumerator nextObject]; 38 | 39 | if (next) { 40 | return [[FBNodeEnumerator alloc] initWithObject:next]; 41 | } 42 | 43 | return nil; 44 | } 45 | 46 | - (BOOL)isEqual:(id)object 47 | { 48 | if ([object isKindOfClass:[FBNodeEnumerator class]]) { 49 | FBNodeEnumerator *enumerator = (FBNodeEnumerator *)object; 50 | return [self.object isEqual:enumerator.object]; 51 | } 52 | 53 | return NO; 54 | } 55 | 56 | - (NSUInteger)hash 57 | { 58 | return [self.object hash]; 59 | } 60 | 61 | @end 62 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Detector/FBRetainCycleDetector+Internal.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import "FBRetainCycleDetector.h" 10 | 11 | @interface FBRetainCycleDetector () 12 | 13 | // Unit tests 14 | - (NSArray *)_shiftToUnifiedCycle:(NSArray *)array; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Detector/FBRetainCycleDetector.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import 10 | 11 | //! Project version number for FBRetainCycleDetector. 12 | FOUNDATION_EXPORT double FBRetainCycleDetectorVersionNumber; 13 | 14 | //! Project version string for FBRetainCycleDetector. 15 | FOUNDATION_EXPORT const unsigned char FBRetainCycleDetectorVersionString[]; 16 | 17 | #import 18 | #import 19 | #import 20 | #import 21 | #import 22 | #import 23 | #import 24 | 25 | /** 26 | Retain Cycle Detector is enabled by default in DEBUG builds, but you can also force it in other builds by 27 | uncommenting the line below. Beware, Retain Cycle Detector uses some private APIs that shouldn't be compiled in 28 | production builds. 29 | */ 30 | //#define RETAIN_CYCLE_DETECTOR_ENABLED 1 31 | 32 | /** 33 | FBRetainCycleDetector 34 | 35 | The main class responsible for detecting retain cycles. 36 | 37 | Be cautious, the class is NOT thread safe. 38 | 39 | The process of detecting retain cycles is relatively slow and consumes a lot of CPU. 40 | */ 41 | 42 | @interface FBRetainCycleDetector : NSObject 43 | 44 | /** 45 | Designated initializer 46 | 47 | @param configuration Configuration for detector. Can include specific filters and options. 48 | @see FBRetainCycleDetectorConfiguration 49 | */ 50 | - (nonnull instancetype)initWithConfiguration:(nonnull FBObjectGraphConfiguration *)configuration NS_DESIGNATED_INITIALIZER; 51 | 52 | /** 53 | Adds candidate you are interested in getting retain cycles from. 54 | 55 | @param candidate Any Objective-C object you want to verify for cycles. 56 | */ 57 | - (void)addCandidate:(nonnull id)candidate; 58 | 59 | /** 60 | Searches for all retain cycles for all candidates the detector has been 61 | provided with. 62 | 63 | @return NSSet with retain cycles. An element of this array will be 64 | an array representing retain cycle. That array will hold elements 65 | of type FBObjectiveCGraphElement. 66 | 67 | @discussion For given candidate, the detector will go through all object graph rooted in this candidate and return 68 | ALL retain cycles that this candidate references. It will also take care of removing duplicates. It will not look for 69 | cycles longer than 10 elements. If you want to look for longer ones use findRetainCyclesWithMaxCycleLenght: 70 | */ 71 | - (nonnull NSSet *> *)findRetainCycles; 72 | 73 | - (nonnull NSSet *> *)findRetainCyclesWithMaxCycleLength:(NSUInteger)length; 74 | 75 | /** 76 | This macro is used across FBRetainCycleDetector to compile out sensitive code. 77 | If you do not define it anywhere, Retain Cycle Detector will be available in DEBUG builds. 78 | */ 79 | #ifdef RETAIN_CYCLE_DETECTOR_ENABLED 80 | #define _INTERNAL_RCD_ENABLED RETAIN_CYCLE_DETECTOR_ENABLED 81 | #else 82 | #define _INTERNAL_RCD_ENABLED DEBUG 83 | #endif 84 | 85 | @end 86 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/FBRetainCycleDetector-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/FBRetainCycleUtils.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import 10 | 11 | @class FBObjectGraphConfiguration; 12 | @class FBObjectiveCGraphElement; 13 | 14 | #ifdef __cplusplus 15 | extern "C" { 16 | #endif 17 | 18 | /** 19 | Wrapper functions, for given object they will categorize it and create proper Graph Element subclass instance 20 | for it. 21 | */ 22 | FBObjectiveCGraphElement *_Nullable FBWrapObjectGraphElementWithContext(FBObjectiveCGraphElement *_Nullable sourceElement, 23 | id _Nullable object, 24 | FBObjectGraphConfiguration *_Nullable configuration, 25 | NSArray *_Nullable namePath); 26 | FBObjectiveCGraphElement *_Nullable FBWrapObjectGraphElement(FBObjectiveCGraphElement *_Nullable sourceElement, 27 | id _Nullable object, 28 | FBObjectGraphConfiguration *_Nullable configuration); 29 | 30 | #ifdef __cplusplus 31 | } 32 | #endif 33 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/FBRetainCycleUtils.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import "FBRetainCycleUtils.h" 10 | 11 | #import 12 | 13 | #import "FBBlockStrongLayout.h" 14 | #import "FBClassStrongLayout.h" 15 | #import "FBObjectiveCBlock.h" 16 | #import "FBObjectiveCGraphElement.h" 17 | #import "FBObjectiveCNSCFTimer.h" 18 | #import "FBObjectiveCObject.h" 19 | #import "FBObjectGraphConfiguration.h" 20 | 21 | static BOOL _ShouldBreakGraphEdge(FBObjectGraphConfiguration *configuration, 22 | FBObjectiveCGraphElement *fromObject, 23 | NSString *byIvar, 24 | Class toObjectOfClass) { 25 | for (FBGraphEdgeFilterBlock filterBlock in configuration.filterBlocks) { 26 | if (filterBlock(fromObject, byIvar, toObjectOfClass) == FBGraphEdgeInvalid) { 27 | return YES; 28 | } 29 | } 30 | 31 | return NO; 32 | } 33 | 34 | FBObjectiveCGraphElement *FBWrapObjectGraphElementWithContext(FBObjectiveCGraphElement *sourceElement, 35 | id object, 36 | FBObjectGraphConfiguration *configuration, 37 | NSArray *namePath) { 38 | if (_ShouldBreakGraphEdge(configuration, sourceElement, [namePath firstObject], object_getClass(object))) { 39 | return nil; 40 | } 41 | FBObjectiveCGraphElement *newElement; 42 | if (FBObjectIsBlock((__bridge void *)object)) { 43 | newElement = [[FBObjectiveCBlock alloc] initWithObject:object 44 | configuration:configuration 45 | namePath:namePath]; 46 | } else { 47 | if ([object_getClass(object) isSubclassOfClass:[NSTimer class]] && 48 | configuration.shouldInspectTimers) { 49 | newElement = [[FBObjectiveCNSCFTimer alloc] initWithObject:object 50 | configuration:configuration 51 | namePath:namePath]; 52 | } else { 53 | newElement = [[FBObjectiveCObject alloc] initWithObject:object 54 | configuration:configuration 55 | namePath:namePath]; 56 | } 57 | } 58 | return (configuration && configuration.transformerBlock) ? configuration.transformerBlock(newElement) : newElement; 59 | } 60 | 61 | FBObjectiveCGraphElement *FBWrapObjectGraphElement(FBObjectiveCGraphElement *sourceElement, 62 | id object, 63 | FBObjectGraphConfiguration *configuration) { 64 | return FBWrapObjectGraphElementWithContext(sourceElement, object, configuration, nil); 65 | } 66 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Filtering/FBStandardGraphEdgeFilters.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import 10 | 11 | #import 12 | 13 | #ifdef __cplusplus 14 | extern "C" { 15 | #endif 16 | 17 | /** 18 | Standard filters mostly filters excluding some UIKit references we have caught during testing on some apps. 19 | */ 20 | NSArray *_Nonnull FBGetStandardGraphEdgeFilters(void); 21 | 22 | /** 23 | Helper functions for some typical patterns. 24 | */ 25 | FBGraphEdgeFilterBlock _Nonnull FBFilterBlockWithObjectIvarRelation(Class _Nonnull aCls, 26 | NSString *_Nonnull ivarName); 27 | FBGraphEdgeFilterBlock _Nonnull FBFilterBlockWithObjectToManyIvarsRelation(Class _Nonnull aCls, 28 | NSSet *_Nonnull ivarNames); 29 | FBGraphEdgeFilterBlock _Nonnull FBFilterBlockWithObjectIvarObjectRelation(Class _Nonnull fromClass, 30 | NSString *_Nonnull ivarName, 31 | Class _Nonnull toClass); 32 | 33 | #ifdef __cplusplus 34 | } 35 | #endif 36 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Filtering/FBStandardGraphEdgeFilters.mm: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import "FBStandardGraphEdgeFilters.h" 10 | 11 | #import 12 | 13 | #import 14 | 15 | #import "FBObjectiveCGraphElement.h" 16 | #import "FBRetainCycleDetector.h" 17 | 18 | FBGraphEdgeFilterBlock FBFilterBlockWithObjectIvarRelation(Class aCls, NSString *ivarName) { 19 | return FBFilterBlockWithObjectToManyIvarsRelation(aCls, [NSSet setWithObject:ivarName]); 20 | } 21 | 22 | FBGraphEdgeFilterBlock FBFilterBlockWithObjectToManyIvarsRelation(Class aCls, 23 | NSSet *ivarNames) { 24 | return ^(FBObjectiveCGraphElement *fromObject, 25 | NSString *byIvar, 26 | Class toObjectOfClass){ 27 | if (aCls && 28 | [[fromObject objectClass] isSubclassOfClass:aCls]) { 29 | // If graph element holds metadata about an ivar, it will be held in the name path, as early as possible 30 | if ([ivarNames containsObject:byIvar]) { 31 | return FBGraphEdgeInvalid; 32 | } 33 | } 34 | return FBGraphEdgeValid; 35 | }; 36 | } 37 | 38 | FBGraphEdgeFilterBlock FBFilterBlockWithObjectIvarObjectRelation(Class fromClass, NSString *ivarName, Class toClass) { 39 | return ^(FBObjectiveCGraphElement *fromObject, 40 | NSString *byIvar, 41 | Class toObjectOfClass) { 42 | if (toClass && 43 | [toObjectOfClass isSubclassOfClass:toClass]) { 44 | return FBFilterBlockWithObjectIvarRelation(fromClass, ivarName)(fromObject, byIvar, toObjectOfClass); 45 | } 46 | return FBGraphEdgeValid; 47 | }; 48 | } 49 | 50 | NSArray *FBGetStandardGraphEdgeFilters() { 51 | #if _INTERNAL_RCD_ENABLED 52 | static Class heldActionClass; 53 | static Class transitionContextClass; 54 | static dispatch_once_t onceToken; 55 | dispatch_once(&onceToken, ^{ 56 | heldActionClass = NSClassFromString(@"UIHeldAction"); 57 | transitionContextClass = NSClassFromString(@"_UIViewControllerOneToOneTransitionContext"); 58 | }); 59 | 60 | return @[FBFilterBlockWithObjectIvarRelation([UIView class], @"_subviewCache"), 61 | FBFilterBlockWithObjectIvarRelation(heldActionClass, @"m_target"), 62 | FBFilterBlockWithObjectToManyIvarsRelation([UITouch class], 63 | [NSSet setWithArray:@[@"_view", 64 | @"_gestureRecognizers", 65 | @"_window", 66 | @"_warpedIntoView"]]), 67 | FBFilterBlockWithObjectToManyIvarsRelation(transitionContextClass, 68 | [NSSet setWithArray:@[@"_toViewController", 69 | @"_fromViewController"]]), 70 | FBFilterBlockWithObjectIvarRelation([UIGestureRecognizer class], @"_gestureEnvironment")]; 71 | #else 72 | return nil; 73 | #endif // _INTERNAL_RCD_ENABLED 74 | } 75 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Graph/FBObjectGraphConfiguration.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import 10 | 11 | #import "FBObjectiveCGraphElement.h" 12 | 13 | typedef NS_ENUM(NSUInteger, FBGraphEdgeType) { 14 | FBGraphEdgeValid, 15 | FBGraphEdgeInvalid, 16 | }; 17 | 18 | @protocol FBObjectReference; 19 | 20 | /** 21 | Every filter has to be of type FBGraphEdgeFilterBlock. Filter, given two object graph elements, it should decide, 22 | whether a reference between them should be filtered out or not. 23 | @see FBGetStandardGraphEdgeFilters() 24 | */ 25 | typedef FBGraphEdgeType (^FBGraphEdgeFilterBlock)(FBObjectiveCGraphElement *_Nullable fromObject, 26 | NSString *_Nullable byIvar, 27 | Class _Nullable toObjectOfClass); 28 | 29 | typedef FBObjectiveCGraphElement *_Nullable(^FBObjectiveCGraphElementTransformerBlock)(FBObjectiveCGraphElement *_Nonnull fromObject); 30 | 31 | /** 32 | FBObjectGraphConfiguration represents a configuration for object graph walking. 33 | It can hold filters and detector specific options. 34 | */ 35 | @interface FBObjectGraphConfiguration : NSObject 36 | 37 | /** 38 | Every block represents a filter that every reference must pass in order to be inspected. 39 | Reference will be described as relation from one object to another object. See definition of 40 | FBGraphEdgeFilterBlock above. 41 | 42 | Invalid relations would be the relations that we are guaranteed are going to be broken at some point. 43 | Be careful though, it's not so straightforward to tell if the relation will be broken *with 100% 44 | certainty*, and if you'll filter out something that could otherwise show retain cycle that leaks - 45 | it would never be caught by detector. 46 | 47 | For examples of what are the relations that will be broken at some point check FBStandardGraphEdgeFilters.mm 48 | */ 49 | @property (nonatomic, readonly, copy, nullable) NSArray *filterBlocks; 50 | 51 | @property (nonatomic, readonly, copy, nullable) FBObjectiveCGraphElementTransformerBlock transformerBlock; 52 | 53 | /** 54 | Decides if object graph walker should look for retain cycles inside NSTimers. 55 | */ 56 | @property (nonatomic, readonly) BOOL shouldInspectTimers; 57 | 58 | /** 59 | Decides if block objects should include their invocation address (the code part of the block) in the report. 60 | If set to YES, then it will change from: `MallocBlock` to `<>`. 61 | You can then symbolicate the address to retrieve a symbol name which will look like: 62 | `__FOO_block_invoke` where FOO is replaced by the function creating the block. 63 | This will allow easier understanding of the code involved in the cycle when blocks are involved. 64 | */ 65 | @property (nonatomic, readonly) BOOL shouldIncludeBlockAddress; 66 | 67 | /** 68 | To enabled the inclution of swift Objects in the object graph. 69 | */ 70 | @property (nonatomic, readonly) BOOL shouldIncludeSwiftObjects; 71 | 72 | /** 73 | Will cache layout 74 | */ 75 | @property (nonatomic, readonly, nullable) NSMutableDictionary> *> *layoutCache; 76 | @property (nonatomic, readonly) BOOL shouldCacheLayouts; 77 | 78 | - (nonnull instancetype)initWithFilterBlocks:(nonnull NSArray *)filterBlocks 79 | shouldInspectTimers:(BOOL)shouldInspectTimers 80 | transformerBlock:(nullable FBObjectiveCGraphElementTransformerBlock)transformerBlock 81 | shouldIncludeBlockAddress:(BOOL)shouldIncludeBlockAddress 82 | shouldIncludeSwiftObjects:(BOOL) shouldIncludeSwiftObjects NS_DESIGNATED_INITIALIZER; 83 | 84 | - (nonnull instancetype)initWithFilterBlocks:(nonnull NSArray *)filterBlocks 85 | shouldInspectTimers:(BOOL)shouldInspectTimers 86 | transformerBlock:(nullable FBObjectiveCGraphElementTransformerBlock)transformerBlock 87 | shouldIncludeBlockAddress:(BOOL)shouldIncludeBlockAddress; 88 | 89 | - (nonnull instancetype)initWithFilterBlocks:(nonnull NSArray *)filterBlocks 90 | shouldInspectTimers:(BOOL)shouldInspectTimers 91 | transformerBlock:(nullable FBObjectiveCGraphElementTransformerBlock)transformerBlock; 92 | 93 | - (nonnull instancetype)initWithFilterBlocks:(nonnull NSArray *)filterBlocks 94 | shouldInspectTimers:(BOOL)shouldInspectTimers; 95 | 96 | @end 97 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Graph/FBObjectGraphConfiguration.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import "FBObjectGraphConfiguration.h" 10 | 11 | @implementation FBObjectGraphConfiguration 12 | 13 | - (instancetype)initWithFilterBlocks:(NSArray *)filterBlocks 14 | shouldInspectTimers:(BOOL)shouldInspectTimers 15 | transformerBlock:(nullable FBObjectiveCGraphElementTransformerBlock)transformerBlock 16 | shouldIncludeBlockAddress:(BOOL)shouldIncludeBlockAddress 17 | shouldIncludeSwiftObjects:(BOOL)shouldIncludeSwiftObjects 18 | { 19 | if (self = [super init]) { 20 | _filterBlocks = [filterBlocks copy]; 21 | _shouldInspectTimers = shouldInspectTimers; 22 | _shouldIncludeBlockAddress = shouldIncludeBlockAddress; 23 | _shouldIncludeSwiftObjects = shouldIncludeSwiftObjects; 24 | _transformerBlock = [transformerBlock copy]; 25 | _layoutCache = [NSMutableDictionary new]; 26 | } 27 | 28 | return self; 29 | } 30 | 31 | - (instancetype)initWithFilterBlocks:(NSArray *)filterBlocks 32 | shouldInspectTimers:(BOOL)shouldInspectTimers 33 | transformerBlock:(nullable FBObjectiveCGraphElementTransformerBlock)transformerBlock 34 | shouldIncludeBlockAddress:(BOOL)shouldIncludeBlockAddress 35 | { 36 | if (self = [super init]) { 37 | _filterBlocks = [filterBlocks copy]; 38 | _shouldInspectTimers = shouldInspectTimers; 39 | _shouldIncludeBlockAddress = shouldIncludeBlockAddress; 40 | _shouldIncludeSwiftObjects = false; 41 | _transformerBlock = [transformerBlock copy]; 42 | _layoutCache = [NSMutableDictionary new]; 43 | } 44 | 45 | return self; 46 | } 47 | 48 | - (instancetype)initWithFilterBlocks:(NSArray *)filterBlocks 49 | shouldInspectTimers:(BOOL)shouldInspectTimers 50 | transformerBlock:(nullable FBObjectiveCGraphElementTransformerBlock)transformerBlock 51 | { 52 | return [self initWithFilterBlocks:filterBlocks 53 | shouldInspectTimers:shouldInspectTimers 54 | transformerBlock:transformerBlock 55 | shouldIncludeBlockAddress:NO 56 | shouldIncludeSwiftObjects:NO]; 57 | } 58 | 59 | - (instancetype)initWithFilterBlocks:(NSArray *)filterBlocks 60 | shouldInspectTimers:(BOOL)shouldInspectTimers 61 | { 62 | return [self initWithFilterBlocks:filterBlocks 63 | shouldInspectTimers:shouldInspectTimers 64 | transformerBlock:nil]; 65 | } 66 | 67 | - (instancetype)init 68 | { 69 | // By default we are inspecting timers 70 | return [self initWithFilterBlocks:@[] 71 | shouldInspectTimers:YES]; 72 | } 73 | 74 | @end 75 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Graph/FBObjectiveCBlock.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import 10 | 11 | #import "FBObjectiveCGraphElement.h" 12 | 13 | @class FBGraphEdgeFilterProvider; 14 | 15 | /** 16 | Object Graph element representing block. 17 | */ 18 | @interface FBObjectiveCBlock : FBObjectiveCGraphElement 19 | @end 20 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Graph/FBObjectiveCBlock.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import "FBObjectiveCBlock.h" 10 | 11 | #import 12 | 13 | #import "FBBlockStrongLayout.h" 14 | #import "FBBlockStrongRelationDetector.h" 15 | #import "FBObjectGraphConfiguration.h" 16 | #import "FBObjectiveCObject.h" 17 | #import "FBRetainCycleUtils.h" 18 | 19 | struct __attribute__((packed)) BlockLiteral { 20 | void *isa; 21 | int flags; 22 | int reserved; 23 | void *invoke; 24 | void *descriptor; 25 | }; 26 | 27 | @implementation FBObjectiveCBlock 28 | 29 | - (NSSet *)allRetainedObjects 30 | { 31 | NSMutableArray *results = [[[super allRetainedObjects] allObjects] mutableCopy]; 32 | 33 | // Grab a strong reference to the object, otherwise it can crash while doing 34 | // nasty stuff on deallocation 35 | __attribute__((objc_precise_lifetime)) id anObject = self.object; 36 | 37 | void *blockObjectReference = (__bridge void *)anObject; 38 | NSArray *allRetainedReferences = FBGetBlockStrongReferences(blockObjectReference); 39 | 40 | for (id object in allRetainedReferences) { 41 | FBObjectiveCGraphElement *element = FBWrapObjectGraphElement(self, object, self.configuration); 42 | if (element) { 43 | [results addObject:element]; 44 | } 45 | } 46 | 47 | return [NSSet setWithArray:results]; 48 | } 49 | 50 | /** 51 | * We want to add more information to blocks because they show up 52 | * in reports as MallocBlock and StackBlock which is not very informative. 53 | * 54 | * A block object is composed of: 55 | * - code: what should be executed, it's stored in the .TEXT section ; 56 | * - data: the variables that have been captured ; 57 | * - metadata: notably the function signature. 58 | * 59 | * We extract the address of the code, which can then be converted to a 60 | * human readable name given the debug symbol file. 61 | * 62 | * The symbol name contains the name of the function which allocated 63 | * the block, making is easier to track the piece of code participating 64 | * in the cycle. The symbolication must be done outside of this code 65 | * since it will require access to the debug symbols, not present at 66 | * runtime. 67 | * 68 | * Format: <> 69 | */ 70 | - (NSString *)classNameOrNull 71 | { 72 | /* @cwt-override FIXME[T168581563]: -Wnullable-to-nonnull-conversion */ 73 | #pragma clang diagnostic push 74 | #pragma clang diagnostic ignored "-Wnullable-to-nonnull-conversion" 75 | NSString *className = NSStringFromClass([self objectClass]); 76 | #pragma clang diagnostic pop 77 | if (!className) { 78 | className = @"(null)"; 79 | } 80 | 81 | if (!self.configuration.shouldIncludeBlockAddress) { 82 | return className; 83 | } 84 | 85 | // Find the reference of the block object. 86 | __attribute__((objc_precise_lifetime)) id anObject = self.object; 87 | if ([anObject isKindOfClass:[FBBlockStrongRelationDetector class]]) { 88 | FBBlockStrongRelationDetector *blockObject = anObject; 89 | anObject = [blockObject forwarding]; 90 | } 91 | void *blockObjectReference = (__bridge void *)anObject; 92 | if (!blockObjectReference) { 93 | return className; 94 | } 95 | 96 | // Extract the invocated block of code from the structure. 97 | const struct BlockLiteral *block = (struct BlockLiteral*) blockObjectReference; 98 | const void *blockCodePtr = block->invoke; 99 | 100 | return [NSString stringWithFormat:@"<<%@:0x%llx>>", className, (unsigned long long)blockCodePtr]; 101 | } 102 | 103 | @end 104 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Graph/FBObjectiveCGraphElement.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import 10 | 11 | @class FBObjectGraphConfiguration; 12 | 13 | /** 14 | Base Graph Element representation. It carries some data about the object and should be overridden in subclass 15 | to provide references that subclass holds strongly (different for blocks, objects, other specializations). 16 | The Graph Element itself can only provide references from FBAssociationManager. 17 | */ 18 | @interface FBObjectiveCGraphElement : NSObject 19 | 20 | /** 21 | Designated initializer. 22 | @param object Object this Graph Element will represent. 23 | @param configuration Provides detector's configuration that contains filters and options 24 | @param namePath Description of how the object was retrieved from it's parent. Check namePath property. 25 | */ 26 | - (nonnull instancetype)initWithObject:(nullable id)object 27 | configuration:(nonnull FBObjectGraphConfiguration *)configuration 28 | namePath:(nullable NSArray *)namePath; 29 | 30 | /** 31 | @param object Object this Graph Element will represent. 32 | @param configuration Provides detector's configuration that contains filters and options 33 | */ 34 | - (nonnull instancetype)initWithObject:(nullable id)object 35 | configuration:(nonnull FBObjectGraphConfiguration *)configuration; 36 | 37 | 38 | /** 39 | Name path that describes how this object was retrieved from its parent object by names 40 | (for example ivar names, struct references). For more check FBObjectReference protocol. 41 | */ 42 | @property (nonatomic, copy, readonly, nullable) NSArray *namePath; 43 | @property (nonatomic, weak, nullable) id object; 44 | @property (nonatomic, readonly, nonnull) FBObjectGraphConfiguration *configuration; 45 | 46 | /** 47 | Main accessor to all objects that the given object is retaining. Thread unsafe. 48 | 49 | @return NSSet of all objects this object is retaining. 50 | */ 51 | - (nullable NSSet *)allRetainedObjects; 52 | 53 | /** 54 | @return address of the object represented by this element 55 | */ 56 | - (size_t)objectAddress; 57 | 58 | /** 59 | @return class of the object 60 | */ 61 | - (nullable Class)objectClass; 62 | 63 | /** 64 | @return a string of the classname or "(null)" 65 | */ 66 | - (nonnull NSString *)classNameOrNull; 67 | 68 | /** 69 | @return return true if it is a swift type class" 70 | */ 71 | - (bool)isSwift; 72 | 73 | @end 74 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Graph/FBObjectiveCGraphElement.mm: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import "FBObjectiveCGraphElement+Internal.h" 10 | 11 | #import 12 | #import 13 | #import 14 | 15 | #import "FBAssociationManager.h" 16 | #import "FBClassStrongLayout.h" 17 | #import "FBObjectGraphConfiguration.h" 18 | #import "FBRetainCycleUtils.h" 19 | #import "FBRetainCycleDetector.h" 20 | #import "FBClassSwiftHelpers.h" 21 | 22 | @protocol FBRetainCycleDetectorCustomClassDescribable 23 | 24 | - (NSString *)customClassDescription; 25 | 26 | @end 27 | 28 | @implementation FBObjectiveCGraphElement 29 | 30 | - (instancetype)initWithObject:(id)object 31 | { 32 | return [self initWithObject:object 33 | configuration:[FBObjectGraphConfiguration new]]; 34 | } 35 | 36 | - (instancetype)initWithObject:(id)object 37 | configuration:(nonnull FBObjectGraphConfiguration *)configuration 38 | { 39 | return [self initWithObject:object 40 | configuration:configuration 41 | namePath:nil]; 42 | } 43 | 44 | - (instancetype)initWithObject:(id)object 45 | configuration:(nonnull FBObjectGraphConfiguration *)configuration 46 | namePath:(NSArray *)namePath 47 | { 48 | if (self = [super init]) { 49 | #if _INTERNAL_RCD_ENABLED 50 | // For an object that is not created using malloc/realloc, running RCD on it is pointless. 51 | // Hence adding a condition to check if object we are considering for RCD is malloced or not. 52 | malloc_zone_t *zone = malloc_zone_from_ptr((__bridge void *)object); 53 | if (zone) { 54 | // We are trying to mimic how ObjectiveC does storeWeak to not fall into 55 | // _objc_fatal path 56 | // https://github.com/bavarious/objc4/blob/3f282b8dbc0d1e501f97e4ed547a4a99cb3ac10b/runtime/objc-weak.mm#L369 57 | 58 | Class aCls = object_getClass(object); 59 | 60 | BOOL (*allowsWeakReference)(id, SEL) = 61 | (__typeof__(allowsWeakReference))class_getMethodImplementation(aCls, @selector(allowsWeakReference)); 62 | 63 | if (allowsWeakReference && (IMP)allowsWeakReference != _objc_msgForward) { 64 | if (allowsWeakReference(object, @selector(allowsWeakReference))) { 65 | // This is still racey since allowsWeakReference could change it value by now. 66 | _object = object; 67 | } 68 | } else { 69 | _object = object; 70 | } 71 | } 72 | #endif 73 | _namePath = namePath; 74 | _configuration = configuration; 75 | } 76 | 77 | return self; 78 | } 79 | 80 | - (NSSet *)allRetainedObjects 81 | { 82 | NSArray *retainedObjectsNotWrapped = [FBAssociationManager associationsForObject:_object]; 83 | NSMutableSet *retainedObjects = [NSMutableSet new]; 84 | 85 | for (id obj in retainedObjectsNotWrapped) { 86 | FBObjectiveCGraphElement *element = FBWrapObjectGraphElementWithContext(self, 87 | obj, 88 | _configuration, 89 | @[@"__associated_object"]); 90 | if (element) { 91 | [retainedObjects addObject:element]; 92 | } 93 | } 94 | 95 | return retainedObjects; 96 | } 97 | 98 | - (BOOL)isEqual:(id)object 99 | { 100 | if ([object isKindOfClass:[FBObjectiveCGraphElement class]]) { 101 | FBObjectiveCGraphElement *objcObject = object; 102 | // Use pointer equality 103 | return objcObject.object == _object; 104 | } 105 | return NO; 106 | } 107 | 108 | - (NSUInteger)hash 109 | { 110 | return (size_t)_object; 111 | } 112 | 113 | - (NSString *)description 114 | { 115 | if (_namePath) { 116 | NSString *namePathStringified = [_namePath componentsJoinedByString:@" -> "]; 117 | return [NSString stringWithFormat:@"-> %@ -> %@ ", namePathStringified, [self classNameOrNull]]; 118 | } 119 | return [NSString stringWithFormat:@"-> %@ ", [self classNameOrNull]]; 120 | } 121 | 122 | - (size_t)objectAddress 123 | { 124 | return (size_t)_object; 125 | } 126 | 127 | - (NSString *)classNameOrNull 128 | { 129 | NSString *className; 130 | 131 | if ([_object respondsToSelector:@selector(customClassDescription)]) { 132 | className = [_object customClassDescription]; 133 | } else { 134 | /* @cwt-override FIXME[T168581563]: -Wnullable-to-nonnull-conversion */ 135 | #pragma clang diagnostic push 136 | #pragma clang diagnostic ignored "-Wnullable-to-nonnull-conversion" 137 | className = NSStringFromClass([self objectClass]); 138 | #pragma clang diagnostic pop 139 | } 140 | 141 | if (!className) { 142 | className = @"(null)"; 143 | } 144 | 145 | return className; 146 | } 147 | 148 | - (Class)objectClass 149 | { 150 | return object_getClass(_object); 151 | } 152 | 153 | - (bool)isSwift 154 | { 155 | Class cls = self.objectClass; 156 | return cls != nil && FBIsSwiftObjectOrClass(cls); 157 | } 158 | 159 | @end 160 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Graph/FBObjectiveCObject.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import 10 | 11 | #import "FBObjectiveCGraphElement.h" 12 | 13 | @class FBGraphEdgeFilterProvider; 14 | 15 | /** 16 | FBObjectiveCGraphElement specialization that can gather all references kept in ivars, as part of collection 17 | etc. 18 | */ 19 | @interface FBObjectiveCObject : FBObjectiveCGraphElement 20 | @end 21 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Graph/FBObjectiveCObject.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import "FBObjectiveCObject.h" 10 | 11 | #import 12 | 13 | #import "FBClassStrongLayout.h" 14 | #import "FBObjectGraphConfiguration.h" 15 | #import "FBObjectReference.h" 16 | #import "FBRetainCycleUtils.h" 17 | 18 | @implementation FBObjectiveCObject 19 | 20 | - (NSSet *)allRetainedObjects 21 | { 22 | Class aCls = object_getClass(self.object); 23 | if (!self.object || !aCls) { 24 | return nil; 25 | } 26 | 27 | NSArray *strongIvars = FBGetObjectStrongReferences(self.object, self.configuration.layoutCache, self.configuration.shouldIncludeSwiftObjects); 28 | 29 | NSMutableArray *retainedObjects = [[[super allRetainedObjects] allObjects] mutableCopy]; 30 | 31 | for (id ref in strongIvars) { 32 | id referencedObject = [ref objectReferenceFromObject:self.object]; 33 | 34 | if (referencedObject) { 35 | NSArray *namePath = [ref namePath]; 36 | FBObjectiveCGraphElement *element = FBWrapObjectGraphElementWithContext(self, 37 | referencedObject, 38 | self.configuration, 39 | namePath); 40 | if (element) { 41 | [retainedObjects addObject:element]; 42 | } 43 | } 44 | } 45 | 46 | if ([NSStringFromClass(aCls) hasPrefix:@"__NSCF"]) { 47 | /** 48 | If we are dealing with toll-free bridged collections, we are not guaranteed that the collection 49 | will hold only Objective-C objects. We are not able to check in runtime what callbacks it uses to 50 | retain/release (if any) and we could easily crash here. 51 | */ 52 | return [NSSet setWithArray:retainedObjects]; 53 | } 54 | 55 | if (class_isMetaClass(aCls)) { 56 | // If it's a meta-class it can conform to following protocols, 57 | // but it would crash when trying enumerating 58 | return nil; 59 | } 60 | 61 | if ([aCls conformsToProtocol:@protocol(NSFastEnumeration)]) { 62 | BOOL retainsKeys = [self _objectRetainsEnumerableKeys]; 63 | BOOL retainsValues = [self _objectRetainsEnumerableValues]; 64 | 65 | BOOL isKeyValued = NO; 66 | if ([aCls instancesRespondToSelector:@selector(objectForKey:)]) { 67 | isKeyValued = YES; 68 | } 69 | 70 | /** 71 | This codepath is prone to errors. When you enumerate a collection that can be mutated while enumeration 72 | we fall into risk of crash. To save ourselves from that we will catch such exception and try again. 73 | We should not try this endlessly, so at some point we will simply give up. 74 | */ 75 | NSInteger tries = 10; 76 | for (NSInteger i = 0; i < tries; ++i) { 77 | // If collection is mutated we want to rollback and try again - let's keep refs in temporary set 78 | NSMutableSet *temporaryRetainedObjects = [NSMutableSet new]; 79 | @try { 80 | for (id subobject in self.object) { 81 | if (retainsKeys) { 82 | FBObjectiveCGraphElement *element = FBWrapObjectGraphElement(self, subobject, self.configuration); 83 | if (element) { 84 | [temporaryRetainedObjects addObject:element]; 85 | } 86 | } 87 | if (isKeyValued && retainsValues) { 88 | FBObjectiveCGraphElement *element = FBWrapObjectGraphElement(self, 89 | [self.object objectForKey:subobject], 90 | self.configuration); 91 | if (element) { 92 | [temporaryRetainedObjects addObject:element]; 93 | } 94 | } 95 | } 96 | } 97 | @catch (NSException *exception) { 98 | // mutation happened, we want to try enumerating again 99 | continue; 100 | } 101 | 102 | // If we are here it means no exception happened and we want to break outer loop 103 | [retainedObjects addObjectsFromArray:[temporaryRetainedObjects allObjects]]; 104 | break; 105 | } 106 | } 107 | 108 | return [NSSet setWithArray:retainedObjects]; 109 | } 110 | 111 | - (BOOL)_objectRetainsEnumerableValues 112 | { 113 | if ([self.object respondsToSelector:@selector(valuePointerFunctions)]) { 114 | NSPointerFunctions *pointerFunctions = [self.object valuePointerFunctions]; 115 | if (pointerFunctions.acquireFunction == NULL) { 116 | return NO; 117 | } 118 | } 119 | 120 | return YES; 121 | } 122 | 123 | - (BOOL)_objectRetainsEnumerableKeys 124 | { 125 | if ([self.object respondsToSelector:@selector(pointerFunctions)]) { 126 | // NSHashTable and similar 127 | // If object shows what pointer functions are used, lets try to determine 128 | // if it's not retaining objects 129 | NSPointerFunctions *pointerFunctions = [self.object pointerFunctions]; 130 | if (pointerFunctions.acquireFunction == NULL) { 131 | return NO; 132 | } 133 | } 134 | 135 | if ([self.object respondsToSelector:@selector(keyPointerFunctions)]) { 136 | NSPointerFunctions *pointerFunctions = [self.object keyPointerFunctions]; 137 | if (pointerFunctions.acquireFunction == NULL) { 138 | return NO; 139 | } 140 | } 141 | 142 | return YES; 143 | } 144 | 145 | @end 146 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Graph/Internal/FBObjectiveCGraphElement+Internal.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import 10 | 11 | #import "FBObjectiveCGraphElement.h" 12 | 13 | @interface FBObjectiveCGraphElement () 14 | 15 | - (instancetype)initWithObject:(id)object; 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Graph/Specialization/FBObjectiveCNSCFTimer.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import 10 | 11 | #import 12 | 13 | /** 14 | Specialization of FBObjectiveCObject for NSTimer. 15 | Standard methods that FBObjectiveCObject uses will not fetch us all objects retained by NSTimer. 16 | One good example is target of NSTimer. 17 | */ 18 | @interface FBObjectiveCNSCFTimer : FBObjectiveCObject 19 | @end 20 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Graph/Specialization/FBObjectiveCNSCFTimer.mm: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import "FBObjectiveCNSCFTimer.h" 10 | 11 | #import 12 | 13 | #import "FBRetainCycleDetector.h" 14 | #import "FBRetainCycleUtils.h" 15 | 16 | @implementation FBObjectiveCNSCFTimer 17 | 18 | #if _INTERNAL_RCD_ENABLED 19 | 20 | typedef struct { 21 | long _unknown; // This is always 1 22 | id target; 23 | SEL selector; 24 | NSDictionary *userInfo; 25 | } _FBNSCFTimerInfoStruct; 26 | 27 | - (NSSet *)allRetainedObjects 28 | { 29 | // Let's retain our timer 30 | __attribute__((objc_precise_lifetime)) NSTimer *timer = self.object; 31 | 32 | if (!timer) { 33 | return nil; 34 | } 35 | 36 | NSMutableSet *retained = [[super allRetainedObjects] mutableCopy]; 37 | 38 | CFRunLoopTimerContext context; 39 | CFRunLoopTimerGetContext((CFRunLoopTimerRef)timer, &context); 40 | 41 | // If it has a retain function, let's assume it retains strongly 42 | if (context.info && context.retain) { 43 | _FBNSCFTimerInfoStruct infoStruct = *(_FBNSCFTimerInfoStruct *)(context.info); 44 | if (infoStruct.target) { 45 | FBObjectiveCGraphElement *element = FBWrapObjectGraphElementWithContext(self, infoStruct.target, self.configuration, @[@"target"]); 46 | if (element) { 47 | [retained addObject:element]; 48 | } 49 | } 50 | if (infoStruct.userInfo) { 51 | FBObjectiveCGraphElement *element = FBWrapObjectGraphElementWithContext(self, infoStruct.userInfo, self.configuration, @[@"userInfo"]); 52 | if (element) { 53 | [retained addObject:element]; 54 | } 55 | } 56 | } 57 | 58 | return retained; 59 | } 60 | 61 | #endif // _INTERNAL_RCD_ENABLED 62 | 63 | @end 64 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Layout/Blocks/Circle-LICENSE: -------------------------------------------------------------------------------- 1 | Circle is licensed under a BSD license, as provided below. Facebook provides this code under the BSD-style license found in the LICENSE file in the root directory of this source tree. 2 | 3 | 4 | Copyright (c) 2012, Michael Ash 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 8 | 9 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 10 | 11 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 12 | 13 | Neither the name of Michael Ash nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 16 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Layout/Blocks/FBBlockInterface.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import 10 | 11 | /** 12 | We are mimicing Block structure based on Clang documentation: 13 | http://clang.llvm.org/docs/Block-ABI-Apple.html 14 | */ 15 | 16 | enum { // Flags from BlockLiteral 17 | BLOCK_HAS_COPY_DISPOSE = (1 << 25), 18 | BLOCK_HAS_CTOR = (1 << 26), // helpers have C++ code 19 | BLOCK_IS_GLOBAL = (1 << 28), 20 | BLOCK_HAS_STRET = (1 << 29), // IFF BLOCK_HAS_SIGNATURE 21 | BLOCK_HAS_SIGNATURE = (1 << 30), 22 | }; 23 | 24 | struct BlockDescriptor { 25 | unsigned long int reserved; // NULL 26 | unsigned long int size; 27 | // optional helper functions 28 | void (*copy_helper)(void *dst, void *src); // IFF (1<<25) 29 | void (*dispose_helper)(void *src); // IFF (1<<25) 30 | const char *signature; // IFF (1<<30) 31 | }; 32 | 33 | struct BlockLiteral { 34 | void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock 35 | int flags; 36 | int reserved; 37 | void (*invoke)(void *, ...); 38 | struct BlockDescriptor *descriptor; 39 | // imported variables 40 | }; 41 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Layout/Blocks/FBBlockStrongLayout.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import 10 | 11 | #ifdef __cplusplus 12 | extern "C" { 13 | #endif 14 | 15 | /** 16 | Returns an array of id objects that will have only those references 17 | that are retained by block. 18 | */ 19 | NSArray *_Nullable FBGetBlockStrongReferences(void *_Nonnull block); 20 | 21 | BOOL FBObjectIsBlock(void *_Nullable object); 22 | 23 | #ifdef __cplusplus 24 | } 25 | #endif 26 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Layout/Blocks/FBBlockStrongLayout.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #if __has_feature(objc_arc) 10 | #error This file must be compiled with MRR. Use -fno-objc-arc flag. 11 | #endif 12 | 13 | #import "FBBlockStrongLayout.h" 14 | 15 | #import 16 | 17 | #import "FBBlockInterface.h" 18 | #import "FBBlockStrongRelationDetector.h" 19 | 20 | /** 21 | We will be blackboxing variables that the block holds with our own custom class, 22 | and we will check which of them were retained. 23 | 24 | The idea is based on the approach Circle uses: 25 | https://github.com/mikeash/Circle 26 | https://github.com/mikeash/Circle/blob/master/Circle/CircleIVarLayout.m 27 | */ 28 | static NSIndexSet *_GetBlockStrongLayout(void *block) { 29 | struct BlockLiteral *blockLiteral = block; 30 | 31 | /** 32 | BLOCK_HAS_CTOR - Block has a C++ constructor/destructor, which gives us a good chance it retains 33 | objects that are not pointer aligned, so omit them. 34 | 35 | !BLOCK_HAS_COPY_DISPOSE - Block doesn't have a dispose function, so it does not retain objects and 36 | we are not able to blackbox it. 37 | */ 38 | if ((blockLiteral->flags & BLOCK_HAS_CTOR) 39 | || !(blockLiteral->flags & BLOCK_HAS_COPY_DISPOSE)) { 40 | return nil; 41 | } 42 | 43 | void (*dispose_helper)(void *src) = blockLiteral->descriptor->dispose_helper; 44 | const size_t ptrSize = sizeof(void *); 45 | 46 | // Figure out the number of pointers it takes to fill out the object, rounding up. 47 | const size_t elements = (blockLiteral->descriptor->size + ptrSize - 1) / ptrSize; 48 | 49 | // Create a fake object of the appropriate length. 50 | void *obj[elements]; 51 | void *detectors[elements]; 52 | 53 | for (size_t i = 0; i < elements; ++i) { 54 | FBBlockStrongRelationDetector *detector = [FBBlockStrongRelationDetector new]; 55 | obj[i] = detectors[i] = detector; 56 | } 57 | 58 | @autoreleasepool { 59 | dispose_helper(obj); 60 | } 61 | 62 | // Run through the release detectors and add each one that got released to the object's 63 | // strong ivar layout. 64 | NSMutableIndexSet *layout = [NSMutableIndexSet indexSet]; 65 | 66 | for (size_t i = 0; i < elements; ++i) { 67 | FBBlockStrongRelationDetector *detector = (FBBlockStrongRelationDetector *)(detectors[i]); 68 | if (detector.isStrong) { 69 | [layout addIndex:i]; 70 | } 71 | 72 | // Destroy detectors 73 | [detector trueRelease]; 74 | } 75 | 76 | return layout; 77 | } 78 | 79 | NSArray *FBGetBlockStrongReferences(void *block) { 80 | if (!FBObjectIsBlock(block)) { 81 | return nil; 82 | } 83 | 84 | NSMutableArray *results = [NSMutableArray new]; 85 | 86 | void **blockReference = block; 87 | NSIndexSet *strongLayout = _GetBlockStrongLayout(block); 88 | [strongLayout enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { 89 | void **reference = &blockReference[idx]; 90 | 91 | if (reference && (*reference)) { 92 | id object = (id)(*reference); 93 | 94 | if (object) { 95 | [results addObject:object]; 96 | } 97 | } 98 | }]; 99 | 100 | return [results autorelease]; 101 | } 102 | 103 | static Class _BlockClass(void) { 104 | static dispatch_once_t onceToken; 105 | static Class blockClass; 106 | dispatch_once(&onceToken, ^{ 107 | void (^testBlock)(void) = [^{} copy]; 108 | blockClass = [testBlock class]; 109 | while(class_getSuperclass(blockClass) && class_getSuperclass(blockClass) != [NSObject class]) { 110 | blockClass = class_getSuperclass(blockClass); 111 | } 112 | [testBlock release]; 113 | }); 114 | return blockClass; 115 | } 116 | 117 | BOOL FBObjectIsBlock(void *object) { 118 | Class blockClass = _BlockClass(); 119 | 120 | Class candidate = object_getClass((__bridge id)object); 121 | return [candidate isSubclassOfClass:blockClass]; 122 | } 123 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Layout/Blocks/FBBlockStrongRelationDetector.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import 10 | 11 | /** 12 | We created a detector object that will fake *any* object that block can retain. 13 | 14 | Using clever trickery from Circle: 15 | https://github.com/mikeash/Circle/blob/master/Circle/CircleIVarLayout.m 16 | 17 | We are also faking that this object can be treated as a block. 18 | Otherwise if the block is retained by block, it will try to call byref_dispose and 19 | our object won't be able to respond. 20 | */ 21 | 22 | struct _block_byref_block; 23 | @interface FBBlockStrongRelationDetector : NSObject 24 | { 25 | // __block fakery 26 | void *forwarding; 27 | int flags; //refcount; 28 | int size; 29 | void (*byref_keep)(struct _block_byref_block *dst, struct _block_byref_block *src); 30 | void (*byref_dispose)(struct _block_byref_block *); 31 | void *captured[16]; 32 | } 33 | 34 | @property (nonatomic, assign, getter=isStrong) BOOL strong; 35 | 36 | - (oneway void)trueRelease; 37 | 38 | - (void *)forwarding; 39 | 40 | @end 41 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Layout/Blocks/FBBlockStrongRelationDetector.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #if __has_feature(objc_arc) 10 | #error This file must be compiled with MRR. Use -fno-objc-arc flag. 11 | #endif 12 | 13 | #import "FBBlockStrongRelationDetector.h" 14 | 15 | #import 16 | 17 | static void byref_keep_nop(struct _block_byref_block *dst, struct _block_byref_block *src) {} 18 | static void byref_dispose_nop(struct _block_byref_block *param) {} 19 | 20 | @implementation FBBlockStrongRelationDetector 21 | 22 | - (oneway void)release 23 | { 24 | _strong = YES; 25 | } 26 | 27 | - (id)retain 28 | { 29 | return self; 30 | } 31 | 32 | + (id)alloc 33 | { 34 | FBBlockStrongRelationDetector *obj = [super alloc]; 35 | 36 | // Setting up block fakery 37 | obj->forwarding = obj; 38 | obj->byref_keep = byref_keep_nop; 39 | obj->byref_dispose = byref_dispose_nop; 40 | 41 | return obj; 42 | } 43 | 44 | - (oneway void)trueRelease 45 | { 46 | [super release]; 47 | } 48 | 49 | - (void *)forwarding 50 | { 51 | return self->forwarding; 52 | } 53 | 54 | @end 55 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Layout/Classes/FBClassStrongLayout.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import 10 | 11 | #ifdef __cplusplus 12 | extern "C" { 13 | #endif 14 | 15 | @protocol FBObjectReferenceWithLayout; 16 | @protocol FBObjectReference; 17 | 18 | /** 19 | @return An array of id objects that will have only those references 20 | that are retained by the object. It also goes through parent classes. 21 | */ 22 | NSArray> *_Nonnull FBGetObjectStrongReferences(id _Nullable obj, 23 | NSMutableDictionary> *> *_Nullable layoutCache, 24 | BOOL shouldIncludeSwiftObjects); 25 | 26 | #ifdef __cplusplus 27 | } 28 | #endif 29 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Layout/Classes/FBClassStrongLayout.mm: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import "FBClassStrongLayout.h" 10 | 11 | #import 12 | #import 13 | #import 14 | #import 15 | 16 | #import 17 | 18 | #import 19 | 20 | #import "FBIvarReference.h" 21 | #import "FBObjectInStructReference.h" 22 | #import "FBStructEncodingParser.h" 23 | #import "Struct.h" 24 | #import "Type.h" 25 | #import "FBClassSwiftHelpers.h" 26 | #import "FBObjectReferenceWithLayout.h" 27 | #import "FBSwiftReference.h" 28 | 29 | /** 30 | If we stumble upon a struct, we need to go through it and check if it doesn't retain some objects. 31 | */ 32 | static NSArray *FBGetReferencesForObjectsInStructEncoding(FBIvarReference *ivar, std::string encoding) { 33 | NSMutableArray *references = [NSMutableArray new]; 34 | 35 | std::string ivarName = std::string([ivar.name cStringUsingEncoding:NSUTF8StringEncoding]); 36 | FB::RetainCycleDetector::Parser::Struct parsedStruct = 37 | FB::RetainCycleDetector::Parser::parseStructEncodingWithName(encoding, ivarName); 38 | 39 | std::vector> types = parsedStruct.flattenTypes(); 40 | 41 | ptrdiff_t offset = ivar.offset; 42 | 43 | for (auto &type: types) { 44 | NSUInteger size, align; 45 | 46 | std::string typeEncoding = type->typeEncoding; 47 | if (typeEncoding[0] == '^') { 48 | // It's a pointer, let's skip 49 | size = sizeof(void *); 50 | align = _Alignof(void *); 51 | } else { 52 | @try { 53 | NSGetSizeAndAlignment(typeEncoding.c_str(), 54 | &size, 55 | &align); 56 | } @catch (NSException *e) { 57 | /** 58 | If we failed, we probably have C++ and ObjC cannot get it's size and alignment. We are skipping. 59 | If we would like to support it, we would need to derive size and alignment of type from the string. 60 | C++ does not have reflection so we can't really do that unless we create the mapping ourselves. 61 | */ 62 | break; 63 | } 64 | } 65 | 66 | 67 | // The object must be aligned 68 | NSUInteger overAlignment = offset % align; 69 | NSUInteger whatsMissing = (overAlignment == 0) ? 0 : align - overAlignment; 70 | offset += whatsMissing; 71 | 72 | if (typeEncoding[0] == '@') { 73 | 74 | // The index that ivar layout will ask for is going to be aligned with pointer size 75 | 76 | // Prepare additional context 77 | NSString *typeEncodingName = [NSString stringWithCString:type->name.c_str() encoding:NSUTF8StringEncoding]; 78 | 79 | NSMutableArray *namePath = [NSMutableArray new]; 80 | 81 | for (auto &name: type->typePath) { 82 | NSString *nameString = [NSString stringWithCString:name.c_str() encoding:NSUTF8StringEncoding]; 83 | if (nameString) { 84 | [namePath addObject:nameString]; 85 | } 86 | } 87 | 88 | if (typeEncodingName) { 89 | [namePath addObject:typeEncodingName]; 90 | } 91 | [references addObject:[[FBObjectInStructReference alloc] initWithIndex:(offset / sizeof(void *)) 92 | namePath:namePath]]; 93 | } 94 | 95 | offset += size; 96 | } 97 | 98 | return references; 99 | } 100 | 101 | static NSArray> *FBGetClassReferences(Class aCls) { 102 | NSMutableArray> *result = [NSMutableArray new]; 103 | 104 | unsigned int count; 105 | Ivar *ivars = class_copyIvarList(aCls, &count); 106 | 107 | for (unsigned int i = 0; i < count; ++i) { 108 | Ivar ivar = ivars[i]; 109 | FBIvarReference *wrapper = [[FBIvarReference alloc] initWithIvar:ivar]; 110 | 111 | if (wrapper.type == FBStructType) { 112 | std::string encoding = std::string(ivar_getTypeEncoding(wrapper.ivar)); 113 | NSArray *references = FBGetReferencesForObjectsInStructEncoding(wrapper, encoding); 114 | 115 | [result addObjectsFromArray:references]; 116 | } else { 117 | [result addObject:wrapper]; 118 | } 119 | } 120 | free(ivars); 121 | 122 | return [result copy]; 123 | } 124 | 125 | static NSIndexSet *FBGetLayoutAsIndexesForDescription(NSUInteger minimumIndex, const uint8_t *layoutDescription) { 126 | NSMutableIndexSet *interestingIndexes = [NSMutableIndexSet new]; 127 | NSUInteger currentIndex = minimumIndex; 128 | 129 | while (*layoutDescription != '\x00') { 130 | int upperNibble = (*layoutDescription & 0xf0) >> 4; 131 | int lowerNibble = *layoutDescription & 0xf; 132 | 133 | // Upper nimble is for skipping 134 | currentIndex += upperNibble; 135 | 136 | // Lower nimble describes count 137 | [interestingIndexes addIndexesInRange:NSMakeRange(currentIndex, lowerNibble)]; 138 | currentIndex += lowerNibble; 139 | 140 | ++layoutDescription; 141 | } 142 | 143 | return interestingIndexes; 144 | } 145 | 146 | static NSUInteger FBGetMinimumIvarIndex(__unsafe_unretained Class aCls) { 147 | NSUInteger minimumIndex = 1; 148 | unsigned int count; 149 | Ivar *ivars = class_copyIvarList(aCls, &count); 150 | 151 | if (count > 0) { 152 | Ivar ivar = ivars[0]; 153 | ptrdiff_t offset = ivar_getOffset(ivar); 154 | minimumIndex = offset / (sizeof(void *)); 155 | } 156 | 157 | free(ivars); 158 | 159 | return minimumIndex; 160 | } 161 | 162 | static NSArray> *FBGetStrongReferencesForObjectiveCClass(Class aCls) { 163 | NSArray> *ivars = [FBGetClassReferences(aCls) filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) { 164 | if ([evaluatedObject isKindOfClass:[FBIvarReference class]]) { 165 | FBIvarReference *wrapper = evaluatedObject; 166 | return wrapper.type != FBUnknownType; 167 | } 168 | return YES; 169 | }]]; 170 | 171 | // This only works for objective-c objects 172 | const uint8_t *fullLayout = class_getIvarLayout(aCls); 173 | 174 | if (!fullLayout) { 175 | return @[]; 176 | } 177 | 178 | NSUInteger minimumIndex = FBGetMinimumIvarIndex(aCls); 179 | NSIndexSet *parsedLayout = FBGetLayoutAsIndexesForDescription(minimumIndex, fullLayout); 180 | 181 | NSArray> *filteredIvars = 182 | [ivars filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, 183 | NSDictionary *bindings) { 184 | return [parsedLayout containsIndex:[evaluatedObject indexInIvarLayout]]; 185 | }]]; 186 | 187 | return filteredIvars; 188 | } 189 | 190 | static NSArray> *FBGetStrongReferencesForSwiftClass(id obj, Class aCls) { 191 | // This contains all the Swift properties, including of it superclasses (recursive until any Objective-c class) 192 | NSArray *const properties = [SwiftIntrospector getPropertiesRecursiveWithObject:obj]; 193 | 194 | // Since we only want properties for this class, lets create a map and check against `class_copyIvarList`, which isn't recursive 195 | NSMutableDictionary *const propertyMap = [NSMutableDictionary new]; 196 | for (PropertyIntrospection *property in properties) { 197 | [propertyMap setObject:property forKey:[NSString stringWithFormat:@"%@%i", property.name, (int)property.offset]]; 198 | } 199 | 200 | NSMutableArray> *const result = [NSMutableArray new]; 201 | 202 | // class_copyIvarList still works for Swift objects, including pure Swift objects 203 | // Maybe a safer way would be to use the Mirror API 204 | unsigned int count; 205 | Ivar *ivars = class_copyIvarList(aCls, &count); 206 | 207 | for (unsigned int i = 0; i < count; ++i) { 208 | Ivar ivar = ivars[i]; 209 | const char *utf8Name = ivar_getName(ivar); 210 | NSString *const ivarName = (utf8Name == NULL) ? nil : [NSString stringWithUTF8String:utf8Name]; 211 | const ptrdiff_t offset = ivar_getOffset(ivar); 212 | NSString *const propertyKey = [NSString stringWithFormat:@"%@%i", ivarName, (int)offset]; 213 | 214 | PropertyIntrospection *property = [propertyMap objectForKey:propertyKey]; 215 | if (property && property.isStrong) { 216 | FBSwiftReference *wrapper = [[FBSwiftReference alloc] initWithName:ivarName]; 217 | [result addObject:wrapper]; 218 | } 219 | } 220 | free(ivars); 221 | 222 | return [result copy]; 223 | } 224 | 225 | static NSArray> *FBGetStrongReferencesForClass(id obj, Class aCls, BOOL shouldIncludeSwiftObjects) { 226 | if (aCls == nil) { 227 | return @[]; 228 | } 229 | if(shouldIncludeSwiftObjects){ 230 | if (FBIsSwiftObjectOrClass(aCls)) { 231 | return FBGetStrongReferencesForSwiftClass(obj, aCls); 232 | } else { 233 | return FBGetStrongReferencesForObjectiveCClass(aCls); 234 | } 235 | } else { 236 | return FBGetStrongReferencesForObjectiveCClass(aCls); 237 | } 238 | 239 | } 240 | 241 | NSArray> *FBGetObjectStrongReferences(id obj, 242 | NSMutableDictionary> *> *layoutCache, 243 | BOOL shouldIncludeSwiftObjects) { 244 | NSMutableArray> *array = [NSMutableArray new]; 245 | 246 | __unsafe_unretained Class previousClass = nil; 247 | __unsafe_unretained Class currentClass = object_getClass(obj); 248 | 249 | while (previousClass != currentClass && currentClass) { 250 | NSArray> *ivars; 251 | 252 | const char * className = class_getName(currentClass); 253 | NSString *claseName = [[NSString alloc] initWithCString:className encoding:NSUTF8StringEncoding]; 254 | 255 | ivars = layoutCache[claseName]; 256 | 257 | if (!ivars) { 258 | ivars = FBGetStrongReferencesForClass(obj, currentClass, shouldIncludeSwiftObjects); 259 | layoutCache[claseName] = ivars; 260 | } 261 | [array addObjectsFromArray:ivars]; 262 | 263 | previousClass = currentClass; 264 | currentClass = class_getSuperclass(currentClass); 265 | } 266 | 267 | return [array copy]; 268 | } 269 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Layout/Classes/FBClassStrongLayoutHelpers.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import 10 | 11 | #import 12 | 13 | #ifdef __cplusplus 14 | extern "C" { 15 | #endif 16 | 17 | /** 18 | Returns object on given index for obj in its ivar layout. 19 | It will try to map the object to an Objective-C object, so if the index 20 | is invalid it will crash with BAD_ACCESS. 21 | 22 | It cannot be called under ARC. 23 | */ 24 | id FBExtractObjectByOffset(id obj, NSUInteger index); 25 | 26 | #ifdef __cplusplus 27 | } 28 | #endif 29 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Layout/Classes/FBClassStrongLayoutHelpers.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #if __has_feature(objc_arc) 10 | #error This file must be compiled with MRR. Use -fno-objc-arc flag. 11 | #endif 12 | 13 | #import "FBClassStrongLayoutHelpers.h" 14 | 15 | id FBExtractObjectByOffset(id obj, NSUInteger index) { 16 | id *idx = (id *)((uintptr_t)obj + (index * sizeof(void *))); 17 | 18 | return *idx; 19 | } 20 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Layout/Classes/FBClassSwiftHelpers.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | #ifdef __cplusplus 14 | extern "C" { 15 | #endif 16 | 17 | BOOL FBIsSwiftObjectOrClass(id anything); 18 | 19 | #ifdef __cplusplus 20 | } 21 | #endif 22 | 23 | NS_ASSUME_NONNULL_END 24 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Layout/Classes/FBClassSwiftHelpers.mm: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import "FBClassSwiftHelpers.h" 10 | #import 11 | #include 12 | #include 13 | 14 | #if __LP64__ 15 | typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits 16 | #else 17 | typedef uint16_t mask_t; 18 | #endif 19 | 20 | /* dyld_shared_cache_builder and obj-C agree on these definitions */ 21 | struct preopt_cache_entry_t { 22 | uint32_t sel_offs; 23 | uint32_t imp_offs; 24 | }; 25 | 26 | /* dyld_shared_cache_builder and obj-C agree on these definitions */ 27 | struct preopt_cache_t { 28 | int32_t fallback_class_offset; 29 | union { 30 | struct { 31 | uint16_t shift : 5; 32 | uint16_t mask : 11; 33 | }; 34 | uint16_t hash_params; 35 | }; 36 | uint16_t occupied : 14; 37 | uint16_t has_inlines : 1; 38 | uint16_t bit_one : 1; 39 | preopt_cache_entry_t entries[]; 40 | 41 | inline int capacity() const { 42 | return mask + 1; 43 | } 44 | }; 45 | 46 | // Version of std::atomic that does not allow implicit conversions 47 | // to/from the wrapped type, and requires an explicit memory order 48 | // be passed to load() and store(). 49 | template 50 | struct explicit_atomic : public std::atomic { 51 | explicit explicit_atomic(T initial) noexcept : std::atomic(std::move(initial)) {} 52 | operator T() const = delete; 53 | 54 | T load(std::memory_order order) const noexcept { 55 | return std::atomic::load(order); 56 | } 57 | void store(T desired, std::memory_order order) noexcept { 58 | std::atomic::store(desired, order); 59 | } 60 | 61 | // Convert a normal pointer to an atomic pointer. This is a 62 | // somewhat dodgy thing to do, but if the atomic type is lock 63 | // free and the same size as the non-atomic type, we know the 64 | // representations are the same, and the compiler generates good 65 | // code. 66 | static explicit_atomic *from_pointer(T *ptr) { 67 | static_assert(sizeof(explicit_atomic *) == sizeof(T *), 68 | "Size of atomic must match size of original"); 69 | explicit_atomic *atomic = (explicit_atomic *)ptr; 70 | ASSERT(atomic->is_lock_free()); 71 | return atomic; 72 | } 73 | }; 74 | 75 | struct cache_t { 76 | private: 77 | explicit_atomic _bucketsAndMaybeMask; 78 | union { 79 | struct { 80 | explicit_atomic _maybeMask; 81 | #if __LP64__ 82 | uint16_t _flags; 83 | #endif 84 | uint16_t _occupied; 85 | }; 86 | explicit_atomic _originalPreoptCache; 87 | }; 88 | 89 | #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED 90 | // _bucketsAndMaybeMask is a buckets_t pointer 91 | // _maybeMask is the buckets mask 92 | 93 | static constexpr uintptr_t bucketsMask = ~0ul; 94 | #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS 95 | static constexpr uintptr_t maskShift = 48; 96 | static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1; 97 | static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << maskShift) - 1; 98 | 99 | static_assert(bucketsMask >= MACH_VM_MAX_ADDRESS, "Bucket field doesn't have enough bits for arbitrary pointers."); 100 | #if CONFIG_USE_PREOPT_CACHES 101 | static constexpr uintptr_t preoptBucketsMarker = 1ul; 102 | static constexpr uintptr_t preoptBucketsMask = bucketsMask & ~preoptBucketsMarker; 103 | #endif 104 | #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 105 | // _bucketsAndMaybeMask is a buckets_t pointer in the low 48 bits 106 | // _maybeMask is unused, the mask is stored in the top 16 bits. 107 | 108 | // How much the mask is shifted by. 109 | static constexpr uintptr_t maskShift = 48; 110 | 111 | // Additional bits after the mask which must be zero. msgSend 112 | // takes advantage of these additional bits to construct the value 113 | // `mask << 4` from `_maskAndBuckets` in a single instruction. 114 | static constexpr uintptr_t maskZeroBits = 4; 115 | 116 | // The largest mask value we can store. 117 | static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1; 118 | 119 | // The mask applied to `_maskAndBuckets` to retrieve the buckets pointer. 120 | static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) - 1; 121 | 122 | // Ensure we have enough bits for the buckets pointer. 123 | static_assert(bucketsMask >= MACH_VM_MAX_ADDRESS, 124 | "Bucket field doesn't have enough bits for arbitrary pointers."); 125 | 126 | #if CONFIG_USE_PREOPT_CACHES 127 | static constexpr uintptr_t preoptBucketsMarker = 1ul; 128 | #if __has_feature(ptrauth_calls) 129 | // 63..60: hash_mask_shift 130 | // 59..55: hash_shift 131 | // 54.. 1: buckets ptr + auth 132 | // 0: always 1 133 | static constexpr uintptr_t preoptBucketsMask = 0x007ffffffffffffe; 134 | static inline uintptr_t preoptBucketsHashParams(const preopt_cache_t *cache) { 135 | uintptr_t value = (uintptr_t)cache->shift << 55; 136 | // masks have 11 bits but can be 0, so we compute 137 | // the right shift for 0x7fff rather than 0xffff 138 | return value | ((objc::mask16ShiftBits(cache->mask) - 1) << 60); 139 | } 140 | #else 141 | // 63..53: hash_mask 142 | // 52..48: hash_shift 143 | // 47.. 1: buckets ptr 144 | // 0: always 1 145 | static constexpr uintptr_t preoptBucketsMask = 0x0000fffffffffffe; 146 | static inline uintptr_t preoptBucketsHashParams(const preopt_cache_t *cache) { 147 | return (uintptr_t)cache->hash_params << 48; 148 | } 149 | #endif 150 | #endif // CONFIG_USE_PREOPT_CACHES 151 | #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4 152 | // _bucketsAndMaybeMask is a buckets_t pointer in the top 28 bits 153 | // _maybeMask is unused, the mask length is stored in the low 4 bits 154 | 155 | static constexpr uintptr_t maskBits = 4; 156 | static constexpr uintptr_t maskMask = (1 << maskBits) - 1; 157 | static constexpr uintptr_t bucketsMask = ~maskMask; 158 | static_assert(!CONFIG_USE_PREOPT_CACHES, "preoptimized caches not supported"); 159 | #else 160 | #error Unknown cache mask storage type. 161 | #endif 162 | }; 163 | 164 | // class is a Swift class from the pre-stable Swift ABI 165 | #define FAST_IS_SWIFT_LEGACY (1UL<<0) 166 | // class is a Swift class from the stable Swift ABI 167 | #define FAST_IS_SWIFT_STABLE (1UL<<1) 168 | 169 | struct class_data_bits_t { 170 | // Values are the FAST_ flags above. 171 | uintptr_t bits; 172 | bool getBit(uintptr_t bit) { 173 | return bits & bit; 174 | } 175 | 176 | bool isAnySwift() { 177 | return isSwiftStable() || isSwiftLegacy(); 178 | } 179 | 180 | bool isSwiftStable() { 181 | return getBit(FAST_IS_SWIFT_STABLE); 182 | } 183 | 184 | bool isSwiftLegacy() { 185 | return getBit(FAST_IS_SWIFT_LEGACY); 186 | } 187 | }; 188 | 189 | struct yxy_objc_class : objc_object { 190 | // Class ISA; 191 | Class superclass; 192 | cache_t cache; // formerly cache pointer and vtable 193 | class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags 194 | }; 195 | 196 | extern "C" BOOL FBIsSwiftObjectOrClass(id anything) { 197 | if (anything == nil) { 198 | return NO; 199 | } 200 | Class cls = anything; 201 | if (!object_isClass(cls)) { 202 | cls = object_getClass(cls); 203 | } 204 | 205 | struct yxy_objc_class *objc_cls = (__bridge yxy_objc_class *)cls; 206 | return objc_cls->bits.isAnySwift(); 207 | } 208 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Layout/Classes/Parser/BaseType.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import 10 | 11 | #import 12 | 13 | namespace FB { namespace RetainCycleDetector { namespace Parser { 14 | class BaseType { 15 | public: 16 | virtual ~BaseType() {} 17 | }; 18 | 19 | class Unresolved: public BaseType { 20 | public: 21 | std::string value; 22 | Unresolved(std::string value): value(value) {} 23 | Unresolved(Unresolved&&) = default; 24 | Unresolved &operator=(Unresolved&&) = default; 25 | 26 | Unresolved(const Unresolved&) = delete; 27 | Unresolved &operator=(const Unresolved&) = delete; 28 | }; 29 | } } } 30 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Layout/Classes/Parser/FBStructEncodingParser.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import 10 | 11 | #import "Struct.h" 12 | #import "Type.h" 13 | 14 | namespace FB { namespace RetainCycleDetector { namespace Parser { 15 | 16 | /** 17 | This function will parse a struct encoding from an ivar, and return an FBParsedStruct instance. 18 | Check FBParsedStruct to learn more on how to interact with it. 19 | 20 | FBParseStructEncoding assumes the string passed to it will be a proper struct encoding. 21 | It will not work with encodings provided by @encode() because they do not add names. 22 | It will work with encodings provided by ivars (ivar_getTypeEncoding) 23 | */ 24 | Struct parseStructEncoding(const std::string &structEncodingString); 25 | 26 | 27 | /** 28 | You can provide name for root struct you are passing. The name will be then used 29 | in typePath (check out FBParsedType for details). 30 | The name here can be for example a name of an ivar with this struct. 31 | */ 32 | Struct parseStructEncodingWithName(const std::string &structEncodingString, 33 | const std::string &structName); 34 | 35 | 36 | } } } 37 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Layout/Classes/Parser/FBStructEncodingParser.mm: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import "FBStructEncodingParser.h" 10 | 11 | #import 12 | #import 13 | #import 14 | #import 15 | #import 16 | 17 | #import "BaseType.h" 18 | 19 | namespace { 20 | class _StringScanner { 21 | public: 22 | const std::string string; 23 | size_t index; 24 | 25 | _StringScanner(const std::string &string): string(string), index(0) {} 26 | 27 | bool scanString(const std::string &stringToScan) { 28 | if (!(string.compare(index, stringToScan.length(), stringToScan) == 0)) { 29 | return false; 30 | } 31 | index += stringToScan.length(); 32 | return true; 33 | } 34 | 35 | const std::string scanUpToString(const std::string &upToString) { 36 | size_t pos = string.find(upToString, index); 37 | if (pos == std::string::npos) { 38 | // Mark as whole string scanned 39 | index = string.length(); 40 | return ""; 41 | } 42 | 43 | std::string inBetweenString = string.substr(index, pos - index); 44 | index = pos; 45 | return inBetweenString; 46 | } 47 | 48 | const char currentCharacter() { 49 | return string[index]; 50 | } 51 | 52 | const std::string scanUpToCharacterFromSet(const std::string &characterSet) { 53 | size_t pos = string.find_first_of(characterSet, index); 54 | if (pos == std::string::npos) { 55 | index = string.length(); 56 | return ""; 57 | } 58 | 59 | std::string inBetweenString = string.substr(index, pos-index); 60 | index = pos; 61 | return inBetweenString; 62 | } 63 | }; 64 | 65 | }; 66 | 67 | namespace FB { namespace RetainCycleDetector { namespace Parser { 68 | 69 | /** 70 | Intermediate struct object used inside the algorithm to pass some 71 | information when parsing nested structures. 72 | */ 73 | struct _StructParseResult { 74 | std::vector> containedTypes; 75 | const std::string typeName; 76 | }; 77 | 78 | static const auto kOpenStruct = "{"; 79 | static const auto kCloseStruct = "}"; 80 | static const auto kLiteralEndingCharacters = "\"}"; 81 | static const auto kQuote = "\""; 82 | 83 | static struct _StructParseResult _ParseStructEncodingWithScanner(_StringScanner &scanner, 84 | NSString *debugStruct) { 85 | std::vector> types; 86 | 87 | // Every struct starts with '{' 88 | __unused const auto scannedCorrectly = scanner.scanString(kOpenStruct); 89 | NSCAssert(scannedCorrectly, @"The first character of struct encoding should be {; debug_struct: %@", debugStruct); 90 | 91 | // Parse name 92 | const auto structTypeName = scanner.scanUpToString("="); 93 | scanner.scanString("="); 94 | 95 | while (!(scanner.scanString(kCloseStruct))) { 96 | if (scanner.scanString(kQuote)) { 97 | const auto parseResult = scanner.scanUpToString(kQuote); 98 | scanner.scanString(kQuote); 99 | if (parseResult.length() > 0) { 100 | types.push_back(std::make_shared(parseResult)); 101 | } 102 | } else if (scanner.currentCharacter() == '{') { 103 | // We do not want to consume '{' because we will call parser recursively 104 | const auto locBefore = scanner.index; 105 | auto parseResult = _ParseStructEncodingWithScanner(scanner, debugStruct); 106 | 107 | std::shared_ptr valueFromBefore; 108 | if (!types.empty()) { 109 | valueFromBefore = std::dynamic_pointer_cast(types.back()); 110 | types.pop_back(); 111 | } 112 | const auto extractedNameFromBefore = valueFromBefore ? valueFromBefore->value 113 | : ""; 114 | std::shared_ptr type = std::make_shared(extractedNameFromBefore, 115 | scanner.string.substr(locBefore, (scanner.index - locBefore)), 116 | parseResult.typeName, 117 | parseResult.containedTypes); 118 | 119 | types.emplace_back(type); 120 | } else { 121 | // It's a type name (literal), let's advance until we find '"', or '}' 122 | const auto parseResult = scanner.scanUpToCharacterFromSet(kLiteralEndingCharacters); 123 | std::string nameFromBefore = ""; 124 | if (types.size() > 0) { 125 | if (std::shared_ptr maybeUnresolved = std::dynamic_pointer_cast(types.back())) { 126 | nameFromBefore = maybeUnresolved->value; 127 | types.pop_back(); 128 | } 129 | } 130 | std::shared_ptr type = std::make_shared(nameFromBefore, parseResult); 131 | types.emplace_back(type); 132 | } 133 | } 134 | 135 | std::vector> filteredVector; 136 | 137 | for (const auto &t: types) { 138 | if (const auto convertedType = std::dynamic_pointer_cast(t)) { 139 | filteredVector.emplace_back(convertedType); 140 | } 141 | } 142 | 143 | return { 144 | .containedTypes = filteredVector, 145 | .typeName = structTypeName, 146 | }; 147 | } 148 | 149 | Struct parseStructEncoding(const std::string &structEncodingString) { 150 | return parseStructEncodingWithName(structEncodingString, ""); 151 | } 152 | 153 | Struct parseStructEncodingWithName(const std::string &structEncodingString, 154 | const std::string &structName) { 155 | _StringScanner scanner = _StringScanner(structEncodingString); 156 | auto result = _ParseStructEncodingWithScanner(scanner, 157 | [NSString stringWithCString:structEncodingString.c_str() 158 | encoding:NSUTF8StringEncoding]); 159 | 160 | Struct outerStruct = Struct(structName, 161 | structEncodingString, 162 | result.typeName, 163 | result.containedTypes); 164 | outerStruct.passTypePath({}); 165 | return outerStruct; 166 | } 167 | } } } 168 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Layout/Classes/Parser/Struct.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import 10 | 11 | #import "Type.h" 12 | 13 | #import 14 | #import 15 | #import 16 | 17 | namespace FB { namespace RetainCycleDetector { namespace Parser { 18 | class Struct: public Type { 19 | public: 20 | const std::string structTypeName; 21 | 22 | Struct(const std::string &name, 23 | const std::string &typeEncoding, 24 | const std::string &structTypeName, 25 | std::vector> &typesContainedInStruct) 26 | : Type(name, typeEncoding), 27 | structTypeName(structTypeName), 28 | typesContainedInStruct(std::move(typesContainedInStruct)) {}; 29 | Struct(Struct&&) = default; 30 | 31 | Struct(const Struct&) = delete; 32 | Struct &operator=(const Struct&) = delete; 33 | 34 | std::vector> flattenTypes(); 35 | 36 | virtual void passTypePath(std::vector typePath); 37 | std::vector> typesContainedInStruct; 38 | }; 39 | } } } 40 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Layout/Classes/Parser/Struct.mm: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import "Struct.h" 10 | 11 | #import 12 | 13 | namespace FB { namespace RetainCycleDetector { namespace Parser { 14 | void Struct::passTypePath(std::vector typePath) { 15 | this->typePath = typePath; 16 | 17 | if (name.length() > 0) { 18 | typePath.emplace_back(name); 19 | } 20 | if (structTypeName.length() > 0 && structTypeName != "?") { 21 | typePath.emplace_back(structTypeName); 22 | } 23 | 24 | for (auto &type: typesContainedInStruct) { 25 | type->passTypePath(typePath); 26 | } 27 | } 28 | 29 | std::vector> Struct::flattenTypes() { 30 | std::vector> flattenedTypes; 31 | 32 | for (const auto &type:typesContainedInStruct) { 33 | const auto maybeStruct = std::dynamic_pointer_cast(type); 34 | if (maybeStruct) { 35 | // Complex type, recursively grab all references 36 | flattenedTypes.reserve(flattenedTypes.size() + std::distance(maybeStruct->typesContainedInStruct.begin(), 37 | maybeStruct->typesContainedInStruct.end())); 38 | flattenedTypes.insert(flattenedTypes.end(), 39 | maybeStruct->typesContainedInStruct.begin(), 40 | maybeStruct->typesContainedInStruct.end()); 41 | } else { 42 | // Simple type 43 | flattenedTypes.emplace_back(type); 44 | } 45 | } 46 | 47 | return flattenedTypes; 48 | } 49 | 50 | } } } 51 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Layout/Classes/Parser/Type.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import 10 | 11 | #import 12 | #import 13 | #import 14 | 15 | #import "BaseType.h" 16 | 17 | namespace FB { namespace RetainCycleDetector { namespace Parser { 18 | class Type: public BaseType { 19 | public: 20 | const std::string name; 21 | const std::string typeEncoding; 22 | 23 | Type(const std::string &name, 24 | const std::string &typeEncoding): name(name), typeEncoding(typeEncoding) {} 25 | Type(Type&&) = default; 26 | 27 | Type(const Type&) = delete; 28 | Type &operator=(const Type&) = delete; 29 | 30 | virtual void passTypePath(std::vector typePath) { 31 | this->typePath = typePath; 32 | } 33 | 34 | std::vector typePath; 35 | }; 36 | } } } 37 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Layout/Classes/Reference/FBIvarReference.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import 10 | 11 | #import 12 | 13 | #import "FBObjectReferenceWithLayout.h" 14 | 15 | typedef NS_ENUM(NSUInteger, FBType) { 16 | FBObjectType, 17 | FBBlockType, 18 | FBStructType, 19 | FBUnknownType, 20 | }; 21 | 22 | @interface FBIvarReference : NSObject 23 | 24 | @property (nonatomic, copy, readonly, nullable) NSString *name; 25 | @property (nonatomic, readonly) FBType type; 26 | @property (nonatomic, readonly) ptrdiff_t offset; 27 | @property (nonatomic, readonly) NSUInteger index; 28 | @property (nonatomic, readonly, nonnull) Ivar ivar; 29 | 30 | - (nonnull instancetype)initWithIvar:(nonnull Ivar)ivar; 31 | 32 | @end 33 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Layout/Classes/Reference/FBIvarReference.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import "FBIvarReference.h" 10 | 11 | @implementation FBIvarReference 12 | 13 | - (instancetype)initWithIvar:(Ivar)ivar 14 | { 15 | if (self = [super init]) { 16 | /* @cwt-override FIXME[T168581563]: -Wnullable-to-nonnull-conversion */ 17 | #pragma clang diagnostic push 18 | #pragma clang diagnostic ignored "-Wnullable-to-nonnull-conversion" 19 | _name = @(ivar_getName(ivar)); 20 | #pragma clang diagnostic pop 21 | _type = [self _convertEncodingToType:ivar_getTypeEncoding(ivar)]; 22 | _offset = ivar_getOffset(ivar); 23 | _index = _offset / sizeof(void *); 24 | _ivar = ivar; 25 | } 26 | 27 | return self; 28 | } 29 | 30 | - (FBType)_convertEncodingToType:(const char *)typeEncoding 31 | { 32 | if (typeEncoding == NULL) { 33 | return FBUnknownType; 34 | } 35 | 36 | if (typeEncoding[0] == '{') { 37 | return FBStructType; 38 | } 39 | 40 | if (typeEncoding[0] == '@') { 41 | // It's an object or block 42 | 43 | // Let's try to determine if it's a block. Blocks tend to have 44 | // @? typeEncoding. Docs state that it's undefined type, so 45 | // we should still verify that ivar with that type is a block 46 | if (strncmp(typeEncoding, "@?", 2) == 0) { 47 | return FBBlockType; 48 | } 49 | 50 | return FBObjectType; 51 | } 52 | 53 | return FBUnknownType; 54 | } 55 | 56 | - (NSString *)description 57 | { 58 | return [NSString stringWithFormat:@"[%@, index: %lu]", _name, (unsigned long)_index]; 59 | } 60 | 61 | #pragma mark - FBObjectReference 62 | 63 | - (NSUInteger)indexInIvarLayout 64 | { 65 | return _index; 66 | } 67 | 68 | - (id)objectReferenceFromObject:(id)object 69 | { 70 | return object_getIvar(object, _ivar); 71 | } 72 | 73 | - (NSArray *)namePath 74 | { 75 | /* @cwt-override FIXME[T168581563]: -Wnullable-to-nonnull-conversion */ 76 | #pragma clang diagnostic push 77 | #pragma clang diagnostic ignored "-Wnullable-to-nonnull-conversion" 78 | return @[@(ivar_getName(_ivar))]; 79 | #pragma clang diagnostic pop 80 | } 81 | 82 | @end 83 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Layout/Classes/Reference/FBObjectInStructReference.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import 10 | 11 | #import "FBObjectReferenceWithLayout.h" 12 | 13 | /** 14 | Struct object is an Objective-C object that is created inside 15 | a struct. In Objective-C++ that object will be retained 16 | by an object owning the struct, therefore will be listed in 17 | ivar layout for the class. 18 | */ 19 | 20 | @interface FBObjectInStructReference : NSObject 21 | 22 | - (nonnull instancetype)initWithIndex:(NSUInteger)index 23 | namePath:(nullable NSArray *)namePath; 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Layout/Classes/Reference/FBObjectInStructReference.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import "FBObjectInStructReference.h" 10 | 11 | #import "FBClassStrongLayoutHelpers.h" 12 | 13 | @implementation FBObjectInStructReference 14 | { 15 | NSUInteger _index; 16 | NSArray *_namePath; 17 | } 18 | 19 | - (instancetype)initWithIndex:(NSUInteger)index 20 | namePath:(NSArray *)namePath 21 | { 22 | if (self = [super init]) { 23 | _index = index; 24 | _namePath = namePath; 25 | } 26 | 27 | return self; 28 | } 29 | 30 | - (NSString *)description 31 | { 32 | return [NSString stringWithFormat:@"[in_struct; index: %td]", _index]; 33 | } 34 | 35 | #pragma mark - FBObjectReference 36 | 37 | - (id)objectReferenceFromObject:(id)object 38 | { 39 | return FBExtractObjectByOffset(object, _index); 40 | } 41 | 42 | - (NSUInteger)indexInIvarLayout 43 | { 44 | return _index; 45 | } 46 | 47 | - (NSArray *)namePath 48 | { 49 | return _namePath; 50 | } 51 | 52 | @end 53 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Layout/Classes/Reference/FBObjectReference.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import 10 | 11 | /** 12 | Defines an outgoing reference. 13 | */ 14 | 15 | @protocol FBObjectReference 16 | 17 | /** 18 | For given object we need to be able to grab that object reference. 19 | */ 20 | - (nullable id)objectReferenceFromObject:(nullable id)object; 21 | 22 | /** 23 | For given reference in an object, there can be a path of names that leads to it. 24 | For example it can be an ivar, thus the path will consist of ivar name only: 25 | @[@"_myIvar"] 26 | 27 | But it also can be a reference in some nested struct like: 28 | struct SomeStruct { 29 | NSObject *myObject; 30 | }; 31 | 32 | If that struct will be used in class, then name path would look like this: 33 | @[@"_myIvar", @"SomeStruct", @"myObject"] 34 | */ 35 | - (nullable NSArray *)namePath; 36 | 37 | @end 38 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Layout/Classes/Reference/FBObjectReferenceWithLayout.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import 10 | 11 | #import "FBObjectReference.h" 12 | 13 | /** 14 | Defines an outgoing reference from Objective-C object. 15 | */ 16 | 17 | @protocol FBObjectReferenceWithLayout 18 | 19 | /** 20 | What is the index of that reference in ivar layout? 21 | index * sizeof(void *) gives you offset from the 22 | beginning of the object. 23 | */ 24 | - (NSUInteger)indexInIvarLayout; 25 | 26 | @end 27 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Layout/Classes/Reference/FBSwiftReference.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import 10 | 11 | #import "FBObjectReference.h" 12 | 13 | @interface FBSwiftReference : NSObject 14 | 15 | @property (nonatomic, copy, readonly) NSString *name; 16 | 17 | - (nonnull instancetype)initWithName:(NSString *)name; 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Layout/Classes/Reference/FBSwiftReference.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import "FBSwiftReference.h" 10 | 11 | #import 12 | 13 | @implementation FBSwiftReference 14 | 15 | - (nonnull instancetype)initWithName:(NSString *)name { 16 | if (self = [super init]) { 17 | _name = name; 18 | } 19 | return self; 20 | } 21 | 22 | #pragma mark - FBObjectReference 23 | 24 | - (id)objectReferenceFromObject:(id)object { 25 | return [SwiftIntrospector getPropertyValueWithObject:object name:_name]; 26 | } 27 | 28 | - (NSArray *)namePath { 29 | return @[_name]; 30 | } 31 | 32 | @end 33 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Layout/Classes/SwiftIntrospectionKit.swift: -------------------------------------------------------------------------------- 1 | // (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. 2 | /** 3 | * Copyright (c) 2016-present, Facebook, Inc. 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the BSD-style license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | // From https://github.com/gor-gyolchanyan-swift/introspection-kit 11 | 12 | @objc public class PropertyIntrospection: NSObject { 13 | 14 | // MARK: PropertyIntrospection 15 | 16 | init(id: ID) { 17 | self.id = id 18 | var rawConfiguration: _RawConfiguration = ( 19 | _rawName: nil, 20 | _rawNameRelease: nil, 21 | _isStrong: false 22 | ) 23 | let maybeType = Self._rawType( 24 | _in: id.instanceType.rawValue, 25 | _at: id.index, 26 | _configuration: &rawConfiguration 27 | ) 28 | guard let type = maybeType else { 29 | preconditionFailure("execution has reached a routine that is not supposed to be reachable") 30 | } 31 | self.valueType = TypeIntrospection(rawValue: type) 32 | let maybeName = rawConfiguration._rawName.map(String.init(cString:)) 33 | guard let name = maybeName else { 34 | preconditionFailure("execution has reached a routine that is not supposed to be reachable") 35 | } 36 | self.name = name 37 | rawConfiguration._rawNameRelease?(rawConfiguration._rawName) 38 | self.offset = Self._rawOffset(_in: id.instanceType.rawValue, _at: id.index) 39 | self.isStrong = rawConfiguration._isStrong 40 | super.init() 41 | } 42 | 43 | @objc public let name: String 44 | 45 | let valueType: TypeIntrospection 46 | 47 | @objc public let offset: Int 48 | 49 | @objc public let isStrong: Bool 50 | 51 | // MARK: Identifiable 52 | 53 | let id: ID 54 | } 55 | 56 | extension PropertyIntrospection { 57 | 58 | // MARK: PropertyIntrospection - Raw 59 | 60 | private typealias _RawName = UnsafePointer 61 | 62 | private typealias _RawNameRelease = @convention(c) (_RawName?) -> Void 63 | 64 | private typealias _RawConfiguration = (_rawName: _RawName?, _rawNameRelease: _RawNameRelease?, _isStrong: Bool) 65 | 66 | @_silgen_name("swift_reflectionMirror_recursiveChildMetadata") 67 | private static func _rawType(_in enclosingType: Any.Type, _at index: Int, _configuration: UnsafeMutablePointer<_RawConfiguration>) -> Any.Type? 68 | 69 | @_silgen_name("swift_reflectionMirror_recursiveChildOffset") 70 | private static func _rawOffset(_in enclosingType: Any.Type, _at index: Int) -> Int 71 | } 72 | 73 | extension PropertyIntrospection { 74 | 75 | // MARK: PropertyIntrospection - ID 76 | 77 | struct ID: Hashable { 78 | 79 | // MARK: PropertyIntrospection.ID 80 | 81 | init?( 82 | in instanceType: TypeIntrospection, 83 | at index: Int 84 | ) { 85 | guard instanceType.properties.indices.contains(index) else { 86 | return nil 87 | } 88 | self.instanceType = instanceType 89 | self.index = index 90 | } 91 | 92 | let instanceType: TypeIntrospection 93 | 94 | let index: Int 95 | } 96 | } 97 | 98 | 99 | struct TypeIntrospection: RawRepresentable { 100 | 101 | // MARK: RawRepresentable 102 | 103 | typealias RawValue = Any.Type 104 | 105 | init(rawValue: RawValue) { 106 | self.rawValue = rawValue 107 | } 108 | 109 | let rawValue: RawValue 110 | } 111 | 112 | 113 | extension TypeIntrospection: Hashable { 114 | 115 | // MARK: Hashable 116 | 117 | func hash(into hasher: inout Hasher) { 118 | hasher.combine(ObjectIdentifier(rawValue)) 119 | } 120 | } 121 | 122 | 123 | extension TypeIntrospection.Properties: Collection { 124 | 125 | // MARK: Collection 126 | 127 | var count: Int { 128 | Self._rawPropertyCount(_in: instanceType.rawValue) 129 | } 130 | 131 | // MARK: Collection - Index 132 | 133 | typealias Index = Int 134 | 135 | var startIndex: Index { 136 | return .zero 137 | } 138 | 139 | var endIndex: Index { 140 | return count 141 | } 142 | 143 | func index(after anotherIndex: Index) -> Index { 144 | return anotherIndex + 1 145 | } 146 | 147 | func index(_ anotherIndex: Index, offsetBy distance: Int) -> Index { 148 | return anotherIndex + distance 149 | } 150 | 151 | func index(_ anotherIndex: Index, offsetBy distance: Int, limitedBy limit: Index) -> Index? { 152 | let resultingIndex = index(anotherIndex, offsetBy: distance) 153 | guard resultingIndex < limit else { 154 | return nil 155 | } 156 | return resultingIndex 157 | } 158 | 159 | // MARK: Collection - Element 160 | 161 | subscript(_ index: Index) -> Element { 162 | guard let propertyID = PropertyIntrospection.ID(in: instanceType, at: index) else { 163 | preconditionFailure("property index is out of range") 164 | } 165 | return PropertyIntrospection(id: propertyID) 166 | } 167 | } 168 | 169 | extension TypeIntrospection.Properties { 170 | 171 | // MARK: TypeIntrospection.Properties - Raw 172 | 173 | @_silgen_name("swift_reflectionMirror_recursiveCount") 174 | private static func _rawPropertyCount(_in type: Any.Type) -> Int 175 | 176 | } 177 | 178 | extension TypeIntrospection.Properties: RandomAccessCollection { 179 | 180 | // MARK: RandomAccessCollection 181 | 182 | // This scope is intentionally left blank. 183 | } 184 | 185 | 186 | extension TypeIntrospection { 187 | 188 | // MARK: TypeIntrospection - Properties 189 | 190 | struct Properties { 191 | 192 | // MARK: TypeIntrospection.Properties 193 | 194 | internal init(in instanceType: TypeIntrospection) { 195 | self.instanceType = instanceType 196 | } 197 | 198 | internal let instanceType: TypeIntrospection 199 | } 200 | } 201 | 202 | 203 | 204 | extension TypeIntrospection.Properties: BidirectionalCollection { 205 | 206 | // MARK: BidirectionalCollection - Index 207 | 208 | func index(before anotherIndex: Index) -> Index { 209 | return anotherIndex - 1 210 | } 211 | } 212 | 213 | extension TypeIntrospection.Properties: Equatable { 214 | 215 | // MARK: Equatable 216 | 217 | static func == (_ some: Self, _ other: Self) -> Bool { 218 | return some.lazy.map(\.id) == other.lazy.map(\.id) 219 | } 220 | } 221 | 222 | extension TypeIntrospection.Properties: Sequence { 223 | 224 | // MARK: Sequence - Element 225 | 226 | typealias Element = PropertyIntrospection 227 | } 228 | 229 | extension TypeIntrospection: Equatable { 230 | 231 | // MARK: Equatable 232 | 233 | static func == (_ some: Self, _ other: Self) -> Bool { 234 | ObjectIdentifier(some.rawValue) == ObjectIdentifier(other.rawValue) 235 | } 236 | } 237 | 238 | extension TypeIntrospection { 239 | 240 | // MARK: TypeIntrospection - Properties 241 | 242 | var properties: Properties { 243 | Properties(in: self) 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /FBRetainCycleDetector/Layout/Classes/SwiftIntrospector.swift: -------------------------------------------------------------------------------- 1 | // (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. 2 | /** 3 | * Copyright (c) 2016-present, Facebook, Inc. 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the BSD-style license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | import Foundation 11 | 12 | @objc public class SwiftIntrospector: NSObject { 13 | 14 | /// Get list of properties, which incluses all Swift superclasses (recurive until we get to a Objective-c superclass) 15 | /// The Mirror framework doesn't tell us if a property is strong or weak, so we have to use something like `PropertyIntrospection` 16 | /// Maybe there is a way to use `@_silgen_name` to get non-recursive properties, so we don't need to do this. 17 | @objc public class func getPropertiesRecursive(object:Any) -> [PropertyIntrospection] { 18 | let typeObj = type(of: object) 19 | let introspection = TypeIntrospection(rawValue: typeObj) 20 | var properties:[PropertyIntrospection] = [] 21 | for property in introspection.properties { 22 | properties.append(property) 23 | } 24 | return properties 25 | } 26 | 27 | /// Get the value of the property 28 | @objc public class func getPropertyValue(object:Any, name:String) -> Any? { 29 | let mirror = Mirror(reflecting: object) 30 | var currentMirror: Mirror? = mirror 31 | while currentMirror != nil { 32 | let (value, found) = getChild(mirror: currentMirror!, name: name) 33 | if found { 34 | return value 35 | } else { 36 | // Check parent mirror if present 37 | currentMirror = currentMirror?.superclassMirror 38 | } 39 | } 40 | return nil 41 | } 42 | 43 | private class func getChild(mirror: Mirror, name: String) -> (Any?, Bool) { 44 | guard let array = AnyBidirectionalCollection(mirror.children) else { 45 | return (nil, false) 46 | } 47 | 48 | for child in array { 49 | if let label = child.label, label == name { 50 | return (child.value, true) 51 | } 52 | } 53 | return (nil, false) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /FBRetainCycleDetectorTests/FBAssociationManagerTests.mm: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import 10 | 11 | #import 12 | 13 | #import 14 | #import 15 | #import 16 | #import 17 | 18 | @interface FBAssociationManagerTests : XCTestCase 19 | @end 20 | 21 | @implementation FBAssociationManagerTests 22 | 23 | #if _INTERNAL_RCD_ENABLED 24 | 25 | static const char *strongAssocKey1 = "strong_assoc1"; 26 | static const char *strongAssocKey2 = "strong_assoc2"; 27 | 28 | - (void)testIfRetainCycleWithAssociatedObjectStrongIsFound 29 | { 30 | NSObject *object = [NSObject new]; 31 | NSArray *array = @[object]; 32 | 33 | objc_setAssociatedObject(object, strongAssocKey1, array, OBJC_ASSOCIATION_RETAIN); 34 | 35 | // We are not interposing in tests, sounds too flaky, let's add it manually 36 | FB::AssociationManager::_threadUnsafeSetStrongAssociation(object, (void *)strongAssocKey1, array); 37 | 38 | XCTAssertEqual([FB::AssociationManager::associations(object) count], 1); 39 | 40 | FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; 41 | [detector addCandidate:array]; 42 | NSSet *retainCycles = [detector findRetainCycles]; 43 | 44 | NSSet *expectedCycles = [NSSet setWithObject:[detector _shiftToUnifiedCycle: 45 | @[[[FBObjectiveCObject alloc] initWithObject:object], 46 | [[FBObjectiveCObject alloc] initWithObject:array]]]]; 47 | 48 | XCTAssertEqualObjects(retainCycles, expectedCycles); 49 | } 50 | 51 | - (void)testIfRetainCycleWithAssociatedObjectStrongIsAddedAndThenRemoved 52 | { 53 | NSObject *object = [NSObject new]; 54 | NSArray *array = @[object]; 55 | 56 | objc_setAssociatedObject(object, strongAssocKey2, array, OBJC_ASSOCIATION_RETAIN); 57 | FB::AssociationManager::_threadUnsafeSetStrongAssociation(object, (void *)strongAssocKey2, array); 58 | 59 | objc_setAssociatedObject(object, strongAssocKey2, nil, OBJC_ASSOCIATION_RETAIN); 60 | FB::AssociationManager::_threadUnsafeSetStrongAssociation(object, (void *)strongAssocKey2, nil); 61 | 62 | FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; 63 | [detector addCandidate:array]; 64 | NSSet *retainCycles = [detector findRetainCycles]; 65 | 66 | XCTAssertEqual([retainCycles count], 0); 67 | } 68 | 69 | #endif 70 | 71 | @end 72 | -------------------------------------------------------------------------------- /FBRetainCycleDetectorTests/FBBlockRecognizingTests.mm: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #if __has_feature(objc_arc) 10 | #error This file must be compiled with MRR. Use -fno-objc-arc flag. 11 | #endif 12 | 13 | #import 14 | 15 | #import 16 | #import 17 | 18 | @interface FBBlockRecognizingTests : XCTestCase 19 | @end 20 | 21 | void (^_RCDTestGlobalBlock)(void) = ^{}; 22 | 23 | @implementation FBBlockRecognizingTests 24 | 25 | - (void)testThatGlobalBlockWillBeRecognizedAsBlock 26 | { 27 | XCTAssertTrue(FBObjectIsBlock(_RCDTestGlobalBlock)); 28 | } 29 | 30 | - (void)testThatHeapBlockWillBeRecognizedAsBlock 31 | { 32 | int i = 0; 33 | void (^_RCDTestHeapBlock)(void) = ^{ 34 | printf("%d", i); 35 | }; 36 | 37 | XCTAssertTrue(FBObjectIsBlock([[_RCDTestHeapBlock copy] autorelease])); 38 | } 39 | 40 | - (void)testThatStackBlockWillBeRecognizedAsBlock 41 | { 42 | int i = 0; 43 | 44 | void (^testStackBlock)(void) = ^{ 45 | printf("%d", i); 46 | }; 47 | 48 | XCTAssertTrue(FBObjectIsBlock(testStackBlock)); 49 | } 50 | 51 | @end 52 | -------------------------------------------------------------------------------- /FBRetainCycleDetectorTests/FBBlockStrongLayoutTests.mm: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import 10 | #import 11 | #import 12 | 13 | #import 14 | 15 | #import 16 | #import 17 | @interface FBBlockStrongLayoutTests : XCTestCase 18 | @end 19 | 20 | @implementation FBBlockStrongLayoutTests 21 | 22 | - (void)testBlockDoesntRetainWeakReference 23 | { 24 | __attribute__((objc_precise_lifetime)) NSObject *object = [NSObject new]; 25 | __weak NSObject *weakObject = object; 26 | 27 | void (^block)() = ^{ 28 | __unused NSObject *someObject = weakObject; 29 | }; 30 | 31 | NSArray *retainedObjects = FBGetBlockStrongReferences((__bridge void *)(block)); 32 | 33 | XCTAssertEqual([retainedObjects count], 0); 34 | } 35 | 36 | - (void)testBlockRetainsStrongReference 37 | { 38 | NSObject *object = [NSObject new]; 39 | 40 | void (^block)() = ^{ 41 | __unused NSObject *someObject = object; 42 | }; 43 | 44 | NSArray *retainedObjects = FBGetBlockStrongReferences((__bridge void *)(block)); 45 | 46 | XCTAssertEqual([retainedObjects count], 1); 47 | XCTAssertEqualObjects(retainedObjects[0], object); 48 | } 49 | 50 | - (void)testThatBlockRetainingVectorOfObjectsDoNotCrash 51 | { 52 | NSObject *object = [NSObject new]; 53 | std::vector vector = {object}; 54 | 55 | void (^block)() = ^{ 56 | __unused std::vector someVector = vector; 57 | }; 58 | 59 | NSArray *retainedObjects = FBGetBlockStrongReferences((__bridge void *)(block)); 60 | 61 | XCTAssertEqual([retainedObjects count], 0); 62 | } 63 | 64 | - (void)testThatBlockRetainingVectorOfStructsDoNotCrash 65 | { 66 | struct HelperStruct {}; 67 | std::vector vector = {}; 68 | 69 | void (^block)() = ^{ 70 | __unused std::vector someVector = vector; 71 | }; 72 | 73 | NSArray *retainedObjects = FBGetBlockStrongReferences((__bridge void *)(block)); 74 | 75 | XCTAssertEqual([retainedObjects count], 0); 76 | } 77 | 78 | - (void)testThatBlockUsingCppButRetainingOnlyObjectsWillReturnTheObjectAndNotCrash 79 | { 80 | NSObject *object = [NSObject new]; 81 | 82 | void (^block)() = ^{ 83 | std::vector vector; 84 | vector.push_back(object); 85 | }; 86 | 87 | NSArray *retainedObjects = FBGetBlockStrongReferences((__bridge void *)(block)); 88 | 89 | XCTAssertEqual([retainedObjects count], 1); 90 | XCTAssertEqualObjects(retainedObjects[0], object); 91 | } 92 | 93 | - (void)testThatBlockRetainingMapWillNotCrash 94 | { 95 | struct HelperStruct{}; 96 | std::unordered_map map; 97 | 98 | void (^block)() = ^{ 99 | __unused std::unordered_map someMap = map; 100 | }; 101 | 102 | NSArray *retainedObjects = FBGetBlockStrongReferences((__bridge void *)(block)); 103 | 104 | XCTAssertEqual([retainedObjects count], 0); 105 | } 106 | 107 | @end 108 | -------------------------------------------------------------------------------- /FBRetainCycleDetectorTests/FBClassStrongLayoutTests.mm: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import 10 | 11 | #import 12 | #import 13 | 14 | #import 15 | #import 16 | 17 | @interface _RCDTestEmptyClass : NSObject 18 | @end 19 | @implementation _RCDTestEmptyClass 20 | @end 21 | 22 | @interface _RCDTestClassWithWeakProperty : NSObject 23 | @property (nonatomic, weak) NSObject *object; 24 | @end 25 | @implementation _RCDTestClassWithWeakProperty 26 | @end 27 | 28 | @interface _RCDTestClassWithStrongProperty : NSObject 29 | @property (nonatomic, strong) NSObject *object; 30 | @end 31 | @implementation _RCDTestClassWithStrongProperty 32 | @end 33 | 34 | @interface _RCDTestClassWithMixedWeakAndStrongProperties : NSObject 35 | @property (nonatomic, strong) NSObject *object1; 36 | @property (nonatomic, strong) NSObject *object2; 37 | @property (nonatomic, strong) NSObject *object3; 38 | @property (nonatomic, weak) NSObject *object4; 39 | @property (nonatomic, strong) NSObject *object5; 40 | @property (nonatomic, weak) NSObject *object6; 41 | @end 42 | @implementation _RCDTestClassWithMixedWeakAndStrongProperties 43 | @end 44 | 45 | @interface _RCDTestClassWithSimpleInheritance : _RCDTestEmptyClass 46 | @property (nonatomic, strong) NSObject *object1; 47 | @property (nonatomic, weak) NSObject *object2; 48 | @end 49 | @implementation _RCDTestClassWithSimpleInheritance 50 | @end 51 | 52 | @interface _RCDTestClassSubclassingClassWithStrongProperties : _RCDTestClassWithMixedWeakAndStrongProperties 53 | @property (nonatomic, weak) NSObject *object7; 54 | @end 55 | @implementation _RCDTestClassSubclassingClassWithStrongProperties 56 | @end 57 | 58 | typedef struct { 59 | int someInt; 60 | char someCharacter; 61 | unsigned long long someUnsignedLongLong; 62 | } _RCDTestStructWithPrimitives; 63 | 64 | @interface _RCDTestClassWithSimpleStruct : NSObject 65 | @property (nonatomic, assign) _RCDTestStructWithPrimitives structure; 66 | @end 67 | @implementation _RCDTestClassWithSimpleStruct 68 | @end 69 | 70 | typedef struct { 71 | NSObject *retainedObject; 72 | int number; 73 | NSObject *anotherRetainedObject; 74 | } _RCDTestStructWithObjects; 75 | 76 | @interface _RCDTestClassWithStructContainingObjects : NSObject 77 | @property (nonatomic, assign) _RCDTestStructWithObjects structure; 78 | @end 79 | @implementation _RCDTestClassWithStructContainingObjects 80 | @end 81 | 82 | typedef struct { 83 | __weak NSObject *object; 84 | } _RCDTestStructWithWeakObject; 85 | 86 | @interface _RCDTestClassWithStructContainingWeakObject : NSObject 87 | @property (nonatomic, assign) _RCDTestStructWithWeakObject structure; 88 | @end 89 | @implementation _RCDTestClassWithStructContainingWeakObject 90 | @end 91 | 92 | typedef struct { 93 | int a; 94 | _RCDTestStructWithObjects someStruct; 95 | char b; 96 | int c; 97 | int d; 98 | __weak NSObject *object; 99 | float *e; 100 | void *f; 101 | NSObject *g; 102 | _RCDTestStructWithObjects someStruct2; 103 | } _RCDTestStructWithComplicatedLayout; 104 | 105 | @interface _RCDTestClassWithComplicatedStruct : NSObject 106 | @property (nonatomic, assign) _RCDTestStructWithComplicatedLayout testStruct; 107 | @end 108 | @implementation _RCDTestClassWithComplicatedStruct 109 | @end 110 | 111 | typedef struct { 112 | unsigned somebit1: 1; 113 | unsigned somebit2: 1; 114 | unsigned somebit3: 1; 115 | unsigned somebit4: 1; 116 | unsigned somebit5: 6; 117 | } _RCDTestStructWithBitfields; 118 | 119 | @interface _RCDTestClassWithBitfieldStructAndStrongProperties : NSObject 120 | @property (nonatomic, strong) NSObject *object1; 121 | @property (nonatomic, assign) _RCDTestStructWithBitfields someStruct; 122 | @property (nonatomic, strong) NSObject *object2; 123 | @end 124 | @implementation _RCDTestClassWithBitfieldStructAndStrongProperties 125 | @end 126 | 127 | @interface _RCDTestClassWithEnumValue : NSObject 128 | @end 129 | @implementation _RCDTestClassWithEnumValue 130 | { 131 | UIRectEdge rectEdge; 132 | } 133 | @end 134 | 135 | @interface _RCDTestClassWithSharedPointer : NSObject 136 | @end 137 | @implementation _RCDTestClassWithSharedPointer 138 | { 139 | std::shared_ptr<_RCDTestStructWithObjects> _sharedPointer; 140 | } 141 | - (instancetype)init 142 | { 143 | if (self = [super init]) { 144 | _sharedPointer = std::shared_ptr<_RCDTestStructWithObjects> (new _RCDTestStructWithObjects); 145 | } 146 | 147 | return self; 148 | } 149 | @end 150 | 151 | @interface _RCDTestClassWithCppStructAndStrongProperty : NSObject 152 | @end 153 | @implementation _RCDTestClassWithCppStructAndStrongProperty 154 | { 155 | std::atomic _someAtomicValue; 156 | NSObject *_object; 157 | } 158 | @end 159 | 160 | @interface FBClassStrongLayoutTests : XCTestCase 161 | @end 162 | 163 | @implementation FBClassStrongLayoutTests 164 | 165 | - (void)testLayoutForEmptyClassWillBeEmpty 166 | { 167 | NSArray *ivars = FBGetObjectStrongReferences([_RCDTestEmptyClass new], nil, false); 168 | 169 | XCTAssertEqual([ivars count], 0); 170 | } 171 | 172 | - (void)testLayoutForClassWithWeakPropertyWillBeEmpty 173 | { 174 | NSArray *ivars = FBGetObjectStrongReferences([_RCDTestClassWithWeakProperty new], nil, false); 175 | 176 | XCTAssertEqual([ivars count], 0); 177 | } 178 | 179 | - (void)testLayoutForClassWithStrongPropertyWillHaveOneReference 180 | { 181 | NSArray *ivars = FBGetObjectStrongReferences([_RCDTestClassWithStrongProperty new], nil, false); 182 | 183 | XCTAssertEqual([ivars count], 1); 184 | } 185 | 186 | - (void)testLayoutForClassWithMixedStrongAndWeakWillFetchOnlyStrong 187 | { 188 | NSArray *ivars = FBGetObjectStrongReferences([_RCDTestClassWithMixedWeakAndStrongProperties new], nil, false); 189 | 190 | XCTAssertEqual([ivars count], 4); 191 | } 192 | 193 | - (void)testLayoutForClassSubclassingEmptyClassWillFetchPropertiesProperly 194 | { 195 | NSArray *ivars = FBGetObjectStrongReferences([_RCDTestClassWithSimpleInheritance new], nil,false); 196 | 197 | XCTAssertEqual([ivars count], 1); 198 | } 199 | 200 | - (void)testLayoutForClassSubclassingClassWithStrongPropertiesWillFetchParentsClassProperties 201 | { 202 | NSArray *ivars = FBGetObjectStrongReferences([_RCDTestClassSubclassingClassWithStrongProperties new], nil,false); 203 | 204 | XCTAssertEqual([ivars count], 4); 205 | } 206 | 207 | - (void)testLayoutForClassWithStructAsIvarWillNotCrash 208 | { 209 | NSArray *ivars = FBGetObjectStrongReferences([_RCDTestClassWithSimpleStruct new], nil, false); 210 | 211 | XCTAssertEqual([ivars count], 0); 212 | } 213 | 214 | - (void)testLayoutForClassWithStructContainingObjectsWillFetchThoseObjects 215 | { 216 | NSArray *ivars = FBGetObjectStrongReferences([_RCDTestClassWithStructContainingObjects new], nil, false); 217 | 218 | XCTAssertEqual([ivars count], 2); 219 | } 220 | 221 | - (void)testLayoutForClassWithStructContainingWeakObjectWillBeEmpty 222 | { 223 | NSArray *ivars = FBGetObjectStrongReferences([_RCDTestClassWithStructContainingWeakObject new], nil, false); 224 | 225 | XCTAssertEqual([ivars count], 0); 226 | } 227 | 228 | - (void)testLayoutForClassWithComplicatedStructWillWorkProperly 229 | { 230 | NSArray *ivars = FBGetObjectStrongReferences([_RCDTestClassWithComplicatedStruct new], nil, false); 231 | 232 | XCTAssertEqual([ivars count], 5); 233 | } 234 | 235 | - (void)testLayoutForClassWithBitfieldsWillNotCrash 236 | { 237 | NSArray *ivars = FBGetObjectStrongReferences([_RCDTestClassWithBitfieldStructAndStrongProperties new], nil, false); 238 | 239 | XCTAssertEqual([ivars count], 2); 240 | } 241 | 242 | - (void)testLayoutForClassWithEnumValueWillNotCrash 243 | { 244 | NSArray *ivars = FBGetObjectStrongReferences([_RCDTestClassWithEnumValue new], nil, false); 245 | 246 | XCTAssertEqual([ivars count], 0); 247 | } 248 | 249 | - (void)testLayoutForClassWithSharedPointerWillNotCrash 250 | { 251 | NSArray *ivars = FBGetObjectStrongReferences([_RCDTestClassWithSharedPointer new], nil, false); 252 | 253 | XCTAssertEqual([ivars count], 0); 254 | } 255 | 256 | - (void)testLayoutForClassWithCppStructAndStrongPropertyWillNotCrashAndFetchStrongProperty 257 | { 258 | NSArray *ivars = FBGetObjectStrongReferences([_RCDTestClassWithCppStructAndStrongProperty new], nil, false); 259 | 260 | XCTAssertEqual([ivars count], 1); 261 | } 262 | 263 | @end 264 | -------------------------------------------------------------------------------- /FBRetainCycleDetectorTests/FBGraphEdgeFilterTests.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import 10 | 11 | #import 12 | #import 13 | #import 14 | #import 15 | 16 | @interface FBGraphEdgeFilterTestClass: NSObject 17 | @property (nonatomic, strong) NSObject *filtered; 18 | @property (nonatomic, strong) NSObject *filtered2; 19 | @end 20 | @implementation FBGraphEdgeFilterTestClass 21 | @end 22 | 23 | @interface FBGraphEdgeFilterTests : XCTestCase 24 | @end 25 | 26 | @implementation FBGraphEdgeFilterTests 27 | 28 | #if _INTERNAL_RCD_ENABLED 29 | 30 | - (void)testIfApplyingEmptyFilterWillNotAlterResults 31 | { 32 | FBGraphEdgeFilterTestClass *testObject = [FBGraphEdgeFilterTestClass new]; 33 | testObject.filtered = testObject; 34 | 35 | FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; 36 | [detector addCandidate:testObject]; 37 | 38 | NSSet *retainCycles = [detector findRetainCycles]; 39 | 40 | NSSet *expectedCycles = [NSSet setWithObject:@[[[FBObjectiveCObject alloc] initWithObject:testObject]]]; 41 | 42 | XCTAssertEqualObjects(retainCycles, expectedCycles); 43 | } 44 | 45 | - (void)testIfApplyingFilterForOnePropertyWillFilterOutThatProperty 46 | { 47 | FBGraphEdgeFilterTestClass *testObject = [FBGraphEdgeFilterTestClass new]; 48 | testObject.filtered = testObject; 49 | 50 | NSArray *filterBlocks = @[FBFilterBlockWithObjectIvarRelation([FBGraphEdgeFilterTestClass class], 51 | @"_filtered")]; 52 | FBObjectGraphConfiguration *configuration = 53 | [[FBObjectGraphConfiguration alloc] initWithFilterBlocks:filterBlocks 54 | shouldInspectTimers:YES]; 55 | 56 | FBRetainCycleDetector *detector = [[FBRetainCycleDetector alloc] initWithConfiguration:configuration]; 57 | 58 | [detector addCandidate:testObject]; 59 | 60 | NSSet *retainCycles = [detector findRetainCycles]; 61 | 62 | XCTAssertEqual([retainCycles count], 0); 63 | } 64 | 65 | - (void)testIfApplyingFilterToOnePropertyButSettingBothPropertiesWillStillYieldProperCycle 66 | { 67 | FBGraphEdgeFilterTestClass *testObject = [FBGraphEdgeFilterTestClass new]; 68 | testObject.filtered = testObject; 69 | testObject.filtered2 = testObject; 70 | 71 | NSArray *filterBlocks = @[FBFilterBlockWithObjectIvarRelation([FBGraphEdgeFilterTestClass class], 72 | @"_filtered")]; 73 | FBObjectGraphConfiguration *configuration = 74 | [[FBObjectGraphConfiguration alloc] initWithFilterBlocks:filterBlocks 75 | shouldInspectTimers:YES]; 76 | 77 | FBRetainCycleDetector *detector = [[FBRetainCycleDetector alloc] initWithConfiguration:configuration]; 78 | 79 | [detector addCandidate:testObject]; 80 | 81 | NSSet *retainCycles = [detector findRetainCycles]; 82 | NSSet *expectedCycles = [NSSet setWithObject:@[[[FBObjectiveCObject alloc] initWithObject:testObject]]]; 83 | 84 | XCTAssertEqualObjects(retainCycles, expectedCycles); 85 | } 86 | 87 | - (void)testIfApplyingFilterForBothPropertiesWillFilterOutBothProperties 88 | { 89 | FBGraphEdgeFilterTestClass *testObject = [FBGraphEdgeFilterTestClass new]; 90 | testObject.filtered = testObject; 91 | testObject.filtered2 = testObject; 92 | 93 | NSArray *filterBlocks = 94 | @[FBFilterBlockWithObjectToManyIvarsRelation([FBGraphEdgeFilterTestClass class], 95 | [NSSet setWithArray:@[@"_filtered", @"_filtered2"]])]; 96 | 97 | FBObjectGraphConfiguration *configuration = 98 | [[FBObjectGraphConfiguration alloc] initWithFilterBlocks:filterBlocks 99 | shouldInspectTimers:YES]; 100 | 101 | FBRetainCycleDetector *detector = [[FBRetainCycleDetector alloc] initWithConfiguration:configuration]; 102 | 103 | [detector addCandidate:testObject]; 104 | 105 | NSSet *retainCycles = [detector findRetainCycles]; 106 | 107 | XCTAssertEqual([retainCycles count], 0); 108 | } 109 | 110 | #endif //_INTERNAL_RCD_ENABLED 111 | 112 | @end 113 | -------------------------------------------------------------------------------- /FBRetainCycleDetectorTests/FBObjectiveCBlockTests.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import 10 | 11 | #import 12 | #import 13 | #import 14 | 15 | #import 16 | 17 | typedef void (^_RCDTestBlockType)(void); 18 | 19 | @interface FBObjectiveCBlockTests : XCTestCase 20 | @end 21 | 22 | @implementation FBObjectiveCBlockTests 23 | 24 | #if _INTERNAL_RCD_ENABLED 25 | 26 | - (void)testLayoutForBlockRetainingObjectWillFetchTheObject 27 | { 28 | NSObject *someObject = [NSObject new]; 29 | __block NSObject *unretainedObject; 30 | 31 | _RCDTestBlockType block = ^{ 32 | // Keep strong reference to someObject 33 | unretainedObject = someObject; 34 | }; 35 | 36 | FBObjectiveCObject *wrappedObject = [[FBObjectiveCObject alloc] initWithObject:someObject]; 37 | FBObjectiveCBlock *wrappedBlock = [[FBObjectiveCBlock alloc] initWithObject:block]; 38 | 39 | NSSet *retainedObjects = [wrappedBlock allRetainedObjects]; 40 | XCTAssertTrue([retainedObjects containsObject:wrappedObject]); 41 | } 42 | 43 | - (void)testLayoutForBlockRetainingOtherBlockWillFetchTheBlock 44 | { 45 | _RCDTestBlockType block1 = ^{}; 46 | _RCDTestBlockType block2 = ^{ 47 | block1(); 48 | }; 49 | 50 | FBObjectiveCBlock *wrappedBlock1 = [[FBObjectiveCBlock alloc] initWithObject:block1]; 51 | FBObjectiveCBlock *wrappedBlock2 = [[FBObjectiveCBlock alloc] initWithObject:block2]; 52 | 53 | NSSet *retainedObjects = [wrappedBlock2 allRetainedObjects]; 54 | XCTAssertTrue([retainedObjects containsObject:wrappedBlock1]); 55 | } 56 | 57 | - (void)testLayoutForBlockRetainingFewObjectsWillFetchAllOfThem 58 | { 59 | NSObject *someObject1 = [NSObject new]; 60 | NSObject *someObject2 = [NSObject new]; 61 | NSObject *someObject3 = [NSObject new]; 62 | __block NSObject *unretainedObject; 63 | 64 | _RCDTestBlockType block = ^{ 65 | // Keep strong reference to someObject 66 | unretainedObject = someObject1; 67 | unretainedObject = someObject2; 68 | unretainedObject = someObject3; 69 | }; 70 | 71 | FBObjectiveCObject *wrappedObject1 = [[FBObjectiveCObject alloc] initWithObject:someObject1]; 72 | FBObjectiveCObject *wrappedObject2 = [[FBObjectiveCObject alloc] initWithObject:someObject2]; 73 | FBObjectiveCObject *wrappedObject3 = [[FBObjectiveCObject alloc] initWithObject:someObject3]; 74 | FBObjectiveCBlock *wrappedBlock = [[FBObjectiveCBlock alloc] initWithObject:block]; 75 | 76 | NSSet *retainedObjects = [wrappedBlock allRetainedObjects]; 77 | 78 | XCTAssertTrue([retainedObjects containsObject:wrappedObject1]); 79 | XCTAssertTrue([retainedObjects containsObject:wrappedObject2]); 80 | XCTAssertTrue([retainedObjects containsObject:wrappedObject3]); 81 | } 82 | 83 | - (void)testLayoutForBlockKeepingObjectBlockMixin 84 | { 85 | NSObject *someObject1 = [NSObject new]; 86 | NSObject *someObject2 = [NSObject new]; 87 | NSObject *someObject3 = [NSObject new]; 88 | _RCDTestBlockType someBlock1 = ^{}; 89 | _RCDTestBlockType someBlock2 = ^{}; 90 | _RCDTestBlockType someBlock3 = ^{}; 91 | __block NSObject *unretainedObject; 92 | 93 | _RCDTestBlockType block = ^{ 94 | // Keep strong reference to someObject 95 | someBlock1(); 96 | unretainedObject = someObject1; 97 | unretainedObject = someObject2; 98 | someBlock2(); 99 | someBlock3(); 100 | unretainedObject = someObject3; 101 | }; 102 | 103 | FBObjectiveCObject *wrappedObject1 = [[FBObjectiveCObject alloc] initWithObject:someObject1]; 104 | FBObjectiveCObject *wrappedObject2 = [[FBObjectiveCObject alloc] initWithObject:someObject2]; 105 | FBObjectiveCObject *wrappedObject3 = [[FBObjectiveCObject alloc] initWithObject:someObject3]; 106 | FBObjectiveCBlock *wrappedBlock1 = [[FBObjectiveCBlock alloc] initWithObject:someBlock1]; 107 | FBObjectiveCBlock *wrappedBlock2 = [[FBObjectiveCBlock alloc] initWithObject:someBlock2]; 108 | FBObjectiveCBlock *wrappedBlock3 = [[FBObjectiveCBlock alloc] initWithObject:someBlock3]; 109 | FBObjectiveCBlock *wrappedBlock = [[FBObjectiveCBlock alloc] initWithObject:block]; 110 | 111 | NSSet *retainedObjects = [wrappedBlock allRetainedObjects]; 112 | XCTAssertTrue([retainedObjects containsObject:wrappedObject1]); 113 | XCTAssertTrue([retainedObjects containsObject:wrappedObject2]); 114 | XCTAssertTrue([retainedObjects containsObject:wrappedObject3]); 115 | XCTAssertTrue([retainedObjects containsObject:wrappedBlock1]); 116 | XCTAssertTrue([retainedObjects containsObject:wrappedBlock2]); 117 | XCTAssertTrue([retainedObjects containsObject:wrappedBlock3]); 118 | } 119 | 120 | - (void)testLayoutForEmptyBlockWillBeEmpty 121 | { 122 | _RCDTestBlockType block = ^{}; 123 | FBObjectiveCBlock *wrappedBlock = [[FBObjectiveCBlock alloc] initWithObject:block]; 124 | NSSet *retainedObjects = [wrappedBlock allRetainedObjects]; 125 | XCTAssertEqual([retainedObjects count], 0); 126 | } 127 | 128 | #endif //_INTERNAL_RCD_ENABLED 129 | 130 | @end 131 | -------------------------------------------------------------------------------- /FBRetainCycleDetectorTests/FBObjectiveCNSCFTimerTests.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import 10 | #import 11 | 12 | #import 13 | #import 14 | #import 15 | #import 16 | #import 17 | 18 | @interface _RCDTestTimer : NSObject 19 | @property (nonatomic, strong) NSTimer *timer; 20 | @end 21 | @implementation _RCDTestTimer 22 | @end 23 | 24 | @interface FBObjectiveCNSCFTimerTests : XCTestCase 25 | @end 26 | 27 | @implementation FBObjectiveCNSCFTimerTests 28 | 29 | #if _INTERNAL_RCD_ENABLED 30 | 31 | - (void)testThatNSTimerCreatesRetainCycleAndWillBeDetected 32 | { 33 | _RCDTestTimer *testObject = [_RCDTestTimer new]; 34 | testObject.timer = [NSTimer scheduledTimerWithTimeInterval:DISPATCH_TIME_FOREVER 35 | target:testObject 36 | selector:@selector(description) 37 | userInfo:nil 38 | repeats:NO]; 39 | 40 | FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; 41 | [detector addCandidate:testObject]; 42 | NSSet *retainCycles = [detector findRetainCycles]; 43 | 44 | NSSet *expectedCycle = [NSSet setWithObject:[detector _shiftToUnifiedCycle: 45 | @[[[FBObjectiveCObject alloc] initWithObject:testObject], 46 | [[FBObjectiveCNSCFTimer alloc] initWithObject:testObject.timer]]]]; 47 | 48 | XCTAssertEqualObjects(retainCycles, expectedCycle); 49 | } 50 | 51 | - (void)testThatNSTimerCreatesRetainCycleButWillBeSkippedIfDetectorIsConfiguredToSkip 52 | { 53 | _RCDTestTimer *testObject = [_RCDTestTimer new]; 54 | testObject.timer = [NSTimer scheduledTimerWithTimeInterval:DISPATCH_TIME_FOREVER 55 | target:testObject 56 | selector:@selector(description) 57 | userInfo:nil 58 | repeats:NO]; 59 | 60 | FBObjectGraphConfiguration *configuration = 61 | [[FBObjectGraphConfiguration alloc] initWithFilterBlocks:@[] 62 | shouldInspectTimers:NO]; 63 | 64 | FBRetainCycleDetector *detector = [[FBRetainCycleDetector alloc] initWithConfiguration:configuration]; 65 | 66 | [detector addCandidate:testObject]; 67 | NSSet *retainCycles = [detector findRetainCycles]; 68 | 69 | XCTAssertEqual([retainCycles count], 0); 70 | } 71 | 72 | #endif //_INTERNAL_RCD_ENABLED 73 | 74 | @end 75 | -------------------------------------------------------------------------------- /FBRetainCycleDetectorTests/FBRCDCollectionTests.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import 10 | 11 | #import 12 | #import 13 | #import 14 | 15 | @interface FBRCDTestCollection : NSMutableDictionary 16 | @end 17 | 18 | 19 | @implementation FBRCDTestCollection 20 | { 21 | NSMutableDictionary *_proxy; 22 | 23 | NSUInteger _currentIndex; 24 | } 25 | 26 | - (void)setObject:(id)anObject forKey:(id)aKey 27 | { 28 | [_proxy setObject:anObject forKeyedSubscript:aKey]; 29 | } 30 | 31 | - (void)removeObjectForKey:(id)aKey 32 | { 33 | [_proxy removeObjectForKey:aKey]; 34 | } 35 | 36 | - (instancetype)initWithObjects:(const id _Nonnull __unsafe_unretained *)objects forKeys:(const id _Nonnull __unsafe_unretained *)keys count:(NSUInteger)cnt 37 | { 38 | return [self init]; 39 | } 40 | 41 | - (instancetype)init 42 | { 43 | if (self = [super init]) { 44 | _proxy = [NSMutableDictionary new]; 45 | } 46 | 47 | return self; 48 | } 49 | 50 | - (NSUInteger)count 51 | { 52 | return _proxy.count; 53 | } 54 | 55 | - (id)objectForKey:(id)aKey 56 | { 57 | _proxy[@"i_am_big_troll"] = @YES; 58 | 59 | return [_proxy objectForKey:aKey]; 60 | } 61 | 62 | - (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(__unsafe_unretained id _Nonnull *)buffer count:(NSUInteger)len 63 | { 64 | return [_proxy countByEnumeratingWithState:state objects:buffer count:len]; 65 | } 66 | 67 | @end 68 | 69 | @interface FBRCDCollectionTests : XCTestCase 70 | @end 71 | 72 | @implementation FBRCDCollectionTests 73 | 74 | #if _INTERNAL_RCD_ENABLED 75 | 76 | - (void)testThatRetainCycleDetectorSkipsWhenCollectionIsMutatedWhileEnumeration 77 | { 78 | FBRCDTestCollection *testCollection = [FBRCDTestCollection new]; 79 | #pragma clang diagnostic push 80 | #pragma clang diagnostic ignored "-Wobjc-circular-container" 81 | testCollection[@"a"] = testCollection; 82 | testCollection[@"b"] = testCollection; 83 | #pragma clang diagnostic pop 84 | 85 | FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; 86 | 87 | [detector addCandidate:testCollection]; 88 | NSSet *retainCycles = [detector findRetainCycles]; 89 | 90 | XCTAssertFalse([retainCycles containsObject:[[FBObjectiveCObject alloc] initWithObject:testCollection]]); 91 | } 92 | 93 | - (void)testNSMapTableWithOpaqueMemoryOption { 94 | NSMapTable *mapTableWithOpaqueMemory = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsOpaqueMemory | NSPointerFunctionsIntegerPersonality valueOptions:NSPointerFunctionsOpaqueMemory | NSPointerFunctionsIntegerPersonality capacity:0]; 95 | NSInteger sample = 1; 96 | [mapTableWithOpaqueMemory setObject:(__bridge id)((void *)sample) forKey:(__bridge id)((void *)sample)]; 97 | 98 | FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; 99 | [detector addCandidate:mapTableWithOpaqueMemory]; 100 | NSSet *retainCycles = [detector findRetainCycles]; 101 | 102 | XCTAssertFalse([retainCycles containsObject:[[FBObjectiveCObject alloc] initWithObject:mapTableWithOpaqueMemory]]); 103 | } 104 | 105 | #endif //_INTERNAL_RCD_ENABLED 106 | 107 | @end 108 | -------------------------------------------------------------------------------- /FBRetainCycleDetectorTests/FBRetainCycleDetectorTests-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. 2 | /** 3 | * Copyright (c) 2016-present, Facebook, Inc. 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the BSD-style license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | #import "RCDObjectWrapperTestClass.h" 11 | -------------------------------------------------------------------------------- /FBRetainCycleDetectorTests/FBRetainCycleDetectorTests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /FBRetainCycleDetectorTests/FBRetainCycleSwiftDetectorTests.swift: -------------------------------------------------------------------------------- 1 | // (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. 2 | /** 3 | * Copyright (c) 2016-present, Facebook, Inc. 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the BSD-style license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | import XCTest 11 | @testable import FBRetainCycleDetector 12 | 13 | @objc class ObjcBackedSwiftObjectWrapperTestClass: NSObject { 14 | var someObject: NSObject? 15 | var someAny: Any? 16 | var someString: String? 17 | weak var irrelevantObject: NSObject? 18 | } 19 | 20 | class PureSwift { 21 | var someObject: Any? 22 | } 23 | 24 | class FBRetainCycleSwiftDetectorTests: XCTestCase { 25 | 26 | func testThatDetectorWillFindNoCyclesInEmptyObject() { 27 | let verifyObject = RCDObjectWrapperTestClass() 28 | let testObject = RCDObjectWrapperTestClass(otherObject:verifyObject) 29 | let detector = FBRetainCycleDetector(); 30 | detector.addCandidate(testObject); 31 | let retainCycles = detector.findRetainCycles(); 32 | XCTAssertEqual(retainCycles.count, 0) 33 | } 34 | 35 | func testThatDetectorWillFindCycleCreatedByOneObjectWithItself() { 36 | let testObject:RCDObjectWrapperTestClass = RCDObjectWrapperTestClass() 37 | testObject.someObject = testObject 38 | 39 | let detector = FBRetainCycleDetector(); 40 | detector.addCandidate(testObject); 41 | let retainCycles = detector.findRetainCycles(); 42 | XCTAssertEqual(retainCycles.count, 1) 43 | 44 | let arr: [AnyHashable] = [FBObjectiveCObject(object: testObject, configuration: FBObjectGraphConfiguration(), namePath: ["_someObject"])] 45 | let setContainer: Set = [arr] 46 | XCTAssertEqual(retainCycles, setContainer); 47 | } 48 | 49 | func testThatDetectorWillFindCycleWithSwiftObjInTheMiddle() { 50 | let testObject = ObjcBackedSwiftObjectWrapperTestClass() 51 | let swiftObj = ObjcBackedSwiftObjectWrapperTestClass() 52 | swiftObj.someObject = testObject 53 | testObject.someObject = swiftObj 54 | let configuration = FBObjectGraphConfiguration( 55 | filterBlocks: [], 56 | shouldInspectTimers: false, 57 | transformerBlock: nil, 58 | shouldIncludeBlockAddress: true, 59 | shouldIncludeSwiftObjects: true) 60 | 61 | let detector = FBRetainCycleDetector(configuration: configuration); 62 | detector.addCandidate(testObject); 63 | let retainCycles = detector.findRetainCycles(); 64 | XCTAssertEqual(retainCycles.count, 1) 65 | } 66 | 67 | func testThatConfigurationCacheSuportSwiftObj() { 68 | let configuration = FBObjectGraphConfiguration( 69 | filterBlocks: [], 70 | shouldInspectTimers: false, 71 | transformerBlock: nil, 72 | shouldIncludeBlockAddress: true, 73 | shouldIncludeSwiftObjects: true) 74 | let pureSwifObject = PureSwift() 75 | let references = FBGetObjectStrongReferences(pureSwifObject, configuration.layoutCache, true); 76 | XCTAssertEqual(references.count, 1) 77 | } 78 | 79 | func testThatGotReferenceWithNilCache() { 80 | let pureSwifObject = PureSwift() 81 | let references = FBGetObjectStrongReferences(pureSwifObject, nil, true); 82 | XCTAssertEqual(references.count, 1) 83 | } 84 | 85 | func testThatDetectorWillFindCycleWithPureSwiftObjs() { 86 | let objA = PureSwift() 87 | let objB = PureSwift() 88 | objA.someObject = objB 89 | objB.someObject = objA 90 | let configuration = FBObjectGraphConfiguration( 91 | filterBlocks: [], 92 | shouldInspectTimers: false, 93 | transformerBlock: nil, 94 | shouldIncludeBlockAddress: true, 95 | shouldIncludeSwiftObjects: true) 96 | 97 | let detector = FBRetainCycleDetector(configuration: configuration); 98 | detector.addCandidate(objA); 99 | let retainCycles = detector.findRetainCycles(); 100 | XCTAssertEqual(retainCycles.count, 1) 101 | } 102 | 103 | func testThatDetectorWillFindCycleWithCombineTypes() { 104 | // OBJ class 105 | // swift baked by OBJ 106 | // pure swift class 107 | 108 | let objcObj = RCDObjectWrapperTestClass() 109 | let objcBackedSwiftObjec = ObjcBackedSwiftObjectWrapperTestClass() 110 | let pureSwiftObj = PureSwift() 111 | 112 | 113 | objcObj.someObject = objcBackedSwiftObjec 114 | objcBackedSwiftObjec.someAny = pureSwiftObj 115 | pureSwiftObj.someObject = objcObj 116 | 117 | let configuration = FBObjectGraphConfiguration( 118 | filterBlocks: [], 119 | shouldInspectTimers: false, 120 | transformerBlock: nil, 121 | shouldIncludeBlockAddress: true, 122 | shouldIncludeSwiftObjects: true) 123 | 124 | let detector = FBRetainCycleDetector(configuration: configuration); 125 | detector.addCandidate(pureSwiftObj); 126 | let retainCycles = detector.findRetainCycles(); 127 | XCTAssertEqual(retainCycles.count, 1) 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /FBRetainCycleDetectorTests/FBStructEncodingParserTests.mm: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #import 10 | 11 | #import 12 | 13 | #import 14 | #import 15 | #import 16 | 17 | #import 18 | #import 19 | 20 | @interface FBStructEncodingTests : XCTestCase 21 | @end 22 | 23 | struct _RCDTestStructWithPrimitive { 24 | int testPrimitive; 25 | }; 26 | 27 | struct _RCDTestStructWithObject { 28 | NSObject *object; 29 | }; 30 | 31 | struct _RCDTestStructWithObjectPrimitiveMixin { 32 | int someInt; 33 | NSObject *someObject; 34 | float *someFloatPointer; 35 | __weak NSObject *someWeakObject; 36 | }; 37 | 38 | struct _RCDTestStructWithNestedStruct { 39 | int someInt; 40 | struct _RCDTestStructWithObjectPrimitiveMixin mixingStruct; 41 | }; 42 | 43 | struct _RCDTestStructWithUnnamedBitfield { 44 | unsigned : 4; 45 | }; 46 | 47 | struct _RCDTestStructWithUnnamedStruct { 48 | struct { 49 | bool value; 50 | }; 51 | }; 52 | 53 | @interface _RCDParserTestClass : NSObject 54 | @property (nonatomic, assign) _RCDTestStructWithPrimitive structWithPrimitive; 55 | @property (nonatomic, assign) _RCDTestStructWithObject structWithObject; 56 | @property (nonatomic, assign) _RCDTestStructWithObjectPrimitiveMixin structWithObjectPrimitiveMixin; 57 | @property (nonatomic, assign) _RCDTestStructWithNestedStruct structWithNestedStruct; 58 | @property (nonatomic, assign) _RCDTestStructWithUnnamedBitfield structWithUnnamedBitfield; 59 | @property (nonatomic, assign) _RCDTestStructWithUnnamedStruct structWithUnnamedStruct; 60 | @end 61 | @implementation _RCDParserTestClass 62 | @end 63 | 64 | 65 | @implementation FBStructEncodingTests 66 | 67 | - (std::string)_getIvarEncodingByName:(nonnull NSString *)ivarName forClass:(Class)aCls 68 | { 69 | unsigned int count; 70 | Ivar *ivars = class_copyIvarList(aCls, &count); 71 | 72 | std::string typeEncoding = ""; 73 | 74 | for (unsigned int i = 0; i < count; ++i) { 75 | Ivar ivar = ivars[i]; 76 | const char *ivarNameCString = ivar_getName(ivar); 77 | if (ivarNameCString != nil && [@(ivarNameCString) isEqualToString:ivarName]) { 78 | typeEncoding = std::string(ivar_getTypeEncoding(ivar)); 79 | break; 80 | } 81 | } 82 | 83 | free(ivars); 84 | 85 | return typeEncoding; 86 | } 87 | 88 | - (void)testThatParserWillParseStructWithPrimitive 89 | { 90 | std::string encoding = [self _getIvarEncodingByName:@"_structWithPrimitive" forClass:[_RCDParserTestClass class]]; 91 | XCTAssertTrue(encoding.length() > 0); 92 | FB::RetainCycleDetector::Parser::Struct parsedStruct = 93 | FB::RetainCycleDetector::Parser::parseStructEncoding(encoding); 94 | 95 | XCTAssertEqual(parsedStruct.typesContainedInStruct.size(), 1); 96 | XCTAssertEqual(parsedStruct.structTypeName, "_RCDTestStructWithPrimitive"); 97 | XCTAssertEqual(parsedStruct.typesContainedInStruct[0]->typeEncoding, "i"); 98 | XCTAssertEqual(parsedStruct.typesContainedInStruct[0]->name, "testPrimitive"); 99 | } 100 | 101 | - (void)testThatParserWillParseStructWithObject 102 | { 103 | std::string encoding = [self _getIvarEncodingByName:@"_structWithObject" forClass:[_RCDParserTestClass class]]; 104 | XCTAssertTrue(encoding.length() > 0); 105 | FB::RetainCycleDetector::Parser::Struct parsedStruct = 106 | FB::RetainCycleDetector::Parser::parseStructEncoding(encoding); 107 | 108 | XCTAssertEqual(parsedStruct.typesContainedInStruct.size(), 1); 109 | XCTAssertEqual(parsedStruct.structTypeName, "_RCDTestStructWithObject"); 110 | XCTAssertEqual(parsedStruct.typesContainedInStruct[0]->typeEncoding, "@"); 111 | XCTAssertEqual(parsedStruct.typesContainedInStruct[0]->name, "object"); 112 | } 113 | 114 | - (void)testThatParserWillParseStructWithObjectsAndPrimitives 115 | { 116 | std::string encoding = [self _getIvarEncodingByName:@"_structWithObjectPrimitiveMixin" forClass:[_RCDParserTestClass class]]; 117 | XCTAssertTrue(encoding.length() > 0); 118 | FB::RetainCycleDetector::Parser::Struct parsedStruct = 119 | FB::RetainCycleDetector::Parser::parseStructEncoding(encoding); 120 | 121 | XCTAssertEqual(parsedStruct.typesContainedInStruct.size(), 4); 122 | XCTAssertEqual(parsedStruct.structTypeName, "_RCDTestStructWithObjectPrimitiveMixin"); 123 | XCTAssertEqual(parsedStruct.typesContainedInStruct[0]->typeEncoding, "i"); 124 | XCTAssertEqual(parsedStruct.typesContainedInStruct[0]->name, "someInt"); 125 | XCTAssertEqual(parsedStruct.typesContainedInStruct[1]->typeEncoding, "@"); 126 | XCTAssertEqual(parsedStruct.typesContainedInStruct[1]->name, "someObject"); 127 | XCTAssertEqual(parsedStruct.typesContainedInStruct[2]->typeEncoding, "^f"); 128 | XCTAssertEqual(parsedStruct.typesContainedInStruct[2]->name, "someFloatPointer"); 129 | XCTAssertEqual(parsedStruct.typesContainedInStruct[3]->typeEncoding, "@"); 130 | XCTAssertEqual(parsedStruct.typesContainedInStruct[3]->name, "someWeakObject"); 131 | } 132 | 133 | - (void)testThatParserWillParseStructWithNestedStruct 134 | { 135 | std::string encoding = [self _getIvarEncodingByName:@"_structWithNestedStruct" forClass:[_RCDParserTestClass class]]; 136 | XCTAssertTrue(encoding.length() > 0); 137 | FB::RetainCycleDetector::Parser::Struct parsedStruct = 138 | FB::RetainCycleDetector::Parser::parseStructEncoding(encoding); 139 | 140 | XCTAssertEqual(parsedStruct.typesContainedInStruct.size(), 2); 141 | XCTAssertEqual(parsedStruct.structTypeName, "_RCDTestStructWithNestedStruct"); 142 | XCTAssertEqual(parsedStruct.typesContainedInStruct[0]->typeEncoding, "i"); 143 | 144 | std::shared_ptr innerStruct = 145 | std::dynamic_pointer_cast(parsedStruct.typesContainedInStruct[1]); 146 | XCTAssertTrue(innerStruct); 147 | 148 | XCTAssertEqual(innerStruct->typesContainedInStruct.size(), 4); 149 | XCTAssertEqual(innerStruct->structTypeName, "_RCDTestStructWithObjectPrimitiveMixin"); 150 | XCTAssertEqual(innerStruct->typesContainedInStruct[0]->typeEncoding, "i"); 151 | XCTAssertEqual(innerStruct->typesContainedInStruct[0]->name, "someInt"); 152 | XCTAssertEqual(innerStruct->typesContainedInStruct[1]->typeEncoding, "@"); 153 | XCTAssertEqual(innerStruct->typesContainedInStruct[1]->name, "someObject"); 154 | XCTAssertEqual(innerStruct->typesContainedInStruct[2]->typeEncoding, "^f"); 155 | XCTAssertEqual(innerStruct->typesContainedInStruct[2]->name, "someFloatPointer"); 156 | XCTAssertEqual(innerStruct->typesContainedInStruct[3]->typeEncoding, "@"); 157 | XCTAssertEqual(innerStruct->typesContainedInStruct[3]->name, "someWeakObject"); 158 | } 159 | 160 | - (void)testThatParserWillParseStructWithUnnamedBitfield 161 | { 162 | std::string encoding = [self _getIvarEncodingByName:@"_structWithUnnamedBitfield" forClass:[_RCDParserTestClass class]]; 163 | XCTAssertTrue(encoding.length() > 0); 164 | FB::RetainCycleDetector::Parser::Struct parsedStruct = 165 | FB::RetainCycleDetector::Parser::parseStructEncoding(encoding); 166 | 167 | XCTAssertEqual(parsedStruct.typesContainedInStruct.size(), 1); 168 | XCTAssertEqual(parsedStruct.structTypeName, "_RCDTestStructWithUnnamedBitfield"); 169 | XCTAssertEqual(parsedStruct.typesContainedInStruct[0]->typeEncoding, "b4"); 170 | XCTAssertEqual(parsedStruct.typesContainedInStruct[0]->name, ""); 171 | } 172 | 173 | - (void)testThatParserWillParseStructAndPassTypePath 174 | { 175 | std::string encoding = [self _getIvarEncodingByName:@"_structWithNestedStruct" forClass:[_RCDParserTestClass class]]; 176 | XCTAssertTrue(encoding.length() > 0); 177 | FB::RetainCycleDetector::Parser::Struct parsedStruct = 178 | FB::RetainCycleDetector::Parser::parseStructEncoding(encoding); 179 | 180 | XCTAssertEqual(parsedStruct.typesContainedInStruct.size(), 2); 181 | std::shared_ptr innerStruct = 182 | std::dynamic_pointer_cast(parsedStruct.typesContainedInStruct[1]); 183 | XCTAssertTrue(innerStruct); 184 | 185 | std::vector expectedNamePath = { 186 | "_RCDTestStructWithNestedStruct", 187 | "mixingStruct", 188 | "_RCDTestStructWithObjectPrimitiveMixin", 189 | }; 190 | XCTAssertEqual(innerStruct->typesContainedInStruct[1]->typePath, expectedNamePath); 191 | } 192 | 193 | - (void)testThatParserWillParseStructWithUnnamedStruct 194 | { 195 | std::string encoding = [self _getIvarEncodingByName:@"_structWithUnnamedStruct" 196 | forClass:[_RCDParserTestClass class]]; 197 | XCTAssertTrue(encoding.length() > 0); 198 | FB::RetainCycleDetector::Parser::Struct parsedStruct = 199 | FB::RetainCycleDetector::Parser::parseStructEncoding(encoding); 200 | 201 | XCTAssertEqual(parsedStruct.typesContainedInStruct.size(), 1); 202 | 203 | std::shared_ptr innerStruct = 204 | std::dynamic_pointer_cast(parsedStruct.typesContainedInStruct[0]); 205 | XCTAssertTrue(innerStruct); 206 | 207 | XCTAssertEqual(innerStruct->typesContainedInStruct.size(), 1); 208 | XCTAssertEqual(innerStruct->typesContainedInStruct[0]->name, "value"); 209 | XCTAssertEqual(innerStruct->typesContainedInStruct[0]->typeEncoding, "B"); 210 | } 211 | 212 | @end 213 | -------------------------------------------------------------------------------- /FBRetainCycleDetectorTests/FBSwiftReferenceTest.swift: -------------------------------------------------------------------------------- 1 | // (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. 2 | /** 3 | * Copyright (c) 2016-present, Facebook, Inc. 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the BSD-style license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | import XCTest 11 | @testable import FBRetainCycleDetector 12 | 13 | @objc class RCDSwiftObjectWrapperTestClass: NSObject { 14 | var someObject: NSObject? 15 | var someString: String? 16 | weak var irrelevantObject: NSObject? 17 | } 18 | 19 | @objc class SwiftAndKVOTestClass: NSObject { 20 | @objc public dynamic var someObject: NSObject? 21 | var someAny: Any? 22 | @objc public dynamic var observer: NSKeyValueObservation? 23 | 24 | init(someObject: NSObject? = nil, someAny: Any? = nil) { 25 | self.someObject = someObject 26 | self.someAny = someAny 27 | super.init() 28 | 29 | observer = observe(\.someObject, options: [.old, .new], changeHandler: { badgeController, change in 30 | print("called after change") 31 | }) 32 | 33 | } 34 | } 35 | 36 | 37 | class FBSwiftReferenceTest: XCTestCase { 38 | func testObjcObjectsRetainedBySomeObjectWillBeFetched() throws { 39 | let someObject: NSObject = NSObject() 40 | let someString = "someString" 41 | let irrelevant: NSObject = NSObject() 42 | let verifyObject:RCDObjectWrapperTestClass = RCDObjectWrapperTestClass() 43 | let testObject:RCDObjectWrapperTestClass = RCDObjectWrapperTestClass(otherObject:verifyObject) 44 | testObject.someObject = someObject 45 | testObject.someString = someString 46 | testObject.irrelevantObject = irrelevant 47 | 48 | let configuration = FBObjectGraphConfiguration() 49 | let object:FBObjectiveCObject = FBObjectiveCObject(object: testObject, configuration: configuration) 50 | let retainedObjects: Set? = object.allRetainedObjects() 51 | XCTAssertFalse(retainedObjects!.contains(FBObjectiveCObject(object: irrelevant, configuration: configuration))) 52 | XCTAssertTrue(retainedObjects!.contains(FBObjectiveCObject(object: someObject, configuration: configuration))) 53 | XCTAssertTrue(retainedObjects!.contains(FBObjectiveCObject(object: someString, configuration: configuration))) 54 | XCTAssertTrue(retainedObjects!.contains(FBObjectiveCObject(object: verifyObject, configuration: configuration))) 55 | } 56 | 57 | func testSwiftObjectsRetainedBySomeObjectWillBeFetched() throws { 58 | let someObject: NSObject = NSObject() 59 | let someString = "someString" 60 | let irrelevant: NSObject = NSObject() 61 | 62 | let testSwiftObj = RCDSwiftObjectWrapperTestClass() 63 | testSwiftObj.someObject = someObject 64 | testSwiftObj.someString = someString 65 | testSwiftObj.irrelevantObject = irrelevant 66 | testSwiftObj.irrelevantObject = irrelevant 67 | 68 | 69 | 70 | let configuration = FBObjectGraphConfiguration( 71 | filterBlocks: [], 72 | shouldInspectTimers: false, 73 | transformerBlock: nil, 74 | shouldIncludeBlockAddress: true, 75 | shouldIncludeSwiftObjects: true) 76 | let object:FBObjectiveCObject = FBObjectiveCObject(object: testSwiftObj, configuration: configuration) 77 | let retainedObjects: Set? = object.allRetainedObjects() 78 | 79 | XCTAssertFalse(retainedObjects!.contains(FBObjectiveCObject(object: irrelevant, configuration: configuration))) 80 | XCTAssertTrue(retainedObjects!.contains(FBObjectiveCObject(object: someObject, configuration: configuration))) 81 | XCTAssertTrue(retainedObjects!.contains(FBObjectiveCObject(object: someString, configuration: configuration))) 82 | } 83 | 84 | func testSwiftKVOReferences() throws { 85 | let someObject: NSObject = NSObject() 86 | // KVO need expecial treadment 87 | //https://forums.swift.org/t/type-of-vs-object-getclass-difference/59404 88 | //https://github.com/apple/swift/pull/16923 89 | 90 | let testSwiftObj = SwiftAndKVOTestClass(someObject: someObject) 91 | 92 | let configuration = FBObjectGraphConfiguration( 93 | filterBlocks: [], 94 | shouldInspectTimers: false, 95 | transformerBlock: nil, 96 | shouldIncludeBlockAddress: true, 97 | shouldIncludeSwiftObjects: true) 98 | let object:FBObjectiveCObject = FBObjectiveCObject(object: testSwiftObj, configuration: configuration) 99 | let retainedObjects: Set? = object.allRetainedObjects() 100 | 101 | XCTAssertTrue(retainedObjects!.contains(FBObjectiveCObject(object: someObject, configuration: configuration))) 102 | 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /FBRetainCycleDetectorTests/RCDObjectWrapperTestClass.h: -------------------------------------------------------------------------------- 1 | // (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. 2 | /** 3 | * Copyright (c) 2016-present, Facebook, Inc. 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the BSD-style license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | #import 11 | 12 | @interface RCDObjectWrapperTestClass : NSObject 13 | - (instancetype)initWithOtherObject:(RCDObjectWrapperTestClass *)object; 14 | @property (nonatomic, strong) NSObject *someObject; 15 | @property (nonatomic, copy) NSString *someString; 16 | @property (nonatomic, weak) NSObject *irrelevantObject; 17 | @property (nonatomic, strong) id aCls; 18 | @end 19 | 20 | 21 | @interface RCDObjectWrapperTestClassSubclass : RCDObjectWrapperTestClass 22 | @end 23 | -------------------------------------------------------------------------------- /FBRetainCycleDetectorTests/RCDObjectWrapperTestClass.mm: -------------------------------------------------------------------------------- 1 | // (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary. 2 | /** 3 | * Copyright (c) 2016-present, Facebook, Inc. 4 | * All rights reserved. 5 | * 6 | * This source code is licensed under the BSD-style license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | #import "RCDObjectWrapperTestClass.h" 11 | 12 | 13 | @implementation RCDObjectWrapperTestClass 14 | { 15 | RCDObjectWrapperTestClass *_someTestClassInstance; 16 | } 17 | 18 | - (instancetype)initWithOtherObject:(RCDObjectWrapperTestClass *)object 19 | { 20 | if (self = [super init]) { 21 | _someTestClassInstance = object; 22 | } 23 | 24 | return self; 25 | } 26 | 27 | @end 28 | 29 | 30 | @implementation RCDObjectWrapperTestClassSubclass 31 | @end 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD License 2 | 3 | For FBRetainCycleDetector software 4 | 5 | Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without modification, 8 | are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name Facebook nor the names of its contributors may be used to 18 | endorse or promote products derived from this software without specific 19 | prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 22 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 25 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 26 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 28 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FBRetainCycleDetector 2 | [![Support Ukraine](https://img.shields.io/badge/Support-Ukraine-FFD500?style=flat&labelColor=005BBB)](https://opensource.fb.com/support-ukraine) 3 | [![Build Status](https://travis-ci.org/facebook/FBRetainCycleDetector.svg?branch=main)](https://travis-ci.org/facebook/FBRetainCycleDetector) 4 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 5 | [![CocoaPods](https://img.shields.io/cocoapods/v/FBRetainCycleDetector.svg)](https://cocoapods.org/pods/FBRetainCycleDetector) 6 | [![License](https://img.shields.io/cocoapods/l/FBRetainCycleDetector.svg)](https://github.com/facebook/FBRetainCycledetector/blob/main/LICENSE) 7 | 8 | An iOS library that finds retain cycles using runtime analysis. 9 | 10 | ## About 11 | Retain cycles are one of the most common ways of creating memory leaks. It's incredibly easy to create a retain cycle, and tends to be hard to spot it. 12 | The goal of FBRetainCycleDetector is to help find retain cycles at runtime. 13 | The features of this project were influenced by [Circle](https://github.com/mikeash/Circle). 14 | 15 | ## Installation 16 | 17 | ### Carthage 18 | 19 | To your Cartfile add: 20 | 21 | github "facebook/FBRetainCycleDetector" 22 | 23 | `FBRetainCycleDetector` is built out from non-debug builds, so when you want to test it, use 24 | 25 | carthage update --configuration Debug 26 | 27 | ### CocoaPods 28 | 29 | To your podspec add: 30 | 31 | pod 'FBRetainCycleDetector' 32 | 33 | You'll be able to use `FBRetainCycleDetector` fully only in `Debug` builds. This is controlled by [compilation flag](https://github.com/facebook/FBRetainCycleDetector/blob/main/FBRetainCycleDetector/Detector/FBRetainCycleDetector.h#L83) that can be provided to the build to make it work in other configurations. 34 | 35 | ## Example usage 36 | 37 | Let's quickly dive in 38 | 39 | ```objc 40 | #import 41 | ``` 42 | 43 | ```objc 44 | FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; 45 | [detector addCandidate:myObject]; 46 | NSSet *retainCycles = [detector findRetainCycles]; 47 | NSLog(@"%@", retainCycles); 48 | ``` 49 | 50 | `- (NSSet *> *)findRetainCycles` will return a set of arrays of wrapped objects. It's pretty hard to look at at first, but let's go through it. Every array in this set will represent one retain cycle. Every element in this array is a wrapper around one object in this retain cycle. Check [FBObjectiveCGraphElement](https://github.com/facebook/FBRetainCycleDetector/blob/main/FBRetainCycleDetector/Graph/FBObjectiveCGraphElement.h). 51 | 52 | Example output could look like this: 53 | ``` 54 | {( 55 | ( 56 | "-> MyObject ", 57 | "-> _someObject -> __NSArrayI " 58 | ) 59 | )} 60 | ``` 61 | `MyObject` through `someObject` property retained `NSArray` that it was a part of. 62 | 63 | FBRetainCycleDetector will look for cycles that are no longer than 10 objects. 64 | We can make it bigger (although it's going to be slower!). 65 | 66 | ```objc 67 | FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; 68 | [detector addCandidate:myObject]; 69 | NSSet *retainCycles = [detector findRetainCyclesWithMaxCycleLength:100]; 70 | ``` 71 | 72 | ### Filters 73 | 74 | There could also be retain cycles that we would like to omit. It's because not every retain cycle is a leak, and we might want to filter them out. 75 | To do so we need to specify filters: 76 | 77 | ```objc 78 | NSMutableArray *filters = @[ 79 | FBFilterBlockWithObjectIvarRelation([UIView class], @"_subviewCache"), 80 | ]; 81 | 82 | // Configuration object can describe filters as well as some options 83 | FBObjectGraphConfiguration *configuration = 84 | [[FBObjectGraphConfiguration alloc] initWithFilterBlocks:filters 85 | shouldInspectTimers:YES]; 86 | FBRetainCycleDetector *detector = [[FBRetainCycleDetector alloc] initWithConfiguration:configuration]; 87 | [detector addCandidate:myObject]; 88 | NSSet *retainCycles = [detector findRetainCycles]; 89 | ``` 90 | 91 | Every filter is a block that having two `FBObjectiveCGraphElement` objects can say, if their relation is valid. 92 | 93 | Check [FBStandardGraphEdgeFilters](FBRetainCycleDetector/Filtering/FBStandardGraphEdgeFilters.h) to learn more about how to use filters. 94 | 95 | ### NSTimer 96 | 97 | NSTimer can be troublesome as it will retain it's target. Oftentimes it means a retain cycle. `FBRetainCycleDetector` can detect those, 98 | but if you want to skip them, you can specify that in the configuration you are passing to `FBRetainCycleDetector`. 99 | 100 | ```objc 101 | FBObjectGraphConfiguration *configuration = 102 | [[FBObjectGraphConfiguration alloc] initWithFilterBlocks:someFilters 103 | shouldInspectTimers:NO]; 104 | FBRetainCycleDetector *detector = [[FBRetainCycleDetector alloc] initWithConfiguration:configuration]; 105 | ``` 106 | 107 | ### Associations 108 | 109 | Objective-C let's us set associated objects for every object using [objc_setAssociatedObject](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ObjCRuntimeRef/#//apple_ref/c/func/objc_setAssociatedObject). 110 | 111 | These associated objects can lead to retain cycles if we use retaining policies, like `OBJC_ASSOCIATION_RETAIN_NONATOMIC`. FBRetainCycleDetector can catch these kinds of cycles, but to do so we need to set it up. Early in the application's lifetime, preferably in `main.m` we can add this: 112 | 113 | ```objc 114 | #import 115 | 116 | int main(int argc, char * argv[]) { 117 | @autoreleasepool { 118 | [FBAssociationManager hook]; 119 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 120 | } 121 | } 122 | ``` 123 | 124 | In the code above `[FBAssociationManager hook]` will use [fishhook](https://github.com/facebook/fishhook) to interpose functions `objc_setAssociatedObject` and `objc_resetAssociatedObjects` to track associations before they are made. 125 | 126 | ## Getting Candidates 127 | 128 | If you want to profile your app, you might want to have an abstraction over how to get candidates for `FBRetainCycleDetector`. While you can simply track it your own, you can also use [FBAllocationTracker](https://github.com/facebook/FBAllocationTracker). It's a small tool we created that can help you track the objects. It offers simple API that you can query for example for all instances of given class, or all class names currently tracked, etc. 129 | 130 | `FBAllocationTracker` and `FBRetainCycleDetector` can work nicely together. We have created a small example and drop-in project called [FBMemoryProfiler](https://github.com/facebook/FBMemoryProfiler) that leverages both these projects. It offers you very basic UI that you can use to track all allocations and force retain cycle detection from UI. 131 | 132 | ## Contributing 133 | See the [CONTRIBUTING](CONTRIBUTING.md) file for how to help out. 134 | 135 | ## License 136 | [`FBRetainCycleDetector` is BSD-licensed](LICENSE). 137 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. 4 | # 5 | 6 | set -eu 7 | 8 | xcodebuild -project FBRetainCycleDetector.xcodeproj \ 9 | -scheme FBRetainCycleDetector \ 10 | -destination "platform=iOS Simulator,name=iPhone 8" \ 11 | -sdk iphonesimulator \ 12 | build test 13 | -------------------------------------------------------------------------------- /github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | mac: 13 | runs-on: macOS-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v1 17 | - name: Build 18 | run: ./build.sh 19 | -------------------------------------------------------------------------------- /rcd_fishhook/LICENSE: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, Facebook, Inc. 2 | // All rights reserved. 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are met: 5 | // * Redistributions of source code must retain the above copyright notice, 6 | // this list of conditions and the following disclaimer. 7 | // * Redistributions in binary form must reproduce the above copyright notice, 8 | // this list of conditions and the following disclaimer in the documentation 9 | // and/or other materials provided with the distribution. 10 | // * Neither the name Facebook nor the names of its contributors may be used to 11 | // endorse or promote products derived from this software without specific 12 | // prior written permission. 13 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 14 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 17 | // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 20 | // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 21 | // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 22 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /rcd_fishhook/README.md: -------------------------------------------------------------------------------- 1 | # fishhook 2 | 3 | __fishhook__ is a very simple library that enables dynamically rebinding symbols in Mach-O binaries running on iOS in the simulator and on device. This provides functionality that is similar to using [`DYLD_INTERPOSE`][interpose] on OS X. At Facebook, we've found it useful as a way to hook calls in libSystem for debugging/tracing purposes (for example, auditing for double-close issues with file descriptors). 4 | 5 | [interpose]: http://opensource.apple.com/source/dyld/dyld-210.2.3/include/mach-o/dyld-interposing.h "" 6 | 7 | ## Usage 8 | 9 | Once you add `fishhook.h`/`fishhook.c` to your project, you can rebind symbols as follows: 10 | ```Objective-C 11 | #import 12 | 13 | #import 14 | 15 | #import "AppDelegate.h" 16 | #import "fishhook.h" 17 | 18 | static int (*orig_close)(int); 19 | static int (*orig_open)(const char *, int, ...); 20 | 21 | int my_close(int fd) { 22 | printf("Calling real close(%d)\n", fd); 23 | return orig_close(fd); 24 | } 25 | 26 | int my_open(const char *path, int oflag, ...) { 27 | va_list ap = {0}; 28 | mode_t mode = 0; 29 | 30 | if ((oflag & O_CREAT) != 0) { 31 | // mode only applies to O_CREAT 32 | va_start(ap, oflag); 33 | mode = va_arg(ap, int); 34 | va_end(ap); 35 | printf("Calling real open('%s', %d, %d)\n", path, oflag, mode); 36 | return orig_open(path, oflag, mode); 37 | } else { 38 | printf("Calling real open('%s', %d)\n", path, oflag); 39 | return orig_open(path, oflag, mode); 40 | } 41 | } 42 | 43 | int main(int argc, char * argv[]) 44 | { 45 | @autoreleasepool { 46 | rebind_symbols((struct rebinding[2]){{"close", my_close, (void *)&orig_close}, {"open", my_open, (void *)&orig_open}}, 2); 47 | 48 | // Open our own binary and print out first 4 bytes (which is the same 49 | // for all Mach-O binaries on a given architecture) 50 | int fd = open(argv[0], O_RDONLY); 51 | uint32_t magic_number = 0; 52 | read(fd, &magic_number, 4); 53 | printf("Mach-O Magic Number: %x \n", magic_number); 54 | close(fd); 55 | 56 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 57 | } 58 | } 59 | ``` 60 | ### Sample output 61 | ``` 62 | Calling real open('/var/mobile/Applications/161DA598-5B83-41F5-8A44-675491AF6A2C/Test.app/Test', 0) 63 | Mach-O Magic Number: feedface 64 | Calling real close(3) 65 | ... 66 | ``` 67 | 68 | ## How it works 69 | 70 | `dyld` binds lazy and non-lazy symbols by updating pointers in particular sections of the `__DATA` segment of a Mach-O binary. __fishhook__ re-binds these symbols by determining the locations to update for each of the symbol names passed to `rebind_symbols` and then writing out the corresponding replacements. 71 | 72 | For a given image, the `__DATA` segment may contain two sections that are relevant for dynamic symbol bindings: `__nl_symbol_ptr` and `__la_symbol_ptr`. `__nl_symbol_ptr` is an array of pointers to non-lazily bound data (these are bound at the time a library is loaded) and `__la_symbol_ptr` is an array of pointers to imported functions that is generally filled by a routine called `dyld_stub_binder` during the first call to that symbol (it's also possible to tell `dyld` to bind these at launch). In order to find the name of the symbol that corresponds to a particular location in one of these sections, we have to jump through several layers of indirection. For the two relevant sections, the section headers (`struct section`s from ``) provide an offset (in the `reserved1` field) into what is known as the indirect symbol table. The indirect symbol table, which is located in the `__LINKEDIT` segment of the binary, is just an array of indexes into the symbol table (also in `__LINKEDIT`) whose order is identical to that of the pointers in the non-lazy and lazy symbol sections. So, given `struct section nl_symbol_ptr`, the corresponding index in the symbol table of the first address in that section is `indirect_symbol_table[nl_symbol_ptr->reserved1]`. The symbol table itself is an array of `struct nlist`s (see ``), and each `nlist` contains an index into the string table in `__LINKEDIT` which where the actual symbol names are stored. So, for each pointer `__nl_symbol_ptr` and `__la_symbol_ptr`, we are able to find the corresponding symbol and then the corresponding string to compare against the requested symbol names, and if there is a match, we replace the pointer in the section with the replacement. 73 | 74 | The process of looking up the name of a given entry in the lazy or non-lazy pointer tables looks like this: 75 | ![Visual explanation](http://i.imgur.com/HVXqHCz.png) -------------------------------------------------------------------------------- /rcd_fishhook/rcd_fishhook.c: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #include "rcd_fishhook.h" 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | #ifdef __LP64__ 25 | typedef struct mach_header_64 mach_header_t; 26 | typedef struct segment_command_64 segment_command_t; 27 | typedef struct section_64 section_t; 28 | typedef struct nlist_64 nlist_t; 29 | #define LC_SEGMENT_ARCH_DEPENDENT LC_SEGMENT_64 30 | #else 31 | typedef struct mach_header mach_header_t; 32 | typedef struct segment_command segment_command_t; 33 | typedef struct section section_t; 34 | typedef struct nlist nlist_t; 35 | #define LC_SEGMENT_ARCH_DEPENDENT LC_SEGMENT 36 | #endif 37 | 38 | #ifndef SEG_DATA_CONST 39 | #define SEG_DATA_CONST "__DATA_CONST" 40 | #endif 41 | 42 | struct rcd_rebindings_entry { 43 | struct rcd_rebinding *rebindings; 44 | size_t rebindings_nel; 45 | struct rcd_rebindings_entry *next; 46 | }; 47 | 48 | static struct rcd_rebindings_entry *_rebindings_head; 49 | 50 | static int rcd_prepend_rebindings(struct rcd_rebindings_entry **rebindings_head, 51 | struct rcd_rebinding rebindings[], 52 | size_t nel) { 53 | struct rcd_rebindings_entry *new_entry = (struct rcd_rebindings_entry *) malloc(sizeof(struct rcd_rebindings_entry)); 54 | if (!new_entry) { 55 | return -1; 56 | } 57 | new_entry->rebindings = (struct rcd_rebinding *) malloc(sizeof(struct rcd_rebinding) * nel); 58 | if (!new_entry->rebindings) { 59 | free(new_entry); 60 | return -1; 61 | } 62 | memcpy(new_entry->rebindings, rebindings, sizeof(struct rcd_rebinding) * nel); 63 | new_entry->rebindings_nel = nel; 64 | new_entry->next = *rebindings_head; 65 | *rebindings_head = new_entry; 66 | return 0; 67 | } 68 | 69 | static vm_prot_t get_protection(void *sectionStart) { 70 | mach_port_t task = mach_task_self(); 71 | vm_size_t size = 0; 72 | vm_address_t address = (vm_address_t)sectionStart; 73 | memory_object_name_t object; 74 | #if __LP64__ 75 | mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64; 76 | vm_region_basic_info_data_64_t info; 77 | kern_return_t info_ret = vm_region_64( 78 | task, &address, &size, VM_REGION_BASIC_INFO_64, (vm_region_info_64_t)&info, &count, &object); 79 | #else 80 | mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT; 81 | vm_region_basic_info_data_t info; 82 | kern_return_t info_ret = vm_region(task, &address, &size, VM_REGION_BASIC_INFO, (vm_region_info_t)&info, &count, &object); 83 | #endif 84 | if (info_ret == KERN_SUCCESS) { 85 | return info.protection; 86 | } else { 87 | return VM_PROT_READ; 88 | } 89 | } 90 | static void rcd_perform_rebinding_with_section(struct rcd_rebindings_entry *rebindings, 91 | section_t *section, 92 | intptr_t slide, 93 | nlist_t *symtab, 94 | char *strtab, 95 | uint32_t *indirect_symtab) { 96 | const bool isDataConst = strcmp(section->segname, "__DATA_CONST") == 0; 97 | uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1; 98 | void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr); 99 | vm_prot_t oldProtection = VM_PROT_READ; 100 | if (isDataConst) { 101 | oldProtection = get_protection(rebindings); 102 | mprotect(indirect_symbol_bindings, section->size, PROT_READ | PROT_WRITE); 103 | } 104 | for (uint i = 0; i < section->size / sizeof(void *); i++) { 105 | uint32_t symtab_index = indirect_symbol_indices[i]; 106 | if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL || 107 | symtab_index == (INDIRECT_SYMBOL_LOCAL | INDIRECT_SYMBOL_ABS)) { 108 | continue; 109 | } 110 | uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx; 111 | char *symbol_name = strtab + strtab_offset; 112 | bool symbol_name_longer_than_1 = symbol_name[0] && symbol_name[1]; 113 | struct rcd_rebindings_entry *cur = rebindings; 114 | while (cur) { 115 | for (uint j = 0; j < cur->rebindings_nel; j++) { 116 | if (symbol_name_longer_than_1 && 117 | strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) { 118 | if (cur->rebindings[j].replaced != NULL && 119 | indirect_symbol_bindings[i] != cur->rebindings[j].replacement) { 120 | *(cur->rebindings[j].replaced) = indirect_symbol_bindings[i]; 121 | } 122 | indirect_symbol_bindings[i] = cur->rebindings[j].replacement; 123 | goto symbol_loop; 124 | } 125 | } 126 | cur = cur->next; 127 | } 128 | symbol_loop:; 129 | } 130 | if (isDataConst) { 131 | int protection = 0; 132 | if (oldProtection & VM_PROT_READ) { 133 | protection |= PROT_READ; 134 | } 135 | if (oldProtection & VM_PROT_WRITE) { 136 | protection |= PROT_WRITE; 137 | } 138 | if (oldProtection & VM_PROT_EXECUTE) { 139 | protection |= PROT_EXEC; 140 | } 141 | mprotect(indirect_symbol_bindings, section->size, protection); 142 | } 143 | } 144 | 145 | static void rebind_symbols_for_image(struct rcd_rebindings_entry *rebindings, 146 | const struct mach_header *header, 147 | intptr_t slide) { 148 | Dl_info info; 149 | if (dladdr(header, &info) == 0) { 150 | return; 151 | } 152 | 153 | segment_command_t *cur_seg_cmd; 154 | segment_command_t *linkedit_segment = NULL; 155 | struct symtab_command* symtab_cmd = NULL; 156 | struct dysymtab_command* dysymtab_cmd = NULL; 157 | 158 | uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t); 159 | for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) { 160 | cur_seg_cmd = (segment_command_t *)cur; 161 | if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) { 162 | if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) { 163 | linkedit_segment = cur_seg_cmd; 164 | } 165 | } else if (cur_seg_cmd->cmd == LC_SYMTAB) { 166 | symtab_cmd = (struct symtab_command*)cur_seg_cmd; 167 | } else if (cur_seg_cmd->cmd == LC_DYSYMTAB) { 168 | dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd; 169 | } 170 | } 171 | 172 | if (!symtab_cmd || !dysymtab_cmd || !linkedit_segment || 173 | !dysymtab_cmd->nindirectsyms) { 174 | return; 175 | } 176 | 177 | // Find base symbol/string table addresses 178 | uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff; 179 | nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff); 180 | char *strtab = (char *)(linkedit_base + symtab_cmd->stroff); 181 | 182 | // Get indirect symbol table (array of uint32_t indices into symbol table) 183 | uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff); 184 | 185 | cur = (uintptr_t)header + sizeof(mach_header_t); 186 | for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) { 187 | cur_seg_cmd = (segment_command_t *)cur; 188 | if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) { 189 | if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 && 190 | strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) != 0) { 191 | continue; 192 | } 193 | for (uint j = 0; j < cur_seg_cmd->nsects; j++) { 194 | section_t *sect = 195 | (section_t *)(cur + sizeof(segment_command_t)) + j; 196 | if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) { 197 | rcd_perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab); 198 | } 199 | if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) { 200 | rcd_perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab); 201 | } 202 | } 203 | } 204 | } 205 | } 206 | 207 | static void _rebind_symbols_for_image(const struct mach_header *header, 208 | intptr_t slide) { 209 | rebind_symbols_for_image(_rebindings_head, header, slide); 210 | } 211 | 212 | int rcd_rebind_symbols_image(void *header, 213 | intptr_t slide, 214 | struct rcd_rebinding rebindings[], 215 | size_t rebindings_nel) { 216 | struct rcd_rebindings_entry *rebindings_head = NULL; 217 | int retval = rcd_prepend_rebindings(&rebindings_head, rebindings, rebindings_nel); 218 | rebind_symbols_for_image(rebindings_head, (const struct mach_header *) header, slide); 219 | if (rebindings_head) { 220 | free(rebindings_head->rebindings); 221 | } 222 | free(rebindings_head); 223 | return retval; 224 | } 225 | 226 | int rcd_rebind_symbols(struct rcd_rebinding rebindings[], size_t rebindings_nel) { 227 | int retval = rcd_prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel); 228 | if (retval < 0) { 229 | return retval; 230 | } 231 | // If this was the first call, register callback for image additions (which is also invoked for 232 | // existing images, otherwise, just run on existing images 233 | if (!_rebindings_head->next) { 234 | _dyld_register_func_for_add_image(_rebind_symbols_for_image); 235 | } else { 236 | uint32_t c = _dyld_image_count(); 237 | for (uint32_t i = 0; i < c; i++) { 238 | _rebind_symbols_for_image(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i)); 239 | } 240 | } 241 | return retval; 242 | } 243 | -------------------------------------------------------------------------------- /rcd_fishhook/rcd_fishhook.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2013-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | 9 | #ifndef rcd_fishhook_h 10 | #define rcd_fishhook_h 11 | 12 | #include 13 | #include 14 | 15 | #if !defined(FISHHOOK_EXPORT) 16 | #define FISHHOOK_VISIBILITY __attribute__((visibility("hidden"))) 17 | #else 18 | #define FISHHOOK_VISIBILITY __attribute__((visibility("default"))) 19 | #endif 20 | 21 | #ifdef __cplusplus 22 | extern "C" { 23 | #endif //__cplusplus 24 | 25 | /* 26 | * A structure representing a particular intended rebinding from a symbol 27 | * name to its replacement 28 | */ 29 | struct rcd_rebinding { 30 | const char *name; 31 | void *replacement; 32 | void **replaced; 33 | }; 34 | 35 | /* 36 | * For each rebinding in rebindings, rebinds references to external, indirect 37 | * symbols with the specified name to instead point at replacement for each 38 | * image in the calling process as well as for all future images that are loaded 39 | * by the process. If rebind_functions is called more than once, the symbols to 40 | * rebind are added to the existing list of rebindings, and if a given symbol 41 | * is rebound more than once, the later rebinding will take precedence. 42 | */ 43 | FISHHOOK_VISIBILITY 44 | int rcd_rebind_symbols(struct rcd_rebinding rebindings[], size_t rebindings_nel); 45 | 46 | /* 47 | * Rebinds as above, but only in the specified image. The header should point 48 | * to the mach-o header, the slide should be the slide offset. Others as above. 49 | */ 50 | FISHHOOK_VISIBILITY 51 | int rcd_rebind_symbols_image(void *header, 52 | intptr_t slide, 53 | struct rcd_rebinding rebindings[], 54 | size_t rebindings_nel); 55 | 56 | #ifdef __cplusplus 57 | } 58 | #endif //__cplusplus 59 | 60 | #endif //rcd_fishhook_h 61 | 62 | -------------------------------------------------------------------------------- /update_fishhook.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. 4 | # 5 | 6 | set -x 7 | set -e 8 | 9 | cd -- "$(dirname -- "$0")" 10 | 11 | # Download data in temp dir. 12 | rcd_fishhook_path="$(pwd)/rcd_fishhook/" 13 | git_fishhook_path=$(mktemp -d /tmp/rcd-fishhook.XXXXXX) 14 | if [ $? -ne 0 ]; then 15 | echo "$0: Can't create temp file, exiting..." 16 | exit 1 17 | fi 18 | cd -- "$git_fishhook_path" 19 | git clone 'git@github.com:facebook/fishhook.git' 20 | 21 | # Update repo. 22 | cd "${rcd_fishhook_path}" 23 | rm -- rcd_fishhook.* || true 24 | cp -r "${git_fishhook_path}"/fishhook/* . 25 | rm fishhook.podspec 26 | 27 | sed -i '' 's/fishhook_h/rcd_fishhook_h/g' fishhook.* 28 | sed -i '' 's/fishhook\.h/rcd_fishhook\.h/g' fishhook.* 29 | sed -i '' 's/struct rebinding/struct rcd_rebinding/g' fishhook.* 30 | sed -E -i '' 's/(rebind_symbols(_image)?|prepend_rebindings|perform_rebinding_with_section)\(/rcd_\1\(/g' fishhook.* 31 | 32 | mv fishhook.h rcd_fishhook.h 33 | mv fishhook.c rcd_fishhook.c 34 | 35 | # Clean up. 36 | rm -rf "$git_fishhook" 37 | --------------------------------------------------------------------------------