-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathEBObservation.m
297 lines (243 loc) · 13.1 KB
/
EBObservation.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
#if __has_feature(objc_arc)
#error ARC required to be disabled (-fno-objc-arc)
#endif
#import "EBObservation.h"
#import <objc/runtime.h>
#import <objc/message.h>
#import <libkern/OSAtomic.h>
#import "EBAssert.h"
#import "EBUtilities.h"
static SEL kObserveeSwizzledDeallocSelector = nil;
static EBMakeUniquePointerConst(kObservationsMapKey);
static NSLock *gMasterLock = nil;
@implementation EBObservation
{
@public
int32_t _invalidated;
NSObject *_observee;
NSString *_keyPath;
NSKeyValueObservingOptions _options;
void (^_handlerBlock)(EBObservation *observation, NSDictionary *change);
}
#pragma mark - Creation -
+ (void)initialize
{
static dispatch_once_t initToken = 0;
dispatch_once(&initToken,
^{
kObserveeSwizzledDeallocSelector = @selector(co_echobravo_foundation_observation_observeeSwizzledDealloc);
gMasterLock = [[NSLock alloc] init];
});
}
+ (EBObservation *)observeObject: (NSObject *)observee keyPath: (NSString *)keyPath
options: (NSKeyValueObservingOptions)options handlerBlock: (void (^)(EBObservation *observation, NSDictionary *change))handlerBlock
{
return [[[EBObservation alloc] initWithObservee: observee keyPath: keyPath options: options handlerBlock: handlerBlock] autorelease];
}
- (id)initWithObservee: (NSObject *)observee keyPath: (NSString *)keyPath options: (NSKeyValueObservingOptions)options
handlerBlock: (void (^)(EBObservation *observation, NSDictionary *change))handlerBlock
{
NSParameterAssert(observee);
NSParameterAssert(keyPath && [keyPath length]);
NSParameterAssert(handlerBlock);
if (!(self = [super init]))
return nil;
_invalidated = NO;
_observee = observee; /* Weak reference to observee! */
_keyPath = [keyPath retain];
_handlerBlock = [handlerBlock copy];
_options = options;
NSMutableDictionary *observationsMap = lockWithObserveeAndGetObservationsMap(_observee, YES);
/* Grave error if we didn't acquire the lock */
EBAssertOrBail(observationsMap);
CFMutableSetRef observations = (CFMutableSetRef)[observationsMap objectForKey: _keyPath];
/* Grave error state if this assertion fails */
EBAssertOrBail(!observations || CFSetGetCount(observations) > 0);
if (!observations)
{
observations = (CFMutableSetRef)[(id)CFSetCreateMutable(nil, 0, nil) autorelease];
[observationsMap setObject: (id)observations forKey: _keyPath];
}
CFSetAddValue(observations, self);
swizzleDeallocForObserveeClass([_observee class]);
/* Mask the 'Initial' KVO option to prevent us from calling-out while the lock is held */
[_observee addObserver: (id)[EBObservation class] forKeyPath: _keyPath options: (_options & ~NSKeyValueObservingOptionInitial) context: self];
unlockWithObservee(_observee);
/* Emulate the 'Initial' KVO option now that we've relinquished the lock and it's safe to call out. */
if (_options & NSKeyValueObservingOptionInitial)
{
NSMutableDictionary *change = [NSMutableDictionary dictionaryWithObject: [NSNumber numberWithUnsignedInteger: NSKeyValueChangeSetting] forKey: NSKeyValueChangeKindKey];
if (_options & NSKeyValueObservingOptionNew)
[change setObject: EBValueOrFallback([_observee valueForKeyPath: _keyPath], [NSNull null]) forKey: NSKeyValueChangeNewKey];
_handlerBlock(self, change);
}
return self;
}
- (void)invalidate
{
[self invalidateWithObservationsMap: nil];
}
- (void)invalidateWithObservationsMap: (NSMutableDictionary *)observationsMap
{
/* We use the observationsMap as a flag as well as for its content; if observationsMap == nil, then we need to acquire the
lock for our observee. If observationsMap != nil, then we don't acquire the lock. See handleObserveeDeallocation()
for the rationale. */
EBConfirmOrPerform(OSAtomicCompareAndSwap32(NO, YES, &_invalidated), return);
/* Perform in reverse of -init! */
/* If we weren't given an observations map, we acquire the lock so that we can get it */
BOOL acquireLock = !observationsMap;
if (acquireLock)
observationsMap = lockWithObserveeAndGetObservationsMap(_observee, NO);
/* Grave error if we don't have an observations map at this point, or if the observations map is empty */
EBAssertOrBail(observationsMap && [observationsMap count]);
[_observee removeObserver: (id)[EBObservation class] forKeyPath: _keyPath context: self];
CFMutableSetRef observations = (CFMutableSetRef)[observationsMap objectForKey: _keyPath];
/* Grave error if this assertion fails */
EBAssertOrBail(observations && CFSetGetCount(observations) > 0);
CFSetRemoveValue(observations, self);
if (!CFSetGetCount(observations))
[observationsMap removeObjectForKey: _keyPath];
if (acquireLock)
unlockWithObservee(_observee);
_options = 0;
[_handlerBlock release],
_handlerBlock = nil;
[_keyPath release],
_keyPath = nil;
_observee = nil;
}
- (void)dealloc
{
[self invalidate];
[super dealloc];
}
#pragma mark - Private Methods -
static NSMutableDictionary *lockWithObserveeAndGetObservationsMap(NSObject *observee, BOOL allowCreateObservationsMap)
{
NSCParameterAssert(observee);
/* Retain the observee for the duration that we're locked with it */
[observee retain];
[gMasterLock lock];
NSMutableDictionary *observationsMap = objc_getAssociatedObject(observee, kObservationsMapKey);
if (!observationsMap && allowCreateObservationsMap)
{
observationsMap = [[[NSMutableDictionary alloc] init] autorelease];
objc_setAssociatedObject(observee, kObservationsMapKey, observationsMap, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
/* Grave error if we don't have an observations map and we were supposed to create one */
EBAssertOrBail(observationsMap || !allowCreateObservationsMap);
/* Cleanup if we're not returning that we acquired the lock (result == nil), since unlock() won't be called. */
if (!observationsMap)
{
[gMasterLock unlock];
[observee release];
}
return observationsMap;
}
static void unlockWithObservee(NSObject *observee)
{
NSCParameterAssert(observee);
/* Perform in reverse of lock()! */
NSMutableDictionary *observationsMap = objc_getAssociatedObject(observee, kObservationsMapKey);
/* Grave error if we don't have an observations map, since it was created in lock() and no one else should have removed it but us. */
EBAssertOrBail(observationsMap);
/* Remove the observations map if it's empty */
if (![observationsMap count])
objc_setAssociatedObject(observee, kObservationsMapKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
[gMasterLock unlock];
[observee release];
}
static void handleObserveeDeallocation(NSObject *observee)
{
NSCParameterAssert(observee);
NSMutableDictionary *observationsMap = lockWithObserveeAndGetObservationsMap(observee, NO);
if (observationsMap)
{
/* We have active observations for observee since observationsMap != nil, so iterate over every observations set in the map,
and invalidate every observation. */
/* ### We have to invalidate the observations *before* we relinquish the lock, because an observation could be in the process
of deallocation, and relinquishing the lock would allow the EBObservation to call [super dealloc], and the memory would be
freed. While we hold the lock, we're guaranteed that the EBObservation hasn't acquired the lock in -invalidate, and
therefore we can safely message it until we relinquish the lock. */
/* ### We're forced to use the CFSet APIs to access to the EBObservations, because we have to be sure that they're not going
to be messaged after we relinquish the lock e.g. by being placed in an autorelease pool. */
for (id currentObservations in [[observationsMap allValues] objectEnumerator])
{
CFIndex currentObservationsCount = CFSetGetCount((CFSetRef)currentObservations);
/* Grave error if our observations set is empty! */
EBAssertOrBail(currentObservationsCount > 0);
EBObservation **observations = malloc(sizeof(*observations) * currentObservationsCount);
EBAssertOrRecover(observations, continue);
CFSetGetValues((CFSetRef)currentObservations, (const void **)observations);
for (NSUInteger currentObservationIndex = 0; currentObservationIndex < currentObservationsCount; currentObservationIndex++)
[observations[currentObservationIndex] invalidateWithObservationsMap: observationsMap];
free(observations),
observations = nil;
}
unlockWithObservee(observee);
}
}
static void observeeSwizzledDealloc(NSObject *observee, SEL _cmd)
{
NSCParameterAssert(observee);
handleObserveeDeallocation(observee);
objc_msgSend(observee, kObserveeSwizzledDeallocSelector);
}
static void swizzleDeallocForObserveeClass(Class observeeClass)
{
NSCParameterAssert(observeeClass);
const char *deallocTypeEncoding = method_getTypeEncoding(class_getInstanceMethod([NSObject class], @selector(dealloc)));
EBAssertOrRecover(deallocTypeEncoding, return);
/* Create our swizzled dealloc method on observeeClass. If class_addMethod() fails, it means we already performed our swizzling on observeeClass, so we'll gracefully return. */
BOOL addMethodResult = class_addMethod(observeeClass, kObserveeSwizzledDeallocSelector, (IMP)observeeSwizzledDealloc, deallocTypeEncoding);
EBConfirmOrPerform(addMethodResult, return);
Method swizzledDeallocMethod = class_getInstanceMethod(observeeClass, kObserveeSwizzledDeallocSelector);
EBAssertOrRecover(swizzledDeallocMethod, return);
/* Add a -dealloc method at the level of observeeClass, which will simply call super's implementation of dealloc. We want this method to exist at
the level of observeeClass so that we can swizzle it at that level and not a superclass' level, in order to avoid unnecessary overhead (e.g.,
if a class inherits from NSObject and doesn't implement a -dealloc method, we would otherwise be swizzling NSObject's -dealloc, and our code
would be executed any time any object is deallocated). */
Class observeeSuperclass = [observeeClass superclass];
/* Sanity-check: avoid swizzling the root class because we be crazy if we're overriding NSObject's -dealloc */
EBAssertOrBailWithNote(observeeSuperclass, @"Refraining from swizzling -dealloc of root class");
id deallocTrampolineBlock =
[[^(NSObject *observee)
{
struct objc_super superInfo =
{
.receiver = observee,
.super_class = observeeSuperclass
};
objc_msgSendSuper(&superInfo, @selector(dealloc));
} copy] autorelease];
IMP deallocTrampolineImp = imp_implementationWithBlock(deallocTrampolineBlock);
addMethodResult = class_addMethod(observeeClass, @selector(dealloc), deallocTrampolineImp, deallocTypeEncoding);
/* If we successfully added the method to observeeClass, retain the block so its IMP remains valid. */
if (addMethodResult)
[deallocTrampolineBlock retain];
Method originalDeallocMethod = class_getInstanceMethod(observeeClass, @selector(dealloc));
EBAssertOrRecover(originalDeallocMethod, return);
/* Swizzle the method implementations -- invoking 'dealloc' on instances of observeeClass will actually invoke our observeeSwizzledDealloc(),
and calling kObserveeSwizzledDeallocSelector on instances of observeeClass will actually invoke the original dealloc method. */
method_exchangeImplementations(originalDeallocMethod, swizzledDeallocMethod);
}
+ (void)observeValueForKeyPath: (NSString *)keyPath ofObject: (NSObject *)observee change: (NSDictionary *)change context: (void *)context
{
NSParameterAssert(keyPath && [keyPath length]);
NSParameterAssert(observee);
void (^handlerBlock)(EBObservation *observation, NSDictionary *change) = nil;
NSMutableDictionary *observationsMap = lockWithObserveeAndGetObservationsMap(observee, NO);
if (observationsMap)
{
/* Check if 'context' (a possibly-deallocated EBObservation) exists in the observations set for the given key path.
If so, it's still live and we can safely access its _handlerBlock while we hold the lock, since we know that it
hasn't started its invalidation yet (which happens when it's deallocated.) */
CFSetRef observations = (CFSetRef)[observationsMap objectForKey: keyPath];
if (observations && CFSetContainsValue(observations, context))
handlerBlock = [[((EBObservation *)context)->_handlerBlock retain] autorelease];
unlockWithObservee(observee);
}
if (handlerBlock)
handlerBlock(context, change);
}
@end